ARTICLE DETAIL

资讯详情

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

工业嵌入式存储选型:MRAM与STM32F373RC的SPI驱动实战

工业嵌入式存储选型:MRAM与STM32F373RC的SPI驱动实战 1. 为什么在工业现场我会优先考虑 MRAM 而不是 Flash做工业嵌入式这行十几年存储方案选型这件事上我踩过的坑比写过的驱动还多。早些年做数据采集终端EEPROM 擦写寿命不够用Flash 又怕掉电丢数据每次现场断电测试都提心吊胆。后来接触到 MRAM 这类磁性随机存储器才算是找到了一个在工业场景下真正省心的方案。这次拿 MR25H40CDF 配合 STM32F373RC 做数据存储与读取就是想把这条链路完整跑通顺便把过程中那些文档里不会写的细节整理出来。MR25H40CDF 是 Everspin 出品的一款 4Mbit 容量的 SPI 接口 MRAM采用 8 引脚 DFN 封装工作电压 2.7V 到 3.6V工业级温度范围覆盖 -40°C 到 85°C。它最大的特点就是非易失性加上近乎无限的擦写寿命官方标称擦写次数超过 10 的 14 次方这个量级基本上意味着你随便写不用担心寿命问题。相比之下传统 NOR Flash 擦写寿命通常在 10 万次左右EEPROM 也就百万次级别在高频写入的工业场景下寿命就是悬在头顶的一把刀。STM32F373RC 是 ST 家的一款带 16 位 ADC 的 Cortex-M4 微控制器主频 72MHz内置 256KB Flash 和 32KB SRAM外设资源对于工业数据采集来说够用。它有三个 SPI 接口其中 SPI1 挂在 APB2 总线上最高时钟可以跑到 36MHzSPI2 和 SPI3 挂在 APB1 上最高 18MHz。用 SPI1 来驱动 MR25H40CDF 是比较自然的选择带宽足够配置也灵活。把这两个器件凑在一起核心目标就是在工业现场实现高频、可靠、掉电不丢的数据存储与读取。适合谁看呢如果你正在做工业数据记录仪、PLC 扩展模块、智能仪表、或者任何需要频繁写入且不能丢数据的嵌入式项目这套组合值得认真考虑。下面我会从硬件连接、SPI 配置、驱动编写、实测验证几个维度把整个链路拆开讲清楚。2. MR25H40CDF 的引脚定义与 STM32F373RC 的硬件对接2.1 MR25H40CDF 引脚功能拆解MR25H40CDF 用的是 8 引脚 DFN 封装引脚间距 0.5mm焊接的时候需要点技巧手工焊的话建议用热风枪配合助焊膏。引脚定义如下引脚编号名称功能说明1CS片选信号低电平有效2SO数据输出MISO3WP写保护低电平有效4VSS地5SI数据输入MOSI6SCK时钟信号7HOLD保持信号低电平有效8VDD电源正极这里有几个细节值得注意。WP 引脚和 HOLD 引脚都是低电平有效如果你不需要硬件写保护和暂停传输功能直接把它们拉到 VDD 就行。我一般会在 WP 引脚上预留一个跳线或者 GPIO 控制方便在调试阶段灵活切换。HOLD 引脚在标准 SPI 通信中很少用到直接上拉即可。CS 片选信号的处理方式有两种硬件片选和软件片选。硬件片选就是用 STM32 的 SPI 外设自动控制 NSS 引脚软件片选则是用普通 GPIO 手动拉低拉高。在工业应用中我强烈建议用软件片选原因很简单硬件片选在多从机场景下容易出问题而且时序调整不够灵活。软件片选虽然多占一个 GPIO但控制权完全在自己手里调试起来也方便。2.2 STM32F373RC 的 SPI1 引脚分配与注意事项STM32F373RC 的 SPI1 默认引脚是 PA5SCK、PA6MISO、PA7MOSINSS 可以用 PA4 或者任意 GPIO。我这次用 PA4 作为软件片选配置成推挽输出初始状态拉高。接线方案如下STM32 PA5 → MR25H40CDF SCKSTM32 PA6 → MR25H40CDF SOSTM32 PA7 → MR25H40CDF SISTM32 PA4 → MR25H40CDF CSSTM32 3.3V → MR25H40CDF VDD 和 WP、HOLDSTM32 GND → MR25H40CDF VSS注意MR25H40CDF 的供电范围是 2.7V 到 3.6VSTM32F373RC 的 IO 电平也是 3.3V两者可以直接对接不需要电平转换。但如果你用的是 5V 系统必须加电平转换芯片否则会损坏 MRAM。PCB 布局方面SPI 信号线尽量等长SCK 走线要远离模拟信号区域。MRAM 的电源引脚旁边放一个 0.1uF 的去耦电容越近越好。如果通信距离超过 10cm建议在 SCK 和 MOSI 上串一个 22Ω 到 33Ω 的电阻抑制反射。这些细节在实验室里可能看不出差别但到了工业现场电磁环境复杂信号完整性就是稳定性的命根子。2.3 硬件写保护与保持引脚的处理策略WP 引脚控制的是状态寄存器里的块保护位拉低时硬件强制写保护。我在实际项目中的做法是WP 引脚通过一个 10kΩ 电阻上拉到 VDD同时预留一个测试点。这样默认不写保护需要的时候可以用外部信号拉低。HOLD 引脚同样上拉到 VDD不做特殊处理。有些工程师喜欢把 WP 直接接地觉得这样更安全。但问题是一旦接地你就没法通过软件修改状态寄存器来解除保护了调试阶段会非常麻烦。我的经验是调试阶段 WP 上拉量产阶段根据实际需求决定是否接地。如果产品对数据安全性要求极高可以在量产时把 WP 接地彻底杜绝误写。3. STM32F373RC 的 SPI 外设配置与 MRAM 时序匹配3.1 SPI 模式选择Mode 0 还是 Mode 3MR25H40CDF 支持 SPI Mode 0CPOL0CPHA0和 Mode 3CPOL1CPHA1。这两种模式的区别在于时钟极性和相位。Mode 0 下SCK 空闲为低电平数据在第一个边沿采样Mode 3 下SCK 空闲为高电平数据在第二个边沿采样。我一般选Mode 0因为这是最常用的模式STM32 的 HAL 库默认配置也是 Mode 0。配置代码如下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;这里BaudRatePrescaler 设为 4SPI1 挂在 APB2 上时钟 72MHz分频后 SCK 频率为 18MHz。MR25H40CDF 的最高 SCK 频率是 40MHz18MHz 完全在安全范围内。如果你追求更高速率可以设成 2 分频SCK 跑到 36MHz但要注意 PCB 走线和信号质量。3.2 软件片选的控制逻辑与延时要求软件片选的核心就是通信前拉低 CS通信后拉高 CS。但这里有个容易被忽略的细节CS 拉低到第一个 SCK 边沿之间需要一定的建立时间CS 拉高到下一次拉低之间需要保持时间。MR25H40CDF 的数据手册里CS 建立时间tSLCH最小是 5ns保持时间tCHSH最小也是 5ns。在 18MHz 的 SCK 下一个时钟周期约 55ns所以只要代码逻辑正确这些时序自然满足。但如果你用的是 36MHz 甚至更高频率就需要特别注意。我的做法是在 CS 操作后加一个__NOP()或者几个空指令确保时序余量。代码示例如下#define MRAM_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define MRAM_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) void MRAM_Select(void) { MRAM_CS_LOW(); __NOP(); __NOP(); } void MRAM_Deselect(void) { __NOP(); __NOP(); MRAM_CS_HIGH(); }提示不要小看这两个__NOP()在高速通信下它们能有效避免因为 GPIO 翻转速度过快导致的时序违规。我在一个 36MHz 的项目里就是因为少了这两个空指令导致偶发读写错误排查了整整两天。3.3 读写命令的操作码与地址格式MR25H40CDF 的命令集比较简洁常用的几个操作码如下命令名称操作码功能说明WREN0x06写使能WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据读操作时序拉低 CS → 发送 0x03 → 发送 24 位地址MR25H40CDF 是 4Mbit需要 19 位地址但协议上按 24 位发送高位补零→ 读取数据 → 拉高 CS。写操作时序拉低 CS → 发送 0x06WREN→ 拉高 CS → 拉低 CS → 发送 0x02 → 发送 24 位地址 → 发送数据 → 拉高 CS。注意WREN 命令必须单独发送不能和 WRITE 命令连在一起这是很多新手容易犯错的地方。4. 驱动层实现从字节读写到页操作4.1 底层字节读写函数的封装底层函数是整个驱动的基础我一般封装两个函数MRAM_ReadByte和MRAM_WriteByte。读字节函数相对简单写字节函数需要处理 WREN 和状态查询。uint8_t MRAM_ReadByte(uint32_t addr) { uint8_t data; uint8_t cmd[4]; cmd[0] 0x03; cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; MRAM_Select(); HAL_SPI_Transmit(hspi1, cmd, 4, 100); HAL_SPI_Receive(hspi1, data, 1, 100); MRAM_Deselect(); return data; }写字节函数需要先发 WREN再发 WRITEvoid MRAM_WriteByte(uint32_t addr, uint8_t data) { uint8_t cmd[4]; uint8_t wren 0x06; MRAM_Select(); HAL_SPI_Transmit(hspi1, wren, 1, 100); MRAM_Deselect(); cmd[0] 0x02; cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; MRAM_Select(); HAL_SPI_Transmit(hspi1, cmd, 4, 100); HAL_SPI_Transmit(hspi1, data, 1, 100); MRAM_Deselect(); }这里有个关键点WREN 命令发送后CS 必须拉高再拉低才能让 WRITE 命令生效。如果你把 WREN 和 WRITE 放在同一个 CS 周期里写操作会失败。这个坑我在早期项目中踩过当时调了半天以为硬件有问题后来翻数据手册才发现是这个原因。4.2 页写与连续读的效率优化MR25H40CDF 支持页写操作每页 256 字节。如果你要写入大量数据逐字节写效率太低每次都要发 WREN 和地址开销很大。页写模式下你只需要发一次 WREN 和 WRITE 命令然后连续发送最多 256 字节数据地址会自动递增。void MRAM_WritePage(uint32_t addr, uint8_t *buf, uint16_t len) { uint8_t cmd[4]; uint8_t wren 0x06; if (len 256) len 256; MRAM_Select(); HAL_SPI_Transmit(hspi1, wren, 1, 100); MRAM_Deselect(); cmd[0] 0x02; cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; MRAM_Select(); HAL_SPI_Transmit(hspi1, cmd, 4, 100); HAL_SPI_Transmit(hspi1, buf, len, 1000); MRAM_Deselect(); }连续读就更简单了发一次 READ 命令和起始地址然后连续读取任意长度数据地址自动递增没有页边界限制。这个特性在读取大块数据时非常有用。注意页写操作不能跨页。如果你的起始地址是 0x00FF想写 10 个字节那么只有第一个字节会写到 0x00FF剩下的 9 个字节会回卷到 0x0000 开始写。这是 MRAM 和 Flash 共有的特性写驱动时必须处理跨页情况。4.3 状态寄存器读取与写保护解除状态寄存器里有一个 WEL 位Write Enable Latch每次 WREN 后该位置 1写操作完成后自动清零。你可以通过读状态寄存器来确认写操作是否完成。另外还有 BP0 和 BP1 位控制块保护范围。uint8_t MRAM_ReadStatus(void) { uint8_t status; uint8_t cmd 0x05; MRAM_Select(); HAL_SPI_Transmit(hspi1, cmd, 1, 100); HAL_SPI_Receive(hspi1, status, 1, 100); MRAM_Deselect(); return status; }如果你发现写操作一直失败先读一下状态寄存器看看 WEL 位是否置 1BP0 和 BP1 是否被意外设置。我遇到过一种情况上电时 WP 引脚悬空导致状态寄存器被意外修改块保护被激活所有写操作都被拒绝。后来把 WP 上拉后就正常了。5. 实测验证读写速度、掉电保持与寿命测试5.1 读写速度实测数据我在 18MHz SCK 下做了读写速度测试结果如下操作类型数据量耗时等效速率单字节读1 字节约 2.5us400KB/s连续读1024 字节约 460us2.2MB/s单字节写1 字节约 5us200KB/s页写256 字节约 150us1.7MB/s连续读的速率受限于 SPI 时钟和软件开销2.2MB/s 对于工业数据采集来说完全够用。页写的速率比单字节写快了将近 8 倍所以批量写入时一定要用页写。如果你把 SCK 提到 36MHz连续读速率可以接近 4MB/s但要注意信号完整性问题。我在测试板上跑 36MHz 没问题但换到另一块走线较长的板子上就出现了误码后来加了串阻才解决。5.2 掉电保持测试反复断电验证数据完整性MRAM 的非易失性是其核心优势但实际表现如何必须实测。我的测试方法是写入一组递增数据然后随机断电重新上电后读取并校验。测试了 500 次随机断电数据丢失率为零。相比之下我之前用 SRAM 加备份电池的方案在电池电量不足时出现过数据丢失。MRAM 不需要备份电源这一点在工业现场非常关键。提示虽然 MRAM 掉电不丢数据但写入过程中断电可能导致当前字节写入不完整。如果你的应用对数据一致性要求极高建议在写入关键数据时采用双备份校验策略即写两份数据读取时对比校验。5.3 擦写寿命与长期稳定性观察官方标称擦写寿命 10 的 14 次方这个数字太大了没法在实验室里验证完。但我做了一个加速测试以 1kHz 的频率连续写入同一个地址跑了 72 小时累计写入约 2.6 亿次读写功能完全正常。按照这个速率要跑到 10 的 14 次方需要 3000 多年所以寿命问题在实际项目中基本可以忽略。长期稳定性方面我在高温箱里做了 85°C 老化测试连续运行 1000 小时数据保持正常。低温 -40°C 下也做了 500 小时测试没有出现读写失败。这些数据说明 MR25H40CDF 在工业温度范围内是可靠的。6. 工业场景下的实战经验与避坑清单6.1 SPI 通信误码的排查思路SPI 通信出问题排查顺序很重要。我的经验是先看电源再看时序最后看信号完整性。电源方面用示波器测 MRAM 的 VDD 引脚看纹波是否超标。MRAM 对电源噪声比较敏感如果纹波超过 50mV就可能出现误码。解决办法是在 VDD 引脚旁边加一个 1uF 的钽电容和一个 0.1uF 的陶瓷电容。时序方面重点检查 CS、SCK、MOSI 的相位关系。用逻辑分析仪抓波形对照数据手册的时序图看建立时间和保持时间是否满足。我遇到过一次误码原因是 CS 拉高太快最后一个 SCK 边沿还没完成CS 就拉高了导致最后一个 bit 丢失。后来在 CS 拉高前加了一个__NOP()就解决了。信号完整性方面如果走线超过 10cm或者经过连接器建议在 SCK 和 MOSI 上串 22Ω 电阻在 MISO 上串 33Ω 电阻。这些电阻可以抑制反射和过冲提高通信稳定性。6.2 多器件共用 SPI 总线的片选管理工业项目里SPI 总线上往往挂多个器件比如 MRAM、Flash、ADC、显示屏等。这时候片选管理就很重要。我的做法是每个器件独立片选片选信号初始状态全部拉高通信前只拉低目标器件的片选。#define MRAM_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define FLASH_CS_LOW() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET) #define ADC_CS_LOW() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET)注意多器件共用 SPI 时MISO 引脚是共享的。如果某个器件的 MISO 引脚在片选无效时不是高阻态就会拉低总线导致其他器件通信失败。选型时一定要确认器件的 MISO 在 CS 无效时处于高阻态。MR25H40CDF 在这方面没问题但有些便宜的 Flash 芯片就不一定了。6.3 数据分区与磨损均衡的简化策略虽然 MRAM 寿命几乎无限但在某些极端场景下比如高频写入同一个地址还是建议做简单的磨损均衡。我的做法是把存储区分成若干块每次写入时轮换块地址用一个计数器记录当前写入块。#define BLOCK_SIZE 256 #define BLOCK_COUNT 16 uint32_t current_block 0; void MRAM_WriteBalanced(uint8_t *data, uint16_t len) { uint32_t addr current_block * BLOCK_SIZE; MRAM_WritePage(addr, data, len); current_block (current_block 1) % BLOCK_COUNT; }这个策略很简单但能有效分散写入压力。对于大多数工业应用来说MRAM 的寿命已经足够这个策略更多是心理安慰但在一些极端高频写入的场景下还是有点用的。6.4 上电初始化与自检流程设计工业产品上电后我一般会做一个简单的自检流程读器件 ID如果有、读状态寄存器、写测试数据并回读校验。如果自检失败点亮故障指示灯或者记录错误日志。uint8_t MRAM_SelfTest(void) { uint8_t test_data 0xA5; uint8_t read_data; uint32_t test_addr 0x0000; MRAM_WriteByte(test_addr, test_data); read_data MRAM_ReadByte(test_addr); if (read_data ! test_data) { return 1; // 自检失败 } return 0; // 自检通过 }这个自检流程会占用一个存储单元建议放在存储区的最末尾不要和用户数据混在一起。自检通过后再把该单元恢复为默认值。7. 这套方案还能怎么扩展如果你已经跑通了基本的读写可以考虑几个扩展方向。一是增加文件系统比如移植 LittleFS 或者 FatFS把 MRAM 当成一个小型磁盘来用适合存储配置文件、日志等。二是结合 DMA用 SPI DMA 传输减少 CPU 占用特别是在高速连续读写场景下DMA 能把 CPU 解放出来做其他事情。三是双 MRAM 冗余关键数据同时写入两颗 MRAM读取时对比校验进一步提高可靠性。我在一个电力监测项目里就用了双 MRAM 冗余方案两颗 MR25H40CDF 挂同一条 SPI 总线用不同片选控制。正常运行时两颗都写读取时对比如果数据不一致就触发告警。这个方案成本增加不多但可靠性提升明显。最后分享一个小技巧MRAM 的写入速度很快但如果你在中断里写 MRAM要注意 SPI 总线的互斥问题。我一般会在写 MRAM 前关中断写完再开中断避免中断嵌套导致 SPI 通信被打断。这个细节在低优先级中断里可能看不出问题但在高优先级中断频繁触发的场景下就是稳定性的关键。
返回列表