ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32F0驱动FM24C64铁电存储器:I2C通讯与EEPROM替代实战

STM32F0驱动FM24C64铁电存储器:I2C通讯与EEPROM替代实战 简介面向STM32F0系列单片机开发者这份资源提供了驱动24C64/FM24C64这类64K位I²C接口EEPROM的完整C语言源码解决在STM32F0上配置I²C外设、收发数据与移植适配的核心问题。包内含24Cxx.c与24Cxx.h两个源文件涵盖I²C总线初始化、GPIO引脚映射、读写命令封装以及中断处理等关键环节源码体量精简压缩包仅3KB适合快速集成到现有工程或嵌入式入门练习。目前已有1566人学习下载具备一定参考价值。通过研读这两个文件可以理解EEPROM的页写、随机读等操作时序并掌握在STM32F0平台上从外设时钟配置到中断服务例程的完整驱动实现思路进而针对不同硬件布局灵活调整引脚与传输速率顺利完成移植。1. 项目概述1.1 为什么用 FM24C64 而不是普通 EEPROM接到这个需求的时候第一反应是确认一件事用户说的是 24C64但标题里特意写了 FM24C64。这不是手误而是特意区分。FM24C64 是 Cypress现在归 Infineon出品的铁电存储器容量同样 64KbitI2C 接口引脚和时序兼容普通 24C64但内部原理完全不同。普通 24C64 是 EEPROM 浮栅晶体管存储写入需要电荷泵升压、擦写时间通常要等 5ms 甚至更久而且擦写寿命标称 100 万次实际用下来总让人不踏实。FM24C64 是铁电晶体存储写入不需要擦除步骤也不存在“写一个字节要等 5ms”的说法速度能和总线时序同步寿命标称 1000 亿次10 的 12 次方直接拉开好几个数量级。对于 stm32f0 这种入门级 MCU 来说搭配 FM24C64 的场景非常明确设备参数频繁保存、运行日志滚动记录、校准数据每次上电更新。普通 EEPROM 在这类场景下有两个痛点一个是写一次等 5ms批量写参数时明显卡顿另一个是频繁写入很快逼近寿命上限。FM24C64 在这两点上都是碾压级的优势代价仅仅是单颗价格贵几毛钱到一块多钱很多工程师在方案选型时会忽略这个差异等项目量产之后才发现普通 EEPROM 写入寿命不够再换片子改驱动底板和软件都要动成本反而更高。1.2 这套程序解决的核心问题我把这套驱动拆成三个层面来讲第一层是 stm32f0 的 I2C 外设初始化这个不多说F0 的 I2C 和 F1 的寄存器设计完全不同F1 老代码不能直接搬第二层是 FM24C64 的协议时序实现包括单字节读写、页写、连续读第三层是工程实用细节比如设备检测、写保护处理、异常恢复。读完这篇文章你可以直接在 stm32f0 平台上把 FM24C64 跑通不用再翻数据手册一点点抠时序参数。2. 硬件连接与基础配置2.1 引脚分配和上拉电阻FM24C64 的封装是标准的 8 脚 SOIC 或 DIP引脚定义和 24C64 一致A0、A1、A2 是地址选择脚SDA、SCL 是 I2C 数据时钟WP 是写保护VCC 和 GND 供电。这里第一个坑就是 A0、A1、A2 不能悬空必须接确定的电平。FM24C64 的 7 位 I2C 地址高 4 位固定为 1010低 3 位就是 A2、A1、A0 的输入电平比如三个脚全部接地设备地址就是 0x50写地址 0xA0读地址 0xA1。如果引脚悬空芯片内部读取到的电平不确定地址漂移会造成通信时好时坏排查起来非常折磨人。上拉电阻的选择也值得说一下。I2C 总线是开漏结构SCL 和 SDA 必须有上拉电阻才能输出高电平。常见的选法是 4.7kΩ但如果总线速率跑 400kHz而且线上挂的设备多、走线长4.7kΩ 可能导致上升沿太缓建议换 2.2kΩ。我通常先在原理图上预留 2.2kΩ 到 4.7kΩ 的封装实测波形之后再做调整。stm32f0 的内部上拉不要太指望I2C 应用下内部上拉阻值太大约 30kΩ 到 50kΩ只能勉强维持低速率400kHz 下不建议依赖。2.2 stm32f0 的 I2C 外设与时钟配置stm32f0 的 I2C 外设和 stm32f1 差别很大这点很多人栽过跟头。F1 的 I2C 是传统模块状态标志散落在多个寄存器里用库函数操作相对顺手F0 的 I2C 是新设计的内核兼容 SMBus寄存器集中到 CR1、CR2、ISR、ICR 这些寄存器里状态标志也统一在 ISR 中。所以网上搜到的大部分 F1 I2C 驱动代码不能直接往 F0 上套。时钟配置是 F0 I2C 最容易被忽略的部分。I2C 外设的时钟源来自 APB1而 APB1 的频率又取决于系统时钟和分频配置。F0 的 I2C 时序参数不是简单的分频器设置而是通过 TIMINGR 寄存器里的 SCLL、SCLH、SDADEL、SCLDEL 四个字段精确控制。比如系统时钟 48MHz想跑 400kHz 的 I2CSCLL 和 SCLH 的值需要按公式计算还要考虑上升沿时间。手动算容易出错我的建议是先用 CubeMX 生成一份 I2C 初始化代码把 TIMINGR 的值记下来之后无论用寄存器还是 HAL 库都用这套参数这样可以少踩很多坑。在 CubeMX 中配置 I2C1引脚默认是 PB6(SCL) 和 PB7(SDA)复用功能 AF1。时钟设置里 I2C1 时钟选择 48MHz速度模式选 Fast Mode目标频率填 400000。生成代码之后打开 i2c.c能看到类似这样的时序计算值hi2c1.Instance I2C1; hi2c1.Init.Timing 0x00702991; hi2c1.Init.OwnAddress1 0; hi2c1.Init.AddressingMode I2C_ADDRESSINGMODE_7BIT;0x00702991 这个值不要随便动它是按照 48MHz 输入时钟、400kHz 目标频率、I2C 规范上升沿时间反推出来的。如果后续你把系统主频改了这个值必须重新计算否则通信时序直接异常。3. 软件实现与代码解析3.1 初始化与基础宏定义无论用寄存器还是 HAL 库都要先明确 FM24C64 的地址分配。三个地址脚都接地的情况下项目值7 位设备地址0x50写地址8 位0xA0读地址8 位0xA1存储容量8192 × 8 bit内存地址宽度2 字节FM24C64 的容量是 64Kbit也就是 8KB内存寻址需要 2 个字节这和 24C02 之类的 1 字节寻址设备不一样。驱动里凡是涉及地址发送的地方都要分高字节和低字节两次发送这个细节如果漏掉读写地址就会错乱表现就是读到的数据位置完全不对。使用 HAL 库的话初始化代码极简前面 CubeMX 生成的基础初始化已经完成。自己写的话寄存器版本的关键代码是开时钟、配 GPIO、配置时序再加一个使能void FM24C64_Init(void) { // 1. 使能 GPIOB 和 I2C1 时钟F0 的 GPIO 时钟在 AHB 总线上 RCC-AHBENR | RCC_AHBENR_GPIOBEN; RCC-APB1ENR | RCC_APB1ENR_I2C1EN; // 2. 配置 PB6/PB7 复用为 I2C1AF 编号为 1 GPIOB-AFR[0] ~(0xFFUL (6 * 4)); GPIOB-AFR[0] | (0x01UL (6 * 4)); // PB6 - AF1, SCL GPIOB-AFR[0] ~(0xFFUL (7 * 4)); GPIOB-AFR[0] | (0x01UL (7 * 4)); // PB7 - AF1, SDA GPIOB-MODER ~(0x3UL (6 * 2)); GPIOB-MODER ~(0x3UL (7 * 2)); GPIOB-MODER | (0x2UL (6 * 2)); // PB6 复用模式 GPIOB-MODER | (0x2UL (7 * 2)); // PB7 复用模式 GPIOB-OTYPER | (0x3UL 6); // 开漏输出 GPIOB-PUPDR ~(0xFUL (6 * 2)); // 不使能内部上下拉 // 3. 配置 I2C 时序0x00702991 对应 48MHz/400kHz I2C1-TIMINGR 0x00702991; // 4. 使能 I2C 外设 I2C1-CR1 | I2C_CR1_PE; }注意 GPIO 配置为开漏输出这是 I2C 协议的要求。如果误配置为推挽输出一旦某个设备拉低总线另一个设备输出高就会形成短路电流轻则通信异常重则烧毁引脚。使用内部上拉的习惯在这里也要改掉外接上拉电阻的方案更加可靠。3.2 写入操作单字节与页写入FM24C64 的写入操作流程发送 START发送设备写地址等待 ACK发送内存地址高字节等待 ACK发送内存地址低字节等待 ACK发送数据字节等待 ACK发送 STOP。和普通 24C64 最大的不同是FM24C64 写完不需要等待内部写周期总线时序上可以连续操作但为了代码在两个芯片之间无缝切换很多人会习惯性加一个短延时这个不影响功能。用 HAL 库的话直接调现成的函数最省事uint8_t data 0x5A; uint16_t addr 0x1234; HAL_StatusTypeDef status; status HAL_I2C_Mem_Write(hi2c1, 0xA0, addr, I2C_MEMADD_SIZE_16BIT, data, 1, 100); if (status ! HAL_OK) { // 写入失败可能是设备离线、地址错误、总线异常 }这段代码里的 I2C_MEMADD_SIZE_16BIT 参数就是关键告诉 HAL 库内存地址是 2 字节宽度。如果用了 8BITHAL 库只发一个地址字节FM24C64 会认为这是高字节而低字节则完全没收到写入位置直接错掉。页写入方面普通 24C64 的页大小是 32 字节FM24C64 同样兼容这个分页规则。一次页写入发送的数据一旦超过页边界数据会回卷到页首覆盖掉页开头的数据这是必须注意的。所以实现页写入时要么保证写入长度不超过 32 字节要么手动拆分或者写一个跨页边界检测的函数uint8_t FM24C64_WritePage(uint16_t addr, uint8_t *buf, uint16_t len) { uint16_t page_remain; uint16_t offset 0; if (buf NULL || len 0) return 1; while (offset len) { // 计算当前页剩余空间 page_remain 32 - (addr % 32); if (page_remain (len - offset)) page_remain len - offset; if (HAL_I2C_Mem_Write(hi2c1, 0xA0, addr, I2C_MEMADD_SIZE_16BIT, buf[offset], page_remain, 100) ! HAL_OK) { return 1; } addr page_remain; offset page_remain; } return 0; }这里有一个优化点FM24C64 本质上不要求页写入长度必须限制在 32 字节内它没有普通 EEPROM 的页缓冲回卷问题。但保留分页逻辑是保守且兼容的做法可以防止代码将来直接换 24C64 时踩坑。3.3 读取操作随机读与连续读读取 FM24C64 比写入稍微绕一点。随机读要先发一个“伪写”序列把目标地址写进去然后重新发 START再发设备读地址之后才能从数据线上读数据。连续读则是读完成一个字节后继续接收下一个MCU 每返回一个 ACK芯片地址自动加一直到 MCU 发送 NACK 和 STOP 结束。HAL 库封装了这套流程调用非常简单uint8_t buf[32]; uint16_t addr 0x1234; HAL_StatusTypeDef status; status HAL_I2C_Mem_Read(hi2c1, 0xA1, addr, I2C_MEMADD_SIZE_16BIT, buf, 32, 100); if (status ! HAL_OK) { // 读取失败 }连续读多字节时芯片地址会从起始地址开始逐一递增跨越页边界后自动回卷到存储区的开头这一点和写入时的回卷逻辑不同。如果希望连续读不跨物理区域可以在应用层做长度限制比如一次读取不超过 32 字节或者干脆读取完整区域后再在内存中做拼接。一个实际经验FM24C64 的读操作同样没有等待时间数据手册上标明读周期不消耗写入恢复时间这比普通 EEPROM 在频繁读场景下更省心。但要注意如果总线上同时挂了其他 I2C 设备读操作期间总线的占用时间并不短连续读 1KB 数据是 8192 个时钟再加上 ACK 位 400kHz 下大约 20ms 以上这期间不能让其他设备的实时性要求受到影响。3.4 设备在线检测与写保护处理工程上写驱动一定要有一个“设备是否存在”的检测函数否则主机单方面发数据设备没应答程序就会卡死或者误报成功。判断 FM24C64 是否在线最简单方法是发出设备写地址后看是否收到 ACKuint8_t FM24C64_IsConnected(void) { HAL_StatusTypeDef status; status HAL_I2C_IsDeviceReady(hi2c1, 0xA0, 1, 100); return (status HAL_OK) ? 1 : 0; }这个函数本质是发一个 START然后发送设备写地址监听 ACK/NACK。如果芯片在线会返回 ACK如果不在线总线无响应超时后 HAL 库返回错误。WP 脚的处理在实际项目中经常被忽略。FM24C64 的 WP 接高电平时整个存储区变成只读写命令都不会生效。很多产品在量产时会把 WP 固定接 GND 保证可写但如果有防止误写的需求可以在原理图上把 WP 接到 MCU 的一个 GPIO软件需要写入时拉低平时拉高。注意写入前必须确认这个引脚电平否则程序跑了一整天发现数据一直写不进去排查半天才意识到 WP 被拉高了。4. 常见问题与排查实录4.1 通信卡死标志位未清除用寄存器方式写 F0 I2C 驱动时最常见的故障是程序卡死在等待某个标志位的循环里。F0 的 I2C 中断标志清除方式和 F1 不同很多标志位不是读一下寄存器就自动清除而是要往 ICR 寄存器写特定位才能清掉。比如 NACKF 标志一旦设备无应答NACKF 就会置位如果不清除后续所有通信都会受到影响。标准做法是在每次通信开始前把 ICR 里相关的标志位全部写 1 清除I2C1-ICR | I2C_ICR_NACKCF | I2C_ICR_STOPCF | I2C_ICR_TXISCF;另外还要注意 ARLO 标志这是仲裁丢失标志。多主机环境下两个主机同时抢总线时会出现单主机系统虽然不常见但总线干扰也可能触发。出现后同样要清标志、重新初始化总线状态否则后续通信无法继续。4.2 读回全是 0xFF 或 0x00FM24C64 上电后的默认值通常不是 0xFF普通 EEPROM 出厂默认全 0xFF但很多情况下读回 0xFF 意味着通信没建立起来。排查顺序建议这样走第一确认设备地址。三个地址脚的电平是否和代码里一致特别检查 PCB 上 A0-A2 是否有虚焊。第二用示波器或逻辑分析仪抓 SCL 和 SDA 波形看主机有没有发出 START 信号、地址字节有没有正确发出。第三确认电压。FM24C64 的 VCC 通常支持 2.7V 到 5.5V但如果用的是 FM24CL64 低压版本工作电压范围不同电压过高或过低都会造成通信异常。第四检查 SDA 在 ACK 位的时间点上有没有被拉低如果没有说明设备根本没响应。读回全 0x00 的情况相对少见通常是地址设置到了未初始化的区域或者芯片本身有问题可以用示波器看波形确认数据线上是不是真的读到了 0x00。4.3 写入成功但读出来是旧数据这类问题的根源通常是写入未生效但代码没有检测到错误。用 HAL_I2C_Mem_Write 时如果函数返回 HAL_OK只表示主机把数据发完了不表示芯片内部写入成功。普通 24C64 在页写入期间如果主机发新命令芯片不响应而 FM24C64 没有这个问题所以很多 FM24C64 的写入失败其实不是等待时间的问题而是地址越界或 WP 引脚状态不对。排查时先读一遍 WP 引脚的电压再检查写入地址是否落在 0x0000 到 0x1FFF 范围内8192 字节。这两个地方都没问题的话可以做一个回读校验写完立刻读回来比对这是最简单有效的自检手段我习惯把回读校验做成一个统一的接口调试阶段默认开启。4.4 总线干扰与重试机制I2C 总线在工业现场容易受到干扰表现为偶发的 NACK、总线卡死、数据错位。FM24C64 本身电气特性不错但板级设计和 MCU 配置如果不注意抗干扰能力会大打折扣。一个容易忽略的地方是 SDA 和 SCL 两条线不能长距离并行否则信号串扰严重走线要尽量短且不要靠得太近。软件上建立重试机制非常有必要。我常用的策略是一次读写操作如果失败先清除总线状态发一个 STOP 信号延时 1ms 后重试最多重试 3 次。连续 3 次失败才上报错误这样可以过滤掉大部分瞬时干扰。实测在电机驱动板这样的强干扰环境下重试机制能把 I2C 通信的可靠性从 95% 提升到接近 100%。4.5 代码移植到 F1 或 G0 的问题FM24C64 驱动里硬件层是跟 MCU 绑定的但协议层完全通用。如果从 F0 移植到 F1I2C 外设架构变了寄存器初始化需要重写但设备地址、页写入逻辑、回读校验这些函数直接复用。如果移植到 G0 系列I2C 外设结构和 F0 一致TIMINGR 寄存器依然是同一个套路基本可以无缝迁移只需要重新算一遍时序参数。一个实用的做法是把设备抽象成两层底层是 I2C 读写函数上层是 FM24C64 的地址和数据操作函数。上层完全不依赖具体 MCU 型号换平台时只需要替换底层的几个函数即可编译、测试成本都低很多。5. 扩展应用驱动跑通之后FM24C64 的很多特性值得进一步利用。比如它的写入寿命很长可以把设备的掉电保存参数做成“每次修改立即写入”而不是像传统 EEPROM 那样只在关机前写一次。另一个常见的玩法是日志功能系统运行状态按周期写入存储区配合读指针和写指针实现环形队列FM24C64 的高速写入可以保证日志记录不丢帧寿命也能撑住频繁擦写。我自己用 FM24C64 做过多通道传感器的校准参数存储每次校准时写几十字节一天校准十几次随便用十年都不需要考虑寿命问题。换成普通 24C64 的话按每天写 100 字节计算100 万次寿命大约 5 年就会耗尽对于产品来说这个余量明显不够。FM24C64 的成本优势在这种场景下体现得特别直接一块钱出头的差价换来几十年的寿命余量非常划算。最后分享一个调试验证的小技巧写完驱动后不要直接开始业务逻辑先写一个存储器全片测试函数向每个 32 字节页写入模式数据比如 0x00 到 0xFF 循环然后回读逐字节比对。这个测试能一次性暴露地址线、页回卷、总线稳定性等几乎所有底层问题。跑通了全片测试后面无论业务功能怎么叠加存储部分都可以高枕无忧。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进