☰
STM32F103通过I2C读写AT24C02 EEPROM完整实战指南
2026/10/29 9:37:25 网站建设 项目流程

STM32F103 的 I2C 和 AT24C02 EEPROM,这个组合可以说是嵌入式学习路上的一个经典关卡。很多朋友点完灯、打印完串口,第一次接触“外设之间还要讲协议”的概念,就是从 I2C 读写 EEPROM 开始的。这篇内容不做虚的,直接把 STM32F103 通过 I2C 接口操作 AT24C02 的原理、代码、接线、调试方法,以及我在实际项目中踩过的坑都拆开讲透,给刚入门的朋友一条可以直接参考的路径。

1. 整体设计与思路拆解

1.1 这个组合能解决什么问题

单片机系统经常需要保存一些掉电不能丢失的数据,比如设备编号、校准参数、用户配置、运行计数等。STM32F103 内部虽然有 Flash,也可以存储数据,但 Flash 的擦写次数和擦除方式在有些场景下并不方便,而且如果一个页面里存了多个参数,修改其中一个往往需要整页擦写,很麻烦。外部挂一颗 EEPROM 就灵活得多。

AT24C02 是 Atmel(现在属于 Microchip)的经典串行 EEPROM 芯片,容量是 2Kbit,也就是 256 个字节,支持按字节读写和按页写入。它通过 I2C 接口跟 MCU 连接,整个通信只需要两根线:SCL(时钟)和 SDA(数据)。对于存储量不大、但需要频繁修改的参数场景,比如设备 ID、阈值设置、最后运行状态,AT24C02 这种器件几乎是首选。

用 STM32F103 驱动 AT24C02 的价值在于:你不仅学会了一个具体的元器件怎么用,更重要的是把 I2C 协议的时序、地址、ACK 机制、上拉电阻这些底层细节全部过了一遍。这一课学扎实之后,后面接温湿度传感器、OLED 屏幕、触摸芯片、电源管理芯片,基本都是同一个套路。

1.2 为什么选 AT24C02 作为 I2C 入门器件

市面上 I2C 接口的 EEPROM 非常多,比如 AT24C01/02/04/08、FM24C02、ST24C02 等。但 AT24C02 几乎是最适合新手入门的一颗。

首先是容量适中。256 字节这个量级不大不小,拿来存参数正合适,也不至于让你在处理地址时觉得复杂。其次是器件结构简单,内部就是一个存储阵列加上 I2C 接口逻辑,没有状态机、没有中断、没有复杂的寄存器配置,外部只需要给电源和两根总线就能工作。

更重要的是资料足够多。AT24C02 的数据手册写得非常直白,时序图、电气参数、应用电路都有,网上相关的例程、文章、视频遍地都是。就算你完全没接触过 I2C,遇到问题时也容易找到参考。

AT24C02 的工作电压范围是 1.8V 到 5.5V,这对 3.3V 供电的 STM32F103 最小系统板非常友好,可以直接跟单片机共用 3.3V 电源,不用担心电平匹配问题。这也是我反复强调的:刚开始学,尽量把电压域统一,减少不必要的坑。

1.3 为什么在这里选 I2C 而不是 SPI 或 UART

串行 EEPROM 也有 SPI 接口的,比如 25AA020A(也是一颗容量 2Kbit 的器件)。那为什么这个场景常用 I2C 而不是 SPI?

最核心的原因是引脚占用。I2C 只需要两根线就能挂多个设备,SPI 至少需要三根线(MOSI、MISO、SCK),而且每挂一个设备往往还要多一个片选引脚。如果你需要在一个板子上挂 EEPROM、传感器、显示屏好几个器件,I2C 的接线优势非常明显。

I2C 是多主多从架构,每个从设备都有唯一地址,总线上的设备通过地址区分。AT24C02 的地址线 A0/A1/A2 可以配置,理论上一条 I2C 总线上最多可以挂 8 颗同型号芯片。这对于需要扩展存储容量的场景也很实用。

