☰
FM25CL64铁电存储器SPI驱动详解:无需擦除的掉电不丢数据方案
2026/10/7 8:46:05 网站建设 项目流程

简介:面向STM32F103开发者的铁电存储器驱动代码包,基于SPI接口实现FM25CL64的读写操作,代码精简且支持直接读写单个字节。代码中提供SPI_Flash_Init初始化函数,涵盖GPIO时钟使能、引脚复用配置以及SPI外设参数设置;读写函数则封装了片选控制、地址发送与数据接收流程,方便开发者直接调用。包内共2个文件,分别以C源文件和头文件组织,完整覆盖底层IO配置与SPI通信封装,整体仅2KB大小,适合作为轻量级参考模板,压缩包结构简单,便于快速导入工程。已有1712人学习下载。通过研读代码,可熟悉SPI外设配置、FM25CL64读写时序以及铁电存储器同其他存储芯片的差异,对理解嵌入式存储驱动编写和SPI总线应用具有实际帮助,也能为后续扩展其他FM25系列器件提供参考,同时节省查阅数据手册和调试底层通信的时间。

1. 铁电存储器 FM25CL64:一颗掉电不丢数据的 SRAM,SPI 驱动该这么写

做单片机开发的人多半被 Flash 或 E2PROM 的擦写机制坑过:写一页要等几毫秒,扇区擦除要等更久,意外断电还会丢数据。FM25CL64 属于铁电存储器,它的写入不需要先擦除,也没有单独擦除指令,写一个字节和读一个字节在时序上几乎一样快。这就是它最有价值的反直觉点:你以为它和 W25Q64 一样要等 tPP,实际上它把写入做的和读一样,直接改写目标地址的内容。驱动代码这种资源,拿到手先看三件事:SPI 初始化是不是对的、片选时序有没有踩毛刺、写使能指令有没有漏发——把这三件事确认完,这颗芯片基本不会给你找麻烦。

2. FM25CL64 的技术边界:为什么它适合做参数掉电保存

2.1 铁电存储器和 Flash / E2PROM 的本质区别

FM25CL64 的存储介质是铁电晶体,它靠电容极化方向来保存数据,而不是像 Flash 那样靠电荷俘获。这意味着它没有“块擦除”这种操作,对任意地址直接写就行,写入速度和读取速度一样快。这点在开发中体现出的实际差异是:你用 Flash 保存系统参数,必须先查状态寄存器确认上次擦写完成,再页写入,再等忙标志;而 FM25CL64 只需要把写使能指令发出去,然后直接写目标字节,一个 SPI 传输周期内完成。

寿命方面,FM25CL64 的页读耐久性是 1e12 次,字节写耐久性 1e10 次。对比 AT24C02 的 100 万次擦写,这个数字大了三个数量级左右。如果你做的是频繁记录运行数据的设备,比如每次开关机都更新计数、掉电前存最后状态,铁电的成本虽然比 E2PROM 高一点,但省下来的时间、代码和排错成本其实更划算。

数据保持时间大约 10 年量级,作为参数存储够用。还需要注意容量:FM25CL64 是 64Kbit,也就是 8K 字节,地址范围 0x0000 到 0x1FFF。开发时如果存日志,8K 很容易打满,所以它适合存参数、校准值、运行状态,不适合当日志存储池用。

2.2 FM25CL64 和 W25Q64、AT24C02 的选型对比

这里列一个我实际选型常用的对比表,帮助大家看清这颗芯片在存储方案里占什么位置。

对比项FM25CL64W25Q64AT24C02
容量8K 字节8M 字节256 字节
写入方式直接改写,无擦除先擦除后写支持页写,无擦除
写字节耗时一个 SPI 帧时间页编程约 0.7~3ms写周期约 5ms
擦除耗时无扇区擦除 150ms 量级无
写耐久1e10 次1e5 次1e6 次
掉电保持约 10 年约 20 年约 100 年
主要用途频繁改写参数固件、日志、文件系统配置参数

从这张表能直接看出选型逻辑:如果你只是偶尔烧一次校准参数,AT24C02 够用;如果要存固件升级文件,必须用 W25Q64 这种大容量 Flash;如果每次开机都要更新运行时间、计件数、历史故障,别犹豫,可以直接上铁电。用铁电存储频繁改写数据的另一个好处是你不用做磨损均衡——以 1e10 次写入来看,每秒写一次也能连续写 300 多年,磨损均衡属于无用功。

