尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

嵌入式工业存储:STM32驱动SPI MRAM替换Nor Flash的实践与避坑

嵌入式工业存储:STM32驱动SPI MRAM替换Nor Flash的实践与避坑 去年做一台现场设备的主控升级时被存储方案折腾了快两周。设备要记录运行趋势和故障波形项目原本用的是 SPI Nor Flash 外挂存储跑了一个多月就开始出现整片写不进去、读回来跳变、偶尔读出 0xFF 的现象。后来我把方案换成 Everspin 的 MR25H40CDF 这颗 SPI MRAM配 STM32F469II 主控把工业现场的数据存储和读取整套逻辑重写了一遍。这篇不打算讲太多虚的就聊清楚三件事为什么工业存储我最终选了 MRAMMR25H40CDF 跟 STM32F469II 怎么接线和驱动以及现场跑数据时真正会踩到哪些坑。1. 为什么工业现场我放弃了 Nor Flash改用磁阻式存储先说说原来的故障现象。设备每隔 30 秒记录一组电流、温度和运行状态外挂的 SPI Nor Flash 当作日志盘用。刚开始挺正常大概到两万多次写入之后日志文件开始丢页读出来有乱码最明显的特征是擦写过的块逐渐变成 0xFF 或者卡在某个值。拆下来用编程器读发现是坏块扩张不是软件问题。1.1 Nor Flash 的磨损机制为什么日志这么费 FlashNor Flash 写数据前必须先擦除而擦除以“扇区”为最小单位比如 4KB。也就是说哪怕只改一个字节也需要把整个扇区读出来、擦掉、再写回去。我们 30 秒记录一批Flash 内部却在不停地做整扇区搬移和擦写磨损范围比想象中大得多。标称寿命一般是 10 万次擦写可工业现场还经常叠加高温、电压纹波实际寿命常常到不了标称值。再加上擦写过程中如果突然掉电搬到一半的块会处于“既不是新数据也不是旧数据”的中间态这对故障记录场景是致命的。1.2 工业数据存储的四个硬需求做过几个现场项目之后我总结出工业控制器的数据存储至少要有四个硬需求高频写入不心疼。日志、趋势、波形都可能秒级甚至毫秒级地写。断电瞬间还能存一笔。设备故障往往伴随掉电最后几毫秒的状态必须存得下来。温度范围宽。控制柜里 -20℃ 到 70℃ 很常见部分户外设备还要扛到 -40℃。长时间无人值守。不能指望周期性地重新擦写、格式化或者换存储介质。对比下来你会发现普通的 Nor Flash 在这四点里第一点和第三点就不合格。EEPROM 寿命虽然稍好但容量普遍只有几十 KB写一个波形都费劲。FRAM 寿命很长、速度也快但大容量 SPI 接口的 FRAM 不好买成本也偏高。1.3 MR25H40CDF 是什么为什么恰好合适MR25H40CDF 是 Everspin 的 4Mbit 磁阻式随机存取存储器容量 512KBSPI 接口单电源 3.3VSOIC-8 封装工业级温度范围 -40℃ 到 85℃。它的存储单元是磁隧道结MTJ依靠磁化方向保持数据不是靠电荷所以没有电荷泄漏数据保持时间标称超过 20 年。写入寿命通常标在 10 的 14 次方到 10 的 16 次方量级基本可以当作“无限次写”来用。更关键的是MRAM 的写入机制和 Flash 完全不一样它不需要先擦除直接按字节地址覆盖写入写命令完成后数据立刻生效没有页缓冲、没有块擦除的等待时间。从软件角度来看它更像一颗掉电不丢失数据的 SRAM。1.4 一颗小表的对比特性常见 SPI Nor FlashMR25H40CDF (MRAM)常见 SPI FRAM常见 SPI EEPROM写入方式先擦除再写直接覆盖写直接覆盖写直接覆盖写典型擦写寿命10万次10^14以上10^12以上100万次典型容量1~128MB512KB128KB~2MB2KB~1MB写单个扇区/页速度慢含擦除快无擦除快慢掉电写中断风险高低低低工业温度范围支持支持支持支持MRAM 不是用来替代大容量 Nor Flash 存固件和镜像的它最适合的场景就是小容量、高频次、强掉电要求的数据记录正好是我们这种运行日志应用。2. MR25H40CDF 与 STM32F469II 的接线和 SPI 初始化2.1 为什么控制板上选了 STM32F469IISTM32F469II 属于 STM32F4 系列里的高配型号Cortex-M4F 内核主频 180MHz内部 2MB Flash 和 384KB SRAM还带 TFT 控制器和 SDRAM 接口。做带触摸屏的工业设备 HMI 很顺手。但它内部的 2MB Flash 同样有擦写寿命限制工业日志需要长期高频写把日志放内部 Flash 不合适。所以外扩一颗 MRAM 做“运行数据库”内部 Flash 只放固件和字库这个分工对项目来说非常清晰。2.2 接线表我选了 STM32F469II 的 SPI5 来接 MR25H40CDF片选 CS 用 GPIO 控制而不是 SPI 的硬件 NSS。原因后面讲。具体引脚如下MR25H40CDF 引脚方向连接目标说明CS#输入PF6 (GPIO)低电平选中SI输入PF9 (SPI5_MOSI)主发从收SO输出PF8 (SPI5_MISO)从发主收SCK输入PF7 (SPI5_SCK)时钟配到 22.5MHzWP#输入PE2 (GPIO) 或 VDD写保护低有效HOLD#输入VDD (3.3V)不用时一定要接高VDD电源3.3V就近并 0.1uF去耦电容不能省VSS地GND建议单点接地CS# 和 HOLD# 的处理是我后期重点检查的CS# 通过 10kΩ 上拉到 3.3V防止 MCU 没启动时片选误拉低HOLD# 直接接 VDD绝对不要悬空否则总线传输过程中一旦误触发就会把数据卡在半路。2.3 GPIO 片选和硬件 NSS 的选择很多人习惯把 NSS 引脚配成硬件自动片选省一点代码。但工业项目里我几乎不用硬件 NSS原因有两个一是硬件 NSS 在 SPI 初始化、时钟切换或 DMA 中断时可能产生额外脉冲对 MRAM 这种没有“命令长度自动识别”的器件来说多余的片选翻转很可能被解析成非法命令二是如果以后要在这条 SPI 总线上挂第二颗器件GPIO 片选能更灵活地切分设备。2.4 初始化代码我用 STM32CubeMX 生成工程SPI5 配置成发送/接收主机、8 位数据、MSB 优先SCK 空闲为低、第二个边沿采样也就是 SPI Mode 0。MR25H40CDF 也支持 Mode 3但全板默认 Mode 0 能减少后期排查的变量。// GPIO 初始化CubeMX 生成后的关键部分 GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOF_CLK_ENABLE(); __HAL_RCC_GPIOE_CLK_ENABLE(); // CS: PF6输出推挽初始化为高电平 GPIO_InitStruct.Pin GPIO_PIN_6; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_PULLUP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_VERY_HIGH; HAL_GPIO_Init(GPIOF, GPIO_InitStruct); HAL_GPIO_WritePin(GPIOF, GPIO_PIN_6, GPIO_PIN_SET); // WP#: PE2输出推挽初始化为高电平 GPIO_InitStruct.Pin GPIO_PIN_2; HAL_GPIO_Init(GPIOE, GPIO_InitStruct); HAL_GPIO_WritePin(GPIOE, GPIO_PIN_2, GPIO_PIN_SET);SPI5 的初始化由 CubeMX 完成主要参数如下hspi5.Instance SPI5; hspi5.Init.Mode SPI_MODE_MASTER; hspi5.Init.Direction SPI_DIRECTION_2LINES; hspi5.Init.DataSize SPI_DATASIZE_8BIT; hspi5.Init.CLKPolarity SPI_POLARITY_LOW; hspi5.Init.CLKPhase SPI_PHASE_1EDGE; hspi5.Init.NSS SPI_NSS_SOFT; hspi5.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_4; // APB290MHz 时 22.5MHz hspi5.Init.FirstBit SPI_FIRSTBIT_MSB; HAL_SPI_Init(hspi5);APB2 时钟 90MHz4 分频后 SPI 时钟 22.5MHz。MR25H40CDF 最高支持 40MHz这个频率留了接近一半的余量常温到高温都很稳定没必要硬冲极限速度。3. MR25H40CDF 的读写机制没有擦除直接覆盖3.1 最小命令集MR25H40CDF 的 SPI 命令比 Flash 简单得多日常只用这几个命令操作码说明WREN0x06写使能写数据前必须执行WRDI0x04写禁用读状态寄存器0x05读取状态寄存器写状态寄存器0x01配置保护位读数据0x03读取指定地址字节支持连续读写数据0x02写入指定地址字节支持连续写地址是 19 位范围 0x00000 到 0x7FFFF对应 512KB 空间。3.2 写流程先 WREN再 0x02写数据前必须先发 0x06 使能然后片选拉低发 0x02、三字节地址、数据最后片选拉高。数据在片选拉高的那一刻写入存储阵列不需要额外的擦除命令。void mr25_cs_low(void) { HAL_GPIO_WritePin(GPIOF, GPIO_PIN_6, GPIO_PIN_RESET); } void mr25_cs_high(void) { HAL_GPIO_WritePin(GPIOF, GPIO_PIN_6, GPIO_PIN_SET); } void mr25_write_enable(void) { uint8_t cmd 0x06; mr25_cs_low(); HAL_SPI_Transmit(hspi5, cmd, 1, HAL_MAX_DELAY); mr25_cs_high(); } uint8_t mr25_status(void) { uint8_t cmd 0x05, status 0; mr25_cs_low(); HAL_SPI_Transmit(hspi5, cmd, 1, HAL_MAX_DELAY); HAL_SPI_Receive(hspi5, status, 1, HAL_MAX_DELAY); mr25_cs_high(); return status; } int mr25_write(uint32_t addr, const uint8_t *buf, uint32_t len) { uint8_t header[4]; if (addr 0x7FFFF || (addr len) 0x80000) return -1; mr25_write_enable(); header[0] 0x02; header[1] (addr 16) 0xFF; header[2] (addr 8) 0xFF; header[3] addr 0xFF; mr25_cs_low(); HAL_SPI_Transmit(hspi5, header, 4, HAL_MAX_DELAY); HAL_SPI_Transmit(hspi5, (uint8_t *)buf, len, HAL_MAX_DELAY); mr25_cs_high(); return 0; }每笔写操作都以 CS 从低变高结束这个上升沿就是提交动作。在实际代码里我习惯在写完以后立刻发一条状态寄存器读命令确认 WEL 位被清掉了说明 MRAM 正确接收了这次写命令。这个习惯在排查总线上其他干扰时非常有用。3.3 读流程一个 0x03 搞定读数据就发 0x03 加地址然后连续收字节MRAM 内部地址指针会自动递增。这块芯片没有扇区、页面、块的概念所以跨 256 字节边界、跨 4KB 边界都不需要特殊处理对整个 512KB 空间按逻辑地址直接访问就行。相比 Nor Flash 的“扇区搬移”机制代码简单了不止一个量级。int mr25_read(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t header[4]; if (addr 0x7FFFF || (addr len) 0x80000) return -1; header[0] 0x03; header[1] (addr 16) 0xFF; header[2] (addr 8) 0xFF; header[3] addr 0xFF; mr25_cs_low(); HAL_SPI_Transmit(hspi5, header, 4, HAL_MAX_DELAY); HAL_SPI_Receive(hspi5, buf, len, HAL_MAX_DELAY); mr25_cs_high(); return 0; }3.4 状态寄存器里只需要盯住 WEL状态寄存器里bit0 的 WEL写使能锁存是最常用的写完使能命令后它会置 1完成一次写操作后被清零。其余的位是 WIP 和块保护位块保护位会和 WP# 引脚联动具体布局不同批次的数据手册略有调整使用前建议对着手册核对一遍。WIP 位因为是写入即时完成实际不需要轮询。4. 工业现场的数据可靠性设计4.1 双区轮换和结果校验MRAM 解决了“写到一半坏掉”的问题但数据组织还是得自己设计。我的做法是把 512KB 分成两个逻辑区每个区 256KB头 16 字节是区头包含魔数、写入序号、CRC16。每次选定当前区写一轮数据后在区头里更新序号并同步把“有效区”标志翻转。如果写入过程中掉电上电时读到序号不完整或 CRC 不对就自动回退到上一个有效区。双区轮换配合 MRAM 的无限次写寿命能避免 Flash 时代那种“最后一个扇区反复擦写先坏掉”的问题。4.2 掉电保存PVD 监控和最后一批数据的抢救工业设备最麻烦的是故障掉电。我的方案是使用 STM32F469II 的 PVD 可编程电压检测器设定在 3.0V 左右触发中断在检测到电压跌破阈值后系统还有几十到几百微秒的供电缓冲。这段时间里主控把关键运行状态压缩成一条不到 64 字节的记录直接写入 MRAM。因为 MRAM 写命令是即时生效的不需要等待擦除所以只要 SPI 总线还能跑完这一笔事务数据就能存进去。相比之下同样时间窗口里写 Nor Flash 大概率连擦除都做不完。4.3 CRC 校验覆盖全包对每条记录我都追加 4 字节 CRC32而不是只对区头做校验。这样做一是能发现 SPI 总线上的偶发位错位二是能给量产产线做全地址扫描。每天在设备空闲时我还会做一次后台巡检读把全部记录读出来重新算一遍 CRC有异常就标记并触发重发或隔离。4.4 长稳测试结果我用双区轮换逻辑跑了 100 万次写入循环64 字节记录写完立刻读回比对全部一致。温度试验做的是 -40℃ 保存、85℃ 高温运行各 72 小时数据保持无异常。另外把整板放在变频器柜旁边连续跑了几天没有出现软错误这也是磁阻存储相比靠电荷保持数据的 Flash 的优势之一。5. 实测读写性能、功耗和运行注意点5.1 22.5MHz 下的读写吞吐SPI5 实际波特率 22.5MHz。读 512 字节理论耗时 512×8/22.5M ≈ 182us逻辑分析仪抓下来从拉低 CS 到拉高 CS 是 191us和理论值很接近。写 512 字节因为多了 WREN 命令实测 200us 左右。这个速度在日志记录场景完全够用甚至可以支撑 1kHz 以上波形采样点的批处理。5.2 和 Nor Flash 的耗时对比操作Nor Flash 典型耗时MR25H40CDF擦除一个 4KB 扇区约 50~100ms不需要写 512 字节先擦除再写加起来毫秒级约 200us读 512 字节微秒级但受页读取限制约 191us写入后掉电恢复可能丢页/半页数据已生效这一组数据说明MRAM 在“写”这条路径上把 Flash 的擦除等待整个去掉了。512KB 容量存固件偏小但做运行数据的“内场数据库”绰绰有余。5.3 电流和功耗细节功耗要看具体运行模式。MR25H40CDF 工作电流实测在 22.5MHz 连续读写下大约十几毫安待机时降到微安级对大多数工业控制板的 3.3V 电源没什么压力。需要留意的是上电瞬间和 CS 拉低的瞬间电流会出现一个小尖峰给芯片供电的 0.1uF 去耦电容要尽量靠近 VDD 引脚。我在第一版板上就是因为电容放得远偶尔在高温下出现写数据偶发错误把电容挪近了之后问题消失。6. 这几个坑我替你们踩过了6.1 坑一上电瞬间的 CS 毛刺第一版样机上电MRAM 偶尔出现状态错乱读出来的数据带有不该有的字节。示波器一看是 MCU 的 GPIO 还没完成初始化时PF6 上有一个随机低脉冲等于上电瞬间给 MRAM 发了一个未知命令。解决方法是 CS# 加上拉到 3.3V 的 10kΩ 电阻并且在代码里最先初始化 PF6 为高电平再打开 SPI5 外设时钟。顺序反过来的话毛刺依然存在。6.2 坑二HOLD# 悬空引发数据错位有一段时间数据偶发整体错开一个位查了各种软件都不对。后来发现是 HOLD# 引脚悬空板子上一有继电器吸合电磁干扰灌进这个引脚MRAM 以为进入了 HOLD 状态把时钟边沿丢掉了。解决办法很简单HOLD# 直接接 3.3V。同类问题也会出现在 WP# 引脚上不用硬件写保护的话强烈建议把它固定接高不要悬空。6.3 坑三SPI 工作模式不对读回来满屏是 0MR25H40CDF 支持 Mode 0CPOL0、CPHA0和 Mode 3CPOL1、CPHA1。CubeMX 里如果误配成 Mode 1 或 Mode 2器件也能上电但读出来的数据是稳定的 0非常迷惑。排查方法是用逻辑分析仪同时抓 SCK、MOSI、MISO对比时钟极性和采样沿一眼就能看出来不匹配。我现在把所有 SPI 外设统一固定成 Mode 0 加 MSB first不同器件之间切换时既省事又不容易错。6.4 坑四WP# 跟状态寄存器保护位的组合如果使能了块保护并把 WP# 拉低那么写状态寄存器的命令会被直接忽略。我最初想用块保护功能锁住固件区结果发现代码里mr25_write_enable()之后写状态寄存器怎么都不生效。后来把 WP# 改用 GPIO 控制平时输出高只有在需要临时锁定配置时才把它拉低再配合状态寄存器写入问题就解了。如果不懂这个联动关系很容易误以为是芯片坏了。6.5 坑五中断里跑阻塞式 SPI最早把日志写入直接放在定时器中断里调HAL_SPI_Transmit一跑起来整机中断响应就变差偶发还会卡死。原因很简单阻塞式 SPI 传输在等待时占着 CPU工业现场的高优先级中断会被拖住。后来改成数据先落 RAM 环形缓冲由后台任务用 DMA 方式执行 MRAM 写入写完通过 DMA 完成中断置标志。这样既保证了日志记录不丢也不影响实时控制。说回这个项目MR25H40CDF 不是大容量 Flash 的替代品但如果你要的是高频次、强掉电、长寿命的数据记录它是我试过的最省心的方案。最后分享一个我一直在用的小习惯每次写完数据不着急读回整个包先发一条读状态寄存器命令看看 WEL 有没有被清掉。这条命令开销极小却能验证 CS 时序、SCK 时钟和总线干扰是否正常比整包读回更实用。你要是也在做类似设计把这个动作当成每次写入的收尾能少熬几个夜。
返回列表