
1. 为什么AT24C02是I2C入门的最佳陪练搞STM32的朋友圈里有个挺有意思的现象很多人能把GPIO点灯、串口打印玩得飞起一到I2C就卡壳。问就是时序太麻烦总线死锁搞不定读出来全是0xFF。其实问题不在I2C本身而在于选错了练手对象。你要是拿个OLED屏或者传感器来入门调不通的时候你根本分不清是I2C的问题还是器件本身的问题。AT24C02就不一样了它足够简单、足够便宜、足够皮实而且它的读写逻辑清晰到你可以用逻辑分析仪一帧一帧地看明白。AT24C02是一颗2Kbit256字节的EEPROM存储芯片走标准I2C协议7位从机地址固定为1010开头加上3位可配置的片选位所以一条总线上最多挂8片总容量能到2KB。它的页大小是8字节也就是说连续写入时不能跨页否则会回卷覆盖。这个页写回卷的坑几乎每个新手都会踩一次。我当年第一次用的时候往地址0x00连续写了16个字节读回来发现后8个字节把前8个覆盖了排查了半天才反应过来是页边界的问题。这颗芯片在实际项目里的定位很明确存配置参数、存校准数据、存设备序列号、存掉电前需要保留的状态。它不需要高速读写也不需要频繁擦写胜在掉电不丢数据、接口简单、成本极低。对于STM32F103这类资源有限的MCU来说用I2C挂一颗AT24C02比用内部Flash模拟EEPROM要省心得多——内部Flash擦写次数有限而且擦除粒度大操作起来束手束脚。这篇文章我会把AT24C02的读写全流程拆开讲从I2C时序的底层逻辑到STM32F103的硬件I2C配置再到实际读写代码的逐行分析最后重点讲那些文档里不会写、但实际调试中一定会遇到的坑。不管你是刚接触I2C的新手还是被总线死锁折磨过的老手应该都能从里面找到有用的东西。2. 先把I2C的时序逻辑吃透再动手写代码2.1 起始、停止与应答三根线撑起的通信框架I2C说到底就是两根线SCL时钟线和SDA数据线。所有通信都围绕三个基本动作展开——起始条件、停止条件、应答位。很多人急着写代码结果连起始条件到底是在SCL高电平还是低电平期间拉低SDA都搞不清楚后面调不通是必然的。起始条件的定义是SCL保持高电平期间SDA从高电平跳变到低电平。停止条件反过来SCL保持高电平期间SDA从低电平跳变到高电平。注意这里的关键词是SCL保持高电平期间也就是说SDA的跳变必须发生在SCL高电平的时候。如果SCL是低电平SDA随便怎么变都不算起始或停止。应答位ACK/NACK是每传输完8位数据后的第9个时钟周期。发送方把SDA释放为高电平接收方如果拉低SDA表示应答ACK保持高电平表示非应答NACK。主机在写操作时每发一个字节都要检测从机的应答在读操作时主机每读完一个字节要主动发送应答或非应答来控制读取结束。这里有个容易混淆的点主机读数据时最后一个字节必须发NACK然后发停止条件。如果你最后一个字节发了ACK从机会认为你还要继续读可能会一直占用总线。我见过有人读AT24C02时最后一个字节发了ACK结果下次通信直接失败就是因为从机还在等待下一个时钟。2.2 AT24C02的器件地址与读写位AT24C02的7位器件地址格式是1010 A2 A1 A0其中A2、A1、A0对应芯片的1、2、3脚电平。如果你只挂一片通常把这三个脚都接地地址就是1010000即0x50。加上读写位后写地址是0xA0读地址是0xA1。这里有个细节很多资料里写AT24C02地址是0xA0其实这是把读写位算进去的8位地址。在STM32的HAL库中你传给函数的地址参数通常是左移一位后的7位地址也就是0x50。HAL库内部会自动根据读写方向补上最低位。如果你直接传0xA0进去HAL库会再左移一位变成0x140超出7位范围通信必然失败。这个坑我在论坛上见过无数次新手最容易在这里翻车。2.3 字节写、页写与随机读的时序差异AT24C02有三种基本操作时序理解它们的差异比背代码重要得多。字节写的流程是起始条件 → 发送器件地址写→ 等待ACK → 发送字地址 → 等待ACK → 发送1字节数据 → 等待ACK → 停止条件。注意停止条件之后AT24C02会进入内部写周期大约5ms这段时间内它不会响应任何总线操作。如果你紧接着发下一个起始条件它会直接NACK。很多人在连续写多个字节时忘了加延时结果只有第一个字节写进去了。页写的流程和字节写几乎一样区别在于发送完第一个数据字节后不发停止条件而是继续发送后续数据字节。AT24C02的页大小是8字节如果你从地址0x00开始写最多能连续写8个字节到0x00~0x07。如果你写了9个字节第9个字节会回卷到0x00把第一个字节覆盖掉。这个回卷是硬件行为不会报错你只能通过读回来验证。随机读稍微复杂一点它是写地址读数据的组合操作起始条件 → 发送器件地址写→ 等待ACK → 发送字地址 → 等待ACK → 再次起始条件 → 发送器件地址读→ 等待ACK → 读取数据 → 发送NACK → 停止条件。注意这里用了两次起始条件中间没有停止条件这叫重复起始条件。它的作用是切换读写方向而不释放总线。2.4 上拉电阻与总线电容被忽视的硬件细节I2C总线是开漏输出必须外接上拉电阻。AT24C02的工作频率是100kHz标准模式或400kHz快速模式上拉电阻的取值直接影响信号上升沿。阻值太大上升沿变缓高速通信时数据采样出错阻值太小灌电流过大可能损坏器件。经验公式是上拉电阻最大值由总线电容和上升时间决定。标准模式下上升时间最大1000ns如果总线电容是200pF那么R t / (0.8473 × C) ≈ 1000ns / (0.8473 × 200pF) ≈ 5.9kΩ。所以4.7kΩ是一个很通用的选择。如果你挂的器件多、走线长总线电容增大就要适当减小上拉电阻比如降到2.2kΩ。我实际调试中遇到过一个诡异现象单独测试AT24C02读写正常但把OLED屏也挂到同一条总线上后EEPROM读写就开始随机失败。用示波器看波形发现SCL上升沿明显变缓从原来的1μs变成了3μs。换了2.2kΩ的上拉电阻后问题解决。所以如果你总线上挂了多个器件上拉电阻要重新算不能照搬单个器件的配置。3. STM32F103硬件I2C配置的取舍与实操3.1 硬件I2C还是软件模拟一个老生常谈的选择STM32F103的硬件I2C外设一直有个容易死锁的名声导致很多人宁愿用GPIO模拟。我的看法是硬件I2C在正常工作时效率更高、CPU占用更低但它的死锁问题确实存在主要出现在总线异常比如从机在传输中途掉电时。软件模拟I2C则完全可控出问题容易排查但占用CPU时间高速通信时时序精度依赖延时函数。如果你的项目对I2C速度要求不高100kHz足够而且总线上器件不多我建议用软件模拟。原因很简单调试成本低。硬件I2C死锁后你需要手动复位I2C外设、重新初始化甚至要模拟时钟脉冲来解锁总线。软件模拟的话你随时可以停下来看波形随时可以调整时序。但如果你要用400kHz快速模式或者总线上有多个器件需要频繁通信硬件I2C的优势就体现出来了。STM32F103的硬件I2C支持DMA可以做到零CPU干预的大块数据传输。所以选择哪种方式取决于你的具体需求。3.2 硬件I2C的GPIO配置与时钟使能以STM32F103C8T6为例I2C1的默认引脚是PB6SCL和PB7SDA。配置步骤如下// 使能GPIOB和I2C1时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); // 配置PB6、PB7为复用开漏输出 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_OD; // 复用开漏 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure);注意GPIO_Mode_AF_OD这个模式必须是复用开漏不能是推挽。推挽输出会导致总线冲突因为I2C是线与逻辑任何器件拉低都会把总线拉低。如果你配成推挽当STM32输出高电平时从机如果拉低总线就会形成短路长期下来可能损坏引脚。3.3 I2C外设初始化参数的计算过程I2C的时钟配置涉及几个关键参数时钟频率、占空比、上升时间。STM32F103的I2C时钟源是APB1默认36MHz。要得到100kHz的SCL频率需要设置时钟控制寄存器CCR。计算公式是CCR Fpclk1 / (2 × Fscl)。对于36MHz的APB1时钟和100kHz的SCLCCR 36000000 / (2 × 100000) 180。如果要用400kHz快速模式CCR 36000000 / (2 × 400000) 45。上升时间寄存器TRISE的计算是标准模式下TRISE Fpclk1 / 1000000 1 36 1 37快速模式下TRISE (Fpclk1 × 300) / 1000000000 1 10.8 1 ≈ 12。用HAL库的话这些计算它内部会帮你做你只需要填ClockSpeed参数hi2c1.Instance I2C1; hi2c1.Init.ClockSpeed 100000; // 100kHz hi2c1.Init.DutyCycle I2C_DUTYCYCLE_2; // 占空比2:1 hi2c1.Init.OwnAddress1 0; hi2c1.Init.AddressingMode I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode I2C_DUALADDRESS_DISABLE; hi2c1.Init.GeneralCallMode I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode I2C_NOSTRETCH_DISABLE; HAL_I2C_Init(hi2c1);NoStretchMode建议保持禁用也就是允许时钟拉伸。AT24C02在内部写周期会拉低SCL来暂停总线如果你禁用了时钟拉伸STM32会继续发时钟导致通信错乱。3.4 软件模拟I2C的延时精度控制如果你选择软件模拟延时函数是核心。100kHz的I2C意味着每个时钟周期10μs高电平和低电平各5μs。但实际延时不能刚好5μs因为GPIO操作本身有开销而且要考虑从机的建立时间和保持时间。我的做法是用__NOP()或者简单的for循环来产生微秒级延时然后用示波器实测SCL频率再调整循环次数。比如void I2C_Delay(void) { for(uint8_t i 0; i 10; i) // 72MHz下约5μs { __NOP(); } }这个循环次数不是固定的取决于你的系统时钟和编译器优化等级。我建议先用一个大概值然后用示波器看SCL波形如果频率偏高就增加循环次数偏低就减少。不要迷信网上的标准延时每个人的编译环境和时钟配置都不一样。4. AT24C02读写代码的逐层拆解4.1 字节写从起始条件到写周期等待先看字节写的完整实现基于软件模拟I2Cuint8_t AT24C02_WriteByte(uint8_t addr, uint8_t data) { I2C_Start(); I2C_SendByte(0xA0); // 器件地址写 if(I2C_WaitAck() ! 0) { // 等待应答 I2C_Stop(); return 1; // 器件无应答 } I2C_SendByte(addr); // 字地址 if(I2C_WaitAck() ! 0) { I2C_Stop(); return 2; } I2C_SendByte(data); // 数据 if(I2C_WaitAck() ! 0) { I2C_Stop(); return 3; } I2C_Stop(); HAL_Delay(5); // 等待内部写周期 return 0; }这段代码里最关键的是最后的HAL_Delay(5)。AT24C02的数据手册写的最大写周期时间是5ms但实际测试中大部分情况下2~3ms就完成了。如果你追求写入速度可以用应答查询Acknowledge Polling来替代固定延时发完停止条件后反复发送起始条件器件地址如果收到ACK说明写周期结束可以继续下一次操作。这样能把写入间隔从5ms缩短到实际需要的2~3ms。应答查询的实现void AT24C02_WaitWriteComplete(void) { do { I2C_Start(); I2C_SendByte(0xA0); } while(I2C_WaitAck() ! 0); // 直到收到ACK I2C_Stop(); }注意这个循环里每次都要发起始条件和停止条件不能只发地址。因为AT24C02在写周期内不响应任何总线活动你必须完整地发起一次传输尝试。4.2 页写8字节边界与回卷陷阱页写的代码和字节写几乎一样只是循环发送多个数据字节uint8_t AT24C02_WritePage(uint8_t addr, uint8_t *buf, uint8_t len) { // 检查是否跨页 if((addr / 8) ! ((addr len - 1) / 8)) { return 1; // 跨页拒绝写入 } I2C_Start(); I2C_SendByte(0xA0); if(I2C_WaitAck() ! 0) { I2C_Stop(); return 2; } I2C_SendByte(addr); if(I2C_WaitAck() ! 0) { I2C_Stop(); return 3; } for(uint8_t i 0; i len; i) { I2C_SendByte(buf[i]); if(I2C_WaitAck() ! 0) { I2C_Stop(); return 4; } } I2C_Stop(); HAL_Delay(5); return 0; }这里我加了一个跨页检查。如果你不检查从地址0x06开始写4个字节实际会写到0x06、0x07、0x00、0x01后两个字节把前面的数据覆盖了。这个检查逻辑是起始地址所在的页号addr / 8必须等于结束地址所在的页号(addr len - 1) / 8。如果你确实需要写超过8个字节必须分多次页写每次写完等待5ms。比如写16个字节到0x00~0x0F要分成两次先写0x00~0x07等待再写0x08~0x0F再等待。4.3 随机读重复起始条件的正确使用随机读的代码结构是写地址读数据uint8_t AT24C02_ReadByte(uint8_t addr, uint8_t *data) { I2C_Start(); I2C_SendByte(0xA0); // 器件地址写 if(I2C_WaitAck() ! 0) { I2C_Stop(); return 1; } I2C_SendByte(addr); // 字地址 if(I2C_WaitAck() ! 0) { I2C_Stop(); return 2; } I2C_Start(); // 重复起始条件 I2C_SendByte(0xA1); // 器件地址读 if(I2C_WaitAck() ! 0) { I2C_Stop(); return 3; } *data I2C_ReadByte(); // 读取数据 I2C_SendNack(); // 最后一个字节发NACK I2C_Stop(); return 0; }注意I2C_Start()在第二次调用时产生的是重复起始条件不是普通的起始条件。区别在于普通起始条件之前总线是空闲的SCL和SDA都是高重复起始条件之前总线是忙的刚刚完成一次传输。在代码实现上重复起始条件就是先把SDA拉高、SCL拉高然后拉低SDA和普通起始条件的波形是一样的但时序上下文不同。4.4 连续读用ACK控制读取长度连续读和随机读的区别在于读完一个字节后主机发ACK而不是NACK从机会自动递增地址并继续输出下一个字节uint8_t AT24C02_ReadBuffer(uint8_t addr, uint8_t *buf, uint8_t len) { I2C_Start(); I2C_SendByte(0xA0); if(I2C_WaitAck() ! 0) { I2C_Stop(); return 1; } I2C_SendByte(addr); if(I2C_WaitAck() ! 0) { I2C_Stop(); return 2; } I2C_Start(); I2C_SendByte(0xA1); if(I2C_WaitAck() ! 0) { I2C_Stop(); return 3; } for(uint8_t i 0; i len; i) { buf[i] I2C_ReadByte(); if(i len - 1) { I2C_SendAck(); // 不是最后一个字节发ACK } else { I2C_SendNack(); // 最后一个字节发NACK } } I2C_Stop(); return 0; }连续读的地址会自动递增读到0xFF后会回卷到0x00。这个特性可以用来一次性读取整个EEPROM的内容不需要手动计算地址。5. 调试中一定会遇到的五个坑与排查链路5.1 读出来全是0xFF从硬件到软件的逐层排查0xFF是I2C总线空闲时的默认电平读出来全是0xFF通常意味着从机根本没有应答。排查链路如下第一步用万用表测AT24C02的供电电压确保在2.5V~5.5V之间。我遇到过供电只有1.8V的情况芯片根本不工作。第二步测A0、A1、A2引脚的电平确认器件地址。如果这三个脚悬空地址是不确定的可能不是0x50。第三步测WP引脚写保护。如果WP接高电平写操作会被禁止但读操作应该正常。如果读写都不行WP不是原因。第四步用示波器看SCL和SDA波形。如果SCL没有波形说明STM32的I2C没有正确初始化如果SCL有波形但SDA一直是高说明从机没有应答可能是地址不对或者器件损坏。第五步检查上拉电阻。如果SDA和SCL在空闲时不是高电平说明上拉电阻缺失或阻值太大。5.2 总线死锁SCL被从机拉低的解锁方法总线死锁的典型现象是SCL或SDA被某个器件持续拉低STM32无法发起新的传输。这种情况通常发生在从机在传输中途复位或掉电时。解锁方法是把SCL配置为普通GPIO输出手动发送9个时钟脉冲然后发送一个停止条件。原理是从机可能在等待第9个时钟的应答位你补上时钟后它会完成当前字节的传输并释放总线。void I2C_BusRecovery(void) { // 配置SCL和SDA为普通GPIO GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_OD; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); // 发送9个时钟脉冲 for(uint8_t i 0; i 9; i) { GPIO_ResetBits(GPIOB, GPIO_Pin_6); // SCL低 DelayUs(5); GPIO_SetBits(GPIOB, GPIO_Pin_6); // SCL高 DelayUs(5); } // 发送停止条件 GPIO_ResetBits(GPIOB, GPIO_Pin_7); // SDA低 DelayUs(5); GPIO_SetBits(GPIOB, GPIO_Pin_6); // SCL高 DelayUs(5); GPIO_SetBits(GPIOB, GPIO_Pin_7); // SDA高 DelayUs(5); // 重新初始化I2C I2C_Init(); }这个恢复函数我建议放在I2C读写失败的重试逻辑里每次通信失败后先尝试恢复总线再重试。实测下来90%以上的死锁都能用这个方法解决。5.3 写进去读出来不一样写周期与页边界的双重检查如果你写了一个字节读回来发现不是写入的值先检查两个地方第一写完之后有没有等待足够的写周期时间。AT24C02的最大写周期是5ms如果你用HAL_Delay(1)可能还没写完就去读了读到的自然是旧数据或者随机值。第二写入地址是否跨页了。如果你用页写函数写了超过8个字节或者起始地址不是8的倍数数据会回卷覆盖。检查方法很简单把写入地址和长度打印出来手动算一下是否跨页。我遇到过一个更隐蔽的情况写入的数据是对的但读出来的地址偏移了一位。排查后发现是读函数里的地址参数传错了把addr写成了addr1。这种低级错误在代码量大的时候很容易发生建议在读写函数里加断言或者日志。5.4 连续写多个字节只有第一个生效这个问题的原因几乎可以肯定是写周期等待不足。AT24C02在每次写操作字节写或页写后都会进入内部写周期这段时间内它不响应总线。如果你在第一个字节写完后立即发起第二个字节的写操作从机会NACK你的代码如果没检查ACK就会以为写成功了实际上第二个字节根本没写进去。解决方法有两种一是每次写完后固定延时5ms二是用应答查询反复尝试直到收到ACK。我推荐应答查询因为它在大多数情况下能把等待时间缩短到2~3ms而且不依赖固定的延时参数。5.5 上电后第一次读写失败之后正常这个现象通常和上电时序有关。AT24C02的上电复位需要一定时间如果STM32在上电后立即发起I2C通信AT24C02可能还没准备好。解决方法是在初始化I2C之前加一个10ms~50ms的延时或者在上电后的第一次读写失败后重试。另一个可能的原因是STM32的I2C外设初始化顺序。如果你先初始化I2C再配置GPIOI2C外设可能会在GPIO还没配置好的时候就尝试输出信号导致总线状态异常。正确的顺序是先配置GPIO为复用开漏再使能I2C时钟最后初始化I2C外设。6. 从AT24C02延伸出去的实战经验6.1 用结构体封装读写接口在实际项目中我不会直接调用AT24C02_WriteByte和AT24C02_ReadByte而是封装一层结构体读写接口。比如我要存一个配置结构体typedef struct { uint16_t device_id; uint8_t brightness; uint8_t volume; uint32_t total_runtime; } DeviceConfig; uint8_t SaveConfig(DeviceConfig *cfg) { uint8_t buf[sizeof(DeviceConfig)]; memcpy(buf, cfg, sizeof(DeviceConfig)); // 分页写入 for(uint16_t i 0; i sizeof(DeviceConfig); i 8) { uint8_t len (sizeof(DeviceConfig) - i) 8 ? 8 : (sizeof(DeviceConfig) - i); if(AT24C02_WritePage(i, buf[i], len) ! 0) { return 1; } } return 0; }这样封装的好处是上层业务代码不需要关心EEPROM的页大小和写周期只需要调用SaveConfig和LoadConfig就行。如果以后换用更大的EEPROM或者Flash只需要改底层实现上层代码不动。6.2 数据校验别等到读出来才发现是乱码EEPROM虽然掉电不丢数据但也不是绝对可靠。强电磁干扰、供电波动、器件老化都可能导致数据位翻转。我在实际项目中会在存储的数据后面加一个CRC校验字节uint8_t SaveConfigWithCRC(DeviceConfig *cfg) { uint8_t buf[sizeof(DeviceConfig) 1]; memcpy(buf, cfg, sizeof(DeviceConfig)); buf[sizeof(DeviceConfig)] CalculateCRC8(buf, sizeof(DeviceConfig)); // 写入... } uint8_t LoadConfigWithCRC(DeviceConfig *cfg) { uint8_t buf[sizeof(DeviceConfig) 1]; // 读取... uint8_t crc CalculateCRC8(buf, sizeof(DeviceConfig)); if(crc ! buf[sizeof(DeviceConfig)]) { return 1; // 校验失败 } memcpy(cfg, buf, sizeof(DeviceConfig)); return 0; }校验失败时的处理策略取决于你的应用场景。如果是配置参数可以加载默认值如果是校准数据可能需要提示用户重新校准。6.3 磨损均衡让EEPROM活得更久AT24C02的擦写寿命是100万次看起来很多但如果你每秒写一次不到12天就用完了。对于需要频繁写入的场景比如记录运行时间必须做磨损均衡。最简单的磨损均衡是乒乓存储用两个区域交替写入每次写入时更新一个标志位读取时选择标志位最新的那个区域。这样每个区域的擦写次数减半寿命翻倍。更复杂的做法是把整个EEPROM分成多个块轮流使用。但AT24C02只有256字节空间有限乒乓存储已经够用了。6.4 逻辑分析仪的抓包解读调试I2C时逻辑分析仪是最有用的工具。我用的是某款8通道的逻辑分析仪配合开源软件解码I2C协议。抓包时要注意几点采样率至少要是SCL频率的10倍。100kHz的I2C采样率至少1MHz建议用4MHz以上这样波形细节更清晰。触发条件设置为SCL的下降沿或者SDA的跳变这样可以抓到完整的传输过程。解码后的数据要逐帧核对起始条件、器件地址、读写位、ACK/NACK、数据字节、停止条件。如果某一帧的ACK是NACK说明从机没有应答问题就出在这一帧。我遇到过一个案例逻辑分析仪显示器件地址是0xA0ACK正常但数据字节的ACK是NACK。排查后发现是AT24C02正在写周期内不响应数据字节。等5ms后重新抓包一切正常。6.5 从AT24C02迁移到其他I2C器件的注意事项如果你已经用AT24C02调通了I2C想迁移到其他器件比如AT24C256、OLED屏、传感器有几点需要注意AT24C256的页大小是64字节不是8字节。如果你直接套用AT24C02的页写代码跨页检查的逻辑要改。不同器件的器件地址不同。OLED屏通常是0x78或0x7A传感器可能是0x68、0x76等。地址错了一切白搭。有些器件不支持时钟拉伸有些支持。如果你的代码里禁用了时钟拉伸换到支持拉伸的器件上可能会出问题。读写时序可能不同。比如有些传感器需要先写寄存器地址再读数据和EEPROM的写地址读数据类似但细节有差异。换器件时一定要看数据手册的时序图不要凭经验套用。7. 写在最后一些零散但有用的经验AT24C02这颗芯片我用了很多年从51单片机到STM32从软件模拟到硬件I2C各种组合都试过。踩过的坑不少但每次解决问题后对I2C的理解都会深一层。如果你刚开始接触I2C我的建议是先用软件模拟把AT24C02读写跑通用逻辑分析仪看每一帧的数据确保你完全理解起始条件、应答位、重复起始条件这些概念。然后再切换到硬件I2C对比两者的差异。这个过程可能要多花一两天但基础打牢了后面调其他I2C器件会轻松很多。另外不要迷信标准代码。网上的AT24C02驱动代码很多但每个人的硬件环境、时钟配置、编译器优化都不一样。你拿过来直接用大概率会遇到延时不准、地址不对、上拉电阻不匹配的问题。最好的做法是理解原理后自己写一遍哪怕写得丑一点但每一行你都知道为什么这么写。最后说一个细节AT24C02的WP引脚写保护如果不用一定要接地不要悬空。悬空时引脚电平不确定可能导致写保护随机生效表现为有时候能写有时候不能写。这个坑很隐蔽因为万用表测悬空引脚可能是低电平但实际工作中会受到干扰。接地是最稳妥的做法。