资讯动态

STM32F103驱动AT24C02实战:I2C通信从入门到精通

发布时间:2026/10/25 1:11:53 来源:尧图企业网站定制
1. 为什么偏偏选 I2C 和 AT24C02 来练手1.1 这个组合到底解决了什么问题做嵌入式开发的人迟早会遇到一个绕不开的需求掉电之后数据不能丢。比如设备运行时长统计、用户配置参数、校准系数、开机次数记录这些东西如果只放在 MCU 的 RAM 里一断电就全没了。STM32F103 内部虽然有 Flash但拿它当数据存储器用有几个现实问题擦写寿命只有 1 万次左右而且擦除是按页来的频繁写小数据非常不划算稍不注意还会把程序本身给擦了。AT24C02 这类 EEPROM 就是专门干这个的。它容量 2Kbit也就是 256 字节按字节读写擦写寿命标称 100 万次数据保存 100 年。容量不大但存点配置参数、标志位、小段日志绰绰有余。而它跟 MCU 通信用的就是 I2C 总线两根线SCL、SDA就能挂一堆设备硬件成本极低。所以这个项目的本质是用 STM32F103 的 I2C 外设驱动 AT24C02 完成字节级、页级、任意地址的读写并保证数据可靠。它适合刚接触 STM32 外设的新手作为第一个 I2C 实战也适合老手拿来复习时序细节和排查总线问题。1.2 为什么说它是 I2C 的“标准教材”I2C 协议本身不复杂但坑特别多。AT24C02 恰好是一个协议行为非常典型的从机它有固定的设备地址格式、有写周期等待、有页写边界、有 ACK 应答机制。你把这颗芯片调通了基本上 I2C 的时序、地址、应答、总线仲裁这些概念就都过了一遍。相比之下如果你一上来就去驱动 OLED 屏或者复杂传感器问题会混在一起——到底是 I2C 时序不对还是屏幕初始化命令写错了很难定位。AT24C02 的好处是行为可预测写进去读出来对得上就是对的对不上就是时序或地址有问题排查路径非常清晰。我个人的经验是新手调 I2C先拿 AT24C02 练手成功率最高学到的东西也最扎实。1.3 硬件上你需要准备什么最小系统层面一块 STM32F103 最小系统板就够了。这里有个热词叫“stm32f103 5v转3.3v电路”值得说一句STM32F103 的 IO 是 3.3V 电平而 AT24C02 常见的是 5V 和 3.3V 两种供电版本。如果你手上是 5V 供电的 AT24C02直接跟 3.3V 的 STM32 对接理论上 I2C 是开漏总线靠上拉电阻拉高只要上拉电阻接到 3.3V总线高电平就是 3.3V不会烧 IO。但为了省心建议直接用 3.3V 供电的 AT24C02全系统统一 3.3V避免电平转换的麻烦。接线就四根VCC、GND、SCL、SDA。SCL 和 SDA 各接一个 4.7k 到 10k 的上拉电阻到 3.3V。AT24C02 的 A0、A1、A2 三个地址引脚全部接地这样设备地址就是固定的 0xA0写/0xA1读。WP 引脚接地允许写入如果接高电平就是写保护只能读不能写调试时如果发现写不进去先检查这个脚。注意上拉电阻不能省。I2C 是开漏输出没有上拉电阻总线永远拉不高通信必然失败。这是新手最常见的硬件错误之一。2. I2C 协议里那些必须搞懂的细节2.1 开漏模式和推挽模式到底怎么选热词里有个“i2c的推挽模式和开漏模式”这个问题问得非常多。结论很明确I2C 的 SCL 和 SDA 必须配置为开漏输出Open-Drain配合外部上拉电阻使用。原因在于 I2C 是多主多从的总线结构。如果两个设备同时想拉低总线开漏模式下它们只是各自把线拉到地不会冲突但如果用推挽模式一个设备输出高、另一个输出低就会形成电源到地的直通短路轻则通信异常重则烧毁引脚。开漏模式的电气特性是只能主动拉低不能主动拉高高电平靠上拉电阻提供。这正好实现了 I2C 的“线与”逻辑——只要有一个设备拉低总线就是低所有设备都释放总线才被上拉电阻拉高。在 STM32 的 GPIO 配置里如果你用硬件 I2C 外设引脚会自动配置成复用开漏模式AF Open-Drain。如果你用软件模拟 I2C就要手动把 SCL 和 SDA 配置成GPIO_Mode_Out_OD。这一点在标准库和 HAL 库里写法不同但本质一样。2.2 AT24C02 的设备地址是怎么算出来的AT24C02 的 7 位设备地址是1010 A2 A1 A0。高四位1010是厂商固定的低三位由 A2、A1、A0 三个引脚的电平决定。全部接地时7 位地址就是1010000即 0x50。但 I2C 传输时第一个字节是“地址字节”格式是 7 位地址加 1 位读写方向位。所以写操作地址字节 (0x50 1) | 0 0xA0读操作地址字节 (0x50 1) | 1 0xA1很多人在这里栽跟头是因为有些资料直接给 0xA0有些给 0x50搞不清楚哪个是哪个。记住0x50 是 7 位地址0xA0 是包含写方向位的 8 位地址字节。HAL 库的HAL_I2C_Mem_Write函数参数里填的是左移后的 8 位地址也就是 0xA0。2.3 写周期和 ACK 轮询机制AT24C02 有一个容易被忽略的特性写周期时间Write Cycle Time。每次写完一个字节或一页数据后芯片内部需要时间把数据真正写入 EEPROM 存储单元这个时间最大 5ms。在这 5ms 内芯片不会响应任何 I2C 命令包括它自己的地址。如果你写完立刻发下一个读或写命令芯片不会应答NACK通信就失败了。正确的做法是ACK 轮询写完数据后反复发送起始条件加设备地址直到芯片返回 ACK说明它内部写周期结束了可以接受下一条命令。HAL 库的HAL_I2C_Mem_Write函数内部其实已经处理了等待但如果你用寄存器或软件模拟就必须自己实现这个轮询。我见过太多人写完不等待然后读出来是旧数据还以为是读函数写错了其实是写周期没结束。2.4 页写边界为什么跨页会“回卷”AT24C02 每页 8 字节共 32 页。页写模式下你一次可以连续写最多 8 个字节。但有个陷阱如果你从页内某个位置开始写写到页尾后地址不会自动跳到下一页而是回卷到本页开头。举个例子从地址 0x06 开始写 4 个字节预期写到 0x06、0x07、0x08、0x09。但实际行为是0x06、0x07 写完后地址回卷到 0x00接着写 0x00、0x01。结果就是 0x08、0x09 没被写到反而 0x00、0x01 被覆盖了。这个坑非常隐蔽因为如果你从页对齐地址0x00、0x08、0x10……开始写且不超过 8 字节就不会触发。但一旦跨页数据就乱了。解决办法是写之前先算好当前地址到页尾还剩多少字节分多次写每次不跨页。3. 从零搭建读写工程硬件与软件配置3.1 硬件连接与上拉电阻计算先把手上的板子接起来。STM32F103 这边我习惯用 PB6 做 SCL、PB7 做 SDA这是 I2C1 的默认引脚。AT24C02 的 SCL、SDA 分别对接VCC 接 3.3VGND 共地A0/A1/A2 接地WP 接地。上拉电阻选多大这跟总线电容和通信速率有关。标准模式 100kHz 下4.7k 是通用值快速模式 400kHz 下可以用 2.2k 到 4.7k。电阻太小功耗大低电平灌电流可能超过引脚能力电阻太大上升沿变缓高速通信时波形来不及拉高。粗略估算总线电容一般在 100pF 到 200pF 量级上升时间要求小于 1us快速模式。RC 时间常数要小于 1usR 取 4.7k、C 取 200pFRC 0.94us刚好卡在边缘。所以 4.7k 是稳妥选择别用 10k 以上。实操心得如果你手头只有 10k 电阻低速 100kHz 下也能凑合用但波形上升沿会明显变缓示波器一看就是圆弧状。调试阶段建议直接上 4.7k。3.2 用 CubeMX 配置 I2C 外设现在很多人用 CubeMX 生成初始化代码效率高。配置步骤在 Pinout 视图里找到 I2C1选择 I2C 模式。在 Configuration 里I2C Speed Mode 选 Standard Mode 或 Fast Mode对应 100kHz 或 400kHz。AT24C02 支持 400kHz但初次调试建议先用 100kHz稳。时钟配置里确认 I2C1 的时钟源使能APB1 时钟一般是 36MHz。生成代码后HAL 库会自动生成MX_I2C1_Init函数。这里有个细节CubeMX 生成的 I2C 初始化里ClockSpeed参数决定了实际速率。如果你填 100000它会根据 APB1 时钟自动算分频。但有时候算出来的实际速率跟预期有偏差因为分频系数是整数。比如 36MHz 分频到 100kHz理论分频比是 360实际寄存器只能取整最终可能是 90kHz 或 112kHz。这个偏差在容忍范围内不用纠结。3.3 关键寄存器与 HAL 库函数对照如果你用 HAL 库核心就两个函数HAL_I2C_Mem_Write(hi2c1, 0xA0, addr, I2C_MEMADD_SIZE_8BIT, data, len, timeout)HAL_I2C_Mem_Read(hi2c1, 0xA1, addr, I2C_MEMADD_SIZE_8BIT, data, len, timeout)参数含义设备地址、内存地址AT24C02 是 8 位地址、地址长度、数据指针、长度、超时。但 HAL 库有个已知问题在某些情况下如果 I2C 总线被拉死比如从机在传输中途复位HAL 库会一直返回 BUSY 或超时不会自动恢复。这时候需要手动复位 I2C 外设或者用 GPIO 模拟几个时钟脉冲把总线解锁。这个后面排查章节会细说。如果你用标准库就要自己写起始、发送地址、等待 ACK、发送数据、停止这一套。虽然麻烦但对理解时序帮助极大。我建议新手先用标准库或软件模拟写一遍再用 HAL 库这样出了问题你知道底层发生了什么。4. 读写全流程代码实现与逐行拆解4.1 单字节写最基础也最容易出错单字节写的流程是起始条件 → 发送设备地址写→ 等待 ACK → 发送内存地址 → 等待 ACK → 发送数据 → 等待 ACK → 停止条件 → 等待写周期。用 HAL 库写就是一行HAL_I2C_Mem_Write(hi2c1, 0xA0, 0x00, I2C_MEMADD_SIZE_8BIT, val, 1, 100);但这一行背后HAL 库帮你做了所有时序。如果你用软件模拟代码大致是这样void AT24C02_WriteByte(uint8_t addr, uint8_t data) { I2C_Start(); I2C_SendByte(0xA0); // 设备地址写 I2C_WaitAck(); I2C_SendByte(addr); // 内存地址 I2C_WaitAck(); I2C_SendByte(data); // 数据 I2C_WaitAck(); I2C_Stop(); AT24C02_WaitStandby(); // 等待写周期结束 }AT24C02_WaitStandby就是 ACK 轮询循环发送起始0xA0如果收到 ACK 就说明芯片空闲了发停止条件退出。注意每次写操作后必须等待写周期。如果你连续写多个字节每个字节之间都要等否则后面的写会被忽略。这是新手最常踩的坑。4.2 单字节读先写地址再读数据读操作比写多一步要先“伪写”一个内存地址然后重新起始再发读地址。流程是起始 → 发设备地址写→ 等 ACK → 发内存地址 → 等 ACK → 重新起始 → 发设备地址读→ 等 ACK → 读数据 → 发 NACK → 停止。HAL 库的HAL_I2C_Mem_Read内部就是这个流程。软件模拟时uint8_t AT24C02_ReadByte(uint8_t addr) { uint8_t data; I2C_Start(); I2C_SendByte(0xA0); I2C_WaitAck(); I2C_SendByte(addr); I2C_WaitAck(); I2C_Start(); // 重新起始 I2C_SendByte(0xA1); // 设备地址读 I2C_WaitAck(); data I2C_ReadByte(); I2C_SendNack(); // 最后一个字节回 NACK I2C_Stop(); return data; }关键点读最后一个字节时必须回 NACK告诉从机“我读完了”。如果回 ACK从机会继续输出下一个字节导致总线异常。这个细节很多人忽略。4.3 页写效率高但边界要算清页写一次最多写 8 字节且不能跨页。实现思路计算当前地址在页内的偏移offset addr % 8。计算本页还能写多少字节remain 8 - offset。取len和remain的较小值作为本次写入长度。写完等待写周期地址和长度更新继续下一轮。代码框架void AT24C02_WritePage(uint8_t addr, uint8_t *buf, uint16_t len) { while (len 0) { uint8_t offset addr % 8; uint8_t remain 8 - offset; uint8_t chunk (len remain) ? len : remain; HAL_I2C_Mem_Write(hi2c1, 0xA0, addr, I2C_MEMADD_SIZE_8BIT, buf, chunk, 100); AT24C02_WaitStandby(); addr chunk; buf chunk; len - chunk; } }这样无论从哪个地址开始写、写多长都不会跨页回卷。实测下来很稳。4.4 连续读一次读出整段数据连续读比连续写简单因为读没有页边界限制可以从任意地址连续读任意长度。流程是伪写地址 → 重新起始 → 读地址 → 循环读数据前 N-1 个字节回 ACK最后一个回 NACK → 停止。void AT24C02_ReadBuffer(uint8_t addr, uint8_t *buf, uint16_t len) { HAL_I2C_Mem_Read(hi2c1, 0xA1, addr, I2C_MEMADD_SIZE_8BIT, buf, len, 100); }HAL 库内部已经处理了 ACK/NACK 逻辑。但如果你用软件模拟就要注意最后一个字节的 NACK。4.5 写周期等待的两种实现方式第一种是 ACK 轮询前面说了。第二种是固定延时直接延时 5ms 以上。固定延时简单粗暴但效率低因为实际写周期通常远小于 5ms可能 2ms 就完成了你却等了 5ms。ACK 轮询效率高但实现稍复杂。我的建议是调试阶段用固定延时确保功能正确量产或对速度有要求时换成 ACK 轮询。两者切换只需要改一个函数。实操心得有些批次的 AT24C02 写周期特别短1ms 就完成了有些批次接近 5ms。如果你用固定延时保险起见延时 6ms。别卡在 5ms 边缘温度变化会影响写周期。5. 调试现场那些让人抓狂的问题和排查方法5.1 总线一直 BUSY 或读出来全是 0xFF这是最经典的问题。读出来全是 0xFF说明 SDA 一直是被上拉电阻拉高的状态从机根本没应答。可能原因设备地址错了。确认 A0/A1/A2 的接法算清楚 7 位地址和 8 位地址字节。上拉电阻没接或阻值太大。用万用表量 SCL、SDA 对 VCC 的电阻应该在 4.7k 左右。接线反了。SCL 和 SDA 接反是新手常犯错误交换一下试试。从机供电不对。量一下 AT24C02 的 VCC 引脚确认有 3.3V。如果 HAL 库一直返回 BUSY可能是总线被拉死了。解决办法把 I2C 外设复位或者临时把 SCL、SDA 配置成普通推挽输出手动发送 9 个时钟脉冲再切回 I2C 模式。这个操作能让卡在传输中途的从机释放总线。5.2 写进去读出来不对或者只有部分字节对这种情况通常是写周期没等够或者页写跨页回卷了。排查步骤写一个字节延时 10ms再读看对不对。如果对说明是写周期问题。写 8 个字节从页对齐地址开始看对不对。如果对说明是跨页问题。写 8 个字节从非对齐地址开始看哪些字节错了。如果错误模式是“后面的覆盖了前面的”就是回卷。还有一种可能是 WP 引脚没接地。WP 高电平时芯片进入写保护写操作被忽略但读操作正常。如果你发现读出来是旧数据写不进去先量 WP 引脚。5.3 通信速率上不去400kHz 就出错100kHz 能通400kHz 就挂通常是硬件问题上拉电阻太大上升沿太慢。换成 2.2k 试试。总线走线太长或太乱电容太大。缩短走线或者降低速率。电源去耦不好。在 AT24C02 的 VCC 和 GND 之间加一个 0.1uF 电容紧贴芯片引脚。软件层面检查 CubeMX 里 I2C 的 ClockSpeed 设置以及 APB1 时钟是否正确。有时候时钟树配错了I2C 实际速率远高于预期也会导致通信失败。5.4 常见问题速查表现象可能原因排查方法读出全 0xFF地址错、上拉缺失、接线反量电阻、换地址、交换 SCL/SDA写入无效WP 高、写周期未等量 WP 引脚、加延时部分字节错跨页回卷检查写入地址和长度400kHz 失败上拉太大、走线差换 2.2k、缩短走线HAL 返回 BUSY总线拉死复位 I2C 或发 9 个时钟脉冲读出来是旧数据写周期未完成加 ACK 轮询或延时5.5 用示波器看波形最直接的排查手段如果有示波器一定要看 SCL 和 SDA 的波形。正常波形应该是干净的方波上升沿陡峭。如果上升沿是圆弧状说明上拉电阻太大或电容太大。如果 SDA 在 ACK 位不是低电平说明从机没应答。看波形还能发现时序问题起始条件是不是 SCL 高时 SDA 下降停止条件是不是 SCL 高时 SDA 上升数据位是不是在 SCL 高电平期间稳定这些细节用示波器一看就明白比盲猜快得多。6. 几个容易被忽略的进阶细节6.1 数据校验别假设 EEPROM 永远可靠虽然 AT24C02 标称 100 万次擦写、100 年保存但实际使用中电源波动、总线干扰都可能导致写入错误。稳妥的做法是加校验。简单方案是存两个副本读的时候对比或者加一个 CRC 校验字节。我一般会在数据结构里加一个字节的校验和所有数据字节异或结果存在最后一个字节。读的时候重新算一遍对不上就认为数据损坏用默认值覆盖。这个成本极低但能避免很多莫名其妙的“参数自己变了”的问题。6.2 磨损均衡频繁写同一地址的隐患AT24C02 的 100 万次擦写寿命是针对每个存储单元的。如果你每隔一秒就往地址 0x00 写一次运行时长那 0x00 这个单元很快就会到寿命。虽然 100 万次听起来很多但一秒一次的话11 天就写完了。解决办法是磨损均衡把数据轮流写到不同地址或者用整个 256 字节做一个环形缓冲区。对于运行时长这种频繁更新的数据可以每 100 次更新才真正写一次 EEPROM中间变化存在 RAM 里。这样寿命直接延长 100 倍。6.3 多设备挂载地址冲突怎么办I2C 总线可以挂多个设备但地址不能冲突。AT24C02 通过 A0/A1/A2 可以配置 8 个不同地址所以最多挂 8 片总容量 2KB。如果你需要更多存储可以换 AT24C32、AT24C64 等更大容量的型号地址配置方式类似。挂多个设备时上拉电阻只需要一对接在总线任意位置即可。但总线电容会增加如果设备多、走线长可能需要降低速率或减小上拉电阻。6.4 低功耗场景下的注意事项如果项目是电池供电AT24C02 的待机电流只有几微安不是耗电大户。但要注意写操作瞬间电流较大可能达到 3mA 左右。如果电源内阻大可能导致电压跌落影响 MCU。建议在 AT24C02 的 VCC 附近加一个 10uF 电容储能。另外不操作时可以把 I2C 引脚配置成模拟输入或开漏输出高电平避免漏电流。不过 STM32 的 I2C 引脚在空闲时功耗很低一般不用特别处理。7. 我个人在实际操作中的体会调 AT24C02 这个事说难不难说简单也不简单。我第一次调的时候读出来全是 0xFF查了一下午最后发现是 SCL 和 SDA 接反了。后来养成习惯接线完先用万用表量一遍确认 VCC、GND、上拉电阻都正常再上电调试。另一个体会是不要迷信 HAL 库。HAL 库确实方便但它把很多细节藏起来了。当你遇到 BUSY 卡死、超时返回这些情况如果不懂底层时序根本无从下手。我的建议是至少用软件模拟写一遍完整的读写流程把起始、停止、ACK、NACK 这些概念搞清楚然后再用 HAL 库提高效率。这样出了问题你知道该往哪个方向查。最后分享一个小技巧调试 I2C 时可以先用一个简单的“写一个字节、读一个字节、串口打印”的循环确认基本通信正常再往上叠加页写、连续读、校验这些功能。不要一上来就写一大坨代码出了问题很难定位。一步一步来每步都验证这才是嵌入式调试的正确姿势。这个内容后续还可以这样扩展把 AT24C02 的读写封装成一个通用的参数管理模块支持按键触发保存、开机自动加载、数据版本升级。再进一步可以对比 Flash 模拟 EEPROM 的方案看看什么场景下用外置 EEPROM 更划算什么场景下用内部 Flash 就够了。这些都是实际项目中会遇到的选型问题值得单独拿出来聊。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