
1. 项目缘起与方案选型思路1.1 为什么要在工业场景里折腾 MRAM 这颗料做过工业数据采集或者电力终端的朋友应该都有体会最头疼的往往不是算法多复杂而是数据存不住。设备跑在变电站、工厂产线、户外机柜里夏天六十多度、冬天零下三四十度偶尔还来个意外断电这时候你辛辛苦苦写进 EEPROM 或者 Flash 里的校准参数、累计电量、故障记录说没就没了。我早年做配电终端的时候就吃过这个亏一批设备在现场跑了半年返修回来发现存储区数据大面积损坏最后查出来是 Flash 在低温下写入时序出了问题。后来接触到MRAM这类磁性随机存储器才算真正把这块心病放下。这次要聊的MR25H40CDF就是一颗 4Mbit 的 SPI 接口 MRAM来自 Everspin 的 MR2xH40 系列。它和传统存储最大的区别在于写入不依赖电荷而是靠磁性隧道结的磁化方向翻转。这个物理机制决定了它几个非常硬核的特性——写入速度是纳秒级、擦写寿命近乎无限官方标称 10^14 次以上、不需要擦除操作、掉电数据保持二十年以上而且工作温度覆盖 -40℃ 到 85℃ 的工业级甚至更宽。对比一下常见的方案你就明白为什么选它了。EEPROM 写入慢、寿命只有百万次量级频繁记录的场景根本扛不住NOR Flash 虽然容量大但必须整块擦除、写入前要解锁、还有写放大问题FRAM 性能接近但容量普遍偏小、价格也高。MRAM 基本是这几者的一个平衡点尤其是字节级随机写入这个能力对于那种来一条数据记一条的工业日志场景简直是量身定做。1.2 TM4C1294NCZAD 为什么适合当主控主控这边选的是 TI 的TM4C1294NCZAD这是 Tiva C 系列里比较能打的一颗。它基于 ARM Cortex-M4F 内核主频 120MHz带浮点单元片上 1MB Flash、256KB SRAM外设资源相当丰富——8 个 UART、4 个 I2C、4 个 SSI也就是 SPI、还有以太网 MAC、USB、CAN 这些工业场景常用的接口。封装是 212 脚的 BGA引脚多到用不完做复杂一点的工业网关或者数据记录仪绰绰有余。选它配 MR25H40CDF 有几个现实理由。第一SSI 外设成熟稳定TI 的 TivaWare 库把 SPI 主机的配置封装得很干净FIFO 深度 16 字节配合 DMA 可以做到几乎不占 CPU 的连续读写。第二宽温版本齐全TM4C1294 有工业级温度范围的型号和 MRAM 的温区能对上。第三生态资料多不管是 TI 官方文档还是社区里的踩坑记录遇到问题基本都能搜到答案这对项目落地太重要了。1.3 整体架构长什么样整个方案说白了就是一句话TM4C1294 当 SPI 主机MR25H40CDF 当从机通过四线 SPI 完成数据的存储和读取。硬件上连接 CS、SCK、MISO、MOSI 四根线加上电源和地。软件上分三层底层是 SPI 驱动负责字节收发中间层是 MRAM 驱动封装读写命令、地址处理、状态查询上层是应用逻辑比如参数存储、日志记录、掉电保护。这个架构的好处是解耦彻底。底层 SPI 驱动换成别的 MCU 也能复用中间层 MRAM 驱动只要改改 SPI 收发函数就能移植上层应用完全不关心底下是 MRAM 还是 Flash。我后面会把每一层的实现细节都拆开讲包括那些文档里不会写、只有实际调过才知道的坑。提示MR25H40CDF 是 3.3V 供电的器件TM4C1294 的 IO 也是 3.3V 电平两者可以直接对接不需要电平转换。但如果你的主控是 5V 系统务必加电平转换芯片否则会打坏 MRAM。2. MR25H40CDF 核心细节与硬件设计要点2.1 芯片内部结构和寻址方式MR25H40CDF 的容量是 4Mbit换算成字节就是512KB地址范围从 0x00000 到 0x7FFFF一共 19 位地址线。它内部按字节组织每个字节都可以独立读写没有扇区、没有页的概念这一点和 Flash 完全不同。你给它一个地址它就直接返回那个地址的数据不需要考虑对齐、不需要考虑擦除边界。SPI 接口支持模式 0 和模式 3也就是 CPOL/CPHA 的两种组合。模式 0 是时钟空闲低电平、数据在上升沿采样模式 3 是时钟空闲高电平、数据在下降沿采样。这两种模式 MRAM 都认具体用哪个取决于你的主控配置。TM4C1294 的 SSI 外设两种都支持我一般习惯用模式 0因为示波器上看波形比较直观上升沿采样也符合大多数人的直觉。命令集方面常用的就几条WREN0x06写使能、WRDI0x04写禁止、READ0x03读数据、WRITE0x02写数据、RDSR0x05读状态寄存器、WRSR0x01写状态寄存器。注意写操作之前必须先发 WREN这是硬性要求忘了这一步数据写不进去而且芯片不会报错只会默默忽略你的写命令这个坑我踩过。2.2 硬件连接与 PCB 布局注意事项接线本身很简单四根信号线加电源地。但 PCB 布局有几个细节必须注意否则高速通信时容易出问题。第一SCK 走线要尽量短且远离干扰源。SPI 时钟频率我一般跑到 10MHz 到 20MHzMR25H40CDF 最高支持 40MHz但实际能跑多快取决于你的板子质量。如果 SCK 旁边有大电流的开关电源走线很容易耦合进噪声导致数据采样错误。我建议 SCK 和 MOSI 之间留够间距最好包地处理。第二CS 片选信号的处理。CS 是低电平有效空闲时必须拉高。如果 CS 悬空或者上电时序不对MRAM 可能误进入某种状态。我习惯在 CS 上拉一个 10K 电阻到 3.3V确保上电瞬间芯片处于未选中状态。另外CS 的下降沿到第一个 SCK 边沿之间要留够时间MR25H40CDF 要求至少 5ns一般 MCU 都能满足但如果你的 SPI 配置里 CS 是硬件自动控制的要确认一下时序参数。第三电源去耦。MRAM 在写入瞬间会有电流尖峰虽然不大但如果在电源引脚旁边没有合适的去耦电容可能导致写入失败。我的做法是在 VDD 引脚旁边放一个0.1uF 陶瓷电容紧贴引脚再在电源入口放一个1uF 到 10uF 的钽电容做储能。这两个电容的位置比容值更重要0.1uF 那个一定要离引脚越近越好。第四MISO 的上拉问题。MISO 是 MRAM 的输出当 CS 为高时它处于高阻态。如果主控的 MISO 引脚没有内部上拉悬空状态下可能读到随机数据。TM4C1294 的 SSI 引脚可以配置内部上拉我一般会打开或者在板子上加一个 10K 外部上拉。2.3 关键参数与时序计算要把 MRAM 用稳几个时序参数必须搞清楚。下面这张表是我从数据手册里整理出来的核心参数实际配置 SPI 时对着看就行。参数符号最小值典型值最大值单位供电电压VDD2.73.33.6V最高时钟频率fSCK--40MHzCS 建立时间tCSS5--nsCS 保持时间tCSH5--ns数据输出延迟tV--8ns写入周期时间tWC--0.5ms数据保持-20--年擦写寿命-10^14--次这里重点说两个。tV 是数据输出延迟意思是 SCK 边沿到来后MISO 上的数据最多 8ns 才稳定。如果你跑 40MHz一个时钟周期才 25ns8ns 的延迟占了将近三分之一留给主控采样窗口就很紧张了。所以实际项目中我一般不会跑满 40MHz15MHz 到 20MHz 是比较稳妥的选择既能保证速度又留足了时序余量。tWC 是写入周期时间0.5ms 是最大值。注意 MRAM 的写入是异步的你发完写命令和地址数据后芯片内部需要时间完成磁性翻转。在这 0.5ms 内如果你又发新的写命令可能会被忽略。所以连续写入时要么在每次写之后读状态寄存器确认完成要么在两次写之间插入足够的延时。我实测下来单字节写入后延时 100us 基本就稳了但为了保险批量写入时我会用状态寄存器轮询的方式。3. 软件驱动实现与实操过程3.1 TM4C1294 的 SSI 外设初始化先讲底层 SPI 配置。TM4C1294 的 SSI 外设配置分几步使能外设时钟、配置 GPIO 复用、设置 SSI 参数、使能 SSI。我用的是 SSI2 模块对应 PB4 到 PB7 这几个引脚具体代码逻辑如下。#include inc/hw_memmap.h #include inc/hw_ssi.h #include driverlib/ssi.h #include driverlib/gpio.h #include driverlib/pin_map.h #include driverlib/sysctl.h void MRAM_SPI_Init(void) { // 使能 SSI2 和 GPIOB 时钟 SysCtlPeripheralEnable(SYSCTL_PERIPH_SSI2); SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOB); while(!SysCtlPeripheralReady(SYSCTL_PERIPH_SSI2)); while(!SysCtlPeripheralReady(SYSCTL_PERIPH_GPIOB)); // 配置引脚复用 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主机模式、模式0、16位数据、20MHz SSIConfigSetExpClk(SSI2_BASE, SysCtlClockGet(), SSI_FRF_MOTO_MODE_0, SSI_MODE_MASTER, 20000000, 8); // 使能 SSI SSIEnable(SSI2_BASE); // 清空接收 FIFO uint32_t dummy; while(SSIDataGetNonBlocking(SSI2_BASE, dummy)); }这段代码里有几个点值得展开。SSIConfigSetExpClk 的最后一个参数是数据位宽我写的是 8也就是每次收发 8 位。MRAM 是字节寻址的8 位正好。但 TM4C1294 的 SSI FIFO 是 16 位宽的如果你发 8 位数据实际占用 FIFO 的低 8 位读的时候也要按 8 位读否则会错位。时钟频率我设的是 20MHz前面说过这是稳妥值。SysCtlClockGet() 返回的是系统时钟TM4C1294 默认 120MHzSSI 分频后得到 20MHz。如果你改了系统时钟这个参数要跟着调整。模式 0 的配置对应 SSI_FRF_MOTO_MODE_0这是最常用的。如果你用模式 3改成 SSI_FRF_MOTO_MODE_3 即可但要注意 MRAM 的时序要求是一样的模式选择只影响时钟极性和相位。3.2 MRAM 读写函数的封装底层 SPI 通了之后就可以封装 MRAM 的读写函数了。核心是三个操作写使能、读数据、写数据。先看写使能的实现。#define MRAM_CMD_WREN 0x06 #define MRAM_CMD_WRDI 0x04 #define MRAM_CMD_READ 0x03 #define MRAM_CMD_WRITE 0x02 #define MRAM_CMD_RDSR 0x05 static void MRAM_CS_Low(void) { GPIOPinWrite(GPIO_PORTB_BASE, GPIO_PIN_5, 0); } static void MRAM_CS_High(void) { GPIOPinWrite(GPIO_PORTB_BASE, GPIO_PIN_5, GPIO_PIN_5); } static uint8_t MRAM_SPI_Transfer(uint8_t data) { uint32_t rxData; SSIDataPut(SSI2_BASE, data); while(SSIBusy(SSI2_BASE)); SSIDataGet(SSI2_BASE, rxData); return (uint8_t)(rxData 0xFF); } void MRAM_WriteEnable(void) { MRAM_CS_Low(); MRAM_SPI_Transfer(MRAM_CMD_WREN); MRAM_CS_High(); }这里有个细节CS 的控制方式。TM4C1294 的 SSI 外设可以硬件自动控制 FSS 引脚但我在实际项目里更倾向于软件手动控制 CS。原因是硬件 CS 的时序是固定的有时候在命令和地址之间需要插入额外延时硬件 CS 做不到。软件控制虽然多几行代码但灵活得多尤其是调试阶段你可以用示波器清楚地看到每一帧的边界。MRAM_SPI_Transfer 这个函数是核心它完成一个字节的收发。注意 SSIDataPut 之后要等 SSIBusy 清零确保数据发完然后再读接收 FIFO。如果忘了等 Busy读出来的可能是上一次的残留数据。这个函数在 20MHz 下执行一次大概几微秒512KB 全读一遍大概一两秒对于工业应用完全够用。接下来是读数据函数。MRAM 的读命令格式是发 0x03然后发 3 字节地址实际只用低 19 位然后连续读数据。地址是高位在前MSB first。void MRAM_Read(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t i; MRAM_CS_Low(); MRAM_SPI_Transfer(MRAM_CMD_READ); MRAM_SPI_Transfer((addr 16) 0xFF); MRAM_SPI_Transfer((addr 8) 0xFF); MRAM_SPI_Transfer(addr 0xFF); for(i 0; i len; i) { buf[i] MRAM_SPI_Transfer(0xFF); } MRAM_CS_High(); }读操作不需要写使能直接发命令就行。注意发地址的时候虽然 MRAM 只有 19 位地址但协议要求发 3 个字节也就是 24 位高 5 位会被忽略。我习惯把地址按 24 位处理这样代码通用性好以后换更大容量的 MRAM 也不用改。写数据函数稍微复杂一点因为要处理写入周期。void MRAM_Write(uint32_t addr, const uint8_t *buf, uint32_t len) { uint32_t i; MRAM_WriteEnable(); MRAM_CS_Low(); MRAM_SPI_Transfer(MRAM_CMD_WRITE); MRAM_SPI_Transfer((addr 16) 0xFF); MRAM_SPI_Transfer((addr 8) 0xFF); MRAM_SPI_Transfer(addr 0xFF); for(i 0; i len; i) { MRAM_SPI_Transfer(buf[i]); } MRAM_CS_High(); // 等待写入完成 MRAM_WaitWriteComplete(); } static void MRAM_WaitWriteComplete(void) { uint8_t status; do { MRAM_CS_Low(); MRAM_SPI_Transfer(MRAM_CMD_RDSR); status MRAM_SPI_Transfer(0xFF); MRAM_CS_High(); } while(status 0x01); // WIP 位为1表示正在写入 }MRAM_WaitWriteComplete 这个函数很关键。状态寄存器的 bit0 是 WIPWrite In Progress位写入过程中为 1完成后为 0。轮询这个位比固定延时更可靠因为写入时间会随温度、电压变化。我实测在常温下单字节写入大概 50us 到 100us 完成批量写入时每个字节之间也要等不能连续猛发。注意MRAM 的写入是逐字节进行的即使你一次发了 100 个字节的数据芯片内部也是一个字节一个字节地翻转。所以批量写入时总时间大约是 单字节时间 × 字节数。如果你要写 1KB 数据按 100us 每字节算大概要 100ms这个时间在实时性要求高的场景要提前规划好。3.3 数据存储结构的设计驱动通了之后就要考虑数据怎么组织。工业应用里MRAM 通常存三类东西系统参数、运行日志、掉电快照。我一般把 512KB 的空间划分成几个区域每个区域有固定的起始地址和大小。区域名称起始地址大小用途系统参数区0x000004KB设备ID、校准系数、配置项运行日志区0x01000256KB循环记录运行数据、故障事件掉电快照区0x410004KB掉电瞬间的关键状态预留区0x42000248KB后续扩展系统参数区用结构体方式存储每个参数有固定的偏移。我习惯在结构体开头加一个魔数和一个 CRC 校验上电时先读出来校验如果 CRC 不对就加载默认值。这样即使 MRAM 因为某种极端原因数据出错设备也能正常启动。运行日志区用循环队列的方式管理。维护一个写指针每次写一条日志就往后移写到区域末尾就绕回开头。同时维护一个序号用来判断哪些是最新数据。这种设计的好处是写入均匀不会因为反复写同一个地址导致局部磨损——虽然 MRAM 寿命近乎无限但均匀写入总是好习惯。掉电快照区是配合掉电检测电路用的。当检测到电源电压下降到阈值时MCU 进入中断把关键变量快速写入 MRAM。MRAM 的写入速度是纳秒级比 Flash 快几个数量级这个场景下优势特别明显。我做过测试从掉电中断触发到写完 256 字节数据总共不到 1ms而同样的数据写 Flash 至少要几十毫秒根本来不及。3.4 完整读写流程的实操记录我把整个流程串一遍从设备上电到数据读写每一步都对应实际代码。第一步上电初始化。系统启动后先初始化时钟、GPIO、SSI然后调用 MRAM_SPI_Init()。初始化完成后读一次 MRAM 的设备 ID 或者状态寄存器确认通信正常。我一般会读地址 0x00000 的前几个字节看看是不是之前写入的魔数。第二步参数加载。从系统参数区读取配置结构体校验 CRC。如果校验通过把参数加载到 RAM 里的全局变量如果失败加载默认参数并写回 MRAM。第三步正常运行。主循环里定期采集数据比如每秒一次把数据打包成日志条目写入运行日志区。写入时先更新写指针再写数据最后更新序号。注意写指针和序号的更新顺序我习惯先写数据再更新指针这样即使写入过程中掉电最多丢一条日志不会破坏整个队列结构。第四步掉电保护。掉电中断触发后立即把当前的关键状态写入掉电快照区。这里要注意中断里不要做太复杂的操作直接调用 MRAM_Write 写一小块数据就行。写完之后可以进入低功耗模式或者等待电源彻底掉下去。第五步上电恢复。下次上电时先读掉电快照区看看有没有有效的快照数据。如果有说明上次是异常掉电可以根据快照恢复状态如果没有说明上次是正常关机按正常流程启动。这套流程我在好几个项目里用过稳定性很好。唯一需要注意的是写入频率。虽然 MRAM 寿命长但也没必要每秒写几百次根据实际需求定一个合理的记录周期就行。我一般设 1 秒到 10 秒一次既不会丢关键数据也不会给 CPU 增加太多负担。4. 常见问题排查与避坑经验4.1 通信失败类问题速查调试 SPI 器件十有八九的问题都出在通信上。我整理了一张速查表遇到问题按这个顺序排查基本能覆盖 90% 的情况。现象可能原因排查方法解决方法读出来全是 0xFFMISO 没接好或 CS 没拉低示波器看 MISO 波形检查接线确认 CS 时序读出来全是 0x00MOSI 没数据或时钟没输出示波器看 SCK 和 MOSI检查 SSI 配置和引脚复用数据偶尔错位时钟太快或时序余量不足降低时钟频率测试降到 10MHz 或加延时写入不成功忘了发 WREN读状态寄存器确认写之前必须发 WREN写入后读出来是旧数据没等写入完成读 WIP 位加 WaitWriteComplete上电后第一次读失败CS 上电时序问题看 CS 上电波形加 CS 上拉电阻读出来全是 0xFF是最常见的。0xFF 意味着 MISO 一直是高电平要么是 MRAM 根本没响应要么是 CS 没拉低导致 MRAM 没被选中。我遇到过一次查了半天发现是 CS 引脚配置成了普通 GPIO 输出但初始电平设成了高而 MRAM 的 CS 是低有效结果一直没选中。后来改成初始化时先拉低再拉高确保一个完整的选中周期。写入不成功这个坑我踩过不止一次。MRAM 的写操作必须严格遵循WREN - WRITE - 等待完成的流程。有一次我为了优化速度把 WREN 和 WRITE 合并到一个 CS 周期里结果数据写不进去。后来查手册才发现WREN 必须在独立的 CS 周期里发送芯片需要看到 CS 的上升沿才会锁存写使能状态。这个细节文档里写得很清楚但容易忽略。4.2 数据可靠性问题与对策通信通了之后下一个要关注的是数据可靠性。工业场景对数据完整性要求高我一般会做几层保护。第一层是 CRC 校验。每个数据块后面跟一个 CRC16 或者 CRC32读取时校验。如果 CRC 不对说明数据损坏可以用备份数据恢复。MRAM 本身出错概率极低但加上 CRC 心里踏实而且能防止软件 bug 导致的数据错乱。第二层是双备份。关键参数存两份地址隔开。读取时如果主份 CRC 不对自动切换到备份。两份都错的情况我还没遇到过但设计上要留这个余地。第三层是写入确认。每次写入后立即读回来对比确认写入成功。这个操作会增加一点时间但对于关键数据值得。我一般在参数写入时做确认日志数据为了速度就不做了。第四层是掉电检测。配合硬件掉电检测电路在电源跌落时及时保存数据。这里要注意检测阈值的选择太低可能来不及保存太高可能误触发。我一般设在 2.9V 左右留出足够的保存时间。4.3 性能优化的几个实操技巧如果项目对读写速度有要求下面几个技巧可以试试。用 DMA 搬运数据。TM4C1294 的 SSI 支持 DMA 触发配置好 DMA 通道后SPI 收发可以完全不占 CPU。我做过测试用 DMA 读 512KB 数据CPU 占用率不到 5%而轮询方式要占 60% 以上。配置 DMA 稍微麻烦一点但收益很大尤其是需要同时处理其他任务的场景。合并写入操作。前面说过 MRAM 写入是逐字节的但如果你要写的数据在地址上是连续的可以一次 CS 周期内写完减少 CS 翻转开销。我实测连续写 256 字节比逐字节写每次一个 CS 周期快大约 30%。合理设置 SPI 时钟。20MHz 是个平衡点但如果你的板子质量好、走线短可以试试 30MHz 甚至 40MHz。我有一块板子跑到 35MHz 很稳另一块到 25MHz 就开始出错。这个要实际测试用示波器看波形质量眼图张开度好的话就可以往上提。利用 MRAM 的随机写入特性。这是 MRAM 相比 Flash 最大的优势。Flash 写入前要擦除整个扇区哪怕你只改一个字节。MRAM 不需要直接改那个字节就行。所以在设计数据结构时可以大胆使用就地更新的方式不用像 Flash 那样搞复杂的日志结构或者磨损均衡。4.4 那些文档里不会写的经验最后分享几个我在实际项目中总结的经验都是踩过坑才明白的。关于 CS 的软件控制。前面提过我用软件控制 CS这里补充一个细节在 CS 拉低之后、发第一个时钟之前最好插入一个几微秒的延时。有些批次的 MRAM 对 CS 建立时间比较敏感虽然手册标称 5ns但实际留点余量更稳。我一般加 1us 延时对整体速度影响可以忽略。关于写入后的读取。MRAM 写入完成后理论上可以立即读取。但我遇到过一种情况写入后马上读读出来是旧数据等几微秒再读就是新数据。后来分析可能是芯片内部的状态切换需要一点时间。所以我在关键写入后加一个 10us 的小延时再读确认就再没出现过这个问题。关于温度的影响。MRAM 在极端温度下的写入时间会变长。我在 -40℃ 的低温箱里测试过单字节写入时间从常温的 50us 增加到 150us 左右。所以如果你的设备要在宽温范围工作WaitWriteComplete 的超时时间要设得足够长我一般设 10ms 超时确保任何温度下都能等到写入完成。关于电源的缓慢上升。有些工业电源上电比较慢电压从 0 升到 3.3V 可能要几十毫秒。在这期间MCU 可能已经开始运行但 MRAM 还没准备好。我的做法是在初始化 MRAM 之前加一个延时等电源稳定后再通信。具体延时多久取决于你的电源设计我一般等 100ms保险一点。关于 SPI 模式的选择。虽然模式 0 和模式 3 都能用但我建议固定用一种模式不要在不同项目里换来换去。因为模式切换容易搞混 CPOL 和 CPHA 的配置而且示波器调试时习惯了一种波形换模式后容易看错。我一直用模式 0所有项目统一省心。这套 MRAM 加 TM4C1294 的方案我从最早的评估板到现在量产项目前后用了好几年累计出货几万套现场返修率极低。存储这块基本没再出过问题算是把早年 Flash 掉数据的阴影彻底翻篇了。如果你也在做工业数据存储不妨试试这个组合前期多花点时间把驱动调稳后面就是安心用。