速度方面,AT24C02 标准模式下支持最高 100kHz,快速模式支持 400kHz。EEPROM 这种存储器件用不着很高的时钟频率,I2C 的速度完全够用。而 UART 通常只适合点对点通信,不适合挂多设备,所以在这个场景里 I2C 就是更合理的方案。

2. 核心细节解析与实操要点

2.1 开漏输出、上拉电阻与电平转换

I2C 总线的一个关键电气特点是:SDA 和 SCL 都是开漏结构。什么叫开漏?你可以简单理解成:设备只能主动把引脚拉低,当它想输出高电平的时候,实际上是“释放”这根线,让外部上拉电阻把电平抬上去。

这跟普通 GPIO 推挽输出不一样。推挽输出可以主动输出高电平和低电平,而开漏输出只能输出低电平或者高阻态。既然是高阻态,那总线上就必须有上拉电阻来保证默认电平为高。这个设计的好处是:多个设备都可以挂在同一根线上,任何设备拉低都能让总线变低,不会出现两个设备同时输出高低电平导致冲突的问题。

上拉电阻的阻值不是随便选的。阻值太小,总线上电流过大,低电平可能拉不下去;阻值太大,上升沿太慢,影响通信可靠性。常见做法是 4.7kΩ,我在 3.3V 供电的短距离板子上一律用 4.7kΩ,走线稍长或者总线挂载设备多一些的时候可以用 2.2kΩ。

有些朋友会问:STM32F103 是 3.3V 的器件,AT24C02 如果工作在 5V 怎么办?这个问题其实很常见。注意,I2C 总线的高电平由上拉电阻接的电源决定,不直接由器件的 VCC 决定。如果 AT24C02 用 5V 供电,而你把上拉电阻接到了 3.3V,那么总线高电平就是 3.3V,而 AT24C02 在 5V 供电时的高电平输入阈值是 0.7 × VCC = 3.5V,3.3V 是满足不了的,通信就会不稳定。

我的建议是:在入门阶段,直接把 AT24C02 也接到 3.3V 供电,两边电压域统一,上拉电阻也接到 3.3V,完全没问题。如果项目里 EEPROM 必须用 5V 供电,那就老老实实加 I2C 电平转换芯片,比如 PCA9306,别在总线电平上赌运气。

注意:I2C 的 GPIO 一定要配置成开漏模式,不要配成推挽输出。配成推挽后,一旦总线状态配合不好,可能会造成引脚间直接短路。开漏模式配合外部上拉,才是 I2C 正确的用法。

2.2 I2C 时序拆解:起始、停止、ACK 与数据有效性

I2C 协议看起来复杂,其实核心就几个状态。

每个通信的开始有一个起始条件(START):SCL 为高电平期间,SDA 由高变低。注意,SDA 的电平变化必须发生在 SCL 为高的时候,否则不算起始条件。接着是停止条件(STOP):SCL 为高电平期间,SDA 由低变高。这个组合也是唯一的。

数据位的传输规则是:在 SCL 为高电平期间,SDA 上的数据必须保持稳定;SDA 只能在 SCL 为低电平期间变化。可以理解为:数据在时钟脉冲的高电平期间被采样读取,所以高电平期间 SDA 不能动。很多新手模拟时序时,都是在这里栽跟头,写字节的时候 SDA 变化太早或太晚,导致从机采到的数据错了。

每个字节传输完以后,紧接着的第 9 个时钟脉冲是 ACK 位。发送方在第 9 个时钟前释放 SDA,接收方如果正常接收,会在这期间把 SDA 拉低,表示 ACK;如果 SDA 保持高电平,就是 NACK。

读操作时,主机在读最后一个字节前需要回一个 NACK,告诉从机不要再发了。这是很多新手容易忽略的细节:如果主机一直回 ACK,EEPROM 就会一直输出下一个地址的数据,你读到的就不是你想要的那一段。

2.3 设备地址、页大小与寻址计算

