☰
STM32F103驱动AT24C02:I2C读写EEPROM完整实战解析
2026/10/6 12:03:08 网站建设 项目流程

1. 聊聊这次为什么选 I2C + AT24C02 这套组合

很多刚接触 STM32F103 的朋友,第一块外设往往不是 LED 就是串口,等到真正需要“掉电不丢数据”的时候,才意识到 EEPROM 这种东西是绕不开的。AT24C02 是 I2C 接口 EEPROM 里最经典的一颗芯片,2Kbit 容量,也就是 256 字节,价格几毛钱,市面上几乎所有开发板上都焊着它。拿它练手 I2C,既便宜又直观,写完代码能立刻看到效果,比对着协议栈空想半天实在得多。

先说清楚这套组合解决什么问题:STM32F103 的 I2C 外设历来口碑不算好,很多人反映硬件 I2C 容易卡死、不好调试,所以初学者经常被劝退。但实际项目中,I2C 又是一个绕不开的通信手段,传感器、显示屏、存储芯片都在用它。AT24C02 作为最简单的一种 I2C 从设备,非常适合用来把 I2C 协议彻底跑通:起始条件、停止条件、应答、地址帧、数据帧、写周期延时,这些概念全部能在一次读写过程中对应上。

这篇文章主要面向三类人:一是刚点亮过 STM32F103 最小系统、准备接触通信协议的初学者;二是用 CubeMX 配过 I2C 但卡在读写时序上的朋友;三是想搞清楚硬件 I2C 和软件模拟 I2C 到底怎么选、调试时从哪里入手的工程师。内容会从硬件接线开始讲,一直到 CubeMX 配置、读写代码、异常排查,尽量做到看完就能照着复现。

我在这篇文章里,硬件 I2C 和模拟 I2C 都会讲到,因为实际项目中两者都有人用,而且踩坑的点完全不一样。模拟 I2C 适合快速验证、不依赖外设状态,硬件 I2C 适合低功耗和 DMA 场景,但 STM32F103 的硬件 I2C 用不好真的会卡死,这一点不提前说清楚,后面写代码时会很痛苦。


2. 硬件准备与接线细节

2.1 器件引脚对应关系

AT24C02 是 SOP-8 封装,八个引脚,功能很简单:A0、A1、A2 是地址选择脚,WP 是写保护脚,SCL、SDA 是 I2C 总线,VCC 和 GND 供电。在使用前必须先搞明白器件地址是怎么算出来的,否则代码里写错地址,数据根本进不去。

AT24C02 的设备地址是 7 位,组成格式是1010 + A2 + A1 + A0。比如三个地址引脚都接地,那 7 位地址就是1010000,换算成 8 位写地址(最低位补 0)就是0xA0,读地址就是0xA1。如果 A0 接了高电平,写地址就变成0xA2。这个点非常容易忽略,因为很多开发板出厂时 A0、A1、A2 已经固定接地了,但如果你自己画板子或者飞线,必须确认这三个引脚的接法,最好在代码里用一个宏定义把地址显式写出来,方便后面调整。

引脚对应关系总结如下:

  • SCL:接 STM32F103 的 PB6(I2C1_SCL),部分板子用 PB8,需查原理图确认
  • SDA:接 STM32F103 的 PB7(I2C1_SDA)
  • VCC:接 3.3V
  • GND:接 GND
  • WP:接 GND,允许写入;接 3.3V 则只能读不能写
  • A0/A1/A2:接 GND,地址为 0xA0(写)/0xA1(读)

2.2 电源与上拉电阻的注意事项

AT24C02 的工作电压范围在 1.8V 到 5.5V 之间,接 3.3V 完全没问题。但这里有个关键点:I2C 总线的 SCL 和 SDA 是开漏输出,必须接上拉电阻才能输出高电平。STM32F103 的 I2C 引脚内部虽然有上拉,但阻值偏大,在高速通信时信号边沿不够陡,容易出现通信不稳定。常规做法是在外部接两个 4.7kΩ 上拉电阻,分别接 SCL 和 SDA 到 VCC。如果你的开发板已经集成了上拉电阻,就不用额外加。

热词里出现“stm32f103 5v转3.3v电路”,这里顺便提醒一句:如果你的系统里有 5V 器件,而且 I2C 总线上的从设备是 5V 供电的,那么 STM32F103 的 3.3V GPIO 不能直接承受 5V 电平,需要考虑电平转换。不过 AT24C02 通常都是 3.3V 供电,这就不存在电平兼容问题,可以直接连接。

