ARTICLE DETAIL

资讯详情

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

MR25H40CDF与STM32F042C6的SPI MRAM工业存储设计实战

MR25H40CDF与STM32F042C6的SPI MRAM工业存储设计实战 最近在做一块现场数据记录模块存储要从小的 EEPROM 升级到 4Mbit器件选型最后落在了 Everspin 的 MR25H40CDF 上主控是 STM32F042C6。这套组合不算冷门但真正把数据存进去、读出来、跨掉电跨温度还不丢中间有不少细节比数据手册上写的更值得琢磨。这篇我就按自己的实操顺序把选型逻辑、引脚接线、SPI 驱动、数据组织和几个实测踩坑一起讲透给同样在工业嵌入式里折腾数据存储的朋友做个参考。MR25H40CDF 本质是一个 512KB 的 SPI 接口 MRAMSTM32F042C6 则是一颗 Cortex-M0 内核、主频 48MHz 的小封装 MCU。放在工业场景里这俩组合最舒服的地方是MCU 便宜够用MRAM 不用擦除直接重写寿命又远高于 Flash特别适合频繁写参数、存日志、保存掉电状态的场合。1. 工业现场选存储芯片我为什么绕开了 Flash 和 EEPROM1.1 MR25H40CDF 在这条产品线里的位置Everspin 的 MR25 系列是串行 SPI 接口的 MRAMMR25H40CDF 的容量是 4Mbit也就是 512KB按字节寻址是 512K x 8。它采用磁阻式随机存储技术数据不是靠电荷锁存在浮栅里而是靠磁隧道结的磁化状态来保持所以它天生就有几个别的存储芯片很难同时满足的优点读写随机访问、掉电不丢失、写入不需要先擦除、写入次数几乎没有限制。我最早也是把 EEPROM 和 SPI Flash 当默认方案后来发现工业现场的要求越来越拧巴。设备要频繁记录传感器数据5 秒钟写一次一年下来就是 630 万次写入。普通 EEPROM 的寿命通常在 10 万到 100 万次擦写SPI Flash 虽然容量大但按 sector 擦除的特性让“频繁小块写入”变成一场灾难。MR25H40CDF 这类 MRAM 在这类场景下几乎没有寿命焦虑写之前也不用管原来是什么内容直接把地址覆盖进去就行这是我最看重的一点。1.2 不擦除、直接覆盖这是工业落盘的正确姿势我身边不少同事第一次用 MR25H40CDF 时还是会下意识地按 SPI Flash 的流程走先查哪个 block 有数据、先擦除、再写入。这完全是惯性。MRAM 最关键的一点就是普通数据写入可以直接覆盖你不用维护“哪些块是空的”这种表。举一个实际项目里的例子一个温度采集节点需要每分钟存一组 16 字节的校准结果同时还要保存当天累计量。原来用 64KB SPI Flash每写一次都要先擦除一个 4KB sector而擦除期间还不能断电否则整个 sector 都可能被破坏。换成 MR25H40CDF 之后我直接用固定地址覆盖写写失败了就重写逻辑上简单了不止一个量级。在工业嵌入式里存储可靠性不只是芯片本身的事更关键的是写入模型。MRAM 把这个模型简化成了“像 RAM 一样写掉电不丢”正好卡在工业痛点上。1.3 为什么搭配 STM32F042C6 这类 Cortex-M0 是性价比之选STM32F042C6 是 32KB Flash、6KB RAM 的小芯片很多人第一反应是“这么小能跑什么”。但对一个存储模块来说它完全够用。MR25H40CDF 走 SPI 接口SPI 本身就是 M0 内核的外设不需要什么复杂操作系统更不用跑大算力任务。选 STM32F042C6 还有几个实际原因1.8V 到 3.6V 供电范围跟 MR25H40CDF 的 3.3V 正好同域芯片带独立的 SPI1 和 SPI2可以拿一路专门给存储器还有 RTC、独立看门狗、掉电检测非常适合做数据记录类设备。更关键的是这颗料在工业温度范围内供货稳定交期和成本都可控嵌入式存储这种模块没必要上过剩的主控。2. 拿到 MR25H40CDF 后先过一遍引脚和 SPI 时序再画板2.1 DFN-8 封装引脚与最小电路MR25H40CDF 常见的是 8 脚 DFN 封装引脚功能和普通 SPI Flash 基本兼容但不要想当然直接套 Flash 电路建议画板前把官方数据手册的封装图再核对一次。我这里把引脚关系整理成表方便对照 STM32F042C6引脚信号方向接 STM32F042C6说明CS#输入PA4/任意 GPIO片选低有效必须软件控制SCK输入PA5/SPI1_SCK时钟最高可到 40MHzSI输入PA7/SPI1_MOSI数据输入SO输出PA6/SPI1_MISO数据输出WP#输入接 10k 上拉到 VDD写保护不用时不要悬空HOLD#输入接 10k 上拉到 VDD暂停传输不用时不要悬空VDD电源3.3V就近 100nF 去耦也建议再并一个 1uFVSS地GND铺地要完整芯片的 VDD 引脚旁边我放了 100nF 陶瓷电容这个是基本操作。另外还有个大电容 1uF放在供电入口可以吸收 SPI 突发读取时的电流尖峰。MR25H40CDF 读操作功耗会明显比待机高供电不要用太细的走线。2.2 SPI Mode 0/3 的兼容性设定MR25H40CDF 支持 SPI Mode 0 和 Mode 3。区别在于时钟极性和相位Mode 0 是 CPOL0、CPHA0Mode 3 是 CPOL1、CPHA1。STM32F042C6 自带 SPI 外设可以在初始化里直接配。我习惯用 Mode 0原因不是 Mode 3 不好而是我在这块板子上 SPI 总线上暂时只有一颗存储器Mode 0 的时序更直观逻辑分析仪抓波形时也好判断。有一点要注意千万不要把 CPOL 和 CPHA 配反。如果出现读取数据全是 0xFF 或者第一个字节对不上先怀疑 SPI 模式再去查接线。有人会把 Mode 3 芯片当 Mode 0 用数据错位的时候很难看出来因为有些命令本身还能歪打正着。2.3 WP# 和 HOLD# 不能悬空这个坑我见过不止一次。MR25H40CDF 的 WP# 和 HOLD# 如果悬空板子正常使用可能没事但现场一旦有电磁干扰这两个引脚就可能被拉低。HOLD# 被拉低时 SPI 传输会暂停MCU 那个边沿等不到数据直接死等造成假死机WP# 被拉低时状态寄存器写保护打开你会发现写命令发了但数据没进去。稳妥做法是两个引脚都通过 10k 电阻上拉到 VDD。PCB 上如果空间紧张直接接 VDD 也可以但我保留电阻位还有一个好处需要验证状态寄存器写保护功能时可以临时断开不需要改版。2.4 上电时序和复位后的首次访问MR25H40CDF 没有严格的复位引脚上电之后很快就能访问但工业上为了保证可靠我一般会在 STM32F042C6 初始化的最后再访问存储器也就是给供电稳定留至少几毫秒时间。不要一上电就在 main 函数最开始立刻读 MRAM尤其是电源斜率比较缓的场合偶尔会读出全 0xFF。在固件里我习惯上电后先执行一次读取设备状态并打印再读取一个固定地址的“软件版本”字段。如果这个字段读出来是 0xFF说明存储器还没有被初始化写过这时候才执行全片格式化逻辑。这样可以把“首次上电”和“损坏”明确区分开。3. 512KB 怎么用把容量管理成小型文件系统而不是一条道写到尾3.1 地址映射与分区设计MR25H40CDF 的 512KB 虽然可以直接当一个大数组用但在工业数据存储里我强烈建议先做分区再决定哪些地址放什么。我在项目里的分区表大致是这样的地址范围大小用途0x000000 - 0x000FFF4KB系统参数如设备编号、通信地址、校准系数0x001000 - 0x001FFF4KB掉电保存的临时状态、运行标志0x002000 - 0x003FFF8KB滚动运行日志0x004000 - 0x007FFF16KB报警记录、事件记录0x008000 - 0x7FFFFF~488KB业务数据、采集数据块、升级备份之所以把系统参数放在最前面是因为很多现场故障排查时会直接读整片内容参数区放前面用调试器查看时一眼就能看到。滚动日志区则只做固定 8KB 环形覆盖每次写入一个 64 字节的记录头部包含时间戳和序号这样即使不建复杂索引也能按块回读。3.2 记录头、CRC 和事务性提交工业数据存储最重要的不是“能写”而是“写到一半掉电了怎么办”。MR25H40CDF 虽然不会像 Flash 那样因为擦除掉电丢掉整个 sector但一次超过 64 字节的写入仍然可能在半路被断电中断。为了应对这种情况我给每条记录加了一个 8 字节头前 4 字节魔数和记录长度第 5、6 字节写入序号或时间戳第 7、8 字节整个记录体的 CRC16写入时先写记录体最后写头部。读取时先读头部校验魔数和 CRC如果魔数不正确就认为这条记录不完整直接跳过或者回退到上一条完整记录。这种“提交标记”的思路成本很低但能极大提高现场数据可信度。同样对系统参数区我也不是单份保存而是 A/B 双份。每次更新参数时先写备份区再写主区上电时优先读主区主区校验失败再读备份区。MRAM 寿命长双份开销完全值得。3.3 避免把 MRAM 当无限 RAM 乱写虽然 MR25H40CDF 写入寿命很高但功率和时效仍然有边界。工业嵌入式里最常见的错误是把采集任务里的一个结构体直接 memset 之后整块写进去哪怕里面很多字段根本没变化。我在设计数据结构时会把“固定不变的内容”和“高频变化的内容”分开。比如传感器标定表是固定内容只在产线标定时写一次实时累计量则放到另一个固定地址每次就地覆盖。这样好处有两个一是写入包短耗电和耗时都小二是出问题后很容易定位因为每个地址对应什么含义是固定的。4. STM32F042C6 上的 SPI 驱动写入、回读和自检代码全程4.1 SPI1 引脚复用与初始化STM32F042C6 的 SPI1 默认可以映射到 PA4/5/6/7其中 PA5 是 SCKPA6 是 MISOPA7 是 MOSIPA4 可以作为软件片选。初始化前要先开启 GPIOA 和 SPI1 的时钟。下面是完整的初始化代码我习惯用 HAL 库配合少量寄存器操作。void MRAM_SPI_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_SPI1_CLK_ENABLE(); // PA5: SCK, PA7: MOSI, 复用推挽 GPIO_InitStruct.Pin GPIO_PIN_5 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; GPIO_InitStruct.Alternate GPIO_AF5_SPI1; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // PA6: MISO, 复用浮空输入 GPIO_InitStruct.Pin GPIO_PIN_6; GPIO_InitStruct.Mode GPIO_MODE_AF_INPUT; GPIO_InitStruct.Pull GPIO_NOPULL; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // PA4: CS#, 普通推挽输出, 初始化为高 GPIO_InitStruct.Pin GPIO_PIN_4; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_PULLUP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); MRAM_CS_HIGH(); 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_8; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; HAL_SPI_Init(hspi1); }这里把预分频设为 8STM32F042C6 主频 48MHz 时 SPI 时钟就是 6MHz。MR25H40CDF 支持更高频率但 6MHz 对绝大多数工业存储够用而且布线要求更宽松。如果你要追求最快可以把分频改成 2跑 24MHz但 MISO 和 SCK 走线必须短而干净。4.2 底层字节收发封装HAL 库的 HAL_SPI_TransmitReceive 封装了收发但在高频调用的场景下产生的开销偏大。我改用寄存器方式做一个最精简的 SPI 字节交换函数逻辑非常直白发送一个字节的同时接收一个字节。由于是全双工 SPI读数据时发送 0x00 作为时钟驱动。uint8_t MRAM_SpiXfer(uint8_t txData) { while (!(SPI1-SR SPI_SR_TXE)) { // 等待发送缓冲区为空 } SPI1-DR txData; while (!(SPI1-SR SPI_SR_RXNE)) { // 等待接收缓冲区非空 } return (uint8_t)SPI1-DR; }注意发送后必须等待 RXNE否则连续传输时第二个字节会把第一个字节覆盖。新浪对调试还有个小技巧在逻辑分析仪上看 SCK 波形如果 MISO 数据跳变发生在 SCK 下降沿附近说明模式基本正确如果采样点边缘上有毛刺就要检查时钟相位或者加长 SCK 到 MISO 之间的走线。4.3 写入命令、状态轮询与整包写入MR25H40CDF 最常用的命令有 0x02 写数据、0x03 读数据、0x05 读状态寄存器、0x06 写使能、0x01 写状态寄存器。为保险起见我在写入前都会先发 0x06 写使能。MRAM 对普通数据写入不一定强制要求写使能但这条命令不影响数据写入而且能跟状态寄存器操作保持统一流程。#define MRAM_CMD_WREN 0x06 #define MRAM_CMD_WRITE 0x02 #define MRAM_CMD_READ 0x03 #define MRAM_CMD_RDSR 0x05 uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS_LOW(); MRAM_SpiXfer(MRAM_CMD_RDSR); status MRAM_SpiXfer(0x00); MRAM_CS_HIGH(); return status; } void MRAM_WriteEnable(void) { MRAM_CS_LOW(); MRAM_SpiXfer(MRAM_CMD_WREN); MRAM_CS_HIGH(); }整包写入的思路是先拉低 CS#发送命令和 24 位地址然后连续发送数据全部结束后拉高 CS#。地址最大不能超过 0x7FFFF这一点在传入参数时要加断言。void MRAM_WriteBytes(uint32_t addr, const uint8_t *buf, uint32_t len) { uint32_t i; if ((addr len) 0x80000u) return; // 超出 512KB if (buf 0u || len 0u) return; MRAM_WriteEnable(); MRAM_CS_LOW(); MRAM_SpiXfer(MRAM_CMD_WRITE); MRAM_SpiXfer((uint8_t)(addr 16)); MRAM_SpiXfer((uint8_t)(addr 8)); MRAM_SpiXfer((uint8_t)(addr)); for (i 0; i len; i) { MRAM_SpiXfer(buf[i]); } MRAM_CS_HIGH(); // 简单轮询 WEL 位, 等写使能清除 for (i 0; i 1000; i) { if ((MRAM_ReadStatus() 0x02u) 0u) { break; } } }这里我建议在外部再封装一层“单条记录写入”函数把头部、CRC、双份备份三件事做进去而不是让业务代码直接调用 MRAM_WriteBytes。因为直接调用底线太低了很容易出现长度越界、地址错位等问题。4.4 读取命令与校验回读读取比写入更简单命令 0x03 加上 24 位地址然后连续发 0x00 收数据。下面是一个带 CRC 校验的回读函数实际项目中我会在写入后立刻回读关键字段用来检查 SPI 波形和存储器状态。uint16_t MRAM_CRC16(const uint8_t *data, uint32_t len) { uint16_t crc 0xFFFF; uint32_t i; uint8_t j; for (i 0; i len; i) { crc ^ ((uint16_t)data[i] 8); for (j 0; j 8; j) { if (crc 0x8000u) crc (crc 1) ^ 0x1021u; else crc (crc 1); } } return crc; } void MRAM_ReadBytes(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t i; if ((addr len) 0x80000u) return; if (buf 0u || len 0u) return; MRAM_CS_LOW(); MRAM_SpiXfer(MRAM_CMD_READ); MRAM_SpiXfer((uint8_t)(addr 16)); MRAM_SpiXfer((uint8_t)(addr 8)); MRAM_SpiXfer((uint8_t)(addr)); for (i 0; i len; i) { buf[i] MRAM_SpiXfer(0x00); } MRAM_CS_HIGH(); }写完驱动后我会做一轮全地址自测随机生成几个测试模式比如 0xA5、0x5A、0x00、0xFF写到片内不同区域回读后逐字节比较。这里有一个值得说的细节不要只测 0x00 和 0xFF因为这类模式对 SPI 时序的敏感度低最好用 0xAA 和 0x55 这种交替位模式可以暴露时钟采样点偏移。5. 实测数据、性能数字和我在稳定性上踩过的三个坑5.1 实际写入耗时和全片回读耗时在 6MHz SPI 时钟下写单个字节的固定开销是命令头 4 字节加数据 1 字节也就是 5 字节需要 40 个时钟周期算上 CS 切换和状态位轮询单次写 64 字节记录大概在 0.1ms 级别。如果大批量写 4KB 参数区传输速率约 0.7MB/s不到 10ms 就能写完。把预分频调到 2、SPI 跑到 24MHz 后全片 512KB 回读理论上只要约 0.17 秒实际加上地址递增和命令头损耗大概 0.2 秒出头。这个性能对工业现场完全足够。最重要的是 MRAM 写入不用等擦除所以写入耗时长尾问题基本不存在。Flash 写一个小记录有时会等一个擦除周期可能到几十毫秒甚至上百毫秒这在高速数据记录场景里是不能忍的。5.2 坑一CS# 在上电瞬间被干扰导致首个命令丢失第一批样板回来后我发现偶尔有上电后第一次读参数失败的情况。用示波器抓 CS# 波形发现 PA4 在 MCU 启动初期有一个短暂的抖动正好 MRAM 供电也还没稳定芯片可能把抖动理解成了一次片选导致后续命令对不上。解决办法很简单CS# 引脚加 10k 上拉到 VDD并在 GPIO 初始化时先输出高电平再初始化 SPI 外设。这样上电到 MCU 运行之间CS# 一直保持高不会误触发。如果你的 CS# 接到了某个默认低电平输出引脚上情况会更麻烦。选引脚的时候一定要避开那些在复位期间被外设拉低的管脚这也是我坚持使用 PA4 并做上拉的原因。5.3 坑二低功耗 STOP 模式后 SPI 外设“假死”这块板子为了省电会进入 STOP 模式唤醒后继续记录数据。结果有几次唤醒后写入全部失败。排查发现不是 MRAM 问题而是 STM32F042C6 在 STOP 模式唤醒后SPI1 的时钟树没有按要求重新稳定导致 SPI 状态机卡在某个中间态。解决办法是在退出 STOP 后重新调用 HAL_SPI_Init 做一次外设复位或者把整颗 MRAM 重新初始化一遍。简单粗暴但非常有效。更规范的做法是在进入 STOP 前把所有 SPI 片选拉高并且确保没有正在进行的传输退出 STOP 后再给 SPI 外设一个足够长的稳定时间。如果我们只依赖 HAL_Delay 而没有等待就绪标志偶尔也会踩到。总之低功耗 SPI 的组合一定要把“唤醒后重建外设状态”当成标准流程。5.4 坑三只读校验通过不代表掉电后一定正确有一次自测时读取全部正常但现场断电重启后日志区却出现了部分乱码。后来发现原因很典型我在写入日志时先写了 64 字节记录体又单独写了 8 字节头部。掉电正好发生在写记录体和写头部之间的窗口旧头部还没被覆盖新记录体已经写了一半CRC 校验按旧头部去解析自然就乱了。解决方式就是我前面说的“提交标记”思路头部必须最后写而且头部里要放记录长度和 CRC。系统上电后解析到魔数不匹配的记录直接跳过。另一个辅助方案是写两条记录A 记录写全后置一个“有效”标志B 记录作为冗余覆盖读的时候优先选 AA 无效选 B。这样即使掉电窗口再小也能保证至少有一条完整记录可用。这块芯片我已经用了两轮产线改版最大的体会是MR25H40CDF 本身很皮实真正决定可靠性的是你外围的 GPIO 初始化、掉电处理和数据格式设计。如果你也准备把它跟 STM32F042C6 搭起来建议先按上面这套流程跑通全片自测再开始设计业务数据结构。最后分享一个小技巧第一次打板时在 CS#、SCK、SI、SO 四根线上各留一个测试点调试时用逻辑分析仪抓整包命令很多莫名其妙的问题一眼就能定位。
返回列表