3. 硬件连接与 SPI 初始化:把引脚和寄存器先理清楚

3.1 FM25CL64 引脚功能与最小硬件连接

FM25CL64 是 SOP-8 封装,引脚不算多,但有两个脚容易被忽略:HOLD 和 WP。HOLD 脚置低时暂停通信,不影响片选逻辑;WP 脚置低时写保护生效,此时写使能 WREN 指令也无法完成写操作。我见过不少新手只接四根线(SO、SI、SCK、CS),把 HOLD 和 WP 悬空,结果芯片内部上拉不强,出现偶发写失败。最常见的做法是 HOLD 接 VCC、WP 也接 VCC,把功能关掉,只留最基本的读写。

CS 片选脚要接单片机的 GPIO,用软件控制。因为 FM25CL64 支持多个 SPI 从设备挂在同一总线上,CS 引脚不能接固定电平。SCK、SI、SO 分别接 STM32F103 的 SPI 时钟、主出从入、主入从出。5V 和 3.3V 的兼容性方面,FM25CL64 的工作电压范围比较宽,可以直接由 STM32F103 的 3.3V 供电,SI、SO 信号线在 3.3V 电平下正常通信。

还值得注意的一点是 SPI 的模式。FM25CL64 支持模式 0(CPOL=0、CPHA=0)和模式 3(CPOL=1、CPHA=1),两种模式数据都在 SCK 上升沿采样。和它搭配的 STM32 SPI 外设,只要把 CPOL/CPHA 配成这两个组合之一就没问题。我的习惯是优先用模式 0,因为和大部分 SPI 设备的默认时序一致,逻辑分析仪抓波形时更容易看懂。

3.2 STM32F103 SPI1 引脚初始化示例

下面这段代码在标准库和 CubeMX 生成的工程里都能用,这里按 HAL 库风格写,因为 CubeMX 生成的代码比较常规。核心是把 SPI1 的引脚复用、时钟、模式、速率设置对。

