ARTICLE DETAIL

资讯详情

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

STM32F722ZE 驱动 MR25H40CDF MRAM 工业存储方案实战

STM32F722ZE 驱动 MR25H40CDF MRAM 工业存储方案实战 1. 项目缘起与方案选型1.1 为什么要在工业场景里折腾 MRAM做工业嵌入式这行十几年最头疼的往往不是算法跑不动而是数据存不住。传统方案里EEPROM 擦写寿命十万次封顶SPI NOR Flash 写入前得先擦除整个扇区掉电瞬间还可能把整块数据搞成半吊子状态。铁电存储器 FRAM 倒是快但容量小、价格高而且很多型号已经停产。这几年 MRAM 慢慢进入视野MR25H40CDF 就是其中比较有代表性的一颗。MR25H40CDF 是 Everspin 家的 4Mbit 磁阻随机存储器SPI 接口40MHz 时钟最关键的是它没有擦除概念——写就是写不需要先擦后写字节级随机访问写入延迟和读取延迟基本在一个量级。官方标称擦写次数 10^14 次以上数据保持 20 年工业级温度范围 -40°C 到 85°C。这几个参数放在一起对于需要频繁记录运行日志、保存标定参数、掉电前紧急存盘的工业设备来说吸引力是实打实的。我这次的项目背景是一台工业环境监控终端主控用 STM32F722ZE需要周期性记录传感器数据、保存设备配置、并在异常掉电时把关键状态写进非易失存储。之前用 SPI Flash 方案每次写之前要判断扇区状态、搬移数据、擦除、再写回代码复杂不说掉电保护还得额外加超级电容。换成 MR25H40CDF 之后整个存储层的逻辑简化了一大截。1.2 STM32F722ZE 的 SPI 外设能力评估STM32F722ZE 是 ST 家 F7 系列里性价比比较高的一颗Cortex-M7 内核216MHz 主频带 512KB Flash 和 256KB SRAM外设资源丰富。它有多达 6 个 SPI 接口其中 SPI1 挂在 APB2 上最高时钟可以到 108MHzSPI2/3 挂在 APB1 上最高 54MHz。对于 MR25H40CDF 的 40MHz 上限来说SPI1 完全够用甚至还有余量。选 SPI1 还有几个考虑一是它的引脚在 F722ZE 的常见封装上位置比较顺手PA5/PA6/PA7 分别是 SCK/MISO/MOSI片选可以用 PA4 或者任意 GPIO二是 SPI1 支持硬件 NSS但实际用下来我更倾向于软件片选后面会细说原因三是 DMA 通道资源上SPI1 的 RX 是 DMA2 Stream0/2TX 是 DMA2 Stream3/5和项目里其他外设不冲突。这里插一句很多人一上来就纠结用硬件 NSS 还是软件 NSS。我的经验是单主单从、片选引脚固定、不需要动态切换从机的场景软件片选更省心。硬件 NSS 在 STM32 上有个坑就是当 NSS 配置为硬件模式时如果引脚被拉高SPI 会自动进入从模式稍不注意就出怪问题。软件片选就是普通 GPIO 输出想什么时候拉低就什么时候拉低时序完全可控。1.3 整体存储架构设计这个项目的存储需求分三类第一类是设备配置参数比如传感器校准系数、通信地址、报警阈值写入频率低但要求绝对可靠第二类是运行日志每分钟记录一条包含时间戳、温度、湿度、电压等写入频繁但可以容忍少量丢失第三类是掉电快照检测到电源异常时立刻把当前关键状态写进去要求写入延迟极低。针对这三类需求我在 MR25H40CDF 的 512KB 空间里做了分区区域起始地址大小用途写入策略配置区0x0000016KB设备参数、校准数据双备份轮换写日志区0x04000480KB运行日志环形缓冲顺序写、覆盖最旧快照区0x7C00016KB掉电状态快照双备份轮换写配置区和快照区用双备份是因为这两个区域的数据最关键万一写入过程中掉电至少还有一份旧数据可用。日志区用环形缓冲写满就从头覆盖不需要擦除这也是 MRAM 相比 Flash 的一大优势——Flash 做环形缓冲还得处理扇区擦除MRAM 直接覆盖写就行。2. 硬件连接与底层驱动要点2.1 引脚连接与 PCB 布局注意事项MR25H40CDF 是 8 引脚 DFN 封装引脚定义很标准CS、SCK、MOSI、MISO、VDD、VSS、WP、HOLD。实际连接时CS 接 STM32 的 PA4SCK 接 PA5MISO 接 PA6MOSI 接 PA7VDD 接 3.3VVSS 接地。WP 和 HOLD 这两个引脚如果不用硬件写保护和暂停功能直接上拉到 VDD 就行。PCB 布局这块有几个坑我踩过。第一SPI 走线尽量等长尤其是 SCK 和 MOSI40MHz 下走线差太多会导致采样窗口偏移。第二CS 走线不要和 SCK 平行太长容易耦合噪声片选误触发。第三电源去耦电容必须靠近芯片 VDD 引脚我一般放一个 100nF 加一个 1uF100nF 最近。第四如果板子上有多个 SPI 从机每个从机的 CS 单独走线不要共用。还有一点MR25H40CDF 的 DFN 封装底部有个散热焊盘这个焊盘要接地不仅为了散热也为了信号完整性。我第一版板子忘了接地结果高温测试时偶发读写错误后来补上就好了。2.2 SPI 初始化配置详解用 STM32CubeMX 生成初始化代码很方便但有些参数得手动调。SPI1 的配置如下hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; hspi1.Init.CLKPhase SPI_PHASE_1EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_4; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; hspi1.Init.TIMode SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE; hspi1.Init.CRCPolynomial 7;这里重点说几个参数。CLKPolarity 和 CLKPhase要匹配 MR25H40CDF 的时序要求它支持 SPI Mode 0 和 Mode 3我用的是 Mode 0即 CPOL0、CPHA0空闲时 SCK 低电平第一个边沿采样。BaudRatePrescaler设 4SPI1 挂 APB2 上 108MHz分频后 27MHz在 40MHz 上限内实测很稳。如果想跑更快可以设 2 分频到 54MHz但那就超规格了我不建议。NSS 选 SPI_NSS_SOFT片选用 GPIO 手动控制。初始化 GPIO 的时候PA4 配成推挽输出初始拉高。每次读写前拉低读写完拉高。这里有个细节拉低和第一个 SCK 边沿之间要留一点时间MR25H40CDF 的 CS 建立时间最小 5nsSTM32 的 GPIO 翻转速度足够但如果你在中断里操作最好加一个 __NOP() 或者几个空指令确保时序。2.3 MRAM 指令集与读写时序MR25H40CDF 的指令集比 Flash 简单得多常用的就几条指令编码功能WREN0x06写使能WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据注意MRAM 没有擦除指令也没有页编程概念。写操作就是直接发 WRITE 指令跟地址然后连续写数据。地址是 24 位因为 512KB 需要 19 位地址但芯片按 24 位地址传输高位补零。读时序拉低 CS发 0x03发 24 位地址然后连续读数据每个 SCK 周期出一个字节。写时序拉低 CS发 0x06WREN拉高 CS再拉低 CS发 0x02发 24 位地址发数据拉高 CS。写操作前必须发 WREN这是很多人容易忘的一步忘了就写不进去而且读状态寄存器也看不出明显错误。状态寄存器的 bit0 是 WEL写使能锁存发完 WREN 后读一下确认 WEL1再发 WRITE。写完之后 WEL 自动清零。bit1 是 BP0、bit2 是 BP1用于块保护默认都是 0不保护。如果你不小心写了 WRSR 把块保护开了那就只能写特定区域这个坑我见过有人踩。3. 驱动层实现与核心代码3.1 底层读写函数封装先封装几个基础函数后面所有操作都基于它们static void MRAM_CS_LOW(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); } static void MRAM_CS_HIGH(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); } static uint8_t MRAM_SPI_Transfer(uint8_t data) { uint8_t rx; HAL_SPI_TransmitReceive(hspi1, data, rx, 1, 100); return rx; } void MRAM_WriteEnable(void) { MRAM_CS_LOW(); MRAM_SPI_Transfer(0x06); MRAM_CS_HIGH(); } uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS_LOW(); MRAM_SPI_Transfer(0x05); status MRAM_SPI_Transfer(0xFF); MRAM_CS_HIGH(); return status; } void MRAM_Write(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_WriteEnable(); MRAM_CS_LOW(); MRAM_SPI_Transfer(0x02); MRAM_SPI_Transfer((addr 16) 0xFF); MRAM_SPI_Transfer((addr 8) 0xFF); MRAM_SPI_Transfer(addr 0xFF); for (uint32_t i 0; i len; i) { MRAM_SPI_Transfer(buf[i]); } MRAM_CS_HIGH(); } void MRAM_Read(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LOW(); MRAM_SPI_Transfer(0x03); MRAM_SPI_Transfer((addr 16) 0xFF); MRAM_SPI_Transfer((addr 8) 0xFF); MRAM_SPI_Transfer(addr 0xFF); for (uint32_t i 0; i len; i) { buf[i] MRAM_SPI_Transfer(0xFF); } MRAM_CS_HIGH(); }这段代码看着简单但有几个地方值得说。HAL_SPI_TransmitReceive 的超时设 100ms正常 27MHz 下传一个字节不到 0.3us100ms 是兜底防止总线卡死。每次传输前不重新初始化 SPI因为初始化一次就够了频繁初始化反而容易出问题。CS 拉高之后不要立刻进行下一次操作MR25H40CDF 的 CS 高电平保持时间最小 10nsSTM32 的 GPIO 翻转足够慢一般不用额外延时但如果你在 200MHz 以上主频跑加一个 __NOP() 更保险。3.2 双备份配置区读写策略配置区用双备份逻辑是这样的两个备份块各 8KB每个块头部有一个 4 字节的序列号和 2 字节的 CRC。写入时先读两个块的序列号找到较旧的那个把新数据写进去序列号加一CRC 重算。读取时读两个块校验 CRC选序列号大的那个。typedef struct { uint32_t seq; uint16_t crc; uint8_t data[8186]; } ConfigBlock; #define CONFIG_BLOCK_A_ADDR 0x00000 #define CONFIG_BLOCK_B_ADDR 0x02000 int Config_Read(uint8_t *out) { ConfigBlock blkA, blkB; MRAM_Read(CONFIG_BLOCK_A_ADDR, (uint8_t*)blkA, sizeof(blkA)); MRAM_Read(CONFIG_BLOCK_B_ADDR, (uint8_t*)blkB, sizeof(blkB)); int validA (CRC16((uint8_t*)blkA.data, sizeof(blkA.data)) blkA.crc); int validB (CRC16((uint8_t*)blkB.data, sizeof(blkB.data)) blkB.crc); if (validA validB) { if (blkA.seq blkB.seq) memcpy(out, blkA.data, sizeof(blkA.data)); else memcpy(out, blkB.data, sizeof(blkB.data)); return 0; } else if (validA) { memcpy(out, blkA.data, sizeof(blkA.data)); return 0; } else if (validB) { memcpy(out, blkB.data, sizeof(blkB.data)); return 0; } return -1; }写入的时候先读两个块的序列号假设 A 的序列号是 10B 是 11那就写 A序列号设 12。这样即使写 A 的过程中掉电B 还是完整的下次上电读 B 就行。CRC 用 CRC16-CCITT多项式 0x1021初始值 0xFFFF这个在嵌入式里很常用计算速度快。这里有个经验序列号不要用 0 和 0xFFFFFFFF 作为有效值因为 MRAM 出厂时全是 0如果你第一次上电读到两个块序列号都是 0CRC 也恰好是 0概率极低但存在就会误判为有效数据。我的做法是初始化时先写一个魔数 0x5A5A5A5A 到两个块的序列号位置确保首次上电时 CRC 校验失败走默认配置。3.3 日志区环形缓冲实现日志区 480KB每条日志 32 字节能存 15360 条。环形缓冲的关键是维护一个写指针写到末尾就回绕到开头。因为 MRAM 不需要擦除回绕时直接覆盖写就行。#define LOG_START_ADDR 0x04000 #define LOG_END_ADDR 0x7C000 #define LOG_ENTRY_SIZE 32 #define LOG_MAX_ENTRIES ((LOG_END_ADDR - LOG_START_ADDR) / LOG_ENTRY_SIZE) typedef struct { uint32_t timestamp; int16_t temp; uint16_t humidity; uint16_t voltage; uint8_t reserved[22]; } LogEntry; static uint32_t log_write_idx 0; void Log_Write(LogEntry *entry) { uint32_t addr LOG_START_ADDR log_write_idx * LOG_ENTRY_SIZE; MRAM_Write(addr, (uint8_t*)entry, LOG_ENTRY_SIZE); log_write_idx; if (log_write_idx LOG_MAX_ENTRIES) { log_write_idx 0; } }写指针本身也需要保存不然掉电后不知道写到哪了。我把写指针存在配置区的一个固定偏移里每次写日志后更新。但这样每次写日志都要写两次 MRAM一次日志一次指针效率减半。优化方案是每写 16 条日志更新一次指针掉电最多丢 16 条对于环境监控场景完全可以接受。读日志的时候从写指针往前读读到有效数据为止。判断有效性的方法是看时间戳是否在合理范围内比如大于某个基准时间且小于当前时间。因为 MRAM 掉电不丢数据回绕覆盖后旧数据的时间戳会变成新数据的时间戳所以按时间戳过滤是可行的。3.4 掉电快照的紧急写入掉电快照要求写入延迟极低。MR25H40CDF 的单字节写入时间在 27MHz SPI 下大约是 0.3us 传输加芯片内部写入时间。官方数据手册里写周期时间最大 35ns字节写入实际上 SPI 传输才是瓶颈。写 32 字节快照传输时间约 10us加上指令开销总共不到 20us。这个速度足够在电源跌落的前几毫秒内完成。掉电检测用 STM32 的 PVD可编程电压检测器配置在 2.9V 触发。当 VDD 降到 2.9V 时PVD 中断触发在中断里立刻调用快照写入函数。注意PVD 中断里不要做复杂操作只写快照写完就等复位。快照区也用双备份逻辑和配置区一样。void PVD_IRQHandler(void) { if (__HAL_PWR_GET_FLAG(PWR_FLAG_PVDO)) { __HAL_PWR_CLEAR_FLAG(PWR_FLAG_PVDO); Snapshot_Save(); } }这里有个坑PVD 中断优先级要设最高不然可能被其他中断打断导致快照写不完。我设的是抢占优先级 0子优先级 0。另外快照写入函数里不要用 HAL_Delay 或任何阻塞操作全部用寄存器级操作或者确保 HAL 函数不会阻塞。4. 实测数据与性能分析4.1 读写速度实测我用逻辑分析仪抓了 SPI 波形配合代码里的时间戳测了几组数据操作数据量耗时等效速率连续读1KB312us3.28MB/s连续写1KB340us3.01MB/s单字节写1B1.2us-32字节快照写32B11.5us-读速率 3.28MB/s 对应 SPI 时钟 27MHz理论值 27/83.375MB/s实际有指令和地址开销312us 读 1KB 是合理的。写速率略低因为每次写之前要发 WREN多了一次 CS 翻转和指令传输。单字节写 1.2us 这个数据很关键意味着掉电快照写 32 字节只要 11.5us加上 PVD 响应时间和中断延迟总共不超过 50us。电源从 3.3V 跌到 2.9V 的时间取决于电容容量一般 100uF 电容在 100mA 负载下能撑几毫秒完全够用。4.2 擦写寿命与数据保持MR25H40CDF 标称擦写次数 10^14 次这个数字什么概念如果每秒写一次一年 31536000 次写 10^14 次需要 317 万年。实际项目中日志每分钟写一次一天 1440 次一年 525600 次十年才 525 万次连标称值的零头都不到。所以寿命这块完全不用担心。数据保持 20 年工业级温度范围内。我做了高温老化测试85°C 下连续读写 100 万次数据校验全部通过。低温 -40°C 下读写也正常没有出现数据错误。不过要注意MRAM 对强磁场敏感如果设备附近有大型电机或磁铁建议加磁屏蔽或者保持距离。我测试时把一块钕铁硼磁铁靠近芯片 2cm读写开始出错拿开就恢复数据没丢。4.3 与 SPI Flash 方案的对比对比项MR25H40CDFW25Q64 (SPI Flash)擦除需求无写前必须擦除写入粒度字节页 (256B)擦写寿命10^1410^5写入速度3MB/s0.5MB/s (含擦除)掉电风险低高 (擦除中掉电)价格高低容量4Mbit64Mbit从表里能看出来MRAM 的优势在写入体验和可靠性劣势在容量和价格。对于需要频繁写入、数据量不大的工业场景MRAM 是更优解。对于大容量存储、写入不频繁的场景Flash 更划算。我这个项目日志量不大512KB 够用所以选了 MRAM。5. 常见问题与排查实录5.1 读写失败问题速查现象可能原因排查方法解决方案读全 0xFFCS 没拉低示波器看 CS 引脚检查 GPIO 初始化读全 0x00MISO 没接好万用表测通断重新焊接写不进去没发 WREN读状态寄存器 bit0写前加 WREN偶发错误SPI 时钟太快降低分频改 8 分频高温出错电源去耦不足测 VDD 纹波加 100nF 电容数据错位CPHA/CPOL 不对试 Mode 0 和 Mode 3改 SPI 配置这个表是我实际调试中总结的覆盖了 90% 以上的问题。重点说几个读全 0xFF 最常见的原因是 CS 没拉低因为 MISO 上拉电阻会把线拉高如果 CS 没拉低芯片不驱动 MISO读出来就是 0xFF。写不进去九成是忘了 WREN这个我一开始也犯过后来在 MRAM_Write 函数里强制加 WREN就再没出过。5.2 SPI 时序调试经验调试 SPI 时序逻辑分析仪是必备工具。我用的是一款 8 通道 100MHz 采样率的抓 27MHz SPI 波形够用。抓波形时重点看几个点CS 拉低到第一个 SCK 边沿的时间、SCK 的占空比、MISO 的采样点是否在数据稳定区。有一次遇到读数据偶尔错一位抓波形发现 SCK 上升沿和 MISO 数据跳变几乎同时采样窗口太窄。原因是 PCB 走线太长MISO 有延迟。解决办法是把 SPI 时钟降到 13.5MHz8 分频问题消失。后来改板时缩短了走线27MHz 也能稳定跑。还有一次写数据时好时坏抓波形发现 CS 在数据传输中途被拉高了。查代码发现是中断里也调用了 MRAM 读写函数打断了主循环的写操作。MRAM 读写函数不是可重入的必须加互斥保护。我的做法是在 MRAM_Write 和 MRAM_Read 里关中断操作完再开。虽然会稍微增加延迟但保证了数据完整性。5.3 掉电保护实测与优化掉电保护这块我做了专门的测试。用可调电源模拟掉电从 3.3V 快速降到 0V下降时间设 1ms看快照能不能写进去。第一次测试失败快照没写完整。查原因发现 PVD 中断里调用了 HAL_SPI_TransmitReceive这个函数内部有超时判断会读系统 tick而掉电时系统 tick 可能已经不准了。优化方案PVD 中断里的 SPI 传输改用寄存器级操作不依赖 HAL 库和系统 tick。直接操作 SPI1 的 DR 寄存器轮询 TXE 和 RXNE 标志位。这样即使系统时钟乱了SPI 外设只要有时钟就能工作。改完之后再测1ms 下降时间内快照写入成功率 100%。另外快照区双备份的写入顺序要注意先写备份 B再写备份 A。因为读取时选序列号大的如果写 B 的过程中掉电A 还是旧的但完整下次上电读 A。如果先写 A 再写 B写 A 时掉电A 可能半新半旧B 是旧的读 B 也没问题。但为了逻辑简单我固定先写 B 再写 A序列号 B 比 A 大 1。6. 项目扩展与个人体会这套 MRAM 存储方案在这个环境监控终端上跑了大半年现场部署了二十多台没有出现过数据丢失或读写故障。后来我把这套驱动封装成了一个独立的中间件换到其他 STM32 型号上也能用只需要改 SPI 初始化和 CS 引脚定义。扩展方向有几个一是把日志区改成文件系统比如 LittleFS这样可以直接用 PC 读取日志文件不用自己写解析工具二是加一个 MRAM 到 SD 卡的定期转存把历史数据归档三是利用 MRAM 的快速写入特性做高频数据采集的缓存比如振动监测每秒采几千个点先写 MRAM再慢慢处理。我个人在实际操作中的体会是MRAM 这东西用起来确实省心但价格摆在那选型时要算清楚账。如果你的项目写入频率不高一年写不了几万次那 SPI Flash 完全够用没必要多花钱。但如果你的设备需要频繁记录、掉电保护要求高、或者维护成本很高比如装在高塔上、井下那 MRAM 多出来的成本很快就能通过减少现场故障赚回来。最后分享一个小技巧MR25H40CDF 的 WP 引脚如果不用一定要上拉到 VDD不要悬空。悬空时引脚电平不确定可能随机进入写保护状态表现就是写不进去但读正常这个坑很隐蔽查起来费时间。我现在的做法是在 PCB 上直接放一个 10K 上拉电阻不管用不用都焊上省得后面调试时抓瞎。
返回列表