多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

STM32L162ZE 与 MR25H40CDF MRAM 驱动实战:SPI 时序、掉电保护与性能优化

STM32L162ZE 与 MR25H40CDF MRAM 驱动实战:SPI 时序、掉电保护与性能优化 1. 为什么 MRAM 在工业嵌入式场景里越来越受关注搞过工业数据采集或者电力终端的朋友应该都有体会选存储芯片这件事看着简单实际上坑特别多。EEPROM 擦写寿命十万次铁电存储器容量小还贵NOR Flash 写入前必须擦除整个扇区掉电保护做起来让人头大。我前两年做一个配电终端的项目要求每 200ms 记录一次电量数据掉电不能丢设备设计寿命十年以上算下来单颗存储芯片的擦写次数要过亿次当时选型选得头秃。后来接触到MR25H40CDF这颗 MRAM磁阻随机存储器才算找到了一个比较舒服的解法。MRAM 的核心特点很直接写入不需要擦除、写入速度接近 SRAM、擦写寿命几乎无限10^14 次以上、掉电数据不丢。它把 RAM 的写入体验和 Flash 的非易失性揉到了一起。而STM32L162ZE是 ST 家基于 Cortex-M3 的低功耗系列自带硬件 SPI、LCD 控制器、AES 加密在工业仪表、医疗设备、智能表计里用得很多。这两颗芯片凑一起就是一套典型的“低功耗 MCU 高可靠非易失存储”的组合。这篇文章我想聊的不是简单的“怎么读写一个 SPI 芯片”而是把 MR25H40CDF 和 STM32L162ZE 搭配使用时从硬件设计、SPI 时序配置、读写驱动、掉电保护到实际踩坑的完整过程讲清楚。适合正在做工业数据记录、需要频繁写入非易失存储、或者第一次接触 MRAM 的嵌入式工程师参考。哪怕你用的是别的 MCU 或别的 MRAM 型号里面的时序分析和驱动思路也是通用的。2. 项目整体设计与选型思路拆解2.1 为什么是 MRAM 而不是 EEPROM 或 Flash先把选型逻辑讲透不然很多人会觉得“我用 Flash 加个磨损均衡也能做”。我们拿实际数据说话。假设一个工业记录仪每 100ms 写一次 32 字节的数据设备每天工作 24 小时设计寿命 10 年。总写入次数是10 年 × 365 天 × 24 小时 × 3600 秒 × 10 次/秒 3.1536 × 10^9 次也就是大约31.5 亿次写入。这个量级下存储类型擦写寿命能否满足备注EEPROM约 10^6 次否差 3 个数量级NOR Flash约 10^5 次/扇区否需磨损均衡复杂度高FRAM约 10^12 次勉强容量小、单价高MRAMMR25H40CDF约 10^14 次是无需擦除写入即生效MR25H40CDF 是 4Mbit512KB容量的 SPI MRAM工作电压 2.7V~3.6VSPI 时钟最高支持 40MHz。它的写入是真正的“就地写入”不需要像 Flash 那样先发写使能、再擦除扇区、再写入、再等待内部编程完成。这一点对实时性要求高的场景太关键了——Flash 一次扇区擦除动辄几毫秒到几十毫秒期间 CPU 要么阻塞要么得设计复杂的缓冲机制而 MRAM 写入一个字节的时间和写 SRAM 差不多纳秒到微秒级别。2.2 STM32L162ZE 的匹配度分析STM32L162ZE 属于 STM32L1 系列Cortex-M3 内核最高 32MHz 主频512KB Flash、80KB RAM带 3 个 SPI 接口SPI1/SPI2/SPI3。选它配 MR25H40CDF 有几个现实理由第一低功耗特性匹配工业场景。L1 系列有丰富的低功耗模式Stop 模式下电流只有微安级很多工业终端是电池供电或者对功耗敏感这一点很重要。第二硬件 SPI 资源充足。MR25H40CDF 走 SPI 接口用硬件 SPI 比软件模拟 SPI 稳定得多尤其在 20MHz 以上时钟时软件模拟的时序抖动会导致通信失败。L162ZE 的 SPI1 挂在 APB2 上最高可到 16MHz主频 32MHz 时SPI2/SPI3 挂 APB1 最高 8MHz实际用 SPI1 跑 16MHz 完全够用。第三内置 AES 和 LCD 控制器。如果数据需要加密存储或者设备带显示L162ZE 能省掉外置芯片降低 BOM 成本和 PCB 面积。提示MR25H40CDF 的 SPI 模式支持 Mode 0 和 Mode 3STM32 的 SPI 要对应配置 CPOL 和 CPHA这一点后面会详细讲配错了读出来全是 0xFF 或 0x00。2.3 整体架构设计整个数据存储系统的架构可以这样理解STM32L162ZE 作为主控通过 SPI1 与 MR25H40CDF 通信MRAM 内部按地址划分为几个区域——系统配置区、实时数据区、历史记录区、掉电备份区。MCU 内部再配合一个环形缓冲区把高频采集的数据先缓存在 RAM 里按块写入 MRAM进一步降低 SPI 通信开销。这里有个设计取舍值得说要不要每次都直接写 MRAM虽然 MRAM 寿命几乎无限但每次写入都要走 SPI 时序16MHz 下写 32 字节大约需要几十微秒如果采集频率很高比如 1kHz频繁 SPI 事务会占用 CPU 时间。我的做法是在 RAM 里开一个 256 字节的缓冲区攒够一页或者定时 50ms 触发一次批量写入既保证掉电时最多丢 50ms 数据又降低了 SPI 占用率。3. 硬件连接与 SPI 时序关键细节3.1 引脚连接与硬件设计要点MR25H40CDF 是 8 引脚 SOIC 封装引脚定义很标准引脚名称功能连接到 STM321CS片选低有效PA4SPI1_NSS2SO数据输出MISOPA6SPI1_MISO3WP写保护低有效接 VCC 或 GPIO 控制4VSS地GND5SI数据输入MOSIPA7SPI1_MOSI6SCK时钟PA5SPI1_SCK7HOLD保持低有效接 VCC 或 GPIO 控制8VCC电源3.3V硬件设计上有几个容易翻车的点。第一CS 引脚一定要用 GPIO 控制还是用硬件 NSS我强烈建议用普通 GPIO 手动控制 CS。STM32 的硬件 NSS 在多主或者某些时序下会出现 CS 提前拉高的问题导致最后一个字节没传完。手动控制虽然多几行代码但时序完全可控。第二WP 和 HOLD 引脚不要悬空。这两个引脚内部有弱上拉但工业环境干扰大悬空可能被耦合噪声拉低导致芯片进入写保护或保持状态表现为“偶尔写不进去”。我的做法是直接接 3.3V如果确实需要动态写保护再用 GPIO 控制并在 GPIO 上加上拉电阻。第三电源去耦。MRAM 在写入瞬间电流会有波动VCC 引脚旁边必须放 0.1μF 陶瓷电容最好再并一个 1μF。PCB 走线上SPI 的四根线尽量等长、远离高频干扰源SCK 走线不要和模拟信号平行。3.2 SPI 模式与时序配置MR25H40CDF 支持 SPI Mode 0CPOL0, CPHA0和 Mode 3CPOL1, CPHA1。这两种模式的区别在于时钟空闲电平和采样边沿Mode 0SCK 空闲为低数据在 SCK 上升沿采样下降沿输出Mode 3SCK 空闲为高数据在 SCK 上升沿采样下降沿输出STM32 的 SPI 配置里对应的是CLKPolarity和CLKPhase两个参数。用 HAL 库的话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; // Mode 0 hspi1.Init.CLKPhase SPI_PHASE_1EDGE; // Mode 0 hspi1.Init.NSS SPI_NSS_SOFT; // 软件控制 CS hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_2; // 32MHz/2 16MHz hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; // 高位先出 hspi1.Init.TIMode SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE;这里BaudRatePrescaler设成 2是因为 SPI1 挂在 APB2 上主频 32MHz 时 APB2 也是 32MHz分频 2 得到 16MHz。MR25H40CDF 最高支持 40MHz16MHz 留了充足余量信号完整性更好。注意如果你把主频超到 32MHz 以上或者 APB2 分频设置不同实际 SPI 时钟要重新算。SPI 时钟 APB2 时钟 / 预分频系数。算错了要么通信失败要么虽然能通但误码率上升。3.3 读写命令集解析MR25H40CDF 的命令集和标准 SPI Flash 类似但少了擦除相关命令这是它最大的优势。常用命令如下命令编码功能地址字节数WREN0x06写使能0WRDI0x04写禁止0RDSR0x05读状态寄存器0WRSR0x01写状态寄存器0READ0x03读数据3WRITE0x02写数据3RDID0x9F读设备 ID0关键点MRAM 的写入也需要先发 WREN写使能这一点和 Flash 一样。但区别在于MRAM 不需要擦除WREN 之后直接发 WRITE 命令加地址加数据就行写完不需要等待内部编程周期。状态寄存器的 WEL 位在写入完成后会自动清零。读设备 ID 是个很好的自检手段。发 0x9F 后MR25H40CDF 会返回制造商 ID0xE0和产品 ID。上电初始化时读一次能确认 SPI 通信是否正常、芯片是否焊接良好。4. 驱动实现与核心代码拆解4.1 底层 SPI 收发函数HAL 库提供了HAL_SPI_Transmit和HAL_SPI_Receive但在 MRAM 驱动里我更推荐自己封装一个字节收发函数因为很多操作是“发一个字节同时收一个字节”的全双工模式用 HAL 的两个函数分开调用效率低还容易出错。static uint8_t MRAM_SPI_Transfer(uint8_t txData) { uint8_t rxData 0; HAL_SPI_TransmitReceive(hspi1, txData, rxData, 1, 100); return rxData; }CS 控制用宏定义方便移植#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)4.2 写使能与状态等待写操作前必须发 WREN然后可以读状态寄存器确认 WEL 位被置起。虽然 MRAM 写入很快但养成检查状态的习惯没坏处尤其在调试阶段。static void MRAM_WriteEnable(void) { MRAM_CS_LOW(); MRAM_SPI_Transfer(0x06); // WREN MRAM_CS_HIGH(); } static uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS_LOW(); MRAM_SPI_Transfer(0x05); // RDSR status MRAM_SPI_Transfer(0xFF); MRAM_CS_HIGH(); return status; }状态寄存器 bit1 是 WELWrite Enable Latch写入完成后自动清零。调试时如果发现写不进去先读这个位看看是不是 WREN 没生效。4.3 页写与跨页处理MR25H40CDF 内部按 256 字节为一页共 2048 页。写入时如果跨页地址会自动回卷到当前页开头这是很多 Flash 类芯片的通病MRAM 也继承了这个行为。什么意思呢假设你从地址 0x00F0 开始写 32 字节写到 0x0100 时不会进入下一页而是回到 0x0000 继续写。这会导致数据错位。所以驱动里必须做跨页判断和拆分void MRAM_Write(uint32_t addr, uint8_t *buf, uint16_t len) { while (len 0) { uint16_t pageOffset addr % 256; uint16_t pageRemain 256 - pageOffset; uint16_t writeLen (len pageRemain) ? len : pageRemain; MRAM_WriteEnable(); MRAM_CS_LOW(); MRAM_SPI_Transfer(0x02); // WRITE MRAM_SPI_Transfer((addr 16) 0xFF); MRAM_SPI_Transfer((addr 8) 0xFF); MRAM_SPI_Transfer(addr 0xFF); for (uint16_t i 0; i writeLen; i) { MRAM_SPI_Transfer(buf[i]); } MRAM_CS_HIGH(); addr writeLen; buf writeLen; len - writeLen; } }这段代码的核心逻辑就是每次写入不超过当前页剩余空间写满一页后重新发 WREN 和 WRITE 命令进入下一页。虽然多几次 SPI 事务但保证了数据不会错位。4.4 连续读取实现读取比写入简单发 READ 命令加 24 位地址后就可以连续读出数据地址自动递增没有页边界限制。void MRAM_Read(uint32_t addr, uint8_t *buf, uint16_t len) { MRAM_CS_LOW(); MRAM_SPI_Transfer(0x03); // READ MRAM_SPI_Transfer((addr 16) 0xFF); MRAM_SPI_Transfer((addr 8) 0xFF); MRAM_SPI_Transfer(addr 0xFF); for (uint16_t i 0; i len; i) { buf[i] MRAM_SPI_Transfer(0xFF); } MRAM_CS_HIGH(); }4.5 上电自检与设备 ID 读取上电后第一件事应该是读设备 ID确认通信链路正常uint8_t MRAM_CheckID(void) { uint8_t id; MRAM_CS_LOW(); MRAM_SPI_Transfer(0x9F); // RDID id MRAM_SPI_Transfer(0xFF); MRAM_CS_HIGH(); return (id 0xE0) ? 1 : 0; // 制造商 ID 应为 0xE0 }如果返回 0x00 或 0xFF基本可以判断是硬件问题CS 没接对、SPI 模式配错、电源没上、或者芯片虚焊。这一步能省掉大量调试时间。5. 掉电保护与数据完整性设计5.1 为什么 MRAM 的掉电保护更简单用 Flash 做数据记录最怕的就是写到一半掉电。Flash 擦除一个扇区需要几毫秒这期间掉电整个扇区数据可能全变成 0xFF。所以传统方案要么加超级电容撑到写完要么用双备份加标志位做原子操作复杂度很高。MRAM 的写入是逐字节生效的写入一个字节的时间在纳秒级整个 SPI 事务也就几十微秒。掉电时最多丢失正在传输的那几个字节不会出现“整个扇区被擦除”的灾难性后果。这让掉电保护的设计大大简化。5.2 双区备份加校验的实用方案虽然 MRAM 本身很可靠但为了应对极端情况比如电源跌落导致 SPI 时序异常我还是建议做一层软件保护。我的做法是把关键数据存两份A 区和 B 区每份数据后面跟一个 CRC16 校验和和一个写入序号。写入流程先写 A 区校验通过后写 B 区B 区校验通过后更新序号。读取时比较 A、B 两区的序号取序号大的且 CRC 正确的那个。这样即使某一区在写入时掉电损坏另一区仍然可用。typedef struct { uint32_t seq; uint8_t data[60]; uint16_t crc; } Record_t; // 共 66 字节对齐到 68 或 72 更好CRC 用标准 CRC16-CCITT多项式 0x1021初始值 0xFFFF。计算范围是 seq 和 data 字段。5.3 写入序号与磨损无关的思考有人会问MRAM 寿命那么长还需要磨损均衡吗答案是基本不需要。10^14 次擦写就算每秒写 1000 次也要 3000 多年才用完。所以 MRAM 可以固定地址反复写不用像 Flash 那样搞复杂的地址轮转。这又是一个简化设计的好处。不过要注意频繁写同一地址虽然芯片受得了但 SPI 总线的占用和 CPU 开销是实打实的。所以前面提到的 RAM 缓冲加批量写入策略主要是为了优化系统性能而不是保护芯片寿命。6. 常见问题与排查技巧实录6.1 读出来全是 0xFF 或 0x00这是最常见的现象原因通常有三个。第一SPI 模式配错Mode 0 配成了 Mode 3数据采样边沿不对读回来全是无效值。第二CS 时序问题CS 拉低后没有等待足够时间就发时钟或者 CS 在最后一个字节传完前就拉高了。第三MISO 引脚配置错误STM32 端没有把 MISO 配成复用推挽或者上拉输入。排查顺序先用示波器看 SCK、MOSI、CS 三根线的波形确认命令字节 0x9F 正确发出再看 MISO 有没有响应。如果 MISO 一直不动检查芯片供电和焊接。6.2 写入后读出来数据不对大概率是跨页问题。检查你的写入地址和长度如果(addr % 256) len 256就必须拆分。另一个可能是 WREN 没生效读状态寄存器确认 WEL 位。还有一种情况是 WP 引脚被拉低芯片处于写保护状态这时候写入会被静默忽略不报错但数据不变。6.3 高速 SPI 下偶发通信错误16MHz 以上时如果 PCB 走线较长或者没有阻抗匹配SCK 上升沿可能出现振铃导致数据采样错误。解决办法降低 SPI 时钟到 8MHz 试试如果稳定了就是信号完整性问题在 SCK 和 MOSI 上串 22Ω~33Ω 电阻缩短走线长度。6.4 常见问题速查表现象可能原因排查方法解决读 ID 返回 0xFFSPI 模式错/CS 时序错示波器看波形改 CPOL/CPHA检查 CS读 ID 返回 0x00MISO 未配置/芯片未供电测 VCC 和 MISO 电平配置 GPIO检查电源写入无效WP 拉低/WREN 未生效读状态寄存器拉高 WP重发 WREN数据错位跨页写入检查地址和长度拆分写入高速下偶发错误信号完整性降速测试串电阻缩短走线掉电后数据丢失未做备份检查写入流程双区备份加 CRC6.5 几个实操心得第一个心得调试 SPI 一定要有逻辑分析仪。示波器只能看一两根线逻辑分析仪能同时抓 CS、SCK、MOSI、MISO直接解码出 SPI 事务一眼就能看出命令、地址、数据对不对。几百块的入门款就够用省下的调试时间远超这个成本。第二个心得初始化时先慢后快。第一次调通建议 SPI 时钟设成 1MHz 甚至更低确认读写正常后再逐步提高。这样能把时序问题和信号完整性问题分开定位。第三个心得MRAM 的 WP 和 HOLD 引脚在调试阶段可以先用跳线控制方便快速验证是不是这两个引脚导致的问题。量产时再固定接 VCC。第四个心得写驱动时把地址和长度打印出来配合逻辑分析仪抓到的实际 SPI 事务对比能快速发现软件层的地址计算错误。我踩过一次坑地址左移算错了写到了错误的区域覆盖了配置数据查了半天才发现是addr 8写成了addr 16。7. 性能实测与优化建议7.1 实测写入速度在 STM32L162ZE 主频 32MHz、SPI1 时钟 16MHz 的条件下我实测了不同操作的耗时操作数据量耗时单字节写入含 WREN1 字节约 3.5μs页写入不跨页256 字节约 145μs连续读取256 字节约 138μs读设备 ID1 字节约 2.8μs换算下来16MHz SPI 的理论极限是 2MB/s实际因为命令开销和 CS 切换有效吞吐大约 1.7MB/s。对于工业数据记录场景这个速度绰绰有余。7.2 降低 SPI 占用的优化如果系统对 CPU 占用敏感可以用 DMA 来搬运 SPI 数据。STM32L162ZE 的 SPI1 支持 DMA 请求配置好 DMA 通道后写入 256 字节只需要 CPU 发起一次传输剩下的由 DMA 完成CPU 可以去处理其他任务。不过 MRAM 的写入命令和地址还是需要 CPU 先发DMA 只能接管数据段。另一个优化是合并写操作。如果多个变量需要更新尽量在 RAM 里改好一次性写入 MRAM而不是每个变量单独写一次。每次写操作都有 WREN 和命令地址开销合并后能显著降低总线占用。7.3 低功耗场景的处理STM32L162ZE 在 Stop 模式下 SPI 会停止工作如果 MRAM 正在写入时进入低功耗模式会导致写入中断。正确做法是写入完成后再进入低功耗模式或者在进入前确认没有待处理的写操作。MRAM 本身在待机时功耗很低CS 拉高后进入待机电流只有几微安不会成为系统功耗瓶颈。8. 方案扩展与个人体会这套 MRAM 加 STM32 的方案除了工业数据记录还能用在很多场景。比如智能电表的事件记录需要频繁写入且不能丢医疗设备的参数存储要求高可靠车载终端的路况数据缓存要耐振动耐温度变化。MR25H40CDF 的工作温度范围是 -40°C 到 85°C工业级应用完全覆盖。如果容量不够MRAM 厂商有更大容量的型号接口和命令集基本兼容驱动改一下地址位宽就能迁移。如果 MCU 换成别的型号只要 SPI 配置对了驱动层几乎不用动这也是把底层 SPI 收发和上层读写逻辑分开封装的好处。我个人在实际操作中的体会是MRAM 这类器件最大的价值不是“性能多强”而是把复杂问题变简单。以前用 Flash 做掉电保护要设计磨损均衡、双备份、超级电容代码量和调试时间都很大。换成 MRAM 后写入逻辑就是“发 WREN、发 WRITE、发数据”掉电保护只需要一层 CRC 校验整个存储模块的代码量少了将近一半可靠性反而更高。选对器件很多时候比写更多代码更管用。最后分享一个小技巧如果你手头暂时没有 MR25H40CDF可以用任何一颗 SPI Flash 先调通 SPI 底层和驱动框架因为命令集高度相似。等 MRAM 到货后把擦除相关的代码删掉写入流程简化一下就能直接跑起来。这样能提前把硬件和底层调通不耽误项目进度。
返回列表