互联网上有些教程会用 5V 给 AT24C02 供电,然后 SCL/SDA 直接连 STM32F103。这种接法虽然大多数时候能工作,因为 AT24C02 的 SCL/SDA 引脚对输入高电平的阈值比较宽容,但从设计角度看不够严谨,长期可靠性存疑。建议统一用 3.3V 供电,省心。

2.3 关于 STM32F103 最小系统的提醒

如果你用的是最小系统板,而不是现成的开发板,接线时要特别注意排针的丝印。很多“ STM32F103C8T6 最小系统板”上的 PB6/PB7 旁边可能没有标 I2C 字样,自己对照芯片数据手册确认引脚即可。另外,最小系统板上的 3.3V 电源通常是由板载 LDO 从 USB 5V 转出来的,电流余量足够驱动 AT24C02 这种低功耗芯片,但如果你同时挂了好几路传感器和显示屏,建议用外部稳压供电,避免 LDO 过热导致电压跌落。上电顺序也很重要:先给 STM32F103 上电,确认系统启动正常后,再接入 I2C 从设备,这样能避免总线冲突导致初始化失败。


3. I2C 协议核心概念:从时序图到代码逻辑

3.1 为什么必须理解时序图

很多人写 I2C 代码喜欢直接抄例程,能跑通就算完事,但一旦芯片换成另一型号就抓瞎。I2C 通信的核心在于时序:什么时候 SDA 变化,什么时候 SCL 拉高采样,主机和从机之间如何握手。这些内容在时序图里体现得非常清楚,看懂一张图,胜过硬背十段代码。

I2C 总线只有两根线,SCL(时钟线)和 SDA(数据线)。空闲时两根线都是高电平。主机通过拉低 SDA 来发起起始条件,然后逐个字节地发送数据。每个字节 8 位,高位先发。每发送完一个字节,从机要回一个 ACK(应答位),如果从机没回 ACK,主机就知道从机不在线或者忙。发送完所有数据后,主机拉低 SCL,再让 SDA 从低变高,形成停止条件,总线释放。

模拟 I2C 的代码套路大致是这样:

  • 起始:SCL 高电平时,SDA 由高到低
  • 数据位:SCL 低电平时改变 SDA,SCL 高电平时保持 SDA 稳定,从机采样
  • 停止:SCL 高电平时,SDA 由低到高

3.2 起始、停止、应答的代码映射

在 I2C 驱动代码里,这三个动作是基础中的基础。用 GPIO 模拟时,可以直接用宏定义控制引脚高低电平。下面是我用过的一套很经典的模拟 I2C 代码结构示例:

#define SDA_H() GPIOB->BSRR = GPIO_PIN_7 #define SDA_L() GPIOB->BRR = GPIO_PIN_7 #define SCL_H() GPIOB->BSRR = GPIO_PIN_6 #define SCL_L() GPIOB->BRR = GPIO_PIN_6 #define SDA_READ() ((GPIOB->IDR & GPIO_PIN_7) != 0) static void I2C_Delay(void) { for (volatile int i = 0; i < 20; i++); } void I2C_Start(void) { SDA_H(); SCL_H(); I2C_Delay(); SDA_L(); I2C_Delay(); SCL_L(); } void I2C_Stop(void) { SCL_L(); SDA_L(); I2C_Delay(); SCL_H(); I2C_Delay(); SDA_H(); } uint8_t I2C_WaitAck(void) { uint8_t ack = 0; SDA_H(); // 释放SDA,让从机控制 I2C_Delay(); SCL_H(); I2C_Delay(); if (SDA_READ()) { ack = 1; // 没有应答 } SCL_L(); return ack; }

这套代码是纯软件模拟的,不依赖 STM32F103 的 I2C 外设,好处是逻辑完全掌握在自己手里,遇到问题容易查。I2C_Delay 的时间直接决定通信速率,大约 20 次空循环在 72MHz 主频下大概能产生几百纳秒的延时,可以跑在 100kHz 到 400kHz 之间。

3.3 七位地址与读写位的组织

I2C 的总线通信是以字节为单位的,第一个字节是设备地址加读写位。写操作时,地址字节的最低位是 0;读操作时,最低位是 1。AT24C02 的设备地址是 7 位,记作 A6...A0,实际发送时左移一位,最低位填 R/W 位。很多初学者在这里搞混,把 0xA0 当成了“唯一地址”,其实 0xA0 已经是写地址字节了,如果直接把它当作 7 位地址发送,帧结构就错了。

