ARTICLE DETAIL

资讯详情

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

MRAM替代SPI Flash:TM4C1299工业数据掉电保存与SSI读写方案

MRAM替代SPI Flash:TM4C1299工业数据掉电保存与SSI读写方案 做工业嵌入式项目最绕不开的一个问题就是数据掉电保存和可靠读写。我之前做电机控制器的运行日志记录时用 SPI Flash 存数据结果擦除慢、磨损快、掉电丢数据这些问题一个接一个冒出来折腾了两周后果断换了思路用 MR25H40CDF 这颗 MRAM 存储数据搭配 TM4C1299NCZAD 的 SSI 接口来读写整套方案才算真正稳下来。这篇博客就围绕这套组合展开从为什么选 MRAM、硬件怎么接、读写时序怎么对到基于 TivaWare 的驱动代码怎么落最后再聊几个实际调试中踩过的坑适合正在做工业嵌入式开发、尤其是有掉电保存和数据记录需求的工程师参考。内容不搞虚的基本都能直接抄。1. 为什么工业存储会盯上 MRAM 这颗料1.1 闪存方案在工业场景里的三个硬伤工业设备里最常用的数据存储是 SPI NOR Flash 和 EEPROM但用久了你会发现它们都有让人头疼的一面。第一个是擦除机制NOR Flash 的写入有限制不能像 SRAM 那样直接覆盖要先擦除块而一块往往好几 KB 甚至几十 KB擦一下就是几百毫秒。日志记录这种高频小数据写入每次都要执行“读出—改写—擦除”的流程时间全耗在擦除上实时性根本谈不上。第二个是寿命问题。普通 SPI Flash 擦写寿命一般是十万次左右看着还行但车速、电流、温度这类运行参数如果每秒存一次加上掉电瞬间的多次写入几个月就能把区块磨穿。我见过一个现场设备就是日志写太频繁Flash 坏块引发整包数据损坏售后排查了半天才定位到存储芯片。第三个是掉电一致性。很多控制器在主电源断开后才保存关键参数这时候 Flash 正在擦除电已经不稳了写入一半就会留下一个既不是新数据也不是旧数据的中间态。你当然可以靠双备份和 CRC 弥补但闪存本身“先擦后写”的特点让这个问题变得特别难缠。工业场景里存储设备不仅要存得下还要扛得住Flash 在这类需求面前确实有点力不从心。1.2 MR25H40CDF 的核心特点与选型理由MR25H40CDF 是 Everspin 推出的一颗 4Mbit 串行 MRAM接口跟 SPI NOR Flash 几乎一样指令集里也有 READ、WRITE、WREN 这些熟悉的名字但它内部的存储单元是磁阻结原理和 Flash 完全不同。MRAM 写入时直接翻转磁化方向不需要擦除所以写操作就是纯覆盖没有“先擦后写”的步骤。这颗料最让我满意的是写入性能。MR25H40CDF 最高工作在 40MHz SPI 时钟下写入命令发出后数据字节按时钟连续送进去即可写完也不存在长等待时间。对工业控制器来说这就意味着即使主电源已经掉到临界电压MCU 还是有足够时间把最后几十个字节的关键状态写进 MRAM而不像 Flash 那样擦除一次就要等几个毫秒甚至几十毫秒。寿命方面更有意思。MR25H40CDF 的写耐久性标称达到 10 的 14 次方量级几乎可以认为写不坏。以前用 Flash 要考虑磨损均衡每个块轮流用还得记块表换到这颗 MRAM 后同一地址反复写完全没心理负担。再加上工业级温度范围、数据保持期超过二十年、掉电数据不丢失它就是工业存储里那种“一个顶三个”的存在。1.3 TM4C1299NCZAD 与 MRAM 组合的适配点TM4C1299NCZAD 是 TI Tiva 系列里比较高配的一颗 Cortex-M4F 主控主频 120MHz内置 256KB SRAM 和 1MB Flash还有双 SSI、以太网、USB 等外设。做工业控制和数据采集时它的外设资源基本够用不用为了存储专门加一颗 SPI 控制器。这个组合的适配点在于 SSI 接口。TM4C1299 的 SSI 外设支持标准 SPI 模式时钟极性可配FIFO 深度也够跑 10MHz 或 20MHz 的 SPI 时钟毫无压力而 MR25H40CDF 的理论上限是 40MHz两者一配合读写速度瓶颈反而不在外设在于我们自己的代码逻辑。再从板级看MRAM 工作电压 3.3VTM4C1299 的 GPIO 也是 3.3V 电平不需要额外电平转换接线非常干净。很多嵌入式工程师手里本来就有 TM4C1299 的开发板加一个 8 脚 MRAM 就能把掉电存储能力补齐这也是我推荐这套方案的原因之一。2. 硬件连接与引脚规划先把这些线接对2.1 认识 MR25H40CDF 的引脚功能MR25H40CDF 常见封装是 8 脚 DFN引脚不多但每个引脚都有讲究。下面这张表可以帮你快速建立印象。引脚名称方向作用1CS输入片选低电平有效2SCK输入SPI 时钟3SI输入主发从收接 MCU 的 MOSI4SO输出主收从发接 MCU 的 MISO5WP输入写保护低电平有效6HOLD输入暂停传输低电平有效7GND电源地8VCC电源3.3VWP 和 HOLD 这两个引脚在实际项目里特别容易踩坑。如果你不需要硬件写保护或者暂停功能一定把它们通过电阻上拉到 VCC而不是直接悬空。因为这两个引脚内部没有保证默认高电平一旦受噪声干扰被拉低要么写入命令全被忽略要么正在传输的数据卡在半路排查起来很费劲。2.2 TM4C1299NCZAD 的 SSI 引脚分配SSI 外设的引脚是通过复用功能映射到 GPIO 上的理论上可以接到多个引脚组但实际板上走线、LaunchPad 排针位置都会影响选择。以常用的 SSI0 为例我习惯接 PD0、PD2、PD3片选用 PE4 单独控制信号MRAM 引脚TM4C1299 引脚复用功能SCLKSCKPD3SSI0CLKMOSISIPD0SSI0XDAT1MISOSOPD2SSI0XDAT0CSCSPE4GPIO 输出为什么片选不用硬件 FSS而是用一个普通 GPIO 来控制因为软件可以精确决定每条命令什么时候拉低、什么时候拉高。硬件 FSS 在某些库函数封装下会在 FIFO 没取完数据时就提前拉高导致 MRAM 认为命令提前结束把最后一个字节丢掉。换成 GPIO 控制后命令时序完全在掌控之中调试也直观很多。2.3 电源、去耦与 DFN 封装的注意事项MR25H40CDF 对电源质量不算苛刻但工业环境里纹波和瞬态跌落常有建议在 VCC 引脚旁边放一个 100nF 陶瓷电容如果板上有空间再加一颗 4.7uF 或 10uF 的钽电容做低频去耦。电容要尽量靠近芯片引脚不要隔着一大段走线否则高频噪声和电源跌落问题会直接反映在 SPI 读写出错上。DFN 封装的焊接要格外留意。这种封装底部通常有一个裸露焊盘有的芯片要求它接地有的则要求悬浮或者只做散热连接。我的建议是拿到具体型号的数据手册后先看“Exposed Pad”一节别想当然地把焊盘直接大面积铺铜接地因为铺铜吸热会导致回流焊时助焊剂挥发不充分引脚虚焊概率大增。焊接后最好用放大镜逐个检查引脚特别是 SO 和 HOLD 这类容易被忽视的引脚。3. 读写原理与时序MCU 侧才能真正用好它3.1 指令集梳理与状态寄存器解读MR25H40CDF 的指令集看起来跟 SPI Flash 高度相似刚开始我也担心要改一堆代码结果一查核心命令基本是现成的指令操作码说明WREN0x06写使能WRDI0x04写禁用RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据状态寄存器的 bit0 是 WIP表示内部写操作是否在进行bit1 是 WEL表示写使能锁存状态往上的块保护位用于锁定存储区。很多人写 MRAM 时只发 WRITE 命令忘了发 WREN结果数据写不进去就是因为状态寄存器里的 WEL 没有被置位。MRAM 没有擦除命令也不需要执行擦除流程写入的本质就是覆盖原值这是它跟 NOR Flash 在操作习惯上最大的区别。3.2 一次完整读与写的时序推演理解时序比背操作码重要。以读命令为例整个过程是这样的CS 拉低发送 0x03随后发送三字节地址高字节在前再发送若干个空字节把 MISO 上的数据读回来最后 CS 拉高。地址一共是 24 位但 MR25H40CDF 容量只有 512KB实际有效地址位是 19 位高 5 位必须保持为 0否则地址越界后读到的是什么鬼东西就不一定了。写命令也差不多CS 拉低后发送 0x02、三字节地址然后一个接一个把数据字节送进去最后 CS 拉高。关键在于 CS 拉高的一瞬间MRAM 才把数据真正锁存到存储阵列所以整条命令的字节必须全部发送完毕才能拉高 CS。如果中间因为 FIFO 溢出多等了一段时钟问题不大但要是 CS 提前拉高最后一个字节大概率就丢了。这也是我前面强调用 GPIO 做片选的原因时序控制上没有模糊地带。3.3 掉电保存与循环记录区设计MRAM 写入虽然快但掉电保存不能只靠硬件。实际做方案时一般分成三级第一级是 MCU 检测到电源跌落立刻进入紧急处理把当前关键状态压缩成一个精简帧写入 MRAM第二级是在 MRAM 中预分配一个固定区域用来存放运行参数和报警记录第三级才是普通日志按环形缓冲不断覆盖旧数据。环形缓冲在 MRAM 上实现起来特别舒服因为不需要擦除老数据写完直接覆盖就行。我在很多项目里用的结构是存储区开头放一个写索引后面依次放记录条目每条记录带魔数、序号和 CRC 校验。读取时从索引找到最新记录位置往前回溯碰到校验失败的记录就停止这样即使掉电中断导致某一条写了一半也不会影响前面的历史数据。设计上坚持“记录必须带校验”这个习惯能帮你省下大量后期排查的时间。4. 基于 TivaWare 的完整代码实现4.1 初始化 SSI 和 GPIO把基础配置做对下面这段代码是把 SSI0 配置成主模式、8 位数据、SPI 模式 0SPI 时钟先压到 10MHz。跑通之后你可以再往上提但第一次上电调试建议保守一点。#include stdint.h #include tm4c129x.h #include driverlib/sysctl.h #include driverlib/ssi.h #include driverlib/gpio.h #include driverlib/pin_map.h #define MR_CS_PORT GPIO_PORTE_BASE #define MR_CS_PIN GPIO_PIN_4 void MR_Init(void) { SysCtlPeripheralEnable(SYSCTL_PERIPH_SSI0); SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOD); SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOE); while(!SysCtlPeripheralReady(SYSCTL_PERIPH_SSI0)) {} while(!SysCtlPeripheralReady(SYSCTL_PERIPH_GPIOD)) {} while(!SysCtlPeripheralReady(SYSCTL_PERIPH_GPIOE)) {} GPIOPinConfigure(GPIO_PD0_SSI0XDAT1); // MOSI - MRAM SI GPIOPinConfigure(GPIO_PD2_SSI0XDAT0); // MISO - MRAM SO GPIOPinConfigure(GPIO_PD3_SSI0CLK); // SCLK - MRAM SCK GPIOPinTypeSSI(GPIO_PORTD_BASE, GPIO_PIN_0 | GPIO_PIN_2 | GPIO_PIN_3); GPIOPinTypeGPIOOutput(MR_CS_PORT, MR_CS_PIN); GPIOPinWrite(MR_CS_PORT, MR_CS_PIN, MR_CS_PIN); // 空闲拉高 SSIConfigSetExpClk(SSI0_BASE, SysCtlClockGet(), SSI_FRF_MOTO_MODE_0, SSI_MODE_MASTER, 10000000, 8); SSIEnable(SSI0_BASE); }这里有个细节值得说SysCtlClockGet()取的是当前系统时钟频率而不是固定写死一个 120MHz 的常量。这样做的好处是如果工程之后改用低主频模式或者动态降频SSI 配置会根据实际时钟自动计算分频系数不会出现 SPI 波特率跑偏的问题。4.2 封装读状态、写使能、按地址读写函数下面的函数是把最底层的 SPI 字节收发封装好再往上写 MRAM 的指令。我的习惯是先把MR_SpiByte写稳定因为后面所有命令都依赖它。static void MR_CS_Low(void) { GPIOPinWrite(MR_CS_PORT, MR_CS_PIN, 0); } static void MR_CS_High(void) { GPIOPinWrite(MR_CS_PORT, MR_CS_PIN, MR_CS_PIN); } static uint8_t MR_SpiByte(uint8_t out) { uint32_t rx; SSIDataPut(SSI0_BASE, out); while(SSIBusy(SSI0_BASE)) {} SSIDataGet(SSI0_BASE, rx); return (uint8_t)rx; } static void MR_WriteEnable(void) { MR_CS_Low(); MR_SpiByte(0x06); MR_CS_High(); } uint8_t MR_ReadStatus(void) { uint8_t status; MR_CS_Low(); MR_SpiByte(0x05); status MR_SpiByte(0xFF); MR_CS_High(); return status; } void MR_WriteBuffer(uint32_t addr, const uint8_t *buf, uint32_t len) { uint32_t i; MR_WriteEnable(); MR_CS_Low(); MR_SpiByte(0x02); // WRITE MR_SpiByte((addr 16) 0xFF); MR_SpiByte((addr 8) 0xFF); MR_SpiByte(addr 0xFF); for(i 0; i len; i) { MR_SpiByte(buf[i]); } MR_CS_High(); } void MR_ReadBuffer(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t i; MR_CS_Low(); MR_SpiByte(0x03); // READ MR_SpiByte((addr 16) 0xFF); MR_SpiByte((addr 8) 0xFF); MR_SpiByte(addr 0xFF); for(i 0; i len; i) { buf[i] MR_SpiByte(0xFF); } MR_CS_High(); }写使能为什么单发一次0x06就要立刻拉高 CS这是指令本身的要求CS 的上升沿才把 WEL 位置位如果后面直接发 WRITE 命令而不拉高 CS控制器可能一直停留在写使能指令状态里。我之前偷懒把 WREN 和 WRITE 合并成一个连续时序结果多次写入只成功了一次这就是细节问题。MR_SpiByte用SSIBusy等待是因为 SSI 发送和接收是同时进行的发送最后一个字节后必须等总线不再忙才能从接收 FIFO 里把数据取出来。如果不等就直接SSIDataGet可能在 FIFO 空的时候读到旧数据造成错位。这个等待看起来多花了几微秒但在 10MHz 时钟下完全无所谓。4.3 数据记录示例用固定索引实现环形日志最后给一个可以直接用的日志结构。这里定义一个记录结构包含魔数、序号、采集值和一个 CRC 字段日志区放在从 0x10000 开始的地址写索引存在 0x7FF00 地址处。#include stddef.h typedef struct { uint32_t magic; // 0xA5A55A5A uint32_t seq; // 自增序号 uint16_t adc_value; // 示例数据 uint16_t crc16; // 前面字段的校验 } log_record_t; #define LOG_BASE 0x10000 #define LOG_COUNT 256 #define LOG_INDEX_ADDR 0x7FF00 static uint16_t log_index 0; uint16_t CalcCRC16(const uint8_t *data, uint32_t len) { uint16_t crc 0xFFFF; while(len--) { crc ^ *data; for(int i 0; i 8; i) { if(crc 0x01) crc (crc 1) ^ 0xA001; else crc 1; } } return crc; } void Log_Init(void) { MR_ReadBuffer(LOG_INDEX_ADDR, (uint8_t *)log_index, sizeof(log_index)); if(log_index LOG_COUNT) { log_index 0; MR_WriteBuffer(LOG_INDEX_ADDR, (uint8_t *)log_index, sizeof(log_index)); } } void Log_Save(log_record_t *rec) { uint32_t addr LOG_BASE (log_index % LOG_COUNT) * sizeof(log_record_t); rec-magic 0xA5A55A5A; rec-crc16 CalcCRC16((const uint8_t *)rec, offsetof(log_record_t, crc16)); MR_WriteBuffer(addr, (const uint8_t *)rec, sizeof(*rec)); log_index (log_index 1) % LOG_COUNT; MR_WriteBuffer(LOG_INDEX_ADDR, (uint8_t *)log_index, sizeof(log_index)); }读取日志时按log_index倒序往回找对每条记录先校验 magic 和 CRC校验通过了才认为是有效记录。这种方案的优点是写入时不需要擦除、不需要维护块表掉电把索引写了一半时上电后Log_Init也能从索引异常值恢复正常。实测这套代码在电机控制器里连续跑了几周记录数据没有出现整包错乱的情况。5. 实盘调试那些容易踩的坑和排查思路5.1 SPI 波形正常但读回全 0xFF这是最常见的现象看起来时钟和数据波形都对但 MRAM 读出来全是 0xFF。我最先怀疑的是接线后来发现大部分情况不是线的问题而是 WP 或者 HOLD 引脚悬空。DFN 封装的 HOLD 一旦没有上拉芯片在传输过程中可能随时被拉入暂停状态读出来的数据就变成全 1。另一个高发原因是 SPI 模式选错。MR25H40CDF 支持模式 0 和模式 3但有些开发板的 SPI 外设默认是模式 0宏定义里的 CPOL、CPHA 却配成了其他组合。调试时用示波器看一眼 SCLK 空闲电平如果是低电平说明是模式 0如果是高电平那就是模式 3。设错之后MISO 上采样点正好落在数据跳变的边沿上就会读出乱码或者全 0xFF。还有一类原因是地址越界。MR25H40CDF 只有 512KB有效地址范围是 0x000000 到 0x07FFFF如果你发到 0x080000 以上芯片没有对应的存储单元读出来自然都是 0xFF。写代码时可以在入口加一个addr len 0x080000的断言能省下很多不必要的排查时间。5.2 偶发丢字节与字节错位字节错位比丢字节更隐蔽。我遇到过一次现象每次读 16 字节最后 15 个是对的第一个总是上一个缓冲区的末尾数据典型表现就是数据整体往右移了一个字节。根本原因是 SSI 的接收 FIFO 没有及时清空或者读取时多读了一个旧字节。解决办法有两种一是每次 CS 拉低前把 FIFO 清空一次二是读数据循环里先用带超时的SSIDataGetNonBlocking把 FIFO 掏空。字节偶发丢失往往跟 SPI 时钟太高有关。测试板上用十几厘米的杜邦线连接 MRAM 时10MHz 以下基本稳定跑到 20MHz 就有概率丢字节。不是 MRAM 不支持高频而是飞线和 DFN 焊盘的寄生电容把信号边缘弄得很差。量产板尽量把走线控制在 5mm 以内如果暂时只用开发板验证把 SSI 频率降到 8MHz 或 10MHz先保证功能正确再优化速度。5.3 硬件焊接与电气噪声导致的问题DFN 封装还有一个容易被忽略的坑就是中心焊盘。曾经有一块板子读数据偶尔失败我量了 CS 和时钟都正常最后发现是 SO 引脚虚焊MISO 信号在低电平时只能拉到 0.8V 左右SSI 外设采样时逻辑电平不稳定。补焊之后故障直接消失。这种问题示波器量单根线往往看不出来因为高阻探头一接上去波形就变了最好在板上直接跑读写回环测试来判断。工业环境里的供电噪声也会干扰 MRAM。有一次现场板子只在电机启动瞬间读写失败查到最后是 24V 转 3.3V 的电源模块在重载时纹波突然拉高MRAM 的 VCC 跟着抖了一下CS 时序就乱了。解决方案是给 MRAM 的电源腿单独加磁珠和电容形成局部滤波问题就没有再出现。5.4 状态寄存器被意外锁定怎么办如果你发现 MRAM 可以读、但怎么写都不变十有八九是状态寄存器里的块保护位被置位了。有些旧板子之前用过 SPI Flash代码里可能自动执行过写状态寄存器的操作也有些电源抖动会把状态寄存器改成一个不可写的保护状态。排查方法是上电后先调用MR_ReadStatus()把状态寄存器打印出来看。正常时最低两位应该是 0如果看到 bit2 或 bit3 是 1就发WREN后再发WRSR 0x00把保护位清掉即可。为了不让这个问题再次发生量产代码里我一般会在初始化最后做一个强制姿态写一个测试模式到某个临时地址读回对比如果失败就自动执行一次解锁流程。嵌入式的世界从来不缺“看起来没问题实际已经被保护锁住”的情况加一个自检比事后猜原因有效得多。6. 最后想提的几个工程小习惯这套方案跑通之后我最大的收获不是用会了 MRAM而是养成了一套跟存储相关的开发习惯。第一个习惯是读写函数一定加地址和长度的边界检查。只要 MRAM 地址越界读出来都是 0xFF写进去也不会报错这种“假成功”比直接崩溃更难查。第二个习惯是给存储区划分明确的功能区间比如 0x00000 到 0x0FFFF 放出厂配置0x10000 到 0x7EFFF 放运行日志0x7F000 到 0x7FFFF 放系统索引。不同用途的数据互不干扰以后增加新功能只需要预留新的地址段不用动已有数据。第三个习惯是写完之后一定要读回验证尤其是关键配置。MRAM 可靠性再高MCU 引脚短路、PCB 虚焊这些环节还是存在读回验证一遍只用几微妙却能把大部分问题挡在生产测试阶段。我现在做存储驱动时都留一个verify_enable宏开发阶段打开量产阶段关闭灵活又省心。最后一个经验是别把所有存储任务都压在一颗芯片上。MR25H40CDF 适合做高频率、小批量的数据记录和掉电保存如果日志量大到几 MB 甚至几十 MB还是交给大容量 Flash 或者 SD 卡更合适。芯片选型没有“万金油”把每颗料放在它最擅长的位置系统整体才会稳定。这套 MRAM 加 TM4C1299 的组合我认为就是工业数据记录场景里性价比和可靠性都相当均衡的一套方案值得在下一版设计里认真考虑。
返回列表