ARTICLE DETAIL

资讯详情

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

基于TM4C129的SPI接口MRAM驱动实现与掉电保存实战

基于TM4C129的SPI接口MRAM驱动实现与掉电保存实战 如果你做工业控制或者边缘嵌入式设备一定遇到过这样的场景设备关机前必须把一组校准参数、运行状态、或者最近一条故障记录存到本地下次上电要立刻能读出来。传统方案是 SPI Flash 或者 EEPROM可这两种器件在频繁写入几十万次之后基本就到了寿命尽头。我在今年的一个现场数据采集器项目里用 TM4C129XNCZAD 通过 SPI 接口读写 MR25H40CDF 这颗 4Mbit MRAM 芯片算是把“非易失存储”这几个字重新理解了一遍。下文不是抄数据手册而是我布板、写驱动、做掉电测试后得到的完整复盘。如果你也在选工业存储方案或者正准备把 MRAM 挂到自己 MCU 上这篇应该能帮你少走不少弯路。1. 为什么我在这个项目里选了 MRAM 而不是 Flash 或 EEPROM1.1 MR25H40CDF 这颗芯片的硬实力MR25H40CDF 是 Everspin 的串行接口 MRAM容量 4Mbit也就是 512KB接口是标准 SPI。很多人第一次听到 MRAM 时会以为它是什么新型 Flash其实原理完全不同。它内部用的是磁隧道结数据不是靠电荷保存而是靠磁化方向记录所以天然具备非易失性同时又保留了 RAM 的读写速度。这种结构带来的直接好处有三个。第一是读写耐久性极高Flash 的擦写寿命通常只有 1 万到 10 万次而 MRAM 的写寿命基本上可以看作无限数据手册上写的是可以支持到 10 亿次以上。第二是写入不需要先擦除Flash 在写之前必须把整个扇区擦掉MRAM 则可以直接覆盖写这给软件逻辑省掉了整整一层复杂度。第三是写操作几乎不需要等待时间Flash 写完一个页面要等几毫秒甚至几十毫秒MRAM 在最后一个时钟沿到来时数据就已经落定了。我选 MR25H40CDF 而不是其他接口的 MRAM是因为项目里 CPU 引脚资源紧张SPI 只要四根线就能搞定而且最大速率能跑到 40MHz 左右对于保存实时数据和日志完全够用。再加上宽温版本可以在工业环境下工作基本就是为现场设备准备的。1.2 TM4C129XNCZAD 在系统里的角色TM4C129XNCZAD 是 TI 的 Cortex-M4F 系列 MCU主频能跑到 120MHz片上有浮点单元资源相当充裕。我选它不是因为性能有多夸张而是因为它的 SSI 外设非常成熟可以直接做 SPI 主机也支持 I2C、UART、CAN、以太网这些东西方便和现场总线对接。这颗 MCU 的 SSI 外设最多有 4 个每个都可以独立配置成 SPI、MICROWIRE 或者 TI 同步串行模式而且时钟极性、相位都是可编程的。更关键的是它的 GPIO 复用表里SSI 信号和各种 GPIO 都能灵活映射这在画板子的时候非常舒服。我用 MR25H40CDF 做非易失数据区只是整个控制器里的一小部分MCU 还要同时处理 ADC 采样、继电器控制、远程通信所以必须选一个外设丰富、不会让我在存储器接口上浪费精力的平台。有一点要提前说清楚MR25H40CDF 是 SPI 接口不是像 SRAM 那样挂到总线上的并行存储器所以它没法直接做内存映射访问。CPU 读它的时候跟读 SPI Flash 的流程很像先发命令再读数据。但它不需要擦除、不需要等待编程这让驱动代码比 Flash 驱动简单太多后面我会详细展开。2. 硬件连接一次就点亮的接线方案2.1 引脚映射与连接表MR25H40CDF 是 8 脚封装信号不多但每一根都不能接错。这里给出我当时用的连接方案你可以直接参考。MR25H40CDF 信号信号方向TM4C129 侧说明CS#输入GPIO PB5片选低有效我用普通 GPIO 手动控制SCK输入SSI0Clk / PD0SPI 时钟SI输入SSI0Tx / PD3主机发送数据接到 MRAM 的数据输入SO输出SSI0Rx / PD2MRAM 数据输出接到主机的接收线WP#输入上拉到 3.3V写保护直接拉高保证可写HOLD#输入上拉到 3.3V暂停通讯不用时必须拉高VDD电源3.3V供电需要就近去耦VSS电源GND地这里最值得注意的一点是我把 CS 接到了普通 GPIO而不是直接用 SSI 的硬件 FSS 引脚。原因很简单SPI 设备通常需要在一个完整命令期间一直把 CS 拉低但 Tiva 的硬件 FSS 会在每个数据字之间自动拉高。如果直接用它控制 MRAM多字节的 WRITE 命令会被硬生生拆成几个独立事务数据就报废了。GPIO 手动拉 CS 虽然多占一个引脚但时序完全可控后面如果要扩展第二个 SPI 设备也方便。2.2 电源、去耦和 SPI 信号完整性MRAM 工作电压是 3.3V但工业现场电源纹波往往比实验室环境大所以我在 VDD 引脚旁边放了一颗 100nF 陶瓷电容紧贴封装放置这是最基本的。如果板子上空间允许再放一颗 1uF 的钽电容做低频滤波效果更好。SPI 时钟信号在长线传输时容易出现振铃尤其是 CLK 频率超过 10MHz 以后。我习惯在每个 SPI 信号线上串联 33Ω 的电阻放在靠近主机引脚的位置。这个电阻能吸收一部分反射配合接收端的输入电容可以有效削弱过冲。SI、SO、SCK 这三根线都要串CS 引脚虽然不要求高速但电源纹波掉到地弹时 CS 容易误触发所以也建议串联一个小电阻或者至少加一个 10kΩ 上拉到 VDD。还有一点是地平面的完整性。MRAM 和 MCU 之间的 SPI 线尽量走同一层不要跨越大的地平面开槽。现场继电器、电机一动作如果地平面被切断地弹和环路天线会直接耦合到 SPI 总线上轻则误码重则烧芯片。我在原型板上吃过这个亏后来重新布局才解决。2.3 容易被忽略的两个引脚WP# 和 HOLD#写保护脚 WP# 是很多人第一次上手时翻车的地方。MR25H40CDF 在 WP# 为低电平时会阻止对状态寄存器或者受保护区域的写操作。如果你在代码里发了 WRITE 命令但没有拉高 WP#数据写不进去而且不会报任何错误。HOLD# 引脚则是用来暂停通讯的。当一个外设正在处理某些内部逻辑时主机可以拉低 HOLD# 让总线暂停不需要重新同步。对于 MRAM 来说这个特性偶尔有用但如果不接必须把它拉高不能悬空。悬空状态下引脚电平不确定现场有电磁干扰时可能会随机触发暂停造成读写字节错位。我见过一些参考设计为了让用户灵活控制把 WP# 也接到了 MCU 的 GPIO。但实际项目里写保护策略如果已经由软件层管理硬件上直接把 WP# 拉高更省心。你可以在系统上电后通过 WRSR 指令设置状态寄存器保护而不是依赖引脚电平。3. 从指令层面理解 MR25H40CDF3.1 基本指令集与地址格式MR25H40CDF 的指令集非常精简跟 SPI NOR Flash 很类似但命令字不完全一样。最常用的就六个指令操作码功能描述WREN0x06写使能设置状态寄存器里的 WEL 位WRDI0x04写禁止清除 WEL 位RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03从指定地址读数据WRITE0x02从指定地址写数据地址是 24 位的也就是一次要发送三个地址字节。虽然 MR25H40CDF 最大容量只有 512KB地址只需要 19 位但命令格式还是按标准 SPI 存储器的规矩来高字节可以先发 0。真正要用的地址范围是 0x00000 到 0x7FFFF超过这个范围再往上写地址计数器会回绕到 0所以驱动里一定要做长度校验。READ 和 WRITE 命令都支持连续地址访问。只要 CS 一直保持低电平发送完命令和地址之后每个时钟沿都会读出一个字节或者写入一个字节内部地址会自动加 1。这个特性用来读写日志、参数块非常方便。3.2 WREN、状态寄存器和块保护机制SPI 存储器基本都有一个写使能锁存机制MRAM 也不例外。每次执行 WRITE 或 WRSR 之前必须先执行 WREN也就是发送 0x06把状态寄存器里的 WEL 位置 1。执行完之后 WEL 会自动清 0。也就是说一条 WRITE 命令前必须紧跟一条 WREN不能省。状态寄存器里除了 WEL还有一组块保护位 BP。默认情况下MRAM 上电后可能处于某种写保护状态或者至少不会被其他命令干扰。我在初始化代码里做了一件事先 WREN然后 WRSR 写入 0x00把所有块保护位清零顺便清掉 WPEN。这样后面的写入操作就不受状态寄存器层面的阻碍了。static void MRAM_DisableProtection(void) { MRAM_WriteEnable(); MRAM_ChipSelect(0); MRAM_Transfer(MRAM_CMD_WRSR); MRAM_Transfer(0x00); MRAM_ChipSelect(1); }注意不同批次的数据手册里块保护位的位定义可能略有差异但写入 0x00 来关闭保护是最通用的做法。如果你的产品需要真正实现“只读固件区”可以反向利用这些位这个是后话。3.3 “无页边界”给代码带来了什么便利这里要说一个 MRAM 和 Flash 最本质的区别。SPI NOR Flash 的写入是页编程通常一页 256 字节而且写之前必须做扇区擦除。擦除操作不仅慢还会把整个扇区清空。所以 Flash 驱动的代码要操心页对齐、扇区搬移、磨损均衡、掉电保护。这些逻辑写起来比读出麻烦得多而且每个型号的 Flash 页大小还不一样移植性很差。MRAM 没有页的概念也没有擦除周期。你只需要把 CS 拉低发 WRITE 命令和地址然后一个字节一个字节地发送数据最后拉高 CS数据就全部写进去了。地址可以从任意位置开始长度也没有 256 字节的限制只要不超过整颗芯片的容量就行。这省掉的不仅是代码量更是整个存储管理策略的复杂度。我在项目里就是这么用的把整个存储区看成一个大数组想往哪个偏移写就往哪写想写多少就写多少。这大概就是 MRAM 名字里 RAM 的含义它真的可以像内存一样用只不过掉电不丢数据。4. 基于 TM4C129XNCZAD 的驱动实现4.1 SSI 初始化时钟、模式和片选策略我用的是 TivaWare 驱动库初始化代码不长但有几个参数必须解释清楚。#include stdint.h #include stdbool.h #include inc/tm4c129xnczad.h #include driverlib/sysctl.h #include driverlib/gpio.h #include driverlib/ssi.h #define MRAM_SSI_BASE SSI0_BASE #define MRAM_SSI_PORT GPIO_PORTD_BASE #define MRAM_SSI_PINS (GPIO_PIN_0 | GPIO_PIN_2 | GPIO_PIN_3) #define MRAM_CS_PORT GPIO_PORTB_BASE #define MRAM_CS_PIN GPIO_PIN_5 #define MRAM_CMD_WREN 0x06 #define MRAM_CMD_WRDI 0x04 #define MRAM_CMD_RDSR 0x05 #define MRAM_CMD_WRSR 0x01 #define MRAM_CMD_READ 0x03 #define MRAM_CMD_WRITE 0x02 void MRAM_Init(void) { SysCtlPeripheralEnable(SYSCTL_PERIPH_SSI0); SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOD); SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOB); GPIOPinConfigure(GPIO_PD0_SSI0CLK); GPIOPinConfigure(GPIO_PD2_SSI0RX); GPIOPinConfigure(GPIO_PD3_SSI0TX); GPIOPinTypeSSI(MRAM_SSI_PORT, GPIO_PIN_0 | GPIO_PIN_2 | GPIO_PIN_3); GPIOPinTypeGPIOOutput(MRAM_CS_PORT, MRAM_CS_PIN); GPIOPinWrite(MRAM_CS_PORT, MRAM_CS_PIN, MRAM_CS_PIN); SSIConfigSetExpClk(MRAM_SSI_BASE, SysCtlClockGet(), 20000000, SSI_FRF_MOTO_MODE_0, SSI_MODE_MASTER, 8, 0); SSIEnable(MRAM_SSI_BASE); }时钟我定在了 20MHz没有直接冲 40MHz。原因不是 MRAM 跑不到 40MHz而是现场线缆长度、连接器质量、电源纹波都会影响高速 SPI 的余量。20MHz 在大多数 PCB 布局里都有很高的噪声容限读写 512KB 的数据块也只要几十毫秒对应用来说足够了。SPI 模式选择的是摩托罗拉帧格式 Mode 0也就是时钟空闲为低电平第一个边沿采样。MR25H40CDF 支持 Mode 0 和 Mode 3一般手册推荐 Mode 0那就用 Mode 0。如果代码读出来全是 0xFF先检查一下 SSIConfigSetExpClk 里的模式参数很多时候是这里选错了。还有一个细节PD1 是 SSI0 的硬件 FSS 引脚但我没有把它配置成 SSI 功能因为 CS 由 GPIO 手动控制。如果你不小心把 PD1 也设成了 SSI 复用硬件 FSS 会在每个字节传输间隙自动拉低这跟手动 CS 会打架。所以我只配置了 CLK、RX、TX 三根针脚。4.2 底层字节传输函数SPI 是全双工协议MCU 每发送一个字节同时也会收到一个字节。即使我们只想从 MRAM 读数据也必须持续发送时钟而发送的字节内容是什么其实无所谓。这个底层函数就是标准的“交换字节”。static void MRAM_ChipSelect(uint8_t level) { GPIOPinWrite(MRAM_CS_PORT, MRAM_CS_PIN, level ? MRAM_CS_PIN : 0); } static uint8_t MRAM_Transfer(uint8_t txByte) { uint32_t rxByte; SSIDataPut(MRAM_SSI_BASE, txByte); while(SSIBusy(MRAM_SSI_BASE)) {} SSIDataGet(MRAM_SSI_BASE, rxByte); return (uint8_t)rxByte; }这里踩过一个坑SSIDataPut 只负责把数据塞进发送 FIFOSSIBusy 并不是等 FIFO 空而是等整个移位过程结束。一次 Put 之后必须等 Busy 清除再调 SSIDataGet 把接收 FIFO 里的数据读出来否则接收 FIFO 会积压多传几次后 SSI 外设就会报告溢出错误。刚开始写驱动时我漏掉了 SSIDataGet结果读出来的数据总是少了一位数据全串行错位。如果你要进一步提升性能可以用 TivaWare 的中断或者 uDMA 方式做大数据块读写但底层原理是一样的。现场项目里我先用轮询把功能跑通再决定要不要上 DMA。4.3 核心读写函数含地址处理写使能是每次写操作前必须做的一步单独封装成一个子函数static void MRAM_WriteEnable(void) { MRAM_ChipSelect(0); MRAM_Transfer(MRAM_CMD_WREN); MRAM_ChipSelect(1); }写函数就非常简单了void MRAM_WriteBytes(uint32_t addr, const uint8_t *buf, uint16_t len) { uint16_t i; if ((addr len) 0x80000) { return; } MRAM_WriteEnable(); MRAM_ChipSelect(0); MRAM_Transfer(MRAM_CMD_WRITE); MRAM_Transfer((addr 16) 0xFF); MRAM_Transfer((addr 8) 0xFF); MRAM_Transfer(addr 0xFF); for (i 0; i len; i) { MRAM_Transfer(buf[i]); } MRAM_ChipSelect(1); }读函数跟写函数几乎对称只是命令字变成 READ而且在读数据阶段主机要连续发送 0 来产生时钟void MRAM_ReadBytes(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t i; if ((addr len) 0x80000) { return; } MRAM_ChipSelect(0); MRAM_Transfer(MRAM_CMD_READ); MRAM_Transfer((addr 16) 0xFF); MRAM_Transfer((addr 8) 0xFF); MRAM_Transfer(addr 0xFF); for (i 0; i len; i) { buf[i] MRAM_Transfer(0xAA); } MRAM_ChipSelect(1); }代码里的地址是 32 位无符号但只用了低 19 位也就是最大到 0x7FFFF。这个容量对应 4Mbit。发送地址时按 24 位协议拆成三个字节先发高位再发低位。为什么要做长度校验因为前面说过地址计数器在越界后会回绕到 0。如果不做限制写入到 0x7FFFF 之后继续写数据会被写回起始地址导致原有参数被覆盖而且很难排查。这个校验看似简单在实际产品里能救命。4.4 在中断环境和大文件传输中的注意事项很多人以为 MRAM 写数据只要几条命令就够了但在嵌入式系统里“事务不被中断”同样重要。MR25H40CDF 没有像 Flash 那样的写忙状态你发完最后一个数据字节、拉高 CS写入就完成了。但问题是如果在 CS 拉低期间MCU 被一个高优先级中断打断而且这个中断的时间足够长Cortex-M 不会自动继续后续 SPI 时钟。此时 CS 仍然是低电平MRAM 正等在这个事务的中间状态一旦中断回来继续发送数据命令里后面的字节可能已经错位了。我在代码里做了一个简单的临界区保护在最坏情况下把整个 MRAM 写操作包在一个关中断的任务里#include driverlib/interrupt.h void MRAM_WriteBytesAtomic(uint32_t addr, const uint8_t *buf, uint16_t len) { uint32_t flags IntMasterDisable(); MRAM_WriteBytes(addr, buf, len); IntMasterRestore(flags); }关中断期间不能做耗时过于离谱的事。MRAM 写一个 512 字节的数据块在 20MHz SPI 下大概是 0.2ms 多一点这个时间对绝大多数应用来说是可以接受的。但如果你要写 64KB 的数据关中断 30ms 就太过分了那就要考虑 DMA 方式或者把大块数据拆成多个小事务每个事务之间允许中断响应。RTOS 环境里临界区要换成互斥量和调度器挂起思路一致。核心原则是CS 低电平期间这条 SPI 总线上只能有一个任务说话谁都不能抢。5. 现场调试、问题定位与避坑清单5.1 写不进去先从状态寄存器下手我调试 SPI 存储器的第一反应永远是读状态寄存器。MR25H40CDF 的 RDSR 命令是 0x05CS 拉低后发这个字节再发一个字节的哑数据就能把状态寄存器的内容读出来。uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_ChipSelect(0); MRAM_Transfer(MRAM_CMD_RDSR); status MRAM_Transfer(0x00); MRAM_ChipSelect(1); return status; }如果写完数据发现读回来是 0xFF 或者 0x00第一件事就是读状态看 WEL 位是不是为 1。WEL 位在发送 WREN 之后应该立刻变成 1如果它还是 0说明 WREN 没有正确执行要么 CS 时序不对要么 WREN 字节根本没发出去。在逻辑分析仪上正常的写流程是CS 先拉低发送 0x06CS 拉高再次拉低发送 0x02 3 字节地址 数据最后 CS 拉高。如果你看到 CS 在 0x06 后面没有拉高而是直接发 0x02那 WEL 位很可能还是 0写操作会被忽略。5.2 数据对不上FIFO 和 SPI 模式的坑数据错位是 SPI 调试里最隐蔽的问题。常见原因有两种。第一种是前面说过的发送完指令字节后没有及时读取接收 FIFO导致 RX FIFO 溢出后续数据从错误的偏移开始。解决办法就是在每一次 SSIDataPut 之后用 SSIDataGet 把接收到的字节读走哪怕这个字节是垃圾数据。第二种是 SPI 模式不对。Mode 0 和 Mode 3 的时钟极性和相位正好相反。如果初始化时选成 Mode 3而 MRAM 工作在 Mode 0数据采样点就会正好落在信号翻转沿上读出来的数据会有一半是对的、一半是错的偶尔还会出现连续全 0xFF。确认方法很简单用示波器看 SCK 空闲电平Mode 0 下 SCK 空闲为低Mode 3 下空闲为高。5.3 工业现场干扰导致的偶发错乱实验室里跑得好的程序到了现场偶尔丢数据这是工业嵌入式开发最头疼的事。我遇到过一种情况继电器每次动作MRAM 里某个区域的数据就可能变乱。后来用示波器抓电源线发现继电器吸合的瞬间3.3V 电源上有将近 1V 的尖峰。这个问题的根源不只是去耦电容不够而是 MRAM 的 CS 引脚和电源走线距离继电器驱动电路太近。高频浪涌直接耦合到了 CS 和 SPI 线上导致 MRAM 在非预期时刻收到了错误信号。解决方法是三管齐下把存储相关的信号线远离继电器驱动区域SPI 线加串联电阻并缩短长度电源输入端加一个磁珠加 100uF 电解电容的组合。改版之后再没有出现过偶发错乱。软件层面也做了兜底每条数据记录都加了 CRC 校验读取失败时读取备份区。这个习惯我一直保留因为再好的硬件也不能保证永远不被外部干扰而备份是最后一道防线。5.4 掉电保存的实测方案工业设备最看重的是掉电保存能力。我用 TM4C129XNCZAD 的欠压检测中断做过一次实测交流电源突然断开后MCU 供电电压跌落到某阈值时欠压中断被触发系统从当前程序里跳转到一个紧急保存函数把 64 字节的运行状态写入 MRAM。MRAM 的好处在这里体现得很明显。掉电时刻留给软件的时间非常短通常只有几百微秒到几毫秒。Flash 写一个扇区可能就要 20 到 50 毫秒虽然可以先写 RAM 再判断电压恢复但风险很高。MRAM 写 64 字节在 20MHz SPI 下只需要 32 纳秒乘以 64就算加上中断跳转和函数调用开销也就几十微秒完全能赶在复位之前完成。实测结果很稳连续上百次掉电每次都能在复位后准确读到最后的运行状态。这个性能是 Flash 方案很难给的。6. 项目复盘与后续扩展6.1 循环写入 10 万次后的参数变化项目做完后我专门做了一次耐久性验证。测试逻辑非常简单往地址 0x00000 写入 0xA5、0x5A、0x55、0xAA 四个固定字节然后立即读出来比对连续循环 10 万次。10 万次对 Flash 来说已经接近寿命上限对 MRAM 来说只是热身。整个测试跑下来读写没有任何一次超时数据也全部一致。我还把测试温度放在 60℃ 左右的箱体里跑了一遍同样没有发现性能衰减。这个结果让我很放心。以前用 EEPROM 和 Flash总是怕磨损导致某个扇区失效代码里要写磨损均衡要定期搬移数据出了问题还难定位。换成 MRAM 后存储区我敢随便乱写写坏了再写完全不需要考虑寿命问题。6.2 还能用 MRAM 做什么这次项目验证了基础读写和掉电保存但 MRAM 的发挥空间远不止这些。现在这块板子上还预留了计划用的升级方案把设备运行日志、故障码、累计工作时间这些频繁更新的数据全部从 Flash 挪到 MRAM 里。以后系统重启这些数据不用再像以前那样经历漫长的“恢复”过程。我另外想做的一个应用是远程升级标识。OTA 固件升级最怕升级到一半掉电设备变砖。现在可以把升级状态、已完成分区的 bit map、校验值等信息写到 MRAM 里每次升级开始前更新一次状态。即使升级失败bootloader 也能根据 MRAM 里的标识自动进入恢复模式重试上一次固件包。这个方案比在 Flash 尾部找一块固定区域做标志位可靠得多因为标志位本身也会被频繁改写Flash 不一定扛得住。另外如果你手头有并行接口的 MRAM还可以直接挂在总线控制器上做内存扩展实现真正的 XIP 执行。不过 MR25H40CDF 是 SPI 接口想要这种效果只能选并行 MRAM 型号设计思路会完全不一样。最后说一点个人体会把 MRAM 接到 MCU 上难的不是 SPI 时序而是你是否真的把它当成 RAM 来用。既然它不磨损、不需要擦除、写入又快那设计思路就该向内存靠拢把频繁变化的系统状态直接写进去。很多以前因为 Flash 寿命不得不做的歪门邪道都可以丢进垃圾桶了。这个项目做完之后我再看到“频繁存储掉电保存”的需求第一反应就是放大 MRAM而不是跟 Flash 的扇区较劲。
返回列表