ARTICLE DETAIL

资讯详情

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

MRAM+TM4C1294实战:工业设备掉电不丢数据的存储方案

MRAM+TM4C1294实战:工业设备掉电不丢数据的存储方案 做嵌入式开发这些年我接过不少让人头皮发麻的活儿其中一类特别典型现场设备跑得好好的突然停电、电压跌落、或者工人直接拔了电源等再上电状态乱了关键数据丢了一部分。工业现场不比实验室任何一次“掉电丢数”都可能变成一次安全事故或者至少是一次返厂维修。要根治这个问题存储介质的选择比很多人想象中关键得多。今天这篇就完整拆解我实际量产用过的组合Everspin 的 MR25H40CDF——一颗 4Mbit、SPI 接口的 MRAM配合 TI 的 TM4C1294KCPDT——一颗带以太网、CAN、USB 的 Cortex-M4F 主控在工业和嵌入式应用里做可靠的参数保存、运行日志和掉电数据记录。文章会从选型动机、硬件连接、驱动代码到掉电保护策略一次讲完适合正在做工业控制、物联网终端、仪器仪表的嵌入式软件和硬件工程师参考。1. 存储选型先想清楚工业设备掉电后到底要保住什么1.1 现场真正发生过的问题我接过一个卷绕控制的项目。设备高速运行需要每 100ms 记录一次张力、速度和计米值用来事后分析断带原因。原方案用串行 EEPROM写到一半停电上电后读到的数据是“上一帧 半帧新数据”CRC 怎么都过不了只能把整段日志废弃。还有一个温控器项目固化参数存在 SPI NOR Flash 里产线上频繁断电测试Flash 的擦除等待和掉电中的 page program 问题让整机返修率居高不下。这两个场景其实指向同一个需求要一种写起来像 SRAM 一样快、断电以后像 ROM 一样不丢的存储介质。这些看起来都是“存数据”但工业现场的存储要求和消费类产品有本质区别。消费类设备丢几个字节最多重启恢复工业设备丢了数据可能直接影响工艺参数、报警记录、累计产量甚至引发安全事故。所以选型时我不只看容量和价格更看重“写入过程是否可控”“会不会写到一半坏掉”“擦写次数会不会耗尽”这几个维度。1.2 对比一圈以后为什么是 MRAM常见非易失存储方案我按上面四个维度扫了一遍结果很有意思。串行 EEPROM 擦写寿命大约十万到百万次单次写有等待周期掉电时容易留下半页脏数据SPI NOR Flash 容量大、便宜但必须整页擦除再写寿命只有十万次左右掉电写一半的概率还更高电池备份 SRAM 写起来确实快可电池老化、漏液和维护问题在工业现场很不招人待见FRAM 其实也不错密度和成本到了大容量就吃亏。MRAM磁阻随机存储器在这几个维度上几乎是通吃写入前不用擦除地址随便写没有页边界一个连续写命令能把整个芯片写完写入速度没有“忙等待”CS 拉高就算完成擦写寿命按官方说法基本可以视为无限实际应用根本不用考虑磨损均衡数据保持超过 20 年工业温度范围也不含糊。它的密度虽然没有 Flash 夸张但 4Mbit 这个级别正好覆盖工艺参数、运行日志、标定值这类需求。1.3 MR25H40CDF 这颗料的具体面孔MR25H40CDF 是 Everspin 的 4Mbit512KB串行 MRAM8 脚 DFN 封装供电 3.3V。对外接口就是标准的 SPI同时支持 SPI Mode 0 和 Mode 3最高时钟一般到 40MHz具体以后缀和手册为准。指令集和常见的 25 系列 EEPROM 很像有 WREN写使能、READ、WRITE、RDSR、WRSR 这几条上手成本很低。和 Flash 相比最大的区别是没有擦除命令没有页编程概念写完不需要任何等待。买料的时候注意后缀“CDF”这类代码通常对应供电电压、封装形式和温度等级不同后缀在最高时钟和温度范围上可能有差异。我强调这一点是因为真有人把后缀看错买回来的料在高温段跑得不稳。设计定型前务必拿 Everspin 官方型号定义表和自己的温区需求对一遍。2. TM4C1294KCPDT 主控侧的准备与硬件连接2.1 这颗主控为什么适合搭档TM4C1294KCPDT 属于 Tiva C 系列Cortex-M4F 内核主频 120MHz板载 1MB Flash 和 256KB SRAM在这个项目里还集成以太网 MACPHY、双 CAN、USB OTG 和各种串口。做工业设备一颗片子里能同时解决“本地控制、数据记录、远程上报”省掉一堆外挂这是我选它的核心理由。对 MRAM 来说主控只需要一个 SPI 主机口加一个普通 GPIO 做片选TM4C1294 的 SSI 资源绰绰有余而且两边都是 3.3V 电平不用做电平转换。真正要花心思的不是“能不能接”而是“怎么把片选控制好”这也为后面踩的坑埋下了伏笔。2.2 引脚分配与接线规划我项目里的接法如下表MR25H40 的 SI 接主控的 MOSISO 接主控的 MISOSCK 接 SCKCS 单独用普通 GPIO 拉。这里特意不用 SSI 自带的 FSS 当片选原因后面专门讲。主控引脚信号方向MR25H40CDF 引脚说明PD0SSI0Clk输出SCKSPI 时钟PD3SSI0Tx/MOSI输出SI片选拉低期间发送命令、地址、数据PD2SSI0Rx/MISO输入SO读回数据PB5GPIO输出CS独立片选整个传输期间保持低3V3—VDD0.1uF 就近去耦GND—VSS共地走线短而粗3V3—WP、HOLD按手册固定到合适电平不要悬空注意一点TM4C1294 的引脚复用非常灵活SSI0 不一定固定在 PD0-PD3有些核心板和开发板可能已经占用了这一组引脚。代码里我按 PD0-PD3 举例实际改板时只要保证 GPIOPinConfigure 那一行的引脚映射和你的原理图一致SSI 的配置参数不用动。2.3 板级细节电容、走线和上电时序MRAM 是高速 SPI 器件虽然协议简单但板级处理还是要注意。VDD 引脚旁边至少放一颗 0.1uF 陶瓷电容有条件再加一颗 4.7uF 钽电容用于瞬间负载SCK 走线尽量短洞洞板飞线调试时别把时钟提到 40MHz后面我会细说。WP 和 HOLD 这两个引脚如果不用建议直接固定到高电平而不是悬空。悬空引脚在干扰大的工业现场容易出幺蛾子写不进去的故障最难查。上电后 MCU 先等电源稳定再初始化 GPIO 和 SSI。Tiva 内部有上电复位和掉电检测但软件侧加一个几毫秒延时再访问 MRAM 更保险避免 VCC 还没稳就发了第一条指令。3. 先把驱动写出来SSI 初始化、WREN 与单字节读写3.1 SSI 初始化的正确姿势我用 TivaWare 库写驱动初始化顺序是开外设时钟、配置 GPIO 复用、配置 SSI 主机模式、初始化 CS 引脚。SSI 位时钟在验证阶段先给 20MHz够快也够稳。下面是初始化代码引脚按我上面的接线示例写。#include stdint.h #include stdbool.h #include inc/hw_memmap.h #include inc/hw_types.h #include inc/hw_gpio.h #include driverlib/sysctl.h #include driverlib/gpio.h #include driverlib/ssi.h #define MRAM_CS_GPIO GPIO_PORTB_BASE #define MRAM_CS_PIN GPIO_PIN_5 static void MRAM_CS_LOW(void) { GPIOPinWrite(MRAM_CS_GPIO, MRAM_CS_PIN, 0); } static void MRAM_CS_HIGH(void) { GPIOPinWrite(MRAM_CS_GPIO, MRAM_CS_PIN, MRAM_CS_PIN); } void MRAM_Init(void) { // 1. 使能外设时钟 SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOB); SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOD); SysCtlPeripheralEnable(SYSCTL_PERIPH_SSI0); while(!SysCtlPeripheralReady(SYSCTL_PERIPH_SSI0)) {} // 2. 配置 SSI0 引脚PD0CLK, PD3MOSI, PD2MISO GPIOPinConfigure(GPIO_PD0_SSI0CLK); GPIOPinConfigure(GPIO_PD3_SSI0TX); GPIOPinConfigure(GPIO_PD2_SSI0RX); GPIOPinTypeSSI(GPIO_PORTD_BASE, GPIO_PIN_0 | GPIO_PIN_2 | GPIO_PIN_3); // 3. CS 用普通 GPIO独立控制 GPIOPinTypeGPIOOutput(GPIO_PORTB_BASE, MRAM_CS_PIN); MRAM_CS_HIGH(); // 4. 20MHz 主机模式SPI Mode 08 位数据 SSIConfigSetExpClk(SSI0_BASE, SysCtlClockGet(), 20000000, SSI_FRF_MOTO_FORMAT, SSI_MODE_MASTER, 8, SSI_FRF_MOTO_MODE_0); SSIEnable(SSI0_BASE); }一个非常重要的细节MRAM 的 READ 和 WRITE 都是“一条命令 3 字节地址 数据”整个过程要求 CS 一直保持低电平。所以片选必须由软件 GPIO 控制不能依赖硬件 FSS 逐帧拉高拉低。这也是我在代码里单独封装 CS_LOW 和 CS_HIGH 的原因。3.2 单字节传输、写使能与状态寄存器SSI 是全双工接口发一个字节的同时会收到一个字节。下面这个传输函数同时完成“发命令/地址”和“收数据”读数据时发 0x00 作为空操作把时钟跑起来即可。static uint8_t MRAM_SPI_Transfer(uint8_t out) { uint32_t in; SSIDataPut(SSI0_BASE, out); while(SSIBusy(SSI0_BASE)) {} SSIDataGet(SSI0_BASE, in); return (uint8_t)in; } void MRAM_WriteEnable(void) { MRAM_CS_LOW(); MRAM_SPI_Transfer(0x06); // WREN MRAM_CS_HIGH(); } uint8_t MRAM_ReadStatus(void) { uint8_t st; MRAM_CS_LOW(); MRAM_SPI_Transfer(0x05); // RDSR st MRAM_SPI_Transfer(0x00); MRAM_CS_HIGH(); return st; }MRAM 和 EEPROM 一样写数据之前必须先发 WREN。写完一条 WRITE/WRSR 之后状态寄存器里的 WEL 位会自动清零。调试时如果发现“发了写命令但数据没变”第一步就去读状态寄存器看 WEL 是不是 1。这是最容易踩的坑之一。单字节写入和读取void MRAM_WriteByte(uint32_t addr, uint8_t data) { MRAM_WriteEnable(); MRAM_CS_LOW(); MRAM_SPI_Transfer(0x02); // WRITE MRAM_SPI_Transfer((addr 16) 0xFF); // 地址 A23..A16 MRAM_SPI_Transfer((addr 8) 0xFF); // 地址 A15..A8 MRAM_SPI_Transfer(addr 0xFF); // 地址 A7..A0 MRAM_SPI_Transfer(data); MRAM_CS_HIGH(); } uint8_t MRAM_ReadByte(uint32_t addr) { uint8_t v; MRAM_CS_LOW(); MRAM_SPI_Transfer(0x03); // READ MRAM_SPI_Transfer((addr 16) 0xFF); MRAM_SPI_Transfer((addr 8) 0xFF); MRAM_SPI_Transfer(addr 0xFF); v MRAM_SPI_Transfer(0x00); // 空时钟读回数据 MRAM_CS_HIGH(); return v; }写完一个字节CS 拉高就代表这个字节已经进入存储阵列了。没有擦除等待、没有 busy 轮询这点和 Flash 完全不同。3.3 第一轮验证先别急着写业务逻辑驱动写完我先做的是回读测试写 0xA5 到某个地址再读出来比对。如果对了再对整片做一次“写入模式 回读比对”的扫描确认地址线没有接错、片选没有失效。我习惯在 CS、SCK、MOSI 上挂逻辑分析仪看波形里 WREN 是不是紧跟在命令之前CS 是否在整个传输期间保持低电平。这一步多花十分钟后面能省一整天的排查功夫。4. 从单字节到数据帧批量读写与掉电安全的双缓冲设计4.1 连续读写、地址自动递增单字节只是基本功实际记录数据都是批量读写。MR25H40 的 READ 和 WRITE 命令支持连续访问地址会随时钟自动递增不像 Flash 那样有页边界限制连续读可以一口气从第一个字节读到最后一个字节也没问题。比如连续写一条日志帧我只发一次 WRITE 命令地址给到起始地址后面把结构体数据一个字节一个字节发出去CS 全程拉低到最后一个字节发完再拉高。连续写同样要先发 WREN。注意地址递增越过 512KB 边界后会回绕到开头正常情况下不要故意让日志区域跨边界最好把“环形缓冲”的逻辑在软件里控制让记录区固定在一段安全范围内。批量读也是一样的思路命令后跟 3 字节地址然后连续发空时钟MISO 上就会流出一串数据。Tiva 的 SSI FIFO 在批量传输时能降低 CPU 开销但如果数据量大建议配合 DMASSI 请求 DMA 传输这件事在 Tiva 上很成熟能避免在中断里一个字节一个字节搬。4.2 记录帧格式长度、序号、CRC 一个都不能少工业数据记录要的不是“能存”而是“断电后还能判断哪一笔是完整的、哪一笔是新的”。我固定用下面这种记录帧写入前先算好 CRC再把帧整块写进 MRAM。typedef struct { uint32_t seq; // 序号单调递增 uint32_t timestamp; // 时间戳 int16_t temp; // 示例工艺量 uint16_t pressure; // 示例工艺量 uint8_t status; // 状态字 uint8_t rsv[5]; // 预留保证版本扩展 uint16_t crc; // 对前面字段的 CRC16 } Record_t;CRC 我推荐用 CRC16-CCITT多项式 0x1021查表或逐位计算都行。下面这段逐位版本不用查表代码量小移植方便uint16_t crc16_ccitt(uint16_t crc, const uint8_t *p, uint32_t len) { uint32_t i; while(len--) { crc ^ ((uint16_t)*p) 8; for(i 0; i 8; i) { crc (crc 0x8000) ? ((crc 1) ^ 0x1021) : (crc 1); } } return crc; }写入前把 CRC 填入帧的最后一个字段读取后重算前面所有字段的 CRC 并比对。这里有个工程细节如果直接用结构体指针去发数据要小心编译器在结构体里填充的 padding 字节不同编译选项下这些字节是不确定的。稳妥做法是用#pragma pack(push,1)让结构体按 1 字节对齐或者干脆定义一个固定大小的字节数组逐字段手动填充。MRAM 本身按字节寻址不存在对齐问题但你的“帧格式”必须和编译器对齐策略保持一致。4.3 A/B 双缓冲保证最后一笔永远可恢复只靠 CRC 能发现坏帧但还解决不了一个更隐蔽的问题如果最新一帧恰好在写入过程中掉电两帧都处于不可用状态怎么办工业上常用 A/B 双缓冲也叫双区交替。我把记录区划成 A 和 B 两个位置每次写入交替落在其中一个区域。因为 MRAM 写入是在 CS 拉高的边沿提交的理论上不会留下“半写”但工程上谁也不想赌最后一笔所以 A/B 序号 CRC 依然是标配。上电恢复的逻辑很简单读 A、B 两个区域的帧分别做 CRC 校验校验通过的帧里取序号最大的那个作为最新记录。如果 A 写坏了至少 B 还是上一笔完整的如果 B 写坏了A 顶多损失这一笔更新不会退化成“没有记录”。#define REC_A_ADDR 0x00000 #define REC_B_ADDR 0x00100 bool RecLoadLatest(Record_t *out) { Record_t a, b; MRAM_ReadBytes(REC_A_ADDR, (uint8_t *)a, sizeof(Record_t)); MRAM_ReadBytes(REC_B_ADDR, (uint8_t *)b, sizeof(Record_t)); uint16_t ca crc16_ccitt(0, (uint8_t *)a, sizeof(Record_t) - 2); uint16_t cb crc16_ccitt(0, (uint8_t *)b, sizeof(Record_t) - 2); bool okA (ca a.crc); bool okB (cb b.crc); if(okA okB) { if(a.seq b.seq) { *out a; } else { *out b; } return true; } if(okA) { *out a; return true; } if(okB) { *out b; return true; } return false; }这个方案牺牲了 1 倍存储容量来换可靠性对 512KB 的 MRAM 来说非常划算。如果记录频率很高还可以把 A/B 区域各放大到几 KB每次失败后整体重写逻辑一样只是把“双帧”扩展成“双区”。5. 量产之后才真正理解的细节时序、片选与最后一笔5.1 时钟频率原型阶段先跑 20MHzMR25H40 手册上标称可以跑到 40MHz但我强烈建议原型阶段先用 20MHz。为什么因为很多工程师在洞洞板和杜邦线上就想跑满速结果 SCK 串扰、地线压降、逻辑分析仪探头电容一综合时不时读出错数据然后怀疑芯片有问题。实际上芯片没问题是信号完整性撑不住。用 20MHz 能把“协议问题”和“信号问题”分开定位。PCB 画好后再逐步把 SSI 时钟提到 40MHz。此时要重点看 SCK 的上升沿和 MISO 数据稳定窗口走线短、地回路干净的话40MHz 没有压力。如果走线长宁可在 SSI 配置里加一个时钟分频档稳定比带宽值钱。5.2 为什么不能用 SSI 的 FSS 直接当片选这是我在这个项目里印象最深的一个坑。TM4C1294 的 SSI 自带 FSS 引脚看起来就是片选为什么不用因为 SSI 的 FSS 是按“数据帧”来控制的8 位模式下一字节就是一帧帧和帧之间 FSS 会恢复高电平。而 MRAM 的 READ/WRITE 要求 CS 从发送指令到最后一个数据字节之间全程为低中间任何一次拉高都会被器件当成“命令提前结束”这次传输直接作废。有人会说那我配置成“连续传输模式”让 FSS 保持低行不行不同 Tiva 型号和库版本对连续帧的处理不完全一致硬件行为还受 FIFO 水印影响我不敢在这种关键信号上赌配置。独立 GPIO 控制 CS 是教科书级的可靠做法软件说拉低就拉低说拉高才拉高没有任何模棱两可。这也是我代码里把 CS 单独拿出来封装的根本原因。5.3 WP、HOLD 引脚和写保护的隐藏逻辑MR25H40 有 WP 和 HOLD 引脚很多人在原理图上直接悬空。我的建议是固定到高电平。HOLD 引脚悬空时一旦现场干扰把 HOLD 拉低SPI 传输会被暂停主控以为发完了器件其实没接收完数据就丢了。WP 引脚悬空则可能遇到“写不进去”的诡异现象尤其是器件内部块保护位被改过的情况下排查起来非常痛苦。另外要注意 WRSR 命令。MR25H40 的状态寄存器里有块保护位如果之前软件写过这些位上电后它们依然保持会导致大片地址写不进。我在产线自检程序里加了一条“上电读状态寄存器确认 BP 位是默认值”的检查这比出了故障再查省事得多。5.4 掉电瞬间的“最后一笔”怎么保护即便用了 A/B 双缓冲有些设备还是要求“最后一笔必须真实落盘”比如断电前的产量累计值。这时我一般补两个动作。第一TM4C1294 自带掉电检测配置一个欠压中断检测到电源开始跌落后立刻把还留在内存里的关键数据压缩成最短帧抢在电压跌出工作范围前写完。第二主控的 3.3V 电源输入端放一组足够大的储能电容把“可写时间窗口”从毫秒级拉到几十毫秒级。第三写完后还要判断这次写是否真的完成方法是通过复位后的首轮 A/B 扫描来确认而不是在掉电中断里盲目等待。再多说一句MRAM 的抗辐射和抗干扰特性比电荷存储介质好这是它适合工业现场的底子。但别因此忽略布局靠近大功率电机和变频器的板卡SPI 线照样可能被耦合噪声干扰该做的滤波、短走线、GPIO 上下拉一样都不能省。这套 MR25H40CDF 加 TM4C1294KCPDT 的方案我在两个量产项目里连续跑了几年售后反馈里基本没有再出现“掉电丢数据”的投诉。现在我做任何需要掉电保存的板卡几乎默认就是这个组合一颗 MRAM 做参数和日志区A/B 双缓冲做防撕裂CRC 做完整性校验。成本比 EEPROM 高一点但换来的是半夜不会被电话叫醒的概率。这笔账我觉得非常划算。
返回列表