ARTICLE DETAIL

资讯详情

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

嵌入式存储新选择:SPI MRAM在TM4C129上的驱动实现与避坑指南

嵌入式存储新选择:SPI MRAM在TM4C129上的驱动实现与避坑指南 做嵌入式这些年最烦的事情之一就是处理数据存储。工业设备上要存参数、存日志、存掉电现场选项却总是那几个Flash 怕磨损、EEPROM 容量小、外挂电池SRAM怕丢数据。后来我在一个工控主板上同时用到 Everspin 的 MR25H40CDF 和 TI 的 TM4C129ENCPDT试着把两者用 SPI 串起来当成一个“不掉电的高速笔记本”来用实测下来非常稳。这篇文章就把完整的选型逻辑、硬件接线、驱动实现和踩坑记录都梳理一遍。MR25H40CDF 是 Everspin 的 4Mbit512KB串行 MRAMSPI 接口2.7V 到 3.6V 供电工业温度范围TM4C129ENCPDT 是 TI 的 ARM Cortex-M4F 主控120MHz 主频带单精度浮点单元外设相当全常见的有以太网 MACPHY、CAN、UART、多个 SSI即 SPI。这套组合适合做工业控制器保存标定参数和故障日志、做数据采集终端缓冲现场数据甚至做 Bootloader 的升级标志存储。全文用 C 语言基于 TivaWare 驱动库玩过 STM32 的读者把 SPI 底层换一下就能把这套逻辑迁移过去。1. 为什么选 MR25H40CDF 和 TM4C129ENCPDT 这组搭配1.1 存储器的选型纠结最后可以落到“MRAM 不折腾”很多嵌入式工程师选存储第一反应是 Flash第二反应是 EEPROM第三反应是“要不要上外部 RTC 模块里的 SRAM”。我把常见存储器的关键差异整理成一张表出去谈方案时也经常直接甩给硬件同事看存储类型写数据前要擦除吗写耐久度单字节覆盖写掉电保存典型容量SPI NOR Flash要先擦后写10^5 左右不支持支持1Mbit~256Mbit板载 EEPROM不要擦除但有页写限制10^6 左右支持支持2Kbit~1Mbit大的到 4Mbit 少见电池 SRAM / NVRAM不需要很高支持掉电靠电池常见 256KB~1MBMRAM不需要10^14 以上几乎无限支持支持常见 1Mbit~32MbitMRAM 的原理扯起来又要扯到磁隧道结但实际用的时候你只需要记住一句话它用磁阻状态存数据不是靠电荷。没有电荷泄漏所以断电不丢没有电荷泵和块擦除所以写一个字节和写一块的代价差不多不需要先擦后写没有化学磨损所以几乎可以随便写。工业现场最看重的三点——确定性、耐久性、掉电安全它全占了。那为什么不直接选并口 MRAM并口 MRAM 容量更大、读写更快但 20 多根线在工业主板上布线就是灾难做隔离、做 EMC 都被动。SPI 接口只有 SCK、CS、MOSI、MISO 四根线过隔离便宜易布主控换个引脚重映射也方便。MR25H40CDF 这颗芯片更关键的优势是容量在 4Mbit 这个档位很实用512KB 拿来存配置参数、存运行日志、存升级缓存不会像 256Kbit 的 EEPROM 那样天天抠着算留多少空间。1.2 MR25H40CDF 和 TM4C129ENCPDT 的选型逻辑我为什么挑 TM4C129ENCPDT 来做示例因为它的 SSI 模块主模式时钟可以到很高和 MR25H40CDF 的 SPI 高速特性匹配。MR25H40CDF 官方标称 SPI 时钟最高可以到 40MHz 级别不同批次和具体后缀请一定以你手里数据手册的电气参数为准而 TM4C129 的 SSI 在这种应用中实际跑到 20MHz 左右是稳的再往上就得查走线质量。芯片参数匹配度好不会出现 MRAM 支持 40MHz 但 MCU 只能出 1MHz 的尴尬。另一个核心原因是 TM4C129 的资源对工业场景足够。它本身带以太网和 CAN做主控的时候不需要额外接通信芯片GPIO 多可以单独拿出一根 GPIO 做软件片选内部还有适合做读写校验的硬件 CRC 模块虽然我这篇文章没有用但正式产品里建议用上。1.3 整体方案一条 SPI 总线 三块逻辑分区整个系统可以概括成TM4C129 的 SSI1 模块接 MR25H40CDF软件控制片选 CSMRAM 的 WP# 和 HOLD# 全部拉高VDD 接 3.3V。存储空间按逻辑分区我一般这样划分0x00000 ~ 0x0FFFF设备配置参数区低频写入使用双区备份 CRC 校验0x10000 ~ 0x3FFFF运行日志区高频写入循环覆盖带序号和校验和0x40000 ~ 0x7FFFF升级缓存、系统标志、自检结果存放区。为什么分区最直接的原因是让日志刷写不会覆盖到配置。如果整片当成一个大池子随便写某天日志写穿边界配置参数被改一个字节设备就可能从一个“能开机但参数全错”的状态变到离线设备。分区逻辑虽然简单但能在架构层面兜住很多偶发问题。2. MR25H40CDF 芯片细节决定代码是否稳的关键2.1 引脚功能与硬件接线清单MR25H40CDF 常见的封装是 8 引脚 DFN尺寸很小接地和去耦要特别处理。先看功能引脚引脚功能方向接法建议CS#片选输入低有效接 MCU GPIO软件 CSSCKSPI 时钟输入接 MCU SSI 时钟引脚SI串行数据输入芯片输入接 MCU MOSI 引脚SO串行数据输出芯片输出接 MCU MISO 引脚WP#写保护输入低有效接 10kΩ 上拉到 3.3VHOLD#保持输入低有效接 10kΩ 上拉到 3.3VVDD电源3.3V 并加 0.1uF 去耦电容VSS地接 GND尽量用铺铜减少回路面积硬件接线有两个最常见的低级坑。第一SI 和 SO 容易按 SPI Flash 的习惯接反。注意 MR25H40CDF 的数据手册里SI 表示芯片的输入要和 MCU 的 MOSITX相连SO 表示芯片的输出要和 MCU 的 MISORX相连。第二WP# 和 HOLD# 绝对不能悬空。这个词我用了“绝对”因为实际吃过亏HOLD# 悬空时只要附近有继电器动作产生一点干扰把 HOLD# 拉低几百纳秒SPI 时钟就被忽略表现出来就是读回来的数据偶尔少一个字节非常难排查。接 10kΩ 上拉是最便宜的保险。2.2 MRAM 的 SPI 命令集和状态寄存器MR25H40CDF 的命令集跟普通 SPI NOR Flash 高度相似所以很多老代码可以直接改。核心命令就这几个命令码名称说明0x06WREN写使能设置状态寄存器里的 WEL 位0x04WRDI写禁止清除 WEL 位0x05RDSR读状态寄存器0x01WRSR写状态寄存器0x03READ读数据24 位地址 连续数据输出0x02WRITE写数据24 位地址 连续数据输入状态寄存器里有几个位比较关键bit0 是 WIP表示是否有写操作进行bit1 是 WEL表示写使能锁存器是否置位bit6 是突发模式使能位。MRAM 写单个字节的速度快不需要像 Flash 那样查 WIP 等好几毫秒但读取状态寄存器仍然是调试时确认链路是否通畅的可靠手段。初始化后如果读状态寄存器返回 0xFF基本可以断定 SO 线没接对或者芯片没供电。2.3 避坑重点WREN 必须单独占用一次 CS 低电平窗口从 Flash 转过来的同事经常写这样的代码把 0x06、0x02、地址、数据全部放在同一个 CS 低脉冲里发出去。在 SPI NOR Flash 里这么干通常不行在 MRAM 里也照样不行。WREN 的时序要求是CS# 拉低后发送 0x06然后在 CS# 上升沿把 WEL 锁存起来如果这个上升沿锁存动作没有发生后面的 WRITE 命令会被忽略因为你没有写使能权限。所以必须先把 WREN 完整执行一遍CS# 拉高再拉低 CS# 发 WRITE。这一点在我后面的驱动代码里是强制拆开的宁可多用一次 CS 跳变的开销也不要写出一个看似能工作、实际上随时可能踩坑的版本。2.4 地址空间和突发读写没有“页编程”的麻烦MR25H40CDF 容量 512KB地址范围 0x00000 到 0x7FFFF命令后面跟的是 24 位地址先发高字节再发低字节。它不像 SPI NOR Flash 有页缓冲和页对齐的说法不需要考虑“这一页还剩多少字节、跨页要分两次写”。读和写操作中只要 CS# 保持低电平地址就会自动递增数据可以连续输出或输入。要注意的是地址回卷。如果连续读写到 0x7FFFF 后继续传输地址会绕回 0x00000。如果你的应用可能读或写一整块跨越最大地址的数据就必须在驱动层分两次处理。这种问题在单字节读写时不存在一旦做整片备份或整片擦除操作其实 MRAM 没有擦除命令就会暴露。3. TM4C129ENCPDT 的 SSI 外设与 GPIO 配置3.1 复用引脚选择别在 PinMux 上翻车TM4C129 的 SSI1 最常见的复用位置是 PA4 到 PA7PA4 是 SSI1CLKPA5 是 SSI1FSSPA6 是 SSI1RXPA7 是 SSI1TX。注意 PA5 这个硬件片选引脚我强烈建议不要直接连到 MRAM 的 CS#。原因很简单TM4C129 上电复位瞬间IO 状态可能不受控硬件 FSS 可能产生一个短暂脉冲MRAM 收到一个假片选低电平虽然不一定立刻写入数据但在工业现场这种不确定状态越少越好。我更愿意用 PA3 作为软件 CS完全由程序控制。在 TivaWare 里做引脚复用必须调用 GPIOPinConfigure 把引脚切换到外设功能否则引脚默认还是 GPIO。另外对于不复用成 SSI 的 PA5我会同时把它配置成普通 GPIO 输出并拉高避免 FSS 引脚悬空带来干扰。3.2 SSI 初始化代码为什么用 Mode 0 和 1MHz 起步下面是基于 TivaWare driverlib 的初始化函数假设系统主时钟已经由你的启动代码配置好#define MRAM_CS_PIN GPIO_PIN_3 #define MRAM_CS_HIGH() GPIOPinWrite(GPIO_PORTA_BASE, MRAM_CS_PIN, MRAM_CS_PIN) #define MRAM_CS_LOW() GPIOPinWrite(GPIO_PORTA_BASE, MRAM_CS_PIN, 0) void MRAM_Init(void) { SysCtlPeripheralEnable(SYSCTL_PERIPH_SSI1); SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOA); while (!SysCtlPeripheralReady(SYSCTL_PERIPH_SSI1)) {} while (!SysCtlPeripheralReady(SYSCTL_PERIPH_GPIOA)) {} GPIOPinConfigure(GPIO_PA4_SSI1CLK); GPIOPinConfigure(GPIO_PA6_SSI1RX); GPIOPinConfigure(GPIO_PA7_SSI1TX); GPIOPinTypeSSI(GPIO_PORTA_BASE, GPIO_PIN_4 | GPIO_PIN_6 | GPIO_PIN_7); GPIOPinTypeGPIOOutput(GPIO_PORTA_BASE, GPIO_PIN_3 | GPIO_PIN_5); GPIOPinWrite(GPIO_PORTA_BASE, GPIO_PIN_3 | GPIO_PIN_5, GPIO_PIN_3 | GPIO_PIN_5); SSIConfigSetExpClk(SSI1_BASE, SysCtlClockGet(), SSI_FRF_MOTO_MODE_0, SSI_MODE_MASTER, 1000000, 8); SSIEnable(SSI1_BASE); }几个关键点SSI_FRF_MOTO_MODE_0 对应 SPI Mode 0也就是 CPOL0、CPHA0MR25H40CDF 支持 Mode 0 和 Mode 3这里选 Mode 0 最简单。初始波特率我给 1MHz不是怕 MRAM 跑不动而是为了在逻辑分析仪上数波形时不至于采样点不够。通信正常后改到 10MHz 或 20MHz 再测一遍没有问题再上产线。PA5 被配置成 GPIO 输出拉高这样硬件 FSS 信号不会跑到外部也避免悬空。3.3 逐字节收发的 TransferByte为什么不用批量收发写 SPI 驱动最忌讳“连续 Put 好几个字节最后一次性 Get”。TM4C129 的 SSI 有收发 FIFO如果发送 FIFO 和接收 FIFO 同时堆积程序稍有不慎就会因为 FIFO 满而阻塞或者读取顺序错位。我习惯做一个简单可靠的逐字节传输函数static uint8_t MRAM_TransferByte(uint8_t byte) { uint32_t val; while (SSIDataGetNonBlocking(SSI1_BASE, val)) {} SSIDataPut(SSI1_BASE, byte); while (SSIBusy(SSI1_BASE)) {} SSIDataGet(SSI1_BASE, val); return (uint8_t)val; }SPI 是全双工读一个字节时主机必须同时往 TX 线上发一个字节这个函数发的 0x00 就是 dummy 数据。逐字节轮询的缺点是 CPU 占用高但对于 512KB 级别的存储操作最坏几十毫秒的连续占用完全可以接受。真要追求吞吐再去研究 SSI 的 DMA 方式工程初期用这个函数最不容易出错。4. 读写代码实现与验证4.1 先做三个基础函数读状态、写使能、单字节读写驱动层先做最小可用集合。读状态寄存器用于调试和检查写使能位写使能必须在每次写操作前执行单字节读写是所有上层操作的基石。uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS_LOW(); MRAM_TransferByte(0x05); status MRAM_TransferByte(0x00); MRAM_CS_HIGH(); return status; } void MRAM_WriteEnable(void) { MRAM_CS_LOW(); MRAM_TransferByte(0x06); MRAM_CS_HIGH(); } void MRAM_WriteByte(uint32_t addr, uint8_t data) { MRAM_WriteEnable(); MRAM_CS_LOW(); MRAM_TransferByte(0x02); MRAM_TransferByte((addr 16) 0xFF); MRAM_TransferByte((addr 8) 0xFF); MRAM_TransferByte(addr 0xFF); MRAM_TransferByte(data); MRAM_CS_HIGH(); } uint8_t MRAM_ReadByte(uint32_t addr) { uint8_t data; MRAM_CS_LOW(); MRAM_TransferByte(0x03); MRAM_TransferByte((addr 16) 0xFF); MRAM_TransferByte((addr 8) 0xFF); MRAM_TransferByte(addr 0xFF); data MRAM_TransferByte(0x00); MRAM_CS_HIGH(); return data; }注意每个读或写命令结束CS# 都必须拉高。有人会在写完最后一个数据字节后不拉高 CS# 就去准备下一次操作这会导致命令边界混乱。还有一个小细节MRAM_WriteByte 里先调用了 MRAM_WriteEnable这一步不是可有可无的。即使某些 MRAM 文档里写主阵列数据不强制要求 WREN产品代码里还是要保留这个动作。用 Flash 的思维来约束 MRAM可以在未来替换同封装 SPI NOR 或其他存储芯片时少改代码。4.2 突发读写日志和批数据导入导出实际项目中很少只写一个字节更多是写入配置结构体、日志块、升级包。这时要封装突发读写函数void MRAM_ReadBurst(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t i; MRAM_CS_LOW(); MRAM_TransferByte(0x03); MRAM_TransferByte((addr 16) 0xFF); MRAM_TransferByte((addr 8) 0xFF); MRAM_TransferByte(addr 0xFF); for (i 0; i len; i) { buf[i] MRAM_TransferByte(0x00); } MRAM_CS_HIGH(); } void MRAM_WriteBurst(uint32_t addr, const uint8_t *buf, uint32_t len) { uint32_t i; MRAM_WriteEnable(); MRAM_CS_LOW(); MRAM_TransferByte(0x02); MRAM_TransferByte((addr 16) 0xFF); MRAM_TransferByte((addr 8) 0xFF); MRAM_TransferByte(addr 0xFF); for (i 0; i len; i) { MRAM_TransferByte(buf[i]); } MRAM_CS_HIGH(); }使用突发函数时调用方要自己保证 addr 和 len 不会跨过 0x7FFFF否则地址回卷会造成隐蔽数据覆盖。我建议驱动层再加一个断言或者范围检查比如if ((addr len 0x80000) || (addr len addr)) { // 错误处理不要继续传输 }对于 MRAM 来说突发写不需要等待擦除周期连续写几千字节也不需要在中间拉高 CS#。这一点比 Flash 舒服太多SPI NOR Flash 写跨页时还得手动分页、判断页边界。4.3 一个简单的读写自检套路硬件调试第一件事不是直接跑业务逻辑而是做一轮存储自检。我常用的流程是初始化 SSI 和 GPIO。读状态寄存器确认不是 0xFF否则先查接线和供电。往 0x00000 写 4 个字节0xA5、0x5A、0x3C、0xC3。再往 0x00004 写递增计数 0~255。断电重新上电读回这些地址逐字节比对。MRAM 不像 Flash 需要“擦除后再写”所以自检时可以反复覆盖同一地址每轮写入不同的值。如果第一次对、第二次错多半是高速 SPI 下的信号完整性问题如果第一次就全 0xFF大概率是接线问题如果主控端发不出去命令查 MOSI 线和 SCK 线相位。4.4 20MHz 下的吞吐量估算很多需求文档只写“要能存 512KB 日志”不写时间要求。我们粗略算一下假设 SPI 时钟 20MHz一个字节占 8 个时钟周期就是 0.4us。突发读 N 字节时实际传输的有效字节大约是 1 字节命令 3 字节地址 N 字节数据。读 512KB 全部数据耗时大约(1 3 524288) × 0.4us ≈ 209.7ms写 512KB 数据还要额外算上 WREN 命令大约多 1.6us 的片选和命令开销整体也在 210ms 级别。这个速度对保存参数、写日志、断电紧急保存完全够用。如果对吞吐要求更高可以把时钟提高到 25MHz 左右或者启用 DMA但多数工业应用不需要追求这个极限。5. 常见问题与排查技巧实录5.1 读回全是 0xFF这是最常见问题。先查 CS# 是否真的被拉低注意软件 CS 对应的 GPIO 有没有在上电后被初始化成输出模式。再查 SO 线是否接对MR25H40CDF 的 SO 是芯片输出必须接到 MCU 的 MISO如果接反状态寄存器永远读 0xFF。最后查 VDD 引脚的电压纹波MRAM 供电不稳定时可能出现时序错乱。5.2 写入没反应读回来还是旧数据优先检查 WREN 的 CS 时序。如果 WREN 和 WRITE 被写在同一个 CS 低电平窗口里WEL 没有经过上升沿锁存写入命令会被忽略。我用逻辑分析仪抓过一次典型的错误波形CS# 从头到尾都是低0x06 后面紧接 0x02看着像是“省了一次片选”实际芯片根本不认。另一个原因是 MOSI 线没接好。写入操作依赖 SI 引脚收到正确命令如果 SI 和 SO 接反命令根本进不去。最好在驱动代码里加一个状态寄存器读取调试函数写完 WREN 后立刻读状态若 WEL 位为 0说明写使能没有成功。5.3 高速模式下偶发错位在 20MHz 以上的 SPI 时钟跑突发读写偶尔出现第某几个字节数据错位。原因通常是 PCB 走线太长、SCK 和 SI 走线间距太近、VDD 去耦电容不足。我踩过的案例为了省空间把 SCK 和 MOSI 在两层板上下平行走了 5cm10MHz 正常20MHz 偶尔丢位。后来把走线间距拉开、加宽地线问题消失。所以高速模式不稳定时先别怀疑芯片先检查硬件。5.4 掉电后某次写入损坏MRAM 本身是非易失的但掉电瞬间 MCU 可能输出“濒死”的 SPI 脉冲导致写入假数据。这不是 MRAM 的锅是 MCU 掉电行为导致的。解决方案是加电压检测或欠压复位检测到 VDD 低于阈值就禁止 MRAM_WriteEnable同时可以把 WP# 也接成受控 GPIO在掉电临界状态强制写保护。5.5 开机瞬间 CS 被拉低TM4C129 复位期间 GPIO 是高阻输入如果 CS# 没有外部上拉可能在 MCU 初始化完成前被干扰拉到低。MRAM 收到无效片选脉冲轻则没反应重则写坏数据。解决方案很简单CS# 线上加一个 10kΩ 上拉到 3.3V软件初始化完成后再接管这个引脚。5.6 WEL 一直读为 0检查 CS# 上升沿是否干净检查有没有外部电容让 CS 边沿变得太缓。另外 TivaWare 的 SSIBusy 等待是必要的如果 0x06 命令还在移位寄存器的移位过程中程序就急着拉高 CS#命令根本没发完。我的 MRAM_TransferByte 里先用 SSIBusy 等待传输完成再拉高 CS#能避免这类时序边界问题。5.7 硬件 FSS 干扰如果板子早期版本把 PA5 的 FSS 接到了 MRAM 的 CS#上电瞬间硬件 FSS 可能产生一个脉冲MRAM 会看到一次假片选。我在量产前统一改成软件 CSPA5 只做 GPIO 拉高彻底规避这个问题。这个改动虽然只省了一根 GPIO但对可靠性提升明显。6. 从能跑到可靠工业级存储模块的进阶设计6.1 给 MRAM 做“写保护开关”MR25H40CDF 的 WP# 引脚如果不做控制固定接高即可。但如果你想在业务上做更严格的保护可以把 WP# 接到另一个 GPIO平时输出高只有在需要更新状态寄存器时才拉低。对主阵列写入主要靠 WREN 命令控制掉电异常、程序跑飞时WREN 的锁存机制也能起到一定拦截作用。6.2 掉电紧急保存要预留时间预算MRAM 在掉电保存场景中的最大优势就是快。假设计算紧急保存时间预算单字节写流程包括 WREN1 字节、WRITE 命令1 字节、3 字节地址、1 字节数据总共 6 个字节。在 1MHz 时钟下6 字节一共 48 个时钟周期也就是 48us在 20MHz 下更是不到 3us。这比 Flash 动辄几毫秒的页编程时间快了几个数量级。但要完整算预算还要加上 MCU 检测掉电的中断延迟、GPIO 拉低 CS 的指令周期、以及 MRAM 自身手册里要求的时序余量。所以实际设计时建议在掉电中断里先用 GPIO 拉低 CS 和 3.3V 之间的联系不对是确保电源电容能维持 500us 以上然后在这个窗口内写完一小块关键数据。MRAM 允许你在同一地址反复写不用考虑磨损所以掉电保存逻辑比 Flash 简单得多。6.3 双区备份别把鸡蛋放在一个篮子里虽然 MRAM 比 Flash 耐操但工业现场的电磁干扰、PCB 工艺缺陷、芯片个体差异都可能造成个别位翻转。因此配置参数区还是推荐双区备份。基本做法是Bank A 存当前有效配置带 CRC32 校验Bank B 存新配置或者上次写入的备份一个独立的版本号字段标记哪份数据最新。写入新配置时先写 Bank B再把版本号递增写入一个固定位置最后更新 Bank A。启动读取时先读版本号再读对应的有效 Bank校验失败就自动切到备份。这套逻辑在 MRAM 上做起来没有任何页对齐的负担写起来非常顺。6.4 数据分层设计示例我在实际项目里会把 512KB 空间做如下划分可以参考地址范围内容写入频率校验策略0x00000 ~ 0x0FFFF设备配置参数低频双区 CRC320x10000 ~ 0x3FFFF运行日志高频环形写 序号与累加和0x40000 ~ 0x4FFFF升级缓存低频CRC320x50000 ~ 0x7FFFF系统标志、自检低频多副本 主从投票日志区采用环形缓冲区思路固定起始地址写指针每次写入一条带长度的记录记录头部放序号和 CRC读端按序号找最新记录。MRAM 的好处是写入失败不会留下“半擦除块”状态日志恢复逻辑可以写得很简单。6.5 移植性建议驱动层和业务层严格分开。业务代码只调用 MRAM_Write、MRAM_Read、MRAM_EraseMRAM 没有擦除命令不需要你应该提供的 API 就是初始化、读状态、读突发、写突发、单字节读写。底层换到 STM32 就改 HAL 的 SPI 接口换到 NXP 就改 LPSPI 接口上层完全不用动。最后再分享一个我自己的习惯正式产品里我会在 MRAM 最末尾留 16 字节存一个魔数签名、版本号和累计上电次数。每次开机先读签名不对就说明数据区被压过或者进货时就是全随机状态这时候强制走一次默认配置写入。这个习惯救过我好几次——至少避免了把“全 0xFF”当成有效配置然后设备参数全乱的尴尬。
返回列表