AT24C02 的 I2C 设备地址由两部分组成:固定部分和可配置部分。固定部分是 1010(二进制),可配置部分来自 A0、A1、A2 三个引脚。最后一个 bit 是读写方向位:0 表示写,1 表示读。

如果 A0、A1、A2 都接地,那么:

  • 写地址:1010 000 0,也就是 0xA0
  • 读地址:1010 000 1,也就是 0xA1

很多例程里直接写AT24C02_ADDR_WRITE 0xA0、AT24C02_ADDR_READ 0xA1,新手往往一头雾水。其实只要你清楚这三根地址线的接法,就知道这个 0xA0 是怎么算出来的了。

AT24C02 内部共有 256 个字节地址,对应 0x00 到 0xFF。它总共分 32 页,每页 8 个字节。这个“页大小 8 字节”非常重要,因为 AT24C02 的页写操作最多只能连续写 8 个字节,如果超过页边界,地址会回卷,后面写的数据会覆盖到当前页的开头,而不是自动翻到下一页。

举个例子,如果你想从地址 0x07 开始连续写 4 字节,写到的地址序列是 0x07、0x00、0x01、0x02,而不是 0x07、0x08、0x09、0x0A。因为 0x07 是这一页的最后一个地址,往后再写就回到页头了。这是 AT24C02 使用中非常经典的坑。

2.4 为什么很多老工程师习惯用软件模拟 I2C

STM32F103 内部自带硬件 I2C 外设,按说用外设更省事。但现实是,很多工程师在实战中还是愿意用 GPIO 软件模拟 I2C。

原因不难理解。STM32F103 的硬件 I2C 用标准外设库时,事件标志和中断处理逻辑容易让人头晕,一个 EV5、EV6、EV7、EV8 就能把人绕进去。处理不当的时候还会出现总线卡死、引脚状态异常的问题。虽然用 HAL 库的HAL_I2C_Mem_Write和HAL_I2C_Mem_Read确实省事,但它的内部实现一样涉及超时、错误处理,新手一旦遇到问题,定位起来比较头疼。

软件模拟 I2C 的好处是:时序完全由自己控制,每一笔什么时候拉高、什么时候拉低、延时多久都清清楚楚,出了问题也容易排查。而且代码的可移植性很强,今天在 STM32F103 上用,明天换到 GD32、CH32,换个 GPIO 宏定义就能跑。

我在这个项目里就是用软件模拟的方式来实现的。这不代表硬件 I2C 不可用,而是建议:先通过软件模拟把 I2C 协议吃透,后续如果需要追求效率,再用硬件外设不迟。

3. 实操过程与核心环节实现

3.1 硬件接线与开发环境准备

我用的硬件是常见的 STM32F103C8T6 最小系统板,配套 ST-Link 下载器。AT24C02 我直接用了独立的 SOP-8 封装芯片,在面包板上搭的电路,没有用现成模块,这样更能看清每个引脚的连接关系。

接线方式如下:

STM32F103AT24C02说明
3.3VVCC (Pin 8)电源
GNDGND (Pin 4)地
PB8SCL (Pin 6)I2C 时钟
PB9SDA (Pin 5)I2C 数据
3.3VWP (Pin 7)写保护引脚,接地或悬空表示允许写
GNDA0 (Pin 1)地址位,接地
GNDA1 (Pin 2)地址位,接地
GNDA2 (Pin 3)地址位,接地

在 SCL 和 SDA 上各接一个 4.7kΩ 电阻到 3.3V,这就是 I2C 总线的上拉电阻。WP 引脚是写保护,我给拉到 GND,确保可以正常写入。

软件环境方面,我用的是 Keil MDK 加标准外设库(STM32F10x_StdPeriph_Lib),配合 ST-Link 调试。如果你习惯用 CubeMX 也可以,CubeMX 里把 PB8、PB9 配置成开漏输出就行,不需要启用 I2C 外设。注意 GPIO 初始化时,SCL 和 SDA 都配置成开漏模式,SDA 虽然需要读回电平,但开漏输出模式下是可以直接读引脚状态的,不需要切换成输入模式。

