ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SPI MRAM与TM4C1299的工业存储实践:掉电不丢数据

SPI MRAM与TM4C1299的工业存储实践:掉电不丢数据 做工业设备的这几年我越来越觉得“存数据”比“算数据”更考验人。工控现场要记报警、存参数、保存掉电瞬间的状态传统方案要么用EEPROM慢慢磨要么用Flash先擦后写动不动还得加个电池。直到接触了 Everspin 的 MR25H40CDF 这颗 SPI 接口 MRAM再配上 TI 的 TM4C1299KCZAD 这颗 Cortex-M4F 主控才算是把“存储和读取数据”这件事做顺了。MR25H40CDF 是 4Mbit512KB的磁阻随机存储器不用擦除、直接按字节写、读写次数无限制、掉电数据不丢接口又是标准 SPI非常契合工业嵌入式场景。TM4C1299KCZAD 则是资源很顶的一块 MCU2MB Flash、256KB RAM、以太网/USB/CAN/UART 一应俱全拿它做控制器或者物联网网关都很合适。这套组合特别适合做工业控制器里的运行日志、参数固化、掉电数据抢救这一类需求。正好前阵子刚把一个采集网关的存储模块从“Flash电池RAM”改成了这套方案踩了不少坑这里把从选型、接线、驱动到掉电一致性的工程细节一次性整理出来。1. 为什么这个组合能解决工业存储的痛点1.1 MRAM到底是个什么东西MRAM 的存储单元是靠磁隧道结实现的。简单说就是两层磁性材料夹一层极薄的绝缘层当两层磁化方向相同时隧道电阻小表示 0方向相反时隧道电阻大表示 1。写入时通过流过存储单元的电流改变自由层的磁化方向读的时候检测电阻值就行。用冰箱贴来类比就很直白冰箱贴的磁化方向不会因为断电消失你也不用“擦掉再贴”拨一下方向就换了新状态。MRAM 的写入逻辑也类似——直接改变磁化方向不存在 Flash 那种“先擦后写”的固有操作。所以它既是 RAM 的速度和随机访问特性又有非易失性写入也没有寿命上限。这对工业场景意味着几点写操作不磨损。日志记录一天写几千次十年下来读写次数轻松上亿Flash 和 EEPROM 根本扛不住MRAM 完全无所谓。写入没有延迟。Flash 扇区擦除动辄几十毫秒MRAM 一个字节的写事务就是一次普通 SPI 传输对掉电保存特别友好。不需要电池。以前用 SRAM 掉电保存必须挂电池现在一颗芯片全搞定少一个电池就是少一个几年后漏液的风险点。MR25H40CDF 具体指标可以看这张表参数数值说明容量4Mbit (512KB)8 位宽组织接口SPI支持模式 0 / 模式 3数据保持20 年以上掉电后不丢失读写次数无限制不需要磨损均衡写周期直接写无擦除过程工作电压3.3V与 MCU 电平直连工作温度视后缀而定工业应用要选对应温度等级版本1.2 和Flash、EEPROM、BBSRAM放在一起比做存储选型不能只看容量得结合“写入频率”和“掉电时间窗口”这两个工业场景最关心的指标。NOR Flash 容量大、成本低但按扇区擦除写少量数据时效率极低反复擦写还会导致坏块。EEPROM 一块字节写入虽然方便但容量小最大容量的型号也就几百KB写入速度慢写寿命只有几十万次。BBSRAM电池备份SRAM读写速度和随机访问都好但体积大、要电池、维护麻烦高温环境下电池寿命还得再打折。FRAM 同接口下写入速度不错但当前容量普遍做不大而且内部读操作会破坏数据需要内部回写高频率读取时性能没有 MRAM 那么干脆。实际对比下来MRAM 的综合特性最适合做“频繁写入 掉电保存 随机读取”这类存储。参数存储、运行日志、断电现场保存、升级固件暂存这些任务一条 MRAM 全包了。它价格确实比 Flash 和 EEPROM 贵但换来的是主控省掉文件系统磨损均衡算法、省掉电池维护、掉电保存代码极其简单综合成本反而是划算的。1.3 为什么选中TM4C1299KCZAD做载体TM4C1299KCZAD 是 TI 的 Stellaris 系列后续产品线Cortex-M4F 内核120MHz 主频带硬件浮点单元。对存储应用来说它的优势在于外设丰富且引脚复用灵活有多个 SSI即 SPI接口挂 MRAM 的同时还能并联传感器、屏幕、无线模块。2MB Flash、256KB RAM嵌入式跑协议栈、留日志缓冲都宽裕。以太网 MACPHY 集成在片内做工业网关省一颗 PHY 芯片。UART 数量多一路打印调试一路接工控屏一路走 RS485完全转得开。TivaWare 库成熟SSI 驱动、中断、低功耗模式都有现成接口开发速度快。工业上选 MCU 还有个隐性要求长期供货和文档齐全。TM4C1299 属于 TI 主推的长生命周期工业产品线数据手册上千页勘误表和参考设计齐全这对产品生命周期动辄十多年的工业设备很重要。2. 硬件连接与板级设计注意事项2.1 MR25H40CDF的封装和引脚MR25H40CDF 是 8 脚封装外形跟一颗小 SOP8/DFN 差不多引脚定义遵循 SPI NOR Flash 兼容的排布思路基本可以理解为 CS、CLK、SI、SO、VDD、GND部分型号还带 WP写保护和 HOLD暂停传输脚。由于引脚数量少手工焊接和贴片都比较友好非常适合作为主控板上的一个“小附件”出现。有一点千万别想当然不同后缀的 Everspin 型号引脚定义可能略有差异。拿到样片第一件事就是翻对应型号的数据手册确认引脚顺序我见过不止一个工程师把 SO 和 SI 接反调了半天最后才发现是引脚图看错。2.2 硬件连接表SSI0接口TM4C1299KCZAD 有多个 SSI 外设我习惯把 MRAM 挂在 SSI0 上因为 SSI0 在不少默认引脚映射上跟调试下载器不冲突布线也方便。这里给出一个通用连接示意具体引脚号以你自己板子的 PinMux 分配为准MR25H40CDF 引脚功能连接目标CS片选MCU 的 GPIO手动控制CLK时钟MCU 的 SSI0CLKSI主发从收MCU 的 SSI0TXSO从发主收MCU 的 SSI0RXWP写保护接 3.3V禁用硬件写保护HOLD传输暂停接 3.3V避免偶发进入暂停状态VDD电源3.3V就近加 0.1uF 去耦电容GND地公共地我强烈建议不用 SSI 硬件自带的三线片选而是把 CS 配成普通 GPIO 手动拉低拉高。原因后面代码部分会说手动控制 CS 才能保证 SPI 事务完整性尤其是“写使能”和“单字节读状态”这类多步骤操作硬件自动 CS 很容易在时序上出幺蛾子。2.3 上拉、去耦和PCB布局工程上最容易翻车的是 WP 和 HOLD 两个引脚悬空。WP 悬空可能导致写操作被意外禁止HOLD 悬空在干扰大的现场会随机暂停 SPI 传输表现出来就是“数据偶发错位”。最简单可靠的做法就是两个引脚各自用一个 10K 电阻上拉到 3.3V。CS 建议也加 10K 上拉避免上电过程中浮空导致误选中。VDD 旁边用 0.1uF 高频电容加 4.7uF~10uF 钽电容双级去耦电源引脚要先经过电容再到芯片。PCB 布局上SPI 信号线尽量短控制在 3cm 以内最好不要飞线。CLK 和 SI 之间避免平行长距离走线防止串扰。这两条线可以串联一个 22Ω 左右的电阻匹配振铃对高速下信号质量有明显改善。如果板子空间实在紧张至少保证 MRAM 的地引脚有完整地平面别在中间被别的信号断开。3. 软件驱动设计与读写实现步骤3.1 TivaWare SSI初始化TM4C1299 的 SSI 外设初始化用 TivaWare 很简单但有几个细节要注意。先看代码#include stdint.h #include inc/hw_memmap.h #include driverlib/sysctl.h #include driverlib/gpio.h #include driverlib/pin_map.h #include driverlib/ssi.h #define MRAM_CS_PORT GPIO_PORTA_BASE #define MRAM_CS_PIN GPIO_PIN_3 #define MRAM_CS_LOW() GPIOPinWrite(MRAM_CS_PORT, MRAM_CS_PIN, 0) #define MRAM_CS_HIGH() GPIOPinWrite(MRAM_CS_PORT, MRAM_CS_PIN, MRAM_CS_PIN) void MRAM_Init(void) { // 使能 SSI0 和 GPIOA 外设时钟 SysCtlPeripheralEnable(SYSCTL_PERIPH_SSI0); SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOA); // 引脚复用示意写法具体复用号以 PinMux 工具为准 GPIOPinConfigure(GPIO_PA2_SSI0CLK); GPIOPinConfigure(GPIO_PA5_SSI0TX); GPIOPinConfigure(GPIO_PA4_SSI0RX); // 时钟、发送、接收引脚配置为 SSI 功能 GPIOPinTypeSSI(GPIO_PORTA_BASE, GPIO_PIN_2 | GPIO_PIN_4 | GPIO_PIN_5); // CS 作为普通 GPIO 输出手动控制 GPIOPinTypeGPIOOutput(MRAM_CS_PORT, MRAM_CS_PIN); MRAM_CS_HIGH(); // 配置 SSI0 为 SPI 主模式Motorola 格式先跑 1MHz 调试 SSIConfigSetExpClk(SSI0_BASE, SysCtlClockGet(), SSI_FRF_MOTO_FORMAT, SSI_MODE_MASTER, 1000000, 8); SSIEnable(SSI0_BASE); }这里我的建议是刚调通时先跑 1MHz确认读写正常后再逐步提高时钟。TivaWare 的 SSIConfigSetExpClk 默认配置是 SPI 模式 0SPO0、SPH0正好匹配 MR25H40CDF 支持的模式 0。模式 3 也支持但没必要追模式 0 在所有 SPI 设备上兼容性最好。3.2 核心读写驱动WREN / READ / WRITEMR25H40CDF 的 SPI 指令集参照了行业通用的 SPI NOR Flash 思路核心就四条WREN写使能、READ读数据、WRITE写数据、RDSR读状态寄存器。关键点在于每次写操作前必须发 WREN否则数据写不进去——这也是 SPI 存储器件共有的保护机制。写使能是一个独立的事务发完指令、CS 要重新拉高一次才能生效。这就是我为什么坚持手动控制 CS硬件自动片选难以保证这种“命令事务结束、片选释放”的语义。先写一个底层字节收发函数static uint8_t SPI_Transfer(uint8_t data) { uint32_t rx; SSIDataPut(SSI0_BASE, data); while (SSIBusy(SSI0_BASE)) {} SSIDataGet(SSI0_BASE, rx); return (uint8_t)rx; }然后是驱动核心部分。这里要特别说明一个细节不同 Everspin SPI MRAM 的地址字节数并不完全一样有些型号按 16 位地址编址容量大的型号会用 24 位地址。我这套框架按 24 位地址写移植到具体芯片时如果手册明确写的是 2 字节地址把发送中间那个地址字节的语句删掉即可。驱动移植第一件事就是确认地址位宽。#define MRAM_CMD_WREN 0x06 #define MRAM_CMD_WRITE 0x02 #define MRAM_CMD_READ 0x03 #define MRAM_CMD_RDSR 0x05 static void MRAM_WriteEnable(void) { MRAM_CS_LOW(); SPI_Transfer(MRAM_CMD_WREN); MRAM_CS_HIGH(); } static void MRAM_ReadBytes(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LOW(); SPI_Transfer(MRAM_CMD_READ); SPI_Transfer((addr 16) 0xFF); SPI_Transfer((addr 8) 0xFF); SPI_Transfer(addr 0xFF); while (len--) { *buf SPI_Transfer(0x00); } MRAM_CS_HIGH(); } static void MRAM_WriteBytes(uint32_t addr, const uint8_t *buf, uint32_t len) { MRAM_WriteEnable(); MRAM_CS_LOW(); SPI_Transfer(MRAM_CMD_WRITE); SPI_Transfer((addr 16) 0xFF); SPI_Transfer((addr 8) 0xFF); SPI_Transfer(addr 0xFF); while (len--) { SPI_Transfer(*buf); } MRAM_CS_HIGH(); }这套驱动框架最大的好处是简单直接。因为没有擦除环节写数据就是一个连续 SPI 事务不需要等待内部编程完成写完立刻能读出来校验。这跟 Flash 驱动完全是两种写法千万别把 Flash 那套“发完写命令再等 BUSY”的习惯带进来。3.3 存储布局与参数结构设计MRAM 虽然有 512KB但用起来不能像一块大草稿纸一样乱写。我一般的划分方式是头部参数区、运行日志区、固件暂存区。这里给出一个参考布局区域地址范围大小用途参数区0x000000~0x0000FF256B设备配置、校准系数日志区0x000800~0x0107FF64KB循环覆盖的运行日志固件暂存区0x010800~0x0707FF384KBOTA升级包暂存保留区0x070800~0x07FFFF30KB后续扩展参数区虽然只有 256B但数据组织上要付出和写代码一样的心思。建议所有参数打包成一个结构体结构体头部放 magic 和版本号尾部放 CRC 校验写的时候整块写读的时候先验 magic、版本、CRC 再使用。这样即使在写入中途掉电下次上电也能通过校验发现数据不完整。日志区用“日志槽 滚动指针”的方式每个日志槽 256B槽头放序列号和 CRC上电时扫描最后一个连续有效的槽位作为最新日志。MRAM 不需要磨损均衡这个循环日志算法比在 Flash 上跑简单一个量级。4. 掉电保护与数据一致性工程化4.1 多字节记录的事务提交机制MRAM 的写操作本身是字节级的也就是说单字节写入不会出现“写了一半”的状态。但应用层的一整条记录往往不止一个字节比如一条带时间戳的报警记录可能是十几个字节如果在写第 7 个字节时掉电这条记录既不完整也没有旧值。解决思路跟数据库事务是一样的引入提交标志。具体做法是给每条记录加上一个状态字节写数据前先写状态字节为 PENDING0x5A写数据体全部写完后将状态字节改为 COMMIT0xA5。读取的时候如果发现状态字节既不是 PENDING 也不是 COMMIT或者数据体 CRC 不匹配就认为这条记录未完成直接跳过或回滚到上一份。状态字节本身也是单字节写事务所以它是原子的。这个机制在 MRAM 上实现极其轻量因为不存在 Flash 的擦除延迟写状态字节的开销几乎可以忽略。4.2 CRC校验具体实现CRC 在嵌入式存储里是刚需。SPI 线路上的干扰、MCU 代码 bug、写入时序异常都可能让 MRAM 里落下一个坏数据。MRAM 介质本身可靠但接口链路不可靠CRC 就是最后一道防线。实现一个常见的 CRC16-Modbus代码很短uint16_t CRC16_Modbus(const uint8_t *data, uint32_t len) { uint16_t crc 0xFFFF; for (uint32_t i 0; i len; i) { crc ^ data[i]; for (uint8_t j 0; j 8; j) { if (crc 1) { crc (crc 1) ^ 0xA001; } else { crc 1; } } } return crc; }参数区写入流程建议是填充结构体、计算 CRC 填入尾部、调用 MRAM_WriteBytes 整块写入。读出后先算 CRC不一致就到备用区去读。参数区我一般做两个副本A/B 双备份版本号大的副本优先使用。这套 A/B 加 CRC 的方式在工业现场跑两三年很少出幺蛾子。4.3 掉电瞬间的最后一次写入工业设备最常见的需求之一就是掉电瞬间把当前状态、时间戳、累计数值保存下来。MRAM 的价值在这个场景发挥得最彻底——写入只是几微秒的 SPI 事务不需要等 Flash 内部擦写几乎一定能在电源耗尽前完成。掉电检测我推荐两种方式一是单纯依赖 MCU 的欠压复位BOR在 BOR 中断里做紧急保存。TM4C1299 的 BOR 触发后内核电压还能维持几毫秒足够写完几十个字节。注意紧急保存的代码要精简禁用中断嵌套关闭外设时钟降低功耗确保在电压崩掉之前完成。二是更可靠的硬件方案用带 PGOOD 输出的电源管理芯片或外部电压比较器检测到主电源跌落就拉 GPIO 下降沿在 GPIO 中断里做保存。这种方式比 BOR 早几百微秒时间窗口更充裕。这里有个最重要的经验掉电紧急保存的代码路径要跟正常运行路径分开。千万别在中断里调用带 malloc、带 printf、带互斥锁的函数一定要用一段无锁、无阻塞、不带大循环的直写函数把关键数据写完就死等复位。5. 实测问题与排查经验5.1 写不进去的常见原因最典型的故障就是“读出来全是 0xFF写什么都没反应”。按我的经验90% 是下面三个原因之一写操作前没发 WREN。SPI MRAM/Flash 的写保护是默认生效的发 WREN 是一个独立事务CS 必须拉高一次再拉低。偷懒把 WREN 和 WRITE 放进同一个 CS 低电平事务里写操作肯定被忽略。SPI 模式不对。模式 0 和模式 3 在 SCLK 空闲电平和采样沿上正好相反配置反了读数据也可能偶尔正常但写数据大概率错乱。用示波器量一下 CLK 空闲电平是最快的判断方法。WP 引脚悬空或拉低。WP 被拉低时芯片的块保护生效写指令直接被忽略而且读写寄存器都可能显示正常非常有迷惑性。排查的时候先用万用表量 WP 电平。排查手段上示波器比逻辑分析仪更直接。抓 CS、CLK、SI 三根线看 CS 低电平期内 CLK 是否连续、SI 上的字节先后顺序是否符合预期。命令字节、地址字节、数据字节围起来看问题基本一眼就漏不了。5.2 高速读取数据错乱MR25H40CDF 标称 SPI 时钟能到 40MHz但实际在主控板上跑出来往往到不了这个数。我实测下来短线、布局好的板子 20MHz 很稳30MHz 以上对走线质量和驱动能力就非常敏感了。SI/SO 边沿变缓、振铃严重时读回来的数据会在某个 bit 上错位输出就是字节乱序或 CRC 不过。处理方式按优先级排把 SSI 时钟降到 10MHz 或 20MHz看问题是否消失。CLK 和 SI 各串 22Ω 电阻抑制过冲振铃。确认从 MCU 到 MRAM 的走线长度短且等长特别别让 CLK 绕大圈。用示波器实际测量 CLK 上升沿和 SI 建立时间确认满足数据手册时序。说到底工业现场跑的稳定性比峰值速度值钱。10MHz 的 SPI 读 512KB 也就 0.4 秒左右绝大多数场景完全够用没必要为了一点速度去挑战信号完整性。5.3 把MRAM当Flash用的几个误区从 Flash 切换过来的工程师最容易带过来三个惯性操作全部要改写之前擦除。MRAM 没有擦除的概念直接写新值就会覆盖旧值写入和 RAM 一样是覆盖写。多调用一次擦除指令反而可能因为指令不受支持而把总线搞乱。按页编程、等待 BUSY。MRAM 写一个字节和写一串字节的时间差就是 SPI 传输本身的时间没有内部编程阶段所以不需要轮询状态寄存器。这个特性让 MRAM 在实时性要求高的场景非常加分。主循环里做磨损均衡。MRAM 硬件级别的读写寿命足够长应用层完全不需要维护写入次数统计、磨损均衡、坏块管理。日志区实现一个简单的环形覆盖就行。简单说MRAM 用起来更像一颗“掉电不丢失的 SRAM”而不是一颗“不用擦除的 Flash”。写代码的时候保持这个心智模型很多设计自然而然就简单了。6. 这套方案还能怎么扩展个人玩下来觉得还有几个很有意思的扩展方向。一个是在日志区上挂 FATFS 文件系统。MRAM 的随机写特性让 FATFS 的目录项更新、FAT 表更新都非常顺滑不像 Flash 那样担心频繁写坏 FAT 表。配合 TM4C1299 的以太网接口可以把日志文件通过 FTP 或者 HTTP 接口远程导出工控现场的“黑匣子”就这么搭出来了。另一个是把 MRAM 里的数据通过 Modbus 暴露出来。TM4C1299 有多个串口跑一个 Modbus-Slave 协议栈主机随时读 MRAM 里的参数和日志数据现场调试就不用开壳连调试器了一个 485 转 USB 串口就能搞定。最后再说一个工程上的小技巧新板子贴回来先写一个全地址的回环测试程序对整个 512KB 做“写 0x55、读校验、写 0xAA、读校验”两轮扫描。虽然 MRAM 不像 Flash 会有坏块问题但焊接问题、虚焊、信号线接反这种测试能十分钟内暴露出来比到时候跑到现场再排查节约不知道多少时间。我每批板子都是这么验的强烈建议你也这么做。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进