STM32F103 SPI驱动FM25CL64铁电存储器:从时序解析到DMA优化 📅 发布时间:2026/9/2 7:33:50 👁 浏览次数: 简介本资源是一套面向嵌入式开发初学者与STM32项目实践者的FM25CL64铁电存储器驱动代码专为STM32F103系列单片机设计解决SPI接口下非易失性铁电存储器的可靠读写需求适用于需频繁擦写、高速存取且要求断电不丢数据的工业控制、数据记录等场景。压缩包共含2个核心文件1个C源文件实现SPI初始化、字节级读写及状态检测1个H头文件定义寄存器宏与函数接口总大小仅2KB轻量简洁便于集成到现有工程中。已有1684人学习下载代码结构清晰、注释完整包含SPI1外设与GPIOA初始化配置、FM25CL64指令时序封装及基础读写函数可直接调用完成单字节读写操作无需额外修改即可在标准Keil MDK环境下编译运行是理解铁电存储器SPI协议与底层驱动开发的实用参考范例。1. 项目背景与铁电存储器的独特价值最近在整理一个老项目的代码翻出来一个基于STM32F103的SPI读写FM25CL64铁电存储器的驱动。当时做这个项目主要是为了解决一个数据掉电保存的痛点。你可能也遇到过用普通的EEPROM或者Flash写数据之前得先擦除速度慢不说擦写次数还有寿命限制频繁记录一些关键参数或者事件日志的时候心里总是不踏实。而像FM25CL64这种铁电存储器FRAM它的特性就非常吸引人读写速度堪比RAM没有擦除操作直接覆盖写入而且号称读写寿命能达到10的14次方几乎是无限次了。这对于需要频繁、快速、可靠地进行非易失性数据存储的场景比如数据采集器的实时记录、系统运行状态的黑匣子、或者是一些需要频繁更新的配置参数简直就是量身定做。我用的这颗FM25CL64容量是64Kbit也就是8KBSPI接口工作电压3.3V和STM32F103正好匹配。网上虽然能找到一些例程但很多要么是模拟SPI效率不高要么是库函数版本封装得太深出了问题不好排查还有一些直接就是读写的代码片段缺胳膊少腿没有形成完整的、可移植的驱动模块。所以我当时就决定自己从头撸一个把初始化、读、写、擦除虽然FRAM不需要擦除但指令集里有这个操作码有时为了一致性也会实现、状态寄存器读写这些基本操作都封装好并且用硬件SPIDMA的方式来追求极致的速度和低CPU占用。这个驱动代码包就是那次折腾的成果经过实际项目验证稳定可靠。2. 硬件连接与SPI模式深度解析拿到一颗FM25CL64第一步肯定是把它正确地接到你的STM32F103最小系统板上。这颗芯片是标准的6针SPI接口也有8针带HOLD引脚的版本分别是片选CS、时钟SCK、主机输出从机输入MOSI对应FM25CL64的SI、主机输入从机输出MISO对应FM25CL64的SO以及电源和地。硬件连接很简单关键在于SPI模式的配置这里面的细节如果配错了通信根本建立不起来。FM25CL64的SPI模式固定为模式0CPOL0 CPHA0和模式3CPOL1 CPHA1。这是什么意思呢CPOL是时钟极性0表示空闲时SCK为低电平1表示空闲时为高电平。CPHA是时钟相位决定数据在时钟的哪个边沿被采样。模式0就是空闲低电平在SCK的上升沿采样数据模式3是空闲高电平在SCK的下降沿采样数据。根据FM25CL64的数据手册它同时支持这两种模式这给了我们一些灵活性。但在实际配置时必须确保STM32的SPI外设模式与之一致。我通常习惯用模式0因为很多SPI器件都默认支持这个模式。除了模式还有几个关键参数数据帧格式通常是8位、MSB先行、以及时钟频率。FM25CL64最高支持20MHz的SCK速率对于STM32F103的硬件SPI来说完全是小菜一碟。但这里有个经验之谈在布线不是特别理想或者为了追求极致稳定性的情况下我通常会适当降低SPI时钟频率比如先降到1MHz或5MHz进行调试通信稳定后再逐步提高。配置STM32的SPI外设时需要仔细查阅参考手册特别是“STM32F103中文参考手册”找到SPI对应的章节对照寄存器位一个一个设置或者使用标准外设库SPL或HAL库的函数。我这份驱动是基于标准外设库写的因为我觉得它比HAL库更贴近硬件效率也更高。关于片选CS你可以用硬件SPI自带的NSS引脚但更多时候我更喜欢用任意一个GPIO来软件控制。原因很简单更灵活。比如你的SPI总线上可能挂了不止一个设备用软件控制每个设备的片选引脚会更方便。在驱动代码里拉低CS是启动传输传输完成后一定要拉高CS这是一个完整的SPI事务。3. 驱动程序设计从寄存器操作到DMA加速一个健壮的驱动程序不应该只是一两个读写函数而应该是一个层次清晰、便于管理和移植的模块。我的驱动主要分为以下几个层次3.1 底层硬件抽象层BSP这一层直接操作STM32的GPIO和SPI外设寄存器。它提供了最基本的引脚控制CS、SCK、MOSI、MISO的初始化和电平控制和SPI字节收发函数。例如SPIx_ReadWriteByte这个函数就是通过查询标志位的方式发送一个字节并接收一个字节。这是所有上层功能的基础。为什么用查询而不是中断对于单字节操作查询足够简单高效而对于连续大数据量传输我们会用更高级的方式。3.2 核心指令集实现层这一层实现了FM25CL64支持的所有SPI指令。每个指令都是一个1字节的操作码Opcode。最重要的几个包括WREN(0x06)写使能指令。这是很多新手容易掉坑的地方FM25CL64在每次进行写操作包括写状态寄存器之前都必须先发送WREN指令否则写操作会被忽略。写完成后写使能锁存器会自动清除。WRDI(0x04)写禁止指令。RDSR(0x05)读状态寄存器。WRSR(0x01)写状态寄存器。状态寄存器可以配置写保护区块等。READ(0x03)读存储器数据。WRITE(0x02)写存储器数据。FSTRD(0x0B)快速读指令在标准读的基础上允许在地址传输期间保持SCK为高理论上能更快一点。SLEEP(0xB9)进入低功耗睡眠模式。这一层的函数就是按照SPI时序先发操作码再发地址对于读写操作地址是2字节然后进行数据传输。例如单字节读函数FRAM_ReadByte其流程就是拉低CS - 发送READ操作码 - 发送16位地址 - 接收一个数据字节 - 拉高CS。3.3 应用功能封装层这一层对用户最友好。它基于指令集层封装了常用的操作FRAM_Init初始化函数配置好SPI和GPIO。FRAM_Write写数据函数。内部会自动先发送WREN指令然后发送WRITE指令、地址和数据。FRAM_Read读数据函数。FRAM_Erase擦除函数。虽然FRAM物理上不需要擦除但提供这个函数有时是为了软件逻辑的统一比如擦除整个扇区再写。它内部其实就是用0xFF或0x00去覆盖写一片区域。FRAM_Write_StatusRegister/FRAM_Read_StatusRegister状态寄存器读写。3.4 DMA传输优化层进阶当需要读写大量连续数据时比如存储一个几KB的结构体单字节查询方式的效率就很低了CPU被完全占用。这时就该DMA出场了。STM32的SPI可以配合DMA实现数据在内存和SPI数据寄存器之间的自动搬运。以DMA写为例流程如下配置SPI的Tx DMA通道将内存中待发送的数据缓冲区包含操作码、地址、用户数据设置为DMA的源地址SPI的DR寄存器为目的地址。同样配置Rx DMA通道如果是全双工读操作也需要将SPI的DR寄存器设置为源地址一个接收缓冲区为目的地址。使能DMA和SPI。拉低CS启动传输。此时DMA会自动搬运数据CPU被解放出来。等待DMA传输完成中断或标志位然后拉高CS。在驱动中我实现了FRAM_Write_DMA和FRAM_Read_DMA函数。这里的关键点在于数据缓冲区的构造。对于写DMA你需要构造一个完整的SPI发送序列WREN指令 -WRITE指令 - 16位地址 - 用户数据。这个序列需要放在一个连续的数组里交给DMA。DMA传输完成后一定要检查SPI和DMA的状态标志位是否正常确保数据传输的完整性。注意使用DMA时要特别注意内存对齐和缓冲区的大小。STM32的DMA对数据宽度有要求确保你的缓冲区地址和SPI数据寄存器宽度通常是8位对齐。另外开启DMA传输后要避免在传输完成前访问或修改发送/接收缓冲区。4. 关键代码剖析与避坑指南光讲架构太抽象我们来看几段核心代码里面藏着我踩过的坑。4.1 写使能WREN的严格时序void FRAM_WriteEnable(void) { FRAM_CS_LOW(); // 拉低片选 SPIx_ReadWriteByte(0x06); // 发送WREN指令码 FRAM_CS_HIGH(); // 拉高片选完成指令 // 注意这里不需要延时拉高CS后指令即生效。 // 但写操作必须在此之后、下一次拉低CS之前进行。 }这段代码很简单但坑在于对“之后”的理解。WREN指令执行后写使能锁存器WEL被置位。但这个锁存器只在芯片持续上电且未收到WRDI指令或完成一次写操作时才保持有效。这意味着你不能在发送WREN后隔很久再去写。正确的做法是在同一个“CS低-高”事务周期内或者紧跟着的下一个事务中立即进行写操作。在我的写函数里WREN和WRITE是在两个连续的SPI事务中完成的中间只隔了拉高和拉低CS的时间这是安全的。4.2 读写函数的地址与数据缓冲uint8_t FRAM_ReadByte(uint16_t addr) { uint8_t data 0; FRAM_CS_LOW(); SPIx_ReadWriteByte(0x03); // READ指令 SPIx_ReadWriteByte((uint8_t)(addr 8)); // 地址高字节 SPIx_ReadWriteByte((uint8_t)(addr 0xFF)); // 地址低字节 data SPIx_ReadWriteByte(0xFF); // 发送dummy数据同时接收 FRAM_CS_HIGH(); return data; }读函数需要注意发送完地址后主机STM32需要继续提供时钟才能从机FM25CL64输出数据。所以SPIx_ReadWriteByte(0xFF)这行发送0xFF无意义数据只是为了产生8个SCK时钟脉冲同时把MISO上的数据读回来。写函数也是类似的只是数据流方向相反。4.3 DMA传输的缓冲区构造陷阱这是最易出错的地方。假设我们要用DMA从地址0x1000开始写入100个字节的数据UserData。uint8_t dma_tx_buffer[103]; // WREN WRITE 2字节地址 100字节数据 dma_tx_buffer[0] 0x06; // WREN // 注意这里必须有一个独立的CS事务来执行WREN // 所以我们先不把WREN放到后续的连续DMA序列里而是单独执行一次。 FRAM_WriteEnable(); // 然后构造主要的写序列 dma_tx_buffer[0] 0x02; // WRITE dma_tx_buffer[1] 0x10; // 地址高字节 (0x1000 8) dma_tx_buffer[2] 0x00; // 地址低字节 memcpy(dma_tx_buffer[3], UserData, 100); // 用户数据 // 配置DMA源地址为dma_tx_buffer目标地址为SPI-DR长度为103。 // 然后拉低CS启动DMA传输。我最初的错误尝试是把WREN也塞进同一个DMA序列里放在WRITE前面。但这样不行因为WREN和WRITE必须是两个独立的SPI指令由CS的下降沿和上升沿界定。所以必须分两次一次单字节事务发WREN另一次DMA事务发WRITE地址数据。4.4 状态寄存器的保护功能FM25CL64的状态寄存器Status Register的位0BP1和位1BP0用于设置写保护区块。这可以防止某些内存区域被意外写入。在驱动初始化后我通常会读取一下状态寄存器确认其值是否符合预期通常默认是0x00无保护。如果你的应用需要对某些区域进行写保护可以通过WRSR指令进行配置。但记住写状态寄存器同样需要先发送WREN指令。5. 集成测试与常见问题排查驱动写好了怎么验证它是对的我一般会设计一个简单的测试流程通常放在main函数里或者一个独立的测试函数中。5.1 基础读写测试初始化调用FRAM_Init()确保SPI和GPIO配置正确。可以用逻辑分析仪或者示波器抓一下CS、SCK、MOSI的波形看初始化后引脚状态是否正确。写一个已知模式向一个固定地址比如0x0000写入一个特定的数据序列例如0xAA, 0x55, 0x01, 0x02, 0x03, 0x04。读回验证从同一地址读回相同长度的数据与写入的序列逐字节比较。必须完全一致。跨地址测试在内存的不同位置开头、中间、结尾附近重复步骤2和3。全容量快速测试写一个简单的模式如地址的低字节到所有存储单元然后读回校验。这可以快速发现是否有坏的存储单元。5.2 DMA读写测试构造一个较大的数据块如1KB。使用FRAM_Write_DMA写入。使用FRAM_Read_DMA读入另一个缓冲区。比较两个缓冲区是否完全一致。同时在DMA传输完成中断里设置标志位确保传输完成回调被正确执行。5.3 常见问题与排查思路问题完全读不到数据或者读到的全是0xFF或0x00。排查1硬件连接。这是第一嫌疑犯。用万用表检查VCC和GND是否接好电压是否为3.3V。检查SCK、MOSI、MISO、CS四根线是否与STM32连接正确有没有虚焊、短路。特别注意STM32的MOSI应接FM25CL64的SISTM32的MISO应接FM25CL64的SO。接反了肯定不通。排查2SPI模式。用逻辑分析仪抓取SPI波形。看CS拉低后SCK是否有波形MOSI上是否有数据对应你发送的操作码和地址波形是否符合你配置的模式CPOL CPHA最直观的方法是看数据采样的边沿是否正确。如果模式不对调整STM32的SPI配置。排查3片选CS。确保CS引脚在传输开始时被拉低传输结束后被拉高。CS线是否被其他器件干扰如果是软件控制CSGPIO的推挽输出模式配置是否正确排查4指令和地址。确认你发送的操作码如0x03读是正确的。地址字节序是否正确先高字节后低字节问题可以读但不能写。排查1写使能WREN。这是最高频的原因。确保在每次WRITE或WRSR操作前都正确执行了WREN指令并且WREN和写操作之间的时序符合要求CS拉高再拉低。排查2写保护。检查状态寄存器RDSR的BP1和BP0位看你试图写入的地址是否落在了写保护区域内。如果是你需要先WREN然后WRSR来解除保护或修改保护范围。排查3电源稳定性。写操作对电源纹波可能更敏感。确保电源干净。问题DMA传输数据错乱或卡死。排查1缓冲区地址和长度。DMA配置中的内存地址和外设地址是否正确数据长度是否包含了操作码、地址和所有数据缓冲区是否在传输过程中被意外修改排查2DMA通道与中断。SPI的Tx和Rx DMA是否配置到了正确的通道参考STM32参考手册的DMA章节DMA和SPI的传输完成中断是否使能中断服务函数ISR是否清晰是否清除了相应的中断标志位排查3内存对齐。确保用于DMA传输的缓冲区在内存中对齐避免访问越界。6. 驱动代码的移植与优化建议这份驱动代码虽然基于STM32F103和标准外设库但其设计思想是易于移植的。6.1 移植到其他MCU或平台替换底层硬件抽象层你需要重写spi_gpio.c/.h和spi_hardware.c/.h或者类似名称的文件里的函数。将里面直接操作STM32寄存器的代码替换成目标平台比如GD32、AT32、甚至是ESP32S3的SDK函数或寄存器操作。调整接口定义修改fram.h中关于SPI实例、CS引脚等硬件依赖的宏定义。DMA部分如果目标平台也有DMA且你希望使用则需要按照该平台的DMA驱动方式重写DMA初始化、配置和中断处理部分。如果不用DMA则直接使用查询或中断方式的读写函数即可。6.2 性能优化建议减少CS切换延迟在连续读写多个字节时可以保持CS为低一次性发送读/写指令、地址和所有数据最后再拉高CS。这比每字节都开关CS要快得多。我的驱动中的FRAM_Read和FRAM_Write函数已经支持多字节连续操作。使用快速读FSTRD对于大数据量读可以尝试使用FSTRD(0x0B)指令代替普通的READ(0x03)。FSTRD允许在传输地址期间SCK保持高电平理论上能减少一点指令周期但需要芯片支持且时序配合。中断与DMA的选择对于小数据量、非实时的操作查询法足够简单。对于中等数据量、不希望阻塞CPU太久可以用SPI传输完成中断。对于大数据量、要求高带宽低CPU占用的场景DMA是唯一选择。文件系统适配如果你的应用需要更复杂的存储管理如磨损均衡、坏块管理、目录结构可以考虑将FRAM驱动作为底层设备移植一个轻量级的文件系统如LittleFS或者Flash模拟EEPROM的库如FlashDB到其上。7. 从FM25CL64延伸SPI器件驱动开发通用思路通过这个FM25CL64的驱动开发其实可以总结出一套开发SPI外设驱动的通用方法论这对于你以后接触其他SPI器件比如AD2S1210旋变解码芯片、AD7616ADC、ST7701S屏驱动、SPI Flash、SD卡等都很有帮助。数据手册精读这是第一步也是最关键的一步。找到器件的数据手册Datasheet重点看电气特性工作电压、逻辑电平3.3V/5V/1.8V是否需要电平转换。接口时序图SPI模式CPOL/CPHA、时钟频率最大值、建立保持时间Setup/Hold Time。画一张时序图标出CS、SCK、MOSI、MISO的变化关系。指令集Command Set所有操作码Opcode列表、每个指令的格式操作码后跟几个地址字节、几个哑元字节、几个数据字节。寄存器映射如果有内部寄存器每个寄存器的地址、位定义、读写属性。关键流程如上电初始化序列、写使能序列、睡眠唤醒序列等。硬件连接验证对照原理图确保连线正确电源和地稳定。对于高速SPI10MHz需要考虑信号完整性走线尽量短必要时串联小电阻。分层次驱动设计就像本文的驱动一样分为硬件抽象层、指令层、功能层、应用层。这样代码清晰易于调试和移植。从简单到复杂验证先实现最基本的字节读写函数用逻辑分析仪抓波形确保时序完全符合数据手册。然后实现一个核心功能比如读ID如果器件有。这是验证通信链路是否打通的“Hello World”。再实现主要的数据读写功能。最后考虑高级功能如DMA、中断、低功耗模式。编写健壮的测试用例包括边界测试地址0最大地址、压力测试连续读写、异常测试断电恢复等。文档与注释在代码中详细注释关键时序、注意事项。写一个简单的README说明驱动的接口函数、使用示例和已知问题。回过头看为STM32F103编写FM25CL64的SPI驱动不仅仅是为了让一块芯片跑起来更是一个深入理解SPI协议、掌握单片机与外设交互、实践嵌入式驱动开发分层设计的过程。这份代码包里的每一个函数、每一行注释都对应着一次示波器的测量、一次逻辑分析仪的抓取、一次调试打印的追踪。希望这份详细的拆解和代码思路能帮你少走弯路当你下次遇到“SPI不通”、“写不进去数据”、“DMA卡死”这些问题时能有一个清晰的排查脉络。本文还有配套的精品资源点击获取