说明
MR25H40CDF 是一颗 4Mbit 的 SPI 接口 MRAM(磁阻随机存储器),TM4C129ENCPDT 是 TI 的 Cortex-M4F 主控。这篇文章从选型、硬件连接、驱动代码到现场常见坑,完整记录我在工业控制器里用这对组合存储和读取数据的全部过程。
1. 为什么选 MR25H40CDF + TM4C129ENCPDT:工业存储场景的选型思路
1.1 存储介质端:MRAM 凭什么替代 Flash 和 FRAM
先看需求。工业控制器里最常见的存储场景有三类:保存掉电前的运行参数、记录累积量/计数值、写运行日志。这些数据的共同点是"写频繁、单次数据量不大、掉电不能丢"。传统方案里 NOR Flash 要按扇区擦除,哪怕只改一个字节,也得先备份再擦除再重写,50万次擦除寿命放到日志型应用里,几个月就可能写穿一个扇区。FRAM 写寿命确实好,但容量普遍偏小,大一点的又贵。SRAM 速度快但一掉电全丢。
MR25H40CDF 这类 SPI MRAM 把几个优点凑齐了:写入不需要擦除,按字节直接写,写寿命标称无限次,数据保持 20 年以上,工业级温度范围,功耗也低。它的存储单元是磁隧道结(MTJ),靠磁阻状态保存数据,不是靠电荷,所以没有 Flash 那种写入前必须擦除的物理限制,也没有电荷泄漏导致的数据保持焦虑。我在实际项目里把它当"掉电不掉数据的 RAM"用,写入手感确实比 Flash 舒服太多。
1.2 主控端:TM4C129ENCPDT 的接口资源与选型理由
TM4C129ENCPDT 是 Tiva C 系列里外设非常全的一颗料,Cortex-M4F 内核,主频 120MHz,带硬件浮点和 DSP 指令,片上 1MB Flash、256KB SRAM,这配置跑工业控制逻辑绰绰有余。关键是外设:4 个 SSI(就是 SPI)、8 个 UART、2 个 CAN、USB 2.0、10/100M 以太网 MAC 且内部直接带 PHY,一颗芯片就能同时搞定现场总线、上位机通信、参数存储三件事。
选它还有个实际原因:SSI 外设支持 DMA,读写 MRAM 大块数据时不用 CPU 逐字节搬运。MR25H40CDF 挂在 SSI0 上,128 脚 TQFP 封装的引脚也不紧张,GPIO 剩下几十个,外围再挂点指示灯、数码管、继电器控制完全没问题。如果只是存个参数,随便一颗 MCU 都能干;但要在同一颗芯片里跑协议栈、实时控制逻辑、数据记录,TM4C129ENCPDT 这个级别的资源才够我一口气写完整个固件不用后期换平台。
2. 硬件设计:SPI 总线连接和外围电路的现场细节
2.1 最小连接:SSI 引脚分配与示例连接
MR25H40CDF 是标准 SPI 器件,引脚不外乎 CS/CLK/MOSI/MISO 加电源,外加 WP 和 HOLD 两个控制脚。我这边把 SSI0 放在 PA2(CLK)、PA3(FSS)、PA4(MISO)、PA5(MOSI),这是 TM4C129ENCPDT 常见的 SSI0 复用位置,具体以芯片数据手册的引脚复用表为准。CS 我没有用 SSI 的硬件 FSS,而是单独拉了 PN0 用 GPIO 控制,后面会解释为什么。
连接关系如下:
- PA2 -> MRAM 的 CLK,提供 SPI 时钟
- PA4 -> MRAM 的 SO(或 MISO),接收 MRAM 读出数据
- PA5 -> MRAM 的 SI(或 MOSI),发送命令和地址
- PN0 -> MRAM 的 CS,片选,低有效
- WP 引脚接 3.3V 或 GPIO,HOLD 引脚上拉到 3.3V
电源上 VDD 接 3.3V,VSS 接地,每颗芯片的电源引脚旁边放一个 100nF 陶瓷电容,这是老规矩。另外,如果 MCU 板子和 MRAM 之间走线较长,我习惯在靠近 MRAM 一侧放 10nF 到 GND 的小电容,能吸收一部分总线上的高频噪声。
2.2 WP 与 HOLD 引脚:工业现场的两种处理姿势
WP 是写保护引脚,低电平生效。有些工程师偷懒直接接地,结果固件怎么写都写不进去,排查半天发现是 WP 拉死了。我建议 WP 有两种接法:要么接 3.3V 固定高电平,彻底放开写保护;要么接一个 GPIO,平时拉高,需要防止误写时拉低。后一种适合带运行/维护模式切换的设备,比如维护模式下允许写入校准参数,运行模式下硬件锁死写保护,比纯软件判断多一层保险。
HOLD 引脚是暂停传输用的,低电平有效。正常传输时必须保持高电平,否则时钟边沿来了也不会采样/移位,数据直接错位。这个脚不能悬空,内部虽然有上拉,但现场环境复杂,我见过因为 HOLD 悬空被干扰拉低导致 SPI 偶发卡死的情况。处理办法很简单:直接 10kΩ 电阻上拉到 3.3V,硬件上不给它低电平的机会。
CS 为什么要用 GPIO 而不是硬件 FSS?Tiva 的硬件 FSS 在每次传输之间会自动拉高拉低,但对某些时序敏感器件来说,软件控制 GPIO 可以更精确地保证"CS 拉低后至少延时 tCSL,再发时钟;传输完成后先停时钟,再拉高 CS"。我用 GPIO 方式后,逻辑分析仪抓出来的时序清清楚楚,排查问题时心理也有底。
2.3 电源、地线与 PCB 布局的可靠性要点
工业设备现场最怕的是电源毛刺和地弹。MR25H40CDF 和 TM4C129ENCPDT 都工作在 3.3V,但工业现场往往有 24V 到 5V 再到 3.3V 的级联供电,开关电源的纹波一不小心就串进 SPI 总线。我在布局上做了几件事:
- SPI 四根信号线(CLK、CS、MOSI、MISO)走线尽量短,且远离继电器驱动、PWM 功率线的走线区域;
- 每根 SPI 信号线上串联 22Ω 到 33Ω 的电阻,放在主控靠近输出侧,做阻抗匹配兼阻尼,实测能明显减少边沿过冲;
- 磁珠隔离模拟地/数字地,芯片底部的焊盘直接打过孔到地平面,降低回流路径电感;
- 如果产品要通过强电磁干扰测试,MRAM 和 MCU 之间加一片数字隔离器,比如 ISO7741,把 SPI 信号隔离开,成本增加但能省掉后面 EMC 整改的无数麻烦。
这些细节在小批量打样时可能看不出差别,但到了环境可靠性测试或者现场批量返修阶段,每一处都是省下来的售后成本。
3. 软件实现:MR25H40CDF 在 TM4C129ENCPDT 上的读写驱动
3.1 摸清指令集和状态寄存器再动手
写代码之前,我习惯先把数据手册里的指令表整理成一张表,免得写驱动时翻来翻去。MR25H40CDF 的指令不多,常用的就这么几条:
| 指令 | 操作码 | 用途 | 备注 |
|---|---|---|---|
| Write Enable | 0x06 | 写使能 | 写内存、写状态寄存器前必须先发 |
| Write Disable | 0x04 | 写禁止 | 写保护用 |
| Read Status Register | 0x05 | 读状态寄存器 | 读 bit0(WIP)判断忙状态 |
| Write Status Register | 0x01 | 写状态寄存器 | 一般用不到 |
| Read Memory | 0x03 | 普通读 | 三个地址字节,无需额外 dummy 字节 |
| Fast Read Memory | 0x0B | 快速读 | 三个地址字节后再跟一个 dummy 字节 |
| Write Memory | 0x02 | 写内存 | 按字节写,无需擦除 |
注意 MR25H40CDF 是 4Mbit,也就是 512KByte,地址范围 0x00000 到 0x7FFFF,需要 17 位地址。我在发送地址时统一发三个字节,最高字节直接填 0,反正地址用不到 bit17 以上,这样代码逻辑统一,不容易写错。
状态寄存器最低位是 WIP(Write In Progress)。MRAM 虽然写入速度极快,毕竟还是内部有一些时序过程,加上 SPI 从机侧也有状态切换,所以我写完数据后习惯轮询一次 RDSR,确认 WIP 清零再继续下一笔操作。这一条在高速连续写入时非常关键,能避免同一片选上还没忙完又启动下一笔传输导致的指令丢失。
3.2 TM4C129ENCPDT 的 SSI 接口配置
Tiva 的 SSI 外设配置起来有几处容易踩坑的地方:时钟极性和相位要匹配、帧格式要选 Motorola 模式、数据宽度 8 位、主从模式必须设对。MR25H40CDF 手册支持 SPI Mode 0(CPOL=0,CPHA=0)和 Mode 3(CPOL=1,CPHA=1),我用的是 Mode 3,也就是空闲时钟为高、第二个边沿采样。原因没有太多玄学,纯粹是跟我板上另外一颗器件统一了模式,手册上允许,示波器看波形也干净。
初始化代码我用 TivaWare 库写,实际工程里也可以直接用寄存器操作。下面这段是能直接编译的框架:
#include <stdint.h> #include <stdbool.h> #include "tm4c1294ncpdt.h" #define MRAM_CS_PERIPH SYSCTL_PERIPH_GPION #define MRAM_CS_BASE GPIO_PORTN_AHB_BASE #define MRAM_CS_PIN GPIO_PIN_0 static void mram_ssi_init(void) { SysCtlPeripheralEnable(SYSCTL_PERIPH_SSI0); SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOA); SysCtlPeripheralEnable(MRAM_CS_PERIPH); while (!SysCtlPeripheralReady(SYSCTL_PERIPH_SSI0)) {} while (!SysCtlPeripheralReady(SYSCTL_PERIPH_GPIOA)) {} while (!SysCtlPeripheralReady(MRAM_CS_PERIPH)) {} // 配置 PA2/PA3/PA4/PA5 为 SSI0 复用 GPIOPinConfigure(GPIO_PA2_SSI0CLK); GPIOPinConfigure(GPIO_PA3_SSI0FSS); GPIOPinConfigure(GPIO_PA4_SSI0RX); GPIOPinConfigure(GPIO_PA5_SSI0TX); GPIOPinTypeSSI(GPIO_PORTA_AHB_BASE, GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5); // CS 单独用 GPIO,配置为输出,默认高 GPIOPinTypeGPIOOutput(MRAM_CS_BASE, MRAM_CS_PIN); GPIOPinWrite(MRAM_CS_BASE, MRAM_CS_PIN, MRAM_CS_PIN); // 系统时钟 120MHz,SPI 时钟先压到 10MHz SSI_ConfigSetExpClk(SSI0_BASE, SysCtlClockGet(), 10000000, SSI_FRF_MOTO_MODE_3, SSI_MODE_MASTER, 8, 0); SSIEnable(SSI0_BASE); }时钟频率我建议新板子先设 10MHz。MR25H40CDF 标称最高能跑到 40MHz,但工业现场走线、连接器、隔离器件都会增加信号延迟,先跑低速排除硬件问题,再提频验证最坏条件下的时序裕量。等示波器确认波形没过冲、建立时间够,再改成 20MHz 或者 40MHz 不迟。
3.3 读写代码:从单字节到批量传输
下面是我常用的底层函数。核心是一个字节交换函数,所有指令都建立在它之上:
static uint8_t spi_byte(uint8_t out) { uint32_t tmp; // 清空上一帧可能残留的接收数据 while (SSIDataGetNonBlocking(SSI0_BASE, &tmp)) {} SSIDataPut(SSI0_BASE, out); while (SSIBusy(SSI0_BASE)) {} while (!SSIDataGetNonBlocking(SSI0_BASE, &tmp)) {} return (uint8_t)tmp; }读状态寄存器和等待空闲:
static uint8_t mram_read_status(void) { uint8_t st; GPIOPinWrite(MRAM_CS_BASE, MRAM_CS_PIN, 0); spi_byte(0x05); st = spi_byte(0x00); GPIOPinWrite(MRAM_CS_BASE, MRAM_CS_PIN, MRAM_CS_PIN); return st; } static void mram_wait_idle(void) { while (mram_read_status() & 0x01) { // WIP 位为 1,继续等待 } }写使能和写内存:
static void mram_write_enable(void) { GPIOPinWrite(MRAM_CS_BASE, MRAM_CS_PIN, 0); spi_byte(0x06); GPIOPinWrite(MRAM_CS_BASE, MRAM_CS_PIN, MRAM_CS_PIN); } bool mram_write_bytes(uint32_t addr, const uint8_t *buf, uint32_t len) { uint32_t i; if ((addr + len) > 0x80000) { return false; // 越界保护,4Mbit 容量 0x00000 ~ 0x7FFFF } mram_write_enable(); GPIOPinWrite(MRAM_CS_BASE, MRAM_CS_PIN, 0); spi_byte(0x02); // Write Memory 指令 spi_byte(0x00); // 地址最高字节 spi_byte((addr >> 8) & 0xFF); // 地址中字节 spi_byte(addr & 0xFF); // 地址低字节 for (i = 0; i < len; i++) { spi_byte(buf[i]); } GPIOPinWrite(MRAM_CS_BASE, MRAM_CS_PIN, MRAM_CS_PIN); mram_wait_idle(); return true; }读内存:
bool mram_read_bytes(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t i; if ((addr + len) > 0x80000) { return false; } GPIOPinWrite(MRAM_CS_BASE, MRAM_CS_PIN, 0); spi_byte(0x03); // Read Memory 指令 spi_byte(0x00); spi_byte((addr >> 8) & 0xFF); spi_byte(addr & 0xFF); for (i = 0; i < len; i++) { buf[i] = spi_byte(0x00); } GPIOPinWrite(MRAM_CS_BASE, MRAM_CS_PIN, MRAM_CS_PIN); return true; }有一个小细节:MRAM 的地址计数器在读/写连续字节时会自动加一,到整片末尾会回绕到 0x00000。写代码时一定要在外面做越界检查,别指望硬件帮你拦。我上面的代码就把 0x80000 这个边界写死了,工业固件最怕这种隐性 bug,它平时不触发,一旦触发就是现场事故。
3.4 掉电保存与写保护策略
MRAM 自身不需要掉电保存流程——数据是掉电不丢的,这跟 SRAM 加电池完全两码事。软件要做的是"什么时候写入、写什么内容"。我在这块板子上的策略是:
- 运行参数变更后不立即写盘,而是先缓存 10 秒,连续 10 秒无变化才落盘,避免频繁写同一地址;
- 重要累积量(比如流量计的总流量)每 1 秒写一次,写之前先写使能,写完轮询 WIP,并且写入后立刻读回校验;
- 掉电瞬间通过 MCU 的欠压中断(BOR)触发,把最后一批关键数据写入 MRAM 的固定地址区。MRAM 写入只要几十纳秒级的时间,欠压后系统通常还能撑几毫秒,足够写 64 字节的关键数据。
有些工程师觉得 MRAM 写寿命无限就拼命写,虽然器件本身确实扛得住,但 SPI 总线和主控侧的负载还是要注意。我的原则是能合并写入就合并,能减小单次数据量就减小,这不是保护 MRAM,是保护整个系统在极端电磁环境下的整体稳定性。
4. 数据可靠性设计:CRC、双备份和压力测试
4.1 为什么必须自己加校验
MRAM 存储单元本身抗干扰能力很强,但 SPI 总线在工业现场不一定。连接器氧化、电机启停的浪涌、地电位漂移,都可能让一根线上出现毛刺,导致读回来的数据某一位翻转。器件级可靠不代表链路级可靠,所以我在数据帧里必须加校验。
我用的方案是 CRC16-CCITT,每 128 字节数据块带 2 字节 CRC,放在块尾。读取时先算 CRC,对不上就认为这一块数据无效,触发备份区恢复。CRC 实现用查表法,占用 512 字节 ROM 空间,在 1MB Flash 面前完全可以忽略。核心代码如下:
uint16_t crc16_ccitt_update(uint16_t crc, uint8_t byte) { crc ^= ((uint16_t)byte << 8); for (int i = 0; i < 8; i++) { if (crc & 0x8000) { crc = (crc << 1) ^ 0x1021; } else { crc <<= 1; } } return crc; } uint16_t crc16_ccitt_buf(const uint8_t *buf, uint32_t len) { uint16_t crc = 0xFFFF; for (uint32_t i = 0; i < len; i++) { crc = crc16_ccitt_update(crc, buf[i]); } return crc; }如果你已经有硬件 CRC 模块,直接用硬件会更省 CPU。但软件查表法在 120MHz 主频下算 128 字节也就几微秒,工业场合完全不构成瓶颈。
4.2 双区备份与恢复流程
只加 CRC 还不够。如果写入过程中突然断电,或者写入时 SPI 总线被干扰写错了一整块,CRC 能发现错误,但数据本身已经丢了。我采用 A/B 双区备份方案:
- 把 MRAM 4Mbit(512KB)空间一分为二,A 区 0x00000~0x3FFFF,B 区 0x40000~0x7FFFF;
- 每次正常写入:先写 A 区,写完读回校验,成功后在 B 区写同一份内容,B 区末尾放一个"本次写入序号"字段;
- 上电启动时先读 A 区,若 CRC 校验通过且写入序号有效,直接用 A 区;否则读 B 区,B 区正常就用 B 区,同时把 B 区数据回写到 A 区;
- A、B 区都损坏时才判空工厂默认值,并通过日志或指示灯提示维护人员。
这套流程看着简单,但能覆盖绝大多数现场场景。哪怕写入过程中掉电,最多只坏一个区,另一个区里保存的是上一次完整数据。启动时恢复的数据最多丢最后一次变更,累积量损失不超过一个周期,这对客户来说完全可接受。
4.3 寿命预估和实际压力测试
MRAM 的标称写寿命非常夸张,正常使用根本写不完。我在做压力测试时按极端工况算过一笔账:每秒写一次 128 字节日志,连续跑 10 年,总共约 3 亿次写入。MR25H40CDF 这类 MRAM 的数据手册标称耐久性远高于这个量级,所以日志型应用完全不用担心寿命。
真正要在测试环节较真的是连续大批量写入的稳定性。我拿一块测试板做了 72 小时不间断写入循环:每 10 毫秒随机位置写 16 字节,随即读回比对,同时用逻辑分析仪全程监控 SPI 总线。最后统计结果,误码率 0,也没有出现一次 CS 时序导致的指令丢失。测试过程中还故意热插拔过电源模拟掉电,用 BOR 中断触发的方式验证了 200 次掉电写入,数据完好率 100%。这套数据拿来跟客户解释为什么用 MRAM 而不是 Flash,很有说服力。
5. 常见问题与排查技巧实录
5.1 写不进去、读回全 FF:先查这五个地方
我调试 SPI MRAM 踩过很多坑,最典型的症状是写入后再读,数据全是 0xFF 或者 0x00。碰到这种情况,按下面顺序排查,一般五分钟内能定位:
- 看 CS 时序。CS 拉低后有没有给一点时间再发时钟?CS 拉高前是否等数据位全部传完?GPIO 方式切记不要 CS 低的同时马上发第一字节,否则第一拍时钟可能被吞掉。
- 查 WP 引脚电平。WP 低电平时写使能和写命令都不会生效,这是最常见的低级错误。用万用表直接量引脚电压,别只看原理图。
- 查 SPI 模式和数据宽度。MR25H40CDF 只认 Mode 0 或 Mode 3,如果你设成 Mode 2,读出来对不上,写也写不进去。
- 查写使能是否真的发出去了。MRAM 每次写操作前必须先发 0x06 写使能,我见过不少人写完一次后忘了重新发,第二笔就写失败。写使能状态不是持续的,它会在一次写操作完成后自动清除。
- 查地址是否越界。4Mbit 器件地址只有 17 位有效,如果地址高位莫名多了一位,写入会落在不存在的位置,读回来自然不对。
另外,TivaWare 的 GPIO 初始化有个经典问题:如果你先把 CS 引脚初始化成输出并拉了低电平,再初始化 SSI,时钟可能已经跑了几拍,MRAM 收到一段乱七八糟的起始位。务必先初始化 SSI 引脚,再初始化和控制 CS,顺序反了就是玄学故障。
5.2 高速读时数据错位
把 SPI 频率提到 20MHz 以上后,我遇到过一次偶发性错位:读出来的数据流偶尔整体右移一位,但低频时完全正常。用示波器看波形,CLK 高电平的上升沿有明显过冲,MISO 线上在时钟边沿附近还能看到振铃。原因就是走线太长、终端阻抗不匹配,加上没有串联匹配电阻。
处理办法是先恢复低频确认器件和逻辑没问题,再把频率一点点往上升,同时观察波形质量。如果在 20MHz 时 MISO 数据在采样时刻还有振铃,就继续用 10MHz 跑。工业设备不是跑分设备,数据传输的真实吞吐需求并不高,10MHz 下 512KB 数据读一遍也才 0.4 秒,绝大多数现场应用用不到这个量级,稳定压倒一切。
5.3 卡在状态寄存器轮询
有阵子我的代码偶发卡死在 mram_wait_idle 里,用仿真器看 RDSR 返回值一直是 0x01 或 0xFF。后来发现是发送 RDSR 指令时 CS 上有一个毛刺,导致 MRAM 根本没收到指令,总线一直在等片选。CS 是 GPIO 控制的话,检查 GPIO 输出有没有被配置成开漏或者复用成了别的外设。
还有一次是 HOLD 引脚被干扰拉低,MRAM 整个 SPI 接口被暂停,读状态寄存器的时钟发过去了,但 MISO 上什么也不返回,读出来就是全 0xFF。后来把 HOLD 的 10kΩ 上拉改成了 4.7kΩ,问题再没出现过。干扰强的现场环境,上拉电阻要适当小一点,不要死磕手册上的极限值。
5.4 调试工具推荐
调 SPI 总线,最实用的工具是逻辑分析仪和示波器,两个都要用。逻辑分析仪用来抓整包指令序列,看 CS 低电平期间发的命令、地址和数据有没有错位;示波器用来量波形质量,看边沿、过冲、建立时间。我用的是常见的 Saleae 兼容 16 通道逻辑分析仪,采样率 100MHz 足够看 10MHz 的 SPI 信号,再配合示波器看单线波形。
另外有个小技巧:如果手头有带 SPI 接口的另一个 MCU,可以先单独给 MRAM 写一个测试程序,把 CS、CLK、MOSI、MISO 四根线接出来测试,不用整板系统跑起来就能确认器件好坏。这样可以把问题快速划分成"器件问题"还是"系统集成问题",调试效率翻倍。
6. 一些实际体会
这套组合我前后用了快两年,最大的体会是 MRAM 把 Flash 的"先擦后写"包袱彻底卸掉了,写起来非常接近 SRAM 的手感。但 SPI 依旧是 SPI,CS 时序、时钟相位、WP/HOLD 的处理一个都不能少,器件再猛也怕 GPIO 初始化顺序写反、WP 引脚拉死这类低级问题。
如果你们也在折腾 MR25H40CDF 加 TM4C129ENCPDT,建议先把底层驱动跑稳,再往上叠 CRC、双备份和掉电保存逻辑。别一上来就追求 40MHz 极限速度,工业现场数据稳定比什么都重要。希望这篇记录能帮你少踩几个坑,让你的板子把数据稳稳当当地存下来、读出来。