
1. 项目缘起与方案选型1.1 为什么要在工业场景里盯上 MRAM 这颗料做嵌入式这行十几年掉电丢数据这个问题几乎每隔一段时间就会跳出来折腾人。早些年用 EEPROM写入速度慢得让人抓狂擦写寿命也就百万次级别频繁记录日志的场合根本扛不住后来换 FRAM速度快、寿命长但容量小、价格高而且供货渠道一直不太稳定。锂电池加 SRAM 的方案也试过体积大、有自放电、低温环境直接趴窝工业现场零下四十度根本没法用。MR25H40CDF 这颗 MRAM 算是这几年比较对胃口的选择。MRAM 的存储机理是靠磁性隧道结的磁化方向来表征 0 和 1不是靠电荷所以它天生具备几个讨喜的特性写入不需要先擦除字节级随机写写入速度可以做到纳秒级擦写寿命理论上无限实测标称都在 10 的 14 次方以上掉电数据不丢不需要后备电池工作温度范围宽工业级能覆盖 -40 到 105 摄氏度。MR25H40CDF 具体是 4Mbit 容量也就是 512K 字节SPI 接口40MHz 时钟3.3V 供电SOP-8 或者 DFN 封装引脚和常见的 SPI Flash、SPI EEPROM 基本兼容硬件上替换起来很省事。我选它主要看中三点一是 SPI 接口通用任何带 SPI 的 MCU 都能挂二是容量 512K 字节对于参数存储、日志缓存、掉电保护这些场景刚好够用三是它不需要擦除动作写一个字节和写一页的时序开销差不多这对频繁小数据写入的场合太友好了。1.2 GD32VF103VBT6 这颗 RISC-V 芯片的定位主控这边用的是 GD32VF103VBT6兆易创新基于 RISC-V 内核的通用 MCU。它用的是 Bumblebee 内核主频 108MHz内置 128K Flash、32K SRAM外设资源相当齐全3 个 USART、2 个 I2C、3 个 SPI、2 个 I2S、USB FS、CAN 2.0B、多路定时器和 ADC。封装是 LQFP100引脚多方便扩展。为什么不用更常见的 STM32一方面是 RISC-V 生态这两年成熟度上来了工具链、调试器、RTOS 支持都跟得上另一方面 GD32VF103 的 SPI 外设配置逻辑和 STM32 的 SPI 非常接近寄存器布局和 HAL 风格也有参考价值迁移成本低。它的 SPI0 挂在 APB2 总线上最高时钟可以到 54MHz驱动 MR25H40CDF 的 40MHz 上限绰绰有余。而且这颗芯片工业级温度范围也是 -40 到 85 摄氏度和 MRAM 搭配在工业环境里比较匹配。1.3 整体方案的设计思路整个方案的核心目标很明确让 GD32VF103VBT6 通过 SPI 总线稳定地读写 MR25H40CDF实现掉电不丢的数据存储。硬件上MRAM 的 SCK、MOSI、MISO 接 MCU 的 SPI0 对应引脚CS 用普通 GPIO 做软件片选这样时序控制更灵活也方便后续挂多个 SPI 从设备。软件上分三层底层是 SPI 收发字节的驱动中间层是 MRAM 的读写命令封装上层是面向应用的数据管理接口。这里有个关键取舍用硬件 SPI 还是软件模拟 SPI。硬件 SPI 速度快、CPU 占用低但片选时序受外设逻辑限制软件 SPI 灵活但速度慢。我最终选硬件 SPI 加软件片选因为 MRAM 的时序要求里片选建立和保持时间有明确窗口软件片选能精确控制 CS 拉低和拉高的时刻避免硬件 NSS 自动管理带来的时序偏差。这个选择在后面调试时证明是对的尤其是高速读写时片选时序不对会直接导致数据错位。2. 硬件连接与底层驱动细节2.1 引脚连接与电路注意事项MR25H40CDF 是标准 8 脚 SPI 器件引脚定义是1 脚 CS、2 脚 SO、3 脚 WP、4 脚 VSS、5 脚 SI、6 脚 SCK、7 脚 HOLD、8 脚 VDD。注意 WP 和 HOLD 这两个脚在普通 SPI Flash 上它们有写保护和暂停功能但 MRAM 上这两个脚的功能定义略有不同具体要查数据手册。实际使用中如果不需要硬件写保护WP 和 HOLD 都拉到 VDD 即可避免悬空导致误触发。和 GD32VF103VBT6 的连接是这样的MRAM 的 SCK 接 MCU 的 PA5SPI0_SCKMOSI 接 PA7SPI0_MOSIMISO 接 PA6SPI0_MISOCS 接 PA4 作为普通 GPIO 输出。VDD 和 VSS 之间要放一个 0.1uF 的陶瓷去耦电容位置尽量靠近 MRAM 的电源脚这个电容不能省MRAM 在写入瞬间的电流波动比普通 Flash 大去耦不好会出现写入失败。PCB 布线这块有几个经验点SPI 的 SCK 是高速信号走线尽量短且远离模拟信号MISO 是 MRAM 输出MCU 输入走线不要和 SCK 长距离平行避免串扰CS 走线要单独走不要和时钟线并排。如果板子上还有其他 SPI 从设备每个设备的 CS 独立控制SCK、MOSI、MISO 可以共用但要注意总线负载挂太多设备时信号完整性会下降。2.2 SPI 外设初始化配置GD32VF103 的 SPI0 初始化我习惯直接操作寄存器因为 HAL 库封装层次多调试时不容易看清时序。核心配置项有这几个时钟极性 CPOL 和时钟相位 CPHA。MR25H40CDF 支持 SPI 模式 0 和模式 3我选模式 0即 CPOL0、CPHA0空闲时 SCK 为低电平数据在 SCK 上升沿采样。这个模式和大多数 SPI 器件兼容调试时用逻辑分析仪抓波形也直观。时钟分频方面SPI0 挂在 APB2 上APB2 时钟配置为 108MHzSPI 的波特率预分频器设成 4 分频得到 27MHz 的 SCK 频率。为什么不直接拉到 40MHz因为 27MHz 已经足够快而且留有余量长走线或者温度变化时信号质量更稳。实测 27MHz 下连续读写 512K 字节耗时大约 200 毫秒对于工业数据记录完全够用。数据帧格式设成 8 位MSB 先行这是 SPI 器件的通用约定。NSS 引脚配置成软件管理因为我们用 PA4 做软件片选硬件 NSS 要禁用否则会干扰。具体寄存器操作是SPI_CTL0 寄存器里设置 MSTMOD1 主模式、PSC 分频、CPOL、CPHA、FF16 设 0 表示 8 位帧、SWNSSEN 使能软件 NSS 管理。SPI_CTL1 里设置 NSSDRV 驱动能力一般默认即可。初始化完成后还要配置 PA4 为推挽输出初始状态拉高保证 CS 空闲时为高电平。这里有个细节GPIO 输出速度要设成 50MHz因为 CS 翻转频率跟 SCK 同步速度设低了会导致 CS 边沿变缓影响时序。2.3 MRAM 读写命令与时序MR25H40CDF 的命令集比较简洁。读数据用 0x03 命令后面跟 3 字节地址然后连续输出数据地址自动递增。写数据用 0x02 命令同样跟 3 字节地址然后连续写入数据。写使能命令是 0x06每次写操作前必须先发写使能这是 MRAM 和 Flash 不同的地方——Flash 是写前擦除MRAM 是写前使能。状态寄存器读命令是 0x05写状态寄存器是 0x01。时序上CS 拉低到第一个 SCK 上升沿之间要有至少 5ns 的建立时间最后一个 SCK 下降沿到 CS 拉高之间要有至少 5ns 的保持时间。27MHz 下 SCK 周期约 37ns这些时间窗口很容易满足但软件片选时要注意在发命令前先拉低 CS发完最后一个字节后再拉高 CS中间不能有额外的 GPIO 操作插入否则会拉长 CS 有效时间虽然 MRAM 对此不敏感但养成好习惯对调试其他 SPI 器件有帮助。写操作还有一个细节写使能命令发出后CS 必须拉高再拉低才能开始写数据也就是说写使能和写数据是两个独立的 SPI 事务。我一开始图省事写使能后不拉高 CS 直接发写命令结果数据写不进去。后来查手册才明白MRAM 内部状态机需要在 CS 上升沿锁存写使能位所以这个拉高动作不能省。3. 软件架构与核心代码实现3.1 分层驱动设计软件分三层来写这样移植和调试都方便。最底层是spi_mram_hw.c只负责 SPI 字节收发和 CS 控制不涉及任何 MRAM 协议。中间层是mram_driver.c实现读、写、写使能、读状态等命令封装。最上层是mram_app.c提供按地址读写任意长度数据的接口以及一些应用层的封装比如参数保存、日志追加。底层字节收发函数长这样uint8_t spi_transfer_byte(uint8_t tx_data) { while(SPI_STAT(SPI0) SPI_STAT_TBE 0); SPI_DATA(SPI0) tx_data; while(SPI_STAT(SPI0) SPI_STAT_RBNE 0); return SPI_DATA(SPI0); }这个函数先等发送缓冲区空写入数据再等接收缓冲区非空读出数据。注意读出的数据是上一次传输时从 MISO 采到的所以全双工通信时发送和接收是同步的。对于只写不读的场景返回值可以忽略但必须把数据读出来否则接收缓冲区溢出会导致后续传输异常。CS 控制用宏定义#define MRAM_CS_LOW() gpio_bit_reset(GPIOA, GPIO_PIN_4) #define MRAM_CS_HIGH() gpio_bit_set(GPIOA, GPIO_PIN_4)用宏而不是函数是为了避免函数调用开销影响 CS 翻转时序。在 27MHz 下一个函数调用的几个时钟周期可能就会让 CS 建立时间变得紧张宏展开后是直接寄存器操作最快。3.2 写使能与写数据流程写一个字节的完整流程是拉低 CS发送 0x06 写使能命令拉高 CS再拉低 CS发送 0x02 写命令发送 3 字节地址发送数据字节拉高 CS。这里两次 CS 拉低拉高是必须的前面说过原因。代码实现void mram_write_byte(uint32_t addr, uint8_t data) { MRAM_CS_LOW(); spi_transfer_byte(MRAM_CMD_WREN); MRAM_CS_HIGH(); MRAM_CS_LOW(); spi_transfer_byte(MRAM_CMD_WRITE); spi_transfer_byte((addr 16) 0xFF); spi_transfer_byte((addr 8) 0xFF); spi_transfer_byte(addr 0xFF); spi_transfer_byte(data); MRAM_CS_HIGH(); }地址是 24 位的因为 512K 字节需要 19 位地址用 3 字节传输兼容性更好也和标准 SPI Flash 的地址格式一致。写完之后可以读状态寄存器确认写入完成但 MRAM 写入是纳秒级完成的实际上不需要等待直接进行下一次操作即可。这一点和 Flash 完全不同Flash 写完后要轮询状态寄存器等内部编程完成MRAM 没这个烦恼。3.3 连续读写与页边界处理MR25H40CDF 支持连续读写地址会自动递增跨页也没有限制。这和 SPI Flash 不同Flash 连续写不能跨页跨页会回卷到页首覆盖数据。MRAM 没有页的概念整个 512K 字节是线性地址空间从 0x00000 到 0x7FFFF连续写可以一直写到末尾。连续写函数void mram_write_buffer(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LOW(); spi_transfer_byte(MRAM_CMD_WREN); MRAM_CS_HIGH(); MRAM_CS_LOW(); spi_transfer_byte(MRAM_CMD_WRITE); spi_transfer_byte((addr 16) 0xFF); spi_transfer_byte((addr 8) 0xFF); spi_transfer_byte(addr 0xFF); for(uint32_t i 0; i len; i) { spi_transfer_byte(buf[i]); } MRAM_CS_HIGH(); }连续读类似只是命令换成 0x03然后循环读入数据。读操作不需要写使能直接发命令即可。这里有个性能优化点如果连续写的数据量很大比如几 K 字节可以在写使能之后不拉高 CS直接发写命令。但前面说过写使能需要 CS 上升沿锁存所以这个优化不成立。实测下来每写一页比如 256 字节做一次写使能比每写一个字节做一次写使能效率高很多。所以上层应用在写大块数据时应该按块调用而不是逐字节调用。3.4 掉电保护与数据校验工业场景最怕的是写入过程中掉电导致数据半新半旧。MRAM 本身写入是原子性的一个字节的写入要么完成要么没完成不会出现中间状态。但如果是多字节写入过程中掉电就可能出现前几个字节写入了、后几个字节没写入的情况。解决办法是在数据区加校验和或者 CRC。我的做法是每个数据块前面加 4 字节的 CRC32写入时先算好 CRC 再写数据读取时先读 CRC 再读数据校验不通过就认为数据无效。这样即使掉电导致数据不完整也能检测出来。CRC32 的计算可以用查表法速度快占用空间小。另外对于关键参数我采用双备份加版本号的方式。两个备份区交替写入每个区带一个递增的版本号读取时选版本号大的且 CRC 正确的那个。这样即使一个区在写入时掉电损坏另一个区还是好的。4. 调试实录与常见问题排查4.1 逻辑分析仪抓波形定位问题调试 SPI 器件逻辑分析仪是必备工具。我用的是一款 8 通道、100MHz 采样率的便携逻辑分析仪接 SCK、MOSI、MISO、CS 四根线采样率设成 50MHz足够看清 27MHz 的 SCK 波形。第一次调试时发现读回来的数据全是 0xFF。抓波形一看CS 拉低后 SCK 没有输出MOSI 也没有变化。检查代码发现 SPI 外设的使能位没置位SPI_CTL0 寄存器的 SPIEN 位忘了设。补上之后波形正常了但读回来的数据还是不对。再抓波形发现 MISO 上的数据在 SCK 上升沿时不稳定有毛刺。把 SCK 频率从 27MHz 降到 13.5MHz数据就对了。这说明 PCB 走线或者 MRAM 的驱动能力在高速下有问题降频是最快的验证方法。后来查出来是 MISO 走线太长而且和 SCK 平行走了一段串扰导致数据眼图变差。重新布线后 27MHz 下稳定工作。这个坑告诉我SPI 虽然简单但高速时信号完整性不能忽视尤其是 MISO 这种从设备驱动的信号驱动能力通常比 MCU 弱。4.2 写使能失效的几种情况写使能命令发了但写不进去我遇到过三种原因。第一种是前面说的 CS 没有拉高再拉低写使能位没锁存。第二种是 WP 引脚悬空或者被拉低硬件写保护生效了。第三种是电源电压偏低MRAM 在 2.7V 以下写入会不可靠读正常但写失败。用万用表量一下 VDD如果低于 3.0V 就要查电源电路了。排查写使能问题有个简单方法发完写使能后读状态寄存器看 WEL 位是否置 1。如果没置 1说明写使能没生效按上面三种原因逐一排查。状态寄存器读命令是 0x05读回来的 bit1 就是 WEL 位。4.3 数据错位的地址计算问题有一次读出来的数据整体偏移了一个字节比如写入的是 0x01 0x02 0x03读出来是 0x00 0x01 0x02。查了半天发现是地址发送时多了一个字节。MRAM 的地址是 3 字节我代码里写成了 4 字节多发了一个 0x00导致 MRAM 把第一个数据字节当成了地址的一部分。这种问题用逻辑分析仪一看便知MOSI 上数一下命令后面的字节数就能发现。地址计算还要注意大小端。MRAM 是 MSB 先行地址的高字节先发。比如地址 0x123456发送顺序是 0x12、0x34、0x56。如果搞反了数据就会写到错误的地址去。我习惯在代码里用移位操作显式提取每个字节避免用指针强转带来的大小端问题。4.4 常见问题速查表现象可能原因排查方法解决办法读回全 0xFFSPI 未使能、CS 未拉低、MISO 断线逻辑分析仪看 SCK 和 MISO检查 SPIEN 位、CS 控制、焊接读回全 0x00MISO 被拉低、MRAM 未供电量 VDD、看 MISO 电平检查电源、更换芯片写入无效写使能未生效、WP 拉低读状态寄存器 WEL 位确保 CS 翻转、WP 拉高数据错位地址字节数错误、大小端反了逻辑分析仪数 MOSI 字节修正地址发送逻辑高速下数据不稳信号完整性差、走线过长降频测试重新布线、加匹配电阻低温下写入失败电源电压不足、时序裕量小低温箱测试、量电压提高电源电压、降频4.5 几个实操心得第一个心得MRAM 的写入速度极快但不要因此就无节制地频繁写入。虽然寿命理论上无限但频繁写入会增加功耗而且如果电源设计不好写入瞬间的电流波动可能影响系统其他部分。我一般会在应用层做缓冲攒够一定数据或者定时再写入而不是每次数据变化都立即写。第二个心得MRAM 的 SPI 接口和 Flash 兼容但命令集不完全一样。比如 Flash 的 0x03 读命令在 MRAM 上也是读但 Flash 的 0x0B 快速读命令 MRAM 不一定支持。移植代码时不要想当然查一下数据手册的命令表确认每个命令都支持。第三个心得调试 SPI 时先降频到 1MHz 以下确认功能正常再逐步提高频率。很多问题在低频下不出现高频下才暴露逐步升频能快速定位是功能问题还是信号问题。我一般从 1MHz 开始然后 5MHz、10MHz、27MHz 逐级测试每级都跑一遍读写校验。第四个心得MRAM 的 CS 建立时间虽然只要 5ns但软件片选时 GPIO 翻转有延迟尤其是开了中断或者跑了 RTOS 的情况下GPIO 操作可能被中断打断。如果对时序要求严格可以在 CS 操作前后关中断或者用硬件 NSS 配合 DMA 来保证时序。我在裸机环境下用软件片选没问题但在 RTOS 下会加临界区保护。5. 性能实测与场景扩展5.1 读写速度实测数据用 27MHz SCK 实测单字节写入含写使能耗时约 2.5 微秒其中写使能命令占 0.6 微秒写命令加地址加数据占 1.9 微秒。连续写入 256 字节耗时约 85 微秒平均每字节 0.33 微秒。连续读取 256 字节耗时约 78 微秒平均每字节 0.3 微秒。写 512K 全片耗时约 180 毫秒读全片约 160 毫秒。这个速度对于工业数据记录来说绰绰有余。比如每秒记录一次传感器数据每次 32 字节写入耗时约 11 微秒占 CPU 时间可以忽略不计。即使每秒记录 1000 次总写入时间也就 11 毫秒CPU 占用率约 1%。5.2 在工业数据记录中的应用工业现场的数据记录器通常要求掉电不丢数据、宽温工作、长期可靠。MRAM 加 GD32VF103 的组合正好满足这些要求。我的做法是把 MRAM 分成几个区参数区存设备配置日志区循环记录运行数据故障区存异常事件。参数区用双备份加 CRC日志区用环形缓冲区加时间戳故障区每条记录带 CRC。环形缓冲区的实现要注意读写指针的原子性。写指针在写入新数据后更新读指针在读取后更新。掉电时如果写指针更新了但数据没写完下次上电读取时会发现 CRC 错误跳过这条记录即可。为了减少这种概率我每次写入前先写数据再更新指针而且指针更新用单字节写保证原子性。5.3 扩展思路多设备共用 SPI 总线如果系统里还有别的 SPI 设备比如显示屏、ADC、无线模块可以共用 SCK、MOSI、MISO每个设备独立 CS。GD32VF103 有 3 个 SPI 外设但也可以用一个 SPI 外设挂多个 CS。共用总线时要注意总线仲裁同一时刻只能有一个 CS 有效。我的做法是在驱动层加一个总线锁每次 SPI 事务前获取锁事务完成后释放锁避免多任务环境下冲突。如果设备多、数据量大可以考虑用 DMA 来搬运 SPI 数据减少 CPU 占用。GD32VF103 的 SPI 支持 DMA 请求配置好 DMA 通道后SPI 收发可以自动进行CPU 只需要处理完成中断。不过 MRAM 的 CS 控制还是需要软件介入DMA 传输前后要手动拉低拉高 CS。5.4 替代方案对比方案写入速度擦写寿命掉电保持温度范围成本适用场景MRAM MR25H40纳秒级无限是-40~105高高频写入、关键数据SPI Flash W25Q64毫秒级10万次是-40~85低大容量、低频写入EEPROM毫秒级100万次是-40~85中参数存储、低频写入FRAM纳秒级无限是-40~85高高频写入、小容量SRAM电池纳秒级无限依赖电池-20~60中临时缓存、有电池维护从表里可以看出MRAM 的优势在于速度、寿命和温度范围的综合平衡。Flash 便宜但写入慢、寿命有限EEPROM 寿命比 Flash 好但速度还是慢FRAM 速度快但容量小、温度范围窄SRAM 加电池有维护成本且低温性能差。工业场景下如果数据写入频繁且不能丢MRAM 是当前比较省心的选择。5.5 后续可以扩展的方向这个方案跑通之后可以往几个方向扩展。一是加文件系统把 MRAM 当成块设备上层用 FatFS 或者 LittleFS 管理方便存储结构化数据。二是加无线传输把记录的数据通过 LoRa 或者 NB-IoT 上传到云端实现远程监控。三是加数据压缩对于重复性高的工业数据用简单的 RLE 或者差分编码压缩后再存储提高有效容量。四是加安全芯片对存储的数据做加密和签名防止篡改。我个人在实际操作中的体会是MRAM 这颗料最大的价值不在于它有多快而在于它让嵌入式工程师不用再为掉电丢数据这件事提心吊胆。以前用 Flash 的时候每次写数据都要考虑擦除、页边界、写入等待代码写得小心翼翼用 MRAM 之后写数据就像写 RAM 一样直接代码简洁了很多调试时间也大幅缩短。当然它的价格比 Flash 高不少所以选型时要根据实际需求权衡如果数据写入频率不高、容量要求大Flash 还是更经济的选择如果写入频繁、数据关键、环境恶劣MRAM 多花的钱会在可靠性和开发效率上省回来。