尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32 SPI接口驱动MRAM:MR25H40CDF非易失性存储方案详解

STM32 SPI接口驱动MRAM:MR25H40CDF非易失性存储方案详解 1. 项目概述与需求拆解1.1 这个项目到底在解决什么问题做工业嵌入式开发的人应该都有过这种体验设备在产线上跑得好好的突然掉电重要参数丢了或者EEPROM的寿命到了写不进数据又或者是Flash在进行页擦除的时候被打断整块数据变成垃圾。这些问题在数据采集终端、远程运维控制器、电力监测设备这类需要长期稳定运行的场景里尤其致命。这个项目的核心就是一套“非易失性存储方案”把 Everspin 的 MR25H40CDF 这款 SPI 接口 MRAM磁阻随机存取存储器接到 STM32F417ZG 这颗主控上用一套健壮的驱动代码实现数据的写入、读取、校验和掉电保护。说白了就是要让系统有一个“断电也不丢、还不怕反复写”的数据仓库。MRAM 和传统存储器最大的区别在于物理存储原理。MRAM 用磁性隧道结MTJMagnetic Tunnel Junction存储数据通过磁化方向的改变来代表 0 和 1读写过程和电荷存储无关。这意味着它既不像 EEPROM 那样写几次就磨损也不像 Flash 那样要分块擦除、按扇区管理。MR25H40CDF 的标称写次数是 10 万亿次几乎可以当 SRAM 用但数据掉电不丢。这种特性放在工业设备里太合适了。STM32F417ZG 这边选它的原因有两个一是主频 168MHz、带硬件 FPU跑通信协议栈和数据处理都绰绰有余二是 F4 系列有丰富的 SPI 外设和 DMA 通道配合 MRAM 的高速率能发挥出全部性能。整个方案的定位是面向工业级应用的可落地方案而非实验室里的玩具 demo。1.2 硬件选型为什么是这两位主角选 MR25H40CDF 而不是别的存储芯片是因为这款芯片刚好卡在几个关键需求的交汇点上容量 4Mbit512KBSPI 接口最高时钟 40MHz工作温度范围 -40 到 85 摄氏度工业级版本写入不需要先擦除单字节可写掉电不丢数据。再配合 STM32F417ZG 这颗主控的硬件资源就构成了一套典型的“高可靠性存储子系统”。F417ZG 的 SPI 外设支持 DMAMRAM 支持连续突发写两者一起工作可以实现接近 SRAM 体验的写入吞吐率。对于有数据记录需求的场景比如“每毫秒存一组传感器数据”这套组合是非常实用的。权限不匹配已经加了权限后还是不行可能是 BMI/BOR 相关的选项字被改了。可以检查 option bytes 里 BOR_LEV 和 nRST_STOP 等位是否正常。2.3 片选、写保护与保持引脚的肉身接线MR25H40CDF 有 8 个引脚SOIC-8 封装其中除了电源和地就是 CS、SCK、SI、SO、WP、HOLD 这六个信号。注意这个芯片没有额外的 RESET 引脚这一点和 SPI Flash 不同。上电时它自己会复位内部状态机不需要外部拉一个复位脚。WP写保护和 HOLD保持引脚如果不用必须拉高不要悬空。悬空状态下引脚电平不确定轻则误触发写保护导致写入失败重则 HOLD 误拉低让时钟中断。我习惯的做法是各接一个 10k 电阻上拉到 3.3V两组信号直接连到 MCU 的 GPIO。其中 WP 脚接到普通 GPIO 即可驱动代码里把这个 GPIO 作为写保护开关默认拉高要写数据时如果硬件上需要临时解除保护可以控制它。CS 片选信号必须由 MCU 的 GPIO 控制不要直接用硬件的 NSS 自动模式。SPI 从机设备的片选最好由软件直接控制这样时序上更灵活尤其是 MRAM 这类支持“边写边等”的芯片CS 拉低开始指令CS 拉高结束指令软件控制不会受硬件 NSS 状态机的干扰。实际工程里我始终用软件片选稳定性和可控性都更好。另外MR25H40CDF 的 SO 引脚在没有选中时是高阻态。这意味系统里如果总线上还挂了其他 SPI 设备只要片选逻辑正确MRAM 不会干扰其他设备通信。这也省了一些信号隔离的麻烦。2.4 PCB 布局与信号完整性注意点SPI 时钟最高 40MHz虽然频率不算特别高但在工业环境里仍然建议注意信号完整性。SCK 和 SI 走线尽量短不要绕远路更不要在附近走大电流的开关电源线。如果板子上有电机驱动、继电器这类干扰源MRAM 和主控之间的走线要尽量避开这些区域。去耦电容不要省。在 MRAM 的 VDD 引脚旁边放一个 0.1uF 的小电容再在稍远处放一个 1uF 或 4.7uF 的钽电容这是常规操作。很多奇怪的问题——比如写入数据偶尔出错——排查到最后都是电源纹波惹的祸。MRAM 写入瞬间电流变化不大但如果供电线路太长、去耦不足SCK 边沿正好碰上电源跌落就可能产生亚稳态导致数据错位。PCB 走线还有一个容易被忽视的细节WP 和 HOLD 的弱上拉电阻要尽量靠近芯片引脚而不是靠近 MCU。因为上拉电阻靠近芯片可以在上电瞬间更快地让引脚进入确定电平减少上电过程中引脚浮空的风险窗口。3. STM32F417ZG 侧的系统设计与初始化3.1 为什么选 STM32F417ZG 做主控F417ZG 这颗芯片是 STM32F4 系列里很强的型号Cortex-M4F 内核168MHz 主频1MB Flash192KB RAM资源相当充裕。更重要的是它的外设配置很适合做这个项目有 6 个 SPI 接口多个 DMA 控制器以及充足的 GPIO。在工业数据采集场景里主控往往不只干“存储数据”这一件事。它可能同时要跑 Modbus 协议栈、采集模拟量、控制继电器、维护实时时钟。F417ZG 的余量保证了这些任务可以并行跑不会出现“为了存个数据把 CPU 占死”的窘境。尤其值得一提的是 F417ZG 的 SPI 位速率。SPI1 挂在 APB2 总线上APB2 时钟最高可为 84MHzSPI1 的分频配置可以做到 42MHz超过了 MR25H40CDF 的 40MHz 上限。实际上我们不会真跑满通常选择 21MHz4 分频或 10.5MHz8 分频留一些时序余量兼顾稳定性和速度。3.2 SPI 引脚分配与时序参数配置本方案里 STM32F417ZG 的 SPI1 具体分配如下SCKPA5MOSIPA7对应芯片的 SIMISOPA6对应芯片的 SOCSPB0软件控制任意 GPIO 均可使用 SPI1 而不用 SPI2/SPI3是因为 SPI1 挂在 APB2 上可以拿到更高的时钟频率。如果你希望用其他 SPI 外设也完全可以只要根据相应的总线频率重新计算分频系数就行。SPI 模式方面MR25H40CDF 支持 SPI Mode 0CPOL0CPHA0和 Mode 3CPOL1CPHA1。我用的是 Mode 0这也是大多数 SPI 器件默认的工作模式。要注意的是不同的 SPI 模式之间不能混用比如 Flash 的 W25Q 系列和这个 MRAM 通常都兼容 Mode 0 和 Mode 3但如果你把 SPI 外设配置成 Mode 0 去读一个只支持 Mode 3 的器件数据读出来就会乱掉。下面是 STM32CubeMX 里的关键配置参数配置值说明SPI ModeFull-Duplex Master全双工主模式CPOLLowMode 0CPHA1 EdgeMode 0Prescaler4SPI1 时钟 84MHz / 4 21MHzData Size8 bits字节传输First BitMSB First高位先出NSSSoftware软件管理片选3.3 供电与复位时序STM32F417ZG 的 VDDA、VDD 供电默认都是 3.3V。MR25H40CDF 的供电电压范围在 3.0V-3.6V和 STM32 刚好共用 3.3V 电源轨。这样省去了电平转换电路直接相连即可。上电时序上要注意 MRAM 的 VDD 达到最小工作电压后还需要一小段内部上电初始化时间典型值在微秒到毫秒量级。这个时间内芯片可能无法响应指令。为了稳妥驱动代码里在 SPI 初始化之后执行任何读写操作之前加一个 1ms 左右的延时。实操中我直接调 HAL_Delay(1)简单有效。另外MR25H40CDF 在 VDD 上电时会默认进入深度掉电Deep Power Down状态吗答案是否定的。上电后芯片默认处于正常待机状态不需要像某些 Flash 那样发“退出掉电”指令。但如果之前软件里主动调用了 DPDDeep Power Down指令那么下次上电后需要先执行退出命令再操作这一点在写驱动时要留意。3.4 硬件引脚初始化代码CubeMX 生成的 SPI1 初始化代码已经足够用但要注意把 CS 引脚配置为推挽输出并拉高。这里给出初始化后的 GPIO 配置代码实际项目中改动不大static void MRAM_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); /* SPI1 引脚 */ GPIO_InitStruct.Pin GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_VERY_HIGH; GPIO_InitStruct.Alternate GPIO_AF5_SPI1; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); /* CS 引脚软件控制 */ GPIO_InitStruct.Pin GPIO_PIN_0; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_VERY_HIGH; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); }如果你自己画板时用了其他的 CS 引脚只需改 Pin 和 GPIO 端口即可。把 CS 设成推挽输出并且初始化完成后置高是为了保证在 SPI 外设还没有就绪的时候不会误选中 MRAM。4. 软件驱动与数据读写实现4.1 驱动架构把 MRAM 当“超强 EEPROM”用MRAM 驱动写起来不难但也不要一上来就简单封装几个 SPI 读写函数就完事。我会建议你把存储层抽象成两层底层是硬件访问层负责操作 SPI 外设、控制 CS 引脚、发送指令字上层是存储逻辑层提供“写入一段数据”“读取一段数据”“擦除一段数据”这样的接口。这个分层的价值在于上层业务代码完全不需要关心底层是 MRAM、NRAM 还是 SPI Flash。以后要换存储芯片只要重写底层驱动上层代码一行不用动。MRAM 没有“擦除”概念所谓的“擦除”其实就是写入 0xFF。对于业务逻辑层你可以提供一个“擦除”接口内部实现为写一段全 0xFF 的数据。这样做的好处是上层代码的语义和原来用 Flash 时保持一致。4.2 核心读写指令与时序MR25H40CDF 的指令集和普通 SPI Flash 相似几条核心指令如下WREN0x06写使能。每次写入前必须发送芯片内部有写使能锁存器。WRDI0x04写禁用。READ0x03从指定地址读取数据支持连续读。WRITE0x02写入数据支持连续写。RDSR0x05读状态寄存器。WRSR0x01写状态寄存器。RDID0x9F读 ID 寄存器返回 3 个字节的设备 ID。其中最容易踩坑的就是 WREN。SPI Flash 和 MRAM 都要求在写操作之前先发 WREN 指令否则写操作会被忽略。原因是芯片内部有个“写使能锁存器”只有 WREN 指令能把它的值置 1向状态寄存器写 0 也不能清掉它只有掉电或者 WRDI 才会清除。所以驱动里每个写入序列的顺序必须是CS 拉低 - 发 WREN (0x06) - CS 拉高 - CS 拉低 - 发 WRITE (0x02) 24 位地址 数据 - CS 拉高中间那个“CS 拉高再拉低”是必须的不能省略。如果 CS 一直保持低电平直接发 WRITEMRAM 会认为 WRITE 指令前的 WREN 没有被“锁存”写操作依然无效。这个细节我也是踩过坑才记住的。4.3 单字节写、连续写与页边界处理单字节写最简单但效率低。MR25H40CDF 支持连续突发写你在 WRITE 指令后面跟着数据每写完一个字节内部地址自动加一直到 CS 拉高才会停止。注意MR25H40CDF 没有像 SPI Flash 那样的“页”限制。SPI Flash 通常一页是 256 字节写的时候不能跨页跨页要重新发指令而 MRAM 整个 512KB 是一个连续空间可以一条 WRITE 指令从地址 0x00000 一直写到 0x7FFFF。这是 MRAM 的一个巨大优势也是很多人转过来用之后觉得“真舒服”的地方。不过在驱动里我仍然建议设定一个“单次写入长度上限”比如 4096 字节。这样做的原因不是因为 MRAM 不支持更长而是为了配合上层协议的校验逻辑。如果一次写几千个字节后发现校验失败回滚的成本太高。限制单次写入长度加上块内 CRC定位问题会方便得多。4.4 完整的 SPI 读取与写入示例代码直接上代码。先看最基础的 SPI 收发封装static void MRAM_CS_Low(void) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); } static void MRAM_CS_High(void) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); } static uint8_t MRAM_SPI_TransferByte(uint8_t byte) { uint8_t rx 0; HAL_SPI_TransmitReceive(hspi1, byte, rx, 1, HAL_MAX_DELAY); return rx; }读取一段数据的函数void MRAM_ReadData(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[4]; cmd[0] 0x03; // READ 指令 cmd[1] (addr 16) 0xFF; // 地址高 8 位 cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; MRAM_CS_Low(); for (int i 0; i 4; i) { MRAM_SPI_TransferByte(cmd[i]); } for (uint32_t i 0; i len; i) { buf[i] MRAM_SPI_TransferByte(0x00); } MRAM_CS_High(); }写入一段数据的函数void MRAM_WriteData(uint32_t addr, const uint8_t *buf, uint32_t len) { uint8_t cmd[4]; /* 第 1 步发送 WREN 指令 */ MRAM_CS_Low(); MRAM_SPI_TransferByte(0x06); // WREN MRAM_CS_High(); /* 第 2 步发送 WRITE 指令 地址 数据 */ cmd[0] 0x02; // WRITE cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; MRAM_CS_Low(); for (int i 0; i 4; i) { MRAM_SPI_TransferByte(cmd[i]); } for (uint32_t i 0; i len; i) { MRAM_SPI_TransferByte(buf[i]); } MRAM_CS_High(); }在 MRAM 上WRITE 指令之后不需要等待内部编程完成因为写入是即时完成的。这一点和 SPI Flash 差别很大。Flash 在发完 WRITE 指令后还要读状态寄存器轮询 BUSY 位而 MRAM 把 CS 拉高数据就已经“固化”了。写驱动的复杂度直线下降。如果你是从 Flash 驱动改过来的写完后千万别把“等待 busy”那段代码硬套在 MRAM 上。MRAM 没有忙状态状态寄存器里也没有 BUSY 位。读回来的状态字节含义和 Flash 不完全一样这一点需要仔细看 datasheet 的状态寄存器定义。4.5 读状态寄存器与设备 ID 校验上电自检的好习惯是读取 MRAM 的 JEDEC ID 确认芯片是真的并且接线正常。MR25H40CDF 的 RDID 指令是 0x9F读回 3 个字节对于 MR25H40CDF这 3 个字节通常是 0x29、0x40、0x27。在系统启动时做一次 ID 校验意义重大。很多看起来玄学的失败最后都是芯片虚焊、型号买错、或者引脚接反。ID 校验能把这类问题提前暴露int MRAM_CheckID(void) { uint8_t id[3]; MRAM_CS_Low(); MRAM_SPI_TransferByte(0x9F); id[0] MRAM_SPI_TransferByte(0x00); id[1] MRAM_SPI_TransferByte(0x00); id[2] MRAM_SPI_TransferByte(0x00); MRAM_CS_High(); if (id[0] ! 0x29 || id[1] ! 0x40 || id[2] ! 0x27) { return -1; } return 0; }如果在自己的项目里读到 ID 不同先查一下是不是买到了其他型号。Everspin 的 MR25H 系列是一整个家族容量不同 ID 也不同。万一读出来全 0xFF那十有八九是 SPI 模式不对或者 CS 没拉低先检查硬件和初始化参数。4.6 掉电保护与数据完整性设计工业设备最怕的就是“写一半掉电”。虽然 MRAM 写得快但主控程序侧的写入过程是分步骤的先发 WREN再发 WRITE数据。如果恰好在这期间掉电可能发生的情况是部分字节写入成功部分字节是旧的。传统的解决手段是“双备份”也叫 A/B 区交替写入。具体做法是把数据区分为两个逻辑槽位每个槽位头部放一个序列号和 CRC32 校验值。写入时先写备用槽位等写完并校验通过后再把“当前有效槽位”的标记切换过去。启动时读取两个槽位的序列号谁新选谁。这种方式可以保证任何时刻意外掉电至少有一个完整的有效数据副本。MRAM 的好处在于不用考虑“磨损均衡”因为它的擦写寿命近乎无限双备份带来的额外写入开销可以完全忽略。这在传统 Flash 方案里是不敢想的Flash 双备份等于寿命减半而 MRAM 完全没有这个问题。伪代码如下typedef struct { uint32_t seq; uint32_t crc; uint8_t payload[512]; } StorageBlock; void Storage_Write(const uint8_t *payload) { StorageBlock temp; /* 读取两块区的序列号找到旧数据所在的备用区 */ uint32_t slot (block0.seq block1.seq) ? 1 : 0; temp.seq (slot 0 ? block1.seq : block0.seq) 1; temp.crc CRC32_Calc(payload, sizeof(payload)); memcpy(temp.payload, payload, sizeof(payload)); /* 写入备用区并校验 */ MRAM_WriteData(slot * sizeof(StorageBlock), (uint8_t *)temp, sizeof(StorageBlock)); /* 读回校验失败则重试 */ ... }启动时读出两个槽位校验 CRC然后选择序列号大且校验通过的那个。这套方案在电力设备这类要求严格的场景里是非常常用的。配上 MRAM 的无限寿命基本上“写到天荒地老”都不用担心数据损坏。4.7 性能数据一次实测记录我在实际项目里测过一次性能数据SPI 时钟配置为 21MHz连续写 1KB 数据耗时大约 0.5ms 到 0.6ms 左右。对比同一块板子上原来的 SPI FlashW25Q64在 21MHz 下写 1KB需要先擦除整页再编程写入加上等待内部忙耗时通常要 3ms 到 5ms。MRAM 在写入性能上的优势一目了然。不过要注意SPI 传输本身受限于时钟频率MRAM 的“快”主要体现在没有擦除等待和即时写入。如果你用 21MHz 的 SPI单字节传输耗时约 0.4us读 1KB 就是约 410us。这个速度在绝大多数工业数据记录场景下完全够用。如果你的应用需要更快可以把 SPI 时钟提到 40MHz 或者使用 QSPI 接口的外部 RAM。但 MR25H40CDF 本身是标准 SPI40MHz 已经是上限再快就要考虑并行 MRAM 或者改用其他接口了。5. 常见问题与排查技巧实录5.1 SPI 读出来全是 0xFF 或 0x00这是最常见的问题。我先说排查顺序先查 SPI 工作模式。MR25H40CDF 要求 Mode 0 或 Mode 3如果你配置成了 Mode 1 或 Mode 2读出来的数据就是错乱或全 0xFF。其次查 CS 引脚信号是不是真的拉低了很多时候是 GPIO 初始化没做对CS 一直高电平芯片根本没选中。再一个隐藏雷区是 MISO 和 MOSI 接反。有的工程师画 PCB 时把主机的 MISO 接到了从机的 MISO其实是正确的但如果从机角度标的是 SO/SI物理连接时搞混了就会读到全 1 或全 0。建议先跑一个“回环测试”把 MOSI 和 MISO 短接发 0xA5 看能不能收回来能收回来说明 SPI 外设本身没问题再查外部接线。5.2 写入数据后断电重启就丢失MRAM 掉电不丢数据如果出现断电丢失基本可以排除芯片本身的问题。先检查是不是写完成后没有正确拉高 CS。MRAM 的写入动作是在 CS 拉高的瞬间“提交”的如果写完数据后 CS 一直保持着低电平就断电了这次写入可能不会生效。还有一种情况是写保护状态被设置过。MR25H40CDF 的状态寄存器里有写保护位如果之前误写了状态寄存器把某些地址区域设成了写保护写入操作会“成功”但实际没有修改数据。排查时读一下状态寄存器确认保护位状态正常。不过默认状态下状态寄存器的值都是 0也就是全片可写。最后别忘了查电源电容。VDD 引脚上的去耦电容如果靠近芯片放置得当一般不会出问题但如果电源纹波大导致芯片内部电压不稳写入数据可能落到“不确定状态”表现为重启后部分数据内容变成随机值。5.3 高时钟下数据错位SPI 时钟配置太高比如直接跑 40MHz可能会出现数据错位尤其是线比较长或者 PCB 布局不理想的时候。表现是连续读没问题但跨板通信偶尔出错或者读取的数据中间夹几个字节乱码。解决办法不是加复杂算法而是降速。把 SPI 分频从 2 改成 4也就是 21MHz问题通常会消失。工业环境里 21MHz 已经足够满足绝大多数数据吞吐需求。不要在 SPI 总线上盲目追求极限频率稳定压倒一切。5.4 从 Flash 驱动迁移过来的适配问题很多现有的项目原先是 W25Q 系列 Flash往 MRAM 迁移时容易漏掉几个差异点Flash 需要先擦除再写MRAM 不需要直接写即可。Flash 的写指令后要轮询状态寄存器等 BUSYMRAM 不需要CS 拉高即完成。Flash 有页限制MRAM 无页限制可连续写。Flash 的状态寄存器忙标志位bit 0和 MRAM 的状态寄存器格式不同不能复用代码。如果你之前的应用层调用了“擦除扇区”这类接口迁移到 MRAM 后这个接口可以直接实现为写入全 0xFF 或者直接返回成功。因为 MRAM 不需要擦除任何地址都可以直接写入新数据。5.5 一颗教训不要让 HOLD 引脚悬空我第一次设计这个电路时HOLD 引脚没有处理直接悬空。板子回来后大多数时候工作正常但偶尔在高温老化测试中出现 SPI 通信卡死、数据读不出来。排查了很久最后用示波器抓 HOLD 引脚发现存在明显的不确定电平毛刺。把 HOLD 拉高之后问题再没出现过。所以我的原则是所有“使能类”引脚绝不能悬空。WP 和 HOLD 一律通过 10k 电阻上拉至 VDD。如果你需要硬件写保护功能可以把 WP 接 MCU GPIO 控制如果不需要直接硬拉高即可。写在最后这个方案做下来最大的感受是 MRAM 让存储相关代码变得简单了很多。不需要管理擦除、不需要处理写均衡、不需要担心寿命写驱动的精神负担小了很多。MR25H40CDF 与 STM32F417ZG 的组合在工业数据采集、仪器仪表、电力监控这类需要“随时写、不怕掉电、长期稳定”的场景里是非常值得考虑的一套方案。我自己在实际项目里后来还把这款 MRAM 用在了 bootloader 的参数备份区和黑匣子数据记录区。它的写入延迟低容错性好调试时甚至可以直接用内存读写的方式操作极大降低了开发调试的复杂度。如果你也在考虑给嵌入式系统加一块可靠的非易失性存储可以仔细看看 MRAM 这条路——说不定你也会觉得数据存储本来就不该那么麻烦。
返回列表