GPIO 初始化的核心代码如下:

void I2C_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); I2C_SCL_HIGH(); I2C_SDA_HIGH(); }

配置成开漏输出之后,你在代码里写GPIO_SetBits和GPIO_ResetBits,实际上是控制引脚是“释放”还是“拉低”。在开漏模式下,写 1 是释放,外部上拉会把它拉高;写 0 才是真正的驱动低电平。

3.2 软件模拟 I2C 基础函数实现

首先定义好宏,方便后面移植和修改引脚:

#define I2C_GPIO_PORT GPIOB #define I2C_SCL_PIN GPIO_Pin_8 #define I2C_SDA_PIN GPIO_Pin_9 #define I2C_SCL_HIGH() GPIO_SetBits(I2C_GPIO_PORT, I2C_SCL_PIN) #define I2C_SCL_LOW() GPIO_ResetBits(I2C_GPIO_PORT, I2C_SCL_PIN) #define I2C_SDA_HIGH() GPIO_SetBits(I2C_GPIO_PORT, I2C_SDA_PIN) #define I2C_SDA_LOW() GPIO_ResetBits(I2C_GPIO_PORT, I2C_SDA_PIN) #define I2C_SDA_READ() GPIO_ReadInputDataBit(I2C_GPIO_PORT, I2C_SDA_PIN)

然后是延时函数。标准的延时可以用 SysTick 实现,我这里直接用了一个简单的微秒级延时,因为 AT24C02 在标准模式下 400kHz 的时钟周期约 2.5us,每个电平维持的时长只要大于 1.3us 左右就能满足要求。为了稳妥,我一般给 2us 以上的延时。

起始条件的代码:

void I2C_Start(void) { I2C_SDA_HIGH(); I2C_SCL_HIGH(); I2C_SDA_LOW(); I2C_SCL_LOW(); }

注意顺序:SCL 高期间 SDA 由高变低,产生起始条件,然后 SCL 拉低为后续传输做准备。如果把 SCL 先拉低再操作 SDA,起始条件就不成立了。

停止条件的代码:

void I2C_Stop(void) { I2C_SDA_LOW(); I2C_SCL_HIGH(); I2C_SDA_HIGH(); }

在 SCL 低电平期间先把 SDA 拉低,然后把 SCL 拉高,最后再把 SDA 拉高,产生停止条件。

发送一个字节并检查 ACK:

