ARTICLE DETAIL

资讯详情

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

STM32硬件IIC驱动AT24CXX:实现任意地址读写任意字节

STM32硬件IIC驱动AT24CXX:实现任意地址读写任意字节 直接说结论这标题里最有含金量的词不是“IIC”也不是“AT24CXX”而是“任意地址读写任意字节”。很多人在STM32上挂EEPROMCubeMX点一点生成代码然后调HAL_I2C_Mem_Write、HAL_I2C_Mem_Read读写固定地址、固定长度没问题但项目一改需求要把一串不定长的数据写到任意偏移地址再按任意长度读回来问题就全冒出来了——跨页写失败、地址自增越界、读到一半数据错位、总线忙死锁。这篇文章就把这套东西彻底讲透为什么AT24CXX需要跨页处理HAL库硬件IIC的API到底该怎么用以及一个能真正“任意地址读写任意字节”的通用驱动怎么封装。这篇内容的定位很明确给已经会用CubeMX配好I2C、但被EEPROM读写折腾过的开发者看。零基础也能跟下来但至少要知道寄存器、地址、字节这些基本概念否则跳到代码段你会蒙。我会把原理、代码、坑一次性讲完代码基于STM32F103 HAL库但思路适用于F0/F4/G0全系列。1. 先搞明白AT24CXX的“页”为什么是绕不过去的坎1.1 页写机制EEPROM不是你想怎么写就怎么写AT24CXX系列是Atmel现在是Microchip的I2C接口EEPROM常见型号从AT24C01到AT24C256容量从1Kbit到256Kbit。这系列芯片有个共同设计内部存储按“页”划分写操作必须遵守页边界规则。具体来说AT24C01/02每页8字节AT24C04/08/16每页16字节AT24C32及以上每页32字节AT24C512每页128字节页写规则是一次写操作最多写入“当前页剩余空间”那么多字节。如果你从地址0x00开始写10字节而页大小是8字节那么前8字节写入第0页地址指针自增到0x07后溢出回卷到0x00第9、第10字节会被写到页首——把刚才写进去的前两个字节覆盖掉。这就是“任意地址任意字节”最大的坑。我的数据是0x00~0x09实际EEPROM里存的却是0x02~0x09前两字节被覆盖再加0x00、0x01回卷写到了页头。数据错得乱七八糟还不好排查。1.2 为什么要特别强调“任意地址”固定地址固定长度的读写你可以在写之前人为保证“不从页中间开始写”“长度不超过页大小”一切相安无事。但这个前提在实际项目中根本不成立系统需要分段存储日志每条日志长度不固定从上次结束位置接着写校准参数结构体变更版本号、CRC、数据分布位置调整掉电保存键值对键的偏移地址动态计算这些场景下调用方只关心“我要把n字节写到addr”驱动层必须自己处理跨页拆分。把跨页逻辑封装在驱动内部上层业务就不用天天惦记“我这页还剩几个字节”。1.3 读操作其实没有页限制但地址自增同样有边界读操作不受页写限制可以从任意地址连续读取任意字节。因为EEPROM内部读操作地址指针在芯片内是自动递增的只要不越过整个存储空间的末尾就可以一直读下去部分型号超过末尾后回卷行为取决于具体芯片。所以读这边搞“任意地址任意字节”其实天然支持真正的门槛在写。但读也不是完全没有坑HAL库的I2C_Mem_Read在连续读的时候如果中间发生NACK或总线错误地址指针停在什么位置是不可预知的恢复策略要考虑到。2. HAL库I2C的API选型Mem系列和Master系列到底差在哪2.1 CubeMX/HAL库的I2C外设配置先用CubeMX把I2C外设配好。我以I2C1为例PB6-SCL、PB7-SDA配置如下I2C Speed ModeStandard Mode100KHz起步不要一上来就Fast ModeI2C Clock Speed100000内部上拉CubeMX里I2C没有内部上拉配置选项上拉在GPIO配置里STM32的I2C引脚是开漏模式外部必须接上拉电阻这个后面第三节细讲生成代码后你会看到hi2c1这个句柄所有I2C操作都通过它展开。2.2 HAL_I2C_Mem_Write/Mem_Read的适用边界先看HAL库提供的两组关键API// 写单个字节/多字节不含内存地址参数依赖从机内部地址指针 HAL_StatusTypeDef HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout); // 读单个字节/多字节同样不含内存地址参数 HAL_StatusTypeDef HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout); // 指定内存地址写EEPROM这类器件专用 HAL_StatusTypeDef HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout); // 指定内存地址读 HAL_StatusTypeDef HAL_I2C_Mem_Read(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout);HAL_I2C_Master_Transmit/Receive只发设备地址DevAddress后跟纯数据。适用于寄存器地址由上次操作决定的器件比如某些传感器内部自己维护寄存器指针或者你在应用层手动拼装完整帧的情况。HAL_I2C_Mem_Write/Mem_Read则不同HAL库会自动帮你构造“Start → 设备地址写 → 内存地址1字节或2字节 → 数据/重复Start读”的完整时序。AT24CXX这种需要通过I2C帧内携带内存地址访问的器件天然适合Mem系列API。选择逻辑一句话只要从机内部有“地址寄存器”这个概念就得用Mem系列。AT24CXX就是典型代表。2.3 为什么我不推荐直接用Master_Transmit写EEPROM地址有人会问“我可以先发设备地址再发EEPROM内存地址最后发数据这不就是Master_Transmit的活吗为什么非要Mem系列”能这么做的前提是你能精确控制I2C帧的每一个字节顺序。HAL_I2C_Master_Transmit确实可以你把整个帧设备地址、内存地址高字节、内存地址低字节、数据……拼在一个数组里发出去就行。但这样有几个副作用设备地址的读写控制位bit0需要自己拼写操作发0xA0读操作要先发0xA0再重复Start发0xA1这套逻辑HAL库已经在底层帮你处理好了数组缓冲区的构造、字节序转换都是额外代码代码可读性差别人看你代码不知道你在操作EEPROM还是在操作传感器用Mem系列语义清晰还省掉手动拼帧的麻烦。你只要告诉HAL库“设备地址是什么、内存地址是什么、数据在哪、多长”HAL库自己处理剩下的。2.4 MemAddSize参数1字节还是2字节的坑MemAddSize是内存地址宽度I2C_MEMADD_SIZE_8BIT对应1字节地址适合AT24C01/02/04I2C_MEMADD_SIZE_16BIT对应2字节地址适合AT24C08及以上。为什么AT24C04是8BITAT24C08反而要16BIT因为AT24C04只用1个字节的低4位做页选择PAGE内偏移剩下3位是块选择地址空间只有512字节1字节够了。AT24C08有1024字节需要11位地址1字节放不下得2字节地址帧。这个参数传错数据读写位置完全错乱。我见过有人把AT24C16配成8BIT地址读出来的数据全是乱的排查了半天发现是MemAddSize的问题。CubeMX不会帮你判断这个得自己根据型号选。3. 硬件IIC和模拟IIC的选型F1的硬件I2C到底能不能用3.1 网上对硬件IIC的骂声从哪来玩STM32的人应该都听过“STM32F1的硬件I2C有bug”这种说法于是大量教程默认用GPIO模拟I2C时序甚至国内有些开发板厂商直接放弃硬件I2C。这个说法的源头有几个F1的老批次芯片I2C在总线错误恢复上确实有缺陷BUSY位可能在异常总线状态后无法自动清除早期标准外设库StdPeriph的I2C驱动写得不够顺手事件处理逻辑复杂很多人调不通就归咎于“硬件有bug”部分开发板PCB上I2C上拉电阻没焊或焊错导致时序工作不稳定最后也甩锅给硬件I2C但从F0、F3、F4、L0、L4这些后来的系列起I2C外设已经非常成熟。即便在F1上大部分“卡死”问题也是因为总线忙没有恢复逻辑、中断优先级配置错误、在中断里做阻塞式读写。这些是使用方式问题不是硬件问题。3.2 HAL库下硬件IIC的优势用HAL库的硬件I2C配合Mem系列API一个HAL_I2C_Mem_Write调用就完成了整个帧的收发底层由外设自动管理时钟和数据的位时序。相比模拟IIC的“拉高拉低延时翻转”循环优势很明显CPU占用率低阻塞模式下也只是一次函数调用模拟IIC每个bit都要CPU参与速率越高越吃力时序稳定硬件生成精确的SCL频率、建立保持时间不依赖延时函数精度支持中断/DMA模拟IIC很难做DMA硬件I2C可以直接把内存数据搬走代码简洁不需要自己实现Start/Stop/ACK/NACK时序函数我的结论是新项目直接上硬件IIC遇到问题解决问题。模拟IIC作为兜底方案保留但不再默认使用。3.3 什么时候依然建议用模拟也不是说硬件IIC永远最优。下面这些情况你可以继续用模拟IIC引脚不够需要用任意GPIO复用出I2C功能硬件I2C引脚是固定的芯片组太老确实存在已确认的硬件I2C锁死问题且没有规避手段需要同时挂很多I2C器件到不同引脚外设数量不够模拟IIC还有一个隐形优势它天然带时序韧性严格的主从时序要求可以被软件fudge掉。但这属于用软件bug对抗硬件特性的做法工程上不推荐在正式产品里依赖这种“韧性”。4. 通用驱动的完整实现支持任意地址、任意长度、自动跨页4.1 驱动文件骨架和数据结构实际做项目不要每次用到EEPROM就现写一遍读写逻辑封装一个通用驱动文件一劳永逸。我的驱动一般分两个文件at24cxx.h和at24cxx.c。#ifndef AT24CXX_H #define AT24CXX_H #include main.h /* 根据实际型号定义容量和页大小 */ #define AT24CXX_PAGE_SIZE 8 // AT24C01/02为8字节型号更大改16或32 #define AT24CXX_MAX_ADDR 0xFF // AT24C02256字节最大地址0xFF /* 操作结果枚举 */ typedef enum { AT24CXX_OK 0, AT24CXX_ERR_NACK, AT24CXX_ERR_TIMEOUT, AT24CXX_ERR_INVALID_PARAM, AT24CXX_ERR_OTHER } AT24CXX_Status; AT24CXX_Status AT24CXX_WriteBytes(I2C_HandleTypeDef *hi2c, uint16_t devAddr, uint16_t memAddr, uint8_t *pData, uint16_t len); AT24CXX_Status AT24CXX_ReadBytes(I2C_HandleTypeDef *hi2c, uint16_t devAddr, uint16_t memAddr, uint8_t *pData, uint16_t len); #endifdevAddr这里传7位设备地址AT24C02默认为0x50即A0A1A2全接地时的地址。注意HAL库内部会自动左移1位加上读写位传0x50就对了千万别自作主张传0xA0。4.2 任意字节写的核心跨页切分算法跨页写的核心思路把一次“任意长度”的写请求按页边界切分成多次“当前页剩余空间内连续写”的子操作。AT24CXX_Status AT24CXX_WriteBytes(I2C_HandleTypeDef *hi2c, uint16_t devAddr, uint16_t memAddr, uint8_t *pData, uint16_t len) { if (hi2c NULL || pData NULL || len 0) { return AT24CXX_ERR_INVALID_PARAM; } if (memAddr len - 1 AT24CXX_MAX_ADDR) { return AT24CXX_ERR_INVALID_PARAM; } while (len 0) { /* 计算当前页还剩多少字节可以写 */ uint16_t pageRemain AT24CXX_PAGE_SIZE - (memAddr % AT24CXX_PAGE_SIZE); uint16_t chunk (len pageRemain) ? len : pageRemain; if (HAL_I2C_Mem_Write(hi2c, (uint16_t)(devAddr 1), memAddr, I2C_MEMADD_SIZE_8BIT, pData, chunk, 100) ! HAL_OK) { return AT24CXX_ERR_NACK; } /* 等待EEPROM内部写周期结束 */ HAL_Delay(5); memAddr chunk; pData chunk; len - chunk; } return AT24CXX_OK; }关键地方就一个pageRemain AT24CXX_PAGE_SIZE - (memAddr % AT24CXX_PAGE_SIZE)。memAddr是页内偏移页大小减去当前偏移得到的就是本页还能连续写的字节数。比如页大小8memAddr5pageRemain3说明从地址5开始最多连续写3字节地址5、6、7再往后就到下一页了。HAL_Delay(5)是AT24CXX内部写周期的等待时间。芯片规格书里写周期一般最大5ms写完一页后必须等它把数据真正烧录进非易失存储才能发起下一次写操作。这个等待是必要的。后面第5节我会讲怎么用轮询替代固定延时进一步提速。4.3 读函数为什么不需要跨页切分读操作直接调HAL_I2C_Mem_Read即可一次读多长都行不能超过存储空间尾部AT24CXX_Status AT24CXX_ReadBytes(I2C_HandleTypeDef *hi2c, uint16_t devAddr, uint16_t memAddr, uint8_t *pData, uint16_t len) { if (hi2c NULL || pData NULL || len 0) { return AT24CXX_ERR_INVALID_PARAM; } if (memAddr len - 1 AT24CXX_MAX_ADDR) { return AT24CXX_ERR_INVALID_PARAM; } if (HAL_I2C_Mem_Read(hi2c, (uint16_t)(devAddr 1), memAddr, I2C_MEMADD_SIZE_8BIT, pData, len, 100) ! HAL_OK) { return AT24CXX_ERR_NACK; } return AT24CXX_OK; }EEPROM的读时序是先发设备地址写位、内存地址然后重复Start再发设备地址读位从机转为发送模式主机连续接收。HAL_I2C_Mem_Read在内部完整处理了这套流程。需要注意的是如果len很大比如超过128字节还是建议分批次读避免阻塞时间太长影响其他任务。对大多数应用读几十字节一次搞定没问题。4.4 测试用例验证跨页写入正确性驱动写完上板验证。我习惯写一个跨页测试函数故意让起始地址落在页内部写长度超过页边界然后回读比对void AT24CXX_Test(I2C_HandleTypeDef *hi2c) { uint8_t writeBuf[20]; uint8_t readBuf[20]; uint16_t startAddr 0x05; // 故意从页中间开始 uint16_t i; for (i 0; i 20; i) { writeBuf[i] (uint8_t)(i 1); } if (AT24CXX_WriteBytes(hi2c, 0x50, startAddr, writeBuf, 20) AT24CXX_OK) { HAL_Delay(10); if (AT24CXX_ReadBytes(hi2c, 0x50, startAddr, readBuf, 20) AT24CXX_OK) { if (memcmp(writeBuf, readBuf, 20) 0) { // 数据一致跨页写正确 } else { // 数据不一致排查时序或地址计算 } } } }另一个快速验证先把整片EEPROM擦成0xFF全部写0xFF再在目标地址写已知数据读回看是否只有目标区域变化。这样能排除“覆盖了别的数据”这种干扰。5. HAL库硬件IIC踩坑实录从BUSY锁死到无应答5.1 总线BUSY锁死最经典最致命的坑现象程序运行一段时间后调用HAL_I2C_Mem_Write一直返回HAL_BUSY后续所有I2C操作全部卡死。SCL和SDA线上看波形有异常或者SCL正常但SDA一直低。根因I2C总线上出现异常状态比如主从不同步、中途断线、从机复位导致时钟 stretch 异常外设内部状态机的BUSY位被置1如果HAL库没有复位外设的操作BUSY永远不会自清除。解决方案在每次操作前加一个“总线恢复”函数检测BUSY标志如果是置位状态就把I2C外设复位重新初始化同时把SCL线拉几个时钟脉冲让总线上的从机恢复空闲状态。void I2C_Bus_Reset(I2C_HandleTypeDef *hi2c) { GPIO_InitTypeDef GPIO_InitStruct {0}; if (__HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_BUSY) SET) { __HAL_I2C_DISABLE(hi2c); // 将SCL和SDA配置为推挽输出手动拉出9个时钟脉冲 HAL_GPIO_DeInit(hi2c-SCL_GPIO_Port, hi2c-SCL_Pin); GPIO_InitStruct.Pin hi2c-SCL_Pin | hi2c-SDA_Pin; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_OD; GPIO_InitStruct.Pull GPIO_PULLUP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(hi2c-SCL_GPIO_Port, GPIO_InitStruct); for (int i 0; i 9; i) { HAL_GPIO_WritePin(hi2c-SCL_GPIO_Port, hi2c-SCL_Pin, GPIO_PIN_SET); HAL_Delay(1); HAL_GPIO_WritePin(hi2c-SCL_GPIO_Port, hi2c-SCL_Pin, GPIO_PIN_RESET); HAL_Delay(1); } // 重新初始化I2C外设 HAL_I2C_Init(hi2c); } }这个方法能恢复绝大多数总线死锁情况。注意别在I2C中断回调里调用这个函数会递归或造成不可预料的行为。5.2 无应答NACK的两种常见场景场景一写地址后从机不应答。排查思路设备地址是否正确7位地址左移一位变成8位地址后是否匹配A0/A1/A2硬件地址引脚是否和代码里一致EEPROM是否处于内部写周期中上一次写操作还没完成就不应答场景二写完数据后没有等内部写周期完成紧接着去读大概率NACK。解决办法就是上文的延时5ms或者轮询写周期状态。AT24CXX有个“Acknowledge Polling”机制在内部写周期里从机不应答任何地址但当写周期完成后第一次地址查询会正常应答。你可以利用这个特性把固定5ms延时换成更快更可靠的“等待应答”AT24CXX_Status AT24CXX_WaitReady(I2C_HandleTypeDef *hi2c, uint16_t devAddr) { uint32_t tick HAL_GetTick(); while (HAL_I2C_Master_Transmit(hi2c, (uint16_t)(devAddr 1), NULL, 0, 10) ! HAL_OK) { if (HAL_GetTick() - tick 100) { return AT24CXX_ERR_TIMEOUT; // 超过100ms认为出错 } } return AT24CXX_OK; }用这个函数替代HAL_Delay(5)写多页时每页节省几毫秒等待时间批量写大块数据的效率会有明显提升。5.3 I2C中断/DMA方式下容易忽略的“重入问题”阻塞方式下HAL_I2C_Mem_Write是不允许被中断嵌套调用的。比如主循环在主从通信过程中定时器中断里又来了一次I2C读写HAL库内部的I2C_Lock机制会直接返回HAL_BUSY数据根本没发出去代码却不知道。解决方案用一个互斥信号量/标志位保护I2C总线谁拿到锁谁用或者所有I2C操作都放在一个地方其他任务通过队列请求I2C操作或者在中断里只置标志主循环里统一处理第二种方案在裸机工程里最推荐。简单可靠不用引入RTOS的信号量机制还能统一管理I2C操作的优先级。5.4 上拉电阻硬件IIC的“隐形参数”很多I2C问题根本不是代码问题是上拉电阻没接对。I2C是开漏总线SCL/SDA必须通过外部上拉电阻接VCC。电阻大小选择原则100KHz标准模式4.7KΩ~10KΩ400KHz快速模式2.2KΩ~4.7KΩ线缆较长或从机数量多适当减小电阻但别小于1KΩ否则驱动能力不够在开发板上很多板子已经焊了上拉电阻。自己画板子时这4.7K×2的电阻千万别省。我踩过的坑就是自己画的板子忘加上拉I2C波形上升沿缓慢偶尔通信正常偶尔超时查了两天才发现是硬件问题。6. 从“能用”到“好用”驱动进一步优化与功能扩展6.1 写保护引脚的处理AT24CXX系列大多有WPWrite Protect引脚高电平时整个芯片只读。量产产品里经常用MCU的GPIO控制WP引脚写数据前拉低写完后拉高防止误写。#define WP_ENABLE() HAL_GPIO_WritePin(WP_GPIO_Port, WP_Pin, GPIO_PIN_SET) #define WP_DISABLE() HAL_GPIO_WritePin(WP_GPIO_Port, WP_Pin, GPIO_PIN_RESET)为什么值得这么做因为掉电瞬间MCU的IO口电平不确定如果WP引脚悬空或上拉为高正好在掉电时序里有一次未经授权的写操作就会破坏EEPROM里的关键参数。用MCU引脚主动控制WP是产品级设计该有的基本功。批量写时我在AT24CXX_WriteBytes函数开头WP_DISABLE()写完全部数据后WP_ENABLE()。这样既保证写过程顺畅又让非写时间段EEPROM处于硬件保护状态。6.2 跨芯片型号兼容用一个宏搞定项目升级换更大的EEPROM比如从AT24C02换到AT24C256驱动的修改只涉及两个宏#define AT24CXX_PAGE_SIZE 32 // 换成AT24C256时改32 #define AT24CXX_MAX_ADDR 0x7FFF // 32K字节空间 #define AT24CXX_MEMADDR_SIZE I2C_MEMADD_SIZE_16BIT // 地址宽度也要改改完重新编译驱动内部逻辑不用动。如果追求极致通用性可以把页大小、最大地址、地址位宽做成结构体成员通过一个初始化函数传参typedef struct { uint16_t pageSize; uint16_t maxAddr; uint16_t memAddrSize; } AT24CXX_Config; AT24CXX_Status AT24CXX_Init(AT24CXX_Config *cfg);这样同一个驱动文件可以同时服务多个不同型号的EEPROM在多实例场景比如一块板子上挂了AT24C02和AT24C256下非常舒服。6.3 大数据块写入的缓冲区管理如果上层业务要一次写几KB数据比如存储一段日志应用层调AT24CXX_WriteBytes时传一个大数组驱动内部会自动跨页切分。但要注意MCU RAM不够时不要一次性把几KB数据都读到RAM里再写考虑边读边写、分帧处理每页写完的5ms等待时间对实时性要求高的系统来说是不能接受的。解决方案是用DMAI2C中断的方式配合“写完成中断再把下一页数据灌进去”具体实现后面有机会单独写一篇6.4 校验与冗余掉电保存可靠性的最后防线EEPROM用来保存关键参数一定要加校验。我常用的方案结构体尾部加CRC16或简单的和校验记录双份备份一份损坏时自动用另一份恢复写入前先读回验证写后回读typedef struct { uint16_t magic; // 固定魔数判断是否被初始化过 uint16_t calValue; uint16_t crc; // 数据区CRC } SysParam_t;读数据时检查magic和crc任一不匹配就回滚到默认参数。这套机制用不了多少代码量但在掉电保存场景里能避免最尴尬的“设备重启后参数全丢/全乱”问题。7. 贴两个实际调试方法帮你快速定位I2C问题7.1 用逻辑分析仪看I2C时序排查I2C问题最有效的手段是逻辑分析仪。淘宝几十块钱的8通道逻辑分析仪配上软件就能解码I2C协议。抓一次读操作你能清楚地看到Start条件设备地址读写位ACK/NACK数据字节Stop条件实际排查时我最常用的步骤抓正常读操作波形确认设备和地址正确抓跨页写操作看每次子写之间的地址和数据是否按预期递增如果出现丢失ACK把示波器探头或逻辑分析仪通道挂在SDA上看ACK位时SDA是高是低判断从机为什么不应答有了波形图很多“玄学”问题直接变成“看得见摸得着”的问题。7.2 用SCL时钟脉冲练手手动扫描I2C设备如果总线上挂的从机地址不确定比如同一个项目里既有EEPROM又有传感器可以写一个I2C地址扫描函数void I2C_Scan(I2C_HandleTypeDef *hi2c) { for (uint8_t addr 1; addr 127; addr) { if (HAL_I2C_IsDeviceReady(hi2c, (uint16_t)(addr 1), 3, 50) HAL_OK) { printf(Found device at 0x%02X\r\n, addr); } } }HAL_I2C_IsDeviceReady会通过发地址读位探测从机是否应答不影响总线数据。项目初期用它确认硬件连接和地址是否正确比对着原理图猜快多了。AT24C02接地时地址是0x50扫描输出会看到这一条。如果输出的地址和期望不一致优先检查A0/A1/A2引脚的接法以及I2C总线有没有接错线。结尾就不做总结了继续分享最后一个坑。我早期用HAL库的I2C做EEPROM时特别喜欢在初始化后立刻做第一次读写结果总是偶发失败。后来才意识到问题CubeMX生成的I2C初始化代码里外设使能之后需要一小段时间让总线稳定尤其在上电瞬间电源还没升稳的时候。实际工程里我在main函数里加了一个100ms的上电延时或者用状态机等电源稳定标志后再访问EEPROM这个问题就再没出现过。你可以试试。
返回列表