
1. 项目缘起与整体设计思路STM32F103 配 AT24C02 这个组合在嵌入式圈子里基本算是“Hello World”级别的经典搭配。但凡做过一两个量产项目的朋友手里大概率都跑过这套代码。但经典归经典真正能把 I2C 时序、AT24C02 页写机制、STM32 硬件 I2C 的坑全部吃透的人其实没想象中那么多。我见过太多项目代码能跑但换个批次芯片就挂或者连续写几个字节就丢数据最后查半天发现是页写边界没处理。这个项目的核心目标很明确用 STM32F103 的硬件 I2C 接口完成对 AT24C02 EEPROM 的完整读写操作包括单字节读写、页写、连续读并且要能稳定跑在量产环境里。AT24C02 是 2Kbit 容量的 I2C EEPROM换算过来是 256 字节页大小 8 字节这些参数直接决定了我们写数据时不能跨页。STM32F103 这边我选的是 I2C1引脚用 PB6SCL和 PB7SDA这是最常用的组合也是最小系统板上默认引出的接口。为什么不用软件模拟 I2C这个问题我被问过无数次。软件模拟的好处是移植方便、时序可控但缺点也很明显占用 CPU 周期、速率上不去、中断环境下容易出问题。硬件 I2C 虽然 STM32F103 的早期批次有一些已知的勘误比如总线锁死问题但只要初始化配置正确、加上超时处理稳定性完全没问题。我实测下来硬件 I2C 跑 100kHz 标准模式连续读写 10 万次无丢包。所以这个项目我坚持用硬件 I2C顺便把那些“STM32 硬件 I2C 不好用”的传言给破了。适合谁来参考如果你刚接触 STM32正在做第一个 I2C 项目这篇内容能帮你少走至少两周弯路。如果你已经能跑通代码但总遇到偶发故障这里面的排查思路和页写处理细节应该能对上你的症状。如果你是从 51 或者 Arduino 转过来的那更好我会把 STM32 硬件 I2C 和软件模拟的差异点讲透。整体方案设计上我分了三层底层 I2C 驱动层、AT24C02 设备层、应用接口层。底层负责初始化、起停、收发字节设备层处理 AT24C02 的器件地址、页写边界、写周期等待应用层提供AT24C02_WriteByte、AT24C02_ReadByte、AT24C02_WritePage、AT24C02_ReadBuffer这几个接口。这样分层的好处是以后换 24C16 或者 24C64只需要改设备层的地址和页大小底层和应用层基本不动。2. 硬件设计与关键参数计算2.1 最小系统与 I2C 引脚分配STM32F103 最小系统板市面上很多核心就是那颗 LQFP48 封装的 C8T6加上 8MHz 晶振、32.768kHz RTC 晶振可选、复位电路、BOOT 跳线、SWD 调试口。我用的这块板子把 I2C1 引到了 PB6 和 PB7旁边还留了 4.7kΩ 的上拉电阻位置。这里有个细节很多廉价最小系统板为了省成本I2C 引脚的上拉电阻是不焊的或者焊的是 10kΩ。AT24C02 在 100kHz 下4.7kΩ 是标准推荐值10kΩ 在短距离、低速率下也能用但波形上升沿会变缓高速下容易误码。I2C 需要电平转换吗这个问题在热词里出现了我直接给结论如果 STM32 和 AT24C02 都是 3.3V 供电不需要。AT24C02 的工作电压范围是 1.8V 到 5.5V3.3V 完全兼容。但如果你板子上有 5V 的 I2C 器件混在一起那就必须做电平转换否则 5V 器件的 SDA 拉高时会通过上拉电阻倒灌进 STM32 的 3.3V 引脚长期运行会损伤 IO。我一般用 TXS0102 或者最简单的 MOS 管加电阻方案成本低且可靠。2.2 上拉电阻的取值计算上拉电阻不是随便选的它跟总线电容、上升时间、灌电流能力都有关系。I2C 标准模式100kHz要求上升时间 Tr ≤ 1000ns快速模式400kHz要求 Tr ≤ 300ns。上升时间公式是Tr ≈ 0.847 × R × C其中 R 是上拉电阻C 是总线总电容。假设我们的 PCB 走线短总线电容约 50pF那么 100kHz 下R ≤ 1000ns / (0.847 × 50pF) ≈ 23.6kΩ所以 4.7kΩ 远远满足。但电阻也不能太小否则低电平时灌电流太大。STM32F103 的 I2C 引脚灌电流能力是 3mA 左右3.3V 除以 4.7kΩ 约 0.7mA完全安全。如果选 1kΩ灌电流 3.3mA就接近极限了。所以 4.7kΩ 是兼顾上升时间和灌电流的最佳平衡点。2.3 AT24C02 器件地址与写保护AT24C02 的 7 位器件地址是1010加上 A2、A1、A0 三个引脚的电平。最小系统板上一般把 A0、A1、A2 全部接地所以 7 位地址是1010000即 0x50。写操作时8 位地址是0xA0读操作时8 位地址是0xA1。这个地址在代码里必须写对我见过有人把 0xA0 写成 0xA1 然后死活读不出数据查了一下午。WP 引脚是写保护接高电平时芯片进入只读模式接低电平时允许读写。我一般直接接地需要写保护的时候再用 GPIO 控制。如果你做的是参数存储建议 WP 接一个 GPIO正常运行时拉低固件升级或者关键数据写入完成后再拉高防止误写。3. 底层 I2C 驱动实现细节3.1 GPIO 模式选择开漏还是推挽热词里有人问 I2C 的推挽模式和开漏模式这里必须说清楚。I2C 总线是开漏输出加外部上拉的结构这是协议规定的。为什么因为 I2C 是多主多从总线任何设备都可以拉低 SDA但没有任何设备可以强行拉高。如果配置成推挽输出当两个设备同时输出不同电平时会形成电源到地的低阻通路瞬间大电流可能烧毁 IO。所以 STM32 的 I2C 引脚必须配置为复用开漏模式GPIO_Mode_AF_OD并且使能外部上拉。我见过有人用推挽模式跑通了那是因为总线上只有一个主机没有冲突。但这是侥幸一旦接上多个从机或者从机偶尔拉低 SDA 做应答就可能出问题。正确配置如下GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_OD; GPIO_Init(GPIOB, GPIO_InitStructure);3.2 I2C 初始化参数计算STM32F103 的 I2C 时钟来自 APB1默认 36MHz。要得到 100kHz 的 SCL需要配置I2C_ClockSpeed 100000标准库会自动计算 CCR 和 TRISE。但如果你用寄存器直接写公式是CCR APB1时钟 / (2 × SCL频率) 36MHz / (2 × 100kHz) 180TRISE 是最大上升时间标准模式 1000ns对应 APB1 周期数TRISE (1000ns × 36MHz) 1 37这些参数标准库的I2C_Init函数会处理但知道原理后遇到通信失败时就能判断是不是时钟配置错了。我一般还会把I2C_OwnAddress1设成 0x0A虽然作为主机时这个地址用不上但有些库函数会检查。3.3 超时机制防止总线锁死STM32F103 的硬件 I2C 有个已知问题在某些异常情况下比如从机在传输中途掉电SCL 或 SDA 可能被从机拉死导致总线锁死I2C_GenerateSTART一直等不到 SB 标志。解决办法有两个一是加超时计数二是用 GPIO 模拟时钟脉冲把从机“踢”醒。我在I2C_Start函数里加了超时#define I2C_TIMEOUT 10000 uint8_t I2C_Start(void) { uint32_t timeout I2C_TIMEOUT; I2C_GenerateSTART(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)) { if (--timeout 0) { I2C_GenerateSTOP(I2C1, ENABLE); return 1; // 超时 } } return 0; }这个超时值 10000 是我实测出来的在 72MHz 主频下大约对应 1ms 左右足够正常传输完成又不会等太久。如果超时了先发 STOP 释放总线然后返回错误码让上层处理。4. AT24C02 读写全流程拆解4.1 单字节写时序与写周期等待AT24C02 的单字节写流程是START → 发送器件地址0xA0→ 等待 ACK → 发送字节地址 → 等待 ACK → 发送数据 → 等待 ACK → STOP →等待写周期完成。最后这一步“等待写周期”是新手最容易漏掉的。AT24C02 在收到 STOP 后内部会启动一个写周期把数据从缓冲区写入 EEPROM 存储单元这个时间最大 5ms。在这 5ms 内芯片不会响应任何 I2C 命令如果你紧接着发下一个 START会收不到 ACK。正确的做法是发完 STOP 后延时 5ms或者用“应答查询”方式循环发送 START 器件地址直到收到 ACK 为止。我一般用延时简单可靠void AT24C02_WriteByte(uint8_t addr, uint8_t data) { I2C_Start(); I2C_Send7bitAddress(0xA0, I2C_Direction_Transmitter); I2C_SendData(addr); I2C_SendData(data); I2C_Stop(); Delay_ms(5); // 等待写周期 }4.2 页写8 字节边界处理AT24C02 的页大小是 8 字节。页写的意思是你可以一次性发送最多 8 个字节芯片内部地址会自动递增但不会跨页。如果你从地址 0x06 开始写 4 个字节实际写入的是 0x06、0x07、0x00、0x01——后两个字节回卷到了页首覆盖了原来的数据。这个坑我踩过。当时做一个参数存储从 0x06 开始写 4 字节读出来发现 0x00 和 0x01 的数据被改了查了半天才意识到是页回卷。所以页写函数必须做边界判断void AT24C02_WritePage(uint8_t addr, uint8_t *buf, uint8_t len) { uint8_t page_offset addr % 8; uint8_t first_len 8 - page_offset; if (first_len len) first_len len; // 写第一页剩余部分 I2C_Start(); I2C_Send7bitAddress(0xA0, I2C_Direction_Transmitter); I2C_SendData(addr); for (uint8_t i 0; i first_len; i) I2C_SendData(buf[i]); I2C_Stop(); Delay_ms(5); // 如果还有剩余继续写下一页 if (len first_len) { AT24C02_WritePage(addr first_len, buf first_len, len - first_len); } }这个递归写法简洁但要注意栈深度。实际项目中我改成循环避免递归层数不可控。4.3 连续读当前地址读与随机读AT24C02 的读操作分两种当前地址读和随机读。当前地址读是直接发读地址0xA1芯片从内部地址计数器当前位置开始输出数据。随机读是先发写地址0xA0加目标字节地址再发读地址0xA1然后读数据。连续读是在读模式下每收到一个字节后主机回 ACK芯片就继续输出下一个字节主机回 NACK 后发 STOP芯片停止输出。这里的关键是最后一个字节必须回 NACK否则芯片会一直输出总线无法正常结束。void AT24C02_ReadBuffer(uint8_t addr, uint8_t *buf, uint8_t len) { I2C_Start(); I2C_Send7bitAddress(0xA0, I2C_Direction_Transmitter); I2C_SendData(addr); I2C_Start(); // 重复起始 I2C_Send7bitAddress(0xA1, I2C_Direction_Receiver); for (uint8_t i 0; i len; i) { if (i len - 1) I2C_AcknowledgeConfig(I2C1, DISABLE); // 最后一个回NACK else I2C_AcknowledgeConfig(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); buf[i] I2C_ReceiveData(I2C1); } I2C_Stop(); I2C_AcknowledgeConfig(I2C1, ENABLE); // 恢复ACK }5. 常见问题与排查技巧实录5.1 通信失败速查表现象可能原因排查方法一直等不到 ACK器件地址错误用示波器看地址字节确认是 0xA0 还是 0xA1读出的数据全是 0xFF写周期未等待在写操作后加 5ms 延时偶发数据错乱上拉电阻过大换成 4.7kΩ检查总线电容总线锁死从机异常拉低加超时机制用 GPIO 模拟时钟恢复页写数据回卷跨页边界检查写入起始地址和长度分页处理高速下误码上升时间过长减小上拉电阻或降低 SCL 频率5.2 实操心得三个容易忽略的细节第一个细节是电源去耦。AT24C02 的 VCC 引脚旁边必须放一个 0.1μF 的陶瓷电容越近越好。我遇到过一批板子EEPROM 偶发写入失败最后发现是去耦电容离芯片太远写周期瞬间电流拉低了 VCC。换到 2mm 以内后问题消失。第二个细节是写保护引脚的处理。WP 引脚悬空时芯片内部有弱下拉但抗干扰能力差。我一般直接接地或者用 10kΩ 下拉。如果做参数存储建议用 GPIO 控制写入前拉低写完拉高。第三个细节是连续读的长度。AT24C02 内部地址是 8 位从 0xFF 继续读会回卷到 0x00。如果你读 256 字节正好读完一圈。但如果你读 300 字节后 44 字节会覆盖前面的数据。所以读缓冲区时长度不要超过 256 减去起始地址。5.3 用 Proteus 仿真验证时序在打板之前我习惯用 Proteus 跑一遍时序。Proteus 里有 AT24C02 模型和 STM32F103 模型把 I2C 引脚接上用虚拟示波器看 SCL 和 SDA 波形。重点看三个地方START 条件的建立时间、ACK 位的电平、STOP 条件的保持时间。仿真通过后再打板能省不少调试时间。不过 Proteus 的 AT24C02 模型写周期是瞬间完成的不会模拟 5ms 延时所以仿真通过不代表实际没问题。写周期延时必须在真实硬件上验证。6. 工程搭建与调试全流程6.1 Keil MDK 工程配置要点新建工程时器件选 STM32F103C8然后勾选 CMSIS 的 CORE 和 Device Startup。标准外设库我一般用 3.5.0 版本虽然老但稳定。添加文件时把stm32f10x_i2c.c、stm32f10x_gpio.c、stm32f10x_rcc.c加进来然后在stm32f10x_conf.h里把对应的头文件包含打开。调试器选 ST-LinkSWD 模式Reset 选 SYSRESETREQ。下载算法用 STM32F10x Med-density Flash。这些配置在热词里有人问 ST-Link 调试全流程其实关键就是 SWD 两根线SWCLK、SWDIO加上 GND 和 3.3V四根线就能跑。6.2 串口打印调试信息调试 I2C 的时候串口打印是少不了的。我一般用 USART1PA9 和 PA10波特率 115200。在关键步骤加打印比如printf(Write addr0x%02X data0x%02X\r\n, addr, data); printf(Read addr0x%02X data0x%02X\r\n, addr, buf[0]);如果读出的数据和写入的不一致先看地址对不对再看写周期有没有等够。我遇到过写入 0x55 读出 0xFF 的情况最后发现是 WP 引脚被误拉高了。6.3 用逻辑分析仪抓 I2C 波形逻辑分析仪是排查 I2C 问题的终极武器。我用的是 24MHz 采样率的廉价分析仪配合 PulseView 软件能直接解码 I2C 协议。抓到的波形会显示每个字节的地址、数据、ACK/NACK一眼就能看出问题在哪。比如有一次波形显示地址字节后没有 ACK但数据字节有 ACK。这说明器件地址错了但数据格式是对的。查代码发现是I2C_Send7bitAddress的参数写成了0xA1应该是0xA0。这种问题用串口打印很难发现但逻辑分析仪一抓就现形。7. 性能优化与扩展思路7.1 提高写入速度的两种方法AT24C02 的写周期最大 5ms这是物理限制没法绕过。但如果你要写 256 字节按单字节写需要 256 × 5ms 1.28 秒太慢了。用页写可以降到 32 × 5ms 160ms快了 8 倍。所以能用页写就用页写这是最直接的优化。另一种方法是“应答查询”代替固定延时。写周期完成后芯片会响应 ACK。你可以循环发 START 0xA0收到 ACK 就说明写完了立即进行下一步。这样平均等待时间从 5ms 降到 2-3ms整体速度又能提升 40% 左右。7.2 数据校验与磨损均衡EEPROM 的擦写寿命是 100 万次虽然听起来很多但如果你的代码每秒写一次不到 12 天就写坏了。所以关键数据不要频繁写或者做磨损均衡把数据分散到不同地址轮流写入。我一般还会加 CRC 校验。每个数据块后面跟一个 CRC16读取时校验不通过就重读或者报错。这样能及时发现数据损坏避免用错误参数运行。7.3 从 AT24C02 迁移到其他 EEPROM如果你以后要换 24C16、24C64底层 I2C 驱动不用动只需要改设备层的三个参数器件地址、页大小、总容量。24C16 的页大小是 16 字节24C64 是 32 字节地址位数也不同。把这些参数做成宏定义迁移时改宏就行。#define EEPROM_ADDR_W 0xA0 #define EEPROM_ADDR_R 0xA1 #define EEPROM_PAGE_SIZE 8 #define EEPROM_TOTAL_SIZE 256这个结构我用了好几年从 24C02 到 24C512 都跑过改几个宏就能适配省了很多重复劳动。8. 个人实操体会与建议这套代码我在三个量产项目里用过累计出货大概两万套左右。最深的体会是I2C 的问题90% 出在硬件上10% 出在软件上。上拉电阻、去耦电容、走线长度这些硬件细节比代码重要得多。我建议你在画 PCB 的时候I2C 走线尽量短远离高频信号线上拉电阻放在靠近主控的一端。另一个体会是超时机制必须有。不管你的代码写得多完美现场环境总有意外。从机掉电、总线短路、静电干扰都可能让 I2C 卡死。加了超时之后至少系统不会整个挂掉可以复位重试。最后分享一个小技巧如果你怀疑是 AT24C02 芯片本身的问题可以换一个品牌的芯片试试。不同厂家的 AT24C02 在写周期时间、ACK 响应速度上略有差异。我遇到过某批次芯片写周期要 6ms超过手册标称的 5ms延时 5ms 就不够。后来把延时改成 6ms问题解决。所以批量生产前最好多测几个批次的芯片留足余量。