简介这份资源面向使用STM32F407进行嵌入式开发的工程师与学习者聚焦HAL库下I2C总线读写AT24C02 EEPROM的完整实现适合需要掌握外设驱动、数据持久化存储的初中级开发者参考。压缩包共1532个文件约7.47MB以950个c源文件与270个h头文件为主体涵盖HAL库外设驱动、ARM数学库与工程配置另有46个icf、1个uvprojx、1个ioc等工程与链接脚本文件以及hex、axf、map等编译产物便于直接打开验证。资源围绕I2C时钟与GPIO复用配置、HAL_I2C_Init初始化、主模式收发函数调用、AT24C02页写与随机读时序、ACK/NACK应答处理及DMA传输等关键环节展开可帮助读者理解从寄存器抽象到应用层调用的完整链路。目前已有427人学习下载适合作为I2C通信与EEPROM读写调试的实践参考。1. 从一份 HAL 库 I2C 读写 AT24C02 的工程包说起如果你手头正捏着一块 STM32F407 的开发板想找个能直接跑通的 I2C 读写 EEPROM 的例程那这份基于 HAL 库的 AT24C02 工程包大概率能省你一个下午。它不是什么大而全的框架就是一份把 I2C 外设配置、AT24C02 页写时序、字节读写和连续读写都封装好的最小可运行工程。STM32F407 的 I2C 外设用起来不算省心HAL 库又把很多细节藏进了HAL_I2C_Mem_Write这类函数里新手照着 CubeMX 生成代码后经常卡在“为什么写进去读出来全是 0xFF”。这份资源的价值就在于它把从 CubeMX 配置到实际读写验证的整条链路都摊开了适合刚接触 STM32 HAL 库、想拿 EEPROM 练手 I2C 通信的嵌入式开发者。下面我按自己拆包复现的顺序把这份工程讲透。2. 工程结构与 CubeMX 配置先把 I2C 外设的底子打对拿到一份 HAL 库工程我习惯先看目录结构和.ioc文件因为 I2C 能不能通八成在配置阶段就决定了。这份工程的结构很典型Core 目录放主逻辑Drivers 目录放 HAL 库和 CMSISAT24C02 的驱动单独抽了一个at24c02.c/h这种分层对后续移植到别的板子很友好。2.1 目录拆解与关键文件定位工程根目录下通常能看到这些内容Project/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── i2c.h │ │ └── at24c02.h │ └── Src/ │ ├── main.c │ ├── i2c.c │ └── at24c02.c ├── Drivers/ │ ├── STM32F4xx_HAL_Driver/ │ └── CMSIS/ ├── MDK-ARM/ (或 STM32CubeIDE 工程目录) └── Project.ioci2c.c里是 CubeMX 生成的 I2C 初始化代码at24c02.c是这份资源的核心里面封装了字节写、页写、当前地址读、随机读和连续读。我一般会先打开at24c02.h看对外暴露了哪些函数再倒推它依赖了哪些 HAL 接口。这份工程用的是HAL_I2C_Mem_Write和HAL_I2C_Mem_Read而不是手动拼HAL_I2C_Master_Transmit说明作者走的是“内存地址式”读写这对 EEPROM 这类带内部地址的设备是最省事的写法。2.2 CubeMX 里 I2C 参数怎么设如果你要自己从零配一份CubeMX 里 I2C 的配置有几个参数不能拍脑袋。STM32F407 的 I2C 挂在 APB1 上时钟源默认 42MHz配置界面里主要盯这几个参数推荐值说明I2C Speed ModeStandard / FastAT24C02 支持 400kHzFast Mode 够用Clock Speed100000 或 400000先用 100kHz 调通再往上提Duty Cycle2:1Fast Mode 下的标准占空比Addressing Mode7-bitAT24C02 是 7 位地址器件Dual AddressDisable单设备不需要配置完生成代码后MX_I2C1_Init里会看到hi2c1.Init.ClockSpeed 400000这样的赋值。我建议第一次调先把 ClockSpeed 降到 100000因为 400kHz 对布线、上拉电阻和电源质量都有要求板子上如果只焊了 4.7k 上拉长杜邦线一接波形很容易塌。2.3 AT24C02 的地址与写保护引脚AT24C02 的 7 位器件地址是1010加上 A2/A1/A0 三个引脚的电平。工程里一般把 A2/A1/A0 全接地所以地址是0x50。HAL 库函数要求传入的是左移一位后的 8 位地址也就是0xA0。这一点是新手翻车重灾区HAL_I2C_Mem_Write的第一个参数DevAddress要写0xA0不是0x50。另外 AT24C02 的 WP 引脚如果拉高整个芯片进入写保护只能读不能写。我见过有人调了半天写不进去最后发现是模块上 WP 被默认上拉了。常见做法是 WP 接地或者用 GPIO 控制需要写的时候拉低。/* at24c02.h 里的地址定义注意左移一位 */ #define AT24C02_ADDR_WRITE 0xA0 #define AT24C02_ADDR_READ 0xA1 #define AT24C02_PAGE_SIZE 8 #define AT24C02_MEM_SIZE 256这段定义里AT24C02_ADDR_WRITE是写地址AT24C02_ADDR_READ是读地址。虽然HAL_I2C_Mem_Read内部会自动把地址最低位置 1但显式定义出来可读性更好。AT24C02_PAGE_SIZE是 8 字节这个值在页写函数里会用到写跨页的时候必须按它切分。3. 字节读写与页写时序把 HAL 接口用对配置对了接下来就是怎么调 HAL 接口。这份工程把 AT24C02 的操作分成了字节写、页写、随机读和连续读四个函数每个函数对应 EEPROM 的一种时序。理解这些时序比死记 HAL 函数原型有用得多。3.1 字节写与写周期等待AT24C02 的字节写时序是发启动条件发器件地址加写位发内存地址发一个数据字节发停止条件。HAL 库用HAL_I2C_Mem_Write一个函数就包完了。/** * brief 向 AT24C02 指定地址写入一个字节 * param mem_addr: 内部内存地址 0~255 * param data: 待写入的数据 * retval HAL_OK 表示成功 */ HAL_StatusTypeDef AT24C02_WriteByte(uint16_t mem_addr, uint8_t data) { HAL_StatusTypeDef status; /* MemAddressSize 用 I2C_MEMADD_SIZE_8BIT因为 AT24C02 地址只有 8 位 */ status HAL_I2C_Mem_Write(hi2c1, AT24C02_ADDR_WRITE, mem_addr, I2C_MEMADD_SIZE_8BIT, data, 1, 100); if (status ! HAL_OK) { return status; } /* 关键EEPROM 写完需要内部擦写时间典型 5ms这里给 10ms 余量 */ HAL_Delay(10); return HAL_OK; }这里有两个参数值得说。I2C_MEMADD_SIZE_8BIT表示内存地址是 8 位AT24C02 容量 2Kbit 也就是 256 字节地址刚好 8 位用 16 位反而会多发一个地址字节导致时序错。Timeout给 100ms 是 HAL 库等待标志位的超时不是 EEPROM 的写周期。真正让数据落盘的是后面那个HAL_Delay(10)AT24C02 的写周期最大 5ms不给延时连续写就会丢数据这是最隐蔽的坑之一。3.2 页写与跨页边界处理AT24C02 支持 8 字节页写一次 I2C 事务可以连续写 8 个字节超过 8 字节就会回卷到页首把前面的数据覆盖掉。所以页写函数必须处理跨页。/** * brief 向 AT24C02 写入任意长度数据自动处理页边界 * param mem_addr: 起始内存地址 * param pdata: 数据缓冲区 * param len: 数据长度 */ HAL_StatusTypeDef AT24C02_WriteBuffer(uint16_t mem_addr, uint8_t *pdata, uint16_t len) { HAL_StatusTypeDef status; uint16_t page_remain; uint16_t write_len; while (len 0) { /* 计算当前页还能写多少字节 */ page_remain AT24C02_PAGE_SIZE - (mem_addr % AT24C02_PAGE_SIZE); write_len (len page_remain) ? len : page_remain; status HAL_I2C_Mem_Write(hi2c1, AT24C02_ADDR_WRITE, mem_addr, I2C_MEMADD_SIZE_8BIT, pdata, write_len, 100); if (status ! HAL_OK) { return status; } HAL_Delay(10); /* 每页写完都要等擦写周期 */ mem_addr write_len; pdata write_len; len - write_len; } return HAL_OK; }page_remain的计算是这段代码的核心。假设从地址 6 开始写 10 个字节第一轮page_remain 8 - (6 % 8) 2先写 2 字节到地址 7然后地址跳到 8第二轮写满 8 字节。如果不做这个切分直接调HAL_I2C_Mem_Write写 10 字节AT24C02 会在写到第 8 字节后回卷到页首把地址 0 和 1 覆盖掉读出来就是错乱的数据。这个坑我在早期项目里踩过现象是数组前两个元素莫名其妙变成最后两个元素的值。3.3 随机读与连续读读操作比写简单因为不需要等擦写周期。随机读是“假写地址真读数据”先发器件地址加写位发内存地址再发重复启动条件发器件地址加读位然后读数据。HAL 的HAL_I2C_Mem_Read把这些都包了。/** * brief 从 AT24C02 指定地址读取任意长度数据 * param mem_addr: 起始内存地址 * param pdata: 接收缓冲区 * param len: 读取长度 */ HAL_StatusTypeDef AT24C02_ReadBuffer(uint16_t mem_addr, uint8_t *pdata, uint16_t len) { /* 读操作没有页限制可以一次读完整个容量 */ return HAL_I2C_Mem_Read(hi2c1, AT24C02_ADDR_READ, mem_addr, I2C_MEMADD_SIZE_8BIT, pdata, len, 100); }注意这里第一个参数用的是AT24C02_ADDR_READ也就是0xA1。虽然 HAL 库内部会根据读写方向自动调整最低位但传0xA1语义更清晰。读操作没有页边界问题因为 EEPROM 内部地址指针会自动递增可以一口气读完 256 字节。Timeout给 100ms 对 256 字节 100kHz 来说绰绰有余100kHz 下每字节 9 个时钟周期256 字节大约 23ms。4. 避坑与排查I2C 读写 EEPROM 最常见的五个翻车点I2C 这个外设玄学得很同样的代码换块板子就挂。下面这五个问题是我在调 AT24C02 时反复遇到的按“现象 → 原因 → 解决”整理出来你对着排查能省不少时间。4.1 读出来全是 0xFF现象写进去数据读回来全是0xFF或者第一次读对、复位后再读就变0xFF。原因0xFF是 EEPROM 擦除后的默认值说明写操作根本没生效。最常见的是器件地址传错把0x50直接传给了 HAL 函数HAL 内部再左移一位变成0xA0实际发出去的是0xA0但如果你传的是0xA0HAL 又左移变成0x140低 8 位是0x40地址就错了。另一个原因是 WP 引脚被拉高芯片处于写保护状态。解决确认DevAddress传的是0xA0用逻辑分析仪抓一下 I2C 波形看第一个字节是不是0xA0。同时用万用表量 WP 引脚电平确保写操作时为低。4.2 连续写多字节后数据错乱现象写 10 个字节读回来发现第 8、9 个字节覆盖了第 0、1 个字节。原因没有处理页边界AT24C02 页大小 8 字节跨页写会回卷。解决用 3.2 节的AT24C02_WriteBuffer按AT24C02_PAGE_SIZE切分每页写完加 10ms 延时。如果你用的是别的型号比如 AT24C16 页大小也是 16 字节改宏定义就行。4.3 HAL_I2C_Mem_Write 返回 HAL_BUSY现象函数返回HAL_BUSY或者卡在HAL_I2C_Mem_Write里出不来。原因I2C 总线被拉低或者上一次事务没正确结束。常见于上拉电阻缺失或阻值过大总线电容太大导致上升沿太缓HAL 库等不到标志位。解决检查 SDA/SCL 上拉电阻4.7k 是常用值如果总线长或者设备多降到 2.2k。用示波器看波形上升沿如果超过 1us 还没到高电平就是上拉不够。另外确认hi2c1句柄没有被多个任务同时调用裸机下也要避免中断里调 I2C。4.4 写完之后立刻读数据没更新现象写完马上读读到的还是旧数据延时一段时间再读就对了。原因AT24C02 的写周期最大 5ms在这期间芯片不响应任何 I2C 命令HAL 库如果在这期间发起读会返回HAL_ERROR或者读到旧值。解决每次写操作后加HAL_Delay(10)。更严谨的做法是用“应答查询”机制循环发器件地址直到收到 ACK表示芯片内部写完成。但 10ms 延时对大多数应用足够了简单可靠。4.5 换一块板子就通信失败现象同一份代码A 板子能跑B 板子不行。原因B 板子的 I2C 引脚可能被别的外设占用或者 CubeMX 里引脚复用没配对。STM32F407 的 I2C1 默认在 PB6/PB7但也可以重映射到 PB8/PB9如果.ioc里配的是 PB8/PB9而实际接线在 PB6/PB7自然不通。解决打开 CubeMX 看引脚图确认 SCL/SDA 对应的物理引脚再用万用表通断档从 MCU 引脚量到 EEPROM 模块引脚排除虚焊。我习惯在main.c初始化后加一句翻转 LED确认程序确实跑到了 I2C 初始化之后。5. 进阶验证用读写测试和逻辑分析仪把工程跑透工程能编译下载不代表 I2C 时序没问题我一般会写一个自检函数把 256 字节全写一遍再读回来比对同时用逻辑分析仪抓一次完整波形确认起始条件、地址字节、ACK 和停止条件都对得上。5.1 全容量读写自检下面这个测试函数放在main.c的while(1)之前跑一次能快速判断 EEPROM 是否工作正常。/** * brief AT24C02 全容量读写自检 * retval 0 表示通过非 0 表示失败位置 */ uint16_t AT24C02_SelfTest(void) { uint8_t write_buf[AT24C02_MEM_SIZE]; uint8_t read_buf[AT24C02_MEM_SIZE]; uint16_t i; /* 构造测试数据地址低 8 位异或 0x5A */ for (i 0; i AT24C02_MEM_SIZE; i) { write_buf[i] (uint8_t)(i ^ 0x5A); } /* 全片写入 */ if (AT24C02_WriteBuffer(0, write_buf, AT24C02_MEM_SIZE) ! HAL_OK) { return 0xFFFF; /* 写入失败 */ } /* 全片读出 */ if (AT24C02_ReadBuffer(0, read_buf, AT24C02_MEM_SIZE) ! HAL_OK) { return 0xFFFE; /* 读取失败 */ } /* 逐字节比对 */ for (i 0; i AT24C02_MEM_SIZE; i) { if (read_buf[i] ! write_buf[i]) { return i 1; /* 返回第一个不匹配的位置从 1 开始 */ } } return 0; /* 全部通过 */ }测试数据用i ^ 0x5A而不是直接写i是为了避免地址线和数据线短路时“碰巧读对”的假象。如果返回 0说明 256 字节全通过返回其他值直接定位到出错字节。这个函数跑一次大约 256/8 * 10ms 320ms加上读的时间不到 400ms可以接受。5.2 逻辑分析仪抓波形的几个观察点没有逻辑分析仪的话用示波器双通道看 SDA/SCL 也行。重点看这几个地方观察点正常表现异常表现与含义起始条件SCL 高时 SDA 由高变低SDA 没变化I2C 外设没启动地址字节第 1 字节为 0xA0写出现 0x50 或 0x40地址传错ACK 位第 9 个时钟 SDA 被拉低SDA 保持高从机没应答器件地址错或没供电停止条件SCL 高时 SDA 由低变高没有停止位HAL 库超时或总线锁死我一般会把逻辑分析仪的采样率设到 4MHz 以上100kHz 的 I2C 波形一个位 10us4MHz 采样能看清每个边沿。抓到的数据用协议解码器直接解成地址和数据比对着波形数时钟快得多。5.3 从这份工程往外延伸的两个方向把 AT24C02 跑通之后这份工程还能往两个方向用。一是换更大容量的 EEPROM比如 AT24C256地址变成 16 位I2C_MEMADD_SIZE_8BIT要改成I2C_MEMADD_SIZE_16BIT页大小变成 64 字节其他逻辑不变。二是把 I2C 读写封装成带超时重试的接口在AT24C02_WriteByte外面套一层循环失败重试三次每次间隔 20ms这样在电源波动或者总线干扰时更稳。从那以后我每次调 I2C 设备都强制先用逻辑分析仪抓一次起始条件和地址字节确认波形对了再写业务代码。这个习惯帮我省掉了大量“代码看着没问题但就是不通”的排查时间。希望这份工程和上面的排查思路能帮你把 STM32F407 的 I2C 读写 AT24C02 一次跑通。本文还有配套的精品资源点击获取