一个标准的写命令帧如下:主机发起始条件,发 0xA0(10100000,其中 A2/A1/A0 都接地,最低位为 0 表示写),AT24C02 收到后比对前 7 位,确认匹配后拉低 SDA 回复 ACK。接着主机发要写入的字节地址(0~255),再发数据字节。每个字节后从机都会回 ACK。最后主机发停止条件,触发 EEPROM 内部写周期,大约 5ms 内完成写入。

读命令帧则复杂一点,分两种:当前地址读和随机读。随机读需要先发一个伪写命令来指定地址:主机发起始、发 0xA0、发内存地址,然后主机发重复起始条件、发 0xA1,此时 AT24C02 切换方向,开始向主机发送数据。这里“重复起始条件”是 I2C 协议里的关键动作,模拟 I2C 代码里其实就是再调用一次起始函数,但 SCL/SDA 的状态转换必须正确,否则从机可能不识别。


4. CubeMX 初始化配置与工程搭建

4.1 CubeMX 中的 I2C 配置项说明

用 STM32CubeMX 配置 STM32F103 的 I2C1 外设非常快,但里面几个参数不搞清楚,后面调代码会莫名其妙地出问题。打开 CubeMX 后,先把 PB6 和 PB7 配置为 I2C1_SCL 和 I2C1_SDA,然后在 I2C1 的 Mode 里选择 I2C,下面的参数基本保持默认即可,有几个值值得解释一下:

  • I2C Speed Mode:标准模式(100kHz)或快速模式(400kHz)。AT24C02 支持 400kHz,但建议前期先用 100kHz 跑通,降低信号干扰对调试的影响。
  • I2C Clock Speed:这个值和 PCLK1 的时钟频率密切相关。F103 的 APB1 最高 36MHz,CubeMX 会自动算分频系数,不用手动干预。
  • Rise Time / Fall Time:输入输出信号的边沿转换时间,AT24C02 的数据手册上会给出范围,默认值通常都能用。

配置完成后,记得在 Project Manager 里勾选 Generate peripheral initialization as a pair of .c/.h files,这样生成的代码结构更清晰,后面添加自己的驱动函数也方便。

4.2 HAL 库读写函数签名与用法

STM32F103 的 HAL 库提供了一组 I2C 读写函数:HAL_I2C_Master_Transmit、HAL_I2C_Master_Receive、HAL_I2C_Mem_Write、HAL_I2C_Mem_Read。前两个是直接读写设备寄存器用的,后两个带 MemAddress 参数,正是为 EEPROM 这类“先指定内部地址再传数据”的器件设计的,所以直接用 Mem_Write 和 Mem_Read 最合适。

关键在于:HAL_I2C_Mem_Write 的 I2C_HandleTypeDef 参数是 &hi2c1,DevAddress 参数是 AT24C02 的 8 位设备地址(写地址 0xA0),MemAddress 是 EEPROM 内部字节地址,MemAddSize 是 I2C_MEMADD_SIZE_8BIT(因为 AT24C02 的地址只有 8 位),Buffer 是数据缓冲区指针,Size 是传输数据长度。看一下 HAL 库源码就会发现,它内部会自动帮你组织起始条件、地址字节、内存地址字节和数据字节,不需要你手动操作 I2C_Start 这些底层函数。这大大降低了代码复杂度,也是推荐 HAL 的主要原因之一。

4.3 Keil MDK 工程搭建与 ST-Link 调试要点

热词里有一条“stm32f103 keil mdk工程搭建与st-link调试全流程”,说明很多人卡在工程搭建阶段。从 CubeMX 生成的工程可以直接用 Keil MDK 打开,但前提是你得在 CubeMX 里选对工具链版本,建议选 MDK-ARM V5。打开工程后,第一个要做的事是检查魔术棒里的 Debug 设置:选择 ST-Link Debugger,然后在 Settings 里确认 SW 模式连接正常,设备 ID 能识别出来。

涉及 I2C 调试,强烈建议在代码里加一个软件延时,不是 Hack,而是缓解调试器连接时对时序的影响。当你全速跑的时候,写 EEPROM 后没有等待写周期就立刻去读,大概率读出来是 0xFF。这种情况不是代码逻辑错,而是 AT24C02 还没写完。HAL 库的 HAL_I2C_Mem_Write 函数并不会自动等待内部写周期结束,所以写入之后要 delay 5~10ms,再执行读操作。

