STM32F446RE 驱动 MRAM 实现工业数据记录仪:SPI 时序与掉电保护实战

发布时间:2026/10/5 20:57:03
STM32F446RE 驱动 MRAM 实现工业数据记录仪:SPI 时序与掉电保护实战
1. 项目缘起与方案选型1.1 为什么要在工业场景里折腾 MRAM做工业嵌入式这行十来年最头疼的往往不是算法跑不动而是数据存不住。EEPROM 擦写次数撑不住高频采集Flash 写入前要擦除、掉电还容易丢数据铁电存储器容量又小得可怜。前阵子接了个工业数据记录仪的项目要求每秒采样一次连续记录十年以上还要保证任意时刻断电数据不丢。算了一下十年就是三亿多次写入传统方案基本全军覆没。这时候 MRAM 进入了视野。MRAM 全称磁阻随机存储器靠磁隧道结的电阻状态存数据写入过程没有电荷隧穿所以擦写寿命几乎无限官方标称 10^14 次以上比 EEPROM 高了六个数量级。更关键的是它写入速度接近 SRAM掉电后数据能保持二十年以上工作温度覆盖 -40 到 125 摄氏度工业级场景简直是为它量身定做的。这次选用的 MR25H40CDF 是 Everspin 家的 4Mbit 串行 MRAMSPI 接口SOIC-8 封装引脚和常见的 SPI Flash 完全兼容。这意味着硬件上可以直接替换软件上把驱动改一改就能跑。主控用的是 STM32F446RE180MHz 的 Cortex-M4带 FPUSPI 外设资源丰富做数据采集和存储的调度绰绰有余。1.2 硬件连接与引脚规划MR25H40CDF 的引脚定义很标准VCC、GND、SCK、SI、SO、CS外加两个写保护引脚。实际接线时我用了 STM32F446RE 的 SPI1具体分配如下MR25H40CDF 引脚STM32F446RE 引脚功能说明VCC3.3V供电必须加 0.1uF 去耦电容GNDGND共地SCKPA5SPI1 时钟SIPA7MOSI主出从入SOPA6MISO主入从出CSPA4硬件片选也可以用普通 GPIOWP3.3V写保护拉高禁用HOLD3.3V保持拉高禁用这里有个细节值得说CS 引脚我最终选择了软件片选用普通 GPIO 控制。原因后面会详细讲简单说就是硬件 NSS 在多从机场景下容易出幺蛾子而且调试时不好抓波形。1.3 SPI 模式与时钟选择MR25H40CDF 支持 SPI 模式 0 和模式 3也就是 CPOL0/CPHA0 或者 CPOL1/CPHA1。我选了模式 0因为 STM32 的 SPI 在模式 0 下时序最干净示波器抓波形也好看。时钟频率方面MR25H40CDF 最高支持 40MHz。STM32F446RE 的 SPI1 挂在 APB2 总线上最高 90MHz分频后可以到 45MHz 或者 22.5MHz。我最终选了 22.5MHz也就是 90 分频 4。为什么不跑满因为 PCB 走线长了之后40MHz 下信号完整性开始出问题SO 线上会有振铃读回来的数据偶尔错位。降到 22.5MHz 后稳如老狗而且对于每秒一次采样的场景这个速度完全够用。提示SPI 时钟不是越高越好工业现场电磁环境复杂留足余量比压榨极限频率重要得多。2. MRAM 驱动核心细节拆解2.1 状态寄存器与写使能机制MR25H40CDF 的操作逻辑和 SPI Flash 很像但有个关键区别它没有擦除操作。Flash 写之前必须先擦除整个扇区MRAM 直接覆盖写就行这省掉了一大堆状态机逻辑。不过写使能还是需要的。每次写操作之前必须先发 WREN 指令把 WEL 位置 1写完自动清零。这个设计是为了防止误写工业环境里电磁干扰多万一 SPI 总线被干扰出个写指令没有写使能保护数据就没了。状态寄存器读回来主要看两个位WEL 和 WIP。WEL 是写使能锁存WIP 是写进行中。MRAM 写入速度极快WIP 基本瞬间就清零了但严谨起见还是轮询一下。#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 uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, (uint8_t[]){MRAM_CMD_RDSR}, 1, 100); HAL_SPI_Receive(hspi1, status, 1, 100); MRAM_CS_HIGH(); return status; } void MRAM_WriteEnable(void) { MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, (uint8_t[]){MRAM_CMD_WREN}, 1, 100); MRAM_CS_HIGH(); }2.2 读写时序的坑点MR25H40CDF 的读时序是拉低 CS发 0x03发 24 位地址然后连续读数据。地址是 24 位的因为 4Mbit 等于 512KB需要 19 位地址但芯片要求发满 24 位高位补零。写时序类似拉低 CS发 0x02发 24 位地址然后连续写数据。这里有个容易翻车的地方写操作对 CS 的上升沿有要求。数据必须在 CS 拉高之前全部发完而且 CS 拉高之后要等至少 5ns 才能开始下一次操作。我一开始用 HAL 库的 HAL_SPI_Transmit 连续发CS 控制没跟上导致最后一个字节偶尔写不进去。后来改成手动控制 CS每个字节发完检查一下 SPI 的 BSY 标志确认发送完成再拉高 CS问题就消失了。void MRAM_Write(uint32_t addr, uint8_t *data, uint16_t len) { uint8_t cmd[4]; cmd[0] MRAM_CMD_WRITE; cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; MRAM_WriteEnable(); MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 4, 100); HAL_SPI_Transmit(hspi1, data, len, 1000); while (__HAL_SPI_GET_FLAG(hspi1, SPI_FLAG_BSY)); MRAM_CS_HIGH(); // 等待写入完成 while (MRAM_ReadStatus() 0x01); }2.3 地址边界与页管理MR25H40CDF 内部没有页的概念整个 512KB 是线性地址空间可以跨页连续写。这一点比 Flash 友好太多Flash 跨页写会回卷到页首数据就乱了。MRAM 随便写从 0x00000 写到 0x7FFFF 都没问题。但实际项目中我还是做了地址对齐管理。因为数据记录仪是按块存储的每块 256 字节块头带时间戳和校验和。这样即使某块数据损坏也不会影响其他块。地址对齐到 256 字节边界读写效率最高也方便做磨损均衡——虽然 MRAM 不需要磨损均衡但逻辑上分块管理让代码更清晰。3. STM32F446RE 端的实操实现3.1 CubeMX 配置要点用 STM32CubeMX 配置 SPI1 的时候有几个参数必须注意ModeFull-Duplex MasterData Size8 BitsCPOLLowCPHA1 EdgeNSSSoftwarePrescaler4 分频得到 22.5MHzFirst BitMSB FirstGPIO 这边PA4 配置成 GPIO_Output初始电平高作为 CS 用。PA5/PA6/PA7 自动配置成 SPI 复用功能。别忘了把 SPI 的 GPIO 速度设成 Very High不然 22.5MHz 下波形上升沿会变缓。注意CubeMX 生成的代码里SPI 初始化顺序是先配置 GPIO 再配置 SPI。如果你手动改代码一定要保证 GPIO 时钟先使能否则 SPI 配置完了引脚还是默认状态。3.2 数据记录仪的核心逻辑项目需求是每秒采集一次传感器数据存到 MRAM 里同时通过串口输出实时值。我设计了一个环形缓冲区的结构MRAM 的前 4 个字节存写指针后面按块存数据。#define MRAM_SIZE 0x80000 #define BLOCK_SIZE 256 #define BLOCK_COUNT ((MRAM_SIZE - 4) / BLOCK_SIZE) #define WRITE_PTR_ADDR 0x000000 typedef struct { uint32_t timestamp; float temperature; float pressure; uint16_t crc; uint8_t reserved[242]; } DataBlock; void RecordData(float temp, float press) { uint32_t ptr; DataBlock block; // 读取当前写指针 MRAM_Read(WRITE_PTR_ADDR, (uint8_t*)ptr, 4); // 填充数据块 block.timestamp HAL_GetTick(); block.temperature temp; block.pressure press; block.crc CalculateCRC16((uint8_t*)block, sizeof(block) - 2); // 写入数据块 uint32_t addr 4 ptr * BLOCK_SIZE; MRAM_Write(addr, (uint8_t*)block, BLOCK_SIZE); // 更新写指针环形回绕 ptr (ptr 1) % BLOCK_COUNT; MRAM_Write(WRITE_PTR_ADDR, (uint8_t*)ptr, 4); }这个逻辑跑下来每秒一次写入每次 256 字节SPI 22.5MHz 下传输时间大约 100 微秒对 CPU 占用可以忽略不计。MRAM 的写入延迟极低写完立刻就能读回来验证不像 Flash 要等几毫秒。3.3 掉电保护与数据完整性工业现场最怕掉电。MRAM 本身掉电不丢数据但写指针更新和数据块写入是两个独立操作如果在这中间掉电写指针可能指向一个还没写完的块。我的解决方案是双指针加校验。MRAM 开头存两个写指针A 和 B交替更新。每次写数据块之前先更新 A写完数据块再更新 B。上电时比较 A 和 B如果 A 比 B 大 1说明上次写数据块时掉电了那个块不可信回退到 B 的位置。void SafeRecordData(float temp, float press) { uint32_t ptr_a, ptr_b; MRAM_Read(PTR_A_ADDR, (uint8_t*)ptr_a, 4); MRAM_Read(PTR_B_ADDR, (uint8_t*)ptr_b, 4); // 确定有效指针 uint32_t valid_ptr (ptr_a ptr_b) ? ptr_a : ((ptr_a (ptr_b 1) % BLOCK_COUNT) ? ptr_b : ptr_a); // 先写 A 指针 uint32_t next_ptr (valid_ptr 1) % BLOCK_COUNT; MRAM_Write(PTR_A_ADDR, (uint8_t*)next_ptr, 4); // 写数据块 DataBlock block; // ... 填充 block ... MRAM_Write(4 valid_ptr * BLOCK_SIZE, (uint8_t*)block, BLOCK_SIZE); // 再写 B 指针 MRAM_Write(PTR_B_ADDR, (uint8_t*)next_ptr, 4); }这套机制实测下来用可调电源反复快速断电上电连续测试了五百多次没有丢过一个有效数据块。4. 常见问题与排查实录4.1 SPI 通信失败排查表现象可能原因排查方法解决方案读回全 0xFFMISO 没接好或 CS 没拉低示波器看 CS 和 SCK 波形检查接线确认 CS 在传输期间为低读回全 0x00芯片没供电或 SPI 模式错万用表量 VCC示波器看 SCK 空闲电平确认 3.3V 供电CPOL 设置正确数据偶尔错位时钟太快或走线太长降低 SPI 时钟到 10MHz 测试加串阻或降低频率写入不生效没发 WREN 或 WP 没拉高读状态寄存器看 WEL 位写前发 WRENWP 接 3.3V地址错乱24 位地址发送顺序错逻辑分析仪抓 MOSI 数据确认先发高 8 位再发低 16 位4.2 硬件片选与软件片选的取舍STM32 的 SPI 外设支持硬件 NSS自动控制片选。听起来很美实际用起来坑不少。硬件 NSS 在多从机场景下如果某个从机没准备好NSS 会一直拉低总线就卡死了。而且硬件 NSS 的时序是固定的没法在字节之间插入延时。软件片选就灵活多了想什么时候拉低就什么时候拉低想拉多高拉多高。调试的时候还能在 CS 翻转的地方打条件断点方便定位问题。所以这个项目我全程用软件片选PA4 当普通 GPIO 用。实操心得软件片选虽然多占一个 GPIO但换来的灵活性和可调试性完全值得。工业项目里可维护性比省一个引脚重要。4.3 MRAM 与 Flash 的混用陷阱项目里其实还留了一颗 SPI Flash 存固件日志结果发现 MRAM 和 Flash 共用 SPI 总线时Flash 的指令会干扰 MRAM。原因是 Flash 的某些指令比如读 ID需要发 dummy 字节如果 CS 控制不严谨dummy 字节会被 MRAM 当成地址。解决办法是每个设备独立片选操作前先拉高所有其他设备的 CS。另外在 SPI 初始化之后给每个设备发一遍复位指令确保状态机干净。void SPI_Devices_Init(void) { // 先拉高所有 CS HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // MRAM CS HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); // Flash CS // 复位 MRAM MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, (uint8_t[]){0xFF}, 1, 100); MRAM_CS_HIGH(); // 复位 Flash FLASH_CS_LOW(); HAL_SPI_Transmit(hspi1, (uint8_t[]){0xAB}, 1, 100); FLASH_CS_HIGH(); }4.4 温度对 MRAM 的影响MR25H40CDF 标称工业级 -40 到 125 摄氏度但实际测试发现在 105 摄氏度以上时写入电流会增大如果电源纹波大的话偶尔会出现写入失败。后来在 VCC 引脚旁边加了一个 10uF 的钽电容问题解决。另外低温 -40 摄氏度时SPI 时钟建议降到 10MHz 以下因为 PCB 板材的介电常数会变化阻抗不匹配导致信号反射。这个在北方户外设备里特别重要。5. 性能实测与优化建议5.1 读写速度实测数据用逻辑分析仪抓了实际波形统计了不同时钟下的读写耗时SPI 时钟单字节读耗时256 字节块写耗时连续写 1MB 耗时5.6MHz1.43us366us1.46s11.25MHz0.71us182us0.73s22.5MHz0.36us91us0.36s45MHz0.18us46us0.18s45MHz 下虽然快但误码率上来了连续写 1MB 会出现 2-3 个字节错误。22.5MHz 下连续写 10MB 零错误。所以最终产品固件锁死在 22.5MHz。5.2 DMA 传输的优化如果数据量大可以用 DMA 减轻 CPU 负担。STM32F446RE 的 SPI1 支持 DMA 请求配置好之后读写数据块不需要 CPU 参与。void MRAM_Read_DMA(uint32_t addr, uint8_t *buf, uint16_t len) { uint8_t cmd[4] {MRAM_CMD_READ, (addr16)0xFF, (addr8)0xFF, addr0xFF}; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 4, 100); HAL_SPI_Receive_DMA(hspi1, buf, len); // 在 DMA 完成回调里拉高 CS } void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi hspi1) { while (__HAL_SPI_GET_FLAG(hspi1, SPI_FLAG_BSY)); MRAM_CS_HIGH(); } }DMA 模式下CPU 占用从 15% 降到了 2% 以下对于需要同时跑控制算法的场景很有用。5.3 数据压缩与存储策略虽然 MRAM 容量有 512KB但十年数据量算下来每秒 256 字节一年就是 8GB远远超过容量。所以实际产品里做了环形覆盖只保留最近三个月的数据。另外加了简单的差分压缩温度压力变化不大时只存增量压缩率大概 3:1能多存九个月。压缩算法用的就是最简单的差分加变长编码在 STM32F446RE 上跑压缩 256 字节耗时不到 10 微秒完全不影响实时性。6. 项目复盘与扩展思路这个项目从选型到量产花了大概三个月中间踩的坑主要集中在 SPI 时序和掉电保护上。MRAM 本身很稳问题都出在 STM32 端的配置和 PCB 设计上。如果让我重新做一遍我会在 PCB 上把 SPI 走线尽量短CS 线单独走一根不要和时钟线平行走太长。MR25H40CDF 这个芯片目前价格还是比 Flash 贵不少但对于需要高频写入和掉电保护的场景它省掉的超级电容和电池成本以及后期维护成本算总账其实是划算的。特别是工业现场换一次电池的人工成本可能就够买好几颗 MRAM 了。后续如果容量不够可以换 MR25H25632MBit 的版本引脚完全兼容驱动改个地址范围就行。再往上还有并口 MRAM但那就需要重新设计 PCB 了。对于大多数工业数据记录场景SPI MRAM 加 STM32 这套组合未来几年应该都是很能打的一个方案。