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 的 PA5(SPI0_SCK),MOSI 接 PA7(SPI0_MOSI),MISO 接 PA6(SPI0_MISO),CS 接 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,即 CPOL=0、CPHA=0,空闲时 SCK 为低电平,数据在 SCK 上升沿采样。这个模式和大多数 SPI 器件兼容,调试时用逻辑分析仪抓波形也直观。
时钟分频方面,SPI0 挂在 APB2 上,APB2 时钟配置为 108MHz,SPI 的波特率预分频器设成 4 分频,得到 27MHz 的 SCK 频率。为什么不直接拉到 40MHz?因为 27MHz 已经足够快,而且留有余量,长走线或者温度变化时信号质量更稳。实测 27MHz 下连续读写 512K 字节,耗时大约 200 毫秒,对于工业数据记录完全够用。
数据帧格式设成 8 位,MSB 先行,这是 SPI 器件的通用约定。NSS 引脚配置成软件管理,因为我们用 PA4 做软件片选,硬件 NSS 要禁用,否则会干扰。具体寄存器操作是:SPI_CTL0 寄存器里设置 MSTMOD=1 主模式、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 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决办法 |
|---|---|---|---|
| 读回全 0xFF | SPI 未使能、CS 未拉低、MISO 断线 | 逻辑分析仪看 SCK 和 MISO | 检查 SPIEN 位、CS 控制、焊接 |
| 读回全 0x00 | MISO 被拉低、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 多花的钱会在可靠性和开发效率上省回来。