调试时 ST-Link 的 RTT 或者串口打印都行,我习惯用串口把读出来的数据打印出来,配合十六进制显示,一眼就能看出写入是否成功。但注意 ST-Link 的 SWD 引脚和 I2C 引脚如果碰巧复用了,调试时会影响 I2C 波形。


5. 读写代码实现:从单字节到页写

5.1 单字节写入与读取的实现

先写最基础的单字节写入函数。AT24C02 内部写单个字节的流程是:主机发起始、发 0xA0、发内存地址、发数据、发停止。下面这段代码在我的工程里测试过,可以直接套用:

#define AT24C02_ADDR_W 0xA0 #define AT24C02_ADDR_R 0xA1 uint8_t AT24C02_WriteByte(uint8_t memAddr, uint8_t data) { uint8_t buf[1]; buf[0] = data; if (HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_W, memAddr, I2C_MEMADD_SIZE_8BIT, buf, 1, HAL_MAX_DELAY) != HAL_OK) { return 1; } HAL_Delay(5); return 0; } uint8_t AT24C02_ReadByte(uint8_t memAddr) { uint8_t buf[1]; if (HAL_I2C_Mem_Read(&hi2c1, AT24C02_ADDR_R, memAddr, I2C_MEMADD_SIZE_8BIT, buf, 1, HAL_MAX_DELAY) != HAL_OK) { return 0xFF; } return buf[0]; }

HAL_MAX_DELAY 表示阻塞等待传输完成,不设超时。实际项目中建议换成具体的超时值比如 100,避免 I2C 总线异常时程序卡死。第 5ms 的 HAL_Delay 是为了等待 EEPROM 内部写周期,这个时间来自 AT24C02 数据手册中的 tWR 参数,最小 5ms。读过一遍数据手册就能理解,这个延时不能省。

5.2 页写入与页边界陷阱

AT24C02 的页大小是 8 字节,页写功能可以一次连续写入最多 8 个字节。页写的逻辑和单字节写差不多,只是数据长度变成 n。但这里有一个几乎所有初学者都会踩的坑:跨页问题。如果从地址 7 开始连续写 4 字节,这 4 字节会跨过第 0 页和第 1 页的边界,此时如果芯片内部没有做跨页保护,数据会回绕到页首,导致数据错乱。AT24C02 手册明确说明,页写不允许跨页。

跨页问题的本质是内部地址计数器的自增行为:每收到一个字节,内部地址自动加 1,但如果地址到了页边界,并不会自动进位到下一页,而是回绕到当前页的起始地址。所以程序里必须自己判断剩余字节数是否会跨页,需要分两次写。我的习惯是写一个封装的页写函数,内部自动拆分,上层不用管:

uint8_t AT24C02_WritePage(uint8_t memAddr, const uint8_t *data, uint8_t len) { uint8_t firstPageBytes = 8 - (memAddr % 8); uint8_t offset = 0; if (len <= firstPageBytes) { if (HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_W, memAddr, I2C_MEMADD_SIZE_8BIT, (uint8_t *)data, len, 100) != HAL_OK) { return 1; } HAL_Delay(5); return 0; } if (HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_W, memAddr, I2C_MEMADD_SIZE_8BIT, (uint8_t *)data, firstPageBytes, 100) != HAL_OK) { return 1; } HAL_Delay(5); offset += firstPageBytes; // 剩余部分再写 if (HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_W, memAddr + offset, I2C_MEMADD_SIZE_8BIT, (uint8_t *)(data + offset), len - offset, 100) != HAL_OK) { return 1; } HAL_Delay(5); return 0; }

两次页写之间留 5ms 写周期延时,能让每次写入都稳定完成。对于 256 字节的整片写入,也可以用类似思路拆分成 32 次页写循环。

5.3 连续读取与地址自增

连续读相对简单,AT24C02 支持顺序读:你指定起始地址后,它会自动把后续地址的数据一个个推出来。HAL 库的 HAL_I2C_Mem_Read 天然支持多字节读取,只要 Size 大于 1 即可。比如从 0 地址连续读 256 字节,代码如下:

uint8_t buffer[256]; HAL_I2C_Mem_Read(&hi2c1, AT24C02_ADDR_R, 0, I2C_MEMADD_SIZE_8BIT, buffer, 256, 100);

