ARTICLE DETAIL

资讯详情

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

STM32H743驱动FM25CL64铁电存储器:SPI总线实战避坑指南

STM32H743驱动FM25CL64铁电存储器:SPI总线实战避坑指南 这板子是拿来做一台带屏工业设备的主控用的STM32H743屏幕逻辑、JPEG解码、LVGL界面都压在它身上。原来的方案里存参数用的是24C64 EEPROM跑了一段时间发现两个问题一个是写参数时要先擦后写掉电瞬间如果正在写数据容易半路没了另一个是循环记录运行日志时EEPROM写寿命扛不住频繁擦写虽然选了大厂料心里还是没底。后来决定换成FM25CL64铁电存储器64Kbit容量8KB空间写不用擦除理论上写10的12次方次都不用担心寿命。结果就是这块小小的存储芯片让我在SPI总线上折腾了两天。这篇文章就是把我在STM32H743上驱动FM25CL64遇到的坑按从硬件到软件、从初始化到稳定运行的顺序整理出来。如果你也在用H743、H750这类高性能M7内核芯片挂FRAM、Flash这类SPI从机或者正打算把EEPROM换成铁电那这篇东西应该能帮你少走点弯路。文中涉及的时序细节、HAL库的隐藏行为和排查思路我都会尽量讲清楚。1. 这项目是干嘛的FM25CL64为什么值得折腾1.1 铁电存储器到底是什么东西铁电存储器FRAM本质上是一种非易失存储器核心工艺是铁电晶体材料利用铁电晶体的极化状态来存储数据。它跟Flash、EEPROM最大的区别在于写入机制Flash和EEPROM写数据前必须擦除靠的是浮栅电荷的增减而FRAM的写入是改变晶体极化方向不需要擦除操作所以写入速度能跟普通RAM差不多写一个字节就是一次总线周期的事。FM25CL64就是一颗SPI接口的铁电存储器容量是64Kbit换算下来正好8KB。它的特点有三个第一写入不需要先擦除想覆盖就直接写第二写入寿命极高手册上写的是10的12次方次实际上你把它当普通RAM那么频繁写都写不坏第三功耗非常低待机电流微安级别很适合电池供电的设备。当然代价是成本比同容量的EEPROM贵不少所以它不会取代EEPROM而是用在那些对写入频率、数据可靠性要求高的特定场景里。1.2 我为啥没选EEPROM而是选了FM25CL64不少朋友会问24C64不也是64Kbit吗容量一样为什么要换成FM25CL64主要就是上面说的两个痛点EEPROM写之前要擦除而且Page Write虽然一次能写几十个字节但一个page占用的时间固定是5ms左右如果你要频繁更新一组运行参数累积起来很可观。更关键的是EEPROM写寿命一般在100万次左右对于每天要写几千次运行记录的项目来说几个月就到寿命边缘了。FM25CL64就完全没这个问题。不需要擦除页写和连续写的速度只受限于SPI时钟频率实际测试下来写满8KB也就几毫秒。写寿命号称10的12次方次基本等于不坏。所以对于我这种需要频繁记录参数、掉电要保存、还可能要循环覆盖日志的场景FRAM是比EEPROM更合适的选择。1.3 硬件连接和接线注意事项我的接线非常简单用STM32H743的SPI1来挂FM25CL64引脚分配如下FM25CL64引脚STM32H743引脚说明CSPA4软件控制片选GPIO输出SCKPA5SPI1_SCKMISOPA6SPI1_MISOMOSIPA7SPI1_MOSIHOLDVCC必须接高电平不能悬空WPVCC建议接高电平禁用写保护VDD3.3V供电VSSGND接地这里有两个引脚要特别提醒。第一个是HOLD它低电平会让FRAM暂停SPI通信如果悬空在电磁环境稍差点的地方很容易被干扰拉低然后SPI就假装死机了。第二个是WP写保护引脚很多模块板上这个脚是悬空的如果状态寄存器里的WPEN位被置1芯片就进入写保护状态你发什么写命令都不生效。稳妥做法是硬件上把WP直接拉高让写保护永远失效。另外我建议在MISO上加一个10kΩ上拉电阻FRAM的MISO在未选中时是高阻加个上拉能避免读数据时采到浮空电平。2. 初始化才是最容易被忽略的坑2.1 CubeMX里SPI参数的正确配置很多踩坑其实从CubeMX配置阶段就埋下了。我一开始用的默认配置SPI跑起来后读状态寄存器能读出来但写数据就是不对后面排查下来发现是参数配得不严。这里直接给出我最终稳定运行的配置SPI ModeMasterDirection2 Lines Full DuplexData Size8 BitClock PolarityLowClock Phase1 EdgeNSSSoftware片选完全用GPIO控制Baud Rate Prescaler8First BitMSB FirstTIModeDisableCRCCalculationDisable对应的HAL初始化结构体是这样的SPI_HandleTypeDef hspi1; 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; hspi1.Init.TIMode SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE; hspi1.Init.CRCPolynomial 7; HAL_SPI_Init(hspi1);这里有几个细节值得展开说。第一个是时钟频率H743如果跑480MHz主频APB2一般是120MHzSPI1挂在APB2上分频8就是15MHz。FM25CL64B的最高SPI时钟是40MHz但老批次FM25CL64最高只有20MHz所以我直接按15MHz来跑留足余量。第二个是GPIO速度PA5、PA6、PA7这些引脚如果复用为SPIGPIO Output Speed一定要设成Very High否则在10MHz以上的SCK时波形边沿会明显变缓容易读错。2.2 FM25CL64命令集和写使能机制FM25CL64的指令集非常简单一共就6条命令命令操作码功能WREN0x06写使能WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据关键点在于写使能机制。FM25CL64上电后默认处于写禁止状态要执行一次WREN命令才能把状态寄存器里的WEL位置1然后才能执行WRITE或WRSR。这一点跟SPI Flash的机制很像。关于WEL位在写完一次后会不会自动清零不同版本的芯片手册描述有差异我实测中发现芯片在完成一次写操作后WEL状态不是特别稳定可控。所以我的代码习惯是每次写操作前都固定发一次WREN多花几微秒换来的是确定性。这是驱动层面第一个需要养成的习惯。状态寄存器只有两个有效位bit1是WELbit7是WPEN。可以用RDSR命令读出来调试。下面这段代码就是读状态寄存器的uint8_t fm25cl64_read_status(void) { uint8_t cmd FM25CL64_RDSR; uint8_t status 0; fm25cl64_cs_low(); HAL_SPI_Transmit(hspi1, cmd, 1, 100); HAL_SPI_TransmitReceive(hspi1, cmd, status, 1, 100); fm25cl64_cs_high(); return status; }注意读状态这步发完命令之后要同时在MOSI上发送一个字节内容无所谓一般发0x00或0xFFSCK才会产生时钟从机才会把状态寄存器从MISO送出来。用HAL_SPI_TransmitReceive是最稳妥的有些HAL版本用HAL_SPI_Receive虽然也会产生时钟但我遇到过部分H7库实现里接收前必须保证TX已经有数据在写否则SCK不动作的情况所以直接用TransmitReceive最省事。2.3 最小读写代码和上板验证流程我先给出一个最简可用的读写函数。写操作分三步使能写、发命令和地址、发数据。读操作就是发命令和地址、连续读数据。注意片选在整个命令地址数据过程中必须一直保持低电平不能拆开发送否则FRAM会认为这是多条新命令。#define FM25CL64_WREN 0x06 #define FM25CL64_WRDI 0x04 #define FM25CL64_RDSR 0x05 #define FM25CL64_WRSR 0x01 #define FM25CL64_READ 0x03 #define FM25CL64_WRITE 0x02 static void fm25cl64_cs_low(void) { HAL_GPIO_WritePin(FRAM_CS_GPIO_Port, FRAM_CS_Pin, GPIO_PIN_RESET); } static void fm25cl64_cs_high(void) { /* 等待SPI移位寄存器真正挪完最后一个bit再拉高CS */ while (hspi1.Instance-SR SPI_SR_BSY); HAL_GPIO_WritePin(FRAM_CS_GPIO_Port, FRAM_CS_Pin, GPIO_PIN_SET); } void fm25cl64_write_bytes(uint16_t addr, const uint8_t *data, uint32_t len) { uint8_t wren FM25CL64_WREN; uint8_t header[3] { FM25CL64_WRITE, (uint8_t)(addr 8), (uint8_t)(addr 0xFF) }; /* 1. 写使能 */ fm25cl64_cs_low(); HAL_SPI_Transmit(hspi1, wren, 1, 100); fm25cl64_cs_high(); /* 2. 命令 地址 数据CS全程保持低 */ fm25cl64_cs_low(); HAL_SPI_Transmit(hspi1, header, 3, 100); HAL_SPI_Transmit(hspi1, (uint8_t *)data, len, 100); fm25cl64_cs_high(); } void fm25cl64_read_bytes(uint16_t addr, uint8_t *buf, uint32_t len) { uint8_t header[3] { FM25CL64_READ, (uint8_t)(addr 8), (uint8_t)(addr 0xFF) }; uint8_t dummy 0xFF; fm25cl64_cs_low(); HAL_SPI_Transmit(hspi1, header, 3, 100); while (hspi1.Instance-SR SPI_SR_BSY); for (uint32_t i 0; i len; i) { HAL_SPI_TransmitReceive(hspi1, dummy, buf[i], 1, 100); } fm25cl64_cs_high(); }上板后第一件事不是直接读写业务数据而是先跑一个最朴素的自测往地址0x0000写0xA5然后读回来比对同时打印状态寄存器。如果这一步通了说明SPI底层、GPIO电平、芯片供电、命令格式都没问题。如果这一步就出错不要去查业务逻辑先查波形和配置。3. 踩坑实录传输异常和数据丢失的几大险情3.1 坑一写不进去读回来全是0xFF这是最经典的现象读状态寄存器正常读数据也正常唯独写进去之后读出来还是0xFF好像芯片根本没保存。排查这个问题我建议按下面的顺序来。第一步查WP引脚和状态寄存器的WPEN位。如果WP引脚硬件上没接上拉而状态寄存器bit7被某些代码误置为1芯片就会进入写保护状态。我当时的板子就是WP悬空后来把WP接到3.3V同时确保状态寄存器WPEN位为0写操作立刻恢复了。第二步查WREN有没有真正生效。在每次写命令前读一次状态寄存器确认WEL位是1。如果WEL位一直是0说明WREN发送有问题或者发送WREN和WRITE之间的片选时序不对。FM25CL64的WREN和WRITE之间可以拉高CS但有一个前提WREN发送过程中CS必须完整地拉低再拉高任何一次不完整的CS跳变都会让命令失效。第三步检查写地址有没有超范围。FM25CL64的地址是16位实际上只有A12到A0参与寻址对应0x0000到0x1FFF这8192字节。很多人习惯性地把地址写成0x2000以上芯片并不会报错而是把高位忽略掉实际操作的是回绕后的地址。我一开始就吃过这个亏写的是0x2000结果数据被写到了0x0000读却去读0x2000自然全是0xFF。3.2 坑二SPI模式配错导致错位FM25CL64支持SPI Mode 0和Mode 3对应CPOL为0/CPHA为1时的波形。很多人用CubeMX默认配置可能配成了Mode 1或者Mode 2结果就是数据链路看似通了但读回来的数据整体错位一位或者每个字节都变成某些bit翻转后的值。这个坑比较隐蔽因为不是完全不通而是数据不对。用示波器抓SCK和MOSI能很快发现问题Mode 0时SCK空闲为低数据在第1个边沿上升沿采样Mode 3时SCK空闲为高数据同样在第1个边沿下降沿采样。如果配成了Mode 1或Mode 2SCK空闲电平和采样沿就全反了FM25CL64不认识这种时序。我实际调试时用逻辑分析仪抓过Mode 2下发送0x06MOSI上的位序看着是对的但MISO返回的状态寄存器数据每一位都错位最后规律很明显。所以如果遇到读数据每个字节都有固定偏移的情况先检查CPOL和CPHA而不要怀疑芯片坏了。3.3 坑三SCK频率一高就乱码这个坑是在我调高SPI频率做速度测试时踩到的。把分频从8改成4也就是SPI时钟从15MHz提到30MHz芯片是FM25CL64B理论上是能支持40MHz的但我实测发现读写偶尔会出乱码频率越高越严重。问题不出在芯片而出在信号完整性。我的板子是手工样板SPI信号线飞线比较多走线长度也不等长30MHz的SCK边沿很陡MISO线上的反射和串扰就暴露出来了。解决方案有几个一是把SPI时钟降回15MHz这是最省事的二是缩短飞线让三根信号线尽量等长且靠在一起三是调整GPIO输出速率不要一味追求Very High有时候把速率降到High反而能减少过冲。我最后的做法是软件上保留15MHz作为默认SPI速率把30MHz做成可选模式但默认不启用。说实话对于FRAM这种存储芯片应用层对读写的绝对速度要求没那么高15MHz下读8KB也只要4毫秒出头稳定远比那几毫秒的差异重要。3.4 坑四H7的FIFO和HAL库超时STM32H7的SPI跟F1/G0系列不一样带了FIFOHAL库的实现也更复杂这里有个很典型的坑HAL_SPI_Transmit返回的时候最后一个字节可能还没有完全从移位寄存器里移出去。如果你在这时候立刻拉高CSCS的上升沿很可能发生在最后一个SCK时钟的中间于是最后一个字节就被截断了。我遇到的现象是写一批128字节的数据读回来总是前127字节正确最后一个字节是错的而且错的字节跟原来写的值毫无规律。一开始我以为是芯片问题后来用示波器看CS和SCK才发现CS拉高的时间点跟SCK最后一个沿靠得很近时序余量不够。解决办法就是代码里已经在用的那行在拉高CS之前等待SPI_SR的BSY位清零。这是H7上跑SPI时必须养成的习惯除非你用的是硬件NSS让它自动管理否则手动CS控制的场景一定要等BSY。同样的道理也适用于HAL_SPI_TransmitReceive函数返回不代表数据已经物理传输完毕。另外H7的HAL库还有一类问题如果在中断里调用SPI传输或者两个任务同时操作同一个SPI外设HAL内部的状态机可能卡在HAL_SPI_STATE_BUSY导致后续所有调用都超时返回HAL_ERROR或HAL_TIMEOUT。这个在FreeRTOS环境下尤其容易遇到后面坑五会专门讲。3.5 坑五多任务下CS管理混乱项目里实际跑了FreeRTOS有多个任务会访问FM25CL64一个是界面配置任务用户改参数时写入一个是日志任务周期性地追加运行数据。本来各自跑都没问题但任务一多CS管理就乱了。典型场景是这样的任务A把CS拉低开始发写命令还没发完任务B抢占了CPU把CS拉低重复拉低之后开始读写自己的数据。两个任务的操作交织在一起SPI上的数据流完全错乱FRAM收到的就是一串没头没尾的垃圾指令。更隐蔽的是如果两个任务恰好同时发了WREN操作结果是不可预期的。解决方案分两层。第一层是在SPI外用互斥锁保护整个CS低到CS高的临界区确保一次完整的读写操作期间不会被其他任务打断第二层是把底层SPI访问封装成同一个入口所有任务统一走这个入口而不是各自直接操作HAL_SPI和GPIO。我用的是SemaphoreHandle_t在读写函数开头加锁结尾释放锁。这样虽然多了一点调度开销但安全性提升明显。extern SemaphoreHandle_t spi_mutex; void fm25cl64_write_bytes_safe(uint16_t addr, const uint8_t *data, uint32_t len) { xSemaphoreTake(spi_mutex, portMAX_DELAY); fm25cl64_write_bytes(addr, data, len); xSemaphoreGive(spi_mutex); }4. 性能实测与可靠性建议4.1 读写速度和吞吐实测调试稳定之后我做了几组简单的性能测试。测试条件是SPI时钟15MHz一次连续写8192字节然后再连续读回来。理论计算一下8192字节乘以8bit是65536个SCK周期15MHz下耗时约4.37ms再加上每条命令头部额外3字节的开销实测一次满容量写入大约5ms左右读取也是类似水平。这个速度跟EEPROM对比就很直观了。24C64的Page Write一页最多32字节写完一页要5ms。写满8KB需要256个page光写入时间就要1.28秒还要加上页地址管理、擦除逻辑。FM25CL64写满8KB是几毫秒差距接近三个数量级。如果不在乎寿命可以用循环写读的方式对FRAM做压力测试。我在调试期间跑过连续写读10万次数据全部正确状态寄存器没有任何异常。这个测试一方面验证芯片品质另一方面也验证了SPI驱动在长时间运行下没有积累性错误。4.2 掉电保存和抗干扰设计FRAM虽然是非易失的但掉电瞬间的可靠性还是得靠系统设计来保证。FRAM写入速度很快实际使用的坑不在于保存失败而在于不该写的时候写了。如果系统掉电时MCU的GPIO处于未知状态SPI信号线上可能出现毛刺而CS如果恰好被拉低FRAM可能会执行一次随机写。针对这个场景我在项目里做了三件事。第一MCU电源前端增加复位监控芯片电压跌落时立刻复位MCU让IO口回到确定状态。第二FRAM的CS引脚加一个RC延迟网络CS不会被瞬态毛刺直接拉低。第三如果系统中还有空闲的GPIO可以做一个掉电检测中断检测到供电异常时第一时间把CS拉高阻止后续的随机写入。另一个建议是把FRAM的HOLD引脚利用起来做硬件保护。掉电瞬间把HOLD拉低FRAM的SPI接口就会冻结无视SCK和CS上的一切毛刺。这算是我实测出来的一个比较偏门但有效的防护手段。4.3 我建议的数据存储策略FRAM不需要像Flash那样做磨损均衡寿命足够长但数据完整性仍然值得重视。我现在项目里用了一套简单的数据冗余方案每份业务数据写两份一份主备份一份次备份每条记录后面附一个CRC16校验值。读取时先读主备份校验通过就用校验失败就读次备份校验通过就用次备份两份都不通过就把这块区域恢复成默认值并重新初始化。这套方案在FRAM上实现很简单因为写两次几乎没有额外时间成本但对于异常断电、极端电磁干扰这些场景能多一层保险。我实际遇到过MCU复位之后主备份区域出现一个bit翻转的情况虽然概率极低但有了备份方案设备就没有因为存储数据错误而需要现场调试。5. 排查流程和速查经验5.1 排查流程一步一步定位问题如果FM25CL64在STM32H743上出现读写异常我推荐的排查顺序是这样一个流程。每走一步都能缩小问题范围不要直接去翻业务代码。先用示波器确认CS、SCK、MOSI、MISO四根线的电平状态。上电后CS应该是高电平SCK空闲状态要符合你配置的CPOLMISO如果没有数据输出应该被上拉电阻拉到高电平或悬空但不能有严重的抖动噪声。读一次状态寄存器。如果能够读到状态寄存器的值说明SPI基本链路是通的命令收发没问题。如果读不到先检查引脚配置、GPIO AF映射、CubeMX里的外设分配有没有冲突。写一个固定的测试地址和测试数据比如往0x0000写0xA5然后读回来比对。如果读回来不对重点检查WP引脚、WREN时序、地址范围。检查CS时序。用一个200M采样率的逻辑分析仪抓CS低到高期间的完整波形确认命令、地址、数据期间CS没有抖动确认BSY等待代码生效后CS拉高的位置在最后一个SCK沿之后。如果一切正常但业务数据还是丢考虑是不是软件层面任务并发导致的问题查看有没有两个任务同时访问SPI有没有用互斥锁保护。5.2 避坑速查表最后把这篇文章里提到的坑整理成一张表方便你直接对照检查。现象可能原因解决手段写不入读回全是0xFFWP引脚悬空或WPEN1WP接高电平确保WPEN0写不入读回全是0xFF地址超过0x1FFF确认地址在0x0000-0x1FFF内读回数据整体错位SPI Mode配置错误用Mode 0或Mode 3高频下偶发乱码信号完整性不足降频缩短走线调整GPIO速率最后一个字节丢失CS拉高时SPI还在移位等待SPI_SR_BSY清零后再拉高CS两个任务同时操作SPI无互斥锁保护Semaphore保护整个CS低到高临界区掉电后数据被随机改写掉电瞬间CS毛刺误触发复位IC、CS上拉/RC网络、掉电检测SPI总线操作卡死H7 SPI FIFO/中断状态异常排查中断优先级检查HAL状态机这轮调试下来我最大的体会是MCU性能越强外设越复杂反而越容易在小地方翻车。FM25CL64本身是个很简单的芯片命令集就6条没有状态机缓存没有页边界限制也没有Flash那种擦写调度问题真正坑人的往往不是芯片本身而是MCU外设的配置和工程上的资源协调。H7的SPI比F1复杂了不少HAL库的封装又把这层复杂性包了起来如果你不了解BSY位、FIFO、中断状态这些东西出了问题就很容易抓瞎。最后再分享一个小技巧如果条件允许可以留两个普通GPIO接一个SPI Flash和一个FRAM共用SPI总线。这样芯片驱动调试和可靠性测试可以同时做关键数据放FRAM固件升级的临时缓冲区放Flash。反正STM32H743的SPI资源足够多稍微花点时间把片选和互斥锁设计好后面扩展功能会从容很多。
返回列表