十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32硬件I2C读写EEPROM:AT24C02驱动开发与调试全攻略

STM32硬件I2C读写EEPROM:AT24C02驱动开发与调试全攻略 简介面向 STM32F407 嵌入式开发者的 HAL 库 I2C 读写 AT24C02 EEPROM 完整工程源码包。资源以实际可编译的 Keil MDK 工程为核心覆盖 RCC/GPIO/I2C 初始化、主模式收发、AT24C02 分页读写与错误处理适合需要快速掌握 HAL 库 I2C 驱动或移植存储模块的开发者。压缩包共 1532 个文件约 7.47 MB包含 950 个 C 源码、270 个头文件、71 个启动汇编文件以及链接脚本、工程配置、HEX 固件、ioc 图形化配置和调试辅助文件目录结构完整。其中的 txt 说明和 map/axf 构建产物可辅助理解编译链接过程、定位工程配置问题。已有 423 人学习下载具备一定参考价值。这套工程既提供了 STM32F407 HAL 库调用的完整例程又展示了 AT24C02 的读写时序、寄存器地址与分页机制通过阅读底层文件还能梳理时钟树、复用引脚配置以及 I2C 中断/DMA 设计思路对入门 I2C 通信和 EEPROM 应用有直接帮助。 做STM32开发的人大部分都会碰到一个需求用I2C接口去读写EEPROM最常见的就是AT24C02。这个需求几乎贯穿所有需要掉电保存参数的项目——不管是保存设备校准值、记录运行状态还是临时缓存数据。我最近在STM32F407上用HAL库重写了这块驱动顺手把整个流程整理出来从CubeMX配置到最终调试该踩的坑和该注意的细节都会提到对刚接触HAL库I2C的人应该会很有帮助。这篇文章我会重点聊几件事为什么优先用硬件I2C而不用模拟I2C、AT24C02的地址格式和页写机制是怎么回事、HAL库的Mem_Write和Mem_Read到底怎么用、以及调试时最常见的几个“卡死”和“数据错乱”问题怎么解决。全程直接给代码、给参数、给步骤不绕弯子。1. 方案选型与整体设计思路1.1 硬件I2C还是模拟I2C先说大家最纠结的问题I2C用硬件外设还是用GPIO模拟我这次选择STM32F407的硬件I2C1外设原因是实际项目里需要在读写EEPROM的同时还要驱动OLED和传感器多个I2C设备挂同一条总线上硬件I2C的时序稳定性更好CPU占用也更低。再说说两者的区别方便你根据自己的项目情况取舍硬件I2C时序由芯片内部硬件产生不占用CPU配合中断或DMA可以做到高效收发。但F407的I2C外设老生常谈的“BUSY锁定”问题确实让人头疼不过HAL库已经做了不少处理只要配置正确实际体验并不差。模拟I2CGPIO翻转用IO口手动拉高拉低模拟时序胜在灵活、不易锁死缺点就是CPU要一直参与主频紧张、或者总线速率要求高的时候不太合适。我这次用的是硬件I2C1对应引脚PB6SCL和PB7SDA代码上配合HAL库的阻塞式API简单直接逻辑也清晰。如果你的板子上I2C引脚不是这两个记得在CubeMX里对应改一下。另外一个要提前想清楚的点是总线上接几个设备、设备地址分别是什么。AT24C02的硬件地址由A0、A1、A2三个引脚决定我的板子上这三个引脚全部接地地址是0xA08位写地址。要注意的是HAL库的HAL_I2C_Mem_Write函数的DevAddress参数填的是“8位地址”也就是0xA0不是0x50这个非常容易弄反后文排查部分还会提到。1.2 AT24C02芯片特性与I2C协议要点AT24C02是一个2Kb256字节的串行EEPROM内部按8字节一页组织总共32页。I2C通信时支持单字节读写也支持页写。写操作最麻烦的地方在于页写不能跨页一次最多写8个字节超过8字节就会回卷到当前页的起始位置把已经写入的数据覆盖掉这是最容易踩的坑。再一个是写周期等待。AT24C02每次写操作完成后内部会进行一轮编程tWR典型值为5ms在这段时间内芯片不响应任何命令你必须等它完成。代码里用轮询ACK的方式最稳妥也就是写完后尝试发一个空的起始信号加地址字节芯片有ACK应答就说明它空闲了。速率方面AT24C02支持标准模式100kHz和快速模式400kHz。我这次用的100kHz标准模式信号裕量更充裕调试时更容易排除速率过高带来的问题。如果你是批量高速场景400kHz也行但要确保总线上拉电阻和寄生电容不会让边沿太缓。2. CubeMX配置与工程初始化2.1 引脚分配与时钟树检查打开STM32CubeMX芯片型号选STM32F407VET6按你自己的板子来在Pinout Configuration视图里找到I2C1将Mode设为I2C标准模式或快速模式都可以取决于你的需求。CubeMX会自动把PB6和PB7分配给I2C1_SCL和I2C1_SDA。然后到Clock Configuration视图里确认APB1总线时钟。这一步很多人会忽略但很关键——I2C1是挂在APB1上的如果APB1时钟配置不对I2C的实际通信速率会和你预期的不一致。F407的APB1上限是42MHz我的工程里系统主频168MHzAPB1分频系数为4APB142MHzI2C外设时钟就是42MHz。CubeMX会自动根据I2C外设时钟和目标速率计算分频寄存器值你在I2C1的Parameter Settings里把Speed Mode设为Standard Mode100kHz即可下面的Timing Register会自动生成一般不需要手动改。如果改成Fast Mode时钟源42MHz时400kHz也足够但注意总线上挂多个设备时100kHz更不容易出错。2.2 生成代码与初始化检查配置完成后直接生成工程选MDK-ARM或STM32CubeIDE都行。初始化代码集中在main.c里关键部分是static void MX_I2C1_Init(void) { hi2c1.Instance I2C1; hi2c1.Init.ClockSpeed 100000; hi2c1.Init.DutyCycle I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 0; hi2c1.Init.AddressingMode I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode I2C_DUALADDRESS_DISABLE; hi2c1.Init.OwnAddress2 0; hi2c1.Init.GeneralCallMode I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode I2C_NOSTRETCH_DISABLE; if (HAL_I2C_Init(hi2c1) ! HAL_OK) { Error_Handler(); } }这里有几个参数要说明ClockSpeed是目标速率设为100000即100kHzOwnAddress1是STM32自己的从机地址我们没有让STM32做从机填0就行AddressingMode是7位地址模式AT24C02也工作在7位地址模式一致就可以了。另外我习惯生成后检查一下GPIO初始化确认PB6和PB7被配成了开漏输出并开启了上拉。HAL库的GPIO_Init结构体里会设置好GPIO_MODE_AF_OD复用开漏模式上拉电阻默认也打开了这是I2C正常工作的基础。如果发现SDA和SCL没有配上拉通信波形会很差甚至完全不通。3. AT24C02驱动核心代码与实现3.1 单字节读写HAL库Mem函数用法直接上手写核心代码。HAL库为我们封装好了I2C内存操作的API最常用的就是HAL_I2C_Mem_Write和HAL_I2C_Mem_Read它们和我们之前用模拟I2C时手动拼地址、发数据的过程是对应的只是HAL库把这一整套时序全部封装好了。先看单字节读uint8_t AT24C02_ReadByte(uint8_t memAddr) { uint8_t data 0; if (HAL_I2C_Mem_Read(hi2c1, AT24C02_DEV_ADDR, memAddr, I2C_MEMADD_SIZE_8BIT, data, 1, 100) ! HAL_OK) { /* 读失败处理 */ } return data; }第一个参数是I2C句柄指向hi2c1。第二个参数是设备地址注意这里是0xA0。第三个参数是EEPROM内部的内存地址也就是0~255。第四个参数I2C_MEMADD_SIZE_8BIT表示内部地址是8位宽度因为AT24C02一共就256字节一个字节足够表达。如果你的EEPROM容量更大比如AT24C256有32KB那就要用16位地址模式。第五、第六个参数分别是要读的数据缓冲区和长度这里读1字节。最后一个参数是超时时间单位毫秒阻塞模式下如果总线异常会卡在这里直到超时返回。单字节写也非常类似uint8_t AT24C02_WriteByte(uint8_t memAddr, uint8_t data) { if (HAL_I2C_Mem_Write(hi2c1, AT24C02_DEV_ADDR, memAddr, I2C_MEMADD_SIZE_8BIT, data, 1, 100) ! HAL_OK) { return 1; } /* 等待写周期结束 */ AT24C02_WaitForWriteComplete(); return 0; }注意点Mem_Write发送的是“起始信号 设备地址 内存地址 数据”的完整帧和Master_Transmit不一样。如果写成HAL_I2C_Master_Transmit就把内存地址也当成数据发出去了EEPROM会理解成“往某个内存地址写一个数”但由于没有指定地址数据会乱掉。所以这里必须用Mem系列函数。3.2 页写与跨页处理逻辑AT24C02一次最多写8字节且必须在同一页内。让用户自己保证每次写入都在8字节范围内不现实所以驱动里最好封装一个“跨页自动拆分”的写函数。我是这样设计的先算出当前页中剩余可写的字节数再根据剩余要写的长度决定分几次写。比如从地址0x05开始写10个字节第一页还剩3个字节0x05、0x06、0x07那第一次写3字节第二次从新页0x08开始写7字节。void AT24C02_WriteBytes(uint8_t memAddr, uint8_t *data, uint16_t len) { uint8_t pageSize 8; uint16_t offset 0; while (offset len) { /* 当前这一页还能写多少字节 */ uint8_t remainInPage pageSize - (memAddr % pageSize); /* 本批写入字节数 min(剩余页空间, 剩余总长度) */ uint8_t writeLen (len - offset) remainInPage ? (len - offset) : remainInPage; if (HAL_I2C_Mem_Write(hi2c1, AT24C02_DEV_ADDR, memAddr, I2C_MEMADD_SIZE_8BIT, data[offset], writeLen, 100) ! HAL_OK) { /* 写失败处理 */ } AT24C02_WaitForWriteComplete(); memAddr writeLen; offset writeLen; } }这个函数的重点就是remainInPage的计算用当前地址对页大小8求余余数表示已经用了多少字节8减掉这个数就是页内剩余。注意边界情况如果memAddr正好是8的倍数remainInPage等于8整页全写也是正确的。跨页处理是EEPROM驱动里最实用的一个函数建议直接复制进你的项目里以后不管写几字节只要调这个函数就行不用每次自己操心分页的事情。3.3 写周期等待轮询ACK方法写完数据之后不能立刻发下一条I2C命令因为AT24C02内部正在把缓存区数据写入非易失存储这个时间大约5ms。在这段时间内芯片就像“死”了一样对任何命令都不应答。如果强行读写HAL_I2C_Mem_Write可能会一直等到超时返回HAL_TIMEOUT非常浪费时间。最可靠的做法是轮询设备是否就绪不断发送设备地址直到芯片返回ACK。HAL库可以用HAL_I2C_IsDeviceReady来实现void AT24C02_WaitForWriteComplete(void) { HAL_I2C_IsDeviceReady(hi2c1, AT24C02_DEV_ADDR, 1000, 10); }HAL_I2C_IsDeviceReady的第三个参数是重试次数第四个是每次重试的间隔时间。它会发送一个起始信号和地址字节如果EEPROM忙不会ACKHAL库会一直重试直到成功为止。这里的1000次重试对EEPROM的5ms写周期来说完全足够实测不会卡很久。如果不想轮询也可以简单粗暴地HAL_Delay(10)但轮询ACK的好处是一旦芯片提前就绪能立刻继续执行效率更高。批量写入大量数据时能明显感觉到速度快一些。3.4 多字节连续读取读取不像写入有页限制AT24C02支持从任意地址连续读任意长度因为读操作不会触发内部编程也就不存在覆盖风险。所以多字节读取可以一次性完成void AT24C02_ReadBytes(uint8_t memAddr, uint8_t *data, uint16_t len) { HAL_I2C_Mem_Read(hi2c1, AT24C02_DEV_ADDR, memAddr, I2C_MEMADD_SIZE_8BIT, data, len, 100); }注意HAL_I2C_Mem_Read的时序起始信号 → 设备地址写方向→ 内存地址 → 重启信号 → 设备地址读方向→ 连续读数据 → 停止信号。HAL库会帮你处理好这些不用手动操作。唯一要注意的是缓冲区长度要够别把栈上一个小数组的地址传进去然后读一大堆数据那就越界了。4. 调试实测与常见问题排查4.1 实测读写的逻辑验证驱动写完以后先在main函数里做一个简单的回环测试。我把一个8字节的数组写进EEPROM再从另一个地址读回来用串口打印到调试助手确认。第一轮实测就很顺利数据全部读回一致。但有一个现象值得说一说复位MCU之后我直接调AT24C02_ReadByte去读上次写入的数据发现前两次读回来的是0xFF后面才正常。原因不在于EEPROM本身而是板子刚上电时电源还在爬升EEPROM内部电压不稳定还没来得及进入正常状态。解决办法很简单在读取前加一个至少10ms的延时等电源稳定后再操作。如果你也遇到类似“上电立刻读EEPROM读不到”的问题优先怀疑这个。再一个是确认地址边界。我把写入起始地址设为0x05写入长度10字节通过逻辑分析仪观察I2C总线波形可以看到总共发起了两次写操作第一次3字节第二次7字节刚好符合跨页逻辑。如果你没有逻辑分析仪也可以在page写函数里加一个计数变量把写入分段情况通过串口打印出来效果一样。4.2 I2C BUSY锁定问题F407硬件I2C最出名的问题就是总线忙锁死。表现是程序跑到某个I2C操作后一直卡住用调试器暂停查看HAL状态会看到hi2c1-State不是READY或者说总线上检测到BUSY但实际没有设备在通信。我在调试过程中也遇到过这个情况原因是EEPROM写周期还没结束的时候我手动用调试器暂停过程序然后又强行单步导致I2C状态机和实际总线状态错位。修复方法分两种软件复位I2C外设调用HAL_I2C_DeInit再重新HAL_I2C_Init相当于把外设恢复到初始状态。硬件复位总线把SCL引脚配置成普通GPIO输出手动翻转9个时钟周期再恢复正常复用模式把挂在总线上的从机状态机复位到空闲态。考虑到实际项目中I2C总线上可能挂着多颗设备一次异常就软复位整个外设可能影响其他设备。所以我更常用第二种方法把下面的函数封装起来遇到BUSY卡住直接调用void I2C_Bus_Reset(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; HAL_I2C_DeInit(hi2c1); GPIO_InitStruct.Pin GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_OD; GPIO_InitStruct.Pull GPIO_PULLUP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6 | GPIO_PIN_7, GPIO_PIN_SET); 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); } MX_I2C1_Init(); }这里SCL翻转9次是为了让总线上可能处于半传输状态的从机完整接收一个错误帧并恢复空闲。翻转节奏不要太快我加了1ms延时确保从机时钟极值都能识别到。4.3 常见问题速查表我在调试过程中把所有可能遇到的问题整理成了这张表供你对照排查现象可能原因解决办法读回数据全是0xFF上次没写进去写周期没等够WP引脚被拉高写保护检查写等待函数是否调用确认WP引脚接GND写操作返回HAL_TIMEOUT设备地址错误总线上拉电阻缺失I2C被锁死确认地址是0xA0检查上拉电阻执行总线复位函数写入数据乱序/交叉跨页未处理越过8字节回卷覆盖用跨页拆分函数替代直接Mem_Write读地址错误使用的是Master_Transmit而非Mem_Transmit改成HAL_I2C_Mem_Read上电后前几次读是0xFFEEPROM上电未稳定初始化后加10~50ms延时SDA/SCL波形高电平很低上拉电阻过大或总线电容太大检查上拉电阻常用2.2kΩ~4.7kΩ这些坑我一个一个踩过来其中最隐蔽的就是跨页回卷它不会报任何错误就是悄悄把你之前写的字节覆盖掉如果有日志记录类的需求排查起来会有点绕。4.4 扩展从EEPROM到其他I2C设备把AT24C02调通了之后你会发现HAL库I2C操作的套路基本一样换其他I2C设备都能很快上手。比如我后来在同一个工程里挂了OLEDSSD1306和温湿度传感器SHT30核心区别就是设备地址不同、寄存器映射不同但读写的骨架还是那套HAL_I2C_Mem_Write、HAL_I2C_Mem_Read或者有些传感器需要先写寄存器地址再读数据用HAL_I2C_Master_Transmit加HAL_I2C_Master_Receive组合也能完成。有一个小技巧想分享给刚开始用HAL库的朋友遇到I2C设备通信不正常先写一个I2C地址扫描函数遍历0x01~0x7F所有地址看哪些地址有ACK响应能很快确认设备和MCU之间有没有建立起基本的物理层通信。下面这段代码我用过很多次直接放在工程里备用void I2C_Scan(void) { for (uint8_t addr 1; addr 128; addr) { uint8_t devAddr addr 1; if (HAL_I2C_IsDeviceReady(hi2c1, devAddr, 1, 5) HAL_OK) { printf(Found I2C device at 0x%02X\r\n, devAddr); } } }这套调试思路对所有I2C从机都适用先扫地址确认物理层通了再用逻辑分析仪抓时序确认数据帧正确最后才深入分析寄存器层面的问题。按照这个顺序绝大部分I2C问题都能在半小时内定位到原因。最后再分享一个这次调试中的体会HAL库的确把I2C时序封装得很简洁但正因为封装得好反而容易让人忽略时序细节。我在调试跨页写入时一度以为HAL_I2C_Mem_Write自己会处理分页结果仔细看芯片手册才发现页边界是硬件定的软件必须主动规避。所以不管用标准外设库还是HAL库I2C时序和EEPROM页结构这两个基础概念始终是绕不开的把它们吃透了用任何库都不会被“黑盒”坑到。本文还有配套的精品资源点击获取
返回列表