uint8_t I2C_WriteByte(uint8_t data) { uint8_t i; uint8_t ack; for (i = 0; i < 8; i++) { if (data & 0x80) I2C_SDA_HIGH(); else I2C_SDA_LOW(); data <<= 1; I2C_SCL_HIGH(); I2C_Delay(); I2C_SCL_LOW(); I2C_Delay(); } // 第9个时钟检测从机ACK I2C_SDA_HIGH(); I2C_SCL_HIGH(); I2C_Delay(); ack = I2C_SDA_READ(); I2C_SCL_LOW(); I2C_Delay(); return ack; // 返回0表示从机回复ACK,返回1表示NACK }

这里有个细节:发送完第 8 个数据位之后,主机要把 SDA 释放(置高),因为第 9 个时钟期间是让从机控制 SDA 拉低来表示 ACK。如果你没有释放 SDA,从机根本拉不低,总线永远是高电平,ACK 检测必然失败。

接收一个字节并控制 ACK/NACK:

uint8_t I2C_ReadByte(uint8_t ack) { uint8_t i; uint8_t data = 0; I2C_SDA_HIGH(); for (i = 0; i < 8; i++) { data <<= 1; I2C_SCL_HIGH(); I2C_Delay(); if (I2C_SDA_READ()) data |= 0x01; I2C_SCL_LOW(); I2C_Delay(); } // 主机在第9个时钟发送ACK或NACK if (ack) I2C_SDA_LOW(); else I2C_SDA_HIGH(); I2C_SCL_HIGH(); I2C_Delay(); I2C_SCL_LOW(); I2C_Delay(); I2C_SDA_HIGH(); return data; }

读字节的时候,每次 SCL 拉高之后,从机才会把下一位数据放到 SDA 上,所以读取数据要在 SCL 高电平期间采样。读完 8 位以后,如果是继续读下一个字节,主机要回 ACK(SDA 拉低);如果是最后一个字节,主机要发 NACK(SDA 释放),从机收到 NACK 后就不再发送。

3.3 AT24C02 读写函数实现

有了基础的 I2C 时序函数,读写 AT24C02 就只是拼装命令的问题了。

写单个字节的流程是:起始条件 → 发送写地址 0xA0 → 发送内部字节地址 → 发送要写入的数据 → 停止条件。

void AT24C02_WriteByte(uint8_t addr, uint8_t data) { I2C_Start(); I2C_WriteByte(0xA0); // 器件写地址 I2C_WriteByte(addr); // 内部字节地址 I2C_WriteByte(data); // 写入数据 I2C_Stop(); AT24C02_WaitWriteComplete(); // 等待写周期结束 }

这里要注意的是:写操作完成后,AT24C02 会进入一个内部写周期(tWR),典型时间是 5ms,期间不响应任何外部命令。如果你不等它写完就去读,很可能读到的还是旧数据。等待的方式有两种:固定延时 5~10ms,或者轮询 ACK。轮询方法更高效,后面单独说。

页写操作的代码:

void AT24C02_PageWrite(uint8_t addr, uint8_t *data, uint8_t len) { uint8_t i; I2C_Start(); I2C_WriteByte(0xA0); I2C_WriteByte(addr); for (i = 0; i < len; i++) I2C_WriteByte(data[i]); I2C_Stop(); AT24C02_WaitWriteComplete(); }

页写时请务必检查两点:写入字节数不能超过 8,写入起始地址到当前页边界剩余空间要足够。否则数据会回卷覆盖。举个例子,从地址 0x05 开始页写 4 字节,写到 0x07 后下一个地址会变成 0x00,这可能完全不是你预期的行为。

随机读一个字节的流程是:起始条件 → 发送写地址 0xA0 → 发送内部字节地址 → 重复起始条件 → 发送读地址 0xA1 → 读取一个字节(最后一个字节回 NACK)→ 停止条件。

uint8_t AT24C02_ReadByte(uint8_t addr) { uint8_t data; I2C_Start(); I2C_WriteByte(0xA0); I2C_WriteByte(addr); I2C_Start(); // 重复起始条件 I2C_WriteByte(0xA1); // 器件读地址 data = I2C_ReadByte(0); // 读最后1个字节,回NACK I2C_Stop(); return data; }

连续读取多字节时,除了最后一个字节回 NACK,其余字节都要回 ACK:

void AT24C02_ReadBytes(uint8_t addr, uint8_t *buffer, uint8_t len) { uint8_t i; I2C_Start(); I2C_WriteByte(0xA0); I2C_WriteByte(addr); I2C_Start(); I2C_WriteByte(0xA1); for (i = 0; i < len; i++) { if (i == len - 1) buffer[i] = I2C_ReadByte(0); // 最后一个NACK else buffer[i] = I2C_ReadByte(1); // 中间字节回ACK } I2C_Stop(); }

这个函数里最关键的就是 ACK 和 NACK 的切换时机。许多朋友写的连续读,要么全部回 ACK,导致从机一直输出,读到的数据尾部错乱;要么过早回 NACK,从机提前停止发送,数据少了。

3.4 写周期等待与轮询 ACK 技巧

AT24C02 的数据手册给出 tWR 典型时间是 5ms,最大 10ms。最简单的做法是每次写完后延时 10ms,稳稳当当。但如果你的系统里频繁写 EEPROM,每次都干等 10ms 就太浪费时间了。

更高效的办法是利用 ACK 轮询。AT24C02 在内部写周期内不会响应任何命令,也就是说,你给它发送写地址 0xA0,它不会回 ACK,SDA 保持高电平。一旦内部写周期结束,它才能正常响应。

所以轮询写周期完成的函数可以这样写:

void AT24C02_WaitWriteComplete(void) { uint8_t ack; do { I2C_Start(); ack = I2C_WriteByte(0xA0); // 发送器件写地址,等待ACK I2C_Stop(); } while (ack != 0); // 非0表示NACK,继续等 }

因为I2C_WriteByte返回 0 表示 ACK,所以循环直到返回 0 就说明芯片已经准备好接收新命令了。实测下来,这个方式的等待时间通常比固定的 10ms 短很多,尤其在连续写入多个参数的时候,整体效率提升非常明显。

4. 常见问题与排查技巧实录

4.1 设备无应答(NACK)的排查思路

AT24C02 不回 ACK,是最常见的故障现象之一。如果你在I2C_WriteByte(0xA0)这一步就拿到了 NACK,先别急着怀疑芯片坏了,按顺序排查。

第一,检查电源。AT24C02 的 VCC 是否有稳定的 3.3V,GND 是否跟 STM32 共地。面包板接触不良可能导致电源有电压但芯片没工作,经验之谈。

第二,检查地址。A0、A1、A2 是否接对了?如果是模块,看模块上有没有已经把地址端固定接地的焊盘;如果是自己搭的,确认三个引脚都接到了 GND。如果某一个引脚接到了高电平,那么设备地址就会变化,0xA0 就找不到了。

第三,检查上拉电阻。缺少上拉电阻时,总线默认电平不是高,波形和逻辑都会乱。要验证这一点,直接用万用表量 SCL 和 SDA 的对地电压,正常空闲状态应该都在 3.3V 左右。

第四,检查 GPIO 配置。SDA 和 SCL 是不是配成了开漏输出?如果配成了推挽输出,通信时可能会导致总线逻辑异常。最简单的验证方式:把 SDA 和 SCL 都配置成开漏 + 外部上拉,在初始化代码里手动写 SDA_HIGH、SCL_HIGH,然后测电压,应该是高电平。

4.2 读回全是 0xFF 或乱码

写进去的数据,读出来全是 0xFF,这通常说明 I2C 从机并没有真正执行写操作,或者读操作的地址没对上。

先确认一个容易忽略的点:写完数据后是否等待了写周期?如果你写完立即读,EEPROM 还在内部写周期,读操作可能完全不生效,甚至读到的是旧数据。用我上面写的AT24C02_WaitWriteComplete()就能解决。

另一个常见原因是器件写保护引脚 WP 被接到了高电平。WP 接高时,整个芯片的写操作会被禁止,芯片照样能读,但写不进去。你写进去是假象,读出来自然还是 0xFF。

如果读出来的数据是乱码而不是全 0xFF,很大概率是时序不对。比如 SCL 高电平期间 SDA 变化了,或者数据位的时钟个数不对。这时候建议用逻辑分析仪抓一次波形,对照数据手册的时序图逐位查看,很快就能定位到是哪个字节的位偏移了。

4.3 跨页写入导致数据覆盖

前面提过,AT24C02 每页 8 字节,页写最多 8 字节,而且写到页尾后地址会回卷。很多朋友没有意识到这个问题,直接从地址 0x07 连续写 10 字节,结果发现 0x00 地址的数据莫名被改了,怎么排查都找不到原因,其实就是页回卷。

解决办法有两种。第一种是保证每次页写都不跨页:写数据前计算当前地址到页尾还剩多少字节,超出部分拆分到下一轮写。第二种更简单,干脆就用单字节写,虽然效率低一些,但在数据量不大(几十字节以内)的场景下完全够用。

如果你希望实现一个通用的写函数,可以这样做:检查(addr % 8) + len是否大于 8,如果大于就拆分两次写。这个检查一定要在写入之前做,否则数据一旦回卷覆盖,很难找回原数据。

4.4 常见问题速查表

现象可能原因排查方向
发送 0xA0 后无 ACK地址线接错、芯片无供电、上拉缺失万用表查电源,确认 A0/A1/A2 接地,检查 4.7k 上拉
写后读回全 0xFF未等待写周期、WP 写保护加上写周期等待,确认 WP 接地
读回数据乱码时序不对、SCL 高期间 SDA 变化用逻辑分析仪抓波形,对比数据手册
页写出现覆盖跨页写入计算页剩余空间,按页拆分写入
总线一直低电平SDA 被某设备拉死、缺少上拉断开所有设备,逐个测试哪个引脚异常
通信偶尔失败上拉电阻过大导致上升沿过慢减小上拉电阻到 2.2k~4.7k,缩短总线走线

5. 调试工具与验证手段:从波形到回读

5.1 用逻辑分析仪抓 I2C 波形

我调试 I2C 习惯挂一个逻辑分析仪并行观察,虽然串口打印也能验证数据对不对,但波形能告诉你协议层次的问题。推荐使用采样率 24MHz 以上的 8 通道逻辑分析仪,几十块钱的入门款足以分析 100kHz 和 400kHz 的 I2C 信号。

抓波形的时候,把 SCL 接到逻辑分析仪的 CH0,SDA 接到 CH1,地线必须跟板子共地。软件里选择 I2C 协议解码器,设置好高电平和采样率,然后触发一次写读操作。解码结果会直接显示起始位、地址字节、ACK 位、数据字节,一目了然。

我个人最常看的几个点:地址字节是不是 0xA0 和 0xA1;第 9 个时钟的 ACK 位是低电平还是高电平;连续读数据时最后一个字节前面的 ACK/NACK 状态。有一次我排查一个偶发通信失败,波形上看到地址字节后 ACK 位没有出现,后来发现是上拉电阻虚焊导致高电平不稳定,这个在打印数据里很难察觉。

5.2 通过串口回读验证读写结果

写完驱动,总得有一个直观的验证手段。我的做法是通过串口上位机给板子发命令,控制 MCU 执行写入或读取,然后把结果通过串口打印出来。

具体流程可以这样设计:上位机发送一个字符串,比如W 0x10 0x55,表示往地址 0x10 写入数据 0x55;发送R 0x10,表示读取地址 0x10 的值,MCU 通过串口返回类似addr=0x10 data=0x55的信息。这样可以反复测试,确认读写都正常。

更完整的压力测试可以这样:循环向 0x00~0xFF 所有地址写入对应的值,再全部读回来对比。这个测试能一次性暴露页边界问题、写周期未等待问题、地址计算错误等问题。我第一次跑这种全量回环测试时,就在 0x07 和 0x08 之间的跨页写入上报错了,也正是那次让我彻底记住了页大小的限制。

跑全量回环测试时,建议在循环里用轮询 ACK 而不是固定延时,否则 256 个字节全部写一遍,每个都等 10ms,总耗时长达 2.56 秒,调试效率低得让人抓狂。用轮询方式,一般几十毫秒就能完成一轮,即使多跑几轮测试也不会心烦。

5.3 一些后续可以尝试的扩展

AT24C02 读写跑通之后,很多有意思的外设都变得好理解多了。比如 SSD1306 这种 OLED 屏幕,也是 I2C 接口,控制流程不过是初始化序列加显存数据写入。你掌握了 I2C 的起始、停止、ACK、多字节读写,再把 OLED 的命令字和数据字弄清楚,离点亮屏幕就只差一个驱动函数了。

还可以试试在一条 I2C 总线上同时挂 AT24C02 和 OLED,地址分别是 0xA0 和 0x78,总线不会冲突。这时候你会发现 I2C 双线挂多设备的优势真正体现出来了。再往后,如果想提高通信效率,就可以尝试用 STM32F103 的硬件 I2C 外设配合 DMA 传输,不过那又是另一个话题了。

我从实际项目里得到的体会是:I2C 协议本身并没有多高深,但它的每一个细节都值得认真对待。从开漏配置到上拉电阻,从 ACK 时序到页边界处理,只有亲手写一遍、跑一遍、再排查一遍,这些知识点才是真正属于你的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询