1. 项目缘起与方案选型
1.1 为什么要在工业场景里折腾 MRAM
做工业嵌入式这行十来年,最头疼的往往不是算法跑不动,而是数据存不住。EEPROM 擦写次数撑不住高频采集,Flash 写入前要擦除、掉电还容易丢数据,铁电存储器容量又小得可怜。前阵子接了个工业数据记录仪的项目,要求每秒采样一次,连续记录十年以上,还要保证任意时刻断电数据不丢。算了一下,十年就是三亿多次写入,传统方案基本全军覆没。
这时候 MRAM 进入了视野。MRAM 全称磁阻随机存储器,靠磁隧道结的电阻状态存数据,写入过程没有电荷隧穿,所以擦写寿命几乎无限,官方标称 10^14 次以上,比 EEPROM 高了六个数量级。更关键的是它写入速度接近 SRAM,掉电后数据能保持二十年以上,工作温度覆盖 -40 到 125 摄氏度,工业级场景简直是为它量身定做的。
这次选用的 MR25H40CDF 是 Everspin 家的 4Mbit 串行 MRAM,SPI 接口,SOIC-8 封装,引脚和常见的 SPI Flash 完全兼容。这意味着硬件上可以直接替换,软件上把驱动改一改就能跑。主控用的是 STM32F446RE,180MHz 的 Cortex-M4,带 FPU,SPI 外设资源丰富,做数据采集和存储的调度绰绰有余。
1.2 硬件连接与引脚规划
MR25H40CDF 的引脚定义很标准:VCC、GND、SCK、SI、SO、CS,外加两个写保护引脚。实际接线时我用了 STM32F446RE 的 SPI1,具体分配如下:
| MR25H40CDF 引脚 | STM32F446RE 引脚 | 功能说明 |
|---|---|---|
| VCC | 3.3V | 供电,必须加 0.1uF 去耦电容 |
| GND | GND | 共地 |
| SCK | PA5 | SPI1 时钟 |
| SI | PA7 | MOSI,主出从入 |
| SO | PA6 | MISO,主入从出 |
| CS | PA4 | 硬件片选,也可以用普通 GPIO |
| WP | 3.3V | 写保护,拉高禁用 |
| HOLD | 3.3V | 保持,拉高禁用 |
这里有个细节值得说:CS 引脚我最终选择了软件片选,用普通 GPIO 控制。原因后面会详细讲,简单说就是硬件 NSS 在多从机场景下容易出幺蛾子,而且调试时不好抓波形。
1.3 SPI 模式与时钟选择
MR25H40CDF 支持 SPI 模式 0 和模式 3,也就是 CPOL=0/CPHA=0 或者 CPOL=1/CPHA=1。我选了模式 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 的时候,有几个参数必须注意:
- Mode:Full-Duplex Master
- Data Size:8 Bits
- CPOL:Low
- CPHA:1 Edge
- NSS:Software
- Prescaler:4 分频,得到 22.5MHz
- First Bit:MSB First
GPIO 这边,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 通信失败排查表
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 读回全 0xFF | MISO 没接好或 CS 没拉低 | 示波器看 CS 和 SCK 波形 | 检查接线,确认 CS 在传输期间为低 |
| 读回全 0x00 | 芯片没供电或 SPI 模式错 | 万用表量 VCC,示波器看 SCK 空闲电平 | 确认 3.3V 供电,CPOL 设置正确 |
| 数据偶尔错位 | 时钟太快或走线太长 | 降低 SPI 时钟到 10MHz 测试 | 加串阻或降低频率 |
| 写入不生效 | 没发 WREN 或 WP 没拉高 | 读状态寄存器看 WEL 位 | 写前发 WREN,WP 接 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.6MHz | 1.43us | 366us | 1.46s |
| 11.25MHz | 0.71us | 182us | 0.73s |
| 22.5MHz | 0.36us | 91us | 0.36s |
| 45MHz | 0.18us | 46us | 0.18s |
45MHz 下虽然快,但误码率上来了,连续写 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, (addr>>16)&0xFF, (addr>>8)&0xFF, addr&0xFF}; 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 了。
后续如果容量不够,可以换 MR25H256,32MBit 的版本,引脚完全兼容,驱动改个地址范围就行。再往上还有并口 MRAM,但那就需要重新设计 PCB 了。对于大多数工业数据记录场景,SPI MRAM 加 STM32 这套组合,未来几年应该都是很能打的一个方案。