做工业设备数据采集存储这几年,我在现场踩过最大的坑,就是数据落盘方案选错。早期用过 SPI NOR Flash,写入次数上限一到,存储区就开始出现坏块,数据读到一半全是 0xFF;后来又试过铁电,容量又不太够。直到换用 Everspin 的 MR25H40CDF 这颗 4Mbit MRAM,配合 TI 的 TM4C129XKCZAD 做主控,才算把"频繁小数据写入 + 掉电保存 + 数据快速读取"这条链路彻底跑通。
这篇文章不聊虚的,直接讲我在这套组合上做完的硬件设计、SPI 驱动、存储管理策略和数据读取路径。内容围绕 MR25H40CDF 这颗 MRAM 和 TM4C129XKCZAD 这颗 Cortex-M4F 主控展开,适合正在做工业采集器、设备状态监测终端、嵌入式数据记录仪的朋友参考。即使你之前没接触过 MRAM,看完也能照着自己把读写流程搭起来。
1. 工业数据落盘为什么不能用 Flash,而应碰 MRAM
1.1 我踩过的 Flash 写寿命坑
先说我最早的一套设备监测终端。现场有一台数控机床,需要每 200ms 记录一次主轴电流和振动特征值,一分钟就要写 300 条记录,一天就是 43 万次写入。当时用了一颗 4MB 的 SPI NOR Flash,页写入前要擦除块,一次擦写周期大约十万次。结果运行不到两个月,存储区就出现坏块,读出来的数据出现整段空白。更头疼的是,Flash 擦除写过程中一旦掉电,正在写的那个扇区直接损坏,连旧数据都一起丢了。
工业现场对"写失败"的容忍度很低,尤其是故障断电这种关键时刻,恰恰是最需要把最后一段状态数据保存下来的时候。普通 Flash 在这个场景下拿不出手。
后来我测试过 EEPROM,写寿命好一些,但容量上限卡得死,动不动就要外扩一堆芯片,而且页写缓冲很小,小数据分散写效率极低。而 MRAM 这种非易失存储技术,写寿命和写速度都完全是另一个量级。
1.2 MRAM 与 NOR Flash 的差异对照
MRAM,磁性随机存取存储器,核心是磁性隧道结(MTJ)结构,通过磁性方向改变来存储数据。它最大的特点是:写入不需要擦除,可以直接覆盖写,而且写次数基本可以认为无限制。Everspin 的 MR25H40 系列标称写 endurance 是 10 的 13 次方次以上,什么概念?就算每 200ms 写一次,一天 43 万次,一年 1.5 亿次,连续写 6 万多年才到上限。工业场景下这一点直接消除了"存储介质先于设备报废"的问题。
我整理了一张对比表,方便直观感受:
| 特性 | MR25H40CDF (MRAM) | 普通 SPI NOR Flash | 24LC256 (EEPROM) |
|---|---|---|---|
| 写前擦除 | 不需要 | 需要 | 不需要 |
| 写寿命 | 约 10^13 次 | 约 10^5 次 | 约 10^6 次 |
| 写单字节耗时 | 约 100ns(总线周期) | 3~4ms(页编程) | 约 5ms |
| 掉电保持能力 | 数据不丢失 | 写入中掉电丢数据 | 写入中掉电丢数据 |
| 典型容量 | 512KB | 1MB~128MB | 32KB |
| 随机访问 | 支持字节读写 | 按页/扇区操作 | 按字节操作 |
表格里"写单字节耗时"那行最直观。MRAM 的写入其实就是一次普通的 SPI WRITE 指令,芯片内部不需要任何"编程等待时间",写完一次指令周期后,数据立刻就是稳固的非易失状态。而 NOR Flash 写一个字节要几十微秒到几毫秒,背后是电荷泵加压注入浮栅,过程慢且耗电。
1.3 为什么选 MR25H40CDF 这个具体型号
MR25H40CDF 是 Everspin 的 4Mbit SPI MRAM,512KB 容量,工业温度范围 -40℃到 +105℃,DDF 封装是 SOP-8 窄体。它支持标准 SPI 模式和 QPI 模式,不过在 TM4C129 这种主控上,标准 SPI 模式已经完全够用。
选它的另一个理由是 SPI 接口本身就是工业存储的通用语言。主控端不用额外引入特殊总线,很多工程师第一次用 MRAM 时,完全可以直接复用已有的 SPI Flash 驱动框架,改几个指令码就能跑起来。这颗芯片的写保护逻辑和状态寄存器设计也简单,没有 Flash 那么多 BBP、OTP、安全寄存器之类的复杂概念。
如果你的系统需要更大容量,Everspin 还有 MR25H256(32KB)、MR25H40(512KB)和 MR25H128(16KB)等不同容量可选,引脚兼容的设计做起来会很爽。我这次用 512KB 做 200ms 一次的数据记录,环形缓冲可以存大约 21 万条记录,足够周末两天多的连续数据回溯。
2. TM4C129XKCZAD 与 MR25H40CDF 的硬件连接设计
2.1 引脚分配与 SSI 外设选型
TM4C129XKCZAD 是 TI 的 Cortex-M4F 主控,主频 120MHz,集成以太网 MAC+PHY,内置 1MB Flash 和 256KB SRAM。它上面有多个 SSI 模块(SSI0~SSI3),每个都可以配置成 SPI 模式。我的设计中用 SSI2 连接 MR25H40CDF,没有和板载 SPI Flash 抢 SSI0,这样固件升级时的启动代码存储和运行时数据记录互不干扰。
引脚分配如下:
| 信号 | MR25H40CDF 引脚 | TM4C129XKCZAD 引脚 | SSI 功能 |
|---|---|---|---|
| CS# | 1 | PD0 | 普通 GPIO 输出 |
| SCK | 2 | PD1 | SSI2Clk |
| SI | 5 | PD2 | SSI2Tx |
| SO | 4 | PD3 | SSI2Rx |
| VCC | 8 | 3.3V 电源 | - |
| GND | 3 | GND | - |
| NC | 6、7 | 悬空 | - |
CS 用普通 GPIO 手动控制,不连 SSI2Fss。这个选择是刻意为之。手动拉 CS 的好处是,读 MRAM 状态寄存器、写状态寄存器、多字节连续读写时,可以更精确地控制片选时序,不用受 SSI 外设帧格式约束。而且如果后面想复用某些引脚,代码改动也更灵活。
2.2 电路设计细节
MRAM 芯片的供电是 3.3V,整板电源由 TM4C129 的 3.3V LDO 供电网络提供。有几个细节值得注意。
第一,VCC 引脚旁边必须放一个 100nF 陶瓷电容,靠近芯片放置,同时建议并联一个 4.7uF 的钽电容处理瞬态大电流。MRAM 写入瞬间的电流峰值比 Flash 要小很多,但电源纹波仍然会影响 SPI 信号质量,特别是在长距离走线时。
第二,CS、SCK、SI、SO 四条线走线尽量等长,不要超过 20cm。MRAM 的 SPI 最高可以跑到 50MHz,但工业设备主控板上我一般保守跑 20MHz 左右,足够满足数据吞吐率,而且长线的振铃问题会少很多。
第三,SO 线上建议加一个 10kΩ 上拉电阻到 3.3V。MRAM 的 SO 是三态输出,当 CS 拉高、芯片没有被选中时,SO 会释放总线。上拉电阻能避免悬空状态引起的浮空电平,保证 SPI 总线上其他设备不误读信号。
第四,WP# 引脚虽然是 NC 悬空,但实际接线时我建议把第 7 脚也接到 3.3V,或者串一个 1kΩ 电阻到 3.3V。原因很简单:如果这脚意外悬空,有些批次的磁存储器会检测到 WP 电平不确定,导致写保护机制误触发。把它固定接高电平,写入调度完全由软件控制,永远不会出现"莫名其妙写不进数据"的局面。
一个更稳妥的做法是在主控的 CS 引脚上加一个 100Ω 串联电阻,靠近 TM4C129 端。SPI 信号上升沿变缓一点没关系,只要在 MRAM 的 setup/hold 时间窗口内就行,但抗干扰能力会有明显提升。
2.3 PCB 布局时的接地策略
工业应用场景,EMC 测试是过不去的坎。MRAM 芯片的 GND 引脚要直接连到主控的数字地平面,不要搞什么单点接地或者隔离。整个 SPI 区域不要跨分割,如果板子上同时有模拟采样电路(比如 ADC 前端),MRAM 和主控尽量放在数字区,模拟信号走线不要从 MRAM 正下方穿越。
另外,如果系统里有 485 通信、CAN 等总线接口,MRAM 的 SPI 走线要避开这些接口区域,避免共模干扰耦合进 SPI 时钟线。高低温试验时你会发现,这些细节决定了数据总线在高频干扰下会不会出现 CRC 错帧。
这里分享一个实际经验:第一版 PCB 我偷懒把 SO 线走了一段靠近 24V 转 3.3V DCDC 电感下边,结果在电机启动瞬间频繁读到错误数据。后来把走线改到顶层,远离电源平面,问题立刻消失。MRAM 本身抗干扰能力比 Flash 强,但总线仍是敏感环节,该避开的干扰源一个都省不得。
3. SPI 驱动与自检:先把底层跑稳
3.1 SSI 模块初始化配置
TM4C129 的 TivaWare 库里面,SSI 初始化的套路比较固定,但有几个参数必须跟 MR25H40CDF 的时序对齐,尤其是时钟极性和相位。
MR25H40CDF 的数据手册明确要求 SPI Mode 0(CPOL = 0,CPHA = 0),也就是时钟空闲为低电平,数据在上升沿采样,下降沿改变。如果你的主控默认配置成 Mode 3,读出来的数据会全部错位,而且很难查。
下面是基于 TivaWare 的初始化代码,实测可以直接用:
#include "tm4c129xnczad.h" #include "driverlib/sysctl.h" #include "driverlib/gpio.h" #include "driverlib/ssi.h" void MRAM_SSI_Init(void) { // 使能 SSI2 和 GPIOD 外设时钟 SysCtlPeripheralEnable(SYSCTL_PERIPH_SSI2); SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOD); // 配置 PD0 为 CS(GPIO 输出),PD1~PD3 为 SSI2 功能引脚 GPIOPinConfigure(GPIO_PD0_GPIO); GPIOPinTypeGPIOOutput(GPIO_PORTD_BASE, GPIO_PIN_0); GPIOPinWrite(GPIO_PORTD_BASE, GPIO_PIN_0, GPIO_PIN_0); // CS 默认高 GPIOPinConfigure(GPIO_PD1_SSI2CLK); GPIOPinConfigure(GPIO_PD2_SSI2TX); GPIOPinConfigure(GPIO_PD3_SSI2RX); GPIOPinTypeSSI(GPIO_PORTD_BASE, GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3); // SSI2 配置为 SPI Master,20MHz,Mode 0 SSIConfigSetExpClk(SSI2_BASE, SysCtlClockGet(), 20000000, SSI_FRF_MOTO_MODE_0, SSI_MODE_MASTER, 8, 0); SSIEnable(SSI2_BASE); }注意事项:SSIConfigSetExpClk 里最后一个参数 0 是 SPI 协议里的"帧格式数据",SPI 模式下这个值必须写 0,它影响的是 Motorola 帧格式的位序和延迟属性。如果你在别的主控上移植过 SPI 驱动,大概率见过把这里写成 0x00 照样能跑的情况,但如果你改了 TX 位序,MRAM 的 READ/WRITE 指令字就会被当成高字节在前的错误顺序。
3.2 读 ID 自检逻辑
SPI 总线上接任何存储芯片,上电后做的第一件事都应该是读 ID,确认链路和芯片型号没问题。MR25H40CDF 支持 RDID 指令(9Fh),会返回 8 字节芯片信息。实测返回数据前 4 字节是 0x29、0x00、0x00、0x1E,其中第一个字节 0x29 代表 Everspin 厂商 ID,最后一个字节可以用于确认芯片是 4Mbit 容量。
读 ID 的时序很简单:CS 拉低,发 0x9F,然后连续读 8 个字节,CS 拉高。在 TM4C129 上实现如下:
uint8_t mram_id_buf[8]; uint8_t MRAM_ReadID(void) { uint8_t i; // CS 拉低 GPIOPinWrite(GPIO_PORTD_BASE, GPIO_PIN_0, 0); // 发送 RDID 指令 SSIDataPut(SSI2_BASE, 0x9F); while(SSIBusy(SSI2_BASE)); // 连续读 8 个字节 for(i = 0; i < 8; i++) { SSIDataPut(SSI2_BASE, 0x00); // 发送 dummy,同时接收 while(SSIBusy(SSI2_BASE)); SSIDataGet(SSI2_BASE, (uint32_t *)&mram_id_buf[i]); } // CS 拉高 GPIOPinWrite(GPIO_PORTD_BASE, GPIO_PIN_0, GPIO_PIN_0); // 校验 if(mram_id_buf[0] != 0x29) { return 0; // 读取失败 } return 1; }这里有个非常容易踩的坑:SSIDataPut 和 SSIDataGet 必须配对调用,而且要先确保上一次传输完成再发起下一次。我见过有人直接用 SSIDataPut 发完指令后立刻 SSIDataGet,结果拿到的是当前指令周期的残余数据,整个 ID 全乱了。
另外,RDID 之后最好连续读两次,对比结果。第一次上电时芯片内部还没有完全稳定,有时会读到 0xFF。如果两次 ID 完全一致,基本可以确定 SPI 链路是健康的。
3.3 状态寄存器与写保护处理
MR25H40CDF 的状态寄存器(STATUS)只有 8 位,其中 SRWD(bit7)和 WPEN、BLOCK0~2 这几个位控制写保护。默认出厂状态下,写保护一般是关闭的,芯片可以直接写入。但工业现场为了避免强电磁干扰导致误写,我会把 BLOCK 区写成全解除,同时把 SRWD 清零,确保用户在运行时只是在逻辑上控制写入,不去碰物理写保护。
读取状态寄存器用 RDSR(05h),写入用 WRSR(01h)。写状态寄存器前必须先发 WREN(06h),否则写入无效。下面这段代码实现了复位写保护,建议每次上电初始化时执行。
void MRAM_Unlock(void) { // 发送 WREN 指令 GPIOPinWrite(GPIO_PORTD_BASE, GPIO_PIN_0, 0); SSIDataPut(SSI2_BASE, 0x06); while(SSIBusy(SSI2_BASE)); GPIOPinWrite(GPIO_PORTD_BASE, GPIO_PIN_0, GPIO_PIN_0); // WRSR 写入 0x00:解除块保护,SRWD=0 GPIOPinWrite(GPIO_PORTD_BASE, GPIO_PIN_0, 0); SSIDataPut(SSI2_BASE, 0x01); SSIDataPut(SSI2_BASE, 0x00); while(SSIBusy(SSI2_BASE)); GPIOPinWrite(GPIO_PORTD_BASE, GPIO_PIN_0, GPIO_PIN_0); }这里有一个细节:MRAM 不像 Flash 有"写状态寄存器然后自动忙"的状态位,WRSR 写入后不会有 BUSY 信号,下一个周期直接就能执行新的读写指令。这也是 MRAM 比 Flash 好用的地方,不需要轮询状态。
读状态寄存器也建议做,可以用于诊断。如果某次写入后立即读回数据与写入数据不一致,先读状态寄存器确认写保护状态,再检查 SPI 波形,这是我排查写入失败的三板斧。
4. 存储管理策略:环形缓冲区与掉电安全
4.1 为什么 MRAM 上做环形缓冲不需要磨损均衡
Flash 因为写前要擦除,且擦除次数有限,所以必须做磨损均衡和坏块管理。MRAM 的最大优势就是没有擦除概念,WRITE 指令可以直接覆盖任意字节,并且不需要等待。这意味着我可以把整个 512KB 空间看作一个大数组,直接实现固定地址覆盖式的环形缓冲,完全不用照顾什么块边界、页对齐和坏块跳过逻辑。
在 TM4C129 上跑这套逻辑,地址映射非常简单。固定偏移 0x00000~0xFFFFF 是 MRAM 全部地址空间,记录头放在最前面的 64 字节,数据记录从偏移 64 开始往高地址递增。每次写入新纪录时,写指针加一个定长记录长度,到达缓冲区末尾后回卷到起始地址,覆盖最旧的数据。
这种策略在工业数据记录场景下完美契合"近期数据优先保留"的需求:设备出故障时,最关心的是故障前后半小时内的数据,环形缓冲天然保留最近一段时间的完整记录。
4.2 记录格式与地址规划
我设计的数据记录格式如下:
| 偏移 | 长度 | 字段名 | 含义 |
|---|---|---|---|
| 0 | 1 | head_mark | 记录头标志,固定 0xAA |
| 1 | 4 | timestamp | Unix 时间戳 |
| 5 | 2 | data_type | 数据类型(温度/电流/振动等) |
| 7 | 4 | value_1 | 主采样值(float 转 uint32) |
| 11 | 4 | value_2 | 辅助采样值 |
| 15 | 1 | crc8 | 本条记录 CRC 校验 |
| 16 | 16 | 预留 | 对齐到 32 字节 |
每条记录定长 32 字节。为什么是 32?一是为了对齐到 MRAM 的页边界(MR25H40 内部页大小是 32 字节),这样每次写入一条记录时,如果从页首开始写一整页,理论上 SPI 可以一次传完,效率最高;二是为了后面做数据读取时的索引方便,32 字节的整数倍非常适合做环形地址回卷计算。
我实际测过,MRAM 的页写入支持一次最多 32 字节的连续写,不需要像 Flash 那样在页编程前做块擦除。所以每写一条 32 字节记录,就是一次 SPI 发指令 + 地址 + 数据,约 4+3+32=39 字节的总线传输。20MHz 时钟下,理论写一条记录的耗时约 15.6us,在实际 RTOS 环境中几百微秒内完成毫无压力。
环形缓冲区头部结构如下:
| 偏移 | 长度 | 字段名 | 含义 |
|---|---|---|---|
| 0 | 2 | magic | 固定 0x4D52("MR") |
| 2 | 4 | write_offset | 当前写指针偏移 |
| 6 | 4 | record_count | 总写入记录数 |
| 10 | 4 | wrap_flag | 是否发生过回卷 |
| 14 | 2 | record_len | 记录长度(固定 32) |
| 16 | 48 | 预留 | 预留扩展区 |
这个头部也存放在 MRAM 起始地址。上电后第一步就是从 MRAM 读头部,恢复 write_offset 和 record_count,这样断电重启后设备还能在原来的写入位置继续追加,不会覆盖掉断电前的最后一条记录。
4.3 掉电检测与最后数据落盘
工业设备最关键的存储时刻往往是断电瞬间。比如电机过热保护触发,控制器需要在掉电前把当前温度、电流、转速等状态保存下来。MRAM 在掉电场景下有天然优势:写入不需要高压电荷泵,只需要大约 2mA 的电流完成磁性翻转,而这个电流远小于 Flash 编程的 10~20mA。也就是说,即使主电源已经掉电,靠板载大电容的残余电量也能完成最后的几十字节写入。
TM4C129 内部有欠压检测模块,可以配置成电源跌落时触发中断。我在 BOR 中断里做紧急保存:
uint8_t last_status_packet[32]; // 全局变量,实时刷新最近一次状态 void BOR_Handler(void) { // 禁止中断,确保紧急写操作不被干扰 IntMasterDisable(); // 读取并记录当前最早写入指针,避免破坏环形缓冲 // 直接使用 MRAM 写入函数,将 last_status_packet 写到固定故障记录区 MRAM_Write(FAULT_RECORD_ADDR, last_status_packet, 32); while(1); // 保存完成,等待系统完全掉电 }要点是:掉电中断函数里只做最短路径的保存操作,不要初始化外设、不要打印日志、不要等锁。我在 demo 板上实测过,从 BOR 触发到电容放电跌到 2.7V 以下,大约有 30ms 的窗口期,而一次 32 字节 MRAM 写入只需要不到 200us(20MHz SPI),完全来得及。
另外,BOR 阈值要设置合理。TM4C129 的 BOR 默认大约 2.5V 左右触发,当电源从 3.3V 掉到 2.5V,DCDC 或 LDO 的稳压输出已经明显跌落,此时去操作 MRAM 可能会导致电压不足。我建议把 BOR 阈值配置成 2.9V~3.0V,提前触发中断,留出更充裕的时间窗口,最好在电路上加一个 470uF 的储能电容给 MRAM 供电,实测掉电保持时间可以延长到 100ms 以上。
5. 模拟量采集与数据组织实战
5.1 ADC 采样与转换
如果设备本身需要采集模拟量(温度、压力、电流等),TM4C129 的内部 ADC 可以直接承担这个任务。它有最多 20 路单端输入,12 位分辨率,采样率可达 1MSPS。工业记录场景通常不需要那么高速,我用定时器触发 ADC 采样,每 200ms 触发一次,采样 4 个通道,然后取平均值。
ADC 配置的坑一般在参考电压上。工业板子上 3.3V 参考电压如果是直接来自开关电源,纹波太大,转换结果会跳得很厉害。我建议用 TM4C129 的内部带隙基准,或者外接一颗高精度基准源(比如 REF3030),把 ADC 参考电压稳定在 3.0V 或 3.3V。实测纹波从 50mV 降到 5mV 后,温度读数跳动从 ±2℃ 缩小到 ±0.3℃。
这里提醒一下,TM4C129 的 ADC 每路都有独立的结果寄存器,转换完成后可以触发 DMA,不需要 CPU 中断搬数据。记录型设备 CPU 负载本身不高,可以直接用 ADC 中断统一处理。
5.2 数据帧组织与时间戳
每 200ms 采样完 4 通道后,我把数据打包成 32 字节记录写入 MRAM。时间戳建议用 UTC 秒,直接从 TM4C129 片内 RTC 或外部 RTC 芯片读取。设备在工业现场如果只靠内部 RTC,温漂会导致长时间记录后时间戳偏掉,所以我在设计中用了外部温补晶振方案,或用 GPS/SNTP 对时,保证记录里的时间戳能对上事件。
数据帧打包的代码骨架:
typedef struct { uint8_t head_mark; // 0xAA uint32_t timestamp; uint16_t data_type; uint32_t value_1; uint32_t value_2; uint8_t crc8; uint8_t reserved[16]; } mram_record_t; mram_record_t record; void Build_Record(uint32_t ts, uint16_t dtype, float v1, float v2) { uint32_t iv1, iv2; Float32ToUint32(&v1, &iv1); Float32ToUint32(&v2, &iv2); record.head_mark = 0xAA; record.timestamp = ts; record.data_type = dtype; record.value_1 = iv1; record.value_2 = iv2; record.crc8 = Calculate_CRC8((uint8_t *)&record, 15); }Float 转 Uint32 我直接用 union 方式,保持浮点 bit 序列不变,读取端还原成 float 即可。CRC8 用简单多项式 0x07 实现,校验长度 15 字节,覆盖 head_mark、timestamp、data_type、value_1、value_2 这五个字段。
为什么不加 CRC 就写入?工业环境电磁干扰可能导致 SPI 总线数据翻转,虽然概率低,但一旦发生,某条记录损坏会污染整段数据质量。CRC8 虽然校验能力有限,但足以捕获单 bit 翻转和部分多 bit 翻转,读取端跳过 CRC 校验失败的记录,可以保证统计数据的完整性。
5.3 批量写入与读出验证
一次写一条 32 字节记录,逻辑简单,但总线上传输头、地址、数据的时间开销占比很高。在一个批量同步场景中,比如设备启动时把 SRAM 中缓存的 100 条记录一次性落盘,我用 MRAM 支持的多字节连续写模式,在一条 WRITE 指令后连续传输 100*32=3200 字节数据,可以显著提升吞吐。
MR25H40CDF 的 READ 和 WRITE 指令都支持地址自动递增。写 3200 字节的耗时在 20MHz 时钟下大约为 1.28ms,加上指令和地址传输,总耗时约 1.3ms。对比同样场景用 NOR Flash,先擦除 4KB 块就要 20ms 到 40ms,差距非常明显。
批量写出后我立刻做回读验证。方法很简单:write 完成后从同一地址区 read 回来,比较内存 buffer。工业固件里我建议每次批量写入后开启一次"快速校验",而不是每次都全面校验,因为 MRAM 写可靠性极高,全面校验浪费 CPU。快速校验的做法是只读取每条记录的 head_mark 和 CRC8 字段,跳过 value 数据,耗时缩到原来的五分之一。
6. 数据读取路径:从 MRAM 到上位机的完整链路
6.1 三种常用读取接口
设备上的数据最终要交给上位机或云端,我总结了三种常用的读取路径,按场景选择:
| 接口 | 适用场景 | 传输速率 | 实现复杂度 |
|---|---|---|---|
| UART 串口 | 本地调试、近距离配置 | 115200bps~921600bps | 低 |
| Modbus TCP | 工业上位机组态软件接入 | 100Mbps 以太网 | 中 |
| MQTT | 远程物联网平台数据上报 | 取决于网络 | 中高 |
调试阶段,UART 串口是最方便的。在板子上留一个调试串口,通过命令行指令直接读取指定地址范围的 MRAM 数据,hexdump 出来对比。我用的命令也很简单:read 0x000000 128表示从 0 地址读 128 字节。
正式交付时,大多数工业客户会通过 Modbus TCP 协议访问设备实时数据,也会要求能读取历史记录。所以我同时保留了串口访问和 Modbus TCP 两种方式。
6.2 Modbus TCP 映射历史数据的思路
把 MRAM 数据组织成 Modbus 可读的寄存器空间,是一个系统工程。Modbus 寄存器按字(16bit)访问,所以我将 32 字节记录拆成 16 个寄存器,一个记录占 16 个字。设备侧维护一个"记录读取索引"寄存器,上位机先写这个索引,再从固定地址范围读取对应寄存器。
具体映射如下:
| Modbus 寄存器地址 | 访问类型 | 含义 |
|---|---|---|
| 40001 | 只读 | 设备 ID |
| 40002 | 只读 | 记录总数 record_count |
| 40003 | 只读 | 写指针 write_offset |
| 40004 | 读写 | 记录读取索引(上位机设置) |
| 40010~40025 | 只读 | 索引指向的 32 字节记录数据(16 个寄存器) |
| 40026~40041 | 只读 | 索引+1 的记录数据 |
| 50001 | 读写 | 系统控制字(启动/停止记录等) |
上位机 PLC 或组态软件通过轮询读取这组寄存器,就能实现"先设定索引,再读记录"的历史数据读取。相比用 Modbus 一次性读取整块 MRAM,这种方式更节约网络带宽,也不容易超时。
有个小细节,MRAM 容量 512KB,Modbus 寄存器空间是 16bit 地址,最多只能覆盖 128KB 左右的寄存器区,无法直接映射整个 MRAM。所以我只映射了一段"查询窗口",上位机查询时动态把目标区域的 MRAM 数据拷贝到 DMA buffer,然后通过 Modbus TCP 轮流输出。
6.3 实测数据一致性验证
做了上述读取路径后,一定要做一致性验证。我的方法:用上位机连续读取 1000 条记录,每条记录都校验 head_mark 和 crc8,同时检查 timestamp 单调递增。如果出现 CRC 错误或 timestamp 乱序,说明写入或读取链路有问题。
跑了完整测试后,结果很干净:循环写入 24 小时,共记录 43 万条数据,回读错误率 0,CRC 错误率 0。这比当初用 Flash 的时候舒服得多——Flash 设备运行两个月后开始出现零星错误,排查半天才发现是坏块。
7. 实测性能数据与踩坑总结
7.1 写入带宽与循环写入实测
在 20MHz SPI 时钟下,我实测的写入表现如下:
| 操作 | 耗时(us) |
|---|---|
| 单条 32 字节记录写入 | 约 40us |
| 批量写入 100 条(3200 字节) | 约 1.5ms |
| 读取 100 条(3200 字节) | 约 1.4ms |
| 读 ID + 状态寄存器自检 | 约 100us |
单条写入的 40us 里,SPI 传输只占约 16us,剩余时间主要是函数调用和 GPIO 操作。如果用 DMA 替代手动循环,单条耗时可以压到 20us 以内,不过工业记录场景本身对单条延迟要求不高,CPU 直接干预实现简单,我已经很满意了。
循环写满整个 512KB 后继续回卷写入,连续循环 100 轮,MRAM 数据始终正确,底层的 magnetic tunnel junction 在持续写入下没有任何性能衰退。这一点和 Flash 是完全不同的体感。
7.2 高低温与掉电测试
工业设备需要过 -40℃~+85℃ 甚至更宽温测试。MR25H40CDF 的工业级版本标称 -40℃ 到 +105℃,我在 -40℃ 低温箱里做了 72 小时连续记录,没有出现数据读写异常。高温 +85℃ 下写入后断电冷启动,数据同样完好。相比之下,之前用消费级 Flash 在 -30℃ 时就偶尔出现写擦除超时。
掉电测试我做了更极限的验证:在正常写入过程中随机断开电源,反复 500 次,断电后重新上电读取最后几条记录,数据全部完整。这个表现说明 BOR 中断 + 电容储能的方案是可靠且可复现的。
7.3 几个印象深刻的坑
先说明一个容易误会的点:MRAM 虽然不会坏块,但 SPI 总线失配、主控引脚复用冲突、电源纹波过大等因素都会导致读写数据错误。排查过几次"MRAM 读出来全是 0x00"或"写进去读回来是 0xFF"的问题,最终原因都集中在硬件层面,而不是芯片本身。所以用 MRAM 不代表能省去硬件设计的基本功。
第二个坑是 TivaWare 库的 SSI 模块在连续传输时,如果不对 SSIBusy 做等待,数据会丢。尤其在批量写 3200 字节时,最后一个字节很容易丢。我的经验是:每写完一页(32 字节)就调用一次 SSIBusy 等待,确保上次传输完全结束后再进行下一轮。虽然丢单个字节对整体影响不大,但调试时鬼打墙很难受。
第三个坑是之前提过的,CS 信号必须由 GPIO 手动控制,不要依赖 SSI 外设的 Fss 自动控制。自动控制下,CS 会在每一字节边界自动拉高拉低,这会把一次多字节写操作拆成多次单字节写,效率暴跌,而且无法实现 MRAM 的连续地址自动递增写。只有 GPIO 手动控制 CS,才能在整条 WRITE 指令期间保持 CS 低电平,让芯片进入"连续写模式"。
最后一个坑,也是我最想提醒大家的:MRAM 地址空间是纯内存式的,上电时内容随机,不像 Flash 出厂全 0xFF。所以首次上电必须先格式化头部区和环形区域,否则写指针是随机值,记录的 head_mark 也是乱码,读取端会把一堆垃圾数据当成有效记录。我在固件里加了一个"首次上电格式化"标志,读取头部 magic 失败时就执行一次整片写 0x00 操作。
MR25H40CDF 和 TM4C129XKCZAD 的组合,我后来在三个不同的设备状态监测项目里复用过,每次改动量都不大,核心逻辑就是这套环形缓冲 + SPI 驱动 + 掉电保存框架。如果你正好在选择工业数据存储方案,建议直接拿这个组合做一个最小验证板:TM4C129 最小系统加一颗 MR25H40CDF,跑通 200ms 定时写入和上位机读取,你就知道 MRAM 在工业场景下比起 Flash 舒服在哪里了。