从零开始:用STM32 CubeMX搞定I²C读写EEPROM的完整实战指南
你有没有遇到过这样的场景?设备掉电后,用户设置全没了;调试时反复烧录Flash太伤寿命;或者想记录一些运行日志却苦于没有可靠的存储空间?
这时候,外接一个小小的AT24C系列EEPROM,通过I²C总线和 STM32 对接,就能轻松解决这些问题。而借助STM32 CubeMX + HAL库,原本繁琐复杂的底层配置,现在只需点几下鼠标就能自动生成。
本文不讲空话,带你一步步从硬件连接、CubeMX配置到代码实现,完整走通 I²C 读写 EEPROM 的全过程。无论你是刚入门的新手,还是需要快速搭建功能模块的工程师,都能拿来即用。
为什么选择I²C + 外部EEPROM?
STM32 自带 Flash,但不适合频繁写入——它的擦写寿命通常只有1万次左右,而且每次写操作前还得先擦除页,流程复杂。相比之下,像AT24C02这样的串行EEPROM:
- 擦写寿命高达100万次
- 数据可保存100年
- 接口简单,仅需两根线(SCL/SDA)
- 支持多设备并联
- 工作电压宽(1.8V~5.5V),兼容性强
再加上 I²C 协议本身引脚少、布线简洁、支持多主多从,是连接传感器、RTC、配置存储器等低速外设的事实标准。
所以,在智能家居控制板、工业采集节点、医疗监测设备中,这套组合几乎是标配。
先搞懂I²C通信的核心机制
在动手之前,得知道“它到底是怎么工作的”。
I²C是怎么传数据的?
I²C 是一种同步半双工总线,靠两条线完成通信:
-SCL:时钟线,由主机驱动
-SDA:数据线,双向传输
所有设备都挂在同一条总线上,靠“地址”来区分彼此。
一次典型的读写过程包括:
1.起始信号(Start):SCL高电平时,SDA从高变低
2.发送设备地址 + 方向位(7位地址 + 1位R/W)
3.等待应答(ACK):从机拉低SDA表示“我收到了”
4.数据传输:按字节进行,高位在前
5.停止信号(Stop):SCL高电平时,SDA从低变高
📌 小贴士:如果某个环节没收到ACK,说明设备没响应——可能是地址错了、电源没上、或线路虚焊。
常见速率模式有哪些?
| 模式 | 速率 | 应用场景 |
|---|---|---|
| 标准模式 | 100 kbps | 最常用,稳定可靠 |
| 快速模式 | 400 kbps | 要求稍高速度 |
| 高速模式 | 3.4 Mbps | 特殊需求,需额外使能 |
我们一般用100kHz就够了,稳定性更好,对布线和上拉电阻要求也不那么苛刻。
AT24C02是怎么被读写的?
以最常见的AT24C02(2Kbit = 256字节)为例,它是如何接收命令和返回数据的?
写操作三步走
[Start] → [设备写地址] → [内存地址] → [数据1] → [数据2]... → [Stop]比如你要把"AB"写到地址0x10处:
1. 主机发 Start
2. 发送0xA0(AT24C02写地址,A0=A1=A2=GND)
3. 发送内存地址0x10
4. 发送数据0x41,0x42
5. Stop,芯片开始内部写入(耗时约5ms)
读操作分两种方式
方法一:当前地址读(Current Address Read)
上次读/写到了哪个地址,下次就从那里继续读。适合连续读取。
[Start] → [设备读地址(0xA1)] → [接收数据] → [Stop]方法二:随机读(Random Read)——更常用
指定任意地址读取:
[Start] → [写地址(0xA0)] → [目标地址] → [Repeated Start] → [读地址(0xA1)] → [接收数据] → [Stop]注意中间有个重复起始(Repeated Start),不能发Stop,否则会重置地址指针。
硬件怎么接?别忘了这几个细节!
这是很多初学者踩坑最多的地方。
典型连接图(STM32F103C8T6 + AT24C02)
STM32 AT24C02 PB6 (I2C1_SCL) ──▶ SCL PB7 (I2C1_SDA) ──▶ SDA A0, A1, A2 ──▶ GND ← 设备地址为 0xA0 WP ──▶ GND ← 允许写入 VCC ──▶ 3.3V GND ──▶ GND SCL ──┬── 4.7kΩ ──▶ VCC │ SDA ──┘关键设计要点
| 项目 | 注意事项 |
|---|---|
| 上拉电阻 | 必须加!推荐4.7kΩ(3.3V系统)。阻值太大会导致上升沿缓慢,通信失败;太小则功耗高。 |
| A0~A2引脚 | 接地为0,接VCC为1,决定设备地址。多片共存时必须错开。 |
| WP引脚 | 写保护。正常工作接地,防止误写可拉高。 |
| 去耦电容 | 在VCC附近加0.1μF陶瓷电容,滤除电源噪声。 |
| 总线长度 | 尽量短,超过30cm要考虑信号完整性问题。 |
💡 经验之谈:如果你发现I²C总是NACK,第一反应应该是查上拉电阻有没有焊、电源是否正常、GND是否共地。
STM32 CubeMX配置:三步搞定I²C初始化
打开STM32CubeMX,选好你的芯片型号(比如STM32F103C8T6),开始配置。
第一步:启用I2C1
在 Pinout 视图中找到 I2C1:
- 点击 PB6 → 功能选为I2C1_SCL
- 点击 PB7 → 功能选为I2C1_SDA
这两个引脚会自动变成开漏输出,并开启复用功能。
第二步:配置I2C参数
双击“I2C1”进入参数设置:
| 参数 | 设置值 | 说明 |
|---|---|---|
| Mode | I2C | 不要选成 SMBus |
| Clock Speed | 100 kHz | 标准模式,稳妥 |
| Pull-up Resistor | External | 提醒自己外部已加上拉 |
| Addressing Mode | 7-bit | AT24Cxx使用7位地址 |
其余保持默认即可。
第三步:生成代码
在 Project Manager 中建议勾选:
✅Generate peripheral initialization as a pair of .c/.h files per peripheral
这样每个外设都有独立的初始化文件,结构更清晰,方便移植。
点击 “Generate Code”,工程就准备好了。
核心代码实现:HAL库函数怎么用?
CubeMX已经帮你生成了MX_I2C1_Init()初始化函数,接下来只需要调用 HAL 提供的API完成读写。
定义设备地址
#define EEPROM_ADDR 0xA0 // 注意:这是7位地址左移一位后的8位形式⚠️ 重点提醒:HAL库中的设备地址必须是8位格式!
例如,AT24C02的7位地址是1010000(A0~A2接地),转换成8位就是:
- 写操作:0b10100000 = 0xA0
- 读操作:0b10100001 = 0xA1
写数据到EEPROM
uint8_t txData[] = "Hello"; uint16_t memAddress = 0x00; // 起始内存地址 if (HAL_I2C_Mem_Write(&hi2c1, EEPROM_ADDR, // 设备地址 memAddress, // 内存地址 I2C_MEMADD_SIZE_8BIT, // 地址宽度(8位) txData, // 数据缓冲区 sizeof(txData), // 数据长度 100) != HAL_OK) { // 超时100ms Error_Handler(); } HAL_Delay(10); // ⚠️ 必须延时!等待内部写周期完成(典型5ms)从EEPROM读数据
uint8_t rxData[6] = {0}; if (HAL_I2C_Mem_Read(&hi2c1, EEPROM_ADDR, memAddress, I2C_MEMADD_SIZE_8BIT, rxData, 5, // 读5个字节 100) != HAL_OK) { Error_Handler(); }函数解析
| 函数 | 作用 |
|---|---|
HAL_I2C_Mem_Write() | 自动执行“写设备地址 → 发送内存地址 → 写数据”流程 |
HAL_I2C_Mem_Read() | 自动处理“写地址 → 目标位置 → Repeated Start → 读数据” |
I2C_MEMADD_SIZE_8BIT | 表示内存地址是8位(适用于≤256B的EEPROM) |
I2C_MEMADD_SIZE_16BIT | 用于AT24C64等大容量型号(地址范围更大) |
常见问题与调试技巧(避坑指南)
❌ 问题1:总是返回 HAL_ERROR 或 HAL_TIMEOUT
可能原因:
- 上拉电阻未焊接或阻值过大
- SDA/SCL 接反了
- 电源未上电或接触不良
- 地址错误(A0~A2设置不对)
🔧排查方法:
1. 用万用表测VCC和GND是否正常
2. 用逻辑分析仪抓包看是否有Start信号、ACK回应
3. 查看示波器上的SCL/SDA波形是否完整
❌ 问题2:写进去的数据读不出来
最大嫌疑:没等写周期结束!
AT24C02每次写完都要花最多5ms完成内部编程。在这期间,任何访问都会被忽略甚至返回NACK。
✅ 解决方案:每次写完后加HAL_Delay(10);
也可以采用“轮询确认法”:
while (HAL_I2C_Mem_Write(&hi2c1, EEPROM_ADDR, 0, I2C_MEMADD_SIZE_8BIT, &dummy, 1, 100) != HAL_OK);不断尝试写一个字节,直到成功为止,说明芯片已准备好。
❌ 问题3:跨页写入数据错乱
AT24C02每页8字节,若从地址7写入4个字节,会变成:
Page N: [7][0][1][2] ← 实际写到了下一页开头!但I²C协议不会自动分页,必须手动拆分成两次写入。
✅ 正确做法:
// 判断是否会跨页 uint16_t page_size = 8; uint16_t offset_in_page = memAddress % page_size; if (offset_in_page + data_len > page_size) { // 分两次写 uint16_t first_part = page_size - offset_in_page; HAL_I2C_Mem_Write(..., data, first_part, ...); HAL_Delay(10); HAL_I2C_Mem_Write(..., data + first_part, data_len - first_part, ...); HAL_Delay(10); } else { // 单次写完 HAL_I2C_Mem_Write(...); HAL_Delay(10); }如何提升可靠性?几个实用技巧
✅ 添加CRC校验
在写入数据的同时,计算一段CRC并一起存入,读出后再验证,防止数据损坏。
uint8_t data[10]; uint8_t crc = crc8(data, 10); EEPROM_Write_Page(addr, data, 10); EEPROM_Write_Byte(addr + 10, crc);读取时重新计算对比,不一致则重试。
✅ 实现重试机制
通信受干扰可能导致失败,加入最多3次重试:
for (int i = 0; i < 3; i++) { if (HAL_I2C_Mem_Write(...) == HAL_OK) { HAL_Delay(10); break; } HAL_Delay(5); }✅ 使用DMA提升效率(进阶)
对于批量数据传输,可以用DMA模式解放CPU:
HAL_I2C_Mem_Write_DMA(&hi2c1, EEPROM_ADDR, addr, I2C_MEMADD_SIZE_8BIT, buf, len);配合中断回调处理完成事件,适合大容量EEPROM连续写入场景。
总结一下最关键的知识点
到现在为止,你应该已经掌握了整套流程。我们再来快速回顾一遍核心内容:
- I²C协议只需两根线就能实现多设备通信,非常适合资源紧张的MCU。
- AT24C02是性价比极高的外部存储方案,寿命长、接口简单。
- STM32 CubeMX让I²C初始化变得可视化,一键生成代码,避免寄存器配置错误。
- HAL_I2C_Mem_Write/Read是操作带子地址设备的关键函数,专为EEPROM这类器件设计。
- 写后必须延时至少10ms,否则后续操作可能失败。
- 上拉电阻不可少,4.7kΩ是3.3V系统的黄金搭配。
- 多设备时注意地址冲突,合理设置A0~A2引脚。
- 跨页写入要分段处理,不然数据会“跳页”。
如果你正在做一个需要保存配置、记录状态的小项目,不妨试试这个方案。成本不到一块钱的EEPROM,配上CubeMX几分钟配置,就能让你的产品拥有掉电不丢数据的能力。
想试试更大的容量?换成 AT24C64,只要把地址改成16位模式即可:
c HAL_I2C_Mem_Write(&hi2c1, 0xA0, target_addr, I2C_MEMADD_SIZE_16BIT, data, len, 100);
整个框架完全通用。
如果你在实现过程中遇到了其他问题,欢迎留言交流。下一篇文章我们可以聊聊如何封装一个通用的EEPROM驱动模块,支持自动分页、缓存管理、磨损均衡等功能。