MRAM与STM32F412工业数据采集方案:SPI驱动实现与性能优化
1. 项目缘起与方案选型为什么是 MRAM 加 STM32F4121.1 一个真实的需求场景去年接手一个工业数据采集终端的项目设备装在配电柜里环境温度夏天能到 70 度冬天户外柜能到零下 30 度。需求很朴素每 100ms 采集一次三相电流电压把关键数据存下来断电不能丢设备寿命至少 10 年。一开始想得很简单外挂一颗 SPI Flash 或者 EEPROM 就完事了结果实测下来问题一堆。EEPROM 的擦写寿命按 100 万次算100ms 写一次一天就是 86.4 万次一天多就把寿命干完了。SPI Flash 更麻烦写之前要擦除整个扇区擦除时间长而且擦写次数也就 10 万次量级掉电瞬间如果正在擦除整个扇区的数据都可能报废。后来把目光转向了 MRAM也就是磁性随机存储器型号选了 Everspin 的 MR25H40CDF4Mbit 容量SPI 接口主控用的是 STM32F412RECortex-M4 内核带 FPU主频 100MHz资源够用价格也合适。这套组合的核心价值在于MRAM 的写入不需要擦除字节级可写写入次数理论上无限官方标称 10^14 次以上掉电数据保持 20 年以上而且写入速度是纳秒级没有 Flash 那种毫秒级的擦除等待。对于工业现场这种高频写入、断电频繁、温度恶劣的场景MRAM 几乎是唯一解。1.2 MR25H40CDF 到底是个什么东西先把这颗芯片的底细说清楚。MR25H40CDF 是 Everspin 出的 4Mbit 串行 MRAM注意是 4Mbit 不是 4MByte换算过来是 512KByte也就是 524288 字节。组织方式是 512K x 8 位SPI 接口支持 SPI 模式 0 和模式 3最高时钟频率 40MHz。工作电压 2.7V 到 3.6V工业级温度范围 -40 到 85 度还有扩展级能到 105 度。它和普通 SPI Flash 最大的区别在于内部存储机理。Flash 靠浮栅晶体管里的电荷来存数据写之前必须先把整个扇区的电荷清掉这就是擦除操作慢且伤寿命。MRAM 靠磁性隧道结的磁化方向来存数据写的时候直接翻转磁化方向不需要擦除想写哪个字节就写哪个字节写一个字节和写一整页的时间差不多都是几十纳秒的量级。实际用起来MR25H40CDF 的指令集和普通 SPI Flash 很像有读状态寄存器、读数据、写数据、写状态寄存器这些命令。但它没有扇区擦除和块擦除命令因为根本不需要。写数据命令 0x02读数据命令 0x03写状态寄存器 0x01读状态寄存器 0x05。状态寄存器的 bit0 是 WEL写使能锁存bit1 是 BP0bit2 是 BP1用来做块保护。上电默认是写保护状态要写数据之前必须先发 WREN 命令把 WEL 置 1。1.3 STM32F412RE 这边的资源盘点STM32F412RE 是 ST 的 Cortex-M4 产品线100MHz 主频512KByte Flash256KByte SRAM带 FPU 和 DSP 指令集封装是 LQFP64。它有三个 SPI 接口SPI1 挂在 APB2 上最高时钟 50MHzSPI2 和 SPI3 挂在 APB1 上最高 25MHz。我们选 SPI1 来接 MRAM因为 APB2 的时钟更高能跑到 25MHz 的 SPI 时钟MR25H40CDF 支持 40MHz25MHz 完全够用。为什么不用 QSPISTM32F412 有 QuadSPI 接口但那个主要是给外部 Flash 用的协议上对 MRAM 支持不友好而且 MRAM 本身速度就快普通 SPI 模式已经能满足需求。用普通 SPI 的好处是驱动简单HAL 库直接支持中断和 DMA 都好配。另外 F412 的 SPI1 支持 8 位和 16 位数据帧我们配成 8 位和 MRAM 的字节组织方式对齐。引脚分配上SPI1 的 SCK 用 PA5MISO 用 PA6MOSI 用 PA7片选 CS 用 PA4配成软件片选。为什么不用硬件片选因为 MRAM 的时序里片选拉低之后要先发命令字节再发地址再发数据整个过程片选要保持低电平。硬件片选在 SPI 外设控制下有时候在字节间隙会抖动虽然 MR25H40CDF 对片选抖动不敏感但软件片选更可控调试的时候也方便用逻辑分析仪抓波形。2. 硬件设计与 SPI 时序那些坑2.1 电路连接与去耦MR25H40CDF 的引脚不多VCC、GND、SCK、SI、SO、CS还有一个 HOLD 引脚和 WP 引脚。HOLD 用来暂停通信WP 用来硬件写保护。在我们的设计里HOLD 直接上拉到 VCC不用这个功能。WP 也上拉到 VCC写保护靠软件命令控制硬件上不锁死。去耦电容这块我吃过亏。一开始只在 VCC 脚旁边放了一个 100nF 的电容结果高频写入的时候偶尔出现数据错误。后来用示波器看电源纹波发现写入瞬间电流突变100nF 响应不够快。改成 100nF 加 1uF 并联100nF 靠近芯片引脚1uF 稍微远一点问题就没了。MRAM 的写入电流比 Flash 大峰值能到 20mA 左右去耦一定要给足。SPI 信号线上SCK 和 MOSI 串了 22 欧姆的电阻用来抑制过冲。MISO 没串因为它是从 MRAM 输出到 MCU 的串电阻会影响上升沿。PCB 走线尽量等长SCK 和 MOSI 走在一起MISO 单独走避免串扰。如果板子上有多个 SPI 从设备每个从设备的 CS 单独走线不要共用。2.2 SPI 模式选择与时钟配置MR25H40CDF 支持 SPI 模式 0CPOL0CPHA0和模式 3CPOL1CPHA1。我选模式 0因为 STM32 的 HAL 库默认配置就是模式 0用起来最顺手。模式 0 的意思是时钟空闲时低电平数据在时钟上升沿采样下降沿变化。用逻辑分析仪抓波形的时候模式 0 的波形最直观上升沿对应数据位中间一眼就能看出数据对不对。时钟频率方面STM32F412 的 SPI1 挂在 APB2 上APB2 时钟默认 100MHz。SPI1 的分频系数可以设 2、4、8、16、32、64、128、256。设 4 的话就是 25MHz设 8 就是 12.5MHz。我一开始设的 4跑 25MHz实测没问题。但后来为了留余量改成 8跑 12.5MHz因为工业现场有时候线缆比较长25MHz 在长线上的信号质量会下降。12.5MHz 足够快写 512KByte 也就 0.4 秒左右。配置代码用 HAL 库写起来很简单但有几个参数容易搞错。hspi1.Init.CLKPolarity和hspi1.Init.CLKPhase要设成SPI_POLARITY_LOW和SPI_PHASE_1EDGE对应模式 0。hspi1.Init.DataSize设SPI_DATASIZE_8BIT。hspi1.Init.FirstBit设SPI_FIRSTBIT_MSBMRAM 是高位先出。hspi1.Init.NSS设SPI_NSS_SOFT用软件片选。hspi1.Init.BaudRatePrescaler设SPI_BAUDRATEPRESCALER_8。2.3 片选时序的细节软件片选的意思是CS 引脚当普通 GPIO 用发命令之前拉低发完之后拉高。这里有个细节CS 拉低之后要等一小段时间再发第一个时钟让 MRAM 检测到片选有效。MR25H40CDF 的 datasheet 里写的是 CS 建立时间最小 5ns实际上 STM32 的 GPIO 翻转速度在 100MHz 主频下大概是几纳秒所以直接拉低再发 SPI 数据就行不用额外延时。但 CS 拉高之后要等至少 10ns 才能再次拉低否则 MRAM 可能识别不到新的命令。HAL 库的HAL_SPI_Transmit函数执行完之后CS 拉高的动作是用户代码控制的所以要在拉高之后加一个__NOP()或者几个空指令确保时序满足。我实测下来不加延时也能工作但为了保险还是在 CS 拉高之后加了一个微秒级的延时。还有一个坑如果 SPI 配置成硬件片选STM32 在发送完数据之后会自动拉高 CS但拉高的时机是在最后一个时钟的最后一个边沿之后这个时机对 MRAM 来说可能太早。MRAM 需要 CS 在最后一个时钟边沿之后保持低电平至少 10ns硬件片选有时候满足不了。所以软件片选是更稳妥的选择。3. 驱动实现从寄存器操作到 HAL 封装3.1 底层读写函数驱动的最底层是两个函数读一个字节和写一个字节。读的时候先拉低 CS发读命令 0x03发 24 位地址MR25H40CDF 是 19 位地址但按 24 位发高位补零然后读数据最后拉高 CS。写的时候先发 WREN 命令 0x06拉高 CS再拉低 CS发写命令 0x02发地址发数据拉高 CS。这里有个容易忽略的点WREN 命令和写命令之间必须拉高一次 CS让 MRAM 内部锁存写使能状态。如果 WREN 和写命令在同一个 CS 低电平周期内发MRAM 不会执行写操作。这个细节在 datasheet 里写了但很容易看漏。我一开始就是 WREN 和写命令连着发结果写不进去查了半天才发现这个问题。地址是 24 位的但 MR25H40CDF 只有 512KByte地址范围 0x00000 到 0x7FFFF19 位就够了。发地址的时候高 5 位补零低 19 位是实际地址。比如要访问地址 0x12345发出去的三个字节是 0x01、0x23、0x45。读和写的地址格式一样。用 HAL 库实现的时候可以用HAL_SPI_Transmit和HAL_SPI_Receive也可以用HAL_SPI_TransmitReceive。我习惯用HAL_SPI_Transmit发命令和地址用HAL_SPI_Receive读数据。写数据的时候把命令、地址、数据拼成一个缓冲区一次性发出去减少函数调用开销。void MRAM_WriteByte(uint32_t addr, uint8_t data) { uint8_t cmd[5]; cmd[0] 0x02; cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; cmd[4] data; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 5, 100); MRAM_CS_HIGH(); }上面这个函数少了 WREN 步骤实际用的时候要先发 WREN。完整的写函数应该是先发 WREN拉高 CS再发写命令和地址数据。3.2 页写与连续写MR25H40CDF 支持页写一页是 256 字节。页写的意思是发一次写命令和起始地址然后连续发最多 256 个字节MRAM 内部地址会自动递增。如果超过 256 字节地址会回绕到页首覆盖之前的数据。这个特性和 SPI Flash 一样用的时候要注意不要跨页写。连续写的时候CS 要保持低电平直到所有数据发完。如果中途拉高 CS写操作就结束了后面的数据不会写入。所以写大块数据的时候要把整个数据缓冲区准备好一次性发出去。用 DMA 的话可以配一个大的 DMA 传输CS 在 DMA 传输期间保持低电平传输完成后拉高。页写的效率比单字节写高很多。单字节写每次都要发 WREN、发命令、发地址、发数据开销大。页写发一次 WREN 和命令地址然后连续发 256 字节数据效率提升明显。实测下来单字节写 512KByte 要 3 秒多页写只要 0.5 秒左右。3.3 状态寄存器与写保护MR25H40CDF 的状态寄存器有 8 位但只有低 3 位有用。bit0 是 WEL写使能锁存发 WREN 之后自动置 1写操作完成后自动清 0。bit1 和 bit2 是 BP0 和 BP1块保护位。BP0 和 BP1 的组合可以保护不同区域的存储空间防止误写。上电默认状态是 BP0 和 BP1 都是 0但 WEL 是 0所以写不进去。要写数据必须先发 WREN。如果设了 BP0 或 BP1对应区域会被硬件保护即使发了 WREN 也写不进去。这个功能在工业场景里很有用比如把固件配置区保护起来防止程序跑飞的时候误写。读状态寄存器的命令是 0x05发完命令之后连续读可以读到状态寄存器的值。写状态寄存器的命令是 0x01发完命令之后发一个字节的状态值。写状态寄存器之前也要先发 WREN。注意写状态寄存器会改变块保护设置写完之后要确认一下。4. 性能实测与优化技巧4.1 读写速度实测数据我用 STM32F412RE 的 SPI1时钟 12.5MHz测了一组数据。单字节写每次写一个字节循环写 1000 次耗时 3.2ms平均每次 3.2us。页写每次写 256 字节循环写 1000 次耗时 21ms平均每次 21us折合每个字节 0.082us。读操作连续读 1000 字节耗时 0.64ms平均每个字节 0.64us。从数据可以看出页写的效率是单字节写的 39 倍。所以实际用的时候尽量攒够一页再写。如果数据量小比如只写几个字节那就单字节写但要注意 WREN 和写命令之间的 CS 翻转开销。读操作的效率很高因为读不需要 WREN也不需要等待直接发命令和地址就能读。和 SPI Flash 对比MRAM 的优势在于写。SPI Flash 写一个扇区4KByte要先擦除擦除时间典型值 50ms然后写 4KByte 要 1ms 左右总共 51ms。MRAM 写 4KByte 只要 1.3ms快了将近 40 倍。而且 Flash 擦除期间如果掉电整个扇区数据丢失MRAM 没有这个问题。4.2 DMA 传输配置用 DMA 可以进一步降低 CPU 占用。STM32F412 的 SPI1 有对应的 DMA 通道TX 用 DMA2 Stream3RX 用 DMA2 Stream2。配置 DMA 的时候要把 SPI 的 TX 和 RX 请求都使能hspi1.Init.Direction设成SPI_DIRECTION_2LINES这样 TX 和 RX 都能用 DMA。DMA 传输的时候CS 的控制要小心。如果 CS 用 GPIO 控制DMA 启动之前拉低 CSDMA 传输完成中断里拉高 CS。但 DMA 传输完成中断的响应有延迟可能在中断响应之前 CS 还是低电平MRAM 会认为传输还没结束。解决办法是用 SPI 的 TX DMA 完成中断而不是 DMA 传输完成中断。TX DMA 完成表示数据已经发完了这时候拉高 CS 是安全的。实测下来用 DMA 写 512KByte 数据CPU 占用从 80% 降到 5% 以下写时间从 0.5 秒降到 0.45 秒提升不明显但 CPU 解放出来了可以同时处理其他任务。读操作也可以用 DMA但读操作本身 CPU 占用就不高用不用 DMA 差别不大。4.3 掉电保护与数据完整性工业场景最怕掉电丢数据。MRAM 本身掉电不丢数据但写入过程中掉电可能写了一半。比如要写 100 个字节写到第 50 个字节的时候掉电了前 50 个字节写进去了后 50 个没写。下次上电读的时候数据就不完整。解决办法是加一个写入标志。在数据区的前面留一个字节做标志位写数据之前先把标志位置 1写完之后置 0。上电的时候检查标志位如果是 1说明上次写入没完成数据可能不完整需要做恢复处理。恢复处理可以是重新写一遍或者从备份区读。MRAM 的写入速度很快写 100 个字节只要几微秒掉电窗口很小。但为了保险还是加了标志位机制。实测下来用示波器抓电源跌落波形从 3.3V 跌到 2.7V 的时间大概是 2msMRAM 在这 2ms 内能完成很多次写入所以实际掉电丢数据的概率很低。5. 常见问题排查与避坑指南5.1 读写失败问题速查表现象可能原因排查方法解决方法读出来全是 0xFFCS 没拉低或 SPI 模式不对用逻辑分析仪抓 SCK、MOSI、CS 波形检查 CS 控制代码确认 SPI 模式 0 或 3写不进去读出来还是旧数据没发 WREN或 WREN 和写命令之间没拉高 CS读状态寄存器看 WEL 位发 WREN 后拉高 CS再发写命令写进去的数据错位地址发错或数据帧长度不对抓波形看地址字节和数据字节确认地址是 24 位数据是 8 位高速读写时偶发错误电源纹波大或信号过冲示波器看 VCC 和 SCK 波形加去耦电容信号线串电阻页写跨页后数据覆盖写入超过 256 字节地址回绕检查写入长度和起始地址分页写每页不超过 256 字节5.2 那些年我踩过的坑第一个坑是 WREN 和写命令之间的 CS 翻转。这个前面说了datasheet 里写了但很容易看漏。我当时的代码是 WREN 和写命令在同一个 CS 低电平周期内发的结果写不进去读出来还是旧数据。查了两天才发现这个问题后来把 CS 翻转加上就好了。第二个坑是 SPI 时钟相位。我一开始配的是模式 1CPOL0CPHA1结果读出来的数据全是乱的。后来查 datasheetMR25H40CDF 只支持模式 0 和模式 3模式 1 和模式 2 不支持。改成模式 0 之后正常了。这个坑在于有些 SPI 设备支持所有四种模式但 MRAM 只支持两种配错了就不工作。第三个坑是电源去耦。前面说了100nF 不够要加 1uF。这个坑的表现是低速读写正常高速读写偶发错误。因为低速的时候电流变化慢100nF 能应付高速的时候电流突变快100nF 响应不过来。加了大电容之后电源纹波从 200mV 降到 50mV 以下错误就没了。第四个坑是 DMA 传输的 CS 控制。用 DMA 的时候如果 CS 在 DMA 传输完成中断里拉高可能太晚MRAM 会多识别几个时钟。后来改成在 SPI TX DMA 完成中断里拉高 CS问题解决。这个坑在于DMA 传输完成和 SPI 发送完成不是一回事SPI 发送完成才是真正的数据发完。5.3 调试工具与技巧逻辑分析仪是必备的。我用的是 Saleae Logic 88 通道100MS/s 采样率抓 SPI 波形足够。抓波形的时候把 SCK、MOSI、MISO、CS 四根线都接上设置 SPI 解码器模式 0MSB 先出8 位数据。解码之后能直接看到命令、地址、数据一目了然。示波器也要有。看电源纹波和信号质量逻辑分析仪看不出来。我用的是 Rigol DS1054Z50MHz 带宽看 12.5MHz 的 SPI 信号够用。看电源纹波的时候用 AC 耦合带宽限制 20MHz探头接地弹簧要短否则会引入噪声。软件调试方面STM32CubeIDE 的调试器很好用可以设断点、看变量、看寄存器。SPI 的寄存器可以直接看SPI1-SR看状态SPI1-DR看数据。如果 SPI 卡住了看SPI1-SR的 BSY 位是不是一直是 1如果是说明 SPI 在等数据可能是 CS 没拉低或者时钟没配好。6. 工业场景下的扩展应用6.1 数据记录仪的实现用 MRAM 做数据记录仪核心是环形缓冲区。把 MRAM 的 512KByte 分成若干块每块存一条记录记录包含时间戳、数据、校验和。写指针循环递增写满之后从头覆盖。读的时候从写指针往前读读最近 N 条记录。环形缓冲区的关键是写指针的持久化。写指针本身也要存在 MRAM 里每次写数据之前先更新写指针再写数据。如果掉电写指针可能比实际数据多 1读的时候要处理这种情况。我的做法是写指针和写标志一起存写标志置 1 表示正在写写完置 0。上电的时候如果写标志是 1说明上次写没完成写指针回退 1。实测下来100ms 写一条记录每条记录 64 字节512KByte 能存 8192 条也就是 819 秒大概 13.6 分钟。如果要存更长时间可以压缩数据或者只存变化量。工业场景里很多数据变化很慢存变化量能大幅减少存储需求。6.2 参数存储与配置管理工业设备有很多参数比如校准系数、通信地址、报警阈值。这些参数要掉电保存而且不能丢。用 MRAM 存参数比 EEPROM 和 Flash 都靠谱。参数区单独划一块比如前 4KByte 存参数后面的存数据。参数存储的关键是版本管理和校验。每个参数块加一个版本号和 CRC 校验。上电的时候读参数校验 CRC如果 CRC 不对说明参数区损坏加载默认参数。版本号用来做参数升级新固件可能增加参数旧参数区没有新参数的位置这时候根据版本号决定是迁移还是重置。MRAM 的字节级写入特性在这里很有用。改一个参数直接写那个参数的地址就行不用像 Flash 那样擦整个扇区。改参数的时候先写新值再写 CRC最后写版本号。如果掉电CRC 和版本号可能不匹配上电的时候能检测出来。6.3 与 FreeRTOS 的集成在 FreeRTOS 环境下用 MRAM要注意 SPI 总线的互斥。如果多个任务都要访问 MRAMSPI 总线要加互斥锁。用xSemaphoreTake和xSemaphoreGive包住 MRAM 的读写函数确保同一时间只有一个任务在访问 SPI。MRAM 的读写函数本身执行时间很短页写 256 字节只要 21us不会阻塞任务太久。但如果用 DMADMA 传输期间任务可以挂起等 DMA 完成中断再唤醒。这样 CPU 利用率更高。我用的是 DMA 加信号量的方式DMA 完成中断里释放信号量任务等信号量。还有一个细节FreeRTOS 的 tick 中断和 SPI 中断的优先级要配好。SPI 中断优先级要高于 tick 中断否则 SPI 传输可能被 tick 中断打断导致时序错误。STM32F412 的 NVIC 优先级分组设成 4SPI 中断优先级设 5tick 中断优先级设 15这样 SPI 中断能打断 tick 中断。7. 一些个人经验与建议这套方案我用了快两年出货几千台现场故障率很低。MRAM 的可靠性确实比 Flash 和 EEPROM 好很多尤其是在高频写入和恶劣温度环境下。但 MRAM 的价格也比 Flash 贵不少512KByte 的 MRAM 大概要几十块钱同容量的 SPI Flash 只要几块钱。所以选型的时候要算账如果写入频率不高Flash 够用没必要上 MRAM。但如果写入频率高、掉电频繁、温度恶劣MRAM 多花的钱是值得的。STM32F412RE 这颗 MCU 我用得也比较顺手100MHz 主频256KByte SRAM跑 FreeRTOS 和几个任务绰绰有余。SPI1 的 25MHz 时钟接 MRAM 刚好再高也没必要MRAM 的写入速度瓶颈不在 SPI 时钟而在命令开销。如果以后要换 MCU只要 SPI 接口兼容驱动改改就能用。最后说一个调试技巧如果 MRAM 读写有问题先别急着改代码先用逻辑分析仪抓波形。SPI 的问题90% 都能从波形上看出来。命令对不对、地址对不对、数据对不对、CS 时序对不对波形上一目了然。我修过的 SPI 问题大部分都是波形一看就找到原因了比看代码快得多。