ARTICLE DETAIL

资讯详情

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

STM32F756ZG携手MR25H40CDF:工业日志用MRAM替代Flash实践

STM32F756ZG携手MR25H40CDF:工业日志用MRAM替代Flash实践 前阵子调试一台需要连续记录传感器曲线的测试台,发现上位机重启后部分标定数据总是丢位。最后定位到不是代码问题,是原来用的串行 NOR Flash 在频繁擦写上撑不住,导致坏块和偶发写失败。手上的新项目是 STM32F756ZG 做主控,加分项是它可以在线一边采集一边落盘,还希望断电瞬间不丢关键帧。这时候脑子里第一个跳出来的方案就是 Everspin 的 MR25H40CDF——一颗 SPI 接口的 4Mbit MRAM。写这篇文章,把我在这个组合上从硬件到驱动再到掉电策略的调试过程完整记一遍。如果你正打算在工业数据记录、跑批参数备份或者设备配置存储里用 MRAM 替代 Flash,这篇应该能给你省掉不少试错时间。1. 先聊清楚:为什么工业日志要用 MRAM 而不是 Flash1.1 先摆一张表:Flash 的死穴很多嵌入式工程师一提到非易失存储,默认就是 NOR Flash 或者 EEPROM。这套方案在消费级产品里没问题,但放到工业现场,有一个绕不开的物理限制:擦写寿命和写放大。以常见的 W25Q 系列 SPI NOR Flash 为例,单个扇区的擦除寿命一般是 10 万次到 20 万次。如果你的设备每 10 秒记录一条事件日志,每次都触发一次扇区擦写,那这个扇区大概 23 天就会到寿命边界。EEPPROM 稍微好一点,单字节擦写寿命能到 100 万次,但容量小、写入速度慢,而且面向页编程时同样有编程前擦除的要求。我把常用方案做了个对比:参数串行 NOR FlashI2C EEPROMMRAM(MR25H40CDF)典型擦写寿命10 万次/扇区100 万次/字节10^14 次写入前是否需要擦除必须按扇区擦除部分型号按页编程,仍需擦除不需要,直接随机写最小写入单元页(通常 256B 以上)字节字节写入时延擦除 100ms 级 编程5ms 级即时写,无编程等待读随机性快但无地址随机优势慢接近 SRAM 的随机读注意这张表的数字是按各家典型器件手册给的,具体型号会有出入,但结论是一致的:只要写操作很频繁,Flash 的擦除机制就是整个系统里最脆弱的环节。MR25H40CDF 就是在这种背景下被选进来的。它的存储介质是磁阻随机存取存储器,数据极性靠磁化方向保持,掉电不丢,但读写行为却像 SRAM:你写哪个地址就改哪个地址,不碰其他位,也不用先擦一个大块。对频繁小包数据写入来说,这几乎是最理想的介质。1.2 MR25H40CDF 的定位与参数点MR25H40CDF 是 Everspin 的一颗 4Mbit SPI MRAM,内部组织是 512K x 8,也就是 512KB 容量。供电 3.3V,封装常见 8 脚 DFN,温度范围按订货代码能覆盖工业级。它除了标准 SPI,还支持 Dual SPI、Quad SPI,意味着同样的时钟下可以把吞吐带宽翻倍或翻四倍。对 STM32F756ZG 这种主频 216MHz 的芯片来说,标准 SPI 拉到几十 MHz 已经足够快,双线模式反而很少用。这颗器件的核心参数点,我是这么记的:容量:512KB,对日志、配置、标定数据这类应用非常合适写寿命:10^14 次,工程上基本等于无限次读寿命:和写一致,没有读干扰问题数据保持:随温度不同有差别,工业温度范围内通常能保 20 年以上写行为:字节级随机写,无擦除等待,无页对齐要求你看 10^14 这个数,我用一个很朴素的方法算过:假设设备每隔 10 秒写一次日志,每次写 10 字节,那么一年大概写 315 万次。按 10^14 次寿命算,这个地址要写 3000 万年才会到极限。所以做磨损均衡?在 MRAM 上基本属于过度设计。1.3 什么时候我会提醒你别上 MRAMMRAM 不是万金油。如果你要存几百 MB 的固件镜像、批量采集的波形文件,那 512KB 根本不够,老老实实选大容量 Flash 配文件系统。如果产品只在上电时写一次配置、断电前清零一次标记,普通 EEPROM 的成本优势更明显。另外 MRAM 单位比特价格目前仍然比 Flash 高,买料时要有心理准备。选型的关键是看写入频率和数据量,而不是看容量大小。2. 硬件连线与 STM32F756ZG 侧的准备2.1 引脚分组和 SPI 时钟的选择STM32F756ZG 的 SPI 资源很丰富,SPI1/2/3 是一组,SPI4/5/6 是另一组。MRAM 对时钟频率不敏感,但 MCU 侧要留意 SPI 外设挂在哪个总线。F756ZG 的 APB2 最高 108MHz,SPI1/4/5/6 挂在这条总线上;APB1 最高 54MHz,SPI2/3 挂在这条总线上。我当时把 MRAM 放在 SPI1,标准四线连接:信号方向STM32F756ZG 引脚说明CS#输出PA4 或任意 GPIO必须软件控制,不用硬件 NSSSCK输出PA5SPI1_SCKMOSI(SI)输出PA7SPI1_MOSIMISO(SO)输入PA6SPI1_MISOWP#输入直接上拉 3.3V不用写保护时拉高HOLD#输入直接上拉 3.3V禁用保持功能拉高时钟预分频这里有个坑。SPI1 在 APB2 上,如果预分频 /2,时钟就是 54MHz,很多 MRAM 的手册标注上限是 40MHz 或 50MHz,已经超了。就算手册支持 50MHz,我也不建议一上来就顶格跑。螺旋线的分布电容、接插件的接触电阻,都会让 50MHz 的 SPI 波形出现振铃和误码。我的做法是先配 /4,也就是 27MHz,跑通协议、验完可靠性,再去追极限速度。SPI 的容忍度比你想象的高,但工业现场能少一个变量就少一个。HAL 初始化代码如下:SPI_HandleTypeDef hspi1; void MX_SPI1_Init(void) { 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; // CPOL0 hspi1.Init.CLKPhase SPI_PHASE_1EDGE; // CPHA0, 即 SPI Mode 0 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 10; HAL_SPI_Init(hspi1); }MR25H40CDF 的手册里通常支持 Mode 0 和 Mode 3,我选 Mode 0 是因为它和后续驱动的 READ/WRITE 命令在调试时不容易混淆相位。如果你的旧代码是从某款 Flash 驱动里改过来的,原来是 Mode 3,那也能跑,但记得把波形抓一下再固化下来。2.2 控制线该接哪里,不该省哪根上拉CS# 我强烈建议用普通 GPIO 控制,而不是硬件 NSS。原因很简单:MRAM 的一条写流程是先发 WREN(写使能)指令,再拉高 CS,然后才能发 WRITE 命令。用硬件 NSS 的话,片选时序跟应用层事件交互很别扭,容易把两次 CS 周期当成一次。WP# 和 HOLD# 这两根脚,很多人画原理图时直接悬空,这是个大坑。HOLD# 一旦因为噪声被拉低,芯片就会暂停 SPI 通信,SCK 再有脉冲也不响应,表现为读出来全 FF或者状态寄存器卡死。我在测试台上遇到过好几次,最后把两根都加了 10k 上拉到 3.3V,问题彻底消失。这个细节成本几乎为零,回报却很大。另外注意 STM32F756ZG 的引脚不是全 5V 容忍。MRAM 是 3.3V 器件,直接接 3.3V 域没问题,但如果你板子上还有 5V 的外设,要确认 PA5/PA6/PA7 没有被配置成 5V 电平。最稳妥的办法就是全部围绕 3.3V 供电设计。2.3 原理图之外的 PCB 建议工业板子经常会有 20 厘米以上的连接排线,或者把 MRAM 放在可插拔子板上。这种情况下,SCK 线上串一个 22 到 33 欧姆的电阻,能明显抑制反射。MOSI 和 MISO 也要尽量避免和电机驱动、继电器控制信号平行走线。调试时一定要留出测试点,至少能勾到 CS、SCK、MISO 三个信号,不然后面一出现时序问题,你只能靠猜。3. 驱动封装:让 MR25H40CDF 像一块超大 EEPROM 一样用3.1 HAL 初始化里的两个易错位一个是 SPI 传输方向。HAL_SPI_Transmit 和 HAL_SPI_Receive 是半双工概念,但对于 SPI 外设本身,它永远是全双工发送接收的。MRAM 在 READ 命令之后,只要 CS 拉低、SCK 在翻转,MOSI 上是任意值,芯片都会从 MISO 吐出数据。所以 READ 流程我用HAL_SPI_Transmit发指令头,再用HAL_SPI_Receive收数据,这在 HAL 层面是可行的。但这里有一个 F7 特有的坑:HAL_SPI_Transmit返回时,可能只是把数据写进了发送 FIFO/移位寄存器,最后一比特还在线上。如果你紧接着操作 CS,或者立刻切换成接收模式,数据边缘会被切断。我的处理是每次发完命令或数据后,都等一下 SPI 总线忙标志:static void spi_wait_busy(void) { while (hspi1.Instance-SR SPI_SR_BSY); }这样做的好处是无论后面是收还是发,都不会出现半比特错位。虽然牺牲了一点速度,但换来的是驱动稳定性。第二个易错位是片选拉低和 SPI 数据发送之间,最好加一个空的定时点。实测中如果 CS 刚拉低就立刻发命令,而 GPIO 的翻转距离 SPI 外设有较长布线,芯片可能采样不到第一个字节的起始沿。我一般会在mram_cs(0)后面加几条空语句或者一个短延时,保证 CS 建立时间。3.2 三个基本函数:读、写、状态我按照先读状态、再读数据、最后写数据的顺序封装。地址是 19 位,三个地址字节里最高字节只用 bit2 到 bit0。#define MRAM_SIZE 0x80000 // 512KB typedef enum { MRAM_OK 0, MRAM_ERR_PARAM, MRAM_ERR_TIMEOUT, MRAM_ERR_SPI, } MRAM_Status; static void mram_cs(uint8_t level) { HAL_GPIO_WritePin(MRAM_CSN_GPIO_Port, MRAM_CSN_Pin, level ? GPIO_PIN_SET : GPIO_PIN_RESET); } static void mram_send_cmd_addr(uint8_t opcode, uint32_t addr) { uint8_t hdr[4]; hdr[0] opcode; hdr[1] (uint8_t)((addr 16) 0x07); // A18..A16 hdr[2] (uint8_t)(addr 8); hdr[3] (uint8_t)(addr); mram_cs(0); HAL_SPI_Transmit(hspi1, hdr, 4, 100); spi_wait_busy(); }读函数:MRAM_Status mram_read(uint32_t addr, uint8_t *buf, uint32_t len) { if (addr MRAM_SIZE || len MRAM_SIZE - addr) { return MRAM_ERR_PARAM; } mram_send_cmd_addr(0x03, addr); // READ HAL_SPI_Receive(hspi1, buf, len, 100); mram_cs(1); return MRAM_OK; }写函数比读多一步写使能。MRAM 的写使能闩锁在每次指令周期后自动清零,所以每次 WRITE 之前都必须发 WREN,这是从 SPI Flash 时代延续下来的规矩,别想当然地以为写一次后面就不用发了。MRAM_Status mram_write(uint32_t addr, const uint8_t *buf, uint32_t len) { uint8_t wren 0x06; if (addr MRAM_SIZE || len MRAM_SIZE - addr) { return MRAM_ERR_PARAM; } // WREN: 写使能 mram_cs(0); HAL_SPI_Transmit(hspi1, wren, 1, 100); spi_wait_busy(); mram_cs(1); // WRITE mram_send_cmd_addr(0x02, addr); HAL_SPI_Transmit(hspi1, (uint8_t *)buf, len, 100); spi_wait_busy(); mram_cs(1); return MRAM_OK; }状态寄存器读取用来做写入前最后的确认:uint8_t mram_read_status(void) { uint8_t cmd 0x05, val 0; mram_cs(0); HAL_SPI_Transmit(hspi1, cmd, 1, 100); spi_wait_busy(); HAL_SPI_Receive(hspi1, val, 1, 100); mram_cs(1); return val; }这里我建议在写函数里加一层防护:执行 WRITE 前先读一次状态寄存器,确认写使能位已经置起。如果因为某种原因 WREN 没发出去,后面的写命令直接无效,而且要等 5 个周期后才能重新操作。这个检查在开发阶段特别有用,能帮你把 SPI 层的问题和应用层隔离开。3.3 为什么这里没有 Flash 的擦除和页规整逻辑如果你以前只写过 Flash 驱动,第一次拿到 MRAM 可能会到处找擦除命令。没有。MRAM 的物理机制决定了你可以直接对任意字节写入,哪怕新增数据覆盖旧数据,也不需要先把整页读回来再改。这就引出了两个直接红利:写放大为零:想改一个字节,真的就只写一个字节不需要磨损均衡:10^14 次寿命让均衡逻辑失去意义我在代码里专门留了一个mram_erase_all的占位函数,但注释写明MRAM 不需要擦除,仅用来兼容旧上层软件。后来发现这个函数唯一的价值就是测试时把整块填成 0xFF,模拟出厂状态。4. 应用层存储规划:数据分区、校验与掉电一致性4.1 分区方案:把 512KB 当成一个小型虚拟磁盘驱动跑通之后,界面是简单的地址空间,但应用层不能乱写。我是这样分区的:地址范围大小用途0x00000 - 0x00FFF4KB启动参数、版本号0x01000 - 0x07FFF28KB设备标定数据0x08000 - 0x0BFFF16KB事件日志槽 A0x0C000 - 0x0FFFF16KB事件日志槽 B0x10000 - 0x7FFFF448KB用户数据区每个分区的头部固定写一个魔数、版本号、数据长度和 CRC16/CRC32。启动时先检查魔数和 CRC,再决定是否信任该分区。工业设备最怕的就是上电时读到合法但损坏的数据,魔数能挡住大部分随机乱码。4.2 用双槽互为备份解决写一半掉电工业现场掉电很残酷,可能你正在写一条 64 字节的日志,电源啪一声断了。MRAM 不会丢数据,但这次写可能只完成了一半。为了解决半个新数据 半个旧数据的问题,我用了双槽方案:配置和日志分别放在 A/B 两个槽每次更新先写备用槽,完整写入并确认 CRC 通过后,再更新一个指针字段,把这个槽标记为当前有效上电时根据指针字段选有效槽,如果指针字段异常,回退到另一个槽因为 MRAM 的指针字段更新是独立的字节写,只要指针字段本身有防错机制(比如写两遍,或者用 0x5A5A 这种模式判断),掉电导致的应用级数据撕裂就可以完全规避。这套思路在 NOR Flash 上也能用,但 Flash 的擦除等待让双槽切换时间变得很长;MRAM 的好处是切换几乎是瞬时的,掉电窗口从毫秒级缩到微秒级。4.3 校验域与版本号的坑校验域设计看起来简单,踩坑却不少。我第一次写的时候只放了一个 CRC16,结果设备里同时存在新固件写的日志和旧固件留下的残余日志,CRC 通过但结构对不上。后来强制在魔数后面加一个 4 字节的数据域版本号,任何写操作都必须先递增版本号,再更新内容。启动加载时,除了校验 CRC,还要比对版本号是否在支持的范围内。版本号不匹配直接按损坏处理,而不是强行解析。另一个实践提醒:不要频繁改写整个分区头。比如每次记录日志都更新时间戳,如果时间戳刚好在分区头,那就相当于每次都写头部。MRAM 虽然不在乎寿命,但头部数据如果写坏,整个分区就废了。所以我的设计是头部只写静态信息和 CRC,动态时间戳放在日志记录正文里。5. 实测与踩坑:温度冲击、长时间写入和 SPI 波形细节5.1 最大头的一个坑:CS 与 SPI 时钟的时序先说结论:你把 CS 拉高太早,最后一次传输的数据会被截断。我一开始写的驱动是直接在HAL_SPI_Transmit返回后就把 CS 拉高,当时在 27MHz 下大部分数据是对的,但偶发出现最后几个字节变成 0xFF。用逻辑分析仪抓波形才发现,HAL_SPI_Transmit返回时移位寄存器里还残留着最后一个字节的低位数据。如果把 CS 拉高,SCK 停止输出,这个字节的尾部直接被切掉。之所以偶发而不是必现,是因为 HAL 内部实际等过一部分硬件标志,残余量不稳定。后来在所有传输后面强制等待SPI_SR_BSY清空,这个问题再也没出现过。这个经验记在驱动注释里,后面换芯片、换板子都会少走弯路。5.2 温度与走线对高速 SPI 的影响我做了 45 度到 70 度的高低温箱测试,27MHz 下波形很稳。但把分频改为 /2(54MHz)且板间用 20 厘米 FPC 连接时,高温下开始出现误码,概率不高但足够烦人。用示波器看,SCK 上升沿振铃能到 3.8V 以上,远超 3.3V 的逻辑上限。给板子留了 33 欧姆串阻,再配合把时钟降回 27MHz,高低温拷机三天没再出问题。这里有个经验:工业现场的高速 SPI,瓶颈往往不在芯片本身,而在连接器和线缆质量。如果你被迫使用长线,先把时钟压到 20~30MHz,再考虑加源端匹配电阻。不要为了和某颗 Flash 的 50MHz 参数较劲,把整个系统的余量吃掉。5.3 断电实验暴露出的问题我在一个简易夹具上做了 2000 次随机断电,每 2 毫秒切换一次供电,同时让 MCU 持续写日志。结果 MRAM 内部本身没有任何单比特翻转,但收到三次因为电源压降导致的 SPI 时钟失真,读回的数据出现错误。这印证了一个观点:MRAM 只是介质可靠,外部总线的电平完整性依旧要自己保。处理方法是在产线烧录和标定环节结束后,把 SPI 时钟降到 13.5MHz,同时开启 CRC 接收校验。反正数据量不大,慢一点无感,但断电抗性明显上升。5.4 给 F756ZG 加 DMA 后的效果如果只是标记和配置存储,轮询 SPI 足够了。但要连续记录几百字节的波形日志,轮询会占掉大量 CPU。F756ZG 的 SPI 支持 DMA 传输,我用一个组合缓冲区分两步走:先把命令头放高位,后面填满 dummy 字节,然后一次性启动HAL_SPI_TransmitReceive_DMA,接收缓冲区的头部 4 字节不要,从偏移 4 开始就是 MRAM 读出的有效数据。uint8_t tx_buf[4 MRAM_DMA_MAX]; uint8_t rx_buf[4 MRAM_DMA_MAX]; void mram_read_dma(uint32_t addr, uint8_t *data, uint32_t len) { tx_buf[0] 0x03; tx_buf[1] (uint8_t)((addr 16) 0x07); tx_buf[2] (uint8_t)(addr 8); tx_buf[3] (uint8_t)(addr); memset(tx_buf[4], 0xFF, len); // 时钟占位 mram_cs(0); HAL_SPI_TransmitReceive_DMA(hspi1, tx_buf, rx_buf, len 4); // 在 DMA 完成回调里拉高 CS,并从 rx_buf4 拷贝数据 }这样一次 DMA 就把命令发送和数据接收全部做完,MCU 只需要在回调里搬数据。实测连续读 256 字节,占用的 CPU 时间基本可以忽略。写方向同理,只是把数据放在 tx_buf 后面,接收缓冲区丢弃即可。DMA 模式的坑在于 CS 控制不能依赖 HAL 内部时序,必须在回调里手动拉高。还有一个细节:F756ZG 的 SPI DMA 请求线和中断优先级要配好,否则在高负载下会出现 DMA FIFO 溢出,表现为数据整体偏移 1 字节。配优先级的原则是 DMA IRQ 高于任何频繁触发的外设中断。写了这么多,最后分享一个我在实际项目里的体会:MRAM 这类器件,真正省心的地方不是速度快,而是它把存储这件事从 Flash 的擦除、磨损、写放大这些复杂概念里解放了出来。你不再需要为写日志设计环形缓冲加磨损均衡,不再需要担心某个扇区 10 万次擦写后挂掉,甚至连掉电前赶工备份的紧迫感都少了很多。如果你也正准备把嵌入式项目里的非易失存储从 Flash 换成 MRAM,建议第一次打样时就把 SPI 时钟降低一档、WP 和 HOLD 全部上拉、CS 用 GPIO 控制、传输后等待 BSY 清空。这四个习惯能帮你绕开我踩过的绝大部分坑,剩下的就交给时间去验证。
返回列表