多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

MR25H40CDF与TM4C1294的工业存储掉电数据保护实践

MR25H40CDF与TM4C1294的工业存储掉电数据保护实践 去年在现场处理一台控制柜的上电参数丢失故障折腾了我好几天。设备每次重启后偶尔会把校准参数读成全 0后来查到底部是一颗 SPI NOR Flash 在写日志时被掉电打断整个扇区跟着损坏。那次之后我对关键数据存储的选型变得非常谨慎。最终在工业应用里我把方案落到了 Everspin 的 MR25H40CDF 这颗 SPI MRAM 上搭配 TI 的 TM4C1294KCPDT 做主控通过 SSI 接口完成存储和读取数据。这套组合解决的不只是“掉电不丢”的问题还把嵌入式应用里频繁改写、随时写入、异常断电、日志记录这些需求一并理顺了。MR25H40CDF 是 4Mbit 的串行 MRAM虽然容量不大但胜在非易失、字节可写、没有 Flash 的擦除和磨损限制。TM4C1294KCPDT 是 Tiva C 系列里的 Cortex-M4F主频 120MHz带多个 SSI、网络控制器和 uDMA工业温度等级做中小型设备的主控很合适。这篇文章把我这次移植、驱动编写、掉电一致性设计、现场调试的完整过程写出来包括硬件上容易忽略的引脚处理、TivaWare 库的使用、AB 区提交协议以及我在实测中遇到的几个坑。1. 为什么是MR25H40CDF这颗MRAM把工业存储需求拆开看1.1 工业现场对存储介质的三个硬指标工业设备里存储数据和消费电子不太一样。消费产品写个配置、存个照片坏了重刷一次问题不大。工业现场则要面对几个绕不开的约束频繁断电、反复改写、环境恶劣。先说频繁改写。一台设备即使只在开机和参数变更时写配置一天写上几十次也很正常如果是事件日志或者电能质量记录一天几千次写入都不夸张。普通的 SPI NOR Flash 标称擦写寿命通常在 10 万次左右听着不少但注意它是以“扇区擦除”为单位的。一条日志只有几十字节为了写它可能要擦掉一个 4KB 扇区寿命耗得很快。我见过许多设备运行一两年后出现配置区损坏大概率就是日志把同一块 Flash 反复擦写拖垮了。第二个问题是掉电一致性。NOR Flash 写数据要先擦除擦除和编程过程一旦被断电打断扇区内容可能处于“半擦半写”的垃圾状态。如果代码区没坏还好配置区坏了设备上电直接丢参数。就算加了校验和备份恢复策略也要写一大堆。第三个是温度和环境。控制柜里的温度可能从零下到七十多度加上湿度、振动、电磁干扰。Flash 本身抗温不差但“擦写状态机”对供电质量比较敏感供电一抖动就可能出坏块。MRAM 在这三个维度上几乎是降维打击。MR25H40CDF 是磁阻随机存取存储器每一个存储单元是磁隧道结写入靠电流翻转磁性状态不靠电荷保存所以没有“擦除”动作也不存在写坏一说。写一个字节就是直接覆盖掉电时数据已经物理落盘不需要等什么内部 EEPROM 完成信号。1.2 MR25H40CDF 的关键参数和适用边界这颗芯片的具体型号我手里是 MR25H40CDF8 引脚封装SPI 接口。容量 4Mbit也就是 512KB这个容量在嵌入式里够放参数、日志、临时掉电保存区但别想着拿它当大容量文件盘。供电范围大约在 3.3V 附近和 TM4C1294KCPDT 的 IO 电平完美匹配不需要电平转换。SPI 时钟官方可以到几十 MHz不过实际跑多少要看主控和外设。我在 TM4C1294KCPDT 上把 SSI 时钟配到 16MHz 或 20MHz 都稳定运行具体后面说。MRAM 最大的特点是没有写前擦除也没有写忙等待。写完就是写完了不需要像 Flash 那样轮询状态寄存器也不怕读改写时的“写 0 变 1”限制。适用边界也要说清楚。512KB 存不了多少音频、图片、固件备份。它适合的场景是小容量、高可靠、高频率写入的数据。例如设备参数区、运行日志、校准系数、固件版本指针、掉电过渡暂存区。大容量数据放外置 SD 卡或 SPI NOR Flash 当仓库盘MRAM 只放最核心的元数据和索引这种组合很常见。1.3 为什么不用 FRAM、BBSRAM 或者直接裸用片内 Flash项目里很多人会问我既然要字节级写入、无限寿命为什么不用 FRAM 铁电FRAM 确实也是字节级非易失但市面上大容量的串行 FRAM 不多常见的是几 KB 到几十 KB再往上要换并行接口成本和布线都上去了。MR25H40CDF 的直接优势是容量更大同时 SPI 接口简单512KB 足够放一个像样的日志系统。也有人提议用电池备份 SRAMBBSRAM。它确实无限次写入、速度快但要配电池或者超级电容电池有寿命、有温度限制还要定期维护。工业现场我最不想看到的就是“电池没电导致数据清零”。MRAM 属于非易失存储不需要备份电源省掉一大块心病。再说 TM4C1294KCPDT 的片内 Flash。片内 Flash 放代码没毛病但运行时频繁写配置会有两个问题一是擦写寿命有限二是部分 MCU 在 Flash 擦写期间会暂停总线影响实时性。Tiva 的 Flash 控制器虽然做得还行但日志这种高频写入还是别放在程序 Flash 里为好。MR25H40CDF 把参数和日志独立出去代码区更安全数据区也更耐操。2. TM4C1294KCPDT 侧的外设准备SSI、时钟与最容易被忽略的上拉2.1 引脚规划与 SSI 模块选择TM4C1294KCPDT 上有多个 SSI 模块我选的是 SSI0引脚走的是默认的 GPIO PA 组SCK、SI、SO片选用 GPIO 单独控制。这里要特别提醒MRAM 的片选请务必用普通 GPIO 手动拉不要图省事直接接 SSI 的硬件 FSS 输出。原因很简单。MRAM 写数据前要先发 WREN 写使能指令然后把 CS 拉高再拉低去发真正写指令。硬件 FSS 擅长的是单次传输它在每字节之间会自动拉高片选做多字节突发时虽然能控制但和“WREN 后必须断开、再启动新一轮事务”这个流程配合起来很别扭。自己用 GPIO 控制 CS逻辑清清楚楚出问题也好排查。接线我列一下参考具体引脚还得看你自己的板子定义信号MCU 引脚MRAM 引脚备注SCLKPA2SCLKSSI0 时钟MOSIPA5SI数据输入MISOPA4SO数据输出CS任意 GPIOCS#软件控制WP#3.3V 上拉WP#禁用写保护HOLD#3.3V 上拉HOLD#禁止 HOLD 功能VDD3.3VVDD加去耦电容GNDGNDGND共地CS 那个 GPIO 一定要选一个上电默认是高电平的引脚并在系统初始化里先设置为输出高再初始化 SSI。不然复位期间 CS 如果处于低电平MRAM 可能会收到随机时钟线上的噪声而进入写状态这是后文要讲的坑之一。2.2 系统时钟配置与 SSI 时钟计算TM4C1294KCPDT 跑 120MHz 主频很轻松系统时钟我习惯用 PLL 配置。TivaWare 的写法很直接SysCtlClockFreqSet(SYSCTL_XTAL_25MHZ | SYSCTL_OSC_MAIN | SYSCTL_USE_PLL | SYSCTL_CFG_VCO_480, 120000000);这个函数会把系统时钟拉到 120MHz。SSI 外设的时钟源就是系统时钟SSIConfigSetExpClk 的第一个参数要传实际时钟频率这样库才能帮你算分频系数。MR25H40CDF 支持挺高的 SPI 时钟但我不建议在 TM4C1294KCPDT 上把 SSI 开到极限。原因有两个一是 Tiva 的 SSI 时钟分频粒度有限太高频率和信号质量未必匹配二是工业现场线缆可能有长度PCB 走线也可能不是特别讲究留出裕量更稳妥。我最终选择了 16MHz。配置如下SSIConfigSetExpClk(SSI0_BASE, SysCtlClockGet(), 16000000, SSI_FRF_MOTO_MODE_0, SSI_MODE_MASTER, 8);SSI_FRF_MOTO_MODE_0 就是 SPI Mode 0时钟空闲低电平首个边沿采样。MR25H40CDF 的手册说明它支持 Mode 0 和 Mode 3所以 Mode 0 没问题。8 表示 8 位数据帧。2.3 WP# 和 HOLD# 上拉三个翻车点里最隐蔽的一个MR25H40CDF 的 WP# 引脚是写保护输入低电平时不允许写操作。HOLD# 引脚是传输暂停低电平时暂停当前 SPI 传输。这两个引脚如果直接悬空后果非常隐蔽。我第一次打样时 HOLD# 没接板子在实验室怎么跑都没事一到现场出现偶发死机程序看起来还在运行但 MRAM 读写返回的数据是 0xFF。后来用逻辑分析仪抓波形发现CS 已经拉低SCK 也在跳但 SO 上一直是高电平。再用万用表量 HOLD#发现它在干扰下会被拉出低电平毛刺MRAM 的内部状态机认为传输被暂停了所以迟迟不输出有效数据。解决办法很简单WP# 和 HOLD# 各接一个 10kΩ 电阻到 3.3V让它们常态保持高电平。没有这个上拉MRAM 就处于一种“时好时坏”的状态排查起来非常难受。如果 MCU 引脚资源富裕也可以用 GPIO 控制 WP#在写操作前拉高写完后拉低等于硬件写保护不过大多数应用直接把 WP# 拉高就够了。电源上去耦也不能省。VDD 引脚旁边我放了 0.1uF 陶瓷电容再并一个 4.7uF 的钽电容。MRAM 写入时内部电流脉冲虽然不大但工业电源总线经常有毛刺去耦电容能吸收一部分。另外 VDD 和 GND 之间不要离得太远尽量在封装附近打孔。3. 读写驱动实战从字节级 API 到 DMA 批量搬运3.1 SSI 初始化和底层字节收发Tiva 的 SSI 初始化在上面已经给了核心配置完整一点还要使能 GPIO 和外设时钟SysCtlPeripheralEnable(SYSCTL_PERIPH_SSI0); SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOA); 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); GPIOPinTypeGPIOOutput(GPIO_PORTA_BASE, GPIO_PIN_3); // CS 脚配置完以后SSI 的收发是一个字节一个字节完成的。底层发送函数要注意Tiva 的 SSI 发送要等 TX FIFO 非满接收要等 RX FIFO 非空。读写同时进行所以发一个字节的同时会收到一个字节。我封装了一个基础函数uint8_t mram_spi_byte(uint8_t out) { while(SSIDataGetNonBlocking(SSI0_BASE, dummy)); // 清空 RX FIFO SSIDataPut(SSI0_BASE, out); while(SSIBusy(SSI0_BASE)); uint32_t rx; SSIDataGet(SSI0_BASE, rx); return (uint8_t)rx; }注意 SSIBusy 检查一定要做否则可能数据没有发完就切 CS。CS 片选操作是读操作前拉低读完拉高写操作前先拉低发 WREN拉高再拉低发写指令发送完拉高。顺序错了MRAM 不会理你。3.2 读接口和写接口关键是指令序列MR25H40CDF 的 SPI 指令集很标准。读数据指令是 0x03后面跟 3 字节地址然后连续读数据。写数据指令是 0x02后面跟 3 字节地址然后连续写数据。写之前必须先发 0x06 写使能否则写操作无效。读状态寄存器是 0x05写状态寄存器是 0x01。读数据的代码void mram_read(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LOW(); mram_spi_byte(0x03); mram_spi_byte((addr 16) 0xFF); mram_spi_byte((addr 8) 0xFF); mram_spi_byte(addr 0xFF); for(uint32_t i 0; i len; i) { buf[i] mram_spi_byte(0x00); } MRAM_CS_HIGH(); }写数据的代码void mram_write(uint32_t addr, const uint8_t *buf, uint32_t len) { MRAM_CS_LOW(); mram_spi_byte(0x06); // WREN MRAM_CS_HIGH(); MRAM_CS_LOW(); mram_spi_byte(0x02); mram_spi_byte((addr 16) 0xFF); mram_spi_byte((addr 8) 0xFF); mram_spi_byte(addr 0xFF); for(uint32_t i 0; i len; i) { mram_spi_byte(buf[i]); } MRAM_CS_HIGH(); }为什么写使能要在独立的片选周期里完成因为 MRAM 的机制是 WREN 命令被片选上升沿锁存。如果你在一条事务里把 0x06 和 0x02 连在一起发中间没有拉高 CSMRAM 会把它当成另一条命令或者直接忽略。这一点和很多 SPI EEPROM 是类似的千万别省。写完以后如果你不放心可以立刻回读验证。MRAM 没有写忙状态理论上写操作是立即生效的但 SP I链路本身可能出错尤其线比较长或者现场干扰大的时候。我的做法是关键参数写完以后回读前几个字节比对不一致就重试三次。日志写入则靠 CRC 校验整体兜底。3.3 突发读写和 uDMA 批量搬运MRAM 和 NOR Flash 的一个大区别是只要地址连续它可以按任意长度连续读、连续写不需要按页回卷也不需要在页边界停下。这对 DMA 特别友好。我在读日志、读上传配置块时用 Tiva 的 uDMA 把 SSI0 的 RX 数据直接搬到内存缓冲区。配置 uDMA 时选择 SSI0 RX 通道数据宽度 8 位目标地址递增源地址固定模式用 BASIC 或 PINGPONG。核心代码如下uDMAChannelAttributeDisable(UDMA_CH_SSI0_RX, UDMA_ATTR_ALTSELECT | UDMA_ATTR_HIGH_PRIORITY); uDMAChannelControlSet(UDMA_CH_SSI0_RX | UDMA_ARB_4, UDMA_SIZE_8, UDMA_ARB_4, ((len - 1) 4) | UDMA_SRC_INC_NONE | UDMA_DST_INC_8 | UDMA_MODE_BASIC); uDMAChannelTransferSet(UDMA_CH_SSI0_RX | UDMA_ARB_4, UDMA_MODE_BASIC, (void *)(SSI0_DR_R), buf, len); uDMAChannelEnable(UDMA_CH_SSI0_RX);然后手动向 SSI0 的 TX FIFO 里填 0x00 来产生时钟。这个过程中 CPU 不参与每个字节的接收整个 1KB 数据读完DMA 中断再通知 CPU。实测下来同样读 1KB 数据逐字节 read 和 DMA 读完 CPU 占用差距非常大DMA 方式只需要启动和结束中断两个瞬间。不过这里有个使用心得uDMA 的 BASIC 模式只能传一轮每次传输前都要重新设置控制字和传输地址。如果日志系统需要周期性地从固定地址读一大块数据建议用 PINGPONG 模式两块缓冲区交替接收CPU 在后台处理另一块效率能再上一个台阶。但如果只是偶尔读一次参数没必要上 DMA普通中断函数足够。4. 工业级掉电一致性MRAM 给了你原子写你还缺一个提交协议4.1 单字节原子性和多字节事务是两回事MRAM 最吸引人的一点是写原子性。写一个字节MCU 通过 SPI 把数据移进 MRAM 后这个字节在物理上已经变成非易失即使下一秒掉电也不会丢。这和 Flash 完全不一样Flash 写一个字节也可能因为掉电停留在中间状态。但是这并不代表“写一条记录”是原子的。假设你要写一条 16 字节的日志包含时间戳、事件类型、负载数据。SPI 是按顺序传的前 8 字节已经写进 MRAM后 8 字节还在 SSI FIFO 里的时候电源突然断了。最终 MRAM 里会出现一条“半条日志”时间戳是新值数据却是旧的甚至乱的。如果你只依赖“日志区在最后写入长度字段”来识别日志就会出现长度和内容不匹配的问题。所以 MRAM 解决的是“单字节不丢”多字节事务的一致性还得靠软件协议来保证。这个道理和写文件系统的日志类似只是底层的原子粒度从“扇区”变成了“字节”设计可以更简单。4.2 AB 双区提交把“最后一步”变成原子提交点我这次给参数区设计的是 AB 双区加索引指针的方案。简单说就是MRAM 里划分两个参数区区 A 和区 B大小相同都存一份完整配置数据。另有一个专门的“活动索引”地址存放当前有效数据区的编号例如 0xA5 表示 A 区有效0x5A 表示 B 区有效。写参数时不直接改当前有效区而是先写另一个区。比如当前是 A 区有效我就把新参数完整写入 B 区写完计算 CRC也附在 B 区数据尾部。CRC 确认无误后再改活动索引把它从“A 区有效”改成“B 区有效”。索引只有 1 个字节可以选择 0xA5/0x5A 这种互补模式避免单 bit 翻转出现歧义。开机时先读活动索引再读对应区域的 CRC。校验失败则尝试读另一个区如果两个区都坏了才判定配置丢失。这个方案的好处是无论掉电发生在哪个时刻最多损失“正在后台更新的那一份”已经提交的那一份始终完整。修改活动索引是单字节写MRAM 保证原子性所以提交点本身不会半途而废。换成 Flash 的话你要频繁擦写整个扇区来更新一个指针还要处理磨损麻烦得多。掉电时序上最好配合一个电容或者电源监控电路。工业现场里MCU 检测到掉电瞬间往往还有几毫秒的供电余量。我在这段时间里把关键日志的最后一条补写进去然后再等系统彻底断电。MRAM 写一条日志只要几微秒到几十微秒完全来得及。4.3 开机自检、CRC 与数据回读的兜底策略现场设备最怕“表面能跑数据其实是脏的”。我在每次上电时做的自检包括三层第一层读 MRAM 的产品识别信息。MR25H40CDF 可以通过 0x9F 读 JEDEC ID虽然是厂商 ID 不是唯一序列号但能确认 SPI 通信链路是通的。如果读回来是全 FF 或者全 00那板子大概率硬件有问题尽早报警别等到运行中才发现。第二层对整个参数区做 CRC16 校验。校验值在写参数时就计算好并保存在同一记录里。怀疑数据损坏时用另一区的备份恢复。第三层对日志区做“临时写读测试”。我每个启动周期选一个不用的地址写入固定 pattern读回比对再写回原数据。因为 MRAM 写寿命无限这种自检操作完全不用心疼。放在 Flash 上你敢这么干擦写几百次自检估计就报废了。这套策略配合 AB 双区能保证即使我在现场遇到极强的电磁干扰最坏情况也只是“最近一次参数更新丢失”而不会出现参数校验失败而系统无法启动的问题。从客户角度设备参数被篡改或丢失是重大故障但上一次正常配置还在重启动一下就恢复售后压力和停机时间都小很多。5. 实测性能、波形与我在现场踩过的三个坑5.1 20MHz SPI 下的实际读写数字我最终把 SSI 时钟设置在 16MHz原因前面说了留信号裕量。测一下数据操作数据量实测耗时备注单字节读1 字节约 5us含 CS 拉低、命令地址数据、CS 拉高单字节写1 字节约 8us含 WREN 和写指令两次事务突发读1KB约 600us命令地址连续读含函数开销突发写1KB约 1.2ms含 WREN、写指令、回读校验 16 字节这个数字对工业应用来说完全够用。读 1KB 参数只要 600us写一条 64B 日志不到 200us。MRAM 没有擦除等待所以写性能非常平稳不像 Flash 写一个扇区要几十毫秒期间还要做坏块管理。有一点要向做 Flash 的朋友强调MRAM 不需要在写后延时等待“内部编程完成”也不需要轮询 WIP 位。写完 CS 拉高数据就已经是新的了。因此驱动里那些 us 级 delay 可以全部删掉只保留 GPIO 片选的高电平和低电平间隔即可。片选两次事务之间倒是要留一点点时间其实 GPIO 翻转本身就有百纳秒级别的开销自然满足。5.2 坑一HOLD# 引脚悬空引发的“假死”这个坑我在前面已经预告过。单独拿出来说是因为它的排查过程很有代表性很多工程师可能会在那块板子上浪费一整天。现象是设备运行大约几个小时以后程序陷入等待 MISO 数据的循环出不来。逻辑分析仪挂上以后能看到 CS 是低电平SCK 一直在跳MOSI 发的全是 0x00唯独 MISO 一直是高。初看像是 MISO 虚焊量引脚和 MCU 之间通路又是好的。后来怀疑是电平问题把 SPI 速率降到 1MHz还是老的样。最后我把 MRAM 所有引脚波形都抓了一遍才注意到HOLD# 引脚在故障发生时有一个低电平毛刺时间大概几百纳秒。MRAM 内部把 HOLD 识别成了暂停请求于是它不响应后续命令。由于我的是软件 CS代码里也没有超时机制于是程序就卡在死等里。解决HOLD# 接 10kΩ 上拉到 VDD。接完以后再跑两星期再没有出现这个现象。这也是我把“WP# 和 HOLD# 必须上拉”写在外设准备那一段的原因。5.3 坑二复位瞬间 CS 毛刺导致写入了垃圾数据有块样机在断电重上电后MRAM 里偶尔多一段乱码。抓波形时发现MCU 复位释放瞬间GPIO 会先进入默认状态CS 对应的引脚在那一瞬间不是高电平而是短暂的低电平。恰好此时 SCLK 线上有一个耦合进来的尖峰MRAM 以为 CS 有效且收到了一条写指令就把 SI 线上的随机数据写进了一个随机地址。虽然概率低但工业现场只要发生一次就可能造成设备配置被破坏。我的处理办法是三管齐下选一个上电默认高电平的 GPIO 做 CS并在 GPIO 初始化里最先把它置高。CS 线上串联一个 100Ω 电阻再对地并联一个几十 pF 的小电容构成简单低通滤波。100Ω 不会影响 SPI 上升沿但能削弱外部干扰尖峰。在系统复位期间用外部逻辑把 CS 强制拉高比如用带 OE 的缓冲器或者用一个 MOS 管做反相控制。成本不高但对量产产品来说很值。另外WP# 外接上拉的另一个好处是复位期间即使 CS 真的被毛刺选中因为 WP# 已经是高电平而 MRAM 出厂默认写保护使能除非先发过 WREN否则写入不会生效。这等于多一道保险。5.4 坑三突发写跨越芯片地址边界时的回卷行为MRAM 没有页的概念但我在测试突发写 512 字节后发现如果我设置的地址在 “最后一个地址” 附近继续写下去会怎样实测这颗芯片的表现是数据写到边界后不会自动回卷到 0而是超出部分丢失或者产生不可预期结果。具体行为可能和批次有关但我定下的规矩是驱动里所有连续读写都要先检查地址加长度是否越界越界就拆成两段。别指望芯片帮你处理边界。另外突发写长度太长时SPI 主控的 FIFO 跟不上中间可能因为 CS 拉低时间过长MRAM 内部计数器错位。建议单次事务不要传超过 1KB日志系统按 256B 分包写既方便校验也方便出错重传。这个限制不是 MRAM 的物理问题而是 SPI 协议层面的可靠性设计。写在最后一个实用的选型心态这次项目下来我的最大体会是在工业和嵌入式应用里存储和读取数据这件事不能只看容量和单价还要看“异常掉电后的行为”和“长期写入后的稳定性”。一颗几块钱的 NOR Flash 看起来便宜但后续为磨损均衡、掉电恢复写出去的补丁代码、出差处理故障的机票钱早就超过 MRAM 的成本了。如果你的设备也有参数区频繁写入、日志掉电要求、现场复位后数据不能乱的需求MR25H40CDF 加上 TM4C1294KCPDT 这套组合是很顺手的方案。MRAM 容量有限不适合当大仓库但当“最关键的那 512KB”完全够用。将来如果日志量增大我可以把 MRAM 用作日志索引和目录区把大数据块放 NOR Flash这样两者分工更合理可靠性也能兼顾。最后再分享一个小技巧MRAM 的驱动代码建议把“片选控制”单独抽一个宏方便将来换 MCU 平台或接不同的 GPIO。这次写的 AB 双区协议、CRC 校验、回读重试逻辑都是平台无关的将来不管换 STM32 还是换国产 ARM搬过去改个底层收发函数就能跑。这种思路才是工业代码该有的样子。
返回列表