ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

工业数据存储选型:MRAM替代Flash的嵌入式驱动实践

工业数据存储选型:MRAM替代Flash的嵌入式驱动实践 前阵子做的一套工业现场设备需要高频记录运行数据主控选了 Microchip PIC18F97J94存储介质则换成了 Everspin 的 MR25H40CDF一颗 4Mb 的 SPI 接口 MRAM。之前这块板子用 SPI NOR Flash 存日志几个月就跑出各种诡异问题偶尔丢记录、写入失败、整片数据读出来异常。换到 MRAM 之后读写逻辑简单了一个量级故障也消失了。这篇就把整个项目从选型、硬件连接、驱动实现到可靠性设计的过程写一写给正在做嵌入式数据存储的朋友一个参考。1. 为什么用 MR25H40CDF 而不是 Flash 或 EEPROM工业存储选型的那点事1.1 从一块被“写穿”的 SPI NOR Flash 说起最开始选存储芯片时并没有太上心觉得日志量不大容量用 1MB 或 4MB 的 SPI NOR Flash买个常见型号就行。结果设备在现场连续跑了两三个月陆续有用户反馈设备重启后部分历史记录丢失甚至开机自检报存储器错误。当时排查了很久最终定位到几个根因。第一个根因比较直接NOR Flash 写之前必须先擦除而擦除的最小单位通常是 4KB 或 64KB 的扇区。日志是追加写的如果某个扇区反复写、反复擦寿命消耗非常快。常见 SPI NOR Flash 标称擦写寿命是十万次左右看着不少但工业现场如果每 10 秒写一条日志一天就是 8640 次一个扇区撑不到半个月。就算做磨损均衡也要在固件里维护块映射表麻烦不说掉电期间映射表一旦写坏整个存储分区就废了。第二个根因来自掉电。NOR Flash 写数据时如果突然断电当前扇区可能处于“擦除一半”或“写一半”的中间状态。重新上电后芯片状态机是否还能恢复完全看运气。我们有一台返修机就是拆开看Flash 里某些地址读出来既不是旧数据也不是新数据而是 0x5A5A5A5A 这种乱码典型的半写。第三个根因是写延迟。NOR Flash 的字节写周期通常要几十微秒到几百微秒擦写一个扇区至少几十毫秒。数据记录本身不复杂但为了避开忙等待往往要引入任务调度或延迟代码越写越绕。后来重新选型把目光放到 MRAM 上。MRAM 的写机制和 Flash 完全不同不需要擦除改哪个字节就直接写哪个字节供电掉了数据也不会丢没有“半个扇区”的概念。所以到了第二轮设计直接用 MR25H40CDF 替换掉了原来的 Flash。1.2 MRAM 与 NOR Flash、EEPROM 的横向对比MR25H40CDF 属于 Everspin 的串行 MRAM 系列容量 4Mb也就是 512KBSPI 接口8 脚封装。很多同学一听到“磁随机存储器”就觉得是黑科技其实工程上不需要理解太多物理原理只需要把它理解为“像失效 RAM 一样按字节随机读写但是掉电不丢数据”的芯片就行。为了说明这个问题我把几类常用非易失存储放在一起做了个对比不一定覆盖所有型号但整体趋势是这样的特性MR25H40CDFMRAMSPI NOR FlashSPI EEPROM容量范围常见 256Kb~4Mb也有更大1Mb~512Mb 甚至更大通常 1Kb~1Mb写前是否要擦除不需要必须擦除扇区不需要随机字节写支持直接改写有限制只能把 1 改 0要擦除才能写 1支持擦写寿命理论 10^14 次量级10^4~10^6 次10^6 次左右写周期SPI 时钟级无忙状态页编程几毫秒擦除更久5ms 级别掉电数据保持20 年量级10~20 年100 年以上容量小工作温度工业级选项宽温工业级常见工业级常见单片价格中等低低对于日志记录、故障数据保存、校准参数频繁更新这种场景MRAM 的“无擦除、高耐久、随机写”几乎是量身定做。代价是容量不如 Flash 大、单价高所以在方案里它适合存“频繁写且可靠性要求高”的数据大批量历史文件仍然可以交给大容量 Flash 或 SD 卡。1.3 MR25H40CDF 工作方式和指令概览MR25H40CDF 对外就是一颗标准 SPI Slave一般都支持 SPI Mode 0 和 Mode 3内部地址空间是 0x000000 到 0x07FFFF。要注意这里的单位4Mb 是 4,194,304 位除以 8 是 524,288 字节也就是 512KB所以地址要用 19 位命令后面跟三个字节地址。指令集和串行 Flash 很像这也是工程上容易上手的原因。基本指令就那么几条指令操作码说明WREN0x06写使能锁存WRDI0x04写禁止锁存RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据RDID0x9F读 JEDEC ID需要注意的是MRAM 虽然不需要擦除但写操作前依然要先发 WREN把芯片内部的写使能锁存器置 1。这是很多第一次用的人容易忽略的细节。WREN 命令必须是一个完整的 CS 周期CS 拉低、发 0x06、CS 拉高然后才能发 WRITE 命令。如果偷懒把 WREN 和 WRITE 放在同一个 CS 低电平周期里写操作不会生效。另外MR25H40CDF 写完后没有 Busy 状态不需要轮询状态寄存器等待CS 拉高命令就完成了。这和 Flash 差别很大驱动代码可以写得非常简洁。后面代码部分会具体说明。2. 把 PIC18F97J94 和 MR25H40CDF 连起来硬件设计2.1 PIC18F97J94 的选型与外设分配PIC18F97J94 是 Microchip 的 8 位单片机100 脚封装程序 Flash 空间大外设也比较丰富适合做中小规模工业控制板。选它主要是三方面考虑一是工业级工作范围二是 3.3V 供电和大多数传感器、存储芯片直接电平兼容三是开发工具链成熟用 MPLAB X IDE 加 XC8 编译器嵌入式 C 语言开发效率不低。在这块板子上MR25H40CDF 接在 MSSP1 模块上也就是 SPI1。PIC18F97J94 带两个 MSSP 模块用哪个完全看引脚规划。我的习惯是SPI 时钟和两根数据线尽量选在连续的 IO 上方便布线片选则随便挑一个 GPIO 来做。因为片选用 GPIO 控制比硬件片选更灵活读写时想拉高就拉高想拉低就拉低不用受 MSSP 模块的片选逻辑限制。这里额外说一句选型时的坑8 位 MCU 的 Flash 写擦寿命也不高不要用内部数据区去频繁记录日志。PIC18F97J94 虽然程序空间有 128KB但它的 Flash 也不是为每秒钟写一次设计的拿它当数据记录介质的话很快消耗到寿命极限。外部挂 MRAM 是更稳妥的做法。2.2 引脚连接与上下拉处理MR25H40CDF 是 8 引脚封装除了 VDD 和 GND关键信号是 CS、SCK、SI、SO、WP、HOLD。与 PIC18F97J94 的接法如下MR25H40CDF 引脚PIC18F97J94 引脚说明CS#GPIO 输出片选低电平有效SCKSPI1 SCK时钟主模式输出SI/DISPI1 SDO主出从进MCU 发指令和数据SO/DOSPI1 SDI主进从出MCU 读数据WP#VDD直接接高允许写入HOLD#VDD直接接高不使用暂停功能VDD3.3V按手册加去耦电容GNDGND公共地有两个地方特别提醒。第一SI 和 SO 不要接反。有些工程师习惯了把主机的 SDO 接到从机的 DI结果另一边接成 DO逻辑上没错但 PCB 上稍微绕一下就很容易把两个信号弄混。实测中接反以后读 JEDEC ID 会得到 0x00 或 0xFF很难判断是时序问题还是接线问题排查成本很高。建议打样之前用万用表按网络名一一测过再动烙铁。第二WP# 和 HOLD# 不要悬空。WP# 悬空在某些批次里有内部下拉或弱上拉但波动可能导致误写保护HOLD# 悬空时芯片现场可能突然进入暂停状态SCK 上的脉冲不再被解释成数据会出现偶发读错。这两根脚最简单的做法是直接接到 VDD。如果想让整板在任何异常情况下都不会被软件改掉数据也可以把 WP# 用一个 10k 电阻接地同时在软件里用状态寄存器配合稳妥但灵活性下降。2.3 PCB 布局和现场抗干扰的经验MR25H40CDF 的 SPI 时钟可以跑几十兆赫兹我们实际只跑到几兆赫兹所以 PCB 要求不算苛刻。工业环境里还是建议遵守几个基本原则SPI 的四根线尽量短不要跨太长距离去耦电容放在芯片电源脚附近一般 0.1uF 和 1uF 并联如果信号要经过连接器到另一块板可以在 MOSI、SCK、CS 上各串一个 22 欧姆电阻降低振铃。这块板子最初是手工飞线搭的SCK 线和 SI 线挨得很近跑 8MHz 时钟时读数据偶尔错一个 bit。后来发现是飞线之间耦合噪声导致的。改成 PCB 走线后问题消失。如果你只是做实验验证飞线尽量短优先降低时钟频率写驱动调试时用 1MHz 就够了不需要一上来就跑高频。还有一个很多工程师容易忽略的地方MRAM 的 CS# 在 MCU 复位期间必须保持高电平。PIC18F97J94 复位后 IO 默认是高阻输入如果 CS# 没有外部上拉芯片可能在上电瞬间被拉低收到几根杂散 SCK 脉冲误入某条指令。解决办法就是在 CS# 上加一个 10k 上拉到 VDD或者把 CS 脚配置为带内部上拉的输出并尽早拉高。这个细节看起来小但在工业现场复位瞬间很容易造成不可预期的行为。3. 驱动代码嵌入式 C 语言读写 MR25H40CDF 的完整实现3.1 SPI 初始化用 MCC 还是手写寄存器PIC18F97J94 的 SPI1 初始化我建议优先用 Microchip 的 MPLAB Code Configurator 图形化生成。MCC 里能直接选主模式、SPI 时钟速度、SPI Mode生成代码后就不需要去翻寄存器位定义了。MR25H40CDF 支持 Mode 0 和 Mode 3两者都行我习惯选 Mode 0。MCC 生成的初始化函数大致是这样的void SPI1_Initialize(void) { SPI1_Disable(); SPI1_WriteReg_SSP1STAT 0x40; SPI1_WriteReg_SSP1CON1 0x20; SPI1_WriteReg_SSP1CON1bits.SSPEN 1; }如果不想用 MCC直接手写寄存器也可以。核心是把 MSSP 配成 SPI Master 模式设置空闲时钟极性然后使能模块。具体寄存器位因型号差异略有不同最好对着 PIC18F97J94 数据手册的 MSSP 章节确认一下。实在不放心寄存器配置可以先用 GPIO 模拟 SPI把读写流程跑通再切到硬件 SPI这样问题定位容易很多。SPI 时钟速度不是越高越好。对于这个项目PCB 走线良好时跑 4MHz 完全没压力8MHz 也可以。实验板建议先用 1MHz排除高频干扰因素后再往上提。3.2 WREN / READ / WRITE 基础封装MR25H40CDF 的驱动实现其实很短核心就是几条命令。我习惯先封装一个spi_exchange函数然后在此基础上封装 MRAM 的读、写、状态查询。// 伪代码SPI 收发函数自身是 MCU 硬件 SPI 的封装 uint8_t spi_exchange(uint8_t byte) { SPI1_Exchange8bit(byte); }然后是写使能、读状态寄存器、写一字节、读一字节。#define MRAM_CMD_WREN 0x06 #define MRAM_CMD_WRDI 0x04 #define MRAM_CMD_RDSR 0x05 #define MRAM_CMD_READ 0x03 #define MRAM_CMD_WRITE 0x02 #define MRAM_CMD_RDID 0x9F #define MRAM_CS_LOW() MRAM_CS_SetLow() #define MRAM_CS_HIGH() MRAM_CS_SetHigh() void mram_write_enable(void) { MRAM_CS_LOW(); spi_exchange(MRAM_CMD_WREN); MRAM_CS_HIGH(); } uint8_t mram_read_status(void) { uint8_t status; MRAM_CS_LOW(); spi_exchange(MRAM_CMD_RDSR); status spi_exchange(0x00); MRAM_CS_HIGH(); return status; } void mram_write_byte(uint32_t addr, uint8_t data) { mram_write_enable(); MRAM_CS_LOW(); spi_exchange(MRAM_CMD_WRITE); spi_exchange((addr 16) 0xFF); spi_exchange((addr 8) 0xFF); spi_exchange(addr 0xFF); spi_exchange(data); MRAM_CS_HIGH(); } uint8_t mram_read_byte(uint32_t addr) { uint8_t data; MRAM_CS_LOW(); spi_exchange(MRAM_CMD_READ); spi_exchange((addr 16) 0xFF); spi_exchange((addr 8) 0xFF); spi_exchange(addr 0xFF); data spi_exchange(0x00); MRAM_CS_HIGH(); return data; }这里有一个看起来很啰嗦但非常有用的习惯每次写之前都无条件调用mram_write_enable()。因为 WREN 锁存会在一次成功的写操作之后自动被清除重复调用既不会影响性能又可以避免“上次忘了发 WREN 导致这次写入失败”这类问题。MR25H40CDF 写完后不需要等待这一点和 Flash 完全不同。所以上面的写函数里面没有轮询状态寄存器CS 拉高就完了。如果后续读到错误数据大概率是 SPI 配置或硬件连接问题不是芯片状态问题。3.3 批量读写与 512KB 地址回卷实际项目里不可能一个字节一个字节地读和写日志和参数通常都是一块一块地存取。批量写函数可以这样写void mram_write_buf(uint32_t addr, const uint8_t *buf, uint32_t len) { mram_write_enable(); MRAM_CS_LOW(); spi_exchange(MRAM_CMD_WRITE); spi_exchange((addr 16) 0xFF); spi_exchange((addr 8) 0xFF); spi_exchange(addr 0xFF); for (uint32_t i 0; i len; i) { spi_exchange(buf[i]); } MRAM_CS_HIGH(); }这个函数唯一的坑在于地址回卷。MR25H40CDF 容量是 512KB内部地址从 0x000000 到 0x07FFFF。如果连续发数据地址递增到 0x080000 后会回卷到 0x000000并不是擦除到更大空间。所以写入时如果末尾接近 0x07FFFF必须把一段数据拆成两半前半段写到 0x07FFFF后半段从头开始继续写。更严谨的做法是在写之前做边界判断uint32_t mram_remaining_to_top(uint32_t addr) { return 0x080000 - addr; }然后在调用mram_write_buf之前根据剩余长度决定拆包还是分两次写。这个注意点对数据记录特别重要因为日志区往往就是从高地址往低地址或环形管理一不留神就会覆盖掉开头的参数区。3.4 上电自检读 JEDEC IDMR25H40CDF 支持 RDID 命令也就是 0x9F正常能读回厂商 ID 和器件 ID。上电自检时读一下既能确认 SPI 时序正确也能确认芯片焊接、电源、接线都正常。uint32_t mram_read_jedec_id(void) { uint32_t id 0; MRAM_CS_LOW(); spi_exchange(MRAM_CMD_RDID); id ((uint32_t)spi_exchange(0x00)) 16; id | ((uint32_t)spi_exchange(0x00)) 8; id | (uint32_t)spi_exchange(0x00); MRAM_CS_HIGH(); return id; }读回的值如果全是 0xFF 或者全是 0x00基本上可以断定硬件连接或 SPI 配置有问题。前两方面的经验在调试部分会详细展开。自检通过后再把 MRAM 里的关键参数读出来做 CRC 校验通过后才认为存储系统初始化成功。4. 工业场景的可靠性设计数据存进去不等于万无一失4.1 上电时序、复位与 CS 上拉MR25H40CDF 本身在上电后即可工作不需要漫长的初始化等待。但对整个系统来说MCU 上电到引脚稳定之间必须保证 CS 是高电平。我之前给 CS 脚配了外部 10k 上拉同时把 HOLD 和 WP 都拉到 VDD这样即使 MCU 在复位状态下MRAM 也不会被误触发。还有一个容易被忽视的时序点是 MCU 的片选 GPIO 默认状态。有些引脚在 MCU 复位期间是输出高有些是高阻有些还带弱上拉。上电瞬间如果 MRAM 的 CS 出现低电平脉冲可能被判定为一次命令的开始后续真正的命令时序就乱了。所以即便代码里初始化顺序完全正确也不代表硬件上没有问题。我建议所有 MRAM 相关控制引脚在进入主程序的第一步立即设置为输出并拉高 CS。4.2 日志区格式与 CRC 校验单纯把字节写进 MRAM 只能算是“能存取”离“可靠存取”还差一步。工业日志记录最怕的事情是写了一半掉电重新上电后不知道哪些记录有效哪些记录是残缺的。所以数据格式设计比驱动本身更影响现场可靠性。我采用的做法是把整片 MRAM 分为参数区和日志区。参数区放设备校准参数、序列号、配置字启动时读取并做 CRC 校验日志区放运行记录每条记录固定长度结构大致如下typedef struct { uint16_t magic; // 固定值比如 0xA55A用来快速判断记录是否有效 uint16_t seq; // 自增序号用来识别连续性 uint32_t timestamp; // 时间戳来自 MCU 的 RTC 或外部时间源 uint8_t type; // 记录类型 uint8_t length; // 有效数据长度 uint8_t data[256]; // 实际数据 uint16_t crc; // CRC16覆盖 magic 到 data 的所有字节 } LogRecord;每次写入一条记录时先把整条记录填好再一起写入 MRAM。读的时候先检查 magic再算 CRCCRC 对不上就认为这条记录不完整。根据 seq 是否连续还能判断中间是否丢了记录。这是一个非常简单但很实用的方案。4.3 掉电保护MRAM 不丢数据但你的索引会丢很多人以为芯片掉电不丢数据就万事大吉了。实际上芯片确实不丢数据但固件里的“写指针”如果放在内部 RAM 里掉电一样会丢。如果放在 MRAM 里写指针更新到一半掉电也会出现索引和实际数据不一致的问题。我用的方法是“双份索引加提交标记”。日志区有一个固定位置存放写指针具体包含当前写位置、当前序号、最后一条已提交记录的 CRC。每写完一条日志先更新日志数据最后更新索引区。索引区存两份交替使用每份带 CRC。上电后先读索引 A如果 CRC 不对再读索引 B如果两份都不对就从头扫描日志区找到最后一个 magic 和 CRC 都正确的记录重建索引。MRAM 的随机写特性在这里帮了大忙。如果用 Flash改一个索引字节前要先擦除整个扇区掉电在擦除窗口期几乎必然发生。MRAM 不存在这个问题直接写目标地址即可提交点设计简单得多。4.4 温区、ESD 与长期运行观察工业设备经常要在宽温环境下运行特别是户外机柜白天高温、晚上低温。MR25H40CDF 的工业级型号支持 -40°C 到 105°C 左右PIC18F97J94 也是工业级器件整板注意选择工业级电容和电阻这套组合应付绝大多数场景没有压力。ESD 方面如果 MRAM 和调试接口或者外部排线直接连着建议在 SPI 信号线上加 TVS 管或者在主控和存储之间串电阻。机箱良好接地往往是最有效的防护措施。我们这版板子做了 8kV 接触放电测试没有死机或丢数据稳定性还可以。现场长期运行下来我建议定期做一个存储巡检把关键参数区读出来重新算一遍 CRC和上次保存的校验值比对日志区随机抽样几条记录做完整校验。MRAM 本身几乎不会坏但接口连接器、焊点、电源纹波都可能导致偶发错误巡检能尽早发现而不是等到数据全乱才处理。4.5 实测写性能与耐久性评估在 4MHz SPI 时钟下写 1KB 数据大约需要 2ms这中间还没有擦除等待。如果换上 40MHz 时钟512KB 全部写满也就几十毫秒级别的理论时间。对记录应用来说瓶颈基本只在固件自身的数据组装和串口输出上MRAM 不是瓶颈。耐久性方面MR25H40CDF 号称 10^14 次写实际工程里没机会测到那个数字。我做过一个压力测试用一个 GPIO 翻转触发写日志连续写了几十万条记录每一条都带 CRC读回来全部正确。对于大多数设备的生命周期来说这个耐久度已经可以当“无限”用了。相比之下之前的 NOR Flash 在这种写入频率下早就该换片了。5. 调试阶段踩过的几个坑5.1 读出来全是 0xFF 的排查过程第一次把板子焊好烧了固件满心欢喜地读 MRAM结果读回来的数据全是 0xFF。当时第一反应是 SPI 没通于是把 SCK、MOSI、MISO 都用示波器抓了一遍发现波形都正常MISO 上也有响应。再仔细一看MISO 读回来的每个字节都是 0xFF也就是说芯片根本没把有效数据放到数据总线上。反复查了很久最后发现是 HOLD# 脚悬空导致的。HOLD# 是暂停功能引脚低电平有效悬空时电平不确定芯片偶尔会认为自己被暂停了。SCK 上的脉冲继续跑但内部移位寄存器不工作MISO 就一直保持高电平读出来自然全是 0xFF。把 HOLD# 焊到 VDD 之后问题立刻消失。这个坑提醒我调试 SPI 从设备时除了 SCK、MOSI、MISO、CS 四根线其余控制引脚必须明确接高或接低不能靠内部上拉“赌”电平。5.2 WREN 必须是一个独立的 CS 周期第二个坑出现在写操作上。刚开始写代码时我图省事把 WREN 和 WRITE 放在同一个 CS 低电平周期里MRAM_CS_LOW(); spi_exchange(0x06); // WREN spi_exchange(0x02); // WRITE ... MRAM_CS_HIGH();逻辑上看起来没问题但 MRAM 死活不写入。读了状态寄存器发现 WEL 位始终是 0说明写使能根本没生效。后来查手册才发现WREN 命令本身被芯片解释成“把锁存器置 1”而锁存器状态的更新发生在 CS 上升沿。如果 CS 不拉高锁存器就一直是 0后续 WRITE 命令会被当作非法操作忽略掉。改正后就变成了独立的两个 CS 周期先 WREN CS 拉高再 WRITE CS 拉高。这也是很多串行存储器件共通的时序要求。以后写其他 SPI 芯片的驱动时遇到类似的“写使能不生效”问题第一个该查的就是 CS 时序。5.3 越过 0x07FFFF 后地址回卷把数据写穿第三个坑和硬件无关纯粹是固件逻辑问题。日志区我规划在前 64KB参数区放在最后 32KB按理说互不干扰。结果某次现场测试时设备调试参数突然变成了乱码最开始以为 EEPROM 参数区掉了反复查代码没发现异常。后来用上位机把整片 MRAM 读出来才发现日志区某次批量写入时缓冲区跨越了 0x07FFFF写到末尾后地址自动回卷到 0x000000然后一路继续写把低地址的参数区覆盖了。因为日志区和参数区都在同一颗芯片上这种错误比硬件故障更难查。从此之后我所有批量读写函数都加了边界判断凡是要跨回卷点的写操作一律强制按照“剩余空间”分两次执行。这个经验也分享给用 MRAM、FRAM 这类“无页大小概念”芯片的朋友没有页边界不代表没有地址回卷容量的尽头就是边界。5.4 关于 MRAM 的一些实话MR25H40CDF 并不是在所有场景都比 Flash 好。容量超过 4MB 的数据存储需求MRAM 的性价比就会很难看需要保存海量历史曲线、音频、截图这类大数据还是得用大容量 Flash 加 TF/SD 卡。MRAM 真正擅长的是“频繁写、小容量、高可靠、掉电不丢”的窄而深的领域。用了一段时间后我再也不愿意在工业日志场景里用普通 NOR Flash 做直接存储了至少也会加一层磨损均衡与掉电恢复。而 MR25H40CDF 这种无擦除、高耐久、随机写的特性让固件设计简单太多。个人体会是如果项目里对数据可靠性有硬指标与其花大量精力在 Flash 上做坏块管理、磨损均衡、掉电恢复不如直接换一颗 MRAM省下来的时间足够覆盖芯片差价。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进