1. 项目缘起与方案选型思考
1.1 为什么要在工业场景里折腾 MRAM 这颗料
做工业嵌入式这行的朋友应该都有个共识:数据存储这块,选型选得好,后面少掉一半头发。我这次要聊的项目,核心就是用MR25H40CDF这颗 MRAM(磁性随机存储器)芯片,配合STM32F215RE主控,在工业现场做数据的存储和读取。为什么不用更常见的 EEPROM 或者 NOR Flash?这个问题我在立项的时候被问过不下十次,这里先把选型逻辑讲透。
工业现场的数据存储有几个硬性痛点。第一是写入频率高,比如某些设备需要每秒钟记录一次运行状态、故障码、累计运行时长,一天下来就是八万多次写操作。EEPROM 的擦写寿命通常在 100 万次量级,按这个频率算,十几天就把寿命耗光了。第二是掉电不能丢数据,工业设备经常遇到突然断电的情况,如果数据还在缓存里没落盘,那这条记录就没了。第三是写入速度要快,有些场景要求在断电后的几毫秒内把关键数据保存下来,靠大电容撑住的那点时间窗口,必须完成写入。
MR25H40CDF 这颗芯片恰好把这几个痛点都解决了。它是4Mbit(512KB)容量的 SPI 接口 MRAM,最大的特点就是写入不需要擦除、写入速度接近 SRAM、擦写寿命理论上无限。我实测下来,单字节写入的时序开销比 EEPROM 小一个数量级,而且没有擦除周期这个环节,写进去就是写进去了。对于工业场景里那种"频繁写、小数据量、要求掉电可靠"的需求,这颗料几乎是量身定做的。
STM32F215RE 这边,它是 ST 家的Cortex-M3 内核,主频 120MHz,带 1MB Flash 和 128KB SRAM,外设资源丰富,SPI 接口有好几个。选它主要是因为项目里还要跑一些实时控制逻辑,M3 的性能和中断响应能力足够,而且 STM32 的 HAL 库生态成熟,SPI 驱动写起来不费劲。这套组合在工业仪表、数据采集终端、设备黑匣子这类产品里非常典型。
1.2 SPI 接口选型的几个关键考量
MR25H40CDF 支持SPI 和 Quad SPI两种模式,我这次用的是标准 SPI 模式,四根线:SCLK、MOSI、MISO、CS。为什么不上 Quad?因为工业场景里数据量其实不大,单次写入也就几十个字节,标准 SPI 在 40MHz 时钟下,传输 64 字节也就十几微秒,完全够用。上 Quad 反而要占用更多 IO,PCB 布线也更麻烦,性价比不划算。
这里有个细节值得展开说:硬件片选还是软件片选。STM32 的 SPI 外设支持硬件 NSS 管理,但我强烈建议用软件片选,也就是自己拿一个 GPIO 来控制 CS 引脚。原因很简单,硬件 NSS 在多从机场景下容易出问题,而且时序控制不够灵活。MRAM 的 CS 建立时间和保持时间有明确要求,软件片选可以精确控制拉低和拉高的时机,配合 SPI 传输完成标志,能保证时序余量充足。我踩过的坑是:早期用硬件 NSS,在高速时钟下偶尔出现数据错位,换成软件片选后问题消失。
另外,SPI 的时钟极性(CPOL)和时钟相位(CPHA)必须和 MRAM 的数据手册对齐。MR25H40CDF 支持Mode 0(CPOL=0, CPHA=0)和 Mode 3(CPOL=1, CPHA=1),我用的是 Mode 0,也就是空闲时 SCLK 为低,数据在上升沿采样。这个配置在 STM32CubeMX 里直接选就行,但要注意:如果总线上还挂了别的 SPI 从机,得确认所有从机都支持同一个模式,否则就要分时复用或者用不同的 SPI 外设。
1.3 整体架构设计思路
整个系统的数据流是这样的:STM32F215RE 作为主控,通过 SPI 总线读写 MR25H40CDF。MRAM 里划分了几个区域:系统配置区、运行日志区、故障记录区、参数备份区。配置区存设备 ID、校准参数这些不常变的数据;日志区按环形缓冲区的方式循环写入,写满就覆盖最旧的数据;故障记录区一旦写入就不覆盖,直到人工清除;参数备份区存的是关键运行参数的快照,掉电前更新。
这个分区设计的核心思路是把不同写入频率和不同可靠性要求的数据分开管理。日志区写入最频繁,用环形缓冲避免频繁擦除(虽然 MRAM 不需要擦除,但环形缓冲能控制写入地址的均匀分布);故障记录区要求最高,写入前先校验,写入后回读确认;配置区基本不动,只在出厂标定时写一次。
软件层面,我封装了一个MRAM 驱动层,向上提供MRAM_Read()、MRAM_Write()、MRAM_Erase()(虽然 MRAM 不需要擦除,但为了接口统一保留了这个函数,内部实现为空操作或者写 0xFF)这几个接口。再往上是一个存储管理层,负责地址映射、数据校验(CRC16)、掉电保护逻辑。最上层是应用层,直接调用存储管理层的接口,不用关心底层是 MRAM 还是别的什么存储介质。这种分层设计的好处是,以后如果换存储芯片,只需要改驱动层,上层代码基本不动。
2. 硬件设计与核心细节解析
2.1 电路连接与 PCB 布局要点
先看硬件连接。MR25H40CDF 是 8 引脚封装,关键引脚就那几个:VCC、GND、SCLK、MOSI、MISO、CS。STM32F215RE 这边,我用的 SPI1,对应引脚是 PA5(SCLK)、PA6(MISO)、PA7(MOSI),CS 用 PA4 做软件片选。接线本身不复杂,但有几个细节不注意就会翻车。
电源去耦这块,MRAM 的 VCC 引脚旁边必须放一个0.1uF 的陶瓷电容,越近越好,最好在 2mm 以内。我试过把电容放在离芯片 1cm 远的地方,高速读写时偶尔出现数据错误,挪近之后问题消失。如果 PCB 空间允许,再并一个 1uF 的电容,对低频噪声抑制更好。工业现场电磁环境复杂,电源上的毛刺很容易导致 MRAM 内部状态机异常,去耦电容是最后一道防线。
SPI 信号线的走线要注意等长和阻抗。SCLK 是时钟线,最容易受干扰,尽量走短线,避免跨分割平面。MOSI 和 MISO 如果走线较长,可以考虑串联 22 欧姆的电阻做阻抗匹配,减少反射。CS 线虽然速率不高,但也不能忽视,因为它控制着 MRAM 的片选状态,如果 CS 上有毛刺,可能导致 MRAM 误响应。我在 CS 线上加了一个 10K 的上拉电阻,确保在 STM32 复位期间 CS 保持高电平,MRAM 不会误动作。
接地方面,MRAM 的 GND 引脚要就近打过孔连接到地平面,不要走长线。如果板子上有多个地平面,确保 MRAM 所在的地平面和 STM32 的地平面是同一个,或者通过 0 欧姆电阻单点连接。工业设备里地环路问题很常见,处理不好会导致通信不稳定。
2.2 SPI 时序参数计算与配置
MR25H40CDF 的最高 SPI 时钟频率是40MHz,但实际能用多快,取决于 STM32 的 SPI 外设配置和 PCB 信号质量。STM32F215RE 的 SPI1 挂在 APB2 总线上,最高时钟 60MHz(如果系统时钟 120MHz,APB2 分频系数为 2)。SPI 的波特率预分频器可以设置成 2、4、8、16、32、64、128、256,对应时钟就是 30MHz、15MHz、7.5MHz 等等。
我最终选的是15MHz(预分频系数 4)。为什么不选 30MHz?因为实测下来,30MHz 时信号过冲比较明显,眼图裕量不足,长时间运行偶发误码。15MHz 虽然慢一半,但稳定性大幅提升,而且对于工业数据存储来说,15MHz 下传输 64 字节只需要约 34 微秒,完全满足实时性要求。这里的原则是:在满足速度要求的前提下,时钟越低越稳。
时序参数方面,MR25H40CDF 的数据手册给出了几个关键时间:CS 建立时间(tCSS)最小 5ns,CS 保持时间(tCSH)最小 5ns,数据输出有效时间(tDV)最大 8ns。STM32 的 SPI 在 15MHz 下,时钟周期约 66.7ns,这些时序要求都能轻松满足。但要注意,CS 拉低到第一个 SCLK 上升沿之间要有足够的延迟,我一般留 100ns 以上,确保 MRAM 内部状态机准备好。同样,最后一个 SCLK 下降沿到 CS 拉高之间也要留延迟,我留 50ns 以上。
代码层面,SPI 初始化的时候要设置好这些参数:
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;注意NSS设成SPI_NSS_SOFT,这样硬件 NSS 引脚就释放出来做普通 GPIO 用了。FirstBit设成MSB,因为 MRAM 的指令和地址都是高位在前。
2.3 MRAM 内部地址映射与指令集
MR25H40CDF 的 512KB 地址空间是线性映射的,地址从 0x00000 到 0x7FFFF。读写指令很简单:读指令 0x03,后面跟 3 字节地址,然后就是数据输出;写指令 0x02,后面跟 3 字节地址,然后就是数据输入。另外还有写使能指令 0x06和读状态寄存器指令 0x05。
这里有个关键点:MRAM 的写操作不需要先擦除,但每次写之前要发写使能指令。写使能是一个独立的 SPI 事务:拉低 CS,发 0x06,拉高 CS。然后再拉低 CS,发 0x02 + 地址 + 数据,拉高 CS。写使能标志在 CS 拉高后会自动清除,所以每次写操作都要重新发。这个流程和 EEPROM 类似,但 EEPROM 多了擦除步骤,MRAM 省掉了。
状态寄存器里有一个WEL(Write Enable Latch)位,用来指示写使能是否生效。我一般会在写操作后读一下状态寄存器,确认 WEL 被正确清除,间接验证写操作被 MRAM 接受了。另外还有一个BP(Block Protect)位,可以硬件保护部分地址区域不被写入。工业场景里,如果有些数据是出厂标定后就不允许改的,可以用 BP 位做硬件写保护,比软件保护更可靠。
地址映射方面,我的分区是这样的:
| 区域名称 | 起始地址 | 结束地址 | 大小 | 用途 |
|---|---|---|---|---|
| 系统配置区 | 0x00000 | 0x00FFF | 4KB | 设备ID、校准参数 |
| 运行日志区 | 0x01000 | 0x3FFFF | 252KB | 环形缓冲区 |
| 故障记录区 | 0x40000 | 0x4FFFF | 64KB | 故障码、时间戳 |
| 参数备份区 | 0x50000 | 0x7FFFF | 192KB | 关键参数快照 |
这个分区不是随便定的。配置区放最前面,因为出厂标定时只需要写这一小块;日志区最大,因为写入最频繁;故障记录区独立出来,方便快速定位;参数备份区留足空间,因为有些参数需要保存多个历史版本。
3. 软件实现与核心环节实操
3.1 底层驱动封装:从 GPIO 到 SPI 事务
底层驱动是整个项目的地基,写得好后面省事,写得烂后面天天救火。我的做法是把所有和 MRAM 相关的操作封装成几个原子函数,上层只调这些函数,不直接碰 SPI 外设。
首先是CS 控制函数:
static inline void MRAM_CS_Low(void) { HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_RESET); } static inline void MRAM_CS_High(void) { HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_SET); }这两个函数看起来简单,但有个细节:CS 拉低和拉高之间要有足够的延时。我在MRAM_CS_Low()后面加了一个__NOP()循环,大概几十纳秒,确保 MRAM 检测到片选有效。同样,MRAM_CS_High()之前也要确保最后一个时钟沿已经完成。
然后是SPI 收发函数。STM32 的 HAL 库提供了HAL_SPI_Transmit()和HAL_SPI_Receive(),但在 MRAM 这种需要连续收发(发指令的同时收数据)的场景下,用HAL_SPI_TransmitReceive()更合适。不过 HAL 库的函数有超时机制,在工业现场如果 SPI 总线被干扰,可能会卡在超时等待里。我的做法是自己写一个轻量级的 SPI 收发函数,直接操作寄存器,不依赖 HAL 的超时逻辑:
static uint8_t SPI_TransferByte(uint8_t txData) { while (!(SPI1->SR & SPI_SR_TXE)); *(__IO uint8_t *)&SPI1->DR = txData; while (!(SPI1->SR & SPI_SR_RXNE)); return *(__IO uint8_t *)&SPI1->DR; }这个函数的好处是没有超时,不会卡死,但前提是 SPI 外设已经正确初始化。如果 SPI 时钟没使能或者引脚配置错了,这个函数会死循环。所以我在初始化阶段会先做一次自检,确认 SPI 能正常收发。
基于这个字节收发函数,就可以封装 MRAM 的读写操作了。读操作的流程是:拉低 CS -> 发 0x03 -> 发 3 字节地址 -> 连续读 N 字节 -> 拉高 CS。写操作的流程是:拉低 CS -> 发 0x06 -> 拉高 CS -> 拉低 CS -> 发 0x02 -> 发 3 字节地址 -> 连续写 N 字节 -> 拉高 CS。
这里有个性能优化点:如果连续写多个字节,不需要每个字节都发一次写使能。写使能在一次 CS 有效期内只需要发一次,然后可以连续写多个字节。我实测下来,连续写 64 字节比逐字节写快将近一倍。
3.2 数据校验与掉电保护机制
工业场景里,数据写入后必须能确认写对了,否则存进去的是垃圾还不如不存。我的做法是每条记录后面跟一个 CRC16 校验值。写入的时候计算 CRC,读出的时候重新计算并比对,不一致就认为数据损坏。
CRC16 的实现我用的是查表法,速度快,占用空间小。多项式选的是CRC-16/CCITT-FALSE(0x1021),初始值 0xFFFF。这个多项式在工业通信里很常见,兼容性好。查表法需要一张 256 项的 uint16_t 表,占 512 字节 Flash,对于 STM32F215RE 的 1MB Flash 来说九牛一毛。
static const uint16_t crc16_table[256] = { ... }; uint16_t CRC16_Calculate(const uint8_t *data, uint32_t len) { uint16_t crc = 0xFFFF; while (len--) { crc = (crc << 8) ^ crc16_table[((crc >> 8) ^ *data++) & 0xFF]; } return crc; }掉电保护是另一个核心机制。工业设备断电是常态,不能假设每次写入都能完整完成。我的策略是双备份 + 写入标志。每条关键数据存两份,分别放在两个不同的地址区域。写入的时候先写第一份,再写第二份,最后写一个"写入完成"标志。读取的时候先检查标志,如果标志有效,读第一份并校验;如果第一份校验失败,读第二份;如果两份都失败,返回错误。
这个机制的关键在于写入顺序。必须先写数据,再写标志。如果先写标志再写数据,断电后标志有效但数据不完整,就会读到脏数据。另外,标志本身也要有校验,我一般用取反双写的方式:标志地址写 0xAA,紧接着写 0x55,读取时两个都对了才认为标志有效。
还有一个细节:MRAM 的写入是原子性的吗?严格来说,MRAM 的单字节写入是原子的,但多字节写入不是。如果写到一半断电,已经写进去的字节会保留,没写的字节保持原值。所以对于多字节数据,必须用上面的双备份机制来保证一致性。
3.3 环形日志缓冲区的实现细节
运行日志区我用的是环形缓冲区,这是工业设备里最常用的日志存储方式。核心思路是:写指针一直往前跑,写到末尾就回到开头,覆盖最旧的数据。读的时候从最旧的数据开始读,一直读到写指针的位置。
环形缓冲区的关键是读写指针的管理。我用两个 32 位变量分别记录写指针和读指针,存在 MRAM 的固定位置。每次写入新日志前,先更新写指针,再写数据。这里有个顺序问题:先更新指针还是先写数据?我的做法是先写数据,再更新指针。因为如果先更新指针再写数据,断电后指针指向的位置可能是无效数据;而先写数据再更新指针,断电后指针还指向旧位置,最多丢一条新日志,不会读到脏数据。
日志记录的结构是这样的:
typedef struct { uint32_t timestamp; // 时间戳,秒 uint16_t event_code; // 事件码 uint8_t data[8]; // 附加数据 uint16_t crc; // CRC16校验 } LogEntry_t;每条日志 16 字节,252KB 的日志区可以存约 16000 条。按每秒一条算,可以存四个多小时。如果设备运行时间长,可以适当降低记录频率,或者扩大日志区。
读取日志的时候,从读指针开始,逐条读取并校验 CRC。如果遇到 CRC 错误,说明这条日志损坏(可能是写入时断电),跳过继续读下一条。如果连续多条 CRC 错误,说明读指针可能失效了,需要做一次全区域扫描,找到最后一条有效日志的位置,重建读指针。
这里有个实操心得:环形缓冲区的读写指针最好双备份存储,并且每次更新指针时都做一次 CRC 校验。我早期版本没做指针备份,有一次设备断电后指针区域数据损坏,导致整个日志区无法读取,只能格式化,丢掉了所有历史日志。后来加了指针备份和校验,再没出过这个问题。
4. 常见问题排查与实战避坑指南
4.1 SPI 通信失败排查流程
SPI 通信失败是调试阶段最常见的问题,表现是读出来的数据全是 0xFF 或者 0x00,或者数据随机跳变。我总结了一套排查流程,按顺序走一遍,基本能定位到问题。
第一步:检查硬件连接。用万用表量一下 MRAM 的 VCC 是不是 3.3V,GND 是不是通的。然后量 SCLK、MOSI、MISO、CS 四根线,确认没有虚焊、短路。我遇到过好几次是 CS 线虚焊,时通时不通,折腾了半天。
第二步:用示波器看波形。重点看 SCLK 有没有输出,频率对不对;CS 的拉低和拉高时机对不对;MOSI 上有没有数据。如果 SCLK 没有波形,说明 SPI 外设没初始化好,或者引脚配置错了。如果 CS 一直是低电平,说明 GPIO 初始化有问题。
第三步:读 MRAM 的 ID。MR25H40CDF 有一个RDID 指令(0x9F),可以读出制造商 ID 和设备 ID。如果读 ID 都失败,说明通信链路有问题;如果读 ID 成功但读写数据失败,说明指令或地址有问题。读 ID 是最基础的通信测试,一定要先过这一关。
第四步:降低 SPI 时钟。如果高速下通信失败,把时钟降到 1MHz 试试。如果低速能通高速不通,说明是信号完整性问题,需要检查 PCB 走线、去耦电容、阻抗匹配。
第五步:检查时序参数。用示波器测量 CS 拉低到第一个 SCLK 上升沿的时间,确认大于 5ns;测量最后一个 SCLK 下降沿到 CS 拉高的时间,确认大于 5ns。如果不够,在代码里加延时。
下面这张表是我整理的问题速查表:
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 读数据全 0xFF | MISO 未连接或 MRAM 未响应 | 检查 MISO 连线,读 ID | 修复连线,确认 CS 有效 |
| 读数据全 0x00 | MISO 被拉低或 MRAM 未上电 | 检查 MISO 电平,量 VCC | 修复短路,确认供电 |
| 数据随机跳变 | SPI 时钟太快或信号干扰 | 降低时钟,看波形 | 降频,加去耦电容 |
| 写入后读回不一致 | 写使能未生效或时序不足 | 读状态寄存器 WEL 位 | 确保写使能,加延时 |
| 偶发通信失败 | 电源毛刺或地环路 | 看电源纹波,检查接地 | 加滤波电容,单点接地 |
4.2 写入寿命与数据保持的实测数据
MRAM 标称擦写寿命是无限次,但实际能用多久,我做了个长期测试。测试方法是让 STM32 以 1kHz 的频率连续写入同一个地址,连续跑了一个月,累计写入约 26 亿次。测试结束后,读写功能正常,数据保持正常。这个结果说明 MRAM 的寿命确实远超 EEPROM 和 Flash,对于工业场景里那种高频写入的需求,完全可以放心用。
数据保持方面,MRAM 是非易失性的,断电后数据可以保持20 年以上(室温)。我做了个加速测试:把写入了数据的 MRAM 放在 85 度恒温箱里烤了 1000 小时,相当于室温下 10 年以上的老化,读出来的数据依然正确。这个结果让我对 MRAM 在工业宽温环境下的可靠性有了信心。
不过有个注意事项:MRAM 对强磁场敏感。虽然 MR25H40CDF 内部有磁屏蔽,但如果放在强磁场附近(比如大功率电机、电磁铁旁边),数据可能会受影响。我在一个电机控制项目里遇到过这个问题,MRAM 离电机太近,偶尔出现数据位翻转。后来把 MRAM 挪远了一点,并加了磁屏蔽罩,问题解决。所以 PCB 布局时,MRAM 要远离强磁源。
4.3 工业现场 EMC 问题的实战处理
工业现场的电磁兼容性问题比实验室里复杂得多。我在一个变频器旁边跑这套存储方案时,遇到了几个典型问题,这里分享一下处理经验。
第一个问题是 SPI 通信偶发失败。现象是设备运行几个小时后,突然读不到 MRAM 数据,重启后恢复正常。用示波器抓波形,发现 SPI 时钟上有高频噪声叠加。原因是变频器的开关噪声通过空间耦合到了 SPI 线上。解决方案是在 SCLK 和 MOSI 上串联33 欧姆的磁珠,并在 MRAM 的 VCC 上增加10uF 的钽电容。处理后连续运行一周没再出现通信失败。
第二个问题是数据位翻转。现象是存储的参数偶尔变成错误值,但 CRC 校验能发现。排查后发现是 MRAM 受到了静电放电(ESD)的影响。工业现场操作人员走动、设备启停都会产生 ESD。解决方案是在 MRAM 的 SPI 线上加TVS 二极管,并在 PCB 边缘增加接地铜箔。TVS 选的是SM712,专门用于差分信号线的 ESD 保护。
第三个问题是地环路干扰。现象是设备接入不同的电源插座时,通信稳定性不一样。原因是不同插座的地电位有差异,形成了地环路电流。解决方案是单点接地,把 MRAM 的 GND 和 STM32 的 GND 通过一个 0 欧姆电阻连接,而不是直接铺铜连接。这样地环路电流不会流过信号参考地,通信稳定性大幅提升。
这些 EMC 处理经验,教科书上不会写,都是现场踩坑踩出来的。工业嵌入式开发和消费电子最大的区别就在这里:实验室里跑通不算完,现场跑稳才算数。
4.4 代码移植与不同 STM32 系列的适配
这套驱动代码最初是在 STM32F215RE 上写的,后来项目需要,移植到了 STM32F103 和 STM32H743 上。移植过程中发现几个需要注意的点。
SPI 外设的差异。F2 和 F1 的 SPI 寄存器基本兼容,但 H7 的 SPI 外设变化较大,增加了 FIFO 和更复杂的时钟配置。移植到 H7 时,SPI_TransferByte()函数需要修改,因为 H7 的 SPI 有 16 字节 FIFO,不能简单用TXE和RXNE标志来判断。我的做法是在 H7 上用 HAL 库的HAL_SPI_TransmitReceive(),虽然效率略低,但兼容性好。
GPIO 速度配置。F2 的 GPIO 速度等级和 H7 不同,H7 的 GPIO 可以配置到 Very High Speed。在 H7 上,SPI 时钟可以跑到 50MHz 以上,但 MRAM 最高只支持 40MHz,所以还是要限制在 40MHz 以内。另外,H7 的 GPIO 输出驱动能力更强,如果走线较长,可能需要串联电阻来抑制过冲。
中断优先级。如果 SPI 传输放在中断里,不同系列的 NVIC 优先级分组可能不同。F2 默认是 4 位优先级,H7 支持更灵活的分组。移植时要确认优先级配置不会导致 SPI 传输被高优先级中断打断,否则可能出现时序问题。
HAL 库版本。不同系列的 HAL 库版本不同,函数名和参数可能有细微差异。比如HAL_SPI_Init()在 F2 和 H7 上的参数结构体就不完全一样。移植时最好用 CubeMX 重新生成初始化代码,然后把应用层代码移植过去,不要直接复制初始化代码。
这套代码后来还移植到了 GD32 和 APM32 等国产 MCU 上,基本逻辑一样,只是寄存器地址和 HAL 库不同。国产 MCU 的 SPI 外设和 STM32 高度兼容,移植工作量不大。这也说明,把驱动层和硬件层解耦的设计思路是对的,换平台只需要改底层,上层业务逻辑不用动。
5. 性能优化与进阶玩法
5.1 批量写入与 DMA 传输
标准 SPI 传输是 CPU 轮询的方式,每个字节都要 CPU 参与,效率不高。如果数据量大,比如要一次性写入几 KB 的参数备份,轮询方式会占用大量 CPU 时间。这时候可以用DMA来加速。
STM32F215RE 的 SPI1 支持 DMA 请求,可以配置 DMA 通道来自动搬运数据。配置好之后,CPU 只需要启动 DMA,然后就可以去处理其他任务,等 DMA 传输完成中断再回来处理。我实测下来,用 DMA 传输 1KB 数据,CPU 占用时间从约 500 微秒降到几乎为零,效果非常明显。
不过 DMA 也有坑。DMA 传输期间不能修改 SPI 配置,否则会导致传输错误。另外,DMA 传输完成后要及时关闭 DMA 通道,否则下次传输可能出错。还有,DMA 的传输完成中断优先级要设置好,不能太高也不能太低,太高会影响其他中断,太低会导致响应延迟。
对于 MRAM 的写入,DMA 特别适合参数备份区的大块数据写入。日志区因为每次只写十几字节,用 DMA 反而增加开销,还是轮询更合适。所以我的做法是:小数据用轮询,大数据用 DMA,根据数据量动态选择。
5.2 双缓冲机制提升读写效率
在某些高频读写场景下,比如每毫秒要记录一次数据,如果每次都直接写 MRAM,SPI 传输的开销会累积。我的优化方案是双缓冲:在 STM32 的 SRAM 里开辟两个缓冲区,数据先写到缓冲区 A,写满后切换到缓冲区 B,同时把缓冲区 A 的数据批量写入 MRAM。这样 SPI 传输的次数大幅减少,效率提升明显。
双缓冲的关键是缓冲区大小的选择。太小了切换频繁,太大了占用 SRAM 多。我一般选256 字节,因为 MRAM 的页大小是 256 字节(虽然 MRAM 没有页的概念,但 256 字节是一个合理的批量单位)。256 字节的缓冲区,两个就是 512 字节,对于 STM32F215RE 的 128KB SRAM 来说微不足道。
切换逻辑用状态机实现:缓冲区 A 填充中 -> 缓冲区 A 写 MRAM -> 缓冲区 B 填充中 -> 缓冲区 B 写 MRAM -> 回到缓冲区 A。写 MRAM 的操作放在低优先级任务里执行,不阻塞高优先级的采集任务。这样既保证了数据不丢,又不影响实时性。
5.3 数据压缩与存储空间优化
512KB 的 MRAM 看起来不小,但如果日志记录频繁,很快也会写满。我的优化方案是数据压缩。日志记录里的时间戳、事件码、附加数据,很多都有规律,可以用简单的压缩算法减少存储空间。
比如时间戳,如果日志是等间隔记录的,可以只存起始时间戳 + 间隔,而不是每条都存完整时间戳。事件码如果种类不多,可以用位图表示,一个字节存 8 个事件的状态。附加数据如果变化不大,可以只存变化量而不是完整值。
我实现了一个简单的RLE(游程编码)压缩,对于连续相同的字节,用"计数值 + 字节值"表示。实测下来,对于典型的工业日志数据,压缩率能达到50% 到 70%,相当于存储空间翻倍。解压的时候按相反逻辑展开,速度很快,对 CPU 负担很小。
当然,压缩也有代价:读取时需要解压,增加了 CPU 开销。所以我的策略是:日志区用压缩存储,配置区和参数备份区不压缩。配置区和参数备份区数据量小,压缩意义不大,而且这些数据要求快速读取,解压反而添乱。
5.4 基于 MRAM 的掉电数据保护完整方案
最后聊一个进阶玩法:完整的掉电数据保护方案。工业设备里,掉电保护不只是存数据那么简单,还要保证数据的一致性和可恢复性。
我的方案是三级保护。第一级是SRAM 缓存,数据先写到 SRAM,这是最快的;第二级是MRAM 持久化,定期或在掉电信号触发时把 SRAM 数据写入 MRAM;第三级是备份电池,如果掉电时间较长,用备份电池维持 SRAM 供电,等电源恢复后再写入 MRAM。
掉电检测用PVD(可编程电压检测器),STM32 内部有这个功能,可以设置一个电压阈值,当 VCC 降到阈值以下时触发中断。在中断里,立即启动 MRAM 写入,把关键数据保存下来。PVD 的阈值要设置得比 MRAM 的最低工作电压高,确保在 MRAM 失效之前完成写入。
这里有个时间计算:STM32F215RE 在 120MHz 下,写入 256 字节到 MRAM 大约需要 200 微秒(15MHz SPI,每字节约 0.67 微秒,加上指令和地址开销)。电源从正常电压降到 MRAM 最低工作电压(2.7V)的时间,取决于电源的滤波电容大小。如果电容是 100uF,负载电流 50mA,那么放电时间约2 毫秒(从 3.3V 降到 2.7V)。2 毫秒足够完成 256 字节的写入,还有余量。但如果电容只有 10uF,放电时间只有 200 微秒,就非常紧张了。所以掉电保护电容要选足够大,我一般用 220uF 以上。
这个方案我在多个工业项目里用过,实测掉电后数据零丢失,恢复后系统能正确读取掉电前的状态。对于工业设备来说,这个可靠性是必须的,也是 MRAM 相比其他存储介质的核心优势所在。