ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MRAM与TM4C129的SPI接口实战:工业数据掉电存储方案

MRAM与TM4C129的SPI接口实战:工业数据掉电存储方案 最近在调一套工业数据采集网关要用 TM4C129EKCPDT 这颗 ARM Cortex-M4F 单片机把现场设备的运行状态记录下来而且要保证设备突然断电之后数据不丢。当时手头有两块串行 NOR Flash但我犹豫了一下最后换成了 MR25H40CDF 这颗 4Mbit 的 SPI MRAM配合 TM4C129EKCPDT 的 SSI 外设把读写流程完整跑通了。这个组合非常适合需要频繁写小数据、掉电保存、又不想做 Flash 磨损均衡的工业嵌入式场景。本文就把这套 MR25H40CDF 与 TM4C129EKCPDT 的硬件连接、驱动代码和数据组织方法完整记录一下也给正在做类似项目的朋友一个可以直接参考的模板。1. 选型思路为什么是 MRAM 和 TM4C1291.1 工业数据存储到底难在哪工业设备里常见的存储需求有这么几类第一是设备参数比如 IP 地址、校准系数、量程上下限这类数据不常写但绝对不能丢第二是运行日志比如报警记录、开机时间、温度趋势这类数据会反复追加写入可能一天写几百次第三是掉电瞬间的关键数据比如设备正在执行什么动作、伺服当前位置、最后一条 Modbus 报文内容必须在系统断电前抢救下来。用普通 NOR Flash 做这些事会很别扭。NOR Flash 写数据之前必须先擦除而擦除的最小单位是扇区动不动就是 4KB 甚至 64KB。今天你要改一个 4 字节的参数逻辑上就得先把整个扇区搬出来擦掉改完再写回去。写入次数还受限制工业级 Flash 擦写寿命通常在 10 万次左右如果每秒钟记一条日志用不了多久就到寿命上限。所以传统做法是上外部 EEPROM、电池供电的 SRAM 或者带掉电检测的大电容又或者写复杂的磨损均衡和掉电恢复逻辑。MR25H40CDF 这颗 MRAM 直接把这些麻烦抹掉了。它不需要擦除写入就是覆盖也没有写入次数限制数据掉电不挥发。对我这种想把时间花在业务逻辑而不是 Flash 驱动上的开发来说简直是救命方案。1.2 MR25H40CDF 和传统 Flash 的本质区别MR25H40CDF 是 Everspin 的串行 SPI MRAM容量 4Mbit也就是 512KByte工作电压 2.7V 到 3.6V正好匹配 TM4C129EKCPDT 的 3.3V I/O。它和 Flash 最本质的区别是存储介质MRAM 用的是磁阻效应不是电荷存储所以理论上写入寿命接近无限不存在“擦除伤寿命”的问题。实际使用中我感受到的差异更直接写数据之前不需要发 Sector Erase 命令也不需要考虑页大小1 字节到几 KB 的数据都能直接写写完之后不需要等待擦写时间读回来的数据立刻就是新的。MRAM 的读时序和 SPI NOR Flash 非常像指令也是 READ 0x03、WRITE 0x02但整个逻辑简单了一个量级。这颗芯片支持 SPI Mode 0 和 Mode 3时钟频率最高 40MHz对 TM4C129 来说完全够用。工业现场一般不建议把 SPI 跑满我会留足裕量实际跑 10MHz后面的项目可以稳定复制。1.3 TM4C129EKCPDT 为什么适合干这个活TM4C129EKCPDT 是 TI 的 Tiva C 系列Cortex-M4F主频 120MHz有以太网 MACPHY还有丰富的 UART、CAN、EPI 接口。做工业协议网关时Modbus RTU 走 UARTOPC UA 或 MQTT 走以太网传感器和数控机床的数据从 CAN 或串口进来本地再落到 MRAM一颗芯片就全干了。它的 SSI 外设其实就是标准 SPI支持 8 位帧格式最高跑 25MHz 左右。对 MR25H40CDF 来说初始化一个主模式 SPI 即可。TivaWare 驱动库把寄存器封装得比较友好不需要像老式单片机那样手工操作一堆状态寄存器代码量可控而且整个工程用 TI 的编译器或者 GCC 都能跑。如果单纯为了存数据杀鸡用牛刀但如果你要做的是一台“能联网、能采集、能本地存储”的工业设备这两个器件放在一起很顺手。实际项目里我还在 TM4C129 上同时跑了 Modbus 主站和 MQTT 客户端数据采集线程把现场参数写入 MRAM断网时数据不丢恢复网络后再补传到服务器效果很理想。2. 硬件连接从引脚到电路的细节2.1 SSI 还是 GPIO片选怎么处理TM4C129 的 SSI 在硬件上有一个 FSS 引脚对应 SPI 的片选但这个引脚默认会自动翻转每传完一个字节就拉高一次再传下一个字节再拉低。对于 MRAM 这样的器件一次完整操作必须保证 CS 在整个命令和地址期间保持低电平比如 READ 指令要连续发送指令码、3 字节地址、才能开始接收数据期间 CS 不能断。所以我不建议直接用硬件 FSS 接 MRAM 的 CS。正确做法是把 FSS 所在引脚配置成通用 GPIO 输出用软件控制拉低拉高。这样牺牲一点自动化换来了完全可控的帧时序。实测下来这个选择很关键刚开始我用硬件 FSS发现读 512 字节时中间总是夹着一些毛躁数据后来改成 GPIO 控制一次读写冗长帧完全稳定。2.2 一张表讲清引脚映射我的连接方式是把 MRAM 挂在 TM4C129 的 SSI0 上引脚对应关系如下TM4C129EKCPDT 引脚复用功能MR25H40CDF 引脚说明PA2SSI0ClkCLKSPI 时钟10MHzPA5SSI0TxDI主机发送接 MRAM 数据输入PA4SSI0RxDO主机接收接 MRAM 数据输出PA3GPIO 输出/CS软件控制片选低有效3.3V电源VCC并联 100nF 10uF 电容GND地VSS同层铺地连接3.3V上拉/WP写保护拉高允许正常写3.3V上拉/HOLD保持引脚拉高禁用暂停功能MR25H40CDF 的 /WP 和 /HOLD 如果不接而悬空会因为引脚电平不确定导致偶发写保护或 SPI 暂停这个问题很容易被忽略。我直接在 PCB 上给这两个引脚放了 10kΩ 上拉电阻到 3.3V这样上电即进入可写、正常传输状态。如果你需要通过状态寄存器写保护位也可以把 /WP 接到 GPIO 上动态控制不过多数应用固定拉高就够了。2.3 电源、上拉和布局要注意什么MR25H40CDF 的 VCC 脚不要省去耦电容。我习惯在靠近 VCC 引脚的位置放一颗 100nF 的陶瓷电容再在稍远一点放一颗 10uF 钽电容或者 MLCC。MRAM 虽然不像射频芯片那么娇气但工业现场电源噪声大尤其是旁边有继电器、接触器频繁动作时电源毛刺直接进芯片大概率会造成偶发读写错误。还有一个布局细节CLK 信号尽量短MISO 和 MOSI 不要平行走太长。TM4C129 的 SSI0 引脚电平是 3.3VMRAM 也是 3.3V直接连接没问题。如果控制板和 MRAM 分板中间用排线连接我会在 CLK 和 MOSI 上串 22Ω 到 33Ω 的电阻抑制振铃MISO 上不用串避免降低信号上升沿。对于 10MHz 的 SPI这种处理足够稳妥跑 20MHz 也能工作。另外CS 上电阻要加上拉吗不接也行因为 GPIO 初始化时我先把输出拉高了。但是如果你用的是开漏输出或者引脚复用容易浮空加一颗 10kΩ 上拉更保险避免 MRAM 在 MCU 还没初始化时误触发命令。3. 驱动代码把 MR25H40CDF 跑起来的完整流程3.1 初始化 SSI0TM4C129 的 TivaWare 库初始化外设很简单但要注意顺序先使能外设时钟再配置引脚复用最后配置 SSI 参数。下面这段是我实际用的初始化代码主频通过 SysCtlClockSet 已经设置成 120MHzSSI 波特率目标 10MHz。#include stdint.h #include inc/hw_memmap.h #include driverlib/sysctl.h #include driverlib/gpio.h #include driverlib/ssi.h #include driverlib/pin_map.h #define MRAM_CS_PERIPH SYSCTL_PERIPH_GPIOA #define MRAM_CS_PORT GPIO_PORTA_BASE #define MRAM_CS_PIN GPIO_PIN_3 void MRAM_Init(void) { // 使能 SSI0 和 GPIOA 时钟 SysCtlPeripheralEnable(SYSCTL_PERIPH_SSI0); SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOA); while (!SysCtlPeripheralReady(SYSCTL_PERIPH_SSI0)) ; // PA2SSI0Clk, PA4SSI0Rx, PA5SSI0Tx GPIOPinConfigure(GPIO_PA2_SSI0CLK); GPIOPinConfigure(GPIO_PA4_SSI0RX); GPIOPinConfigure(GPIO_PA5_SSI0TX); GPIOPinTypeSSI(GPIO_PORTA_BASE, GPIO_PIN_2 | GPIO_PIN_4 | GPIO_PIN_5); // PA3 作为软件片选初始化为高电平 GPIOPinTypeGPIOOutput(GPIO_PORTA_BASE, MRAM_CS_PIN); GPIOPinWrite(GPIO_PORTA_BASE, MRAM_CS_PIN, MRAM_CS_PIN); // 主模式Motorola SPI Mode 08位帧10MHz SSIConfigSetExpClk(SSI0_BASE, SysCtlClockGet(), SSI_FRF_MOTO_MODE_0, SSI_MODE_MASTER, 10000000, 8); SSIEnable(SSI0_BASE); }这里用的SSI_FRF_MOTO_MODE_0对应 CPOL0、CPHA0也就是空闲时钟为低电平第二个边沿采样数据。MR25H40CDF 同时支持 Mode 0 和 Mode 3我选 Mode 0 只是因为和其它 SPI 器件能共用一套外设配置。3.2 指令集和读写实现MR25H40CDF 的基本操作和 SPI NOR Flash 很像最常用的指令有这么几条指令操作码功能WREN0x06设置写使能锁存READ0x03从指定地址读一个或多个字节WRITE0x02从指定地址写数据RDSR0x05读状态寄存器WRSR0x01写状态寄存器每次写操作之前必须先发 WREN否则 WRITE 命令会被忽略。读操作不需要写使能直接发命令即可。底层单字节收发我封装成一个函数用 SSIDataPut 发送然后等 SSIBusy 清除再用 SSIDataGet 取回数据。void MRAM_CS_Low(void) { GPIOPinWrite(MRAM_CS_PORT, MRAM_CS_PIN, 0); } void MRAM_CS_High(void) { GPIOPinWrite(MRAM_CS_PORT, MRAM_CS_PIN, MRAM_CS_PIN); } uint8_t MRAM_Transfer(uint8_t byte) { uint32_t rx 0; SSIDataPut(SSI0_BASE, byte); while (SSIBusy(SSI0_BASE)) ; SSIDataGet(SSI0_BASE, rx); return (uint8_t)rx; } void MRAM_WriteEnable(void) { MRAM_CS_Low(); MRAM_Transfer(0x06); MRAM_CS_High(); }需要特别注意的是TivaWare 的SSIDataPut在发送缓冲区满时可能会阻塞但 SSIBusy 循环等待的是硬件移位寄存器完成二者搭配没有问题。如果你一次要传输很多字节还可以用 FIFO 或 DMA但对于 512KB 的 MRAM单字节轮询已经够快没必要把代码复杂度提上去。READ 和 WRITE 操作都用 24 位地址分三字节发送高字节在前。MR25H40 容量只有 512KB所以本地地址最多 0x7FFFF但地址格式仍然是三字节。uint8_t MRAM_ReadByte(uint32_t addr) { uint8_t data; MRAM_CS_Low(); MRAM_Transfer(0x03); MRAM_Transfer((addr 16) 0xFF); MRAM_Transfer((addr 8) 0xFF); MRAM_Transfer(addr 0xFF); data MRAM_Transfer(0x00); MRAM_CS_High(); return data; } void MRAM_WriteByte(uint32_t addr, uint8_t data) { MRAM_WriteEnable(); MRAM_CS_Low(); MRAM_Transfer(0x02); MRAM_Transfer((addr 16) 0xFF); MRAM_Transfer((addr 8) 0xFF); MRAM_Transfer(addr 0xFF); MRAM_Transfer(data); MRAM_CS_High(); }连续读取或连续写入时可以把地址递增之后循环调用这些函数也可以直接在一个 CS 低电平周期内连续传输数据和地址。比如读一条 256 字节的日志我会在 MRAM_ReadByte 基础上封装一个MRAM_ReadBuffer(addr, buf, len)循环传输后面每一个字节函数内部只在第一次发 READ 指令和地址。3.3 数据存储格式与校验MRAM 虽然抗掉电但工业设备不只是怕掉电还怕总线干扰、程序跑飞、地址错乱。所以我在 MRAM 里存的数据从来不是裸数据而是带帧头、长度、CRC 校验的结构体。这样哪怕 MRAM 里出现了莫名其妙的乱码也能在软件层识别出来。以下是一个简单的参数存储区块#define APP_PARAM_MAGIC 0xA5A50001UL #define APP_PARAM_CRC_INIT 0xFFFF typedef struct { uint32_t magic; uint16_t len; uint16_t crc; uint8_t data[64]; } AppParamBlock; uint16_t App_CRC16(uint16_t crc, const uint8_t *buf, uint16_t len) { uint16_t i, bit; while (len--) { crc ^ *buf; for (i 0; i 8; i) { bit crc 1; crc 1; if (bit) crc ^ 0x8408; // CRC-16/CCITT 反向多项式 } } return crc; }写入参数时先填好 magic、len计算 data 区 CRC再调用 MRAM_WriteByte 按偏移写入整个结构体。读取时先检查 magic 是否匹配再重新计算 CRC 和存储的 CRC 比较不一致就判定数据无效自动恢复出厂默认参数。CRC 计算不要用标准库里的慢速查表法虽然表查法更快但在 TM4C129 上通常也没必要用大表占 Flash。按位计算加上循环64 字节数据计算一次也就几十微秒完全够用。如果你要记录的数据更多可以用 CRC32但 CR16 已经能覆盖绝大多数工业干扰场景。4. 掉电保护与工业生产可靠性4.1 为什么 MRAM 不怕掉电丢失MR25H40CDF 最大的价值是在断电瞬间不需要做复杂的“写保护”。NOR Flash 写入需要电荷泵产生高电压写一半断电就容易造成位翻转或者状态机卡死所以很多 Flash 产品要求电源电压低于阈值时禁止写入。MRAM 不是靠浮栅电荷存数据的磁阻状态不会因为电源中断就反转所以它天生具备掉电保持能力。但这不意味着可以直接把一块 MRAM 扔进工业设备就完事。系统掉电的时候 MCU 的电源电压在跌落如果此时正好有一笔 SPI 写操作进行到一半CS 可能会因为 GPIO 供电不稳变成低电平产生一次伪片选MRAM 收到半截命令后也可能进入未定义状态。好在 MRAM 的命令体系简单复位后重新初始化即可不需要担心扇区擦除中断导致坏块。4.2 掉电瞬间写操作怎么防护我实际采用的策略是在系统电源输入端加一颗电压监控芯片比如 TPS3839 或者 TLV809监控 3.3V。当电压掉到 2.93V 以下时监控引脚产生中断TM4C129 的中断服务程序立刻完成当前最后一次数据写入然后禁止后续写入把 CS 拉高拉高 /WP再进入系统休眠。代码层面会让所有写 MRAM 的操作在关闭中断的临界区内执行防止写入过程中被高优先级中断打断。MRAM 写单字节很快关一两次中断影响不大但能保证命令时序的完整性。还有一个很多人会忽略的点掉电检测中断触发后MCU 自身已经在下电过程中留给软件的缓冲时间可能只有几毫秒。因此掉电保存的数据量要尽量小最好就是一个“急救块”比如 64 字节的参数表和最后一条状态信息。如果你想保存大量运行日志应该平时就实时写进 MRAM掉电时只更新一个指针。4.3 用环形日志提高数据可追溯性工业设备需要长时间记录传感器的趋势数据、报警码、控制指令等我会在 512KB MRAM 里划分一个环形日志区。比如从地址 0x00020 开始共 64KB 作为日志区每条日志固定 64 字节总共可以存 1024 条记录。另在 MRAM 固定地址专门保存一个写指针。每写一条日志的流程读当前写指针 head计算本次日志的物理地址 日志区起始地址 (head % 1024) * 64填好日志结构体并加 CRC写入该地址将 head 加 1 后写回 MRAM 指针区读取时从 head 往前回退就能拿到最近 1024 条记录。这样做的好处是日志永远不会因为空间满而拒收旧数据被自然覆盖而且适用于 MRAM 不需要擦除的特性。如果换成 Flash每覆盖一次都要先擦扇区日志数据的实时写入性能会大打折扣。这个环形日志设计同样适用于参数版本管理。比如每写一次参数就生成一个新版本记录并保存一个当前版本号程序启动时读取最新版本。如果运行过程中参数被改坏还能通过日志恢复上一个正常版本这在远程维护场景里非常实用。5. 实测踩坑四个典型问题与速查表5.1 读出来全是 0xFF第一次上电调试时我用 MRAM_ReadByte 读地址 0返回值永远是 0xFF看起来芯片没反应。排查思路是先用示波器看 CS、CLK、MOSI、MISO 四根线。结果发现 CLK 有波形MOSI 也有正确指令波形但 CS 没有变化原来是 PA3 的 GPIO 初始化写在了 GPIOPinConfigure 之前导致引脚复用配置被冲掉CS 信号根本没输出。另一个常见原因是 MRAM 的 /HOLD 引脚悬空。悬空时 HOLD 信号受噪声干扰会变低SPI 时钟一到就被暂停读出来也是全 FF。所以硬件上一律给 /HOLD 和 /WP 加上拉。5.2 写不进去状态寄存器永远是 0程序写 READ 指令读状态寄存器返回 0x00看起来状态正常但 WRITE 之后读数据值是 FF没写成功。问题出在我在 WRITE 前调用了 MRAM_WriteEnable但 WREN 的时序里 CS 拉低、发送 0x06、CS 拉高周期没问题。后来发现 TM4C129 的 SSIBusy 在 SSIDataPut 后立即判断有可能数据还在发送 FIFO 里但 WREN 命令还没真正移出这时拉高 CS 就会把命令截断。解决办法是在 CS 拉高前确保 SPI 移位寄存器完全空闲。SSIBusy检查的就是忙标志但要注意 TivaWare 的 SSI 驱动库在SSIDataPut后如果立即调用SSIBusy有时会读到旧状态。更稳妥的做法是在MRAM_Transfer里发完数据后再加一个while(SSIBusy(SSI0_BASE));同时在发 WREN 后小延时一个 SPI 周期再拉高 CS。5.3 硬件 CS 的坑前面提过FSS 自动片选会在每个字节之间跳变。如果非要用硬件 CS可以通过 SSI 的高级帧格式把每次传输设置成多字节帧但配置复杂收益不大。我最终统一用软件 GPIO 控制 CS代码逻辑直观排查问题也方便。尤其在 DMA 传输时软件 CS 和 DMA 完成中断配合更加可控。5.4 时钟频率过高导致偶发数据错位把 SSI 配置成 20MHz 后连续读写 512 字节时会出现偶发的数据错位比如读回来的第 100 个字节跑到了第 101 个位置。这类问题非常像 SPI 边沿采样点不对。MR25H40CDF 能支持 40MHz但 TM4C129 的 SSI 在 20MHz 下信号边沿比较紧张加上杜邦线连接很容易出现建立时间不足。这种情况下我做过两个处理一是把速率降到 10MHz二是如果必须跑 20MHz把 CPOL/CPHA 从 Mode 0 改成 Mode 3在相反边沿采样有时能躲开信号振铃窗口。我这套板子最后稳定在 Mode 0 10MHz没有任何再错位。5.5 排查速查表现象可能原因处理办法读回全 0xFFCS 没拉低 / HOLD 悬空 / 引脚配置错误示波器看四线时序HOLD/WP 上拉写不进去WREN 未生效 / 命令被截断检查 CS 是否完整覆盖传输降低时钟偶发数据错位速率过高 / 走线过长降速或切换 SPI Mode只有第一次能写上电写使能状态受干扰每次写前强制发 WREN读取数据中夹杂 0x00MISO 干扰或电源噪声加电源去耦缩短 MISO 走线掉电后部分数据旧版本掉电瞬间逻辑没锁存增加电压监控和写保护逻辑排查 SPI 问题时示波器比逻辑分析仪更直接。看 CS 低电平期间SCLK 有没有完整输出 32 个时钟周期指令加地址MOSI 波形是否和预期一致MISO 数据是否在每个时钟边沿稳定基本一眼就能定位是时序问题还是电气问题。这套读写流程我已经跑了好几个版本MR25H40CDF 的驱动代码被封装成一个独立模块在 TM4C129EKCPDT 上跑 Modbus 采集、MQTT 上报、本地日志存储逻辑非常清晰。搭完一遍之后最大的体会是MRAM 让我把精力从“怎么把数据存住”转移到了“怎么让数据有意义”这大概就是存储介质选对了的最大好处。最后提醒一句如果项目里数据量超过 512KB可以考虑 MR25H256 或者更大容量版本但驱动和硬件连线几乎不用改迁移成本很低。
返回列表