这里需要理解的是,当 Size 大于 1 时,HAL 库会在最后一个字节之前发送 NACK。I2C 协议规定:主机在连续读取时,最后一个字节要回 NACK 告诉从机不要继续发了。HAL 库自动处理了这一点,所以直接用即可。如果你的代码是纯软件模拟 I2C,那就必须手动管理这些细节,否则最后一个字节之后总线状态会乱。


6. 硬件 I2C 与模拟 I2C:选型、调试和常见坑

6.1 硬件 I2C 卡死的原因分析

STM32F103 的硬件 I2C 在外设初始化后,如果总线被拉低,主设备会一直等待总线释放,程序就卡死在 HAL_I2C_Mem_Write 里。这种情况我在调试初期遇到过好几次,每次都是因为从设备没有正确回应 ACK。原因通常有两类:一是地址写错,从机不认;二是总线上的上拉电阻缺失,SDA 被某个设备钳低。此外,STM32F103 的硬件 I2C 对时序的要求比较苛刻,如果 SCL 的频率配置过高,从机跟不上,也会表现为总线异常。

一旦进入硬件 I2C 卡死的状态,看门狗又没开的话,整个系统就挂住了。解决办法有两个方向:一是尽量把 I2C 通信的失败检测机制做出来,比如 HAL_OK 判断不通过就复位外设;二是在调试阶段直接用模拟 I2C,等逻辑全部跑通了再切换回硬件 I2C,比对两种实现的差异。

6.2 模拟 I2C 的调试优势

模拟 I2C 本质上是纯粹的 GPIO 操作,不依赖外设状态机,代码里每一拍都有明确的延时,所以非常容易用示波器或逻辑分析仪对应起来。把 SCL 和 SDA 接到逻辑分析仪上,可以看到每一字节的波形,跟 AT24C02 数据手册上的时序图一一对照。这个体验是硬件 I2C 没有的,硬件外设把时序细节藏在状态机里,出问题时很难定位。

我推荐的开发路径是:先用模拟 I2C 把读写 AT24C02 的代码跑通,再用逻辑分析仪记录一次完整的写命令帧和读命令帧,跟时序图校验。确认无误后,再切换到 HAL 库的硬件 I2C,此时你会对每个字节的传输过程有很强的直觉,遇到硬件 I2C 的异常也更容易猜测到原因。

6.3 常见问题与排查实录

以下是我在实际调试过程中遇到的几个典型问题,整理成了速查表。

故障现象可能原因排查思路
写完后读出来全是 0xFF写周期未等待,数据根本没写入在写操作后加 5~10ms 延时再读
指定地址前几个字节正常页写时跨页,地址回绕把长度限制在 8 字节内,按页边界拆分
偶尔写入失败,复位后数据丢失写保护引脚 WP 被拉高检查 WP 是否接 GND,确认允许写入
SCL/SDA 其中一个始终为低上拉电阻缺失,或某设备拉死总线加 4.7kΩ 上拉电阻,断电排查从设备
设备地址写 0xA0 但无应答A0/A1/A2 引脚电平与代码不一致用万用表测引脚电平,核对 7 位地址
硬件 I2C 初始化后卡死总线忙标志未被清除尝试复位 I2C 外设,或改用模拟 I2C 验证
连续读多字节时数据错位主机未在最后字节回 NACK用 HAL_I2C_Mem_Read 自动处理,不要手动逐字节读

还有一个很多人忽略的点:AT24C02 的 WP 引脚如果悬空,内部默认可能是高电平,也可能被外部干扰拉高,导致写操作无效。硬件设计时建议把 WP 明确接 GND,不要悬空。另外,AT24C02 的 SCL/SDA 引脚没有内置钳位二极管,如果总线上有其他设备在另一路供电下保持电平,可能出现漏电问题,所以共地很关键。


7. 扩展到 256 字节存储管理和掉电保存场景

7.1 字节分配与磨损均衡思路

AT24C02 只有 256 字节,放在现代嵌入式项目里实在不算大,但也足以承担关键参数存储任务。分配内存时要有意识地做区域规划:0x00~0x0F 存系统配置项,0x10~0x1F 存用户设置,0x20~0xFF 做数据区。不要把所有东西一股脑塞在一起,因为后续固件升级要预留扩展空间。

EEPROM 的擦写寿命在 100 万次左右,虽然对大多数产品够用,但如果程序里频繁写入同一个地址(比如设备计数),就需要考虑磨损均衡。做法很简单:把计数器存到多个字节,每次递增时写到下一个地址,写满一轮后再统一处理。

