1. 为什么工业存储选 MRAM:先看懂场景再选器件
1.1 工业场景里,数据存储的核心矛盾
做工业设备的朋友应该都有体会,工业现场的数据存储要求跟消费电子完全是两回事。消费电子里存的顶多是几张照片、几个视频,丢了重新下载就行;工业设备里存的是故障录波、工艺参数、掉电时刻的上下文,这些东西一旦丢了,轻则重新调试半天,重则整批产品出质量问题还要背锅。我自己做过一个伺服驱动器项目,最头疼的就是掉电保存。系统检测到母线电压跌落只有不到十毫秒,要在这么短的时间里把位置、速度、报警码全部存进去,传统方案根本来不及。用Flash,页擦除加编程要好几毫秒,用EEPROM,虽然能字节写,但写入时延长达三到五毫秒,而且写次数受限制。到最后我才换成MRAM,也就是磁阻式随机存取存储器。
MR25H40CDF 就是 Everspin 推出的一颗 4Mbit 容量的 SPI 接口 MRAM,数据手册里给的关键指标很漂亮:3.3V 供电、支持最高 40MHz 的 SPI 时钟、写寿命 10 的 14 次方以上、数据保持 20 年以上、工作温度覆盖工业级范围。对 STM32F745ZG 这种主频 216MHz 的 Cortex-M7 单片机来说,接这种芯片没有任何压力,SPI 外设直接挂上去,代码也不用写多复杂。相比 Flash,MRAM 最大的优势是写入不需要先擦除,随时想写就写,而且按字节操作。所以我在三个项目里都选了它来解决“断电瞬间写数据”的问题,现在把这套方案完整拆开讲一遍。
1.2 MRAM 与 Flash 的区别:不是替代,是互补
很多人刚接触 MRAM 会问:为什么不直接用一颗 SPI NOR Flash,价格还便宜?这个问题得看应用场景。NOR Flash 的写入模式是先擦后写,擦除按扇区来,单个扇区擦除时间少则几十毫秒,多则几百毫秒,而且擦写寿命普遍在一万到十万次。你在开发板上存个配置文件没问题,但让它在产线上连续工作十年,每分钟记录一次状态,十万次很快就耗光了。MRAM 的写寿命是 10 的 14 次方,几乎可以认为是无限写,掉电也不丢数据。换个角度说,MRAM 更像一颗“断电不丢失的 RAM”,而不是一颗“慢速 Flash”。两者不是替代关系,而是互补关系:大容量的视频、日志固件放 Flash,需要频繁更新、掉电要保真的关键状态变量放 MRAM。
我自己总结了一套选型经验:如果数据量大于 1MB、写入频率低、允许分块处理,老老实实用 Flash;如果数据量在几 KB 到几百 KB、写入频率高、每次写入数据量小、丢一个字节都不行,那就应该上 MRAM。工业控制器里的运行参数、故障记录、标定数据、网络配置,很多都属于后者。MR25H40CDF 是 4Mbit 也就是 512KB,听起来不算大,但用来存几百条故障记录、几十组标定参数,完全够用。用它跟 STM32F745ZG 搭配,本质上就是用 512KB 的“非易失内存”给 MCU 扩充一块随时可写的高可靠存储区。
| 项目 | SPI NOR Flash | SPI EEPROM | MR25H40CDF(MRAM) |
|---|---|---|---|
| 写入粒度 | 页编程,通常 256 字节 | 字节 | 字节 |
| 写入前是否需要擦除 | 必须擦,擦除按扇区 | 无需擦除 | 无需擦除 |
| 典型写入等待 | 页编程数毫秒加入擦除 | 3~5ms | 几乎即时,无需独立编程时间 |
| 擦写寿命 | 1万次~10万次 | 10万~100万次 | 大于 10^14 次 |
| 数据保持 | 通常 10 年 | 10~100 年 | 20 年以上 |
| 成本 | 低 | 中 | 相对较高 |
从这个表能看出来,MRAM 的定位就是“写入频率高、可靠性要求高、断电不能丢”。它贵,但贵在寿命和确定性。
2. 读懂 MR25H40CDF:管脚、指令集与状态机制
2.1 管脚布局与最小连接系统
MR25H40CDF 常见封装是 8 脚 SOIC,宽体封装,引脚并不多。数据手册里的管脚定义很清晰:1 脚是片选 /CS,2 脚是数据输出 DO,3 脚是写保护 WP,4 脚接地,5 脚是数据输入 DI,6 脚是时钟 CLK,7 脚是保持 HOLD,8 脚接电源。这里最容易出问题的是 3 脚 WP 和 7 脚 HOLD,这两个都是低电平有效的控制引脚。如果不用硬件写保护和保持功能,千万不要把它们直接悬空,尤其 HOLD 引脚悬空后受到干扰拉低,SPI 总线直接进入保持状态,读不出来也写不进去,而且代码层面完全查不到原因,只能靠示波器抓波形。
我在实际电路里,WP 直接通过一个 10kΩ 电阻上拉到 VDD,HOLD 同样上拉到 VDD。这两个引脚本身就是高电平有效,上拉后可以保证上电瞬间不会因为 IO 时序问题误动作。片选 /CS 接到 MCU 的任意普通 GPIO 即可,不一定要用硬件 NSS,用软件控制更灵活,读一个字节也能精确控制片选拉低和拉高的时序。
整颗芯片的供电是 3.3V,范围一般允许 3.0V 到 3.6V。STM32F745ZG 的 VDD 也是 3.3V 系统,直接一个电源轨下来就能用,不需要额外做电平转换。如果系统里有 5V 的外设,注意 3.3V 和 5V 两边共用 SPI 总线时有电平匹配问题,MR25H40 的输入脚不一定能承受 5V,稳妥做法是加电平转换或者选择推挽开漏加上拉的方式。
2.2 SPI 指令集与状态寄存器
MR25H40CDF 的指令集非常简单,比 SPI NOR Flash 少很多。核心指令只有读、写、写使能、写禁用、读状态寄存器、写状态寄存器和睡眠模式控制。我平时最常用的就下面这几条:
| 指令 | 命令字 | 功能说明 |
|---|---|---|
| WRITE ENABLE | 0x06 | 设置状态寄存器中的 WEL 位,写指令执行前必须调用 |
| WRITE DISABLE | 0x04 | 清零 WEL 位,禁止写操作 |
| READ STATUS | 0x05 | 读状态寄存器,判断 WEL 和 WIP |
| WRITE STATUS | 0x01 | 写入状态寄存器,配置块保护和写保护 |
| READ DATA | 0x03 | 从指定地址连续读出数据 |
| WRITE DATA | 0x02 | 从指定地址连续写入数据 |
| SLEEP | 0x66 | 进入睡眠模式,降低功耗 |
| WAKEUP | 0x99 | 唤醒芯片,从睡眠模式返回待机 |
状态寄存器里真正需要关心的主要是最低位 WIP(写忙碌标志)和 WEL(写使能锁存位)。WIP 为 1 表示芯片正忙,写指令或状态寄存器操作无法执行,需要等待。WEL 为 1 表示当前允许写。注意 MR25H40 的写操作和普通 SPI EEPROM 类似,直接写之前必须先发 0x06 写使能指令,否则写指令会被忽略。这一点很容易忽略,因为有些 MRAM 控制器会自动处理,但这个系列芯片的数据手册明确要求先 WREN。
上电时序方面,VDD 从 0 上升到稳定电压后,芯片内部自动进入待机状态,默认状态下 WEL 是 0,也就是不允许写。所以我每次初始化驱动时,第一件事是先发 0x99 唤醒指令,确保芯片不在睡眠状态,然后读一下状态寄存器确认应答正常。
3. STM32F745ZG 侧软硬件准备:SPI 资源怎么分配很重要
3.1 系统时钟与 SPI 外设时钟估算
STM32F745ZG 的主频最高能跑 216MHz,芯片搭载 Cortex-M7 内核和双精度 FPU,在工业控制器里属于性能非常充裕的型号。它上面 SPI 外设不少,SPI1 挂在 APB2 总线上,SPI2 和 SPI3 挂在 APB1 总线上。对于 MR25H40CDF 这种最高 40MHz 时钟的从设备,SPI1 和 SPI2 都能满足。如果系统 216MHz 运行时,APB2 通常是 108MHz,APB1 通常是 54MHz,SPI1 的分频可以做到 2 分频得到 54MHz,但超过 MR25H40 的 40MHz 上限,所以实际要用 4 分频得到 27MHz,或者 8 分频得到 13.5MHz。27MHz 已经很快了,512KB 的数据全量读一遍也就 19ms 左右,完全够用。
我推荐把 MRAM 挂到 SPI1 上,因为 SPI1 所在总线频率高,分频后更容易贴近 40MHz 的极限。另外 STM32F745ZG 的 SPI1 可以映射到 PA5、PA6、PA7 这组引脚,方便画 PCB,不跟 JTAG、串口打架。当然如果板子上 SPI1 已经给了别的外设,SPI2 也能用,我后面写的代码只要改一下句柄和相关引脚就能移植。
3.2 硬件接线与 GPIO 分配
以 SPI1 为例,我常用的引脚分配是这样的:
| 信号名 | MCU 引脚 | 说明 |
|---|---|---|
| CLK | PA5 | SPI1_SCK,阻抗匹配串 22Ω 电阻 |
| MISO | PA6 | SPI1_MISO,连接到 MR25H40 的 DO |
| MOSI | PA7 | SPI1_MOSI,连接到 MR25H40 的 DI |
| CS | PA4 | 普通 GPIO 推挽输出,软件控制片选 |
| WP | 上拉 VDD | 禁用硬件写保护 |
| HOLD | 上拉 VDD | 禁用保持功能 |
焊接时要注意 MR25H40 的丝印方向,SOIC-8 有缺口的一端一般对应 1 脚。我用立创的封装库比较多,但它给的封装和 Everspin 数据手册引脚定义不完全一致,画原理图的时候一定要逐个核对,别想当然地认为拼出来的封装顺序都一模一样。上电后用示波器探 CLK 脚,如果能看到正常的 SPI 时钟,说明 MCU 侧已经工作,接下来再用驱动读状态寄存器验证通信链路。
3.3 HAL 库 SPI 初始化代码
工程我基于 STM32CubeMX 生成,MCU 选 STM32F745ZG,SPI1 配置为主模式、8 位数据、MSB 先发、时钟极性 CPOL 为低、时钟相位 CPHA 为 1 边沿采样。MR25H40 在 Mode 0 和 Mode 3 下都能工作,我用的是 Mode 0,也就是 CPOL=0、CPHA=0。初始化代码如下:
void mram_spi_init(void) { GPIO_InitTypeDef gpio = {0}; SPI_InitTypeDef spi = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_SPI1_CLK_ENABLE(); gpio.Pin = GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7; gpio.Mode = GPIO_MODE_AF_PP; gpio.Pull = GPIO_NOPULL; gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH; gpio.Alternate = GPIO_AF5_SPI1; HAL_GPIO_Init(GPIOA, &gpio); gpio.Pin = GPIO_PIN_4; gpio.Mode = GPIO_MODE_OUTPUT_PP; gpio.Pull = GPIO_PULLUP; gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH; gpio.Alternate = 0; HAL_GPIO_Init(GPIOA, &gpio); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); spi.Instance = SPI1; spi.Init.Mode = SPI_MODE_MASTER; spi.Init.Direction = SPI_DIRECTION_2LINES; spi.Init.DataSize = SPI_DATASIZE_8BIT; spi.Init.CLKPhase = SPI_PHASE_1EDGE; spi.Init.CLKPolarity = SPI_POLARITY_LOW; spi.Init.NSS = SPI_NSS_SOFT; spi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; spi.Init.FirstBit = SPI_FIRSTBIT_MSB; spi.Init.TIMode = SPI_TIMODE_DISABLE; spi.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; spi.Init.CRCPolynomial = 0; HAL_SPI_Init(&spi); }这里有个细节值得说:SPI 的时钟极性、相位一定要跟从设备对上,MR25H40 数据手册里有时序图,SPI Mode 0 下数据在时钟上升沿采样,Mode 3 下也一样能工作,但如果你前后两个项目混用,底层代码里改了 CPOL 和 CPHA,容易把自己坑了。最好固定一种模式,所有 SPI 从设备都用同一种。
4. 驱动代码实现:从字节读写到连续存储
4.1 最低层字节操作与写使能机制
MRAM 的驱动核心其实就几个函数:发一个字节、拉低片选发指令、等待不忙、写使能、写数据、读数据。下面是我在一套量产代码里裁剪出来的精简版本,先看底层:
#define MRAM_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define MRAM_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) static uint8_t mram_transfer_byte(uint8_t byte) { uint8_t rx = 0xff; HAL_SPI_TransmitReceive(&hspi1, &byte, &rx, 1, 10); return rx; } static void mram_wait_not_busy(void) { uint8_t sr = 0; do { MRAM_CS_LOW(); mram_transfer_byte(0x05); sr = mram_transfer_byte(0x00); MRAM_CS_HIGH(); } while (sr & 0x01); } static void mram_write_enable(void) { MRAM_CS_LOW(); mram_transfer_byte(0x06); MRAM_CS_HIGH(); }这个底层设计里有三个我不愿妥协的点。片选操作必须用宏或者单独函数封装,因为 STM32 的 HAL 库调用开销略大,频繁翻转片选时直接操作寄存器会更稳。底层函数只做一次字节传输,不混合地址操作,这样便于调试。等待不忙用的是轮询状态寄存器,工业场景下我更喜欢这种确定性的方式,不用中断,也不会因为中断服务函数优先级没调好导致芯片忙超时。
写使能是 MR25H40 写操作的门槛,发完 0x06 之后应该立刻发写指令,中间不要插入其他 SPI 通信,否则 WEL 位可能被意外清掉。我见过有人把写使能发完后加了一个调试串口打印,结果打印期间 SPI 总线上什么也没发生,按理说 WEL 不会丢,但后来发现系统里另一个任务抢占了 SPI 总线去做别的读取,把 WEL 清掉了。所以写操作和写使能必须做成原子操作,关调度或者用互斥锁保护。
4.2 任意地址连续读写的驱动实现
读取数据用的是 0x03 指令,发完指令后跟 3 字节地址,然后连续读若干字节。地址是大端格式,先发高字节再发低字节。4Mbit 的容量对应 0x00000 到 0x7FFFF,所以我用 3 字节地址中的低 19 位,高 5 位忽略,数据手册也明确要求地址的高位超范围部分忽略。
int mram_read(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t i; if (addr + len > MRAM_SIZE) { return -1; } mram_wait_not_busy(); MRAM_CS_LOW(); mram_transfer_byte(0x03); mram_transfer_byte((addr >> 16) & 0xff); mram_transfer_byte((addr >> 8) & 0xff); mram_transfer_byte(addr & 0xff); for (i = 0; i < len; i++) { buf[i] = mram_transfer_byte(0x00); } MRAM_CS_HIGH(); return 0; }写入数据用 0x02 指令,连续写时不需要像 Flash 那样按页切分。MR25H40 内部按字节直接改写,连续写长度上限数据手册里有所限制,我的经验是单次传输不要越过芯片地址末尾,避免地址计数器回绕。如果写入区域跨越 0x7FFFF 回绕到 0x00000,那数据就乱了。所以驱动里最好加一个边界检查,比如当前地址加上长度超过 512KB,就返回错误或者拆成两段。
int mram_write(uint32_t addr, const uint8_t *buf, uint32_t len) { uint32_t i; if (addr + len > MRAM_SIZE) { return -1; } mram_wait_not_busy(); mram_write_enable(); MRAM_CS_LOW(); mram_transfer_byte(0x02); mram_transfer_byte((addr >> 16) & 0xff); mram_transfer_byte((addr >> 8) & 0xff); mram_transfer_byte(addr & 0xff); for (i = 0; i < len; i++) { mram_transfer_byte(buf[i]); } MRAM_CS_HIGH(); mram_wait_not_busy(); return 0; }写完以后我再调用一次 mram_wait_not_busy,是为了确保下一次写操作开始时芯片已经空闲。MR25H40 虽然是 MRAM,基本没有传统 Flash 的编程等待时间,但芯片内部状态切换仍然需要纳秒到微秒级别,用 WIP 位做握手是最稳妥的做法。实际跑下来,单字节写的时间开销几乎可以忽略,10 万次写入也不会出现装备劣化的问题。
4.3 掉电保存场景的工程化用法
驱动有了之后,最关键的是怎么在工业场景里用好这个特性。我做掉电保存时,会用到 STM32F745ZG 的 ADC 监测电源轨,当 24V 输入跌到某个阈值时,触发外部中断,主程序进入紧急保存流程。这个流程里我只把几个关键结构体写入 MRAM,比如电机位置、速度、报警字、工艺批次号,数据量通常不到 4KB,在 27MHz SPI 频率下,4KB 写完全部不过 0.2ms,加上中断响应时间也就 1ms 左右。如果客户要求更严,可以把 SPI 频率提到 40MHz,甚至用 DMA 传输,速度更快。
还有一种用法是当普通配置存储区用。工业设备的 IP 地址、校准参数、用户偏好这类数据,更新频率不高但要求绝对可靠。MRAM 不需要擦除,可以直接把每个配置项做成“双备份加校验”结构,写入前先写备份区,再写主区,读的时候先读主区校验,不对再读备份区。这套逻辑在 Flash 上也能做,但 Flash 的扇区擦除和磨损均衡让代码复杂度上了一个档次,MRAM 上写起来非常轻松,因为没有擦除和寿命焦虑。
typedef struct { uint32_t magic; float kp; float ki; float kd; uint16_t crc; } motor_cfg_t; void save_motor_cfg(const motor_cfg_t *cfg) { mram_write(MOTOR_CFG_BACKUP_ADDR, (const uint8_t *)cfg, sizeof(motor_cfg_t)); mram_write(MOTOR_CFG_MAIN_ADDR, (const uint8_t *)cfg, sizeof(motor_cfg_t)); }需要注意的是结构体内存对齐问题。Cortex-M7 对自然对齐访问速度快,但 SPI 发送时是按字节逐个发送的,所以结构体里的 padding 字节也会被写进去。如果上位机工具和固件对结构体布局理解不一致,CRC 校验会一直失败。我的建议是给结构体加#pragma pack(1),或者干脆直接定义成固定长度的字节数组,避免跨编译器差异。
5. 现场调试经验:常见问题排查与避坑实录
5.1 常见问题与排查对照
接触这套方案以来,我在现场和实验室遇到过不少问题,整理成一张表,按出现频率从高到低排列,以后大家遇到类似现象可以照着查。
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 读出来全是 0xFF | 芯片未唤醒、SPI 引脚接错、供电异常 | 先发 0x99 唤醒并延时,用示波器抓 CLK/DATA 波形确认引脚 |
| 写不进去,读回旧数据 | 没发 0x06 写使能、WP 引脚被拉低 | 查询状态寄存器 WEL 位,检查 WP 上拉电阻 |
| 偶发写入数据错位 | SPI 时钟极性和相位不匹配 | 结合数据手册时序图检查 CPOL/CPHA,固定使用 Mode 0 |
| 长时间运行后通信卡死 | HOLD 引脚悬空受干扰 | HOLD 一定要上拉 VDD,板卡打胶后引脚污染也会导致 |
| 掉电保存数据部分丢失 | 电源跌落太快,MCU 没完成写操作 | 用示波器看掉电时序,加大母线电容,提前到 20ms 触发保存 |
| 读取速度上不去 | SPI 分频太高 | 根据 APB 总线频率重新计算分频系数,改到 8 分频以下 |
这些坑里的“偶发写入数据错位”是最难查的。有一次客户反馈设备运行两小时后,配置参数里偶尔出现一个字节被写成 0xAA,刚开始怀疑是 MRAM 质量问题,后来用逻辑分析仪抓了整整半天,发现是 SPI 总线空闲时 MOSI 被外部干扰拉低,刚好和 CS 下降沿配合产生了假写入。解决办法是换更短的 SPI 走线、在 MOSI 上串 330Ω 电阻,并且把 CS 改成默认高电平,只在传输窗口内拉低。
5.2 我踩过的几个典型坑
第一个坑是上电初始化顺序。我曾经在 HAL_SPI_Init 之前就去读 MRAM 的状态寄存器,结果 MISO 一直返回 0xFF。不是 SPI 外设没初始化,而是 HAL 库的 SPI 初始化里包含了对 GPIO 复用功能的配置,次序颠倒后引脚根本没切成 SPI 功能,导致数据线悬空。后来我把所有外设初始化都放到一个函数里,严格按照 时钟使能、GPIO 配置、外设配置、目标芯片唤醒 的顺序执行,一次就通了。
第二个坑是休眠模式。MR25H40 有睡眠指令 0x66,一旦进入睡眠,普通读指令不响应,片选拉低后 MISO 也不出数据。我早期没有做唤醒,每次系统低功耗唤醒来读数据,读回来的全是 0xFF。后来我在每次通信前先发一个 0x99 唤醒指令,并且延时至少 1ms。不过要注意,频繁唤醒会增加一点功耗,如果产品对待机电流敏感,建议只在低功耗切换时唤醒一次,退出睡眠后保持待机即可。
第三个坑是 DMA 传输和片选时序配合。用 STM32F745ZG 的 SPI 配合 DMA 做大数据块读写时,DMA 传输完成后会有短暂的“尾巴”,如果代码立刻拉高片选,主设备可能少读了一个字节。HAL 库的 HAL_SPI_Receive_DMA 回调触发时机是最后一个数据已经开始接收,但不代表 SPI 状态机完全结束。我用了一个变通方法:在 DMA 完成回调里加一个 SPI 外设忙检查,或者干脆等 3 个 SPI 时钟周期再拉高 CS。批量读 512KB 全片数据时,这个细节能避免很多匪夷所思的 CRC 错误。
5.3 性能测试与数据可靠性验证
驱动调通后,我建议做一次完整的读写循环测试,不要把 MRAM 当黑盒直接上产线。测试脚本可以做成这样:先写已知模式到全片,再分段回读比对,然后随机地址单字节写 10 万次,每次写完后读出校验。MR25H40CDF 的写寿命极高,这种测试跑一晚也到不了极限,但能暴露时序和电气问题。我一般会定义三个测试模式:全写 0x5A、全写 0xA5、递增模式,每一种跑完都要统计错误地址和错误次数。如果出现错误,先查 SPI 信号完整性和供电噪声,别急着怀疑芯片。
除此之外,数据可靠性还依赖良好的代码容错。工业现场常有电源噪声,SPI 帧可能偶尔错一个位。我自己的代码在每次读取关键配置后,都会用 CRC16 校验,错就重新读三次,三次都错就告警并加载默认参数。成本不高,但对产线一致性很有帮助。
6. 总结与一点个人心得
MR25H40CDF 和 STM32F745ZG 的组合,非常适合那些对数据可靠性要求高、写入频繁、掉电不能丢的工业设备。MRAM 用起来像 RAM 一样自由,又具备非易失存储特性,能让固件开发省掉很多为 Flash 做的磨损均衡、擦除管理等复杂逻辑。
我个人做这套方案的最大体会是,存储器件选型要回到工业现场的本质需求去看。价格差几块钱在 BOM 里占比不小,但设备卖到客户现场,因为丢失数据导致的售后成本往往远高于器件差价。MRAM 贵得值,前提是你把它用在合适的地方。另外,不管驱动写得多顺手,示波器和逻辑分析仪一定要常备,SPI 时序这种东西不抓波形,永远不知道自己离跑通还有多远。如果你准备在自己的产品里加 MRAM,先拿一块核心板打通驱动,再跑一整夜的写入回读测试,确认无误后再改原理图扩大版,这套流程每次都帮我省下了至少一周的调试时间。
最后再分享一个小技巧:MRAM 驱动代码和 Flash 驱动代码尽量共用一套 SPI 读写抽象层,这样未来在 SPI NOR Flash、SPI EEPROM 和 MRAM 之间切换,只需要替换底层命令字和状态寄存器的处理逻辑,上层业务完全不用动。我做存储模块时都会自定义一套 mram_read/mram_write/mram_ioctl 接口,后续扩展其他存储芯片,只改一个文件,省心又稳妥。