ARTICLE DETAIL

资讯详情

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

STM32+SPI MRAM:替代Flash和EEPROM的工业存储方案

STM32+SPI MRAM:替代Flash和EEPROM的工业存储方案 常年跟工业设备打交道的嵌入式工程师应该都有过“存个数据怎么这么难”的感慨。早期的项目里用 EEPROM 存校准参数寿命和速度勉强够用等开始做实时数据记录、故障现场快照甚至频繁更新的运行日志普通 Flash 的擦除机制就让人抓狂先擦后写、块寿命、磨损均衡每一项都要在代码里额外兜底。这次我在一个控制器项目里给 STM32F042C6 外挂了一片 MR25H40CDF来自 Everspin 的 4Mbit SPI MRAM。它最大的特点就是像 RAM 一样写、却掉电不丢数据绕开了 Flash 和 EEPROM 的整套麻烦。这篇文章把从器件选型、硬件接线到 STM32 SPI 读写程序和现场可靠性处理的完整过程整理出来给同样在工业场景里做嵌入式数据存储的朋友一个可以直接抄作业的参考。1. 选型回望MR25H40CDF 靠什么替代 Flash 和 EEPROM1.1 Flash 的擦除与寿命问题MRAM 是怎么绕开的很多同行第一次听到 MRAM 时总会先入为主把它当成“快一点的 Flash”。这个理解不算错但远远低估了它在使用方式上的差别。常规 SPI NOR Flash 写入一个字节之前得先保证目标地址处于擦除态而擦除的最小单位是一个扇区通常几 KB 到几十 KB。这就带来两个麻烦一是擦除操作本身耗时按毫秒甚至几十毫秒算二是频繁修改少量数据时整片 Flash 的寿命会被反复擦写拖累常规 NOR Flash 的擦写寿命普遍在一万到十万次这个量级。EEPROM 虽然可以按字节擦写寿命也到了一百万次的水平但它同样存在写前擦除的内部流程而且容量普遍偏小。工业现场需要的是那种“今天改一个字节、明天改一个字节、改十年都不心疼”的存储介质。MRAM 的工作原理决定了它没有擦除概念它的存储单元是磁隧道结写入过程直接翻转磁化方向本质上是改写而不是擦除后再编程。所以 Everspin 官方给出的寿命参数是写入耐久性超过 10 的 13 次方甚至更高实际项目中根本不用考虑磨损均衡。另一个被低估的点是写入速度。SPI 模式下MR25H40CDF 发完地址和数据字节后数据直接就进去了不需要等待内部页编程或者扇区擦除完成。虽然我仍然会在代码里轮询状态寄存器的 WIP 位但绝大多数情况下轮询第一次读取就已经返回空闲状态。这种“写完即完成”的特性配合掉电不丢失成了我做掉电瞬间数据保存方案的基础。如果换 Flash掉电瞬间那几百微秒根本不够完成一次扇区擦除。1.2 MR25H40CDF 关键参数与命令速查MR25H40CDF 的容量是 4Mbit换算过来是 512KB对于存故障记录、运行日志、工艺参数来说非常舒服。供电电压支持 3.3V 上下 10% 的范围直接和 STM32F042C6 的 3.3V 电源轨对接。工业级温度范围覆盖 -40 到 85 摄氏度在大多数设备现场环境里都够用。这里列一下关键参数方便对照选型参数MR25H40CDF 特性备注容量4Mbit / 512KB组织为 512K x 8bit接口SPI支持 Mode 0 和 Mode 3SCK 最高约 40MHz写入方式字节/连续写无需擦除可跨地址连续写耐久性超过 10 的 13 次方次可视为无需磨损均衡写保护软件状态寄存器 WP 位、硬件 WP# 引脚WP# 接高禁用硬件保护待机功耗低功耗模式可用SLEEP 命令进入低功耗命令集和普通 SPI NOR Flash 非常接近迁移成本很低。我实际用到的就这几条命令opcode功能WREN0x06设置写使能锁存位WRDI0x04清除写使能锁存位RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据最长连续读FSTRD0x0B快速读带一个 dummy 周期WRITE0x02写数据最长连续写RDID0x9F读器件 ID用于自检写操作前必须发 WREN这个习惯要从 STM32 代码的第一版就养好。我第一次调这个片子时光想着地址对不对漏掉了 WREN结果写进去的数据读出来全是 0xFF排查了半天才意识到是写使能没置位。2. 硬件连接与布局STM32F042C6 和 MRAM 的工业级握手2.1 引脚分配与最小电路设计STM32F042C6 是 Cortex-M0 内核的入门级型号片上资源不多但 SPI1 外设是完整的引脚也便宜。默认的 SPI1 引脚是 PA5 作为 SCKPA6 作为 MISOPA7 作为 MOSI。我把 CS 片选放在 PA4软件 GPIO 控制不用硬件 NSS这样最灵活。接线表如下MR25H40CDF 引脚功能接 STM32F042C6说明1 CS#片选PA4软件拉低使能默认拉高2 SCK时钟PA5SPI1_SCK串 33Ω 电阻3 SI数据输入PA7SPI1_MOSI串 33Ω 电阻4 SO数据输出PA6SPI1_MISO串 22Ω 电阻可选5 VSS地GND直接铺铜连接6 VCC电源 3.3V3.3V就近放 0.1uF 和 4.7uF 电容7 HOLD#暂停通信接 10k 上拉到 3.3V不悬空防止干扰误触发8 WP#写保护接 10k 上拉到 3.3V默认不启用硬件写保护HOLD# 和 WP# 这两个引脚很容易被忽略。HOLD# 拉低时会冻结串行通信如果现场有强干扰把它打到低电平后续 SPI 时序全部错乱。WP# 拉低后状态寄存器里的写保护位被锁定WREN 也解不开。这两个脚必须可靠接上拉电阻最好靠近器件引脚而不是远远挂在电源网上。我见过某块试验板上这两个引脚悬空结果用手一摸器件数据就写不进去原理就在这。电源去耦方面工业板上最常见的错误是在 3.3V 到器件之间走过长的走线电容放在电源源头而不是放在器件旁边。MRAM 写入电流虽然不大但 SPI 高速翻转时电源上会有高频纹波。我习惯在每个存储器件旁边放一个 0.1uF 陶瓷电容再在板级电源入口放一个 4.7uF 或 10uF 的钽电容形成两级滤波。STM32F042C6 的 VDD 也要保持相同的去耦策略特别是当它和 MRAM 共用一组电源轨时。2.2 现场抗干扰设计串联电阻、TVS 和地平面处理SPI 看似简单但在工业现场长走线和不干净的电源是两大致命伤。SPI 信号本身是单端、非差分抗干扰能力远不如 CAN 和 RS485所以布局时要注意几点。第一信号线上加串联电阻。SCK、SI 这类从 MCU 输出给 MRAM 的信号在源端串联 22Ω 到 33Ω 电阻能有效抑制信号边沿的过冲和振铃。输出阻抗和走线阻抗不匹配时高速边沿会产生反射串联电阻可以把反射能量吸收掉一部分。MISO 是 MRAM 驱动同样可以串联一个 22Ω 电阻但要放在靠近 MCU 接收端而不是器件端免得把信号边沿拖得太慢。实测中3MHz 到 8MHz 的 SPI 时钟下这种串联电阻几乎看不见时序恶化但波形明显干净。第二如果 SPI 信号要走出 PCB 到外部接口必须加 TVS 管。MRAM 和 MCU 都是 3.3V 器件耐压余量极低。ESD 打到 SI 或者 SCK 引脚上一场静电就能让器件内部损伤。工业上常用 SOT-23 封装的低电容 TVS 阵列比如 3.3V 工作电压的型号把 SCK、SI、SO、CS 四个信号全部钳位到安全范围。注意 TVS 的地脚要直接接机壳地或大面积地平面否则泄放路径太长保护效果大打折扣。第三地平面尽量完整。SPI 走线应该走在同一个参考地平面上避免跨越地平面缝隙。如果 MRAM 和 MCU 分布在两个不同电源区域中间要加零欧电阻或磁珠连接保证信号回流路径连续。高频时钟沿如果跨过地平面断裂处共模辐射和信号质量都会变差这在做 EMC 认证时尤其明显。我做的板子里MRAM 就放在 MCU 旁边走线长度控制在 15mm 以内SPI 时钟跑 12MHz 也稳定。3. 固件实现SPI1 初始化到连续读写全流程3.1 SPI 外设和 GPIO 初始化STM32F042C6 的开发环境我用的是 STM32CubeIDE固件库采用 HAL 库。既然外接 MRAM 走的是板上短走线SPI 轮询模式已经完全够用不需要上 DMA。初始化分为两部分GPIO 复用配置和 SPI 外设配置。/* SPI1 GPIO 配置PA5 SCKPA6 MISOPA7 MOSI */ GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_SPI1_CLK_ENABLE(); /* 复用功能推挽速度 HIGH */ GPIO_InitStruct.Pin GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; GPIO_InitStruct.Alternate GPIO_AF5_SPI1; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); /* CS软件控制普通推挽输出默认高电平 */ GPIO_InitStruct.Pin GPIO_PIN_4; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);SPI 外设配置中注意模式必须和 MR25H40CDF 匹配。这个器件支持 SPI Mode 0 和 Mode 3我习惯用 Mode 0也就是 CPOL0、CPHA0。分频系数先设为 8 分频STM32F042C6 的系统时钟典型值是 48MHz8 分频就是 6MHz 的 SPI 时钟。这个频率对 MRAM 来说绰绰有余同时留足了信号质量裕量。SPI_HandleTypeDef hspi1; void MX_SPI1_Init(void) { 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; HAL_SPI_Init(hspi1); }这里有一个很实在的调试点如果 SPI 跑起来后 MISO 一直读到 0xFF 或者 0x00不要先怀疑 MRAM先拿示波器看 SCK 是否真的有脉冲。很多时候是 GPIO 复用功能没配好或者 Alternate 编号填错。STM32F042 的 SPI1 默认复用编号是 AF5填成其他数字就是黑屏。3.2 WREN、状态寄存器轮询和按地址读写的核心实现MRAM 的基础读写流程和 SPI Flash 非常像。写操作我先写一个 WREN 命令再拉低 CS发送 WRITE 命令和 24 位地址随后连续发送数据字节。整个过程中 CS 必须保持低电平直到所有数据送完。读操作则更简单不需要 WREN直接发 READ 命令加地址。最基础的 SPI 字节收发函数我封装成下面这样static uint8_t mram_spi_xfer(uint8_t byte) { uint8_t rx 0; HAL_SPI_TransmitReceive(hspi1, byte, rx, 1, HAL_MAX_DELAY); return rx; } #define MRAM_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define MRAM_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET)然后是状态寄存器和写使能的处理。uint8_t mram_read_status(void) { uint8_t status; MRAM_CS_LOW(); mram_spi_xfer(0x05); /* RDSR */ status mram_spi_xfer(0x00); MRAM_CS_HIGH(); return status; } void mram_write_enable(void) { MRAM_CS_LOW(); mram_spi_xfer(0x06); /* WREN */ MRAM_CS_HIGH(); } void mram_wait_busy(void) { uint8_t status; do { status mram_read_status(); } while (status 0x01); /* WIP 位 */ }写使能命令之后必须跟一个 CS 上升沿这个沿会把写使能锁存位真正置起来。所以 WREN 和后面的 WRITE 命令要分成两次 CS 操作不能在同一个 CS 低电平期间连续发。如果图省事把 WREN 和 WRITE 放在一段 CS 低电平里状态寄存器的 WEL 位根本不会有效。这也是 SPI 存储类器件普遍的通则。写函数的核心逻辑需要注意地址和长度的边界判断。MR25H40CDF 是 512KB 地址空间24 位地址线覆盖的范围远大于此我仍然做一个显式的范围检查防止指针越界把数据写到老远去。#define MRAM_SIZE (512UL * 1024UL) int mram_write(uint32_t addr, const uint8_t *buf, uint32_t len) { uint32_t i; if (addr len MRAM_SIZE) { return -1; } mram_write_enable(); MRAM_CS_LOW(); mram_spi_xfer(0x02); /* WRITE */ mram_spi_xfer((addr 16) 0xFF); mram_spi_xfer((addr 8) 0xFF); mram_spi_xfer(addr 0xFF); for (i 0; i len; i) { mram_spi_xfer(buf[i]); } MRAM_CS_HIGH(); mram_wait_busy(); return 0; }读函数不需要写使能直接操作void mram_read(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LOW(); mram_spi_xfer(0x03); /* READ */ mram_spi_xfer((addr 16) 0xFF); mram_spi_xfer((addr 8) 0xFF); mram_spi_xfer(addr 0xFF); while (len--) { *buf mram_spi_xfer(0x00); } MRAM_CS_HIGH(); }这里有个 MRAM 和 Flash 明显不同的细节普通 SPI Flash 写入通常有页边界限制例如一次页编程最多写 256 字节要跨页就得把剩下数据拆成下一次编程。MR25H40CDF 没有页缓冲的概念它内部就是一个按地址连续寻址的 RAM 阵列CS 拉低期间你可以一路把任意长度的数据写下去。这个特性在做日志记录时特别舒服不用像写 Flash 那样反复处理跨页逻辑。不过我还是建议单次写入长度不要无限拉长一是 SPI 总线占用时间太久会影响别的任务二是一旦中途出现 CS 毛刺前功尽弃。3.3 快速读模式与写入后的回读校验MR25H40CDF 和多数 SPI 存储器件一样提供了快速读命令 FSTRD操作码 0x0B地址后加一个字节的 dummy 周期。快速读在高频运行时有用它可以给器件更多时间准备数据SCK 时序余量更好。但实际项目里6MHz 到 12MHz 的 SPI 时钟下普通 READ 命令也完全稳定快速读更多是锦上添花。我在这里保留一个封装方便后续把时钟提上去时切换void mram_read_fast(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LOW(); mram_spi_xfer(0x0B); /* FSTRD */ mram_spi_xfer((addr 16) 0xFF); mram_spi_xfer((addr 8) 0xFF); mram_spi_xfer(addr 0xFF); mram_spi_xfer(0x00); /* dummy */ while (len--) { *buf mram_spi_xfer(0x00); } MRAM_CS_HIGH(); }写入后的回读校验是我在工业代码里一直坚持的习惯。MRAM 的写入可靠性虽然远高于 Flash但现场电源毛刺、SPI 线缆干扰、软件时序 bug都可能让写入数据悄悄出错。回读校验不需要把整块数据都读出来比对那样太浪费 CPU。我常用的做法是写完后读回关键位置的前 16 字节做采样比对或者对整段数据做一个 32 位 CRC把 CRC 值和数据一起存进去下次读出来先算一遍。最简单的回读校验函数可以写成这样int mram_write_verify(uint32_t addr, const uint8_t *buf, uint32_t len) { uint8_t rdBuf[16]; uint32_t offset 0; while (offset len) { uint32_t chunk len - offset; if (chunk sizeof(rdBuf)) { chunk sizeof(rdBuf); } mram_read(addr offset, rdBuf, chunk); if (memcmp(rdBuf, buf offset, chunk) ! 0) { return -1; } offset chunk; } return 0; }这个函数的成本很低却能在现场拦截大量诡异问题。我见过一次数据频繁丢失的案例最后确认是电源芯片在设备启动瞬间输出电压掉到 2.5V 以下导致 MRAM 内部逻辑误动作。如果没有回读校验这种问题可能要等客户报故障才会暴露。4. 可靠性实测上电掉电、写保护与波形调试经验4.1 掉电场景下的数据完整性设计工业设备最怕的不是正常运行时的读写而是突然掉电那一刻。掉电瞬间MCU 的供电电压从 3.3V 开始跌落这时候如果正好在写 MRAM可能出现三种情况一是数据线电平已经不可靠写入内容混乱二是 SPI 时钟脉冲突然中断CS 线被毛刺拉高写了半截的数据丢失三是电源电压低于器件的下限工作电压状态机直接进入未定义状态。要解决这个问题先从硬件上做掉电检测。STM32F042C6 自带可编程电压检测器 PVD可以设定一个电压阈值当 VDD 跌到阈值以下时触发中断。我在项目里把阈值设在 2.9V 左右这个电压还高于 MRAM 的最低工作电压 2.7V同时又留出了反应时间。PVD 中断一旦触发程序立刻停止不重要的任务把当前时间、状态、最近几分钟的关键数据打包写入 MRAM整个过程只需要几百微秒MRAM 不擦除的特性在这里体现出巨大优势。软件层面还有一个容易忽略的细节MRAM 数据是在 CS 上升沿被锁存吗不是SPI 数据在 SCK 时钟边沿逐位移入但整个写命令只有在 CS 从低到高的那一刻才真正生效提交。如果掉电瞬间 CS 产生了一个失控抖动等于把一个残缺的写命令提交进去了。所以我在 PCB 布局时CS 线上也串了一个 33Ω 电阻并用 1k 电阻下拉到地。这样即使 MCU 引脚在掉电时短暂变成高阻态CS 也会被下拉到高电平不会因为悬空而产生毛刺。另一个掉电相关的实测教训是不要在写入过程中关闭 SPI 时钟。F042 的系统时钟来源可能是内部 HSI掉电时时钟频率会随着电压变化飘移SPI 时序彻底混乱。我在代码里配合 PVD 中断先停掉无关中断再直接通过 GPIO 操作把 MRAM 的 CS 拉高放弃未完成的写事务然后重新发起一次新的短写入。宁可丢一次旧的写请求也不能让 MRAM 进入未知状态。4.2 现场调试用逻辑分析仪和回读校验定位问题调试 MRAM 和 MCU 的 SPI 通信我最常用的工具是逻辑分析仪。虽然示波器能看到模拟波形但工业现场很多问题出在时序层面逻辑分析仪直接把命令帧解出来效率高得多。拿到板子先不要跑复杂逻辑就做三件事第一用 RDSR 读状态寄存器确认 SPI 链路基本通畅返回的数据至少不是全 0xFF。第二用 RDID 读器件 ID确认 MRAM 厂商和型号和预期一致。第三对某个固定地址反复写一组特征数据再读出来验证数据通路。这三步过了再上应用层逻辑。有一次我调试一块板子发现 MRAM 偶尔读回全 0x00。逻辑分析仪采样显示CS 拉低后SCK 时钟只发了 4 个字节就自动停了后面数据全是垃圾。查代码发现 SPI 的缓冲区配置在 HAL_SPI_TransmitReceive 里用了单字节变量连续传输时每进一个字节就重新调用一次函数调用开销导致 CS 高电平和低电平之间出现了不规则延迟。后来改成整段的长度传输或者用 DMA问题立刻消失。还有一种现场常见故障是 SCK 频率过快导致的采样错误。我把 SPI 时钟从 6MHz 提到 18MHz 测试时MISO 数据在长线上出现明显的建立时间不足。示波器看波形SO 信号的上升沿比 SCK 的采样沿晚了一点点。解决办法很简单要么降频要么用 FSTRD 快速读命令加一个 dummy 周期给器件多留出半个时钟周期准备输出数据。我最终把量产频率定在 12MHz信号余量非常充足。如果读者手头也有类似的 SPI 片外存储需求建议在调试阶段把 SPI 的波特率先压到 1MHz 甚至更低跑通基本读写逻辑再逐步提速。很多人一上来就跑高频波形一团糟反而分不清是代码问题还是时序问题。低时钟下如果还有问题那基本可以确定是硬件连接或者命令格式写错了。最后再补充一个关于 RoHS 焊接的小经验MR25H40CDF 系列的 8 引脚封装比较紧凑手工焊接时容易在引脚间搭锡。焊完建议用放大镜或者显微镜检查引脚根部再用万用表测量 VCC 和 GND 之间有没有短路。我在早期样板阶段就遇到过一次 VCC 和 CS 引脚短路导致的症状是 MRAM 一直选中SPI 数据完全不正常。后来每次焊接完都先做静态电阻检查这个毛病就再也没出现过。
返回列表