uint8_t counterAddr = 0x20; uint8_t counterValue; // 读取当前计数 counterValue = AT24C02_ReadByte(counterAddr); if (counterValue < 0xFF) { counterValue++; } else { // 此地址已写满,换下一个 counterAddr++; counterValue = 0; } AT24C02_WriteByte(counterAddr, counterValue);

这个例子虽然简单,但思路值得推广。EEPROM 的写入时间远大于 RAM 操作,批量写入时注意控制写次数。

7.2 掉电保存的注意事项

很多产品需要在掉电瞬间保存关键数据。STM32F103 检测掉电的方式有 ADC 监控电源电压、EXTI 外部中断、PVD 可编程电压检测器等。这里要提醒的是:掉电瞬间系统时间有限,必须优先写入最重要的数据,然后立刻进入低功耗模式。写入 AT24C02 时需要 5ms 以上的写周期,能否完成取决于掉电检测的触发电压和电源电容的放电时间。

我自己用过的一个方案是:在 12V 输入处用电容储能,当 PVD 检测到 3.0V 以下时,触发中断,停止其他任务,立即把关键数据写入 EEPROM。写入完成后把 GPIO 全部拉低,系统进入停机模式。这个方案的经验是:主电源电容要预留足够余量,至少保证 10ms 的供电时间,否则 5ms 的 EEPROM 写周期都撑不过去。

7.3 数据校验与突发写失败的补救

EEPROM 在写入过程中掉电,可能导致该字节数据不完整,表现为某些位变成 0xFF。解决的办法是给数据加校验和。比如每 16 字节数据后面跟一个校验字节,读写时计算校验和,不一致就认为数据损坏,可以回退到默认值。

我习惯的做法是定义一个存储结构体,包含前导标志、数据体和校验字节。读取时先检查前导标志,再检查校验,都通过才使用数据:

typedef struct { uint16_t magic; uint8_t data[32]; uint8_t checksum; } ConfigBlock; uint8_t validConfig(ConfigBlock *blk) { uint8_t sum = 0; if (blk->magic != 0x5A5A) return 0; for (int i = 0; i < 32; i++) sum += blk->data[i]; return (sum == blk->checksum); }

这个结构体内的字节数最好对齐到页大小边界,减少写次数。256 字节的存储容量虽然有限,但配合校验和分段管理,足够应付大多数简单产品的需求。


8. 实测记录与最终建议

我的测试环境是 STM32F103C8T6 最小系统板,CubeMX 生成 HAL 库工程,PB6/PB7 接 AT24C02 模块,外接两个 4.7kΩ 上拉电阻。先跑模拟 I2C,把单字节读写、页写、连续读都验证过,然后用逻辑分析仪抓波形,对比手册时序确认无误后,再切换到 CubeMX 生成的硬件 I2C 工程做了同样的测试。硬件 I2C 起初也出现了一次卡死,原因是我把 I2C 时钟频率设到 400kHz 后总线上有干扰,降到 100kHz 后稳定运行。之后我测试了连续写入 256 字节再读回,数据完全一致,验证了页写拆分逻辑的正确性。

这里补一个细节:用逻辑分析仪抓 I2C 波形时,触发条件要设为 SDA 下降沿,采样率至少是 I2C 时钟的 10 倍,也就是 4MHz 以上,否则波形上的边沿会被采模糊。另外,逻辑分析仪的通道区分要清楚,CH0 接 SCL,CH1 接 SDA,否则解码出来的结果是没有意义的。

如果你也在做类似项目,我有几个建议供参考:第一,先用模拟 I2C 跑通,别急着调硬件外设,调试体验完全不同;第二,AT24C02 的器件地址要在代码里用宏集中定义,不要散落各处,后续改地址只改一处;第三,写后延时 5ms 这个动作不要省,哪怕你用硬件 I2C,HAL 函数不会帮你等内部写周期;第四,硬件上 WP 引脚一定要明确接 GND 或 3.3V,不要悬空。

从 STM32F103 到其他 M 系列芯片,这套 I2C 操作思路基本通用。I2C 协议本身并不复杂,复杂度主要来自时序细节和不同器件的差异。把 AT24C02 玩透之后,再去接 OLED 屏(比如 SSD1306)、传感器(比如 HMC5883L、BMP280)都会轻松很多,因为套路是相通的。后续如果想深入,可以尝试看 I2C 的 DMA 传输、多主机仲裁,或者把 AT24C02 换成更大容量的 AT24C256,地址变成了 16 位,那时候你会更深刻地理解地址计数器的差异。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询