
1. 项目缘起与方案选型思考1.1 为什么要在工业场景里折腾 MRAM 这颗料做工业嵌入式的人都有一个共同的痛设备装到现场断电、重启、掉电是家常便饭可数据不能丢。传统方案无非两条路——EEPROM 和 NOR Flash。EEPROM 写入慢、寿命有限擦写次数撑死百万级NOR Flash 更麻烦写之前得先擦一个扇区掉电擦到一半整块数据就废了。我早些年做电力监测终端现场一次意外断电把一整天的电表读数全写成了 0xFF客户差点把设备退回来。后来接触到MRAM磁性随机存储器才算找到一条新路。MRAM 的存储原理是靠磁性隧道结的磁化方向来记录 0 和 1不是靠电荷。这个物理机制决定了它几个非常讨喜的特性写入速度接近 SRAM擦写寿命理论上无限厂家标称 10^14 次以上而且不需要擦除操作直接覆盖写就行。掉电的时候数据天然保持不需要额外的掉电检测和电容储能电路。这次项目选用的MR25H40CDF就是一颗 4Mbit512KB的 SPI MRAMSOIC-8 封装工作电压 2.7V~3.6V工业级温度范围 -40℃ 到 85℃。512KB 这个容量在工业数据记录里非常够用——存配置参数、存校准系数、存历史曲线、存故障日志绰绰有余。1.2 主控为什么挑 TM4C123GH6PZ主控这边我用的是 TI 的TM4C123GH6PZARM Cortex-M4 内核80MHz 主频256KB Flash32KB SRAM带硬件浮点单元。选它的理由很实在SPI 外设成熟TM4C123 有 4 个 SSI同步串行接口模块本质就是 SPI支持 Motorola 格式最高速率可以到 12.5MHz系统时钟 80MHz 时。驱动 MRAM 完全够用。工业级可靠性这颗芯片本身就是为工业控制设计的ADC、PWM、CAN 一应俱全后面要扩展采集功能不用换主控。生态资料多TivaWare 库函数封装得比较完整寄存器操作和库函数操作都能走调试起来不费劲。MR25H40CDF 和 TM4C123GH6PZ 这个组合说白了就是工业级主控 工业级非易失存储一个负责算一个负责记中间用 SPI 连起来。整个链路简单、可靠、可复现。1.3 整体架构长什么样系统结构其实很清爽TM4C123GH6PZ (Master) | | SPI0: PA2(SCK) PA3(FSS) PA4(RX) PA5(TX) | MR25H40CDF (Slave) | | HOLD# 接 VCC | WP# 接 VCC | CS# 接 PA3MRAM 这边一共 8 个引脚VCC、GND、CS#、SCK、SI、SO、WP#、HOLD#。WP# 是写保护HOLD# 是暂停通信正常读写的时候这两个都拉高就行。CS# 片选由主控的 FSS 引脚控制。提示MR25H40CDF 的 WP# 和 HOLD# 内部没有上拉一定要外部拉到 VCC否则悬空的时候可能被干扰触发写保护或者暂停导致读写莫名其妙失败。这个坑我踩过查了半天以为是时序问题。2. MRAM 与 SPI 协议的核心细节拆解2.1 MR25H40CDF 的指令集和寻址方式MR25H40CDF 的容量是 4Mbit也就是 512K 字节地址范围 0x00000 到 0x7FFFF需要19 位地址。SPI 协议里地址是按字节发的所以一次要发 3 个字节24 位高 5 位是无效的芯片内部只取低 19 位。它支持的指令不多但每一个都很关键指令名称指令码功能说明WREN0x06写使能写操作前必须先发WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据这里有个和 Flash 最大的区别MRAM 没有擦除指令。Flash 写之前要发 0x20 扇区擦除或者 0xD8 块擦除MRAM 直接 WREN 然后 WRITE 就完事了。这个差异直接简化了驱动逻辑也避免了擦除过程中掉电导致的数据损坏。状态寄存器只有 bit0WEL写使能锁存和 bit1BP0、bit2BP1这几个位有意义。BP0/BP1 是块保护位可以保护部分地址区域不被写入。工业场景里如果有些区域存的是出厂校准参数不希望被程序误写就可以用这两位锁起来。2.2 SPI 模式选择Mode 0 还是 Mode 3MR25H40CDF 支持 SPI Mode 0CPOL0, CPHA0和 Mode 3CPOL1, CPHA1。这两种模式的区别在于时钟空闲电平和采样边沿Mode 0SCK 空闲为低数据在 SCK 上升沿采样下降沿变化。Mode 3SCK 空闲为高数据在 SCK 上升沿采样下降沿变化。两种模式都能用我一般选Mode 0因为 TM4C123 的 SSI 模块默认配置就是 Mode 0不用额外改寄存器。选哪个模式不影响功能关键是主从要一致。如果你用逻辑分析仪抓波形发现数据总是差一位或者全错八成就是模式没对上。2.3 SPI 时钟频率怎么算TM4C123 的 SSI 时钟来源于系统时钟分频。假设系统时钟 80MHzSSI 的时钟分频寄存器 CPSR 和 SCR 共同决定 SCK 频率SCK SysClk / (CPSR × (1 SCR))CPSR 取值范围 2~254且必须是偶数SCR 取值范围 0~255。我要跑 10MHz 的 SCK可以这样算先取 CPSR2那么 1SCR 80/(2×10) 4SCR3。验证一下80MHz / (2 × 4) 10MHz正好。MR25H40CDF 的最高 SCK 频率是 40MHz读操作写操作也是 40MHz所以 10MHz 完全在安全范围内。实际工业现场我一般不会跑满10MHz 到 12.5MHz 是比较稳的选择速率够用信号完整性也好保证。注意SPI 时钟频率不是越高越好。PCB 走线长、没有阻抗匹配的时候高频下 SCK 和 MOSI 容易振铃导致误码。我一般先用 1MHz 调通功能再逐步往上加加到出错就退一档。3. 硬件连接与底层驱动实现3.1 硬件连线与去耦电容先看硬件连接这是所有软件调试的基础。TM4C123GH6PZ 的 SPI0 映射在 PA2~PA5TM4C123 引脚功能MR25H40CDF 引脚PA2SSI0ClkSCKPA3SSI0FssCS#PA4SSI0RxSOPA5SSI0TxSI3.3V电源VCCGND地GND3.3V上拉WP#3.3V上拉HOLD#去耦电容不能省MRAM 的 VCC 和 GND 之间要放一个0.1μF 陶瓷电容尽量靠近芯片引脚。如果电源纹波大再并一个 1μF 的。工业现场电磁环境复杂电源上的毛刺可能导致写操作出错这个电容就是最后一道防线。3.2 TM4C123 的 SSI 初始化代码用 TivaWare 库来初始化 SPI0代码很直接#include inc/hw_memmap.h #include driverlib/sysctl.h #include driverlib/gpio.h #include driverlib/pin_map.h #include driverlib/ssi.h void SPI0_Init(void) { // 使能 SSI0 和 GPIOA 时钟 SysCtlPeripheralEnable(SYSCTL_PERIPH_SSI0); SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOA); while(!SysCtlPeripheralReady(SYSCTL_PERIPH_SSI0)); while(!SysCtlPeripheralReady(SYSCTL_PERIPH_GPIOA)); // 配置 PA2/PA3/PA4/PA5 为 SSI 功能 GPIOPinConfigure(GPIO_PA2_SSI0CLK); GPIOPinConfigure(GPIO_PA3_SSI0FSS); GPIOPinConfigure(GPIO_PA4_SSI0RX); GPIOPinConfigure(GPIO_PA5_SSI0TX); GPIOPinTypeSSI(GPIO_PORTA_BASE, GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5); // 配置 SSI0主机模式Mode 010MHz8位数据 SSIConfigSetExpClk(SSI0_BASE, SysCtlClockGet(), SSI_FRF_MOTO_MODE_0, SSI_MODE_MASTER, 10000000, 8); // 使能 SSI0 SSIEnable(SSI0_BASE); // 清空接收 FIFO 里的残留数据 uint32_t dummy; while(SSIDataGetNonBlocking(SSI0_BASE, dummy)); }这段代码里几个点值得说SSIConfigSetExpClk的第四个参数是位速率库函数会自动帮你算 CPSR 和 SCR不用手动算分频。传 10000000 就是 10MHz。最后那个清 FIFO 的循环别省。SSI 使能之后 FIFO 里可能有上电残留不清掉的话第一次读会读到垃圾数据。SSI_FRF_MOTO_MODE_0就是 CPOL0、CPHA0和 MRAM 的 Mode 0 对应。3.3 片选控制硬件片选还是软件片选TM4C123 的 FSS 引脚在主机模式下可以配置成硬件片选也可以当普通 GPIO 用软件控制。这两种方式我都试过说说区别。硬件片选SSI 模块在发送数据时自动拉低 FSS发完自动拉高。优点是省代码缺点是片选时序由硬件控制中间可能有间隙而且如果总线上挂了多个从设备硬件片选只能管一个。软件片选把 PA3 配成普通 GPIO 输出手动拉低拉高。优点是时序完全可控多从设备也好处理。缺点是每次读写都要手动操作。我推荐软件片选尤其是工业项目。原因很简单MRAM 的写操作需要拉低 CS → 发 WREN → 拉高 CS → 拉低 CS → 发 WRITE → 发地址 → 发数据 → 拉高 CS这样一个完整序列中间 CS 必须有一次拉高来锁存写使能。硬件片选做不到这种精细控制。软件片选的代码#define MRAM_CS_LOW() GPIOPinWrite(GPIO_PORTA_BASE, GPIO_PIN_3, 0) #define MRAM_CS_HIGH() GPIOPinWrite(GPIO_PORTA_BASE, GPIO_PIN_3, GPIO_PIN_3) void MRAM_CS_Init(void) { GPIOPinTypeGPIOOutput(GPIO_PORTA_BASE, GPIO_PIN_3); MRAM_CS_HIGH(); // 默认拉高 }3.4 SPI 字节收发函数底层收发用一个函数搞定发一个字节同时收一个字节uint8_t SPI_Transfer(uint8_t txData) { uint32_t rxData; SSIDataPut(SSI0_BASE, txData); while(SSIBusy(SSI0_BASE)); SSIDataGet(SSI0_BASE, rxData); return (uint8_t)(rxData 0xFF); }SSIDataPut把数据压进发送 FIFOSSIBusy等发送完成SSIDataGet从接收 FIFO 取数据。注意SSIDataGet是阻塞的如果 FIFO 里没数据会一直等所以前面必须确保SSIBusy返回 false。实操心得SSIBusy判断的是发送 FIFO 和移位寄存器是否都空了。如果只判断发送 FIFO 空可能最后一个字节还在移位寄存器里没发完这时候拉高 CS 会截断数据。这个细节在高速通信时特别重要。4. MRAM 读写操作的完整实现4.1 写使能每次写之前都要做MR25H40CDF 上电后默认是写禁止状态任何写操作之前必须先发 WREN 指令。而且每次写操作完成后WEL 位会自动清零所以每一次写都要重新发 WREN不能发一次管多次。void MRAM_WriteEnable(void) { MRAM_CS_LOW(); SPI_Transfer(0x06); // WREN MRAM_CS_HIGH(); }就三行但少一行都不行。我见过有人写数据一直不成功查了半天发现是忘了发 WREN或者 WREN 之后 CS 没拉高导致写使能没锁存。4.2 写数据地址 数据流写操作的完整时序是拉低 CS → 发 WRITE(0x02) → 发 3 字节地址 → 发 N 字节数据 → 拉高 CS。void MRAM_Write(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_WriteEnable(); MRAM_CS_LOW(); SPI_Transfer(0x02); // WRITE 指令 SPI_Transfer((addr 16) 0xFF); // 地址高字节 SPI_Transfer((addr 8) 0xFF); // 地址中字节 SPI_Transfer(addr 0xFF); // 地址低字节 for(uint32_t i 0; i len; i) { SPI_Transfer(buf[i]); } MRAM_CS_HIGH(); // 等待写完成 while(MRAM_IsBusy()); }地址是 19 位左移 16 位取高字节的时候实际只用了低 3 位高 5 位是 0。芯片内部会自动忽略无效位所以直接移位发送没问题。写完之后要等芯片内部写周期完成。MRAM 的写周期很短典型值几十纳秒但保险起见还是查一下状态寄存器的 WEL 位或者用延时。我一般用轮询状态寄存器的方式uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS_LOW(); SPI_Transfer(0x05); // RDSR status SPI_Transfer(0xFF); MRAM_CS_HIGH(); return status; } bool MRAM_IsBusy(void) { return (MRAM_ReadStatus() 0x01) ! 0; // WEL 位 }4.3 读数据连续读不用重新发地址读操作时序拉低 CS → 发 READ(0x03) → 发 3 字节地址 → 连续读 N 字节 → 拉高 CS。MRAM 支持地址自动递增读完一个字节地址自动加一所以连续读一整块数据只需要发一次地址。void MRAM_Read(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LOW(); SPI_Transfer(0x03); // READ 指令 SPI_Transfer((addr 16) 0xFF); SPI_Transfer((addr 8) 0xFF); SPI_Transfer(addr 0xFF); for(uint32_t i 0; i len; i) { buf[i] SPI_Transfer(0xFF); // 发 dummy 收数据 } MRAM_CS_HIGH(); }读的时候发 0xFF 是 dummy 字节因为 SPI 是全双工主机必须提供时钟才能收数据。发什么内容无所谓芯片会忽略 MOSI 上的数据。4.4 页边界问题MRAM 没有页限制这一点必须单独说。Flash 写数据有页边界限制比如 256 字节一页跨页写会回卷到页首覆盖数据。MRAM 没有这个问题它可以跨页连续写地址一直递增到 0x7FFFF 才回卷到 0x00000。这个特性对工业数据记录太友好了。比如我要存一个 1000 字节的日志结构体直接一次 WRITE 写完不用管页边界不用分多次写。代码简单出错概率也低。注意虽然 MRAM 没有页限制但一次写太多数据会占用 SPI 总线较长时间。如果系统里有其他 SPI 设备建议把大数据拆成小块写每块之间释放一下总线避免影响其他设备的实时性。5. 工业场景下的可靠性设计与避坑5.1 掉电保护MRAM 的天然优势工业设备最怕掉电。用 Flash 存数据掉电时如果正在擦除整个扇区数据全丢。用 EEPROM掉电时如果正在写那个字节可能写成半截。MRAM 的写操作是原子性的——要么写成功要么没写不存在中间状态。而且写周期极短几十纳秒就完成掉电窗口极小。这意味着不需要额外的掉电检测电路和储能电容系统成本直接降下来。不过有个细节要注意SPI 通信过程中掉电可能只发了一半地址或数据。这时候 MRAM 内部可能已经执行了部分写操作。为了避免这种半截写我一般会在数据区加一个双备份 校验的机制数据存两份地址 A 和地址 B。每份数据带一个 CRC16 校验和。读的时候先读 ACRC 对就用 A不对再读 BCRC 对就用 B都不对才报错。这样即使某次掉电写坏了 AB 还是好的。下次上电读到 A 坏了自动用 B 恢复同时把 B 的数据回写到 A。5.2 写保护的使用MR25H40CDF 的状态寄存器里有 BP0 和 BP1 两个块保护位可以保护部分地址区域BP1BP0保护区域00无保护01保护高 1/4 地址0x60000~0x7FFFF10保护高 1/2 地址0x40000~0x7FFFF11保护全部地址出厂校准参数、设备序列号这类数据我一般放在高地址区然后用 BP 位锁起来。程序正常运行的时候只读写低地址区的日志数据高地址区碰不到。这样即使程序跑飞了乱写也写不坏关键参数。设置写保护的代码void MRAM_SetBlockProtect(uint8_t bp) { MRAM_WriteEnable(); MRAM_CS_LOW(); SPI_Transfer(0x01); // WRSR SPI_Transfer(bp 0x06); // 只改 BP1/BP0 位 MRAM_CS_HIGH(); }5.3 SPI 信号完整性PCB 布局要点工业现场电磁干扰大SPI 信号完整性直接决定通信可靠性。几个布局要点SCK 走线尽量短不要绕远路。SCK 是频率最高的信号最容易受干扰。CS 走线单独走不要和 SCK 平行长距离走线避免串扰导致误触发片选。MRAM 靠近主控放置走线长度控制在 10cm 以内。超过 10cm 建议加串联电阻22Ω~33Ω做阻抗匹配。地平面完整SPI 信号线下面要有连续的地平面不要跨分割区。我之前有个项目MRAM 放在板子另一头走线 15cm10MHz 下读写偶尔出错。后来在 SCK 和 MOSI 上各串了一个 33Ω 电阻问题解决。这个电阻的作用是抑制反射成本几分钱效果立竿见影。5.4 常见问题速查表现象可能原因排查方法读出来全是 0xFFCS 没拉低、SPI 模式不对、MRAM 没供电用示波器看 CS 和 SCK 波形读出来全是 0x00MISO 没接好、MRAM 损坏检查 SO 引脚焊接写进去读出来不对忘了发 WREN、写保护没解除读状态寄存器看 WEL 位偶尔读写错误SPI 频率太高、信号完整性问题降频测试加串联电阻地址错位地址字节顺序发反了确认先发高字节还是低字节第一次读不对后面正常FIFO 残留数据初始化后清空接收 FIFO6. 数据存储结构设计与实战经验6.1 存储区划分512KB 看着不大但规划好了很够用。我一般这样分地址范围大小用途0x00000~0x00FFF4KB系统配置参数双备份0x01000~0x01FFF4KB出厂校准数据写保护0x02000~0x3FFFF248KB历史数据记录区环形缓冲0x40000~0x7FFFF256KB故障日志区追加写配置参数区用双备份校准数据区用块保护锁死历史数据区用环形缓冲覆盖最老的数据故障日志区只追加不覆盖写满了就停止记录并报警。6.2 环形缓冲的实现历史数据记录用环形缓冲最合适。维护一个写指针每次写新数据指针后移写到区尾回卷到区首。读的时候从最老的数据开始读。typedef struct { uint32_t writePtr; // 写指针 uint32_t count; // 已存记录数 uint8_t data[RECORD_SIZE]; } Record_t; void SaveRecord(Record_t *rec) { uint32_t addr RECORD_BASE (rec-writePtr % MAX_RECORDS) * RECORD_SIZE; MRAM_Write(addr, (uint8_t*)rec, sizeof(Record_t)); rec-writePtr; if(rec-count MAX_RECORDS) rec-count; }写指针和记录数也要存在 MRAM 里每次上电读出来恢复状态。这两个值我放在配置参数区和系统参数一起双备份。6.3 数据校验CRC16 还是校验和工业数据必须校验。简单的累加和只能发现单字节错误CRC16 能发现绝大多数错误。我一般用CRC16-CCITT多项式 0x1021初始值 0xFFFF。uint16_t CRC16_CCITT(uint8_t *data, uint32_t len) { uint16_t crc 0xFFFF; for(uint32_t i 0; i len; i) { crc ^ (uint16_t)data[i] 8; for(uint8_t j 0; j 8; j) { if(crc 0x8000) crc (crc 1) ^ 0x1021; else crc 1; } } return crc; }每条记录存的时候算一次 CRC 附在末尾读的时候再算一次对比。不匹配就标记这条记录无效跳过。6.4 写入速度实测我用 10MHz SPI 时钟实测了一下 MR25H40CDF 的写入速度单字节写含 WREN 地址 数据 等待约 5μs256 字节连续写约 220μs1KB 连续写约 850μs这个速度在工业数据记录里完全够用。假设每秒存一条 256 字节的记录占用总线时间只有 220μs对系统实时性几乎没有影响。实操心得如果写入频率很高比如每秒几十次建议在 RAM 里做缓冲攒够一批再一次性写入 MRAM。这样能减少 WREN 和地址发送的开销整体吞吐量能提升 30% 以上。7. 调试工具与验证方法7.1 逻辑分析仪抓波形调试 SPI 最直接的工具就是逻辑分析仪。抓 SCK、CS、MOSI、MISO 四根线一眼就能看出问题CS 拉低之后 SCK 有没有正常输出。指令码、地址、数据是不是按预期发送。MISO 上的数据是不是在正确的边沿被采样。我用的是一款 8 通道 24MHz 的逻辑分析仪配合开源软件抓 SPI 波形绰绰有余。抓到的数据可以直接解码成十六进制和代码里发的数据对比很快就能定位问题。7.2 读写一致性测试功能调通之后一定要做读写一致性测试。我的做法是往整个 512KB 空间写递增数据0x00, 0x01, 0x02...。再读回来逐字节对比。写随机数据再读回来对比。写全 0x00读回来确认。写全 0xFF读回来确认。这五步做完基本能确认存储芯片和驱动都没问题。如果某一步出错根据出错地址能判断是硬件问题还是软件问题。7.3 掉电测试工业项目必须做掉电测试。我的方法是在写操作进行到一半的时候用继电器切断 MRAM 的电源然后重新上电读数据看是否损坏。反复做几百次如果数据始终能通过 CRC 校验或者双备份能恢复说明掉电保护机制有效。这个测试能暴露很多平时发现不了的问题比如写时序中的临界状态、电源跌落时的异常行为。8. 项目扩展与后续优化方向8.1 从 SPI 到 QSPIMR25H40CDF 是标准 SPI 接口4 根线。如果以后数据量大了嫌慢可以换 QSPI 接口的 MRAM比如 MR25H40 的 QSPI 版本用 4 根数据线并行传输速率能提升 4 倍。不过 TM4C123 没有 QSPI 外设要换主控才行。8.2 文件系统层封装现在直接操作地址简单但不够灵活。如果项目复杂了可以在 MRAM 上封一个轻量级文件系统比如LittleFS或者SPIFFS。这两个都是为嵌入式设计的支持磨损均衡和掉电保护。MRAM 寿命无限磨损均衡其实用不上但掉电保护和文件管理功能还是很实用的。8.3 多设备共享 SPI 总线如果系统里还有别的 SPI 设备比如 ADC、显示屏就需要处理总线共享。每个设备一个独立的 CS 引脚软件片选控制。访问哪个设备就拉低哪个 CS其他保持高电平。注意 MRAM 的 CS 拉高之后要等一小段时间再操作其他设备确保 MRAM 内部操作完成。8.4 数据加密工业数据有时候涉及商业机密比如生产工艺参数。MRAM 里的数据是明文存储的如果设备被拆开用编程器直接读 SPI 就能拿到数据。要防这一手可以在写入前做加密读出后解密。AES-128 在 Cortex-M4 上跑很快对性能影响不大。我个人在实际操作中的体会是MRAM 这颗料最大的价值不在于速度快或者寿命长而在于它把掉电不丢数据这件事变得极其简单。用 Flash 的时候要操心擦除、页边界、掉电保护用 MRAM 这些统统不用管代码量少一半出错概率也少一半。对于工业嵌入式项目来说简单可靠比什么都重要。