1. 项目缘起与整体设计思路1.1 为什么选 AT24C02 作为 I2C 入门实战对象搞嵌入式的人手里多少都攒着几块 STM32F103 的最小系统板但很多人卡在“点完灯之后该练什么”这个阶段。GPIO 翻转太浅SPI Flash 又偏复杂而 I2C 接口的 AT24C02 这颗 EEPROM 恰好卡在一个黄金位置协议够典型、时序够讲究、容量够小、成本够低几乎每个电子市场一块钱就能抓一把。用它来打通 I2C 的完整链路性价比极高。AT24C02 是 2Kbit 容量的 I2C 串行 EEPROM换算成字节就是 256 字节。别嫌小存个设备序列号、校准参数、开机次数、用户配置这些零碎数据完全够用。它的页大小是 8 字节这个数字后面会反复出现因为写操作跨页是新手最容易翻车的地方。工作电压 1.8V 到 5.5V跟 STM32F103 的 3.3V 系统天然兼容不需要额外电平转换硬件上省事不少。从协议角度看AT24C02 支持标准模式 100kHz 和快速模式 400kHzSTM32F103 的硬件 I2C 外设完全覆盖这个范围。它用 7 位从机地址高四位固定为 1010低三位由 A2/A1/A0 三个引脚决定所以同一条总线上最多挂 8 片地址范围从 0x50 到 0x57。这个地址分配机制很经典理解了它以后碰到其他 I2C 器件也能举一反三。我选这个组合做实战拆解核心原因是它能把 I2C 的三大核心问题一次性暴露出来起始与停止条件的时序、应答位的处理、写周期的等待。这三个问题在纯软件模拟 I2C 时是纯时序问题在硬件 I2C 时是寄存器配置和状态机问题两条路都走一遍对协议的理解会深刻得多。1.2 硬件 I2C 与软件模拟 I2C 的选型权衡这是绕不开的第一个决策点。STM32F103 有硬件 I2C 外设但圈子里一直流传着“STM32 的硬件 I2C 有坑”的说法。这个说法有历史原因早期标准库的 I2C 驱动确实存在一些状态机处理不完善的情况加上 I2C 总线本身对时序敏感一旦从机拉低时钟做时钟同步或者出现总线仲裁调试起来就比较头疼。但我的实际经验是硬件 I2C 在 F103 上是可用的前提是你把状态机的错误处理写扎实。硬件 I2C 的优势在于 CPU 占用低传输 256 字节时不需要 CPU 一直空转等待而且时序由硬件保证不受中断延迟影响。软件模拟 I2C 的优势在于引脚任意、调试直观、出问题容易定位缺点是占用 CPU 且时序受中断影响。我的建议是分场景如果项目里 I2C 只是偶尔读写几个参数软件模拟完全够用代码可控性最强如果要频繁读写、或者总线上挂多个器件、或者系统中断负载较重那就老老实实上硬件 I2C把错误处理写全。这篇拆解我会以硬件 I2C 为主线同时在关键环节对比软件模拟的差异这样两条路你都能掌握。选型时还有一个容易被忽略的点上拉电阻的取值。I2C 是开漏输出必须外接上拉。标准模式 100kHz 下4.7kΩ 是常见值快速模式 400kHz 下通常用 2.2kΩ 到 4.7kΩ。取值太大会导致上升沿变缓波形变成“圆角”高速下容易误判取值太小则灌电流增大增加功耗。总线上挂的器件越多、走线越长上拉电阻就要相应减小。这个参数不是拍脑袋定的后面我会给出计算方法。1.3 整体数据流与分层设计一个干净的 EEPROM 驱动应该分三层底层 I2C 收发层、AT24C02 器件层、应用接口层。底层只负责“把若干字节发出去”和“把若干字节收回来”不关心对面是谁器件层负责拼装 AT24C02 需要的地址字节、处理页边界、等待写周期应用层则提供类似save_config()、load_config()这样的语义化接口。这样分层的好处是哪天你把 AT24C02 换成 AT24C16 或者别的 I2C 器件只需要改器件层底层和应用层基本不动。很多新手把三层揉在一起写结果换颗芯片就要重写一遍这是很典型的教训。数据流上一次完整的写操作是这样的应用层调用写接口 → 器件层判断是否跨页并拆分 → 底层发起起始条件、发送从机地址加写标志、发送字地址、发送数据、发起停止条件 → 器件层等待写周期完成典型 5ms→ 返回。读操作则是底层发起起始、发送从机地址加写标志、发送字地址、重新发起起始、发送从机地址加读标志、接收数据并发送应答或非应答、发起停止。这里有个关键细节读操作里的“写地址”阶段用的是写标志只有真正读数据时才切换成读标志。这个“伪写”步骤是 AT24C02 的随机读所必需的用来设置内部地址指针。很多新手在这里搞混直接发读标志结果读出来全是 0xFF 或者错位数据。2. 核心细节解析与实操要点2.1 AT24C02 的地址机制与页写边界先把地址这件事彻底讲透。AT24C02 的 7 位从机地址是1010 A2 A1 A0假设三个地址脚都接地那就是1010000即 0x50。注意 I2C 传输时是“地址左移一位加读写位”所以写操作发送的字节是0xA0读操作是0xA1。这个左移操作是硬件 I2C 外设自动完成的你只需要在寄存器里填 7 位地址即可但用软件模拟时就得自己移位别搞错。器件内部的字地址是 8 位范围 0x00 到 0xFF正好对应 256 字节。写操作时发送完从机地址后紧跟一个字节的字地址然后才是数据。这个字地址会在每次写入后自动递增所以连续写多个字节时不需要重复发地址。页写边界是重中之重。AT24C02 的页大小是 8 字节页的划分是固定的0x00-0x07 是第一页0x08-0x0F 是第二页以此类推。当你从某个地址开始连续写数据如果写到了页的末尾地址指针会“回卷”到本页的开头而不是进入下一页。举个例子从 0x06 开始写 4 个字节实际会写到 0x06、0x07、0x00、0x01把开头两个字节覆盖掉。这个行为叫“页回卷”是数据损坏的头号元凶。所以器件层必须做跨页拆分。逻辑很简单计算当前地址到本页末尾还剩多少字节如果剩余空间不够本次要写的数据量就先写满这一页等写周期结束后再从下一页开头继续写。这个拆分逻辑写对了页写就稳了。参数数值说明总容量256 字节2Kbit页大小8 字节跨页必须拆分从机地址0x50-0x57由 A2/A1/A0 决定写周期典型 5ms最大 10ms擦写寿命100 万次单字节级别数据保持100 年常温下2.2 写周期等待轮询应答的两种实现AT24C02 在收到停止条件后内部会启动一个写周期把数据真正烧进存储单元这段时间典型 5ms最大 10ms。在这期间器件不会响应任何 I2C 请求也就是发送从机地址后收不到应答。所以写完一页后不能立刻写下一页必须等写周期结束。等待方式有两种。第一种是固定延时直接delay_ms(10)简单粗暴但浪费时间因为大部分情况下 5ms 就够了。第二种是应答轮询也叫 ACK Polling不断发起起始条件、发送从机地址加写标志如果收到应答说明器件空闲了可以继续如果没收到应答就重试直到超时。这种方式效率高平均等待时间接近实际写周期。应答轮询的实现要点是每次轮询失败后要正确产生停止条件否则总线会卡住。硬件 I2C 里发送地址后如果收到 NACK需要清除相关标志位再重新发起起始。软件模拟里则要保证 SDA 和 SCL 都回到高电平空闲态。我实测下来轮询方式比固定延时平均能省 3 到 4ms批量写 256 字节时差距很明显。注意应答轮询一定要设超时上限比如 100 次重试。如果器件坏了或者总线短路没有超时就会死循环整个系统卡死。这个坑我踩过现场调试时找了半天才发现是 EEPROM 虚焊导致轮询永不成功。2.3 硬件 I2C 的关键寄存器与状态机STM32F103 的硬件 I2C 用起来核心是搞懂几个寄存器和事件。控制寄存器 CR1 里的 PE 位使能外设START 位发起起始ACK 位控制是否发送应答STOP 位发起停止。状态寄存器 SR1 和 SR2 里则是一堆标志位比如 SB起始已发送、ADDR地址已发送且收到应答、TXE发送寄存器空、RXNE接收寄存器非空、BTF字节传输完成。事件驱动的写法是发起起始后等 SB 置位然后写数据寄存器 DR 发送地址等 ADDR 置位清除该标志后开始发数据。每发一个字节等 TXE每收一个字节等 RXNE。这套流程用轮询实现最直观用中断实现效率更高但代码复杂。这里有个经典坑ADDR 标志的清除方式。在 F103 上清除 ADDR 需要先读 SR1 再读 SR2顺序不能反。如果只读 SR1 不读 SR2ADDR 清不掉后续状态机会乱。这个细节在参考手册里有写但很容易被忽略导致程序卡在等待 ADDR 的循环里。另一个坑是总线死锁。如果从机在传输中途掉电或者被复位可能会一直拉低 SDA导致总线无法产生起始条件。这时候需要手动恢复把 SCL 配置成普通 GPIO手动发送 9 个时钟脉冲让从机把剩余数据吐完释放 SDA然后再重新初始化 I2C 外设。这个恢复流程建议直接封装成一个函数上电初始化和异常处理时都调用。2.4 上拉电阻与总线电容的计算前面提到上拉电阻不能拍脑袋定这里给出计算方法。I2C 总线的上升时间受上拉电阻 Rp 和总线电容 Cb 影响公式是tr ≈ 0.847 × Rp × Cb。标准模式要求上升时间不超过 1000ns快速模式不超过 300ns。假设总线电容 Cb 是 100pF走线不长、挂两三个器件时的典型值标准模式下Rp ≤ 1000ns / (0.847 × 100pF) ≈ 11.8kΩ所以 4.7kΩ 绰绰有余。快速模式下Rp ≤ 300ns / (0.847 × 100pF) ≈ 3.5kΩ这时候 4.7kΩ 就偏大了得用 2.2kΩ 左右。总线电容怎么估每个器件的引脚电容大约 10pFPCB 走线每厘米大约 1pF 到 2pF。挂 3 个器件、走线 10cm大概就是 30pF 加 15pF 等于 45pF取 100pF 是留了余量的保守估计。实际调试时最好用示波器看波形如果上升沿明显变圆就减小上拉电阻。提示上拉电阻是每条总线一组不是每个器件一组。很多人第一次画板子给每个器件都配上拉结果并联后阻值变得很小灌电流超标。记住整条 SDA 和 SCL 各一个上拉就够了。3. 实操过程与核心环节实现3.1 硬件连接与初始化配置先把硬件接起来。STM32F103 的 I2C1 默认引脚是 PB6SCL和 PB7SDA这两个脚配置成复用开漏输出。AT24C02 的 A2/A1/A0 全部接地地址就是 0x50。SDA 和 SCL 各接一个 4.7kΩ 上拉到 3.3V。WP 引脚是写保护接地表示允许写入接高电平则整个芯片只读。调试阶段建议先接地确认读写正常后再考虑是否启用写保护。初始化配置分几步。第一使能 GPIOB 和 I2C1 的时钟。第二配置 PB6、PB7 为复用开漏输出速度选 50MHz。第三配置 I2C 的时钟控制寄存器 CCR这个值决定了 SCL 频率。计算公式是CCR PCLK1 / (2 × 目标频率)。假设 PCLK1 是 36MHz目标 100kHz那CCR 36M / (2 × 100k) 180。第四配置上升时间寄存器 TRISE标准模式下TRISE PCLK1(MHz) 1 37。第五使能 ACK 和 PE 位。// I2C1 初始化标准模式 100kHzPCLK1 36MHz void I2C1_Init(void) { GPIO_InitTypeDef gpio; I2C_InitTypeDef i2c; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); gpio.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; gpio.GPIO_Mode GPIO_Mode_AF_OD; // 复用开漏 gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, gpio); i2c.I2C_Mode I2C_Mode_I2C; i2c.I2C_DutyCycle I2C_DutyCycle_2; i2c.I2C_OwnAddress1 0x00; // 主机模式随便填 i2c.I2C_Ack I2C_Ack_Enable; i2c.I2C_AcknowledgedAddress I2C_AcknowledgedAddress_7bit; i2c.I2C_ClockSpeed 100000; // 100kHz I2C_Init(I2C1, i2c); I2C_Cmd(I2C1, ENABLE); }这段代码用的是标准库如果你用 HAL 库逻辑一样只是函数名不同。关键参数就是 ClockSpeed 和 DutyCycleDutyCycle 只在快速模式下有意义标准模式下填 2 就行。3.2 单字节写入的完整时序实现单字节写入是最基础的原子操作把它写对了页写和连续读都是它的组合。流程是起始 → 发送从机地址加写标志 → 等应答 → 发送字地址 → 等应答 → 发送数据 → 等应答 → 停止 → 等待写周期。// 向指定地址写一个字节返回 0 成功非 0 失败 uint8_t AT24C02_WriteByte(uint8_t addr, uint8_t data) { // 1. 发起起始条件 I2C_GenerateSTART(I2C1, ENABLE); if (!WaitFlag(I2C_SR1_SB, SET, TIMEOUT)) return 1; // 2. 发送从机地址 写标志 I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter); if (!WaitFlag(I2C_SR1_ADDR, SET, TIMEOUT)) return 2; (void)I2C1-SR1; (void)I2C1-SR2; // 清 ADDR 标志 // 3. 发送字地址 I2C_SendData(I2C1, addr); if (!WaitFlag(I2C_SR1_TXE, SET, TIMEOUT)) return 3; // 4. 发送数据 I2C_SendData(I2C1, data); if (!WaitFlag(I2C_SR1_BTF, SET, TIMEOUT)) return 4; // 5. 停止条件 I2C_GenerateSTOP(I2C1, ENABLE); // 6. 等待写周期 return AT24C02_WaitStandby(); }WaitFlag是我自己封装的超时等待函数轮询某个标志位直到置位或超时。超时值建议设成 10000 次循环左右具体看主频。这里每一步都返回不同的错误码调试时能快速定位卡在哪一步比统一返回一个错误值有用得多。AT24C02_WaitStandby就是前面说的应答轮询。它不断发起起始、发送 0xA0收到应答就返回 0超时返回错误。注意每次轮询失败后要发停止条件否则总线状态不对。3.3 页写与跨页拆分的实现页写是在一次传输里连续发送多个字节效率比逐字节写高得多因为只需要一次起始、一次地址、一次停止。但必须处理跨页。实现思路是先算出当前地址在本页内的剩余空间取剩余空间和待写长度的较小值作为本次写入量写完等写周期再处理剩下的。// 页写自动处理跨页返回 0 成功 uint8_t AT24C02_WritePage(uint8_t addr, const uint8_t *buf, uint16_t len) { while (len 0) { // 计算本页剩余空间 uint8_t page_remain 8 - (addr % 8); uint8_t chunk (len page_remain) ? len : page_remain; // 发起一次页写传输 I2C_GenerateSTART(I2C1, ENABLE); if (!WaitFlag(I2C_SR1_SB, SET, TIMEOUT)) return 1; I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter); if (!WaitFlag(I2C_SR1_ADDR, SET, TIMEOUT)) return 2; (void)I2C1-SR1; (void)I2C1-SR2; I2C_SendData(I2C1, addr); if (!WaitFlag(I2C_SR1_TXE, SET, TIMEOUT)) return 3; for (uint8_t i 0; i chunk; i) { I2C_SendData(I2C1, buf[i]); if (!WaitFlag(I2C_SR1_BTF, SET, TIMEOUT)) return 4; } I2C_GenerateSTOP(I2C1, ENABLE); // 等待写周期 if (AT24C02_WaitStandby() ! 0) return 5; // 更新指针 addr chunk; buf chunk; len - chunk; } return 0; }这段代码的核心就是那个page_remain的计算。addr % 8得到的是当前地址在页内的偏移8 - 偏移就是本页还能写多少字节。比如 addr 是 0x06偏移是 6剩余 2 字节如果 len 是 4那第一次只写 2 字节第二次从 0x08 开始写剩下 2 字节。这样永远不会触发页回卷。实操心得调试页写时建议先用一个 256 字节的数组填满整个 EEPROM再读回来逐字节比对。这个“全片写读校验”能一次性暴露跨页、地址递增、写周期等待的所有问题。我每次移植新驱动都先跑这个测试比单点测试靠谱得多。3.4 随机读与连续读的实现读操作比写操作多一个“伪写”步骤。随机读的流程是起始 → 发送从机地址加写标志 → 等应答 → 发送字地址 → 等应答 → 重新发起起始 → 发送从机地址加读标志 → 等应答 → 接收数据 → 发送非应答 → 停止。连续读则是在接收数据时除了最后一个字节发非应答其余每个字节都发应答这样器件会持续递增地址并输出数据。地址递增到 0xFF 后会回卷到 0x00这是读操作的行为和写操作的页回卷不同读是整片回卷。// 随机读从 addr 开始读 len 个字节 uint8_t AT24C02_ReadBytes(uint8_t addr, uint8_t *buf, uint16_t len) { // 伪写设置内部地址指针 I2C_GenerateSTART(I2C1, ENABLE); if (!WaitFlag(I2C_SR1_SB, SET, TIMEOUT)) return 1; I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter); if (!WaitFlag(I2C_SR1_ADDR, SET, TIMEOUT)) return 2; (void)I2C1-SR1; (void)I2C1-SR2; I2C_SendData(I2C1, addr); if (!WaitFlag(I2C_SR1_BTF, SET, TIMEOUT)) return 3; // 重新发起起始切换为读 I2C_GenerateSTART(I2C1, ENABLE); if (!WaitFlag(I2C_SR1_SB, SET, TIMEOUT)) return 4; I2C_Send7bitAddress(I2C1, 0xA1, I2C_Direction_Receiver); if (!WaitFlag(I2C_SR1_ADDR, SET, TIMEOUT)) return 5; // 接收数据 for (uint16_t i 0; i len; i) { if (i len - 1) { I2C_AcknowledgeConfig(I2C1, DISABLE); // 最后一个发 NACK } (void)I2C1-SR1; (void)I2C1-SR2; if (!WaitFlag(I2C_SR1_RXNE, SET, TIMEOUT)) return 6; buf[i] I2C_ReceiveData(I2C1); } I2C_GenerateSTOP(I2C1, ENABLE); I2C_AcknowledgeConfig(I2C1, ENABLE); // 恢复 ACK return 0; }这里有个细节最后一个字节之前要先关掉 ACK再清 ADDR 标志然后等 RXNE。顺序错了会导致最后一个字节收到 NACK 后器件提前释放总线数据可能不完整。另外读完之后一定要把 ACK 重新打开否则下一次读操作会出问题。3.5 完整测试用例与验证方法驱动写完了得验证。我一般分三步测。第一步单字节读写往 0x00 写 0x5A读回来比对。第二步全片写读用 0 到 255 填充整个 EEPROM再读回来逐字节比对这一步验证跨页和地址递增。第三步边界测试从 0x06 写 4 字节验证页回卷是否被正确规避从 0xFF 读 4 字节验证读回卷。void AT24C02_Test(void) { uint8_t wbuf[256], rbuf[256]; for (int i 0; i 256; i) wbuf[i] i; // 全片写入 if (AT24C02_WritePage(0x00, wbuf, 256) ! 0) { printf(write failed\r\n); return; } // 全片读出 if (AT24C02_ReadBytes(0x00, rbuf, 256) ! 0) { printf(read failed\r\n); return; } // 比对 for (int i 0; i 256; i) { if (wbuf[i] ! rbuf[i]) { printf(mismatch at %d: w%02X r%02X\r\n, i, wbuf[i], rbuf[i]); return; } } printf(all pass\r\n); }这个测试跑通基本可以确认驱动没问题。如果中间有 mismatch看错位的规律如果是每 8 字节错一次那是页边界问题如果是固定偏移错那是地址指针问题如果全是 0xFF那是根本没写进去或者读时序不对。4. 常见问题与排查技巧实录4.1 总线卡死与恢复流程总线卡死是 I2C 调试里最烦人的问题现象是程序卡在等待某个标志位的循环里或者起始条件发不出去。原因通常是上一次传输异常中断从机还在拉低 SDA 等待时钟。这时候硬件 I2C 外设可能已经进入错误状态需要手动恢复。恢复流程是先把 I2C 外设关掉把 SCL 和 SDA 配置成普通推挽输出SCL 输出 9 个时钟脉冲每个脉冲后检查 SDA 是否释放然后手动产生一个停止条件SDA 低时 SCL 高然后 SDA 拉高最后重新初始化 I2C 外设。这个流程封装成I2C_BusRecover()上电时和检测到错误时都调用。void I2C_BusRecover(void) { GPIO_InitTypeDef gpio; I2C_Cmd(I2C1, DISABLE); // 配置为普通推挽输出 gpio.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; gpio.GPIO_Mode GPIO_Mode_Out_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, gpio); GPIO_SetBits(GPIOB, GPIO_Pin_6 | GPIO_Pin_7); delay_us(10); // 发送 9 个时钟脉冲 for (int i 0; i 9; i) { GPIO_ResetBits(GPIOB, GPIO_Pin_6); delay_us(5); GPIO_SetBits(GPIOB, GPIO_Pin_6); delay_us(5); } // 手动产生停止条件 GPIO_ResetBits(GPIOB, GPIO_Pin_7); delay_us(5); GPIO_SetBits(GPIOB, GPIO_Pin_6); delay_us(5); GPIO_SetBits(GPIOB, GPIO_Pin_7); delay_us(5); I2C1_Init(); // 重新初始化 }注意这个恢复函数在软件模拟 I2C 里同样适用因为问题出在从机状态跟主机是硬件还是软件无关。养成上电先调用一次恢复的习惯能避免很多莫名其妙的初始化失败。4.2 写不进去数据的排查思路写完读出来不对或者根本没写进去排查顺序是这样的。第一确认 WP 引脚电平接高就是写保护什么都写不进。第二确认从机地址对不对用示波器或者逻辑分析仪抓一下地址字节看是不是 0xA0。第三确认写周期等待有没有生效如果写完立刻读器件还在忙读回来就是旧数据或者 0xFF。第四确认上拉电阻有没有焊很多人画板子忘了上拉波形根本拉不高。如果这些都对还是写不进那可能是时序问题。用逻辑分析仪抓一次完整写操作的波形对照数据手册看起始条件是否标准SCL 高时 SDA 下降地址和数据是否在 SCL 高电平期间稳定停止条件是否标准SCL 高时 SDA 上升。时序问题在示波器下一目了然比盲猜快得多。4.3 常见问题速查表现象可能原因排查方法读回全 0xFF未写入、地址错、写周期未等检查 WP、地址、加延时重试每 8 字节错位页回卷检查跨页拆分逻辑固定偏移错位地址指针未递增检查字地址发送程序卡在等标志总线死锁、ADDR 未清调用总线恢复、检查 SR1/SR2 读取顺序波形上升沿圆角上拉电阻过大减小上拉阻值或降低速率偶发读写失败中断干扰、时序临界关中断测试、降低 SCL 频率写后立即读失败写周期未完成启用应答轮询4.4 提升可靠性的几个实战技巧第一关键参数存双份。EEPROM 虽然标称 100 万次擦写但实际使用中如果频繁写同一地址局部磨损会很快。重要参数存两份读的时候比对不一致就用备份恢复。第二加校验和。每个配置块末尾加一个 CRC 或者简单累加和读出来先校验防止数据位翻转导致误用。第三写前先读比对。如果新数据和旧数据一样就跳过写入能显著延长寿命。第四降低写频率。参数变化时不要立刻写攒一批或者定时写减少擦写次数。我在一个数据采集项目里原本每秒写一次累计值EEPROM 半年就出现坏块。后来改成每分钟写一次并且加了写前比对用了三年都没问题。这个教训说明EEPROM 的寿命管理不是理论问题是实打实会影响产品可靠性的。4.5 软件模拟 I2C 的时序微调如果你选择软件模拟时序微调是必修课。标准模式 100kHz 意味着每个时钟周期 10us高低电平各 5us。但实际实现时由于 GPIO 翻转和函数调用有开销延时函数要相应减小。我的做法是先按理论值写然后用逻辑分析仪测实际频率再调整延时参数。软件模拟的关键是保证 SCL 高电平期间 SDA 稳定。发送数据时先设置 SDA再拉高 SCL延时拉低 SCL。接收数据时先拉高 SCL延时读 SDA再拉低 SCL。起始条件是 SCL 高时 SDA 从高变低停止条件是 SCL 高时 SDA 从低变高。这些顺序不能乱。提示软件模拟 I2C 在中断频繁的系统里要小心如果 SCL 高电平期间来了中断延时被拉长从机可能误判。解决办法是在关键时序段关中断或者干脆用硬件 I2C。这也是我前面建议中断负载重时用硬件的原因。5. 从 AT24C02 延伸到其他 I2C 器件的通用思路把 AT24C02 这套流程吃透你会发现大部分 I2C 器件都是类似的套路起始、地址、寄存器地址、数据、停止。区别只在于地址格式、寄存器地址宽度、读写时序细节。比如某些传感器是 16 位寄存器地址那就多发一个字节某些器件读之前不需要伪写直接读就行。我的建议是每接触一颗新 I2C 器件先做三件事确认从机地址、确认寄存器地址宽度、确认读写时序图。这三件事搞清楚了剩下的就是套用已有的底层收发函数。AT24C02 作为练手对象最大的价值就是它把这套通用流程用最简单的方式呈现出来了。最后分享一个我常用的调试习惯手边常备一个逻辑分析仪几十块钱的那种就够用。I2C 的问题看波形比看代码快十倍。波形对了代码逻辑再查波形不对先修硬件和时序。这个习惯帮我省下了大量瞎猜的时间。
阅读完成 · 觉得有帮助?