资讯动态

STM32F103 I2C实战:AT24C02 EEPROM读写从入门到避坑

发布时间:2026/10/30 1:14:35 来源:尧图企业网站定制
1. 为什么AT24C02是I2C入门的最佳练手对象搞STM32的朋友大概率都经历过这个阶段GPIO点灯玩腻了串口打印也调通了想找个外设练练手结果一看SPI、CAN、USB这些协议寄存器一大堆直接劝退。这时候I2C加AT24C02的组合就特别适合拿来过渡——协议本身不算复杂AT24C02这颗芯片的逻辑又足够简单读写的全流程能跑通一遍基本上I2C的硬件层和协议层就都有感觉了。AT24C02是一颗2Kbit容量的EEPROM换算过来就是256字节页大小8字节I2C地址固定部分是1010后面三位由A2/A1/A0引脚决定。这颗芯片在市面上随处可见价格便宜到可以忽略不计而且它支持100kHz和400kHz两种速率宽电压范围1.8V到5.5V跟STM32F103的3.3V系统配合起来毫无压力。很多人拿它做参数存储、掉电保存、设备ID记录这类小数据量的持久化场景实际产品里用得非常多。这篇文章面向的是已经能跑通STM32F103最小系统、会用Keil MDK或者STM32CubeMX搭建工程、对GPIO和串口有一定了解的读者。我会从硬件连接开始把I2C的时序逻辑、AT24C02的读写协议、STM32F103的I2C外设配置、代码实现、常见问题排查这几个环节全部拆开讲一遍。不管你是用标准库还是HAL库核心逻辑都是通的区别只在于API的调用方式。注意AT24C02虽然简单但它的写周期时间tWR典型值是5ms最大不超过10ms。这个参数在连续写操作时如果不处理会直接导致数据写不进去后面我会详细说怎么规避。2. 硬件层最小系统、上拉电阻与地址配置2.1 STM32F103最小系统与AT24C02的接线方案先看硬件连接。STM32F103C8T6最小系统板上I2C1的默认引脚是PB6SCL和PB7SDA这两个引脚需要配置为复用开漏模式。AT24C02这边SCL和SDA分别接到PB6和PB7VCC接3.3VGND接地WP引脚接地表示允许写入如果接高电平就进入写保护状态只能读不能写。上拉电阻是必须的I2C总线是开漏输出没有上拉电阻的话总线根本拉不高。一般用4.7kΩ的电阻一端接3.3V另一端分别接SCL和SDA。有些模块板上已经集成了上拉电阻接线前先确认一下如果模块上已经有了就不需要额外再加否则并联之后阻值变小可能导致上升沿太陡反而引起信号完整性问题。A2/A1/A0三个地址引脚全部接地这时候AT24C02的7位I2C地址就是1010000即0x50。如果三个引脚都接VCC地址就是1010111即0x57。同一个I2C总线上最多可以挂8片AT24C02通过地址引脚区分这个设计在做多路参数存储的时候很方便。2.2 上拉电阻选型的计算依据上拉电阻的选型不是随便拍脑袋定的。I2C总线的上升时间由RC时间常数决定R是上拉电阻C是总线电容。标准模式100kHz下上升时间要求不超过1000ns快速模式400kHz下上升时间不超过300ns。总线电容一般估算在100pF到200pF之间取200pF来算快速模式下RC要小于300nsR就要小于1.5kΩ。但电阻太小又会导致功耗增加低电平时的灌电流也会变大。实际工程中4.7kΩ是最常用的折中值100kHz下完全够用400kHz下如果总线电容不大也能跑。如果通信不稳定可以试着降到2.2kΩ。我实测下来在面包板上搭电路线长不超过10cm的情况下4.7kΩ跑400kHz基本没问题。但如果用杜邦线拉得比较长电容增大就得降到2.2kΩ甚至1kΩ。提示如果你用的是STM32F103最小系统板加AT24C02模块的组合先拿万用表量一下模块上SCL和SDA对VCC的阻值确认有没有板载上拉。有的话就不要再外接了。2.3 地址引脚配置与多设备挂载AT24C02的7位地址格式是1 0 1 0 A2 A1 A0最低位是读写位。写操作时发送0xA00x50左移一位读操作时发送0xA1。这个地址在代码里通常写成0xA0和0xA1或者用宏定义区分。如果总线上挂多片AT24C02比如两片一片A0接地地址是0x50另一片A0接VCC地址是0x51。代码里定义两个设备地址宏读写的时候切换就行。注意AT24C02的地址引脚不能悬空必须明确接VCC或GND悬空会导致地址不确定通信失败。3. I2C协议层时序逻辑与AT24C02的读写规则3.1 I2C起始、停止与应答的时序本质I2C协议的核心就是两根线配合出来的几种特定波形。起始条件START是SCL为高电平时SDA从高变低停止条件STOP是SCL为高电平时SDA从低变高。这两个条件必须由主机产生从机只负责检测。数据传输的时候SCL为高电平期间SDA必须保持稳定数据在SCL低电平期间变化。每传输8位数据后第9个时钟周期主机释放SDA从机拉低SDA表示应答ACK保持高电平表示非应答NACK。主机读数据的时候读完最后一个字节要发送NACK然后发停止条件。这些时序在STM32F103的硬件I2C外设里是自动处理的你只需要调用发送和接收函数硬件会帮你生成正确的波形。但如果你用GPIO模拟I2C就得手动控制引脚的电平变化每个时钟周期都要精确控制。模拟的好处是引脚灵活不受硬件外设限制缺点是占用CPU时间高速通信时容易出问题。3.2 AT24C02的字节写与页写流程AT24C02的写操作分两种字节写和页写。字节写的流程是起始条件 → 发送设备地址写→ 等待ACK → 发送字地址 → 等待ACK → 发送数据字节 → 等待ACK → 停止条件。然后芯片进入内部写周期大约5ms后才能响应下一次操作。页写是一次性写入最多8个字节流程类似但发送完第一个数据字节后不发送停止条件而是继续发送后续数据字节直到发完或者凑满一页。AT24C02的页大小是8字节如果跨页写地址会自动回卷到当前页的开头覆盖之前的数据。比如从地址0x07开始写8个字节第1个字节写到0x07第2个字节写到0x00第3个写到0x01以此类推。这个回卷特性很容易踩坑写跨页数据的时候必须手动分页。3.3 当前地址读、随机读与顺序读的区别读操作比写操作稍微复杂一点分三种模式。当前地址读是直接发送设备地址读然后接收数据地址指针是上次操作后的位置。随机读是先发送设备地址写再发送要读的字地址然后重新发送起始条件发送设备地址读最后接收数据。顺序读是在随机读的基础上连续接收多个字节每接收一个字节主机发送ACK接收最后一个字节时发送NACK。实际用的时候随机读是最常用的因为每次都要指定读取的地址。顺序读适合批量读取连续地址的数据比如一次读出一整页。注意读操作不需要等待写周期可以连续进行。4. STM32F103的I2C外设配置与代码实现4.1 硬件I2C与软件模拟I2C的选型对比STM32F103的硬件I2C外设一直有个“不太好用”的名声早期标准库的I2C驱动确实存在一些死锁问题尤其是在总线被拉低或者从机不响应的时候容易卡住。但HAL库出来之后配合CubeMX生成的代码硬件I2C的稳定性好了很多。我实测在F103C8T6上用HAL库跑400kHz读写AT24C02连续几千次没有出现死锁。硬件I2C的优点是速度快、CPU占用低、时序精确缺点是引脚固定调试的时候如果出问题不太好定位。软件模拟I2C的优点是引脚随便选、时序完全可控、调试方便缺点是占用CPU、速度受限、代码量大。如果你只是读写AT24C02这种低速设备两种方式都行。如果总线上还有其他高速设备建议用硬件I2C。对比项硬件I2C软件模拟I2C速度最高400kHz通常100kHz以下CPU占用低高引脚灵活性固定任意GPIO调试难度较高较低代码量少多稳定性HAL库下良好取决于延时精度4.2 CubeMX配置I2C1的关键参数用CubeMX配置I2C1的时候几个关键参数需要留意。I2C Speed Mode选Standard或者Fast对应100kHz和400kHz。Clock Speed根据实际需求填一般填400000。Duty Cycle在快速模式下选2:1或者16:92:1适合总线电容小的情况16:9适合电容大的情况。Analog Filter和Digital Filter都使能可以滤掉一些毛刺。GPIO设置里PB6和PB7自动配置为复用开漏上拉电阻选择Very High或者外部上拉。如果模块上已经有上拉这里选No Pull。NVIC里不需要开I2C中断轮询方式就够了。生成代码后HAL库会初始化I2C外设你只需要调用HAL_I2C_Mem_Write和HAL_I2C_Mem_Read这两个函数就能完成读写。4.3 字节写、页写与随机读的代码实现先定义设备地址和超时时间#define AT24C02_ADDR_W 0xA0 #define AT24C02_ADDR_R 0xA1 #define AT24C02_TIMEOUT 100字节写函数uint8_t AT24C02_WriteByte(uint16_t addr, uint8_t data) { return HAL_I2C_Mem_Write(hi2c1, AT24C02_ADDR_W, addr, I2C_MEMADD_SIZE_8BIT, data, 1, AT24C02_TIMEOUT); }页写函数注意分页处理uint8_t AT24C02_WritePage(uint16_t addr, uint8_t *data, uint8_t len) { uint8_t page_offset addr % 8; if (page_offset len 8) { len 8 - page_offset; } return HAL_I2C_Mem_Write(hi2c1, AT24C02_ADDR_W, addr, I2C_MEMADD_SIZE_8BIT, data, len, AT24C02_TIMEOUT); }随机读函数uint8_t AT24C02_ReadByte(uint16_t addr, uint8_t *data) { return HAL_I2C_Mem_Read(hi2c1, AT24C02_ADDR_R, addr, I2C_MEMADD_SIZE_8BIT, data, 1, AT24C02_TIMEOUT); }顺序读函数uint8_t AT24C02_ReadBuffer(uint16_t addr, uint8_t *buf, uint16_t len) { return HAL_I2C_Mem_Read(hi2c1, AT24C02_ADDR_R, addr, I2C_MEMADD_SIZE_8BIT, buf, len, AT24C02_TIMEOUT); }写完一个字节后必须延时至少5ms才能进行下一次写操作这个延时可以用HAL_Delay实现也可以用一个循环等待。读操作不需要延时。4.4 写周期等待与超时处理的实现细节写周期等待是AT24C02驱动里最容易忽略的地方。芯片在收到停止条件后开始内部写这段时间内不会响应任何I2C命令。如果你紧接着发下一个写命令芯片不会应答HAL库会返回HAL_ERROR或者HAL_TIMEOUT。正确的做法是每次写操作后延时5ms到10ms。如果觉得固定延时浪费时间可以用应答查询的方式发送起始条件加设备地址如果收到ACK说明写周期结束可以继续操作如果收到NACK就继续等待。这种方式比固定延时效率高但代码复杂一些。void AT24C02_WaitReady(void) { uint32_t timeout 0; while (HAL_I2C_IsDeviceReady(hi2c1, AT24C02_ADDR_W, 1, 100) ! HAL_OK) { timeout; if (timeout 100) break; } }HAL_I2C_IsDeviceReady这个函数就是专门用来做应答查询的内部会发送设备地址并检测ACK。用这个函数替代固定延时代码更健壮。5. 实操验证从写入到读出的完整流程5.1 测试用例设计与数据校验方法验证AT24C02读写是否正常我一般分三步走。第一步写一个字节到地址0x00延时后读回来对比。第二步写一串数据到地址0x10开始的位置比如0x01到0x08共8个字节正好一页读回来对比。第三步跨页写从地址0x05开始写10个字节验证分页逻辑是否正确。数据校验用简单的memcmp就行如果读回来的数据和写入的不一致就打印出具体哪个地址对不上。我习惯在串口里输出十六进制的原始数据方便对比。uint8_t write_buf[8] {0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88}; uint8_t read_buf[8] {0}; AT24C02_WritePage(0x10, write_buf, 8); AT24C02_WaitReady(); AT24C02_ReadBuffer(0x10, read_buf, 8); if (memcmp(write_buf, read_buf, 8) 0) { printf(Page write/read OK\r\n); } else { printf(Data mismatch!\r\n); }5.2 用逻辑分析仪抓I2C波形验证时序代码跑通了不代表时序没问题。我强烈建议用逻辑分析仪抓一下SCL和SDA的波形确认起始条件、地址字节、ACK位、数据字节、停止条件都符合预期。特别是ACK位如果从机没有拉低SDA说明地址不对或者芯片没响应。逻辑分析仪上看到的现象应该是起始条件后SCL出现8个时钟脉冲对应地址字节第9个脉冲时SDA被从机拉低。然后又是8个脉冲对应字地址第9个脉冲ACK。接着8个脉冲对应数据第9个脉冲ACK。最后停止条件。整个波形的SCL高电平期间SDA保持稳定低电平期间SDA变化。如果发现SCL被拉低不释放大概率是总线死锁需要重新初始化I2C外设或者手动发送9个时钟脉冲解锁。如果发现ACK位是高电平说明从机没应答检查设备地址和硬件连接。5.3 连续读写压力测试与数据保持验证单次读写通过之后我会跑一个压力测试连续写256个字节每个字节写入后读回校验循环100次。这个测试能暴露写周期等待不充分、地址回卷、总线死锁等问题。我实测下来如果每次写后不加延时大概写到第3个字节就会失败。加上HAL_I2C_IsDeviceReady等待后连续跑1000次没有出错。数据保持验证是断电后重新上电读取之前写入的数据确认没有丢失。AT24C02的数据保持时间标称100年实际使用中只要不超出擦写次数100万次数据可靠性很高。我有一块测试板写了数据放了两年多重新上电读出来还是对的。6. 常见问题排查与避坑经验6.1 通信失败从地址、上拉、时序三个方向排查I2C通信失败是最常见的问题排查顺序我一般是从地址开始。先用HAL_I2C_IsDeviceReady扫描0x50到0x57这8个地址看哪个能应答。如果都不应答检查硬件连接VCC有没有电、GND有没有共地、SCL和SDA有没有接反、上拉电阻有没有装。地址对了但通信不稳定重点查上拉电阻和线长。杜邦线太长、上拉电阻太大、总线电容过大都会导致波形上升沿变缓高速通信时数据出错。把速率降到100kHz试试如果降速后正常说明是信号完整性问题。时序问题一般出在软件模拟I2C上延时不够或者延时太长都会导致时序不满足。用逻辑分析仪抓波形对照AT24C02的数据手册看时序参数逐个调整。6.2 写入无效写周期等待与WP引脚的检查写入无效分两种情况一种是完全写不进去读出来全是0xFF或者0x00另一种是偶尔写失败大部分时候正常。完全写不进去先查WP引脚如果WP接高电平芯片处于写保护状态所有写操作都会被忽略。把WP接地再试。偶尔写失败大概率是写周期等待不够。AT24C02的tWR最大10ms如果你只延时了1ms芯片还没写完你就发下一个命令肯定失败。把延时加到10ms或者用HAL_I2C_IsDeviceReady做应答查询。还有一种情况是页写跨页导致数据被覆盖。比如你从地址0x07开始写8个字节第1个字节写到0x07后面的字节回卷到0x00到0x06把之前的数据覆盖了。写之前先算一下当前地址到页边界的剩余空间不够一页就分两次写。6.3 总线死锁SCL被拉低不释放的恢复方法总线死锁的表现是SCL一直为低电平I2C外设无法产生起始条件。这种情况通常发生在从机正在发送数据时主机突然复位从机还在等待时钟脉冲把SDA拉低不放。恢复方法是把SCL配置为GPIO输出手动发送9个时钟脉冲让从机把剩下的数据发完然后发送停止条件。void I2C_BusRecovery(void) { GPIO_InitTypeDef gpio {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); gpio.Pin GPIO_PIN_6 | GPIO_PIN_7; gpio.Mode GPIO_MODE_OUTPUT_OD; gpio.Pull GPIO_PULLUP; gpio.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, gpio); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET); for (int i 0; i 9; i) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); HAL_Delay(1); } HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET); HAL_Delay(1); MX_I2C1_Init(); }这段代码先把SCL和SDA切成普通GPIO手动发9个时钟再补一个停止条件最后重新初始化I2C外设。实测下来大部分死锁都能用这个方法恢复。6.4 常见问题速查表现象可能原因排查方法解决措施设备无应答地址错误扫描0x50-0x57确认A2/A1/A0引脚设备无应答硬件连接问题量VCC、GND、SCL、SDA重新接线检查上拉写入后读出为0xFFWP引脚拉高量WP引脚电平WP接地偶尔写失败写周期等待不足增加延时到10ms用IsDeviceReady查询跨页数据错乱页回卷检查写入地址和长度分页写入SCL被拉低总线死锁量SCL电平手动发9个时钟恢复高速通信出错上拉电阻过大抓波形看上升沿减小上拉电阻数据偶尔跳变电源噪声示波器看VCC纹波加去耦电容提示AT24C02的VCC引脚旁边一定要加一个0.1uF的去耦电容位置尽量靠近芯片。这个电容能滤掉电源上的高频噪声对数据可靠性影响很大。我遇到过好几次数据偶尔出错的情况最后都是去耦电容没加或者容值不对导致的。7. 从AT24C02延伸出去的几个实用方向AT24C02跑通之后I2C的基本操作就没什么障碍了。接下来可以拿其他I2C设备练手比如OLED屏幕SSD1306驱动、温湿度传感器SHT30、AHT20、气压计BMP280、实时时钟DS3231等等。这些设备的读写逻辑跟AT24C02大同小异区别在于寄存器地址和数据格式。如果你要做参数存储AT24C02的256字节可能不够用可以换成AT24C32、AT24C64这些容量更大的型号地址格式从单字节变成双字节代码里把I2C_MEMADD_SIZE_8BIT改成I2C_MEMADD_SIZE_16BIT就行。再大一点可以用AT24C25632KB容量足够存不少配置数据。软件模拟I2C的代码也值得自己写一遍不依赖HAL库纯GPIO操作。写完之后你对I2C时序的理解会深很多以后遇到任何I2C设备都能快速上手。模拟I2C的延时可以用DWT计数器或者SysTick来实现微秒级精度比HAL_Delay的毫秒级精度更适合高速通信。最后分享一个我在实际项目中用到的小技巧把AT24C02的读写封装成带CRC校验的接口写入的时候计算CRC一起存进去读取的时候校验CRC。这样即使偶尔出现位翻转也能及时发现并重写。CRC用简单的累加和或者异或校验就行不需要上CRC16那么复杂。这个做法在工业现场设备里很实用能避免因为EEPROM数据出错导致的异常行为。

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

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

免费获取报价 →
↑