首页 / 资讯中心 / 文章详情

基于TM4C1294的MRAM存储方案:SPI接口读写与掉电保护实战

基于TM4C1294的MRAM存储方案:SPI接口读写与掉电保护实战 ★ FEATURED ARTICLE
MRAM 这颗料在国内工业项目里其实用得还不算特别广但只要你做过一次掉电存参数、频繁写日志这类需求再回头看 EEPROM 和 Flash 的那点寿命和擦写麻烦基本就很难回去了。我最近在一个数据采集终端里把存储换成了 Everspin 的 MR25H40CDF主控是 TI 的 TM4C1294NCZAD整套 SPI 通信和读写流程调完以后最大的感受是MRAM 的“能处”不只是掉电不丢而是你根本不用再操心擦写均衡和写入延迟。这篇文章我就把从选型、接口设计、底层驱动到实际调通的完整过程拆开讲包括我踩过的坑和最终固化的读写方案给同样在工业和嵌入式场景里折腾存储的朋友一个可以直接抄的参考。1. 方案设计与选型思路1.1 MRAM 凭什么替代 EEPROM 和 Flash工业设备里最常见的数据存储需求无非三类参数配置、运行日志、掉电瞬间的关键状态保存。传统方案基本都是 25AA 系列 EEPROM或者 W25Q 系列的 SPI Flash但用久了都会遇到同一个尴尬EEPROM 写入速度慢、寿命只有百万次级别频繁记录日志很快就把寿命耗尽SPI Flash 虽然容量大、价格低但按扇区擦除的机制很烦人掉电时如果刚好在擦写过程中数据损坏的概率不低。MRAM 的核心优势在于它是真正的非易失性随机存取存储器按字节寻址、无需先擦后写写入速度接近 SRAM而且寿命标称达到 10^14 次写入级别。实际测试下来MR25H40CDF 的写入时间完全在微秒级比 EEPROM 的毫秒级快了几个数量级。更重要的是它内置了断电保护机制电压跌落时写操作会被安全中断不会出现 Flash 那种擦到一半数据全毁的情况。这类特性决定了它天然适合工业控制、电力监测、轨交设备这类对可靠性和写入频次都有硬要求的场景。我选 MR25H40CDF 还有个很实际的原因Everspin 的新型号 MR25H40CDC 虽然封装兼容但内部指令时序有些差异而 CDF 后缀这颗料在工业温度范围、SPI 时序参数上都比较成熟资料也好查。4Mbit 容量对大多数日志类和参数类场景足够如果以后需求扩容同系列的 8Mbit、16Mbit 芯片引脚定义基本一致改一下代码里的容量宏就行。1.2 TM4C1294NCZAD为什么用这颗主控TM4C1294NCZAD 是 TI Tiva C 系列里比较能打的一颗Cortex-M4F 内核主频 120MHz带浮点单元。选它主要看中三点。第一是 SSI 接口数量多。这颗芯片有 4 路 SSI可以同时挂 SPI Flash、MRAM、传感器和显示屏不用像 STM32 某些型号那样为了省外设还要搞软件模拟 SPI。数据采集终端里我同时要挂 W5100 以太网和 MRAMTM4C1294 的 SSI2 和 SSI3 正好各管一路互不干扰。第二是片内有 256KB SRAM 和 1MB Flash。对于要做掉电存档的应用来说大 SRAM 意味着可以在内存里先做数据缓冲和校验再批量写入 MRAM减少外设访问次数。而 1MB 的程序 Flash 在跑 RT-Thread 或者裸机状态机时都非常宽裕不用像小容量芯片那样时刻担心代码放不下。第三是 TI 的驱动库和例程很全。虽然 TM4C1294 的 SSI 模块用起来比 STM32 的 SPI 复杂一些帧格式、时钟极性和相位都要显式配置但官方提供的 TivaWare 库里有现成的 SSI 驱动函数稍微改改就能用开发周期能压得很短。1.3 方案整体架构设计整个存储子系统的设计目标是参数写入要可靠日志写入要高效率掉电瞬间不能丢数据。为此我把存储区划分成三段起始段放设备参数比如设备地址、校准系数、通信配置中间段放环形日志末尾段放系统状态比如上次关机原因、计数累计值。通信接口上TM4C1294 的 SSI3 作为 SPI 主机工作模式配置成 Motorola 格式、模式 0速率先用 1MHz 起步调试稳定后再往上提升。MR25H40CDF 挂在 SSI3 的片选上另外单独用一颗 GPIO 做硬件写保护控制——虽然 MRAM 也有软件写保护寄存器但工控场景多一道硬件保险总是好的。电源部分用 3.3V LDO 单独供电并联 0.1uF 和 4.7uF 电容多留一点储能余量给掉电写保护逻辑使用。接下来我分三部分详细讲引脚和时序细节、底层驱动实现、完整读写流程设计。2. 核心硬件接口解析2.1 MR25H40CDF 引脚与电气特性MR25H40CDF 是标准的 8 脚 SOIC-8 封装引脚定义和普通 SPI EEPROM 几乎一样CS#、SO、SI、SCK 各占一个引脚外加 VDD、VSS还有个 WP# 写保护引脚。这个封装对 PCB 布局非常友好可以直接替换同封装的 SPI NOR Flash前提是软件要做适配。硬件上特别注意三点电源去耦。MRAM 内部虽然号称有掉电保护电路但对外部电源的瞬态跌落仍然敏感。我在 VDD 和 VSS 之间放了 0.1uF 高频去耦电容同时在电源输入端加了 4.7uF 的钽电容。之前测试时发现如果去耦电容离 VDD 引脚太远高速写入时偶发数据错误推近到 3mm 以内就消失了。WP# 引脚不能悬空。虽然软件里可以用状态寄存器的 WEL 位控制写使能但 WP# 一旦悬空EMI 干扰可能误触发写保护。我的做法是上拉到 3.3V并用 GPIO 控制平时高电平允许写入需要锁定配置区时拉低禁止写入。CS# 片选时序。MR25H40CDF 要求 CS# 拉低后才能开始指令指令结束后 CS# 必须拉高而且两次操作之间需要最小的 CS# 高电平时间。这个参数在 datasheet 里有明确规定如果连续操作间隔太短芯片可能漏响应。我习惯在上一次操作结束后 delay 1us 再拉低 CS#。2.2 TM4C1294NCZAD 的 SSI 接口配置TM4C1294 的 SSI 模块和传统 SPI 有个关键区别它的数据帧宽度可配置为 4~16 位。这点在操作 MRAM 时特别重要因为 MRAM 的指令是 8 位地址是 16 位如果只图省事把帧宽配成 16 位读指令和地址就得拼接数据反而容易出错。我的处理方式是把 SSI 配成 8 位帧宽每次只发一个字节16 位地址按高字节、低字节分两次发送。这样做速度上略微慢一点但对于 1~10MHz 的 SPI 速率来说完全不是瓶颈而且代码逻辑清晰很多调起来也省心。初始化 SSI3 的代码基于 TivaWare 库可以这么写void SSI3_Init(void) { SysCtlPeripheralEnable(SYSCTL_PERIPH_SSI3); SysCtlPeripheralReset(SYSCTL_PERIPH_SSI3); while(!SysCtlPeripheralReady(SYSCTL_PERIPH_SSI3)); GPIOPinConfigure(GPIO_PD0_SSI3XDAT0); // MISO GPIOPinConfigure(GPIO_PD1_SSI3XDAT1); // MOSI GPIOPinConfigure(GPIO_PD2_SSI3FSS); // CS#也可以不用硬件FSS改GPIO GPIOPinConfigure(GPIO_PD3_SSI3CLK); GPIOPinTypeSSI(GPIO_PORTD_BASE, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3); SSI3-CR1 SSI_CR1_SSE_DIS; // 先关 SSI3-CC SSI_CC_CSYSPLL; // 时钟源选系统PLL // 分频计算SysClk120MHz目标1MHz SSI3-CPSR 120; // 120/120 1MHz SSI3-CR0 (SSI_CR0_FRF_MOTO 8) | (SSI_CR0_SPO_LOW 6) | (SSI_CR0_SPH_PHA_0 7) | 0x07; // 8bit模式0 SSI3-CR1 SSI_CR1_SSE_EN; }这里补充一个容易踩的细节TM4C1294 的 SSI 时钟源分频走的是 CPSR 寄存器实际分频逻辑是系统时钟除以 CPSR 值。CPSR 必须是偶数不是随便填的。如果发现实际波特率不对先检查 SysCtlClockGet 的实际主频是多少——很多人把 120MHz 当成默认其实取决于 PLL 配置可能是 120M、也可能是 80M 或 120M 的偏差。2.3 SPI 模式匹配的坑MR25H40CDF 的工作模式是 SPI 模式 0CPOL0CPHA0也就是时钟空闲为低数据在第一个时钟沿采样。TM4C1294 的 SSI 模块把这两个参数拆开通过 CR0 寄存器的 SPO 和 SPH 位控制。调试时最典型的故障现象是能读到数据但读回来的字节全是 0xFF 或全是 0x00或者偶尔正确偶尔错误。十有八九是 SPO/SPH 配置和芯片要求不一致。我的习惯是用逻辑分析仪抓 SCK 和 MOSI 的时序确认数据是在 SCK 上升沿还是下降沿被采样再回头调整配置。另外注意一个细节TM4C1294 的硬件 FSS片选输出的是低有效脉冲但它会在每次传输前自动拉低、结束后自动拉高这本来没问题。问题在于硬件 FSS 的高电平时间可能太短不满足 MRAM 对 CS# 高电平时间的要求。稳妥的做法是干脆不用硬件 FSS改用普通 GPIO 软件控制片选时序完全可控。 这个点3. 底层驱动实现与读写原语3.1 MRAM 指令集梳理MR25H40CDF 的指令集和常规 SPI EEPROM 高度相似核心就几条指令编码功能说明WREN0x06写使能每次写操作前必须发送WRDI0x04写禁止关闭写使能RDSR0x05读状态寄存器读取 WEL、WPEN、BP 位WRSR0x01写状态寄存器配置写保护属性READ0x03读数据支持连续读WRITE0x02写数据支持页写最多 256B/页和 Flash 的一个很大不同是MRAM 的 WRITE 指令不需要前置擦除操作也不要求地址对齐到页边界。你可以在任意字节地址写入任意长度的数据写满一页也不会被强制截断只是超过页边界时会回卷覆盖到页首。对于日志类顺序写入这个特性极大简化了边界处理逻辑。3.2 基础读写函数实现先写最基本的字节级 SPI 传输函数基于 TivaWare 的 SSIDataPut 和 SSIDataGetstatic uint8_t SPI_Transfer(uint8_t byte) { uint32_t rx; while(SSIDataGetNonBlocking(SSI3_BASE, rx)) { // 清空可能残留的接收数据 } while(SSIDataPutNonBlocking(SSI3_BASE, byte) 0); while(SSIDataGetNonBlocking(SSI3_BASE, rx) 0); return (uint8_t)rx; }这个函数有个值得注意的细节我在发送前先尝试清空接收 FIFO因为 TI 的 SSI 是双缓冲的上一次操作如果没读干净残留数据会污染下一次的判断。工业代码里宁可多这两条语句也不能在时序上冒风险。基于这个传输函数读取函数可以写成这样void MRAM_Read(uint16_t addr, uint8_t *buf, uint32_t len) { GPIO_CS_LOW(); SPI_Transfer(0x03); // READ 指令 SPI_Transfer((addr 8) 0xFF); // 地址高字节 SPI_Transfer(addr 0xFF); // 地址低字节 while(len--) { *buf SPI_Transfer(0x00); // 发空字节收数据 } GPIO_CS_HIGH(); }写入函数关键是 WREN 指令必须在 CS# 拉低之前发而且 CS# 拉低后只能发 WREN 一条指令发完立刻拉高再重新拉低发 WRITE。如果中间夹了其他字节状态寄存器的 WEL 位不会被置位写操作会被芯片忽略。void MRAM_Write(uint16_t addr, uint8_t *buf, uint32_t len) { GPIO_CS_LOW(); SPI_Transfer(0x06); // WREN GPIO_CS_HIGH(); GPIO_CS_LOW(); SPI_Transfer(0x02); // WRITE SPI_Transfer((addr 8) 0xFF); SPI_Transfer(addr 0xFF); while(len--) { SPI_Transfer(*buf); } GPIO_CS_HIGH(); }写完以后不需要像 Flash 那样轮询 BUSY 位。MRAM 的写入是立即完成的芯片没有忙状态。这也是我后来在日志写入路径里能放开手脚频繁调用的原因。3.3 状态寄存器与写保护配置MR25H40CDF 的状态寄存器有 WEL、BP0、BP1、WPEN 四个关键位。BP0/BP1 控制上 1/4 或 1/2 地址范围的写保护WPEN 配合硬件 WP# 引脚使用。我的配置是把存储区的高端 1/4 空间设为写保护也就是参数区所在的位置。配置参数时先写 WRSR 把 BP 位清掉写参数写完再把 BP 置位。这套流程在代码里对应的动作是void MRAM_Unlock(void) { GPIO_CS_LOW(); SPI_Transfer(0x06); // WREN GPIO_CS_HIGH(); GPIO_CS_LOW(); SPI_Transfer(0x01); // WRSR SPI_Transfer(0x00); // 清除BP0/BP1WPEN0 GPIO_CS_HIGH(); } void MRAM_Lock(void) { GPIO_CS_LOW(); SPI_Transfer(0x06); GPIO_CS_HIGH(); GPIO_CS_LOW(); SPI_Transfer(0x01); SPI_Transfer(0x0C); // BP11, BP01, WPEN0保护高1/2 GPIO_CS_HIGH(); }这里有个细节要提醒WRSR 指令后面跟的操作码也是 0x06WREN但千万别在 WRSR 的 CS# 周期内多传一个 0x06 字节否则芯片会把后续数据当成状态寄存器的值解析导致保护配置错乱。我就是因为在调试时顺手多加了一个字节结果 BP 位一直置位参数区怎么都写不进去排查了很久。4. 数据读写流程设计4.1 参数区的可靠读写与校验参数区的特点是小数据量、不频繁写入、但对完整性要求极高。我把参数块定义成固定 128 字节的结构体包含版本号、设备地址、通信波特率、校准系数等字段头部加上 4 字节 CRC32。读取流程是上电时从 MRAM 读出整个参数块校验 CRC32通过则加载到运行结构体不通过则使用代码里内置的默认参数同时置位“参数异常”标志把默认参数回写 MRAM避免后续运行基于脏数据。写入流程的关键是“整块校验”。任何单字段修改都先把参数结构体整理好再整体写回 MRAM最后回读 128 字节做逐字节比对。虽然 MRAM 写入可靠性已经很高但多一层回读校验对于设备参数这种关键数据并不浪费。实测 1MHz SPI 速率下写 128 字节加回读校验的总耗时约 2ms在设备上电阶段完全可以接受。CRC32 的代码我这里用的是常见多项式查表法实现uint32_t crc32_update(uint32_t crc, const uint8_t *data, uint32_t len) { while(len--) { crc ^ *data; for(int i 0; i 8; i) { if(crc 1) { crc (crc 1) ^ 0xEDB88320u; } else { crc 1; } } } return crc; }注意的是发送到 MRAM 的字节顺序和回读校验的顺序要完全一致如果某天你改了结构体定义却没同步宏里的长度回读校验会非常隐蔽地失败。4.2 日志区的环形缓冲设计日志区我分配了 256KB按 512 字节固定扇区方式做环形覆盖。每条日志记录包含时间戳、事件类型、数据长度和数据负载。写入前先在内存里组装好一条完整记录然后一次性写入 MRAM。为什么选固定 512 字节扇区而不是动态分配工业日志场景下记录是顺序产生的固定块大小让日志区的“写指针”和“覆盖边界”计算变得异常简单。维护一个 4 字节头部存放当前写指针位置相对日志区起始地址的偏移。每次写入 512 字节后写指针偏移加 512到日志区末尾就回卷到 0。这个方案跑起来后实际效果很有意思。传统 EEPROM 方案写日志最怕的就是写入次数多了以后尾部扇区坏块、参数区被磨损均衡逻辑误覆盖而 MRAM 完全没有这些心理负担。我计算过按每 10 秒写一条日志每天约 8640 次写入MRAM 10^14 次寿命意味着理论可以用几千万年实际瓶颈完全在别处。当然日志块还是要做一定容错。每条日志的头部我加了 2 字节魔数和 2 字节长度校验读取时先验证魔数再校验长度防止写入过程中发生意外中断导致后续日志全部无法解析。MRAM 虽然不容易出现半写状态但防人之心不可无。4.3 掉电保护与快速存档策略工业设备最怕的就是掉电瞬间丢失关键状态。传统做法是掉电检测触发中断然后赶在电压跌到阈值之前把关键变量写入 EEPROM。但 EEPROM 写入慢往往时间不够只能在掉电瞬间保存很小的一块数据。MRAM 的快写特性让这个窗口宽裕了很多。我的做法是用 TM4C1294 的模拟比较器监测 3.3V 电源轨当电压降到 3.08V 时触发中断中断服务程序里直接把关键状态当前累计值、运行模式、异常标志写入 MRAM 的低端固定地址。整个写入过程只有 3 条记录每条 8 字节加上指令开销实测在 10MHz SPI 速率下 50 微秒内可以完成。这里要特别提醒一个容易忽略的点掉电中断里写入 MRAM 后千万不要在这个中断里做太多别的事比如初始化变量、更新 CRC 表之类的。中断环境下的时间预算非常紧张每多一条指令就意味着电容储能要多撑一截。我实测发现用 TM4C1294 的 LDO 输出电容加上板上的 470uF 主电容掉电检测触发后大约还能维持 5ms 的稳定供电而我只用掉其中 1% 不到来做存档剩下的时间留给系统做安全关断其余外设。4.4 读写过程中的缓存与批量优化虽然 MRAM 单次写入很快但 SPI 传输本身就是逐个字节搬移如果一条日志拆成几十次小写入效率会很难看。我在日志路径里把“日志记录组装”和“MRAM 写入”彻底分离日志状态机先把记录写入 1KB 的 SRAM 环形缓冲区攒够 512 字节再一次性写入 MRAM。这个缓冲策略带来的另一个好处是对 MRAM 的访问变成“整块、有序、低频”的模式减少 SPI 总线占用给其他外设留出调度时间。在数据采集终端里SSI2 挂着 W5100 以太网控制器SSI3 挂 MRAM两块外设共享同一组 DMA 或者中断优先级时批量写入能显著减少总线冲突概率。批量写还有一个隐藏收益MRAM 虽然不需要擦除但页面写入的连续性越好地址线的翻转次数越少理论上电磁干扰风险也越低。这个属于玄学范畴但工业现场的 EMC 环境确实什么都可能发生能少一份隐患就少一份。5. 常见问题与排查技巧实录5.1 SPI 通信不稳定数据偶发错误这个是最常见的故障尤其在速率拉高以后。我排查时按顺序做这几件事先用逻辑分析仪抓取波形重点看 SCK 的占空比和边沿是否干净。TM4C1294 的 SSI 时钟如果被其他外设共用时钟树占空比可能飘导致采样点落在信号翻转沿上。解决办法是降低速率到 4MHz 以下同时启用 GPIO 的驱动能力配置。再看 MISO 上的波形是否过冲。如果上拉电阻用得太猛或者 PCB 走线过长MISO 信号在采样时刻可能还没稳定。解决方法是减小上拉阻值或调整 PCB 布线。工业环境下我还会串一个 22 欧姆的阻尼电阻在 SCK 和 MOSI 线上效果很明显。最后看 CS# 的时序。用软件 GPIO 控制 CS# 时拉低和拉高之间要留够建立时间和保持时间。我统一在每次传输开始前加 1us 延迟结束后加 1us 延迟保证 MRAM 有充分的准备时间。5.2 写入不生效数据一直是 0xFF遇到这类问题第一步就是查 WREN 有没有正确发送。我在调试时遇到过三种导致 WREN 失效的细节第一种是发送 WREN 时 CS# 没有完全拉低或者拉高太快芯片根本没识别到指令。用逻辑分析仪看 CS# 的下降沿和 SCK 的起始时刻确认 CS# 拉低后至少 idle 一段时间再发时钟。第二种是状态寄存器里写保护位没有清除。如果此前设了 BP 位后来没有先 WRSR 解锁即使 WREN 置位了 WELWRITE 指令仍会被保护逻辑拦截。这种情况读取状态寄存器就能确认值的高两位是 1 就说明被保护了。第三种是 WRSR 后没有重新 WREN。WEL 位是一次性的任何时候执行了 WREN 之后下一条 WRITE 或 WRSR 执行完WEL 就自动清零。很多人写完状态寄存器后忘了再来一次 WREN导致后续写入全部无效。5.3 字节顺序混乱读出来的数据错位严重MRAM 的地址和大多数 SPI 存储芯片一样是高位在前发送也就是 Big Endian 地址序。如果代码里把地址高低字节发反了读出来的数据要么全错位要么高地址读成低地址。还有一个典型问题有些人的上层协议用的是 Little Endian数据字段在结构体里按小端存。写入 MRAM 的数据本身是字节流不存在强制字节序但读取端如果按大端解析就会看到字段颠倒。我的做法是统一约定MRAM 里所有多字节字段一律按大端存放程序里用显式移位拼接避免依赖编译器结构体布局。这个约定在跨平台联调时尤其重要。5.4 掉电写入后数据异常或程序复位掉电场景下的问题往往不是存储芯片本身而是主控侧的供电瞬态。TM4C1294 的 BOR欠压复位电路默认在电压跌落时立刻复位复位后如果代码里没处理好“上次写入是否完成”的标志就可能把不完整的数据当成完整数据用。我的解决思路分三层第一层在掉电中断里设置一个 4 字节的“写入完成”标志写完关键数据后先写数据区最后写完成标志。上电时只有看到完成标志是预期值才认为数据有效。第二层TM4C1294 的 NVS非易失存储里保存一个上次关机原因标志。复位后先读它判断是正常断电还是异常复位走不同的恢复逻辑。第三层也是最实用的掉电中断里先关闭全局中断再执行 MRAM 写入写入完成后立即恢复中断标志或直接软复位。这样即使后续供电不稳主控也不会在写入半程时被普通中断打断。5.5 实测几个速率的读写耗时参考我给出一组我自己实测的数据环境是 TM4C1294 主频 120MHzSSI3 挂 MR25H40CDFGPIO 软件片选室温SPI 速率读 512 字节耗时写 512 字节耗时回读校验 512 字节耗时1MHz约 4.5ms约 4.5ms约 4.5ms4MHz约 1.3ms约 1.3ms约 1.3ms10MHz约 0.6ms约 0.6ms约 0.6ms20MHz约 0.35ms约 0.35ms约 0.35ms可以看到读写耗时几乎对称这就是 MRAM 不需要擦除的直接体现。Flash 的写入耗时通常是读的几倍甚至几十倍在日志高频写入场景里这个差距会被无限放大。最终我选择 10MHz 作为量产速率原因有两点一是这个速率下波形余量足够二是再往上提升到 20MHz 需要更严格的 PCB 布线和阻抗匹配对工业产品来说没必要冒险。6. 一些经验与扩展思路折腾完这套存储方案我最大的感受是MRAM 这类芯片在嵌入式里真正解决的不是“能不能存”的问题而是让开发者把精力从“管理 Flash 的擦写生命周期”里解放出来。以前写日志要算擦写次数、做磨损均衡、处理掉电擦除中断这套复杂度直接转移给了上层逻辑换成 MRAM 以后存储这件事本身的复杂度降了一个量级我可以把更多心思花在业务逻辑和系统稳定性上。如果要在这个方案上继续扩展有几个方向值得试。一是用 DMA 配合 SSI 做大批量日志搬运把 CPU 占用率降到近乎为零适合日志量更大的场景。二是把 MRAM 映射成文件系统比如 LittleFS 或 SPIFFS 适配层让上层应用直接用文件 API 读写而不是操作裸地址。三是多芯片级联多个 MR25H40CDF 挂在同一条 SPI 总线上用不同的 GPIO 片选扩展容量甚至可以做单总线双备份镜像进一步提高可靠性。最后补充一个小技巧量产固件里可以加一段自检程序上电时对 MRAM 全片做一次地址线扫描和数据线扫描也就是每个地址写特征值再读回比对几秒钟就能测出芯片是否有坏点或者焊接虚焊。这个方法帮我成功筛掉过一批手工焊接的板子对于工控产品出厂前的可靠性验证非常实用强烈推荐试一下。
阅读完成 · 觉得有帮助?
咨询建站