
1. 为什么在工业现场我会优先考虑 MRAM 而不是 EEPROM做嵌入式这行十几年存储方案选型这件事踩过的坑比写过的驱动还多。早些年做工业数据采集终端板子上清一色挂 EEPROM比如 AT24C 系列便宜、好买、驱动成熟。但真到了现场问题就来了写一个字节要等 5 毫秒频繁记录运行日志的时候CPU 大量时间耗在等写完成上更头疼的是擦写寿命10 万次到 100 万次的量级对于每分钟都要落盘一次参数的设备来说两三年就把寿命耗光了。后来换 FRAM速度快了、寿命也够了但容量小、价格高而且很多型号供货周期飘忽不定。直到接触到MR25H40CDF这颗 MRAM磁性随机存储器我才觉得找到了一个比较平衡的答案。它是 4Mbit512KB容量的 SPI 接口 MRAM来自 Everspin 的 MR 系列。MRAM 的核心特点用一句话概括它像 SRAM 一样快像 Flash 一样非易失像 RAM 一样可以无限次随机写。没有擦除动作没有写等待写一个字节和写一整页在时间上没有本质区别。我这次搭配的主控是TM4C1294NCZADTI 的 Tiva C 系列ARM Cortex-M4F 内核120MHz 主频片上带 1MB Flash、256KB SRAM还有多个 SSITI 对 SPI 的叫法模块。选它是因为工业现场对实时性和外设资源要求高这颗片子有以太网 MAC、CAN、USBSSI 模块支持 DMA正好能把 MRAM 的读写性能榨出来。这篇文章我想聊的不是怎么点灯级别的入门而是把 MR25H40CDF 和 TM4C1294NCZAD 这套组合真正用在工业和嵌入式数据存储场景里从硬件连接到 SPI 时序、从驱动编写到掉电保护、从性能测试到踩坑记录完整走一遍。如果你正在做工业网关、数据记录仪、PLC 扩展模块、医疗设备参数存储这类项目这套方案可以直接参考。提示MRAM 不是更快的 Flash它的读写模型和 Flash 完全不同用 Flash 的思维去写驱动会出问题后面会详细说。2. MR25H40CDF 的存储模型和 SPI 时序到底特殊在哪2.1 它不是 Flash别用 Flash 的脑子去理解它很多人第一次用 MRAM会下意识地把它当成 SPI Flash 来操作结果发现读出来的数据不对或者写进去没生效。根源在于两者的存储模型根本不同。SPI Flash比如 W25Q64的写操作分三步先发写使能命令再发擦除命令把整个扇区擦成 0xFF最后才能写数据。擦除是必须的因为 Flash 的物理特性决定了它只能把 bit 从 1 变成 0要变回 1 必须整块擦除。而 MR25H40CDF 是 MRAM磁隧道结的物理结构决定了它每个 bit 可以独立地、任意方向地翻转不需要擦除不需要写使能直接发写命令就能改任意地址的任意字节。这个差异带来的直接好处是写延迟极低MR25H40CDF 的写周期时间典型值是几十纳秒级别SPI 接口下瓶颈在总线速率而不是存储单元本身。寿命几乎无限官方标称读写次数超过 10^14 次实际上你根本写不坏它。无需扇区管理不用维护擦除块映射表文件系统层可以做得非常简单。但代价也有容量比同价位的 Flash 小4Mbit 只有 512KB单价偏高适合存关键参数而不是存大块日志。2.2 SPI 模式 0 和模式 3 都能用但时序细节要注意MR25H40CDF 支持 SPI 模式 0CPOL0, CPHA0和模式 3CPOL1, CPHA1。TM4C1294NCZAD 的 SSI 模块可以配置成任意模式我一般选模式 0因为和大多数逻辑分析仪、示波器的默认解码设置一致调试方便。命令集很精简常用的就几条命令名称命令码作用地址字节数WREN0x06写使能0WRDI0x04写禁止0RDSR0x05读状态寄存器0WRSR0x01写状态寄存器0READ0x03读数据3WRITE0x02写数据3注意这里 WREN 和 WRDI 是存在的但和 Flash 里的作用不一样。MRAM 里 WREN 主要是为了配合状态寄存器的块保护位BP0/BP1使用如果你没有开启块保护理论上可以直接发 WRITE。不过为了代码的通用性和安全性我还是习惯每次写之前发一次 WREN。状态寄存器里最关键的是WIP 位Write In Progress。虽然 MRAM 写很快但 SPI 传输本身需要时间连续写的时候还是要检查 WIP 或者用足够长的延时。我实测在 10MHz SPI 时钟下写一个字节从发命令到 WIP 清零大约 1 微秒左右基本可以忽略。2.3 地址空间和页边界MR25H40CDF 的 512KB 地址空间是线性的0x00000 到 0x7FFFF。它没有 Flash 那种页的概念写操作可以跨任意地址边界连续进行只要 CS 保持拉低地址会自动递增。这一点在写大块数据时非常方便不用像 Flash 那样担心跨页写导致回卷。但有一个细节要注意SPI 是全双工的读的时候 MOSI 上发的是哑元数据通常 0x00 或 0xFF写的时候 MISO 上回的是无效数据。TM4C1294 的 SSI 模块在 FIFO 模式下读写是同时进行的如果你用阻塞式 API要确保发送和接收的字节数匹配否则 FIFO 会错位。3. TM4C1294NCZAD 的 SSI 模块配置与硬件连接3.1 硬件连线别小看那几根线TM4C1294NCZAD 有多个 SSI 模块我这次用的是 SSI2映射到 GPIO 的 PB4SSI2Clk、PB5SSI2Fss、PB6SSI2Rx、PB7SSI2Tx。MR25H40CDF 的引脚对应关系SCK→ PB4CS#→ PB5也可以用普通 GPIO 做软件片选后面会讲SO→ PB6MRAM 的输出接 MCU 的输入SI→ PB7MRAM 的输入接 MCU 的输出VCC→ 3.3VGND→ 共地WP#和HOLD#→ 如果不用硬件写保护和暂停功能直接上拉到 3.3V这里有个坑我踩过MR25H40CDF 的 WP# 和 HOLD# 是低电平有效如果你悬空不接引脚状态不确定可能导致写保护随机生效或者通信被暂停。最稳妥的做法是各接一个 10K 上拉电阻到 3.3V。我见过有人直接悬空结果调试了一整天以为驱动有问题其实是 HOLD# 被干扰拉低了。PCB 布线方面SPI 时钟在 10MHz 以上时走线尽量短、等长SCK 和数据的相位关系要保证。如果板子上有多个 SPI 从设备每个设备的 CS 走线独立不要共用。地平面要完整MRAM 的电源引脚旁边放一个 0.1uF 的去耦电容越近越好。3.2 SSI 模块初始化时钟、模式、FIFOTM4C1294 的 SSI 配置我一般用 TI 的 TivaWare 库直接操作寄存器也行但库函数更省事。核心配置项// 使能 SSI2 和 GPIOB 时钟 SysCtlPeripheralEnable(SYSCTL_PERIPH_SSI2); SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOB); // 配置 GPIO 为 SSI 功能 GPIOPinConfigure(GPIO_PB4_SSI2CLK); GPIOPinConfigure(GPIO_PB5_SSI2FSS); GPIOPinConfigure(GPIO_PB6_SSI2RX); GPIOPinConfigure(GPIO_PB7_SSI2TX); GPIOPinTypeSSI(GPIO_PORTB_BASE, GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7); // 配置 SSI主模式10MHz模式016位数据宽度 SSIConfigSetExpClk(SSI2_BASE, SysCtlClockGet(), SSI_FRF_MOTO_MODE_0, SSI_MODE_MASTER, 10000000, 8); // 使能 SSI SSIEnable(SSI2_BASE);这里有几个参数需要解释时钟频率MR25H40CDF 最高支持 40MHz 的 SPI 时钟但实际能跑多快取决于 PCB 布线和主控能力。我一般先用 1MHz 调通再逐步提高到 10MHz 或 20MHz。TM4C1294 在 120MHz 系统时钟下SSI 分频后能稳定输出 10MHz 没问题。数据宽度配置成 8 位因为 MRAM 是按字节寻址的。虽然 SSI 支持 16 位 FIFO但用 8 位更直观。模式 0CPOL0 表示空闲时时钟为低CPHA0 表示在第一个时钟边沿采样。这个和 MR25H40CDF 的数据手册一致。3.3 硬件片选 vs 软件片选我为什么最终选了软件片选TM4C1294 的 SSI 模块自带 FSS 引脚可以自动在传输时拉低片选。但实际用下来我最终改成了普通 GPIO 做软件片选。原因有三第一硬件片选在 FIFO 模式下FSS 的拉低和拉高时机由硬件控制有时候在连续传输多条命令时FSS 会在两条命令之间短暂拉高而 MRAM 要求 CS 在整个命令序列命令码地址数据期间保持低电平。硬件片选可能导致命令被截断。第二软件片选让我可以在发命令前精确控制时序比如发 WREN 之后、发 WRITE 之前插入几个微秒的延时确保状态机就绪。第三调试方便。用逻辑分析仪抓波形时软件片选的 GPIO 翻转点清晰可见容易定位问题。软件片选的实现很简单// 配置 PB5 为普通输出 GPIOPinTypeGPIOOutput(GPIO_PORTB_BASE, GPIO_PIN_5); GPIOPinWrite(GPIO_PORTB_BASE, GPIO_PIN_5, GPIO_PIN_5); // 默认拉高 // 片选拉低 void MRAM_CS_Low(void) { GPIOPinWrite(GPIO_PORTB_BASE, GPIO_PIN_5, 0); } // 片选拉高 void MRAM_CS_High(void) { GPIOPinWrite(GPIO_PORTB_BASE, GPIO_PIN_5, GPIO_PIN_5); }注意如果之前已经把 PB5 配置成了 SSI2FSS 功能要先调用GPIOPinTypeGPIOOutput把它改回普通 GPIO否则写 GPIO 数据寄存器不会影响引脚电平。4. 驱动层实现从单字节读写到 DMA 批量传输4.1 底层字节收发函数TM4C1294 的 SSI 收发用SSIDataPut和SSIDataGet但这两个函数在 FIFO 满或空时会阻塞。我封装了一个简单的字节交换函数uint8_t SPI_TransferByte(uint8_t txData) { uint32_t rxData; SSIDataPut(SSI2_BASE, txData); // 发送 while (SSIBusy(SSI2_BASE)); // 等待传输完成 SSIDataGet(SSI2_BASE, rxData); // 接收 return (uint8_t)rxData; }这里SSIBusy是等待 SSI 忙标志清零确保数据已经移出。如果不加这个等待连续调用时可能读到上一次的残留数据。4.2 读操作发命令、发地址、读数据读 MRAM 的流程CS 拉低发送 READ 命令码 0x03发送 3 字节地址高字节在前连续读取 N 字节数据每读一个字节MOSI 发 0x00CS 拉高void MRAM_Read(uint32_t addr, uint8_t *buffer, uint32_t len) { MRAM_CS_Low(); SPI_TransferByte(0x03); // READ 命令 SPI_TransferByte((addr 16) 0xFF); // 地址高字节 SPI_TransferByte((addr 8) 0xFF); // 地址中字节 SPI_TransferByte(addr 0xFF); // 地址低字节 for (uint32_t i 0; i len; i) { buffer[i] SPI_TransferByte(0x00); // 读数据 } MRAM_CS_High(); }4.3 写操作WREN 不是必须的但建议保留写 MRAM 的流程CS 拉低发送 WREN 命令 0x06CS 拉高WREN 需要独立命令CS 拉低发送 WRITE 命令码 0x02发送 3 字节地址连续写入 N 字节数据CS 拉高void MRAM_Write(uint32_t addr, const uint8_t *buffer, uint32_t len) { // 发送 WREN MRAM_CS_Low(); SPI_TransferByte(0x06); MRAM_CS_High(); // 发送 WRITE 命令和地址 MRAM_CS_Low(); SPI_TransferByte(0x02); SPI_TransferByte((addr 16) 0xFF); SPI_TransferByte((addr 8) 0xFF); SPI_TransferByte(addr 0xFF); // 写数据 for (uint32_t i 0; i len; i) { SPI_TransferByte(buffer[i]); } MRAM_CS_High(); }实测下来在 10MHz SPI 时钟下写 512 字节大约需要 420 微秒读 512 字节大约 410 微秒。这个速度对于工业数据记录来说完全够用。4.4 用 DMA 把 CPU 解放出来如果你的项目里需要频繁读写大块数据比如每秒钟记录几 KB 的传感器数据用阻塞式字节循环会占用大量 CPU 时间。TM4C1294 的 SSI 模块支持 DMA可以配置 uDMA 通道自动搬运数据。配置 DMA 的步骤稍微复杂一些// 使能 uDMA 时钟 SysCtlPeripheralEnable(SYSCTL_PERIPH_UDMA); uDMAEnable(); uDMAControlBaseSet(controlTable); // 配置 SSI2 TX 的 DMA 通道 uDMAChannelAssign(UDMA_CHANNEL_SSI2TX); uDMAChannelAttributeDisable(UDMA_CHANNEL_SSI2TX, UDMA_ATTR_ALTSELECT | UDMA_ATTR_USEBURST | UDMA_ATTR_HIGH_PRIORITY | UDMA_ATTR_REQMASK); uDMAChannelControlSet(UDMA_CHANNEL_SSI2TX | UDMA_PRI_SELECT, UDMA_SIZE_8 | UDMA_SRC_INC_8 | UDMA_DST_INC_NONE | UDMA_ARB_8);DMA 模式下CPU 只需要配置好源地址、目标地址和传输长度剩下的交给 DMA 控制器。我实测用 DMA 写 4KB 数据CPU 占用率从阻塞模式的 60% 降到 5% 以下。不过 DMA 也有坑SSI 的 TX 和 RX 必须同时配置 DMA否则 FIFO 会溢出。因为 SPI 是全双工的你发数据的同时也在收数据如果只配 TX 不配 RX接收 FIFO 满了之后 SSI 会停止发送。这一点在 TI 的文档里写得不够醒目我当初调试了半天才发现。5. 掉电保护与数据完整性工业场景的硬需求5.1 为什么 MRAM 在掉电场景下比 Flash 更可靠工业现场最怕的就是掉电导致数据丢失或损坏。Flash 在写操作过程中掉电可能导致整个扇区数据损坏因为擦除和写入不是原子操作。而 MRAM 的写操作是原子级的每个 bit 的翻转在纳秒级完成掉电时要么写成功要么没写不存在中间状态。但这不代表用了 MRAM 就万事大吉。SPI 传输过程中掉电MCU 可能只发了一半的命令或地址MRAM 收到不完整的命令序列行为不确定。所以还是需要一套完整的数据完整性策略。5.2 双备份CRC 校验的存储结构我在实际项目中用的方案是把关键参数存两份每份带 CRC32 校验交替写入。存储布局区域地址范围内容备份 A0x00000 - 0x00FFF参数结构体 CRC32备份 B0x01000 - 0x01FFF参数结构体 CRC32日志区0x02000 - 0x7FFFF循环日志写入流程读取当前有效备份的序号A 或 B把新参数写入另一个备份区计算 CRC32 并写入备份区末尾更新有效备份序号读取流程分别读取 A 和 B 的 CRC32选择 CRC 校验通过且序号较新的那份如果两份都损坏加载默认参数并报警这个方案的好处是即使写入过程中掉电最多损失一次写入旧数据仍然完好。CRC32 能检测出绝大多数位翻转错误。5.3 上电自检和坏块管理虽然 MRAM 理论上没有坏块但工业环境的电磁干扰可能导致偶发写入错误。我习惯在上电时做一次全区域 CRC 扫描如果发现某块区域 CRC 连续多次校验失败就把它标记为不可用降级使用。MR25H40CDF 本身没有坏块管理机制这层逻辑要在软件里做。我的做法是在日志区头部维护一个简单的块状态表每个块 4KB用 1 个字节记录状态0x00 表示正常0xFF 表示坏块其他值表示擦写次数虽然 MRAM 不需要擦除但记录写入次数有助于评估寿命。6. 实测性能与踩坑记录6.1 不同 SPI 时钟下的读写速度对比我用逻辑分析仪和示波器实测了一组数据测试条件TM4C1294NCZAD 主频 120MHzSSI2 主模式模式 0阻塞式传输每次读写 512 字节。SPI 时钟读 512 字节耗时写 512 字节耗时误码率1 MHz4.2 ms4.3 ms05 MHz850 us870 us010 MHz430 us445 us020 MHz220 us230 us040 MHz115 us120 us偶发误码40MHz 时开始出现偶发误码分析下来是 PCB 走线较长导致信号完整性下降。把走线缩短、加匹配电阻后有所改善但为了稳定性最终产品里我跑在 20MHz。6.2 踩坑一CS 拉高太早导致写失败早期版本里我在发完最后一个数据字节后立即拉高 CS结果发现偶尔写入不生效。用逻辑分析仪抓波形发现最后一个字节的时钟边沿还没完全结束CS 就拉高了MRAM 内部状态机没来得及锁存数据。解决办法在发完最后一个字节后插入一个短延时大约 1 微秒再拉高 CS。或者更稳妥的做法是检查 SSI 的 BUSY 标志确保移位寄存器空了再拉高。SPI_TransferByte(lastByte); while (SSIBusy(SSI2_BASE)); // 等待移位完成 MRAM_CS_High();6.3 踩坑二WREN 后立即 WRITE 导致命令丢失MR25H40CDF 的数据手册里提到WREN 命令后需要等待至少 10 纳秒才能发下一条命令。我一开始没注意WREN 和 WRITE 之间没有延时结果大约每 1000 次写入会出现 1 次失败。后来在 WREN 和 WRITE 之间加了 1 微秒的延时问题消失。虽然 10 纳秒理论上很短但考虑到 GPIO 翻转和 SSI 状态机的开销留足余量更保险。6.4 踩坑三DMA 传输时 CS 控制不当用 DMA 做批量写的时候我一开始用硬件 FSS 自动控制片选结果发现 DMA 传输结束后 FSS 立即拉高但最后一个字节可能还在移位。后来改成软件片选在 DMA 传输完成中断里拉高 CS问题解决。另外DMA 传输前要确保 SSI 的 TX FIFO 是空的否则残留数据会和新数据混在一起。我一般在启动 DMA 前调用SSIDataGetNonBlocking清空 FIFO。6.5 踩坑四电源纹波导致偶发读写错误有一批板子在实验室测试一切正常到了现场运行几天就出现数据错误。排查后发现是开关电源的纹波太大MRAM 的供电引脚上叠加了大约 100mV 的高频噪声。在 MRAM 电源引脚旁边加了一个 1uF 的陶瓷电容和一个小磁珠后问题消失。这个坑给我的教训是MRAM 虽然皮实但对电源质量还是有要求的。工业现场的电源环境恶劣去耦电容不能省。7. 这套方案适合什么场景不适合什么场景7.1 适合的场景工业参数存储PLC 的配置参数、校准系数、设备 ID 等数据量不大但要求高可靠、频繁写入。MRAM 的无限寿命和快速写入完美匹配。数据记录仪的元数据区记录仪的传感器数据可以存在大容量 Flash 或 SD 卡里但文件系统的索引、时间戳、断点续传位置这些元数据存在 MRAM 里掉电不丢恢复快。医疗设备的运行日志医疗设备对数据完整性要求极高MRAM 的原子写特性可以保证日志不会因为掉电而损坏。高频交易或实时控制需要微秒级存储响应的场景MRAM 的写延迟远低于 EEPROM 和 Flash。7.2 不适合的场景大容量数据存储512KB 的容量对于音频、图像、大批量日志来说太小了。这种场景还是用 eMMC、SD 卡或大容量 NAND Flash。成本极度敏感的量产产品MRAM 的单价是同容量 EEPROM 的好几倍如果产品对成本极其敏感且写入频率不高EEPROM 或 FRAM 可能更合适。需要文件系统的复杂存储虽然可以在 MRAM 上跑 LittleFS 或 SPIFFS但 512KB 的空间跑文件系统有点浪费而且文件系统的元数据开销会进一步压缩可用容量。7.3 和 FRAM、nvSRAM、电池备份 SRAM 的对比特性MR25H40CDF (MRAM)FRAMnvSRAM电池备份 SRAM容量512KB通常 64KB 以下256KB 左右512KB 以上写速度极快快快极快寿命10^1410^1210^14无限电池限制是否需要电池否否否是单价高中高高中供货稳定性较好一般一般好从表里可以看出MRAM 在容量、寿命、无需电池这几个维度上综合表现最好代价就是价格。如果你的项目对可靠性要求高、写入频繁、容量需求在几百 KB 级别MRAM 是很值得的选择。8. 几个容易被忽略的工程细节8.1 上电初始化顺序MR25H40CDF 上电后需要一段时间大约 1 毫秒才能接受命令。我在初始化代码里加了 2 毫秒的延时确保 MRAM 内部电源稳定。虽然数据手册说典型值是 1 毫秒但留足余量总没错。另外TM4C1294 的 SSI 模块使能之前要确保 GPIO 已经配置好否则可能出现短暂的引脚状态不确定被 MRAM 误认为是命令。8.2 状态寄存器的块保护位MR25H40CDF 的状态寄存器里有 BP0 和 BP1 两个块保护位可以保护部分或全部地址空间不被写入。默认情况下块保护是关闭的但如果你不小心写了状态寄存器可能把整片 MRAM 锁死。我的做法是初始化时读一次状态寄存器确认块保护位是 0然后就不再动它。如果项目需要写保护功能再单独实现。8.3 温度范围MR25H40CDF 有工业级-40°C 到 85°C和汽车级-40°C 到 125°C版本。如果你的产品要过工业认证一定要选工业级型号。我见过有人用商业级0°C 到 70°C的芯片做户外设备冬天直接不工作。8.4 逻辑分析仪的用法调试 SPI 通信时逻辑分析仪是必备工具。我一般设置采样率至少是 SPI 时钟的 10 倍比如 10MHz SPI 就用 100MS/s 采样率。解码协议选 SPICPOL0、CPHA0片选低有效。抓到的波形要重点看CS 拉低到第一个时钟边沿的建立时间、最后一个时钟边沿到 CS 拉高的保持时间、数据在时钟边沿的建立保持时间。如果条件允许用示波器看信号质量重点关注过冲、振铃和上升沿时间。SPI 时钟频率高的时候这些因素直接影响通信可靠性。8.5 代码的可移植性设计虽然这篇文章讲的是 TM4C1294 和 MR25H40CDF 的组合但驱动层我做了抽象把 SPI 收发、片选控制、延时函数做成函数指针或宏方便移植到 STM32、ESP32 或其他平台。这样下次换主控时只需要改底层接口上层逻辑不用动。typedef struct { void (*cs_low)(void); void (*cs_high)(void); uint8_t (*transfer)(uint8_t); void (*delay_us)(uint32_t); } MRAM_Port_t;这种设计在多个项目之间复用时特别省事推荐大家试试。9. 关于数据存储方案选型的一点个人体会做了这么多年嵌入式我越来越觉得存储方案没有最好只有最合适。早些年我总想找一个万能方案结果发现每个场景都有不同的约束成本、容量、速度、寿命、供货、温度范围这些因素互相牵制必须根据项目实际情况做取舍。MR25H40CDF 加 TM4C1294NCZAD 这套组合在我看来最适合的是数据量不大但要求极高可靠性的工业场景。它的优势不在于便宜或容量大而在于让你不用再为掉电丢数据、写寿命不够、写等待太长这些问题操心。对于工业设备来说少一个隐患比省几块钱重要得多。如果你正在评估这个方案我的建议是先用开发板搭一个最小系统把读写、掉电、高温、低温这几个场景都测一遍确认满足你的需求再上产品。存储这东西实验室里跑通不代表现场能用多测总没错。