ARTICLE DETAIL

资讯详情

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

MRAM+STM32F765ZI工业存储方案:从SPI驱动到掉电保护实践

MRAM+STM32F765ZI工业存储方案:从SPI驱动到掉电保护实践 前阵子在给一套工业数据记录终端做存储方案第一版用的是 SPI NOR Flash结果连续写日志时经常遇到擦除延迟掉电瞬间还会丢最后一帧数据。后来把存储介质换成了 Everspin 的 MR25H40CDF主控用 STM32F765ZI整个“存储和读取数据”的链路一下子干净了很多。这颗 MRAM 是 4Mbit 的 SPI 非易失存储器写入寿命极高不需要先擦除再写写进去立刻掉电也不会丢配合 Cortex-M7 级别的 STM32F765ZI跑数据采集、参数保存、故障日志这类嵌入式应用非常顺手。这篇就把我在这个项目里的硬件接线、驱动实现、应用层分区和踩坑记录完整写一遍。内容适合正在做工业控制器、仪器仪表、边缘计算节点或者运动控制系统的嵌入式工程师参考。如果你只是想把数据存进 Flash那 NOR Flash 更划算但如果你需要频繁写入、掉电不丢、现场维护要省心MRAM 这套思路值得看完。1. 为什么选择 MR25H40CDF STM32F765ZI 做嵌入式存储1.1 现场需求频繁写、掉电不丢、不能有擦除等待这个项目的实际场景是给一条产线上的压力、温度和振动传感器做数据记录终端。设备每 100ms 采集一组数据每组约 64 字节除了实时上传还要在本地保留最近 48 小时的原始数据用于故障回溯。这类需求有几个特点写入频率高。一天下来要写几十万条记录如果用普通的 SPI NOR Flash每页写满后要执行块擦除块擦除时间是几十到几百毫秒系统很容易在上层出现“写不进去”的假卡顿。掉电时机不可控。工业现场经常直接拉闸Flash 在写入过程中掉电轻则丢一页数据重则出现 ECC 都救不回来的坏块。数据要长期可靠。现场设备可能几年不拆下来维护存储介质不能因为写次数耗尽而整体报废。读取速度也不能太差故障分析时要把历史曲线快速拉出来。MR25H40CDF 正好把这些痛点全部避开。它是 MRAM全称是磁阻式随机存取存储器核心存储单元是磁性隧道结写入靠磁化方向翻转实现不需要电荷泵、不需要擦除块。读和写都是字节级的写入后数据立即稳定不存在 Flash 那样的“编程后再校验”机制。加上 STM32F765ZI 的高主频和丰富外设这套组合做工业数据记录终端很合理。1.2 MRAM 与 NOR Flash、FRAM、EEPROM 的差异对比很多工程师一听到非易失存储第一反应就是 NOR Flash 或者 EEPROM。但在频繁写入的场景里它们和 MRAM 的差距非常明显我直接列一张对比表参数MR25H40CDFMRAM常见 SPI NOR Flash常见 SPI EEPROM如 M95xxx写入前是否需要擦除不需要需要按扇区/块擦除不需要最小写入单位1 字节通常 1 字节但页编程前必须先擦除1 字节写寿命大于 1e14 次典型 1e5 ~ 1e6 次典型 1e6 次写入等待核心写入约 35ns命令开销为主页编程几 ms块擦除几十到几百 ms写一字节几 ms掉电数据可靠性写入瞬时稳定可能在掉电时出现半编程页相对可靠但容量小容量选择常见到几十 Mbit可达 Gbit 级通常几百 Kbit~Mbit单位成本高低中这张表让我决定日志和关键参数这类“小容量、高频写、不能丢”的数据放在 MRAM 里大文件、固件升级包这类“低频写、大容量”的数据放到另一颗大容量 NOR Flash 里。MRAM 不是万能的它容量不大、价格偏贵但把它用在最需要可靠性的一小块关键数据上性价比反而很高。FRAM 也要提一下。FRAM 的写寿命同样非常高容量也合适但它的 SPI 接口器件在市场中相对少一些而且不同厂家的 FRAM 在高温、抗辐射等指标上有差异。MRAM 在数据保持、耐温和位翻转方面表现更稳定尤其是设备工作温度可能到 85℃ 以上的场景我最后选了 MRAM。1.3 整体数据流采集、缓存、落盘、上报整个系统的数据流可以概括成四层传感器采集层ADC 或外部总线读取温度、压力、振动波形。STM32F765ZI 处理层滤波、编码、加时间戳生成一条条定长记录。MRAM 存储层通过 SPI 将记录写入预设的数据区同时维护写指针和统计信息。上报与导出层通过以太网或 USB 读取 MRAM 内容生成 CSV 或二进制波形文件。这种分层让“存储和读取数据”不再只是几个 SPI 读写函数而是作为整个嵌入式项目的独立模块。驱动层只负责字节读写应用层负责分区分配、写指针管理和掉电恢复两层解耦后后面换主控或者换存储芯片改动面都很小。2. 硬件接线与最小系统别在 PCB 上省这几件事2.1 MR25H40CDF 引脚和外围电路怎么接MR25H40CDF 是标准的 SPI 接口芯片引脚不多但每个引脚都有讲究。它主要引脚如下VCC、VSS电源和地。供电范围 2.7V ~ 3.6V和 STM32F765ZI 的 3.3V I/O 完全兼容。CS#片选低电平有效。直接用 STM32 的一个普通 GPIO 控制不要和别的外设共享片选。SCKSPI 时钟输入。SIMOSI主发从收数据线。SOMISO从发主收数据线。WP#写保护引脚低电平有效。这个引脚悬空会导致写入指令被忽略必须接上拉电阻到 VCC。HOLD#暂停引脚低电平有效。当它拉低时SPI 时钟被暂停且数据线保持状态正常使用也一定要上拉到 VCC。我刚开始画板子时把 WP# 和 HOLD# 都漏接了结果芯片能读不能写查了半天才发现是引脚电平问题。正确的接法是WP# 接 10kΩ 上拉到 VCC。HOLD# 接 10kΩ 上拉到 VCC。CS# 接 STM32 GPIO平时默认高电平。VCC 旁放一颗 0.1μF 陶瓷电容尽量贴近芯片电源引脚如果电源线路较长再加一颗 4.7μF 电容。注意 MR25H40CDF 虽然有睡眠模式指令但正常运行时不需要额外控制引脚所以硬件上保持 WP# 和 HOLD# 都主动拉高是最省心的做法。2.2 STM32F765ZI 的 SPI 引脚分配与冲突排查STM32F765ZI 的 SPI 资源非常多有 6 个 SPI 控制器。我在项目里选了 SPI1因为它挂在 APB2 总线上外设时钟更高能跑更高的 SPI 速率。具体引脚分配如下信号引脚复用功能说明SCKPA5AF5SPI1 时钟MISOPA6AF5SPI1 主入从出MOSIPA7AF5SPI1 主出从入CS#PA4GPIO 输出软件控制片选选择 PA5/PA6/PA7 主要是因为它们不占用调试下载接口不像 PB3/PB4 在某些 Boot 配置下会被 JTAG 功能占用。PA4 作为片选也不影响其他外设。如果你的板子上 SPI1 引脚被 LCD 或者 ADC 占用了也可以换成 SPI2 或 SPI3只要按数据手册里 AF 复用表重新确认引脚即可。实际焊接时还要检查 PA4、PA5、PA6、PA7 这几个引脚有没有被板卡的跳线或者板载器件占用。比如某些开发板为了兼容 Arduino把 PA6 引出后可能带有下拉电阻这会破坏 SPI 信号电平。定制硬件时最好让 SCK、MOSI 线上不要并接大电容不然 SPI 频率一高就容易边沿失真。2.3 电源、去耦和强磁场防护MRAM 对电源质量不算苛刻但也不是完全无所谓。芯片内部写操作是电流驱动磁性翻转瞬间电流变化比普通 EEPROM 更陡所以电源线上的去耦电容绝对不能省。我实际的电源设计是VCC 用 3.3V 电源轨主控和 MRAM 共用但电源走线宽度不低于 0.5mm。MRAM 电源脚旁边并接 0.1μF 陶瓷电容 4.7μF 钽电容。在 PCB 布局上MRAM 尽量靠近 STM32SCK、MOSI、MISO 这三根线等长走线长度控制在 30mm 以内减少反射。还有一点很多人会忽略MRAM 内部存储单元是磁性材料虽然芯片封装已经做了磁场屏蔽设计但不要让芯片贴近大功率电感、变压器或者强磁铁。我们在后期测试时发现把 MRAM 放在一个额定电流 20A 的功率电感上方 5mm 以内长时间读取会出现偶发数据翻转。后来在布板时把存储芯片和功率部分隔开 2cm 以上再把电源地的环路面积缩小问题就消失了。工业设备里电机、变频器到处都是这一点值得提前注意。3. SPI 驱动实现从 CubeMX 配置到读写函数落地3.1 SPI 参数怎么配模式、时钟、NSSMR25H40CDF 支持 SPI Mode 0 和 Mode 3也就是 CPOL0/CPHA0 和 CPOL1/CPHA1。两种模式在数据手册里都允许我习惯用 Mode 0因为和大部分 SPI 传感器保持一致。在 STM32CubeMX 里把 SPI1 配置成ModeFull-Duplex MasterData Size8 BitFirst BitMSB FirstClock PolarityLowClock Phase1 EdgeNSSSoftwareBaudRate PrescalerSPI1 使用 APB2 时钟我这边 APB2 是 108MHz预分频选 4得到 27MHz 的 SPI 时钟。MR25H40CDF 的 SPI 时钟上限与具体后缀有关常见规格支持到 40MHz 左右27MHz 留了不少余量。如果你用的是低功耗模式或者板卡走线比较长分频 8 拿到 13.5MHz 也更稳妥。SPI 速率不是越高越好稳定性优先。NSS 一定要选 Software。如果选 Hardware NSSSTM32 会自动控制片选在某些封装里 CS 时序会和 MRAM 的指令序列不匹配尤其是连续收到 WREN 后再发 Write 指令时经常出现片选提前释放的问题。3.2 指令集封装WREN、RDSR、READ、WRITEMR25H40CDF 的指令集和 SPI NOR Flash 比较接近核心就这几个指令指令操作码说明WREN0x06写使能WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据我封装驱动时没有用 HAL 的高层扩展而是直接操作片选 GPIO 加 HAL_SPI_Transmit/Receive代码清晰也容易查问题。先看写使能函数#define MRAM_CS_LOW() HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_RESET) #define MRAM_CS_HIGH() HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_SET) #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 static void mram_write_enable(void) { uint8_t cmd MRAM_CMD_WREN; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 1, HAL_MAX_DELAY); MRAM_CS_HIGH(); }注意 WREN 是一个独立命令。发送完 0x06 之后CS# 必须拉高至少一个 SPI 时钟周期让芯片记录这个写使能状态然后再发起后续 Write 指令。很多人第一次移植时只发 WREN 不拉高 CS结果后面写入全部失败。读状态寄存器static uint8_t mram_read_status(void) { uint8_t cmd MRAM_CMD_RDSR; uint8_t status 0; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 1, HAL_MAX_DELAY); HAL_SPI_Receive(hspi1, status, 1, HAL_MAX_DELAY); MRAM_CS_HIGH(); return status; }状态寄存器 bit0 是 WIP表示是否正在写入bit1 是 WEL表示写使能锁存状态。正常情况下 MRAM 写入很快但还是建议在批量写之后检查一次 WIP兼容极端情况。批量写函数void mram_write_buf(uint32_t addr, const uint8_t *buf, uint32_t len) { uint8_t hdr[4]; if ((addr len) MRAM_CAPACITY_BYTES) { return; } mram_write_enable(); hdr[0] MRAM_CMD_WRITE; hdr[1] (uint8_t)(addr 16); hdr[2] (uint8_t)(addr 8); hdr[3] (uint8_t)(addr); MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, hdr, 4, HAL_MAX_DELAY); HAL_SPI_Transmit(hspi1, (uint8_t *)buf, len, HAL_MAX_DELAY); MRAM_CS_HIGH(); while (mram_read_status() 0x01) { /* 等待 WIP 清零 */ } }最大的优势在这里体现出来写数据之前不需要发 Sector Erase也不用担心跨页问题。MRAM 没有页边界概念你从任意地址连续写多少字节都可以芯片自己处理。这在应用层省了非常多事。批量读函数void mram_read_buf(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t hdr[4]; if ((addr len) MRAM_CAPACITY_BYTES) { return; } hdr[0] MRAM_CMD_READ; hdr[1] (uint8_t)(addr 16); hdr[2] (uint8_t)(addr 8); hdr[3] (uint8_t)(addr); MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, hdr, 4, HAL_MAX_DELAY); HAL_SPI_Receive(hspi1, buf, len, HAL_MAX_DELAY); MRAM_CS_HIGH(); }读操作不需要写使能CS# 拉低后先发指令码和 24 位地址再连续读数据即可。接收数据时 HAL_SPI_Receive 内部会自动发送 0xFF 作为时钟所以 MOSI 上不需要额外灌数据。3.3 时序里的关键点WREN 之后必须做一次 CS 高电平我用示波器抓时序时发现这套芯片最容易出问题的地方不是通信速率而是片选和 WREN 的配合。正确的写时序是CS# 从高拉到低。发送 0x06WREN。CS# 从低拉到高。CS# 再次从高拉到低。发送 0x02WRITE 3 字节地址 数据。CS# 从低拉到高。如果你在步骤 3 之后没有把 CS# 拉高紧接着就发 Write芯片内部可能认为整个序列是一条非法指令写入不生效。在软件里一定要保证每个命令都有独立的 CS# 高电平间隔哪怕间隔只是几微秒那也是必要的。还有一点CS# 低电平期间SCK 的下降沿和上升沿必须干净不要有毛刺。MRAM 对边沿比较敏感如果 SCK 信号经过长排线传输可以在靠近芯片端加一个 22Ω 到 33Ω 的串联电阻能有效抑制振铃。4. 应用层设计分区规划、掉电保存与数据校验4.1 512KB 空间怎么分区才够用MR25H40CDF 是 4Mbit也就是 512KB。这个容量不大要提前规划好。我这边分成三个区域区域地址范围说明产品信息区0x00000 ~ 0x0FFFF64KB设备序列号、硬件版本、校准参数关键状态区0x10000 ~ 0x1FFFF64KB当前工作模式、写指针、告警标志日志数据区0x20000 ~ 0x7FFFF384KB循环写入的传感器日志产品信息区只有在出厂校准或者现场升级时才写入平时只读。关键状态区每次开关机或者掉电保存时写入数据量很小。日志数据区是核心每条记录固定 128 字节包含时间戳、通道号、数据值和 CRC按顺序循环写入。日志区用循环队列的思路管理。维护一个 32 位写指针指针本身存放在关键状态区。每次写入时先更新指针再写数据避免中途掉电后找不到有效记录。由于 MRAM 不需要擦除循环覆盖非常容易写满后直接把指针拨回区域起点新数据直接覆盖旧数据不需要像 Flash 那样专门管理“哪些块需要先擦除”。4.2 掉电瞬间抢救关键数据的实现思路工业环境最常出现的致命操作就是突然断电。如果掉电瞬间正好在写日志丢几条日志可接受但关键状态信息不能乱。我在 STM32F765ZI 上用了内部的 PVD 掉电检测把阈值设置到 3.0V 左右当 VDD 降到阈值以下时触发 PVD 中断。中断里只做两件事把关键状态区里的“当前写入地址”和“上一条记录索引”更新到 MRAM 的固定位置。设置一个标志位下次上电时通过这个标志判断是否发生异常掉电。PVD 中断处理要精简不能在中断里做复杂的浮点运算或者整页拷贝只写几十个字节到 MRAM这个操作很快SPI 时钟 27MHz 时不足 0.2ms系统靠电源电容维持这段时间完全够用。还要注意掉电时如果主控有外部看门狗它可能也在复位。所以掉电保存代码不要在中断里等待太长时间写完后直接进入低功耗循环避免因为看门狗复位导致写了一半又重启。4.3 CRC 校验与日志自愈策略MRAM 虽然可靠但工业现场的外部干扰谁也说不准。我每条日志都带 16 位 CRC结构体如下typedef struct { uint32_t timestamp; uint16_t ch1_raw; uint16_t ch2_raw; uint16_t ch3_raw; uint16_t crc16; } log_entry_t __attribute__((packed));每次读取历史日志时逐条校验 CRC。如果某一条 CRC 错误先标记这条记录为“损坏”然后继续向下读而不是直接停止。读取结束后把损坏记录所在的扇区地址记下来在下次写入新日志时优先覆盖掉这些位置。这种“带损坏标记的循环日志”策略比整区格式化实用得多。因为 MRAM 写寿命极高即使某天真的出现了一两条坏数据也不会扩散到整个日志区现场维护只需要把损坏记录挑出来整个存储系统依然可用。我还做了一版双备份产品信息区同时写两个副本一个在主地址一个在偏移 4KB 的位置。读取时比较两个副本的 CRC选正确的那份用。这样即使出现极端情况下的整页写坏设备也能恢复正常参数不需要返厂。5. 调试实录与避坑速查5.1 常见故障速查表这里把我在项目中遇到的和帮别人排查过的问题整理成一张表基本都是可以照着查的现象可能原因排查方向写使能后写入无效WREN 和 WRITE 之间 CS 没有拉高用逻辑分析仪看 CS 电平时序读出来全是 0xFFWP# 或者 HOLD# 悬空检查两个引脚是否有上拉通信错位第一字节丢失SPI 模式不对确认 CPOL/CPHA 是 Mode0 或 Mode3连续写入后偶尔丢数据SPI 时钟过快或者供电不稳降低 SPI 分频检查去耦电容上电后首读正常随后紊乱CS 被硬件 NSS 自动控制把 NSS 配置改成 Software靠近功率器件时数据翻转强磁场干扰增加芯片与磁性元件距离改善地环路写入后立即读回全零地址超出了容量范围检查地址拼接超过 512KB 被截断第一行的问题我印象最深。用 STM32CubeMX 生成的代码每个 SPI 命令都是独立的 HAL_SPI_Transmit看起来没问题但实际 WREN 函数返回后 CS 是拉高的紧接着 Write 函数又把 CS 拉低中间确实有一个高电平脉冲。问题出在有些工程师在实现时把 WREN 和 WRITE 放在同一次 CS 低电平里连续发送芯片完全不认。记住命令独立成帧CS 必须分隔。5.2 我踩过的三个隐蔽坑除了表里的问题还有三个坑比较隐蔽单独拿出来说。第一个坑是地址位宽。MR25H40CDF 虽然是 4Mbit但地址是 24 位只用低 19 位就足够覆盖全部 512KB。然而有些数据手册里的地址格式仍然占用 3 字节高位是无关位。我一开始想把高字节清零省掉直接发两个字节地址结果读到 64KB 之后全是 0因为芯片内部寄存器不接受缩短地址。正确做法是永远把 3 字节地址发全高字节可以填 0。第二个坑是 HAL_SPI_Transmit 接收超时参数。HAL_MAX_DELAY 在裸机环境下没问题但如果你的系统带了 FreeRTOSHAL_SPI_Transmit 内部会释放互斥锁并等待信号量如果不小心在中断里调用会直接卡死。我把驱动里的超时统一改成 100ms并且在写日志任务里绝对不在中断上下文调用 SPI 函数。第三个坑是 HOLD# 引脚在 PCB 上被我不小心连到了 MCU 的复位引脚。看起来像芯片偶发失效实际上是因为系统复位瞬间 HOLD# 被拉低MRAM 进入了暂停状态之后 SPI 通信全部无效。这种问题只能靠查网表发现所以画板的时候一定要把 WP#、HOLD# 这种“平时不该动”的引脚单独拉出来检查。5.3 可靠性与性能测试怎么做做完整套驱动后我跑了几组测试写入速度SPI 时钟 27MHz单条 128 字节日志包含命令开销实测约 1.8MB/s 的有效写入速度。对 100ms 一条记录来说占用不到 1% 时间。连续写入用脚本向日志区写了超过 100 万条记录验证循环覆盖后指针正确、带 CRC 的记录 100% 可读。掉电测试用继电器控制板卡上电和断电每次掉电前随机时刻写入一条日志测了 200 次没有出现指针错乱或关键状态丢失。高温老化把整块板子放进 85℃ 恒温箱持续写入 72 小时读取全部数据比对一致。性能方面 MRAM 和 NOR Flash 差距最明显的是小数据随机写。Flash 在随机写 64 字节时可能先要擦除 4KB 的块时间一下到了几毫秒MRAM 则稳定在几十微秒级别。这个差距在日志记录场景中直接影响任务调度用了 MRAM 后软件里再也不需要单独做“写 Flash 队列”。6. 这样还能怎么扩展容量翻倍与双存储架构6.1 多片 MR25H40 级联扩展容量一片 MR25H40CDF 是 512KB如果日志容量不够最简单的办法是两片并联在同一个 SPI 总线上用两个 CS 引脚区分片选。驱动层加一个“目标芯片号”参数读写前选择对应 CS 即可。硬件上注意两条MISO 是开漏还是推挽MR25H40 的正常 MISO 是三态推挽但仍然建议在每个 MISO 线上加 4.7kΩ 上拉到 VCC避免片选切换瞬间总线悬空。软件上把地址空间从 512KB 扩成 1MB高地址的 CS 选择映射到第二颗芯片上层逻辑基本不用改。如果还想更快可以找支持 Dual SPI 或 Quad SPI 的 MRAM 型号用 STM32F765ZI 的 QSPI 接口做 memory-mapped 模式把 MRAM 映射到 MCU 的地址空间里直接像读 SRAM 一样读写。不过 QSPI 版本的指令时序和 MR25H40 不太一样需要重新适配驱动不是完全 pin-to-pin 兼容选型时要看算力、成本和软件投入。6.2 MRAM 大容量 NOR Flash 的混合方案这个项目到了后期我做了比较务实的容量分配MRAM 负责“必须可靠且频繁更新”的数据NOR Flash 负责“大块但低频”的数据。具体来说系统参数、运行累计量、当前告警状态放在 MRAM。历史告警文件、固件升级包、大段波形文件放在 16MB 的 SPI NOR Flash 里。读取时 MRAM 先把文件索引和地址表加载到 SRAM再通过 NOR Flash 拉取大文件。这种混合方案兼顾了成本、速度和可靠性。MRAM 很贵但只需要小容量NOR Flash 便宜但扛不住高频随机写。把两者组合起来系统既保留了 MRAM 的快速掉电保存能力又有足够空间装固件和日志导出文件。我强烈建议做工业设备时不要指望一颗 MRAM 取代所有存储介质明确数据分类才是正路。6.3 继续深入的几个方向如果这个方案要继续演进我觉得至少有几个方向值得试把 MRAM 的读写封装成一个统一的 KV 存储接口上层不要直接操作原始地址而是通过 key 来读写方便以后迁移到不同的非易失存储芯片。在 STM32F765ZI 上把 SPI 驱动改成 DMA 模式进一步提高写入吞吐给更多采集通道留余量。增加状态检查任务定期读取 MRAM 的状态寄存器判断芯片健康度并上报。对日志区做简单的磨损感知虽然 MRAM 寿命极高但通过记录每个区域的擦写次数可以更早发现异常适合要求严苛的无人值守设备。最后再分享一个小技巧我在应用层给每条日志都加了 2 字节同步头 0xA5 0x5A。读取时先找同步头再根据记录长度跳转。就算哪次记录被意外破坏也不会导致整个日志区解析失败。做工业存储最怕的不是数据坏一块而是坏一块之后整个文件系统都打不开。把日志设计成“条记录独立可解析”比事后恢复数据要省心得多。
返回列表