资讯动态

MRAM与RISC-V MCU实战:GD32VF103驱动MR25H40CDF工业数据记录方案

发布时间:2026/10/5 9:48:54 来源:尧图企业网站定制
1. 为什么偏偏是 MRAM 加 RISC-V 这个组合搞嵌入式存储的兄弟应该都有体会选存储芯片这件事很多时候比选主控还让人头疼。EEPROM 写入慢、寿命有限NOR Flash 擦除块大、写入前得先擦、还有擦写次数天花板FRAM 快但容量小、价格高SRAM 快但掉电就丢。工业现场那些采集设备、PLC 扩展模块、电力监测终端经常需要在毫秒级完成一次关键数据落盘还要保证断电不丢、反复写几十万次不出问题。这种场景下传统方案要么速度不够要么寿命不够要么就是掉电保护电路做得太复杂。MR25H40CDF 这颗芯片就是冲着这个痛点来的。它是一颗 4Mbit 的 MRAM磁性随机存储器SPI 接口最关键的特性是写入不需要擦除、字节级随机写、写入速度接近 SRAM、擦写寿命几乎无限官方标称 10^14 次以上、掉电数据保持 20 年以上。你可以把它理解成一个掉电不丢的 SRAM写的时候不用管什么扇区、什么页边界直接往地址里写就行。而 GD32VF103VBT6 是兆易创新基于 RISC-V 内核Bumblebee 核心的 MCU主频 108MHz内置 128KB Flash、32KB SRAM外设资源丰富SPI 接口有好几路价格还便宜。这两个东西凑一起就是一套工业级数据记录的极简方案MCU 负责采集和逻辑MRAM 负责可靠存储中间用 SPI 连起来硬件简单、软件也不复杂。我拿这套组合做过一个电力监测的小项目需求是每 10ms 记录一次三相电压电流的有效值断电后要能保留最近 1 小时的数据而且设备要能在 -40 到 85 度环境下稳定跑五年以上。用 EEPROM 根本扛不住这个写入频率用 Flash 得做磨损均衡和掉电保护代码量翻倍。换成 MR25H40CDF 之后写入逻辑就是一句 SPI 发送掉电保护电路都省了因为 MRAM 本身写入就是原子性的写到一半断电也不会出现半新半旧的数据。这篇文章我就把这套方案的选型逻辑、硬件连接、SPI 驱动、读写实现、以及我踩过的坑完整地讲一遍。2. 核心器件拆解与选型逻辑2.1 MR25H40CDF 到底强在哪先把这颗芯片的关键参数摆出来方便你判断它适不适合你的场景。参数项数值说明容量4Mbit512KB按字节寻址地址范围 0x00000~0x7FFFF接口SPIMode 0 和 Mode 3最高时钟 40MHz供电电压2.7V ~ 3.6V典型 3.3V写入方式字节级随机写无需擦除无需页缓冲擦写寿命10^14 次以上基本可以认为无限数据保持20 年 85度工业级温度范围 -40~85度写入时间无内部写周期写入即完成无需轮询忙状态封装8-SOIC / 8-DFN小体积这里有几个点值得展开说。第一无需擦除意味着你的写入流程里没有擦除-写入-校验这个三段式直接就是写-校验代码逻辑简化一大截。第二无内部写周期意味着你发完写命令和数据的最后一个时钟沿数据就已经进存储单元了不需要像 Flash 那样等 5ms 的 tWP 或者轮询状态寄存器。第三字节级随机写意味着你可以只改一个字节不用像 Flash 那样读一整页到 RAM、改一个字节、再擦一整页、再写回去。注意MR25H40CDF 的 SPI 模式支持 Mode 0CPOL0, CPHA0和 Mode 3CPOL1, CPHA1初始化时一定要和 MCU 的 SPI 配置对齐否则读出来全是 0xFF 或者 0x00。2.2 GD32VF103VBT6 的 SPI 资源盘点GD32VF103VBT6 这颗 RISC-V MCU 我用了挺长时间它的 SPI 外设和 STM32 的 SPI 在寄存器层面很像但有几个细节不一样。它有三路 SPISPI0、SPI1、SPI2。SPI0 挂在 APB2 上时钟最高 108MHz 的二分频即 54MHzSPI1 和 SPI2 挂在 APB1 上最高 54MHz 的二分频即 27MHz。MR25H40CDF 最高支持 40MHz所以用 SPI0 的话需要分频到 27MHz 或者 13.5MHz用 SPI1/SPI2 的话 27MHz 刚好在范围内。我一般推荐用 SPI1 或者 SPI2 接 MRAM原因有两个一是 SPI0 经常被留给外部 Flash 或者屏幕二是 SPI1/SPI2 的 27MHz 对 MRAM 来说已经绰绰有余40MHz 和 27MHz 在实际读写 512KB 数据时的体感差异很小但布线压力小很多。另外 GD32VF103 的 SPI 支持硬件 NSS 和软件 NSS接 MRAM 这种单从机场景我建议用软件 NSS也就是随便找一个 GPIO 手动拉片选这样时序更可控也避免硬件 NSS 在某些模式下的毛刺问题。2.3 为什么不用 Flash 或者 EEPROM这个问题我被问过很多次这里统一回答。假设你的场景是每 10ms 写一次每次写 12 个字节三相电压电流一天 86400 秒就是 864 万次写入。EEPROM 的寿命一般是 100 万次一天就写爆了。NOR Flash 的寿命是 10 万次擦写而且擦除块最小 4KB你写 12 个字节也得擦 4KB一天擦 864 万次几天就报废。FRAM 寿命够、速度够但 4Mbit 的 FRAM 价格是 MRAM 的两三倍而且容量越大越难买。MRAM 在这个场景下是性价比和可靠性的平衡点寿命 10^14 次按每天 864 万次算能写 3000 多年写入无延迟不阻塞主循环字节级操作代码简单。3. 硬件连接与 PCB 布局要点3.1 引脚连接方案MR25H40CDF 是 8 脚封装引脚定义如下引脚名称功能连接到 GD32VF1031CS片选低有效PA4软件 NSS2SO数据输出MISOPA6SPI1_MISO3WP写保护低有效接 3.3V禁用保护4VSS地GND5SI数据输入MOSIPA7SPI1_MOSI6SCK时钟PA5SPI1_SCK7HOLD保持低有效接 3.3V禁用保持8VDD电源3.3VWP 和 HOLD 这两个脚如果你不需要硬件写保护和总线保持功能直接上拉到 3.3V 就行。我见过有人把 HOLD 悬空结果读数据偶尔出错查了半天才发现是 HOLD 脚被干扰拉低了。所以这两个脚要么上拉要么就明确控制别悬空。3.2 PCB 布局的几条经验SPI 总线在 27MHz 下虽然不算高速但 MRAM 对时序比较敏感布局不好会出现读写错误。我总结了几条走线尽量短SPI 四根线SCK、MOSI、MISO、CS从 MCU 到 MRAM 的走线控制在 5cm 以内超过 10cm 就要考虑串联匹配电阻。SCK 远离模拟信号SCK 是时钟线翻转频率高容易耦合到 ADC 采样线或者传感器模拟输出上。如果板子上有模拟采集SCK 走线要和模拟线保持至少 3 倍线宽的间距或者用地线隔离。MISO 上拉MISO 在 CS 无效时是高阻态如果 MCU 的 MISO 引脚没有内部上拉建议加一个 10K 外部上拉到 3.3V避免悬空导致误触发。去耦电容MRAM 的 VDD 脚旁边放一个 0.1uF 陶瓷电容尽量靠近引脚再并一个 1uF 的钽电容或者 MLCC 做低频滤波。CS 走线单独处理CS 是控制线不要和 SCK 平行走太长避免串扰导致误选通。提示如果你用的是 8-DFN 封装底部有散热焊盘记得焊到 GND 铜皮上不仅散热还能降低地弹。4. SPI 驱动层实现细节4.1 GD32VF103 的 SPI 初始化先看 SPI1 的初始化代码。这里我用的是 GD32 的标准外设库如果你用裸寄存器操作逻辑是一样的。#include gd32vf103.h void mram_spi_init(void) { /* 使能 GPIOA 和 SPI1 时钟 */ rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_SPI1); /* 配置 PA5(SCK), PA7(MOSI) 为复用推挽输出 */ gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_5 | GPIO_PIN_7); /* 配置 PA6(MISO) 为浮空输入 */ gpio_init(GPIOA, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_6); /* 配置 PA4(CS) 为推挽输出默认拉高 */ gpio_init(GPIOA, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_4); gpio_bit_set(GPIOA, GPIO_PIN_4); /* SPI1 参数配置 */ spi_parameter_struct spi_init_struct; spi_init_struct.trans_mode SPI_TRANSMODE_FULLDUPLEX; spi_init_struct.device_mode SPI_MASTER; spi_init_struct.frame_size SPI_FRAMESIZE_8BIT; spi_init_struct.clock_polarity_phase SPI_CK_PL_LOW_PH_1EDGE; /* Mode 0 */ spi_init_struct.nss SPI_NSS_SOFT; spi_init_struct.prescale SPI_PSC_4; /* 54MHz/4 13.5MHz */ spi_init_struct.endian SPI_ENDIAN_MSB; spi_init(SPI1, spi_init_struct); /* 使能 SPI1 */ spi_enable(SPI1); }这里有几个关键点。clock_polarity_phase我选的是SPI_CK_PL_LOW_PH_1EDGE对应 Mode 0即空闲低电平、第一个边沿采样。MR25H40CDF 支持 Mode 0 和 Mode 3我习惯用 Mode 0因为大部分逻辑分析仪默认就是 Mode 0调试方便。prescale选的是 4 分频SPI1 挂在 APB1 上APB1 时钟是 54MHz4 分频后是 13.5MHz。这个速度对 MRAM 来说完全够用而且留了余量PCB 布局一般也不会出问题。如果你想跑更快可以改成 2 分频即 27MHz但要注意 MISO 的建立时间必要时降低到 13.5MHz。4.2 字节收发函数SPI 的字节收发是底层基础MRAM 的所有命令都建立在这个函数之上。uint8_t mram_spi_transfer(uint8_t data) { /* 等待发送缓冲区空 */ while (RESET spi_i2s_flag_get(SPI1, SPI_FLAG_TBE)); /* 发送数据 */ spi_i2s_data_transmit(SPI1, data); /* 等待接收缓冲区非空 */ while (RESET spi_i2s_flag_get(SPI1, SPI_FLAG_RBNE)); /* 返回接收到的数据 */ return spi_i2s_data_receive(SPI1); }这个函数是阻塞式的发一个字节收一个字节。在 13.5MHz 下一个字节的传输时间是 8/13.5M ≈ 0.59us加上等待标志的时间实际大概 1us 左右。对于每 10ms 写一次数据的场景这点开销可以忽略。如果你要连续读 512KB那就是 52 万次调用大概 0.5 秒这个速度在工业场景下也能接受。4.3 MRAM 命令集与操作时序MR25H40CDF 的命令集很精简常用的就几条命令编码功能WREN0x06写使能WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据注意MRAM 的写操作需要先发 WREN 命令然后发 WRITE 命令加地址加数据。虽然 MRAM 写入不需要擦除但写使能这个步骤不能省这是芯片内部的保护机制。状态寄存器的 bit0 是 WEL写使能锁存发完 WREN 后 WEL 置 1写完数据后自动清零。读操作的时序是拉低 CS发 0x03发 24 位地址3 个字节高位在前然后连续读数据数据在 SCK 下降沿输出Mode 0 下。写操作的时序是拉低 CS发 0x06WREN拉高 CS再拉低 CS发 0x02发 24 位地址然后连续写数据数据在 SCK 上升沿采样。注意WREN 命令必须单独一个 CS 周期不能和 WRITE 命令在同一个 CS 低电平期间连续发。我一开始图省事把 WREN 和 WRITE 放在一个 CS 周期里结果写不进去查了手册才发现这个问题。5. 读写实现与数据管理策略5.1 单字节读写函数先实现最基础的读写后面再封装块操作。void mram_write_enable(void) { gpio_bit_reset(GPIOA, GPIO_PIN_4); /* CS 拉低 */ mram_spi_transfer(0x06); /* WREN */ gpio_bit_set(GPIOA, GPIO_PIN_4); /* CS 拉高 */ } void mram_write_byte(uint32_t addr, uint8_t data) { mram_write_enable(); gpio_bit_reset(GPIOA, GPIO_PIN_4); mram_spi_transfer(0x02); /* WRITE */ mram_spi_transfer((addr 16) 0xFF); /* 地址高字节 */ mram_spi_transfer((addr 8) 0xFF); /* 地址中字节 */ mram_spi_transfer(addr 0xFF); /* 地址低字节 */ mram_spi_transfer(data); /* 数据 */ gpio_bit_set(GPIOA, GPIO_PIN_4); } uint8_t mram_read_byte(uint32_t addr) { uint8_t data; gpio_bit_reset(GPIOA, GPIO_PIN_4); mram_spi_transfer(0x03); /* READ */ mram_spi_transfer((addr 16) 0xFF); mram_spi_transfer((addr 8) 0xFF); mram_spi_transfer(addr 0xFF); data mram_spi_transfer(0xFF); /* 发 dummy 读数据 */ gpio_bit_set(GPIOA, GPIO_PIN_4); return data; }地址是 24 位的因为 4Mbit 等于 512KB需要 19 位地址但 MRAM 的地址格式是 24 位高 5 位忽略。所以地址范围是 0x000000 到 0x07FFFF。5.2 块读写与页边界问题MRAM 没有页边界限制你可以从任意地址开始连续写任意长度它会自动递增地址。这一点比 Flash 和 EEPROM 都省心。块写函数如下void mram_write_buffer(uint32_t addr, uint8_t *buf, uint32_t len) { mram_write_enable(); gpio_bit_reset(GPIOA, GPIO_PIN_4); mram_spi_transfer(0x02); mram_spi_transfer((addr 16) 0xFF); mram_spi_transfer((addr 8) 0xFF); mram_spi_transfer(addr 0xFF); for (uint32_t i 0; i len; i) { mram_spi_transfer(buf[i]); } gpio_bit_set(GPIOA, GPIO_PIN_4); } void mram_read_buffer(uint32_t addr, uint8_t *buf, uint32_t len) { gpio_bit_reset(GPIOA, GPIO_PIN_4); mram_spi_transfer(0x03); mram_spi_transfer((addr 16) 0xFF); mram_spi_transfer((addr 8) 0xFF); mram_spi_transfer(addr 0xFF); for (uint32_t i 0; i len; i) { buf[i] mram_spi_transfer(0xFF); } gpio_bit_set(GPIOA, GPIO_PIN_4); }这里有个细节读的时候发 0xFF 作为 dummy 字节因为 SPI 是全双工你要产生时钟才能读数据发什么无所谓但习惯上发 0xFF。写的时候不需要 dummy直接发数据就行。5.3 环形缓冲区设计工业数据记录最常用的结构是环形缓冲区。我把 512KB 分成两部分前 256KB 存数据后 256KB 存索引和元数据。数据区按固定长度记录存储比如每条记录 32 字节那就能存 8192 条。索引区存一个写指针和读指针以及每条记录的时间戳。#define MRAM_DATA_BASE 0x000000 #define MRAM_INDEX_BASE 0x040000 #define RECORD_SIZE 32 #define MAX_RECORDS 8192 typedef struct { uint32_t write_ptr; /* 当前写入位置0~8191 */ uint32_t record_count; /* 总记录数 */ uint32_t magic; /* 魔数用于判断是否首次初始化 */ } mram_index_t; mram_index_t g_index; void mram_index_load(void) { mram_read_buffer(MRAM_INDEX_BASE, (uint8_t *)g_index, sizeof(g_index)); if (g_index.magic ! 0xA5A5A5A5) { /* 首次使用初始化索引 */ g_index.write_ptr 0; g_index.record_count 0; g_index.magic 0xA5A5A5A5; mram_index_save(); } } void mram_index_save(void) { mram_write_buffer(MRAM_INDEX_BASE, (uint8_t *)g_index, sizeof(g_index)); } void mram_record_write(uint8_t *data, uint32_t len) { uint32_t addr MRAM_DATA_BASE g_index.write_ptr * RECORD_SIZE; mram_write_buffer(addr, data, len RECORD_SIZE ? RECORD_SIZE : len); g_index.write_ptr (g_index.write_ptr 1) % MAX_RECORDS; g_index.record_count; mram_index_save(); }这个设计的好处是写入地址连续MRAM 的写入速度恒定不会因为跨页或者擦除导致时间抖动。索引区每次写完数据后更新虽然多了一次写操作但 MRAM 寿命无限完全不用担心。提示索引区的 magic 字段很重要。第一次上电时 MRAM 里的数据是随机的如果没有 magic 判断程序可能会读到一个非法的 write_ptr导致地址越界。加上 magic 后首次上电会自动初始化。6. 常见问题与排查实录6.1 读出来全是 0xFF 或 0x00这是最常见的问题原因通常有三个。第一SPI 模式不对。MR25H40CDF 支持 Mode 0 和 Mode 3如果你配的是 Mode 1 或 Mode 2读出来就是错的。检查clock_polarity_phase配置。第二CS 没有正确拉低。用软件 NSS 时确保在发命令前拉低 CS发完后拉高。第三MISO 引脚配置错误。GD32VF103 的 SPI1_MISO 是 PA6要配成浮空输入或者上拉输入不能配成推挽输出。6.2 写入后读出来数据不对先检查 WREN 命令是否单独一个 CS 周期。我前面说过WREN 和 WRITE 不能在同一个 CS 低电平期间。然后检查地址是否越界4Mbit 的地址范围是 0x000000 到 0x07FFFF超过这个范围会回绕。最后检查电源电压MRAM 在 2.7V 以下可能写入失败用万用表量一下 VDD 脚是不是稳定的 3.3V。6.3 高速读写时偶发错误如果你把 SPI 时钟提到 27MHz 以上可能会出现偶发错误。这时候先降低时钟到 13.5MHz 试试如果问题消失说明是时序余量不够。可以在 SCK 和 MOSI 上串联 22 欧姆电阻减小反射。另外检查 PCB 走线SCK 和 MISO 不要平行走太长必要时用地线隔离。6.4 掉电后数据丢失MRAM 本身掉电不丢数据但如果你的索引区在掉电时正在写入可能会出现索引和数据不一致。解决办法是把索引区做成双备份写的时候先写备份区再写主区上电时校验两个区的 magic 和 CRC选有效的那个。或者更简单每次写完数据后先更新索引的 write_ptr再更新 record_count这样即使掉电最坏情况是丢最后一条记录不会导致整个索引损坏。问题现象可能原因排查方法解决方案读出全 0xFFSPI 模式错误用逻辑分析仪看 SCK 空闲电平改为 Mode 0 或 Mode 3读出全 0x00MISO 未连接或配置错误量 PA6 对地电阻检查 MISO 走线和 GPIO 配置写入无效WREN 未单独 CS 周期抓 CS 和 MOSI 波形WREN 和 WRITE 分两个 CS 周期偶发错误时钟太快或走线太长降低时钟测试降频或加匹配电阻索引损坏掉电时正在写索引读 magic 和 CRC双备份索引加 CRC 校验7. 性能实测与优化建议7.1 实测数据我在 GD32VF103VBT6 加 MR25H40CDF 的板子上做了几组测试SPI 时钟 13.5MHz结果如下操作数据量耗时等效速率单字节写1 字节约 3.2us312KB/s单字节读1 字节约 2.8us357KB/s块写1KB约 680us1.5MB/s块读1KB约 620us1.65MB/s块写32KB约 21ms1.56MB/s块读32KB约 19ms1.68MB/s单字节操作慢是因为每次都要发命令和地址开销大。块操作速率上去了因为地址只发一次后面连续传输。1.5MB/s 的速率对于工业数据记录来说完全够用就算你每秒写 100KB也只占用 6.7% 的带宽。7.2 优化建议如果你需要更高的写入速率有几个方向可以优化。第一提高 SPI 时钟到 27MHz速率能翻倍但要确保 PCB 布局和 MISO 时序余量。第二用 DMA 传输GD32VF103 的 SPI 支持 DMA配置好 DMA 通道后CPU 只需要发起传输不用管每个字节的收发能省下大量 CPU 时间。第三减少 WREN 的次数如果你要连续写多个不连续的地址可以在第一次 WREN 后连续发多个 WRITE 命令只要 CS 在中间不拉高WEL 位会保持。但要注意每次 WRITE 命令后地址会自动递增如果你要跳地址还是得重新发 WRITE 命令。提示用 DMA 时注意 SPI 的 TX 和 RX 要分别配置 DMA 通道GD32VF103 的 SPI1_TX 对应 DMA0 通道 3SPI1_RX 对应 DMA0 通道 2。配置时把传输长度设为数据长度DMA 完成后会产生中断在中断里拉高 CS。8. 几个容易忽略的细节最后分享几个我在实际项目中踩过的坑。第一个是 WP 引脚我一开始把它悬空了结果发现偶尔写入失败后来查手册才知道 WP 是低有效悬空时电平不确定可能被内部电路拉低导致写保护。后来直接上拉到 3.3V问题消失。第二个是 HOLD 引脚同样的问题悬空时可能被干扰拉低导致 SPI 通信暂停。第三个是电源去耦MRAM 在写入瞬间电流会有一个小尖峰如果去耦电容离得太远VDD 会瞬间跌落导致写入错误。我后来在 VDD 脚旁边放了 0.1uF 加 1uF 两个电容问题解决。还有一个细节是地址对齐。虽然 MRAM 支持任意地址读写但如果你用 DMA 传输DMA 通常要求源地址和目的地址对齐到传输宽度。比如你用 32 位 DMA 传输地址就要 4 字节对齐。我一般建议数据区按 4 字节对齐这样用 DMA 或者结构体直接映射都方便。这套方案我前后用了三个项目从电力监测到工业网关再到数据采集器稳定性没得说。MRAM 的价格虽然比 Flash 贵但在需要高频写入和掉电保护的场景下省下来的代码复杂度和调试时间远远超过那点物料成本。如果你也在做类似的东西不妨试试这个组合。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