ARTICLE DETAIL

资讯详情

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

STM32驱动SPI接口MRAM:免擦除、不掉电的工业存储方案

STM32驱动SPI接口MRAM:免擦除、不掉电的工业存储方案 前两年做一套工业现场的数据采集板最头疼的不是采集算法而是存储。NOR Flash 怕擦写次数到头EEPROM 容量小还要等写时间现场断电丢数据丢过好几回。后来我把存储介质换成 Everspin 的 MR25H40CDF——一颗 4Mbit 的 SPI 接口 MRAM搭档是同为老面孔的 STM32F100ZE整套存储方案反而一下子简单了不用擦除、写入微秒级、掉电以后数据照样在。这篇文章就把这套组合的硬件连接、驱动写法、工业场景里的掉电保存和日志设计以及我实测中踩过的坑一次说清楚。想找一套耐造、没磨损、断电不丢数据的存储方案的嵌入式工程师可以直接拿这篇文章当参考资料。1. MRAM 不是 FlashMR25H40CDF 这颗 4Mbit 存储器到底特殊在哪1.1 512K×8 的组织结构与引脚特性MR25H40CDF 是 Everspin 的 SPI 接口 MRAM磁阻随机存取存储器。所谓 MRAM是用磁性隧道结MTJ的磁化方向来存储数据磁化状态本身就是非易失的所以这颗芯片天然具备断电不丢、改写不疲劳两个特性。型号里的 MR25H40 表示 4Mbit 容量按 512K × 8 组织正好相当于 512KByte地址范围 0x00000 到 0x7FFFF。后缀 CDF 对应的是 8 脚 DFN 封装的小型版本和常见的 SOIC-8 相比体积小了很多适合板面积紧张的工业模块。供电是 3.3V工作电流和待机电流都不大待机电流手册上给到微安级深度掉电模式更低做电池供电的现场仪表也扛得住。接口就是标准 SPI 从机CS#、SCK、SI主机 MOSI、SO主机 MISO外加 HOLD# 和 WP# 两个控制引脚。SCK 最高能跑 40MHz对 STM32F100ZE 这种主频只有 24MHz 的 MCU 来说绰绰有余。1.2 和 NOR Flash、EEPROM、FRAM 的横向对比我习惯用这张表跟硬件同事对齐认知项目MR25H40CDFSPI MRAMNOR FlashEEPROMFRAM擦除不需要必须先擦成 0xFF 再写不需要但写慢不需要写时间微秒级页编程毫秒级毫秒级微秒级寿命无限次改写10 万次量级100 万次量级10^15 量级掉电保持20 年10~20 年10 年10 年字节写支持不支持支持支持这里最容易被忽略的是不用擦除带来的连锁好处。写 NOR Flash你得先保证目标扇区是 0xFFFlash 只能把 1 改成 0想写回 1 必须整块擦。MRAM 没有这个约束同一地址先写 0x00过一微秒再写 0xFF完全没问题位可以自由翻转。这意味着你的驱动里不需要任何扇区管理、擦除队列、磨损均衡之类的东西代码量和出错概率同时大幅下降。1.3 哪些场合值回票价MRAM 单价确实比普通 SPI NOR Flash 贵但它不是拿来跟 Flash 打性价比的。我实际用下来以下场景贵得有道理频繁改写的小数据区比如设备运行参数、累计量、配置项一天改几千次也不怕磨损掉电瞬间要原子的关键状态保存MRAM 写完后数据立刻稳固不需要考虑 Flash 擦除过程中的掉电窗口需要循环覆盖的现场日志旧日志直接覆盖新日志不需要设计耗时的磨损均衡长时间无人值守的设备比如变电站监测、石油管道采集节点换一次存储芯片等于一次现场人工成本这个隐性成本远比芯片差价高。反之如果你要存固件镜像、音视频、海量文件那是 NAND/SD 的领域MRAM 应该用来存元数据和索引给它一个信息中枢的位置而不是仓库的位置。2. 引脚分配、SPI 模式与去耦设计MR25H40CDF 和 STM32F100ZE 的硬件连接2.1 最小连接图五根线搞定通信STM32F100ZE 是 144 脚 LQFP 封装的 value line 系列512KB Flash、32KB SRAM虽然主频只有 24MHz但外设齐全、价格便宜在低成本工业控制板里非常常见。和 MR25H40CDF 对接我优先用 SPI1因为它挂在 APB2 总线上24MHz 的 APB2 时钟配合预分频能跑到 12MHz是这颗 MCU 上最快的 SPI。具体引脚分配如下MR25H40CDF 引脚STM32F100ZE 引脚配置CS#PA4普通推挽输出软件片选SCKPA5SPI1_SCK复用推挽输出SIPA7SPI1_MOSI复用推挽输出SOPA6SPI1_MISO浮空输入HOLD#3.3V上拉使无效WP#3.3V上拉禁硬件写保护VDD3.3V100nF 10µF 去耦电容VSSGND接大地连接就是这么五根信号线加电源地。SPI 通信不需要像并行总线那样考虑时序对齐只要保证线序正确、片选可靠剩下的问题基本都是信号质量层面的。2.2 HOLD# 和 WP# 千万不能悬空HOLD# 这个引脚很阴。它低电平时MRAM 会暂停当前 SPI 传输外部 SCK 和 CS# 的信号一律忽略。如果板子上这个脚悬空现场电磁环境一复杂干扰把它拉低几个微秒正在进行的读写序列就直接作废。我遇到过一次读数据偶发全 0xFF 的诡异现象排查到最后就是 HOLD# 悬空惹的祸。所以这两个引脚一律直接接 3.3VHOLD# 拉高禁止暂停功能WP# 拉高禁止硬件写保护让芯片处于纯粹按命令干活的状态。去耦电容也是老生常谈但必须做VDD 引脚旁边放一颗 100nF 陶瓷电容布局上尽量贴近引脚电源入口再加 10µF 电解电容。MRAM 写操作的瞬间电流变化比较快电源旁路做不好高速 SPI 传输时偶尔会出连续误码这种问题查起来非常耗时不如一开始就按规范布。2.3 软件片选为什么比硬件 NSS 可靠STM32 的 SPI1 有个硬件 NSS 引脚可以配置成自动管理片选但我从来不用它做 MRAM 这类外部 SPI 从机。硬件 NSS 主要应付多主机总线仲裁场景在单主机挂一个从机的时候它反而容易因为 NSS 信号时机的细微偏差触发各种保护逻辑比如模式错误标志一置位SPI 外设就罢工了。工业板卡上我统一用普通 GPIO 拉一根线做从机片选SPI 外设配置成 SPI_NSS_Soft软件里 Select 就是拉低 PA4Release 就是拉高 PA4想何时拉就何时拉命令字节、地址字节、数据的边界完全由自己控制出问题也好定位。3. 吃透命令集和状态寄存器再动手写驱动3.1 六个核心命令本质上是带锁存器的状态机MR25H40C 的操作命令和 SPI NOR Flash 非常相似如果你写过 W25Q 系列上手会很快。核心命令如下命令字节码后续数据作用WREN0x06无把内部写使能锁存 WEN 置 1WRDI0x04无把 WEN 清 0RDSR0x05读 1 字节读状态寄存器WRSR0x01写 1 字节配置写保护策略READ0x033 字节地址 连续读数据读数据WRITE0x023 字节地址 连续写数据写数据另外还有 DPD0xB9和 RDPD0xAB用于进入和退出深度掉电模式。工业设备上如果要求极低待机功耗可以在休眠前发 DPD唤醒后发 RDPD但我建议只有确认不再访问 MRAM 时才这么干因为 DPD 状态下芯片除了 RDPD 之外的所有命令都不响应初学阶段容易绕进去。3.2 WEN 写使能锁存忘了它写入等于白写MRAM 内部有一只写使能锁存器这是防误写机制的起点。任何 WRITE 或 WRSR 操作之前必须先发 WREN把 WEN 位置 1一次 WRITE 命令完整结束CS# 拉高之后WEN 会自动清零。这模仿了 SPI Flash 的写保护设计目的是防止上电瞬间、GPIO 抖动或软件异常时杂散信号把存储器内容冲掉。代价就是初学者的典型坑只发了 WRITE 命令和数据没发 WRENMRAM 默默忽略整个写入你读回来发现还是旧数据还以为是 SPI 不通。判断写操作是否真正完成可以用 RDSR 读状态寄存器看其中的 WEN 位是否已归零。不同批次手册对状态寄存器位定义的标注略有差异我代码里都用一个宏来定义 WEN 位掩码每次拿到新批次物料就核对一遍手册。3.3 地址字节、突发传输与边界回卷MRAM 的 READ 和 WRITE 命令后面都跟 3 字节地址先发最高字节。这颗芯片实际只有 19 位地址有效512K 需要 19 位所以 3 字节地址里最高的 5 位会被忽略编程时统一按 32 位变量组织地址取高 16 位、低 16 位分别送入即可。只要 CS# 保持低电平地址会自动递增形成连续突发读写不需要像 Flash 那样按页编程。但地址递增到 0x7FFFF 后会回卷到 0x00000这一点非常关键如果你往 0x7FFF8 连续写 16 字节低 8 字节跑到顶部之后会掉到 0x00000 开头数据布局完全错乱。所以我在应用层强制规定每次读写块不得超过 256 字节并且跨 0x7FFFF 边界的请求必须拆成两段处理。4. 从零写驱动SPI 初始化、读写 API 与自检例程4.1 SPI1 初始化CPOL/CPHA、时钟预分频和 GPIO 速度STM32F100 的库我用的是标准外设库SPL步骤固定先开 GPIOA 和 SPI1 的时钟配置 PA4 为推挽输出PA5/PA7 为复用推挽输出PA6 为浮空输入。GPIO 输出速度要设成 50MHz否则 12MHz 的 SCK 波形爬不上来。void MRAM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); /* PA4: 软件片选初始拉高 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_SetBits(GPIOA, GPIO_Pin_4); /* PA5/PA7: SPI1_SCK/MOSI 复用推挽输出 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); /* PA6: SPI1_MISO 浮空输入 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_2; /* 24MHz/2 12MHz */ SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI1, SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }SPI 模式要特别注意MRAM 手册标注支持 SPI Mode 0CPOL0CPHA0和 Mode 3CPOL1CPHA1我用 Mode 0。绝对不要配成 Mode 1 或 Mode 2这种差半个时钟沿的组合在低速时可能侥幸读对一旦提高频率就会偶发错位而且波形看起来还像模像样排查非常痛苦。4.2 底层收发函数与片选控制SPI 是全双工的发送一个字节的同时必然收到一个字节所以发送函数必须把 RXNE 标志也等掉否则发送队列会被堵死static void MRAM_Select(void) { GPIO_ResetBits(GPIOA, GPIO_Pin_4); } static void MRAM_Release(void) { GPIO_SetBits(GPIOA, GPIO_Pin_4); } static uint8_t MRAM_SendByte(uint8_t data) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI1, data); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) RESET); return SPI_I2S_ReceiveData(SPI1); }每次操作的基本纪律是Select 拉低 CS#发送命令字节、地址字节、数据字节最后 Release 拉高 CS#。CS# 拉高的上升沿是 MRAM 锁存写入、清除 WEN 的触发时刻所以 Release 必须放在所有数据发送完之后。我见过有人在循环发送数据过程中提前拉高了 CS#结果后半部分数据直接丢失。4.3 完整读写 API以 16 字节块为单位封装两个应用层接口一个读一个写参数用 32 位地址和长度#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_WriteEnable(void) { MRAM_Select(); MRAM_SendByte(MRAM_CMD_WREN); MRAM_Release(); } int MRAM_Read(uint32_t addr, uint8_t *buf, uint32_t len) { if ((addr len) 0x80000u) return -1; MRAM_Select(); MRAM_SendByte(MRAM_CMD_READ); MRAM_SendByte((uint8_t)(addr 16)); MRAM_SendByte((uint8_t)(addr 8)); MRAM_SendByte((uint8_t)(addr)); while (len--) { *buf MRAM_SendByte(0x00); } MRAM_Release(); return 0; } int MRAM_Write(uint32_t addr, const uint8_t *buf, uint32_t len) { if ((addr len) 0x80000u) return -1; MRAM_WriteEnable(); MRAM_Select(); MRAM_SendByte(MRAM_CMD_WRITE); MRAM_SendByte((uint8_t)(addr 16)); MRAM_SendByte((uint8_t)(addr 8)); MRAM_SendByte((uint8_t)(addr)); while (len--) { MRAM_SendByte(*buf); } MRAM_Release(); return 0; }调用方式很简单比如设备参数区固定在 0x10000长度 16 字节uint8_t cfg[16]; MRAM_Write(0x10000, cfg, 16); MRAM_Read(0x10000, cfg, 16);注意我在 Write 函数里每次调用都重新发 WREN这是有意的。WEN 在每次写命令结束后自动清零如果程序里到处假设我已经使能过写保护了后续维护者新增一个写调用时很容易踩坑。每次写都先 WREN成本只是一个字节换来的是确定性。4.4 上电自检先证明链路是通的再进主循环工业设备上电之后存储芯片必须自检否则现场跑几个月之后发现日志从来没写进去那就晚了。我一般做三层检查读状态寄存器确认能读到合理的状态字节如果读回全是 0xFF说明 SPI 链路有问题对 0x00000 一个临时测试地址写 0xA5读回校验再写 0x5A读回校验。这两个数互补任何位粘连都能暴露读设备信息区校验里面的魔数。第一次上电没有魔数就初始化信息区并回读确认。自检失败的处理策略也重要不要直接死机而要在串口/指示灯上给出明确错误码同时维持系统其他功能。现场工程师看到明确的MRAM 自检失败代码比看到设备罢工要省事得多。5. 工业场景存储架构掉电保护、环形日志与升级标记5.1 掉电瞬间把关键数据写进去的时序设计这是 MRAM 最舒服的舞台。以前用 EEPROM 做掉电保存掉电检测触发后得预留几毫秒等写完成用 NOR Flash 更麻烦如果恰好赶上擦除窗口数据直接飞了。MRAM 写一个字节在微秒级即使写一串几十字节的参数块整个流程也就一两百微秒。STM32F100 有 PVD可编程电压检测器配置好掉电阈值后会在电压跌落到阈值附近时触发中断。我的做法是RAM 里维护一份脏数据标志key 参数每次变更就置位PVD 中断里先关全局中断然后把脏数据区拷到 MRAM 的固定参数区写完清零标志全程不调用任何慢速库函数SPI 收发就用上面那个 MRAM_SendByte保证在电压彻底跌落前完成。掉电时能写多少数据取决于板卡电源的保持时间通常有个几毫秒的窗口。按 12MHz SPI 算写 100 字节加命令开销也就 110µs 左右留出的余量非常大。但我也提醒一句不要在 PVD 中断里做日志补写、状态文件整理这类复杂操作只救最要紧的关键参数其余数据靠 RAM 里的缓存策略定期落盘。5.2 环形日志记录不需要磨损均衡的快乐实现现场日志是另一大刚需。设备运行状态、温度曲线、故障事件都需要持续记录而且更关注的是最近 N 条不是从开机到现在所有。用 MRAM 实现环形日志比 Flash 简单太多。我在 MRAM 顶部划出一块 256KB 的日志区结构如下日志区起始位置放 16 字节头魔数、版本、写指针、读指针、记录总数每条记录固定 32 字节包括时间戳、事件类型、数据值、CRC8写新记录时覆盖写指针指向的位置然后写指针加 32到日志区末尾就回卷到头部记录区之后。因为 MRAM 可以直接覆盖旧记录不存在先找空白页、再擦除整块的问题。写指针回卷时只需要小心一件事不能让写指针追上读指针否则最老的一条未读记录会被吞掉。每次写完检查一下如果追上了就把读指针也向前提一帧。这个方案在 Flash 上要实现同样的效果光磨损均衡模块就得写几百行MRAM 省掉的这部分开发成本很多时候比芯片差价更有价值。5.3 Bootloader 升级标记与双镜像切换工业设备的 OTA 升级最担心升级到一半断电设备变砖。MRAM 可以存储升级状态标记让 Bootloader 在异常断电后自我恢复。我的设计是MRAM 固定地址 0x7FF00 放一个 32 字节的启动信息块内容包括魔数 0x5354A001升级状态1 表示准备升级2 表示镜像完整待切换3 表示当前镜像运行正常镜像版本号、镜像长度、CRC32。正常流程是App 收到新固件后写入 STM32 内部 Flash 的备份区写完后在 MRAM 里把状态改成 2然后软复位。Bootloader 上电读 MRAM看到状态 2就校验备份区 CRC通过后把它设为启动区并把状态改成 3。升级过程任意一点掉电MRAM 里的状态停留在 1 或 2Bootloader 要么重试、要么回滚到旧镜像绝不会出现两个镜像都不完整还硬着头皮启动的情况。这套机制的重点在于状态标记的写入本身要防掉电撕页。MRAM 虽然写得快但如果整个 32 字节写到一半断电状态字可能处于中间值。我处理的办法是状态字写两次第一次写预备值第二次写确认值Bootloader 以确认值目标状态为准这样任何时刻读到的都是一致的状态。6. 实测踩坑清单频率、写使能、边界与信号问题跑了大半年把最有代表性的问题按排查顺序整理成一张表现象根本原因排查与解决读回全是 0xFFSPI 模式不对、接线虚焊、HOLD# 悬空量 CS#/SCK 波形降频到 1MHz 试上拉 HOLD#写入后读回还是旧值WRITE 前漏发 WREN每次写入前先发 0x06检查状态寄存器 WEN 位写靠近 0x7FFFF 时数据跑到开头地址回卷跨边界拆成两段写应用层限制块大小20cm 杜邦线下偶发误码SCK 沿振铃、干扰耦合信号线串联 33Ω 电阻降频到 6MHz换双绞/屏蔽线偶发一条记录损坏写指针回卷追尾读指针日志写入后立即回读头信息校验上电正常但使用几小时后再读异常深度掉电误触发等异常命令检查代码里是否误发 0xB9恢复用 0xAB第一个坑值得单独强调。读回全 0xFF 是所有 SPI 存储器故障的最大公约数SPI 模式错了读回 0xFF焊盘虚焊读回 0xFFHOLD# 被拉低读回 0xFF芯片挂了也读回 0xFF。所以排查顺序一定是先验证电气再怀疑协议。我一直用一套笨但有效的办法把 SPI 频率降到 1MHz用一个逻辑分析仪抓 SCK、CS#、MOSI、MISO 四根线。如果波形上 CS# 有毛刺基本是焊接问题如果波形干净但读回还是 0xFF再回头看模式配置和 HOLD#。第二个坑我在实验室里栽过一次。最初写驱动时图省事把 WREN 放在初始化函数里只发一次想着初始化之后一直处于写使能状态多好。结果 MRAM 的 WEN 在执行完一条 WRITE 命令后就自动清零第二次调用 MRAM_Write 时其实发的是未使能写数据全部静默丢弃。从那以后我把 WREN 放进每次写操作的第一行宁可多花一个字节也不赌状态。还有一个 DFN 封装带来的现实问题。MR25H40CDF 的 8 脚 DFN 底部有大焊盘手工焊接要比 SOIC 小心得多。焊盘虚焊最典型的症状就是常温能跑温度一变化就出错因为热胀冷缩让虚焊点间歇断开。板子批量做的时候一定让贴片厂用钢网刷锡DFN 底部焊盘通过多个过孔接到 GND 平面既能散热又能增加机械强度。如果只是打样调试用热风枪焊接后务必在显微镜下检查侧边焊趾和底部焊盘是否饱满。最后说选型层面的一句提醒MR25H40C 的常规后缀和温度等级可能不是宽温版本做工业产品时要么选带工业温度等级的型号要么在 PCB 设计上避免把它放在热源附近。别等到现场夏天停机才想起看温度规格。这套 MRAM STM32F100ZE 的方案我用到现在最大的感受是存储问题从项目列表里消失了。以前写 Flash 驱动要想着擦除队列、磨损计数、坏块管理现在就是一段 5 分钟的读写函数剩下的精力全花在真正重要的业务逻辑上。如果你也正在被 Flash 的寿命和掉电问题折磨不妨找一块 MRAM 评估板照着上面的驱动先跑通读写再把你最头疼的存储场景挪上去试试。
返回列表