ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32软件模拟I2C读写AT24C02:时序详解与调试经验

STM32软件模拟I2C读写AT24C02:时序详解与调试经验 1. 项目概述为什么从 AT24C02 切入 I2C做嵌入式开发I2C 是绕不开的坎。屏幕、传感器、存储芯片几乎每个项目里都至少挂着一两个 I2C 设备。而 AT24C02 这块 EEPROM可以说是学习 I2C 协议最标准的练习板——它结构简单、逻辑清晰、时序直观把 I2C 的起始、停止、应答、读写时序全覆盖了。我当年调通这块芯片之后再去看 OLED、温湿度传感器这类 I2C 器件基本是降维打击。这个项目围绕 STM32F103 和 AT24C02 展开核心就一件事用 I2C 总线把数据写进 EEPROM再读出来验证完整流程。适合三类人刚入门 STM32、被 I2C 时序搞到头疼的新手想搞清楚软件模拟 I2C 和硬件 I2C 区别的进阶者以及做项目需要掉电保存参数、但不想上 Flash 文件系统的开发者。无论你是用最小系统板、带着 5V 转 3.3V 电路的小模块还是直接在 Proteus 里做仿真这套流程都能直接搬。先泼一盆冷水STM32F103 的硬件 I2C 外设在圈子里口碑不太好经典问题是总线卡死、时钟线被拉低后无法释放。我见过不少人在这里卡了一两周。所以这个项目我采用的是软件模拟 I2CGPIO 翻转方案。不是说硬件 I2C 不能用新版本的库已经解决了很多问题但作为教学和实际项目软件模拟的稳定性和可控性是无可替代的——出了问题你能清楚看到每一步时序而硬件 I2C 一旦卡住排查起来比较痛苦。2. 硬件准备与选型思路2.1 所需材料清单与连接方案我这次用的是一块 STM32F103C8T6 最小系统板配合独立的 AT24C02 模块。除了这两个核心器件还需要准备3.3V 稳压电路如果用 5V 供电的话、两个 4.7kΩ 上拉电阻、杜邦线若干、USB 转 TTL 或者 ST-Link 调试器。接线表非常关键直接照着来AT24C02 引脚STM32F103 引脚说明VCC3.3V芯片供电注意不要接 5VGNDGND共地WPGND写保护关允许写入SCLPB6I2C 时钟线软件模拟时用 GPIO 控制SDAPB7I2C 数据线软件模拟时双向切换A0/A1/A2GND地址引脚全部接地决定 I2C 设备地址这是最常用的连接方式。A0/A1/A2 全部接 GND 的话AT24C02 的 7 位地址是 0x50加上读写位后写地址是 0xA0读地址是 0xA1。如果你想把多个 EEPROM 挂在同一根 I2C 总线上就靠这三个引脚来区分地址比如 A0 接 3.3V 后地址变成 0x51写地址 0xA2。2.2 上拉电阻选择的门道我见过太多人把 I2C 调不出来最后发现是上拉电阻压根没接。I2C 总线是开漏结构SCL 和 SDA 必须接上拉电阻才能输出高电平。AT24C02 模块上一般都自带了上拉电阻但如果你用裸芯片自己搭电路这个问题必须自己处理。阻值选多少4.7kΩ 是最通用的选择适用于 100kHz 标准模式和 400kHz 快速模式。阻值偏大比如 10kΩ会让上升沿变缓总线电容大的时候波形会很难看阻值偏小比如 1kΩ会增大功耗而且在 3.3V 系统下灌电流会偏大。我之前有一次 400kHz 快速模式下读出来的数据偶尔出错排查了半天最后把 10kΩ 上拉换成 4.7kΩ 就好了。如果你走线特别长、挂载设备多可以适当减小到 2.2kΩ但不要低于 1kΩ。2.3 关于 5V 转 3.3V 电路的补充热词里提到stm32f103 5v转3.3v电路这里展开说一下。STM32F103 的 GPIO 耐压是 5V 容忍的但 AT24C02 的 VCC 必须接 3.3V。很多人用 5V 给板子供电然后直接从 5V 引脚去给芯片供电这是不对的——AT24C02 数据手册上最大额定电压通常是 5.5V但芯片内部的逻辑阈值是按照 3.3V 或者更低设计的长期工作在 5V 下会严重影响寿命和稳定性。正确的做法是5V 输入后经过 AMS1117-3.3 稳压到 3.3V给 STM32F103 和 AT24C02 同时供电。如果你的最小系统板自带稳压电路直接用板上的 3.3V 输出引脚给 AT24C02 供电就行。I2C 总线上电平以 3.3V 为基准这样 STM32 和 EEPROM 之间通信电平是匹配的不需要额外加电平转换。2.4 Proteus 仿真时的注意事项如果你用的是 Proteus 仿真而不是实物AT24C02 器件在仿真库里要注意版本差异。Proteus 里有些版本的 AT24C02 模型对 WP 引脚的处理和实际芯片有出入仿真时建议把 WP 直接接地不要悬空。另外仿真环境下上拉电阻也是必须的Proteus 默认不会帮你自动加上拉。我在仿真里踩过一次坑波形明明都对但 ACK 就是不出来最后发现是上拉电阻没加、高电平根本没拉上去。3. I2C 协议核心拆解读懂了时序就成功了一半3.1 为什么软件模拟 I2C 更适合入门先回答一个很多人纠结的问题STM32F103 明明有硬件 I2C 外设为什么要用 GPIO 软件模拟两方面的原因。第一是硬件 I2C 的实际体验问题。I2C 外设的状态机比较复杂事件标志多配置不当容易出现总线忙标志无法清除的怪毛病。网上关于 STM32 硬件 I2C 卡死的帖子一抓一大把有相当一部分是芯片的硬件 bug 导致的。第二是学习和排查的便利性。软件模拟 I2C 时每一根线的翻转、每一个电平的采样都在你的掌控之中你可以用示波器或者逻辑分析仪逐个节点比对时刻知道协议走到哪一步了。这对理解 I2C 协议本身帮助很大。当然项目量产或者对 CPU 占用有严格要求的场景硬件 I2C 还是更好的选择——它不占用 CPU效率更高配合 DMA 可以做到全自动传输。但在这个教学项目里软件模拟是更稳妥的路径。3.2 起始、停止、数据有效性——三个最核心的时序概念I2C 的时序规则其实只有三条理解了这三条后面所有代码都是围绕它们展开的。起始条件STARTSCL 保持高电平期间SDA 从高电平跳变到低电平。这个下降沿告诉总线上所有从机主机要开始通信了。注意时序要求SDA 变化必须在 SCL 为高的时候进行这是 I2C 总线最关键的规则。停止条件STOPSCL 保持高电平期间SDA 从低电平跳变到高电平。这个上升沿告诉所有从机主机结束通信总线释放。数据有效性SDA 上的数据必须在 SCL 为高电平期间保持稳定SCL 为低电平期间才允许 SDA 变化。换句话说数据是在 SCL 低电平期间切换、高电平期间被采样的。每一 bit 数据对应一个 SCL 高电平周期8 bit 数据后跟着一个 ACK 时钟周期。用生活里的话打个比方SCL 就像老师喊1、2、3的节拍SDA 是学生举手。老师只能在举手期间看学生数据采样学生只能在老师没看的时候换动作数据变化。3.3 从机地址、写地址、读地址——别把 0xA0 和 0xA1 搞混AT24C02 的 I2C 从机地址是一个 8 位字节。高 4 位固定为 1010接下来 3 位是 A0/A1/A2 引脚的电平状态最低位是读写标志位0 表示写1 表示读。三个地址引脚全接地的情况下7 位地址是 0x50二进制 1010000。加上写标志位 0就变成 0xA0加上读标志位 1变成 0xA1。所有 I2C 操作的第一步都是先发送这个地址字节。发送地址后要等待从机的 ACK 应答如果没等到说明从机不在总线上、地址错误或者芯片没上电。有个容易搞混的细节这里说的地址是 8 位字节而你在代码里定义的寄存器地址AT24C02 的存储地址是另一个概念。比如你要读地址 0x10 处的数据流程是起始条件 - 发送 0xA0写地址- 发送 0x10存储地址- 重新发送起始条件 - 发送 0xA1读地址- 读取数据。很多人把第一行的 0xA0 和第二行的存储地址搞混导致读了半天都是 0xFF。3.4 ACK/NACK 应答机制通信双方的握手协议ACK 是 I2C 协议的精髓。每传输完 8 位数据后第 9 个时钟周期由接收方来控制 SDA 电平接收方拉低 SDA 表示 ACK应答成功释放 SDA 保持高电平表示 NACK不应答。主机往从机写数据时每个字节写入后都要检查从机是否返回 ACK。从机返回 ACK 的含义是我已经成功收到这个字节可以发下一个。如果在写设备地址后就收到 NACK说明地址不对或者芯片不在线如果写数据字节后收到 NACK说明写保护打开了WP 引脚接了高电平。主机从从机读数据时角色对调。主机在收完最后一个数据字节后要主动返回一个 NACK告诉从机我不要再读了别发数据了然后产生停止条件。如果你忘记发这个 NACK从机会一直把总线数据线拽住不放后续通信会乱掉。4. 代码实现软件模拟 I2C 的完整框架4.1 GPIO 初始化这两个引脚怎么配置是关键软件模拟 I2C 的第一步是把 PB6SCL和 PB7SDA配置成正确的模式。我的做法是都用开漏输出模式。void I2C_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // SCL - PB6 开漏输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_OD; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); // SDA - PB7 开漏输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_OD; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); // 初始状态SCL 和 SDA 都拉高 SCL_H; SDA_H; }开漏输出的妙处在于当我们需要 SDA 变高时STM32 内部不需要驱动高电平而是把输出锁存器置 1、让引脚悬空由外部上拉电阻把电平拉上去。这样从一个主机控制的总线接口来看天然符合 I2C 开漏的电气要求。如果你用推挽输出就要注意读 SDA 输入状态时需要切换方向操作不对容易烧芯片或者读不到正确的电平。宏定义这部分方便后面代码阅读#define SCL_H GPIOB-BSRR GPIO_Pin_6 #define SCL_L GPIOB-BRR GPIO_Pin_6 #define SDA_H GPIOB-BSRR GPIO_Pin_7 #define SDA_L GPIOB-BRR GPIO_Pin_7 #define SDA_READ (GPIOB-IDR GPIO_Pin_7)注意 SDA 读取用的是 IDR输入数据寄存器而不是 ODR。开漏模式下引脚状态就是电平状态的直接反映。4.2 起始、停止、发送字节、接收字节——底层时序函数这部分是整个 I2C 软件模拟的核心。我直接给出我调试通过的完整代码注释里标注了每步的时序意图。// 起始条件SCL高电平时SDA产生下降沿 void I2C_Start(void) { SDA_H; SCL_H; delay_us(5); // 保持建立时间 SDA_L; // SCL高时SDA下降沿 START delay_us(5); SCL_L; // 拉低SCL准备传输数据 } // 停止条件SCL高电平时SDA产生上升沿 void I2C_Stop(void) { SDA_L; SCL_H; delay_us(5); SDA_H; // SCL高时SDA上升沿 STOP delay_us(5); } // 主机发送一个字节 void I2C_SendByte(uint8_t byte) { uint8_t i; for (i 0; i 8; i) { // 先判断这一位是0还是1然后设置SDA if (byte 0x80) SDA_H; else SDA_L; delay_us(3); SCL_H; // SCL拉高从机采样数据 delay_us(5); SCL_L; // SCL拉低允许SDA变化 delay_us(3); byte 1; } } // 等待从机ACK应答 uint8_t I2C_WaitACK(void) { uint8_t ack 0; SDA_H; // 释放SDA让从机控制 delay_us(3); SCL_H; // 第9个时钟周期 delay_us(5); if (SDA_READ) // SDA为高从机没应答(NACK) ack 0; else // SDA为低从机应答(ACK) ack 1; SCL_L; delay_us(3); return ack; } // 主机接收一个字节参数 ack_flag1发送ACK 0发送NACK uint8_t I2C_RecvByte(uint8_t ack_flag) { uint8_t i, data 0; SDA_H; // 释放SDA准备接收 for (i 0; i 8; i) { SCL_H; delay_us(5); data 1; if (SDA_READ) data | 0x01; SCL_L; delay_us(5); } // 第9个时钟主机发送ACK或NACK if (ack_flag) SDA_L; // ACK else SDA_H; // NACK delay_us(3); SCL_H; delay_us(5); SCL_L; delay_us(3); SDA_H; return data; }每个函数之间要有延时。延时时间取多少标准模式 100kHz 下SCL 半周期至少 5µs即时钟频率不能高于 100kHz我习惯在每次翻转后加 3~5µs这样实际频率大约 80~90kHz留足了边沿余量。如果你追求速度可以把延时缩短到 2µs 级别但第一次调试时宁可慢一点先把功能跑通。4.3 向 AT24C02 写数据字节写与页写AT24C02 的存储容量是 2Kbit也就是 256 字节。写操作分两种字节写和页写。字节写向指定地址写入 1 个字节。流程是起始 - 设备写地址(0xA0) - 存储地址(0~255) - 数据字节 - 停止。每一步之间都要等待 ACK。// 向指定地址写一个字节 uint8_t AT24C02_WriteByte(uint8_t addr, uint8_t data) { I2C_Start(); if (I2C_WaitACK() 0) return 0; // 发送设备写地址0xA0并应答 I2C_SendByte(addr); // 发送存储地址 if (I2C_WaitACK() 0) return 0; I2C_SendByte(data); // 发送要写入的数据 if (I2C_WaitACK() 0) return 0; I2C_Stop(); delay_ms(10); // 等待写周期见下一节 return 1; }页写AT24C02 内部以 8 字节为一页一次可以连续写 1~8 个字节条件是不能跨页。如果写入的字节数超过当前页剩余空间数据会自动绕回到页首覆盖前面的数据——这是 AT24C02 最容易暗坑的地方。比如你从地址 0x06 开始连续写 4 个字节写入过程不会报错但 0x06 和 0x07 两个地址写完后剩余两个字节会写到 0x00 和 0x01把之前的数据覆盖掉。正确的处理方式有两种要么写之前先判断剩余页空间超过页边界就拆分成两次写入要么干脆不用页写一次一个字节简单可靠。我建议新手时期老老实实用字节写等调通之后再尝试页写优化。4.4 写周期等待最容易翻车的 5ms每完成一次写操作无论字节写还是页写AT24C02 内部都要进行擦写操作。数据手册规定这个写周期Write Cycle Time典型值是 5ms最大可能到 10ms。在这段时间内芯片不会响应任何 I2C 指令。两种等待方式固定延时 10ms或者轮询 ACK。轮询 ACK 的原理是写周期结束后芯片重新上电开始响应总线指令。你可以反复发起始条件和设备写地址直到从机返回 ACK 为止——ACK 一旦出现就说明内部写周期已经结束。这种方式比固定延时更高效但代码稍微复杂一点。// 轮询等待写周期结束 void AT24C02_WaitWriteComplete(void) { uint8_t ack 0; do { I2C_Start(); I2C_SendByte(0xA0); // 发送写地址 ack I2C_WaitACK(); I2C_Stop(); if (ack 0) delay_ms(1); } while (ack 0); }这个函数最坑的地方在于如果芯片没正常工作它会一直死循环。所以我在项目里加了超时机制循环超过一定次数就返回错误避免系统卡死。还有一种做法是等 I2C 总线释放、SDA 回到高电平再继续。我建议新手用固定延时 10ms 先把流程跑通再去优化等待效率。4.5 从 AT24C02 读数据当前地址读、随机读、连续读读操作比写操作多一步虚写——发写地址和存储地址之后要重新发起一个起始条件切到读模式。// 从指定地址读一个字节随机读 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_RecvByte(0); // 接收数据发送NACK I2C_Stop(); return data; }连续读就是读完一个字节后不回 NACK 而回 ACK然后接着读下一个字节直到最后一个字节才回 NACK。AT24C02 内部读指针会自动递增不需要你每次发地址。注意连续读也有页边界问题跨页时地址指针同样会回绕到 0x00从 0xFF 处连续读 2 个字节第二个字节是 0x00 而不是 0x00 的下一页。4.6 主函数读-改-写校验的整体框架主函数里典型的测试流程是先写入一串数据到某个地址再把数据读出来通过串口打印对比是否一致。int main(void) { uint8_t write_buf[8] {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88}; uint8_t read_buf[8]; uint8_t i; USART_Init(); // 串口初始化用于打印结果 I2C_GPIO_Init(); // GPIO初始化 delay_ms(100); // 等待芯片稳定 // 写入8个字节 for (i 0; i 8; i) { AT24C02_WriteByte(0x00 i, write_buf[i]); } // 等待写周期 delay_ms(100); // 读出8个字节 for (i 0; i 8; i) { read_buf[i] AT24C02_ReadByte(0x00 i); } // 比对并打印 for (i 0; i 8; i) { if (read_buf[i] write_buf[i]) printf(Addr %d OK: 0x%02X\r\n, i, read_buf[i]); else printf(Addr %d FAIL: 0x%02X ! 0x%02X\r\n, i, read_buf[i], write_buf[i]); } while(1); }注意我在开头的 delay_ms(100)这个不是必需的但在开发板上很管用——芯片上电稳定需要时间有时候你刚上电就发 I2C 指令芯片还没准备好第一帧数据就丢了。5. 调试利器示波器与逻辑分析仪看时序5.1 为什么要看波形代码写完了如果你直接烧录发现读写不对光靠眼睛盯着代码找 bug 是效率很低的。I2C 的总线行为最终都要反映在 SCL 和 SDA 两根线的电平上。示波器或者逻辑分析仪能看到的东西远比你猜得多。我在调这个项目的时候先用逻辑分析仪抓了全程的时序。逻辑分析仪便宜、通道多、带协议解析对于 I2C 这种低速总线来说非常合适。你把 SCL 接到 CH0、SDA 接到 CH1设置好采样率一般 2MHz 以上就够触发条件设为 SDA 下降沿就能抓到完整的通信过程。5.2 用逻辑分析仪验证每一步抓完波形后按照协议逐段分析开头是不是有一个 SCL 高电平期间的 SDA 下降沿起始条件然后是 8 个时钟周期、每周期内 SDA 的电平是否对应你发送的地址字节第 9 个时钟 SDA 是否被拉低ACK如果 ACK 位置 SDA 是高电平问题基本锁定在这几个方向地址错误、芯片没上电、上拉电阻缺失、WP 引脚被拉高。如果波形显示 SCL 正常但 SDA 一直是低电平大概率是总线被某个设备卡住了。拔掉 SDA 线看波形是否恢复高电平就能确认是哪一侧的问题。如果 SDA 在起始条件后一直是高、但 ACK 位置也没变化多半是从机没响应——先查供电和地址引脚接线。5.3 调试时的延时调整技巧软件模拟 I2C 有个典型矛盾延时太短边沿太陡、信号不稳定延时太长通信速度太慢、闪烁明显。我的经验是初次调试时把延时统一设置为 5µs 左右先把功能跑通再用示波器看实际时序是否满足芯片的建立时间和保持时间要求最后逐步缩短延时提高速度。注意一点I2C 最极端的情况是全速 400kHz 时SCL 低电平时间最短 1.3µs、高电平时间最短 0.6µs。软件模拟很难精确控制到这种程度所以 400kHz 下我仍然建议用硬件 I2C。软件模拟跑 100kHz 是完全没问题的也是我推荐的教学速度。6. 关键参数计算时序、延时与时钟频率6.1 STM32F103 系统时钟与延时函数的关系软件模拟 I2C 对延时函数的精度要求不高但你必须知道你的延时函数在什么主频下工作。STM32F103C8T6 最大主频 72MHz如果用了 8MHz 外部晶振通过 PLL 倍频到 72MHz。如果你的最小系统板是 8MHz 晶振但代码里没配置 RCC系统会默认跑在内部 RC 振荡器的 8MHz这时你的延时函数实际时间会比预期长 9 倍左右。我踩过一个大坑代码在 72MHz 下调试没问题换了一块板子忘了改时钟配置I2C 波形异常奇怪——速度慢得离谱。后来才意识到是新板子时钟不对。新手拿到最小系统板先确认时钟树配置正确否则后续所有定时都不可靠。用 delay_us 这类函数时最好用 SysTick 或者定时器做基准不要在裸机上依赖简单的 for 循环次数估算时间——编译器优化等级一变你的延时时间就跟着变了。6.2 I2C 速率与延时的换算公式软件模拟 I2C 的速率取决于 SCL 一个高电平和低电平的持续时间总和。一个完整的 SCL 周期大约等于高电平延时 低电平延时 边沿时间。以标准模式 100kHz 为例一个周期 10µs高低电平理论上各 5µs。我代码里是高电平 5510µs 左右低电平 5510µs 左右实际频率约 50kHz留了很大的余量。计算延时是否足够的核心标准是看 AT24C02 数据手册里的参数参数符号标准模式要求快速模式要求SCL 高电平最小时间tHIGH4.0µs0.6µsSCL 低电平最小时间tLOW4.7µs1.3µsSDA 建立时间tSU:DAT250ns100nsSDA 保持时间tHD:DAT0~3.45µs0~0.9µs起始条件建立时间tSU:STA4.7µs0.6µs停止条件建立时间tSU:STO4.0µs0.6µs总线电容负载Cb最大400pF最大400pF上面表中的时间都是最小值。软件模拟只要确保每个延时都大于这些最小值通信就稳。但也不能太大——如果 SCL 周期超过 1ms有些从机可能会因为超时进入异常状态AT24C02 不会但部分传感器会。6.3 上拉电阻阻值计算上拉电阻的选择前面说过经验值这里补一个简单的计算逻辑。上拉电阻的取值要满足两个边界条件电流边界和上升时间边界。电流边界当总线被拉低时电流不能超过器件允许的最大灌电流。3.3V 系统、AT24C02 的 VOL 参数约 0.4V灌电流一般限制在 3mA 以内。R_min (3.3 - 0.4) / 0.003 ≈ 966Ω所以 1kΩ 是下限附近。上升时间边界RC 充电时间常数决定了 SDA/SCL 从低到高的时间标准模式要求上升时间最大 1µs快速模式最大 300ns。总线电容一般估算 100~200pF模块加了滤波电容的话会更大。R_max 1µs / 200pF 5kΩ所以标准模式用 4.7kΩ 是贴着上限但是安全的。快速模式下要更快上升沿用 2.2kΩ 会更合适。这就是为什么我常说 4.7kΩ 是万能选择它在标准模式下有足够余量总线电容不太离谱的情况下也能满足上升时间要求。如果你测出来的波形上升沿非常缓慢、边沿都成斜坡了第一件事就检查上拉电阻是否偏大、总线电容是否超标。7. 常见问题与排查实录我踩过的坑你就别踩了7.1 问题一SDA 一直被拉低起始条件都发不出去现象波形上看 SCL 偶尔翻一下SDA 一直低电平起始条件根本不成立。排查路径先用万用表量 SDA 对地电压如果接近 0V说明总线被某处短路。拔掉 AT24C02 模块SDA 应该被上拉电阻拉到 3.3V。如果拔掉后还是低问题在你的板上——检查 SDA 引脚是不是被复用成了其他功能、外部是否接了错误的上拉/下拉。如果拔掉后恢复高电平问题在 AT24C02 模块——重点检查 WP 引脚是否意外接高、芯片是否焊反、模块上 SDA 是否存在短路。我遇到过几次模块本身贴片电容虚焊导致 SDA 被拉低的情况重新焊接就好了。7.2 问题二发送设备地址后收不到 ACK现象波形完整地址字节也正确发出了但第 9 个时钟 SDA 一直是高电平。排查路径先核对设备地址。A0/A1/A2 全接 GND 是 0xA0如果这三个引脚接法不同地址要重新算。然后确认供电——用万用表量 AT24C02 的 VCC 引脚是不是真的有 3.3V。接着检查 WP 引脚WP 接高电平时写操作会直接 NACK但读操作通常不受影响如果你发的是写地址收不到 ACK试试改成读地址看能不能收到。最后怀疑芯片本身换一片试试。我过去做项目时遇到过芯片焊好但内部损坏的情况换一片立刻就好。7.3 问题三写完就读读出来全是 0xFF现象读写流程都走通了但读出来的数据全是 0xFF也就是 EEPROM 出厂默认值。排查路径这个问题的根源大概率是写操作没成功。先检查写周期等待——写完立即读芯片还在内部写周期内读出来的是旧值。在写之后加 10ms 延时再读看是否正常。然后检查页写入边界——如果你从某个地址连续写了多个字节而且跨了页后面的数据会回绕覆盖前面的数据导致读出来的数据和预期不一致。最后怀疑 WP 引脚——模块上 WP 如果接了高电平写操作会被硬件屏蔽读出来永远是默认值。把 WP 接 GND 试试。7.4 问题四读出来的数据对一半错一半现象前几个字节正确后几个字节不对且错误数据有规律比如每隔 8 个字节错一次。排查路径99% 是页写入/连续读的页边界问题。AT24C02 的页是 8 字节地址 0x00~0x07 是第一页0x08~0x0F 是第二页。跨页写会回绕跨页读也会回绕。举个例子你从地址 0x05 连续写 4 个字节写入顺序是 0x05、0x06、0x07、0x00最后一个字节写到了页首。解决办法是写之前做边界检查或者单字节写。连续读也一样如果从 0x07 连续读 3 个字节第三个字节是 0x00 处的数据而不是 0x08 处的数据。7.5 问题五用硬件 I2C 时程序卡死在等待事件标志现象用 STM32 硬件 I2C 外设、标准外设库程序运行到 I2C_SendData 或某个事件检查处卡住了。排查路径这是 STM32F103 硬件 I2C 的老大难问题。最经典的场景是复位后总线状态寄存器 BUSY 标志未被清除导致 EV5 事件起始条件发送完成永远等不到。网上流传的土办法是在初始化前手动复位 I2C 外设、把 SCL 切换成 GPIO 模式翻转几次冲开卡住的总线——这个方法我在 F103 上试过有效。但说实话如果你不是必须用硬件 I2C 的场景比如 DMA 高速传输软件模拟真的省心太多了。至少在 AT24C02 这个项目里软件模拟的稳定性和调试友好度完全碾压硬件 I2C。7.6 问题六Proteus 仿真正常、实物不正常现象仿真器里读写一切正常下载到实物上却各种诡异问题。排查路径环境差异。仿真是理想化的真实电路里存在上拉电阻、电源噪声、电平抖动等问题。重点排查检查实物上的上拉电阻是否虚焊用示波器看电源 3.3V 波形供电不足或者电源纹波过大会导致 I2C 时序不稳定检查杜邦线连接是否松动——I2C 是低速总线但杜邦线接触不良会造成信号毛刺。我在实物调试时遇到过 SCL 线上串扰导致时序错乱的情况把杜邦线换成短一点的质量更好的线就好了。如果你用的是面包板还要注意面包板内部的寄生电容和接触电阻。7.7 常见问题速查表现象可能原因排查方向SDA 一直低总线短路、芯片焊错、模块故障拔外设测电平、检查焊接发地址无 ACK地址错误、没供电、WP 高核对 A2A1A0、量 VCC、查 WP写后读 0xFF没等写周期、WP 高、页写错误加延时、拨 WP 跳线、改单字节写数据部分错乱跨页读写回绕检查页边界、拆分次读写卡死在硬件 I2C总线忙标志、芯片 bug复位外设、换软件模拟仿真正常实物异常上拉缺失、供电不稳、接触不良量电平、查波形、换线材8. 经验总结与扩展方向我个人在实际操作中的体会是AT24C02 这个项目虽然是入门级但把 I2C 的底子打牢固了后面碰到的所有 I2C 器件基本都能很快上手。写 OLED 的时候你会发现它的初始化序列本质上就是连续的 I2C 写操作、只是地址和数据格式不同读 MPU6050 这种传感器时随机读和连续读的流程跟 AT24C02 几乎一模一样。这就是为什么我强烈建议新手把这个项目调通、看懂每一个时序、亲手在逻辑分析仪上抓一遍波形——它带给你的收益远超一块 EEPROM 本身。最后再分享一个实用小技巧写完这个项目的读写驱动后我建议你封装一个简单的EEPROM 读写测试函数向所有地址写入对应的地址编号比如地址 0x00 写 0x00、地址 0x01 写 0x01……然后读回来比对。这个测试函数能快速定位芯片坏块、接线错误、地址问题是排查硬件故障的利器。我每次拿到新的 EEPROM 模块第一件事就是跑这个测试不超过两分钟就能判断模块是否能用。后续如果你想让这套代码更实用可以考虑加上简单的磨损均衡、坏块管理或者把数据存储区划分成参数区和日志区——这些都是从能读写到能实际用的关键一步。
返回列表