首页 / 资讯中心 / 文章详情

STM32F103硬件I2C驱动AT24C02实战:从时序到分页写入的避坑指南

STM32F103硬件I2C驱动AT24C02实战:从时序到分页写入的避坑指南 ★ FEATURED ARTICLE
1. 项目缘起与整体设计思路STM32F103 搭配 AT24C02 这个组合在嵌入式圈子里算是“老演员”了。但凡做过数据掉电保存、参数存储、设备序列号写入这类需求的人大概率都绕不开这颗 2Kbit 的 I2C EEPROM。它便宜、好买、驱动简单几乎成了入门 I2C 通信的标配外设。但就是这么个看似简单的东西我在实际项目里见过太多人卡在“读出来全是 0xFF”“写进去读出来不对”“偶尔能通偶尔不通”这些坑上。所以这篇内容我不打算只贴一段能跑的代码而是把整个读写链路从硬件到软件、从时序到状态机、从单字节到页写入全部拆开讲清楚。先明确一下这个项目要解决的核心问题让 STM32F103 通过硬件 I2C 接口稳定可靠地对 AT24C02 进行字节读写、页写入和连续读取并且处理好应答、超时、地址对齐这些容易出问题的细节。适合谁看如果你正在用 STM32F103 最小系统板做数据存储相关的功能或者你用的是 HAL 库但总觉得 I2C 用起来“心里没底”再或者你打算从软件模拟 I2C 切换到硬件 I2C那这篇内容基本可以当作一份实操手册来用。为什么选硬件 I2C 而不是软件模拟这是第一个要讲清楚的设计取舍。软件模拟 I2C 的好处是引脚随便选、时序完全可控、调试直观但缺点也很明显占用 CPU 时间、速率上不去、代码量不小。STM32F103 的硬件 I2C 外设虽然早年有一些“传说级”的坑但在 F1 系列上只要配置正确、超时处理好跑 100kHz 甚至 400kHz 都是稳的。我实测下来用标准库或者 HAL 库的硬件 I2C配合合理的超时机制连续读写几千次没有出现丢数据的情况。所以这个项目的主线就是硬件 I2C。再说 AT24C02 这颗芯片本身。2Kbit 的容量换算过来是 256 字节分成 32 页每页 8 字节。这个“页”的概念非常关键因为 AT24C02 支持页写入一次最多写 8 个字节超过一页就会“回卷”覆盖掉页首的数据。很多人写多字节的时候不处理分页结果写 10 个字节发现最后两个把前面的覆盖了这就是没搞懂页边界。所以整个项目的设计思路里分页写入和地址对齐是必须提前考虑进去的。从整体架构上看这个项目分成三层底层是 I2C 硬件初始化和基本的字节收发函数中间层是 AT24C02 的设备驱动包括字节写、页写、字节读、连续读、应答轮询上层是应用层的测试逻辑比如写入一组数据再读出来比对。这样的分层方式便于移植换一个 I2C 从设备只需要改中间层。下面我就按这个结构把每一层的实现细节和踩过的坑逐一展开。2. 硬件连接与 I2C 基础配置2.1 引脚连接与上拉电阻的选择STM32F103 的硬件 I2C1 默认引脚是 PB6SCL和 PB7SDAI2C2 是 PB10 和 PB11。用哪个都行我一般用 I2C1因为引脚更常见最小系统板上基本都引出来了。接线很简单PB6 接 AT24C02 的 SCLPB7 接 SDAVCC 接 3.3VGND 共地。但这里有一个必须注意的点I2C 总线是开漏输出必须接上拉电阻。上拉电阻选多大这个问题我被问过无数次。标准答案是 4.7kΩ 到 10kΩ 之间具体要看总线速率和总线电容。100kHz 的标准模式下面4.7kΩ 是最常用的值。如果你手头的模块已经带了上拉电阻那就不用额外加如果是从裸芯片自己搭电路一定要加。我见过有人直接拿 STM32 的内部上拉来用结果波形上升沿惨不忍睹通信时好时坏。内部上拉大概在 40kΩ 左右太弱了驱动不了 I2C 总线的电容负载。另外 AT24C02 的 A0、A1、A2 三个地址引脚决定了器件地址。全部接地时7 位地址是 1010000也就是 0x50。写操作地址是 0xA0读操作地址是 0xA1。这个地址在后面代码里会反复用到先记清楚。WP 引脚是写保护接低电平允许写入接高电平禁止写入。调试阶段建议直接接地不然写不进去还以为是代码问题。2.2 I2C 外设初始化参数计算用 HAL 库初始化 I2C 的时候CubeMX 会让我们填几个参数Clock Speed、Duty Cycle、Own Address 等。Clock Speed 设成 100000 就是 100kHz设成 400000 就是 400kHz。AT24C02 支持最高 400kHz所以两个都可以。我一般先用 100kHz 调通再尝试 400kHz。这里有一个隐藏的参数需要算一下I2C 的时钟源来自 APB1 总线。STM32F103 的 APB1 默认是 36MHzI2C 外设通过分频和时钟控制寄存器来产生 SCL。以 100kHz 为例分频系数大概是 36MHz / (2 * 100kHz) 180实际配置时 HAL 库会自动算但如果你用标准库自己写就要手动填 CCR 寄存器的值。标准模式下的公式是 CCR PCLK1 / (2 * SCL)快速模式下降沿和上升沿还要分别设置。这些细节 HAL 库帮我们屏蔽了但知道背后的计算逻辑出问题的时候才能查。还有一个容易忽略的点I2C 的 GPIO 模式必须配置为复用开漏输出。在 CubeMX 里选 I2C 功能后引脚会自动变成 AF Open Drain但如果你手动写 GPIO 初始化代码一定要设成GPIO_Mode_AF_OD否则总线拉不高也拉不低通信直接失败。这个坑我在早期用标准库的时候踩过查了半天以为是时序问题结果就是 GPIO 模式配错了。2.3 超时机制的必要性硬件 I2C 最让人头疼的就是“卡死”。如果从设备没有应答或者总线被意外拉低I2C 状态机可能一直等下去整个程序就挂在那里。所以每一个 I2C 操作都必须带超时。HAL 库的HAL_I2C_Master_Transmit函数最后一个参数就是 Timeout单位是毫秒。我一般设 100ms对于 100kHz 的速率来说传几个字节根本用不了这么久设 100ms 已经非常宽松了。但光有 HAL 的超时还不够。如果 I2C 总线真的被拉死了HAL 的超时返回 HAL_TIMEOUT这时候需要做总线恢复。恢复的方法是在 SCL 上手动发 9 个时钟脉冲让从设备把数据线释放掉。具体做法是把 SCL 临时切成普通 GPIO 推挽输出拉高拉低 9 次然后再切回 I2C 功能。这个恢复流程我后面会在问题排查章节详细讲。3. AT24C02 驱动实现与核心细节3.1 单字节写入的完整流程AT24C02 的单字节写入时序是这样的发送起始条件发送设备地址加写位0xA0等待应答发送要写入的内存地址0x00 到 0xFF等待应答发送要写入的数据字节等待应答发送停止条件。然后芯片内部会启动一个写周期把数据真正写入 EEPROM 存储单元这个写周期典型值是 5ms最大 10ms。这里的关键点是发送完停止条件后不能立刻进行下一次操作必须等待写周期完成。怎么等两种方式。一种是死延时 10ms简单粗暴但浪费 CPU。另一种是应答轮询也就是反复发送起始条件和设备地址如果芯片返回应答说明写周期结束了。应答轮询更高效也是我推荐的方式。用 HAL 库实现单字节写的代码大概长这样HAL_StatusTypeDef AT24C02_WriteByte(uint16_t memAddr, uint8_t data) { uint8_t buf[2]; buf[0] (uint8_t)(memAddr 0xFF); buf[1] data; return HAL_I2C_Master_Transmit(hi2c1, AT24C02_ADDR_WRITE, buf, 2, 100); }注意这里我把内存地址和数据拼成了一个 2 字节的缓冲区一次性发出去。这样比先发地址再发数据少一次函数调用效率更高。AT24C02 的内存地址是 8 位的所以直接取低 8 位就行。如果是 AT24C32 或更大的芯片地址是 16 位的就要拆成高低两个字节。3.2 页写入与地址回卷问题前面说了AT24C02 每页 8 字节页写入时如果跨越页边界地址会回卷到当前页的开头。举个例子假设你从地址 0x06 开始写 4 个字节0x06 和 0x07 是第 0 页的最后两个字节0x08 和 0x09 属于第 1 页。但芯片不会自动跳到第 1 页而是把 0x08 的数据写到第 0 页的 0x000x09 的数据写到 0x01。这就是回卷。所以页写入函数必须做分页处理。我的做法是先算出当前地址到页尾还有多少字节如果剩余空间够就一次写完如果不够就先写满当前页然后从下一页开头继续写。代码逻辑如下HAL_StatusTypeDef AT24C02_WritePage(uint16_t memAddr, uint8_t *data, uint16_t len) { uint16_t pageRemain; HAL_StatusTypeDef status; while (len 0) { pageRemain AT24C02_PAGE_SIZE - (memAddr % AT24C02_PAGE_SIZE); uint16_t writeLen (len pageRemain) ? len : pageRemain; uint8_t buf[AT24C02_PAGE_SIZE 1]; buf[0] (uint8_t)(memAddr 0xFF); memcpy(buf[1], data, writeLen); status HAL_I2C_Master_Transmit(hi2c1, AT24C02_ADDR_WRITE, buf, writeLen 1, 100); if (status ! HAL_OK) return status; AT24C02_WaitStandby(); memAddr writeLen; data writeLen; len - writeLen; } return HAL_OK; }这段代码里AT24C02_WaitStandby()就是应答轮询函数每次写完一页都要等芯片内部写周期结束才能继续写下一页。如果不加这个等待连续写多页的时候第二页的数据大概率会丢。3.3 字节读取与连续读取读取比写入简单一些但也有细节。AT24C02 的读时序是发送起始条件发送设备地址加写位0xA0等待应答发送要读取的内存地址等待应答重新发送起始条件发送设备地址加读位0xA1等待应答然后读取数据。如果是读多个字节每读一个字节后主机要发送应答ACK最后一个字节发送非应答NACK然后发送停止条件。用 HAL 库的话HAL_I2C_Mem_Read这个函数可以直接完成上述流程传入设备地址、内存地址、地址长度、数据缓冲区和长度就行。但要注意HAL 库的HAL_I2C_Mem_Read在 STM32F1 系列上有一个已知的小问题当读取长度超过一定值时可能会因为中断优先级或者时序问题导致数据错位。我实测读 8 个字节以内没问题读更多的话建议分多次读每次不超过 8 字节。连续读取的代码示例HAL_StatusTypeDef AT24C02_ReadBytes(uint16_t memAddr, uint8_t *buf, uint16_t len) { return HAL_I2C_Mem_Read(hi2c1, AT24C02_ADDR_READ, memAddr, I2C_MEMADD_SIZE_8BIT, buf, len, 100); }这里I2C_MEMADD_SIZE_8BIT表示内存地址是 8 位的AT24C02 用这个就对了。如果是 AT24C32 以上要改成I2C_MEMADD_SIZE_16BIT。3.4 应答轮询函数的实现应答轮询是 AT24C02 驱动里最容易被忽略但最重要的部分。它的作用是检测芯片是否完成了内部写周期。实现方式是反复发送起始条件和设备地址如果收到应答就说明芯片空闲了可以继续操作。HAL_StatusTypeDef AT24C02_WaitStandby(void) { uint32_t timeout 0; while (HAL_I2C_Master_Transmit(hi2c1, AT24C02_ADDR_WRITE, NULL, 0, 10) ! HAL_OK) { timeout; if (timeout 100) return HAL_TIMEOUT; } return HAL_OK; }注意这里发送的数据长度是 0也就是只发设备地址不发数据。如果芯片还在写周期内它不会应答HAL 库返回错误等写周期结束芯片应答函数返回成功。超时设 100 次每次 10ms总共 1 秒足够覆盖最坏情况。4. 完整读写测试与数据校验4.1 测试用例设计驱动写好了怎么验证它真的可靠我一般会设计三个测试用例。第一个是单字节读写测试在地址 0x00 写入 0x5A延时后读出来比对。第二个是页写入测试从地址 0x00 开始写入 8 个字节的递增序列读出来比对。第三个是跨页写入测试从地址 0x05 开始写入 10 个字节这个用例专门验证分页逻辑是否正确。测试代码里我会加一个全局的错误计数器每次比对失败就加一最后打印出来。如果错误计数是 0说明驱动稳定。我还会做循环测试比如连续写读 1000 次看看有没有偶发错误。这个循环测试特别重要因为有些问题只在特定时序下出现单次测试根本发现不了。4.2 数据校验与错误处理数据校验最简单的方式是逐字节比对。但如果是存储结构化数据比如一个配置结构体直接 memcmp 就行。更严谨的做法是加 CRC 校验写入的时候算一个 CRC 存在末尾读取的时候再算一遍比对。AT24C02 只有 256 字节加一个字节的 CRC 开销可以接受。错误处理方面每个 I2C 操作都要检查返回值。如果返回 HAL_ERROR可能是总线故障返回 HAL_TIMEOUT可能是从设备没应答返回 HAL_BUSY可能是上一次操作还没完成。针对不同的错误要有不同的处理策略。比如 HAL_TIMEOUT 可以重试几次HAL_ERROR 就要考虑总线恢复了。4.3 实测数据与性能分析我在 100kHz 速率下实测的数据是单字节写入含等待写周期大约 5.2ms页写入 8 字节大约 5.5ms连续读取 8 字节大约 0.8ms。可以看到写入的时间主要花在等待内部写周期上实际 I2C 传输时间很短。所以如果你的应用对写入速度有要求要尽量减少写入次数能页写入就不要单字节写。把速率提到 400kHz 后I2C 传输时间缩短到原来的四分之一但写周期还是 5ms 左右所以整体写入时间改善有限。读取速度提升明显8 字节读取降到 0.2ms 左右。所以 400kHz 对读多写少的场景更有意义。5. 常见问题排查与避坑经验5.1 读出来全是 0xFF 或 0x00这是最常遇到的问题。0xFF 通常意味着读取操作根本没有得到应答主机读到的只是总线空闲时的高电平。原因可能有几个设备地址错了、引脚接反了、上拉电阻没接、芯片坏了。排查的时候先用示波器或者逻辑分析仪看波形确认起始条件、地址字节、应答位是否正常。如果没有仪器就用最笨的办法把 I2C 速率降到 10kHz用万用表测 SCL 和 SDA 的电压正常空闲时都应该是高电平。0x00 则可能是写保护引脚被拉高了或者写入的地址不对。检查 WP 引脚的电平确认是接地的。5.2 写入后立刻读取数据不对这个问题几乎都是因为没有等待写周期。AT24C02 在收到停止条件后需要 5ms 左右把数据写入存储单元这期间芯片不响应任何操作。如果你写完立刻读读到的还是旧数据或者随机值。解决办法就是加应答轮询或者至少延时 10ms。我早期还遇到过一个更隐蔽的情况写周期还没结束就发了下一个起始条件导致芯片状态机混乱后续所有操作都失败。所以应答轮询不仅是等写周期也是让芯片状态复位。5.3 多字节写入时数据错位数据错位通常有两个原因。一个是分页逻辑没写对跨页的时候地址回卷了。另一个是缓冲区溢出比如你定义了 8 字节的数组但写了 10 个字节。排查的时候先把写入长度限制在 8 字节以内确认单页写入没问题再测试跨页。还有一个不太常见但很坑的问题HAL 库的HAL_I2C_Master_Transmit在传输过程中如果被高优先级中断打断可能会导致时序错乱。解决办法是把 I2C 相关的中断优先级设高一点或者在传输期间关中断。但关中断时间不能太长否则影响系统实时性。5.4 总线卡死与恢复方法总线卡死表现为 SCL 或 SDA 被持续拉低I2C 状态机一直处于 BUSY 状态。原因可能是从设备在传输过程中被复位导致它还在等时钟脉冲。恢复方法前面提过把 SCL 切成 GPIO 推挽输出手动发 9 个时钟脉冲然后发一个停止条件。具体代码void I2C_BusRecovery(void) { GPIO_InitTypeDef gpio; gpio.Pin GPIO_PIN_6; gpio.Mode GPIO_MODE_OUTPUT_PP; gpio.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, gpio); 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_I2C_DeInit(hi2c1); HAL_I2C_Init(hi2c1); }恢复完成后重新初始化 I2C 外设一般就能恢复正常。5.5 常见问题速查表现象可能原因排查方法解决措施读出全 0xFF设备无应答检查地址、接线、上拉修正地址补上拉电阻读出全 0x00写保护开启测 WP 引脚电平WP 接地写入后读不对未等写周期检查是否加延时加应答轮询或延时 10ms多字节错位跨页回卷检查地址是否跨页实现分页写入逻辑总线卡死从设备状态异常测 SCL/SDA 电平执行总线恢复流程偶发失败中断干扰检查中断优先级提高 I2C 中断优先级6. 从单字节到结构化存储的扩展思路6.1 参数存储的地址规划AT24C02 只有 256 字节如果用来存设备参数必须提前规划好地址分配。我的习惯是把地址空间分成几个区域0x00 到 0x0F 存设备 ID 和版本号0x10 到 0x7F 存用户参数0x80 到 0xEF 存运行日志0xF0 到 0xFF 保留给 CRC 和标志位。这样规划的好处是各区域互不干扰后期扩展也方便。每个参数项用一个结构体定义写入的时候把结构体首地址和长度传给页写入函数。读取的时候反过来。注意结构体可能会有字节对齐的问题不同编译器对齐方式可能不一样所以最好用__packed关键字或者手动序列化成字节数组再存。6.2 数据版本管理与兼容性产品固件升级后参数结构可能变了这时候读旧数据就会出错。解决办法是在存储区开头放一个版本号读取的时候先检查版本号如果不匹配就加载默认参数并重新写入。版本号可以是一个字节每次参数结构变更就加一。这个机制在量产产品里非常有必要能避免很多现场故障。6.3 磨损均衡的简单实现EEPROM 的擦写次数是有限的AT24C02 标称 100 万次。如果某个地址频繁写入比如每秒写一次那不到 12 天就写坏了。简单的磨损均衡做法是把同一个参数存到多个地址每次写入轮换地址读取的时候取最新的那个。比如用 4 个槽位轮流写寿命就延长到 4 倍。更复杂的做法是加一个写入计数器但 AT24C02 空间有限简单轮换就够了。我在一个数据记录仪项目里用过这个方案每 10 秒记录一次温度256 字节分成 32 个记录槽每个槽 8 字节循环覆盖。这样即使连续运行几个月每个地址的写入次数也远低于极限。配合一个头指针记录当前写到哪个槽读取的时候从头指针往前读就行。7. 硬件 I2C 与软件模拟的取舍经验7.1 什么时候该用硬件 I2C硬件 I2C 的优势是省 CPU、速率高、代码简洁。如果你的项目里 I2C 操作不频繁或者对实时性要求不高硬件 I2C 完全够用。但如果你的 I2C 总线上挂了多个设备而且有些设备时序比较特殊硬件 I2C 的灵活性就不如软件模拟了。我个人的经验是单一从设备、标准时序、速率要求不高的情况下优先用硬件 I2C。多设备、非标时序、需要精细控制的情况下用软件模拟更省心。STM32F103 的硬件 I2C 在 F1 系列上确实有一些历史遗留问题但在 HAL 库的封装下大部分问题已经被屏蔽了。7.2 软件模拟 I2C 的适用场景软件模拟 I2C 最大的好处是引脚任意选、时序完全可控。比如你需要在某个特定引脚上模拟 I2C 时序去驱动一个非标设备硬件 I2C 做不到软件模拟就可以。另外软件模拟 I2C 调试起来更直观因为你可以随时在代码里打断点看波形。但软件模拟的缺点也很明显占用 CPU、速率低、代码量大。100kHz 的软件模拟 I2C 基本会占满 CPU如果你还有其他任务要跑就会受影响。所以我的建议是能用硬件就用硬件实在不行再考虑软件模拟。7.3 两者混合使用的注意事项有些项目里会同时用到硬件 I2C 和软件模拟 I2C比如硬件 I2C 接 EEPROM软件模拟接 OLED。这种情况下要注意引脚不能冲突而且软件模拟的延时函数不能被打断太久否则时序会乱。我一般会把软件模拟 I2C 的延时用 DWT 或者定时器来实现比空循环更精准。另外如果硬件 I2C 和软件模拟 I2C 共用同一组引脚分时复用切换的时候要先把硬件 I2C 反初始化把引脚切成 GPIO再启动软件模拟。切换回来的时候反过来。这种用法比较少见但确实有人这么干过。8. 项目移植与代码组织建议8.1 驱动层的接口设计一个好的驱动层应该做到“换芯片不改上层”。我的做法是定义一组统一的 EEPROM 操作接口初始化、字节写、页写、字节读、连续读、等待就绪。上层应用只调用这些接口不关心底层是 AT24C02 还是 AT24C32。如果换芯片只需要改驱动层的实现上层代码一行不动。接口定义可以放在一个头文件里用函数指针或者宏来区分不同芯片。比如typedef struct { HAL_StatusTypeDef (*Init)(void); HAL_StatusTypeDef (*WriteByte)(uint16_t addr, uint8_t data); HAL_StatusTypeDef (*WritePage)(uint16_t addr, uint8_t *data, uint16_t len); HAL_StatusTypeDef (*ReadBytes)(uint16_t addr, uint8_t *buf, uint16_t len); HAL_StatusTypeDef (*WaitStandby)(void); } EEPROM_Driver_t;这样上层代码通过EEPROM_Driver_t指针来操作换芯片的时候只需要换一个驱动实例。8.2 在不同 STM32 系列上的移植STM32F103 的 I2C 外设和 F4、F7、H7 系列有一些差异主要是时钟配置和 HAL 库版本。移植的时候主要改三个地方I2C 初始化结构体、时钟使能、引脚定义。HAL 库的 API 基本兼容所以移植工作量不大。但要注意 F4 以上的系列 I2C 时序参数计算方式不同CubeMX 会自动处理手动写的话要查参考手册。从 F103 移植到 F030 或者 G030 这类低端系列时要注意 I2C 外设的版本可能更老有些特性不支持。比如 F030 的 I2C 不支持 400kHz 快速模式只能跑 100kHz。移植前先查一下目标芯片的参考手册确认 I2C 外设的规格。8.3 代码分层与目录结构我一般的目录结构是这样的bsp/放硬件抽象层包括 I2C 初始化和 GPIO 配置driver/放设备驱动比如at24c02.c和at24c02.happ/放应用逻辑比如参数管理和测试代码。这样分层清晰移植的时候只需要替换bsp/和driver/里的文件。头文件里用#ifndef做防重复包含所有对外接口加注释说明参数和返回值。驱动层不依赖任何应用层的头文件保持独立。这样即使把驱动单独拿出来放到另一个项目里也能直接编译通过。9. 调试工具与实测技巧分享9.1 逻辑分析仪的使用心得调试 I2C 最有效的工具是逻辑分析仪。几十块钱的 8 通道逻辑分析仪就够用了配合开源软件可以解码 I2C 协议直接看到起始条件、地址、数据、应答位。我遇到的大部分 I2C 问题用逻辑分析仪抓一次波形就能定位。抓波形的时候要注意采样率。100kHz 的 I2C 至少要用 1MHz 以上的采样率否则波形会失真。我一般设 4MHz 或 8MHz这样上升沿和下降沿都能看清楚。触发条件设成 SCL 下降沿或者起始条件这样能抓到完整的传输过程。9.2 用 GPIO 翻转做简易调试没有逻辑分析仪怎么办可以用一个空闲的 GPIO 做调试指示。比如在 I2C 传输开始前拉高结束后拉低用示波器看这个 GPIO 的波形就能知道传输花了多长时间。或者在关键代码分支里翻转 GPIO用 LED 显示程序走到了哪一步。这个方法虽然原始但在没有专业仪器的时候非常管用。我早期调试的时候就是在应答轮询函数里翻转一个 GPIO用 LED 看它闪不闪。如果一直不闪说明芯片一直没应答问题就在硬件或者地址上。如果闪了但数据不对问题就在数据缓冲区或者时序上。这种二分法排查效率很高。9.3 实测中的时序余量分析I2C 的时序余量是指实际波形和协议要求之间的差距。比如协议要求 SCL 高电平时间至少 4μs你实测是 4.5μs那余量就是 0.5μs。余量太小的话温度变化或者电压波动就可能导致通信失败。我一般要求余量至少在 20% 以上。用逻辑分析仪测量 SCL 频率、高电平时间、低电平时间、起始条件的建立时间、停止条件的保持时间和 AT24C02 数据手册里的参数对比。如果都在范围内且有余量基本就稳了。如果某个参数贴着极限就要调整 I2C 时钟配置或者上拉电阻。10. 写在最后的实操体会这个项目我从头到尾做过不下五遍每次都能遇到新的小问题。最开始是地址搞错后来是忘了等写周期再后来是跨页写入没处理。每次踩坑都让我对 I2C 协议和 AT24C02 的理解更深一层。现在回头看这个组合虽然简单但把 I2C 的起始、停止、应答、地址、数据、时序、总线恢复这些概念全都覆盖到了确实很适合用来练手。如果你正在做这个项目我的建议是先用 100kHz 把单字节读写调通再加页写入再加连续读取最后做循环测试。每一步都确认稳定了再往下走不要一上来就写一堆代码然后一起调。另外逻辑分析仪真的值得买一个几十块钱能省你几十个小时的调试时间。最后分享一个小技巧在 AT24C02 的驱动里加一个全局的错误统计结构体记录每种错误发生的次数。产品跑一段时间后把统计读出来就能知道 I2C 通信的稳定性如何。如果某种错误频繁出现就针对性地优化。这个习惯让我在很多项目里提前发现了潜在的硬件问题。
阅读完成 · 觉得有帮助?
咨询建站