void FM25CL64_SPI_Init(void) { // 使能 SPI1 和 GPIOA 时钟 __HAL_RCC_SPI1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct = {0}; // SCK: PA5 复用推挽输出; MOSI: PA7; MISO: PA6 GPIO_InitStruct.Pin = GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // CS: PA4 普通推挽输出,默认拉高 GPIO_InitStruct.Pin = GPIO_PIN_4; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // SPI1 配置:模式0,8位数据,主模式 hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; hspi1.Init.NSS = SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; HAL_SPI_Init(&hspi1); }

这段初始化代码有几个参数值得单独说明。SPI_BAUDRATEPRESCALER_8在 72MHz 主频下得到 9MHz 的 SPI 时钟,FM25CL64 支持 25MHz 时钟频率,理论上没问题,但实际项目里我会把分频系数调到 16 甚至 32,也就是 4.5MHz 或 2.25MHz。原因很简单:铁电芯片不像 Flash 那样要等内部编程时间,它的瓶颈在 PCB 走线和信号完整性,降速换稳定性非常划算,特别是在飞线连接的情况下。NSS配置成软模式,是因为 CS 由 GPIO 手动控制,方便做连续读写的时序控制。FirstBit一定要配成 MSB 先发,这是 SPI 协议的基本要求,FM25CL64 也是高位先传。

4. 驱动核心:读、写、状态寄存器与连续传输

4.1 指令集与状态寄存器的操作码

FM25CL64 的指令集不算复杂,核心指令只要记住六条。WREN(0x06)是写使能,任何写操作之前都必须先发这条指令,否则芯片直接忽略写请求。WRDI(0x04)是写禁止,执行后所有写操作被拒绝,除非重新 WREN。RDSR(0x05)读状态寄存器,WRSR(0x01)写状态寄存器。READ(0x03)读存储阵列,WRITE(0x02)写存储阵列。

状态寄存器各位的定义也和普通 E2PROM 很不一样。最低位是 WEL,写使能锁存位,执行 WREN 后置 1,完成一次写操作后自动清 0。Bit 2 和 Bit 3 是 BP1 和 BP0,块保护位,把这两个位置 1 可以保护存储阵列的上半部分或全部,上电默认 0,也就是不保护。Bit 7 是 WPEN,写保护使能位,只有当 WP 引脚拉低且 WPEN 为 1 时,BP 保护区域才无法被 WRSR 修改。这个状态寄存器里完全没有“忙标志位”——这也是前面说的,铁电写入不需要等待时间,指令发完就写完了。

4.2 基础读写函数代码与参数说明

先写底层字节交换函数,再包装成存储器的读写 API,这样代码结构清晰,以后换芯片也方便。SPI 收发我用 HAL_SPI_TransmitReceive,全双工模式下同时发指令并回收数据。

// 底层:写一个字节并读回一个字节 uint8_t FM25CL64_SPI_SwapByte(uint8_t tx_byte) { uint8_t rx_byte = 0x00; HAL_SPI_TransmitReceive(&hspi1, &tx_byte, &rx_byte, 1, 100); return rx_byte; } // 写使能:WREN 指令执行后 WEL 位置 1 void FM25CL64_WriteEnable(void) { FM25CL64_CS_LOW(); FM25CL64_SPI_SwapByte(0x06); FM25CL64_CS_HIGH(); } // 写禁止:WRDI 指令执行后 WEL 位清 0 void FM25CL64_WriteDisable(void) { FM25CL64_CS_LOW(); FM25CL64_SPI_SwapByte(0x04); FM25CL64_CS_HIGH(); } // 读状态寄存器,返回 8 位状态值 uint8_t FM25CL64_ReadStatus(void) { uint8_t status = 0x00; FM25CL64_CS_LOW(); FM25CL64_SPI_SwapByte(0x05); status = FM25CL64_SPI_SwapByte(0x00); FM25CL64_CS_HIGH(); return status; }

这里有几个芯片级细节容易写错。CS 拉低之后芯片才开始响应指令,所以每次操作都必须从 CS 拉低开始、CS 拉高结束。CS 拉高之后芯片回到待机状态,这也是为什么每次写操作之前都要重新执行 WREN。很多人会想:既然写使能位在上电后是 0,那我干脆每次都发 WREN 再写,不用判断状态寄存器。这个思路是对的,但注意 WREN 必须紧跟在 CS 拉低之后发,不能在 CS 拉低之前发。

4.3 存储阵列读写与地址格式注意事项

FM25CL64 的地址是 9 位,因为容量 8K,需要区分 A0 到 A8。但 SPI 指令通常按字节传输,所以地址分两个字节发:第一个字节是高 8 位地址,第二个字节是低 8 位?不对。仔细看数据手册会注意到:指令后跟两个地址字节,第一个字节的低 7 位是 A6-A0?这里有个常见的翻车点。

正确格式是:READ 指令后先发地址高字节,其中最低位是 A8,高 7 位填充 0;再发低字节,包含 A7-A0。也就是说,9 位地址被拆成“1 位 + 8 位”放进两个字节里。写代码时一定要先算好地址位移,否则从 255 字节边界往后全乱。下面给出带容错处理的读写实现。

// 从指定地址读 len 字节到 buf void FM25CL64_ReadBuffer(uint16_t addr, uint8_t *buf, uint16_t len) { uint16_t i; FM25CL64_CS_LOW(); FM25CL64_SPI_SwapByte(0x03); // 地址高字节:bit0 是 A8,高 7 位为 0 FM25CL64_SPI_SwapByte((uint8_t)((addr >> 8) & 0x01)); // 地址低字节:A7-A0 FM25CL64_SPI_SwapByte((uint8_t)(addr & 0xFF)); for (i = 0; i < len; i++) { buf[i] = FM25CL64_SPI_SwapByte(0x00); } FM25CL64_CS_HIGH(); } // 从指定地址写 len 字节 void FM25CL64_WriteBuffer(uint16_t addr, uint8_t *buf, uint16_t len) { uint16_t i; FM25CL64_WriteEnable(); FM25CL64_CS_LOW(); FM25CL64_SPI_SwapByte(0x02); FM25CL64_SPI_SwapByte((uint8_t)((addr >> 8) & 0x01)); FM25CL64_SPI_SwapByte((uint8_t)(addr & 0xFF)); for (i = 0; i < len; i++) { FM25CL64_SPI_SwapByte(buf[i]); } FM25CL64_CS_HIGH(); }

写缓冲函数的地址上限需要注意:FM25CL64 内部没有页面边界的概念,跨 256 是可行的,只要两个地址都在 8K 范围内。这和 W25Q64 完全不同,W25Q64 写入跨越页边界时会自动回卷,导致数据错位。铁电虽然允许连续写,但跨 0x1FFF 边界时会回卷到 0x0000,所以调用层应该自行为上层应用做 8K 边界检查。

4.4 写状态寄存器配置块保护

有些项目需要把存储区前半部分设成只读,防止参数被意外改写。FM25CL64 的 WRSR 指令加上块保护位可以做到:把 BP0、BP1 置位后,对应的地址区间就不能写入,写操作会被芯片忽略。

// 设置块保护,保护全部 8K 空间 void FM25CL64_EnableFullBlockProtection(void) { FM25CL64_WriteEnable(); FM25CL64_CS_LOW(); FM25CL64_SPI_SwapByte(0x01); FM25CL64_SPI_SwapByte(0x0C); // BP1=1, BP0=1 FM25CL64_CS_HIGH(); }

参数 0x0C 对应二进制 0000_1100,也就是把状态寄存器的 Bit 2 和 Bit 3 置 1。执行完这条指令后,再对任何地址发 WRITE 都不会有效果。注意一个细节:WRSR 写完后 WEL 位会自动清 0,如果要解除保护,得先发 WREN 再写 0x00 到状态寄存器。

5. 避坑实测:五个真实案例,从片选毛刺到地址错位

5.1 读回全是 0xFF 或 0x00

现象:写进去的数据再读出来变成 0xFF 或全 0,查看缓冲区全是同一字节。

原因分析:读回全 FF,先怀疑引脚模式。MOSI、MISO、SCK 任意一根引脚配置成开漏或者没有复用成外设功能,信号就会不正常。另一个常见原因是 SPI 模式不匹配,CPOL 和 CPHA 与芯片期望相反,数据在错误的时钟沿采样。读回全 0 则多半是 MISO 引脚一直输出低电平,CS 没有真正选中芯片,或者芯片进入写保护后读到的数据本身保持初始值。

解决思路:先做引脚回环测试,MOSI 和 MISO 短接,发 0xA5 接收 0xA5,能通过说明 SPI 外设没问题。再检查 CS 波形,用示波器或逻辑分析仪确认 CS 拉低时间覆盖整个指令和地址发送周期。最后用 FM25CL64_ReadStatus 读状态寄存器,确认通信链路是真的通了。

5.2 写操作无效果,状态寄存器 WEL 位始终不置 1

现象:调用写函数后读回数据不变,RDSR 读到的状态值 Bit0 一直是 0。

原因分析:WEL 位没有置 1,说明 WREN 指令没有生效。通常是时序问题:CS 拉低后 SCK 第一个沿开始采样之前,WREN 指令字节必须已经在 SI 上稳定输出。如果代码用了 SPI_DATASIZE_16BIT 或者发送时混入了多余字节,芯片就解不出 0x06。也可以是 WP 引脚被拉低,硬件上禁用了写使能。

解决思路:检查 SCK 和 SI 的时序,用逻辑分析仪抓 CS 拉低后的第一个字节。软件上把 FM25CL64_WriteEnable 单独抽出来,写完立刻 RDSR 读状态,确认 WEL 为 1 再继续写。硬件上确认 WP 引脚接 VCC 而不是悬空或接地。

5.3 地址写入和读出的内容错位

现象:写入地址 0x0100 的数据,从 0x0100 读不出来,却在 0x0000 读到了。

原因分析:这就是前面强调的 9 位地址拆分问题。如果把地址高字节直接传 (addr >> 8),那 0x0100 会得到 0x01,这个字节显然超出了 A8 的单比特范围。芯片会把多余的位当成指令扩展或者直接忽略,地址就被截断了。还有另一种写法:用 8 位地址模式调用,只发一个地址字节,结果超过 256 字节的地址全部回卷。

解决思路:统一封装一个 SendAddress 函数,输入 uint16_t 地址,内部强制拆成两个字节发送。写完地址后用 0x00 填充多读几个字节做自检,对比回读数据和写入数据。如果你是从其他 2K/4K 容量的铁电芯片移植过来的代码,尤其要注意地址拆分逻辑,因为容量不同拆分方式往往也不同。

5.4 CS 控制毛刺导致芯片误操作

现象:正常读写工作,但偶发性出现数据写乱,逻辑分析仪上看到 CS 波形有多个短脉冲。

原因分析:GPIO 速度配置太高,加上 PCB 走线过长,CS 线上出现振铃。单片机引脚输出驱动能力过强时,跳变瞬间产生的过冲可能触发芯片的内部逻辑。另一个典型场景是 DMA 传数据期间,CS 在传输结束前被提前拉高,最后一次收发还没完成就结束了片选周期。

解决思路:软件上把 CS 拉低和拉高之间留出至少一个 SPI 字节的时间,不连续操作时让 CS 保持高电平久了再操作。硬件上在 CS 引脚串一个 33Ω 电阻或者并一个 10pF 电容,抑制边沿振铃。如果用了 SPI DMA,检查传输完成回调里再操作 CS,不要用定时器延时猜时间。

5.5 跨 8K 地址回卷导致数据覆盖

现象:写入地址 0x1FFF 末尾的数据,在地址 0x0000 也出现了同样的内容。

原因分析:FM25CL64 的地址计数器在 0x1FFF 之后再递增会回卷到 0x0000。这在技术上是被芯片允许的行为,但如果你在各层之间没做边界检查,日志记录或者参数批量写入时就会悄无声息地覆盖最早的数据,没有报错也没有标志。

解决思路:在驱动之上再加一层地址范围检查,len 加上起始地址超过 8192 就直接返回错误。也可以用“分段存储”思路:把 8K 拆成两个 4K 区,参数区只写前半部分,日志区只写后半部分,从设计上避免跨界。驱动函数里保留地址回卷的特性,应用层必须自己维护地址边界。

6. 进阶验证:用掉电复读和边界压力测试确认驱动可靠

驱动代码写完不等于能用,铁电存储器的验证比普通 E2PROM 简单,但比想象中容易遗漏。我拿到驱动工程后一般做三组验证:地址边界、频率极限、掉电保持。这三组做完,才算真正把驱动“验收”过了。

第一组验证针对地址边界。写一个测试函数,把 0x0000、0x00FF、0x0100、0x0FFF、0x1FFF 这几个关键地址各写一个特征字节,比如 0xAA、0x55、0x01、0x02、0x03。断电重启后再读回来比对,任何一个对不上都说明地址拆分或者 CS 时序还有问题。跨 255 和跨 255 边界的地址特别值得测一遍,因为多字节地址的读写最容易在这一段翻车。

第二组验证针对 SPI 时钟频率。把分频系数从 8 一路改到 128,也就是 9MHz 到 562.5KHz,每个分频下写 1000 字节再读回比对。这组测试不用在产线做,但如果你准备把驱动放进产品,至少测一次。我遇到过一次芯片在 9MHz 偶发错误,降到 4.5MHz 完全正常的案例,最后定位是 PCB 走线过长导致 SI 信号建立时间不足。这种问题在没有逻辑分析仪的情况下很难抓,频率扫描是最直接的排查手段。

第三组验证模拟真实掉电场景。写一个 count 变量到地址 0x0000,每次上电先读出来加一再写回去。人为断电再上电循环 50 次,确认 count 连续递增没有跳变。这一步实际验证的是芯片在电压跌落到临界值时是否出现半写入状态。铁电存储器理论上不存在半写入,但 CS 信号如果先于 VCC 掉电,可能出现不可预期的行为。测试时用一个可控的电源开关,断电瞬间故意让 CS 拉低,观察数据是否损坏。

完整验证代码可以封装成下面这样:

uint8_t FM25CL64_SelfTest(void) { uint8_t buf[8]; uint8_t i; uint16_t test_addrs[4] = {0x0000, 0x00FF, 0x0100, 0x1FFF}; // 在每个边界地址写入固定模式 for (i = 0; i < 4; i++) { buf[0] = 0xA5; buf[1] = (uint8_t)i; FM25CL64_WriteBuffer(test_addrs[i], buf, 2); } // 回读并比较 for (i = 0; i < 4; i++) { FM25CL64_ReadBuffer(test_addrs[i], buf, 2); if (buf[0] != 0xA5 || buf[1] != i) { return 1; // 自检失败 } } return 0; // 全部通过 }

自检函数本身写的比较紧凑,实际项目里我会把测试失败时的地址和期望值打印出来,定位更快。另外,如果工程里有 RTOS,建议把自检放在初始化任务里,不要放在中断上下文,因为 SPI 通信本身需要时间片。

最后说一个我踩过的细节:FM25CL64 的 WP 引脚如果不接 VCC,而是留了 GPIO 控制,那每次上电时 GPIO 默认状态可能是低电平,导致解锁之前就被写保护了。从那以后,我设计硬件时要么把 WP 直接上拉到 VCC,要么在单片机初始化代码的第一行就把 WP 引脚写高。同样的习惯也用在 HOLD 脚上。这两个引脚处理干净,驱动代码里就能少两个排查方向。希望上面的读写示例和排查思路能帮到你,在项目里少走几步弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询