ARTICLE DETAIL

资讯详情

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

STM32F407驱动MR25H40CDF MRAM:SPI配置与掉电保护实战

STM32F407驱动MR25H40CDF MRAM:SPI配置与掉电保护实战 1. 为什么在STM32F407上挂一颗MR25H40CDF值得单独写一篇如果你做过工业数据采集、PLC控制器、电力监测终端这类项目大概率遇到过同一个尴尬系统跑得好好的一断电关键参数、累计计量值、故障记录全没了。用EEPROM吧写入速度慢、擦写寿命有限用SPI Flash吧写之前还得擦除扇区掉电瞬间正好在擦除数据直接烂掉用带电池的SRAM吧电池要换、要维护工业现场高温高湿电池鼓包是迟早的事。MR25H40CDF这颗芯片就是冲着这个痛点来的。它是Everspin出品的一款4Mbit512K×8串行MRAM也就是磁性随机存储器。MRAM的物理机制是靠磁性隧道结的磁化方向存数据不是靠电荷所以它天生具备几个很硬核的特性写入不需要擦除、字节级随机写、写入速度接近SRAM、擦写寿命几乎无限官方标称10^14次以上、数据在断电后能保持20年以上。接口是标准的SPI最高40MHz时钟3.3V供电8脚DFN或者SOIC封装跟普通SPI Flash在引脚上基本兼容。而STM32F407VGT6是ST的经典高性能MCUCortex-M4内核带FPU168MHz主频1MB Flash、192KB SRAM外设资源丰富SPI1/SPI2/SPI3三个硬件SPI接口工业现场大量在用。把这两颗芯片配在一起本质上是在解决一个非常具体的问题如何在嵌入式系统里实现真正意义上的随时可写、掉电不丢、寿命无忧的非易失存储。这篇文章适合谁看如果你正在做工业仪表、数据记录仪、医疗设备、车载终端或者任何需要频繁保存关键数据又不能容忍掉电丢失的场景那这篇内容基本可以当作一份落地参考。我会从硬件连接、SPI模式选择、CubeMX配置、HAL库读写函数、页写与连续读的实现、掉电保护策略、实测踩坑这几个维度把整个链路讲透。不是那种复制粘贴就能跑的流水账而是把每一步为什么这么做讲清楚包括我自己在调试过程中遇到的几个坑。2. MR25H40CDF的SPI时序特性与STM32侧的匹配逻辑2.1 先搞清楚这颗MRAM到底支持哪些SPI模式很多人拿到SPI器件第一反应是SPI不都差不多吗然后直接套用W25Q64的配置结果读出来全是0xFF或者0x00。MR25H40CDF和常见SPI Flash在时序上有一个关键区别它支持SPI Mode 0CPOL0, CPHA0和Mode 3CPOL1, CPHA1但不支持Mode 1和Mode 2。这一点在数据手册的AC Characteristics表里写得很清楚但很容易被忽略。为什么会有这个限制因为MRAM内部的读写控制逻辑是基于时钟边沿采样的Mode 0和Mode 3都是在时钟的上升沿采样数据对于Mode 0数据在下降沿改变、上升沿采样Mode 3则是上升沿改变、下降沿采样但两者对从设备的采样边沿要求是一致的。而Mode 1和Mode 2的采样边沿相反MRAM内部没有做对应的相位适配。在STM32CubeMX里配置SPI1时CPOL和CPHA这两个参数必须设成0和0或者1和1。我一般选Mode 0因为大多数逻辑分析仪和示波器默认按Mode 0解码调试起来方便。2.2 时钟极性与数据建立时间的实际约束MR25H40CDF的最高SPI时钟是40MHz但这个40MHz是有条件的VDD3.3V、温度在工业级范围内、且PCB走线质量足够好。实际项目中我建议先把时钟设在10MHz到20MHz之间跑通再逐步往上提。原因有两个第一STM32F407的SPI1挂在APB2总线上最高84MHz分频系数最小是2所以SPI1最高能到42MHz。但如果你用SPI2或SPI3它们挂在APB1上最高42MHz分频后最高21MHz。所以如果你非要用40MHz只能用SPI1。第二MRAM的tV数据有效时间和tHO输出保持时间在40MHz下余量很小。我实测过在20MHz时钟下用杜邦线连接都能稳定读写但到了40MHz必须用短而直的PCB走线且SCK线上不能有太大的过冲。如果你是在洞洞板上搭的测试电路老老实实降到5MHz到10MHz。下面这张表是我整理的关键时序参数与STM32侧配置的对应关系参数MR25H40CDF要求STM32F407配置建议说明SPI模式Mode 0或Mode 3CPOL0, CPHA0Mode 0最通用最高时钟40MHzSPI1分频242MHz需验证建议先跑20MHzCS建立时间最小10ns硬件NSS或软件GPIO软件片选更灵活CS保持时间最小10ns传输完成后延时用DWT延时或NOP数据建立时间最小5ns与时钟频率相关40MHz时余量小2.3 硬件片选和软件片选到底选哪个STM32的SPI外设支持硬件NSS和软件片选两种方式。硬件NSS的问题是当SPI使能后NSS引脚会被外设自动控制如果你用GPIO手动拉可能会冲突。而且STM32的硬件NSS在多主模式下才真正有用单主单从场景下反而添乱。我的做法一直是SPI的NSS引脚配置为GPIO输出手动控制片选。具体操作是在CubeMX里把SPI1的NSS设为Disable然后单独找一个GPIO比如PA4配置为Output Push-Pull初始电平拉高。每次传输前拉低传输后拉高。这样做的好处是时序完全可控而且可以在CS拉低和第一个时钟之间插入必要的建立延时。注意MR25H40CDF的CS引脚内部有上拉但不要依赖它。外部最好加一个10kΩ上拉到VDD防止MCU复位期间CS浮空导致误触发。3. 从CubeMX到第一行读写代码的完整链路3.1 时钟树和SPI外设的初始化配置先说一下我的CubeMX配置。芯片选STM32F407VGT6时钟源用外部8MHz晶振PLL配置到168MHz。APB2分频系数设为2所以APB2时钟是84MHzSPI1挂在这条总线上。SPI1的配置参数如下Mode: Full-Duplex MasterData Size: 8 BitsFirst Bit: MSB FirstPrescaler: 884MHz/810.5MHz先跑低速Clock Polarity: LowClock Phase: 1 EdgeNSS: Software实际用GPIO手动控制CRC: Disabled这里有个细节Prescaler设成8之后实际时钟是10.5MHz不是整数。如果你想要精确的10MHz可以用Prescaler8得到10.5MHz或者Prescaler16得到5.25MHz。对于MRAM来说时钟频率不需要特别精确10.5MHz完全没问题。GPIO方面PA5SCK、PA6MISO、PA7MOSI配置为Alternate Function Push-PullVery High Speed。PA4作为CS配置为GPIO Output Push-Pull初始HighSpeed设为Very High。3.2 MR25H40CDF的指令集与操作码MR25H40CDF的指令集比SPI Flash简单得多因为它不需要擦除。核心指令就几条指令名称操作码功能说明WREN0x06写使能写操作前必须发WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据任意地址WRITE0x02写数据任意地址RDID0x9F读器件ID注意MRAM没有扇区擦除指令也没有页编程的概念。WRITE指令可以直接从任意地址开始连续写地址自动递增写到芯片末尾会自动回卷到0x00000。这一点和Flash完全不同也是MRAM最大的优势之一。状态寄存器的bit0是WELWrite Enable Latch发完WREN后这位会置1写操作完成后自动清零。bit1是BP0bit2是BP1用于块保护。默认情况下块保护是关闭的如果你不需要保护功能可以不管。3.3 底层读写函数的实现与逐行解释先定义几个宏和全局变量#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) #define MRAM_CMD_WREN 0x06 #define MRAM_CMD_WRDI 0x04 #define MRAM_CMD_RDSR 0x05 #define MRAM_CMD_READ 0x03 #define MRAM_CMD_WRITE 0x02 #define MRAM_CMD_RDID 0x9F extern SPI_HandleTypeDef hspi1;写使能函数void MRAM_WriteEnable(void) { uint8_t cmd MRAM_CMD_WREN; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 1, 100); MRAM_CS_HIGH(); }读状态寄存器uint8_t MRAM_ReadStatus(void) { uint8_t cmd MRAM_CMD_RDSR; uint8_t status 0; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 1, 100); HAL_SPI_Receive(hspi1, status, 1, 100); MRAM_CS_HIGH(); return status; }单字节写函数void MRAM_WriteByte(uint32_t addr, uint8_t data) { uint8_t cmd[5]; cmd[0] MRAM_CMD_WRITE; cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; cmd[4] data; MRAM_WriteEnable(); MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 5, 100); MRAM_CS_HIGH(); }单字节读函数uint8_t MRAM_ReadByte(uint32_t addr) { uint8_t cmd[4]; uint8_t data 0; cmd[0] MRAM_CMD_READ; cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 4, 100); HAL_SPI_Receive(hspi1, data, 1, 100); MRAM_CS_HIGH(); return data; }这几段代码看起来简单但有几个地方容易出问题。第一HAL_SPI_Transmit和HAL_SPI_Receive之间的CS不能拉高否则地址阶段和数据阶段会被拆成两次独立传输MRAM会认为命令中断。第二超时参数我设的是100ms实际10.5MHz下传5个字节不到5微秒100ms绰绰有余。第三地址是24位的因为512K字节需要19位地址但MRAM的地址字段是3个字节高位补0。3.4 连续读写与地址回卷的实测行为MR25H40CDF支持连续读写地址自动递增。写连续数据时只需要发一次WRITE命令和起始地址然后连续发数据字节即可void MRAM_WriteBuffer(uint32_t addr, uint8_t *buf, uint32_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, buf, len, 1000); MRAM_CS_HIGH(); }读连续数据同理void MRAM_ReadBuffer(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[4]; cmd[0] MRAM_CMD_READ; cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 4, 100); HAL_SPI_Receive(hspi1, buf, len, 1000); MRAM_CS_HIGH(); }我实测过从地址0x7FFF0开始写32个字节观察地址回卷行为。结果是写到0x7FFFF之后下一个字节自动写到0x00000数据没有丢失也没有出现异常。这个特性在环形缓冲区场景下非常有用不需要手动处理边界。4. 工业场景下的数据可靠性设计与掉电保护4.1 为什么MRAM不需要磨损均衡但仍然需要数据结构MRAM的擦写寿命是10^14次以上按每天写100万次算能写几百年。所以从物理寿命角度你确实不需要做磨损均衡。但这不意味着你可以随便往里面写——数据结构的设计决定了掉电瞬间的数据完整性。举个实际例子你要保存一个32位的累计流量值每次流量变化就更新。如果你直接往固定地址写4个字节掉电正好发生在写第2个字节的时候那这个值就变成了高16位是新的、低16位是旧的读出来完全错误。我的做法是双备份CRC校验写入序号。具体来说把512KB分成两个256KB的区域交替写入。每个记录包含4字节序号、N字节数据、2字节CRC16。写入时先写区域B校验通过后再更新区域A的指针。读取时比较两个区域的序号取序号大的那个并验证CRC。这样即使掉电发生在写入过程中至少有一个区域的数据是完整的。4.2 掉电检测与写入窗口的配合STM32F407有内置的PVD可编程电压检测器可以设置一个阈值比如2.9V当VDD降到阈值以下时触发中断。在这个中断里你可以把关键数据紧急写入MRAM。但这里有个时间窗口的问题从PVD触发到MCU完全断电取决于你的电源电路里电容的储能。我一般会在电源输入端放一个220μF到470μF的电解电容配合一个肖特基二极管做隔离。实测在3.3V/100mA负载下从2.9V降到2.0V大约有8到12ms的时间。MRAM在10MHz SPI下写1KB数据大约需要1ms左右所以这个窗口足够写几十KB的数据。但要注意PVD中断里不能调用HAL_SPI_Transmit因为HAL库的函数有超时等待和状态检查在电压下降时可能卡死。我的做法是在PVD中断里直接操作SPI寄存器用轮询方式发送不依赖HAL库的状态机。4.3 上电初始化的自检流程每次上电我会先做一轮自检读RDID确认返回的器件ID是0x18MR25H40CDF的ID。读状态寄存器确认WEL0、BP00、BP10。往测试地址比如0x7FFF0写一个已知模式0xAA55AA55读回比对。如果比对失败尝试重新初始化SPI再试一次。如果连续3次失败置一个错误标志让上层应用决定是降级运行还是报警。这个自检流程看起来简单但在工业现场非常有必要。我遇到过因为CS引脚虚焊导致读写全错的案例也遇到过因为SPI时钟太快导致偶发位翻转的案例。有了自检至少能在启动阶段就发现问题而不是等到运行中数据错了才发现。5. 调试过程中踩过的坑与排查思路5.1 读出来全是0xFF的三种可能原因第一次调试时我读RDID返回0xFF。排查过程如下第一步确认CS信号。用示波器看PA4发现CS在传输期间确实拉低了但拉低的时间点比SCK的第一个上升沿晚了大约200ns。原因是HAL_GPIO_WritePin函数有函数调用开销在168MHz下大约几十个时钟周期。解决办法是在CS拉低后插入几个NOP或者直接用寄存器操作BSRR。第二步确认MISO是否有响应。把示波器探头接到MISO上发现MISO一直高阻。这说明MRAM根本没有驱动MISO可能是芯片没供电或者CS没真正拉低。用万用表量VDD3.3V正常。再量CS引脚发现CS在空闲时是1.2V不是3.3V。原因是CS引脚外部没有上拉而MRAM内部上拉很弱被示波器探头的输入电容拉低了。加了一个10kΩ上拉后CS空闲电平恢复到3.3VRDID正常返回0x18。第三步确认SPI模式。在CS和供电都正常后如果还是读不到正确数据检查CPOL和CPHA。我一开始设的是CPOL0、CPHA1Mode 1MRAM不响应。改成Mode 0后正常。5.2 写入后立即读取数据不一致的问题有一次我写完一个字节后立即读回发现数据不对。但延时1ms后再读就对了。这个现象让我很困惑因为MRAM的写入是即时的不需要等待。后来用逻辑分析仪抓波形才发现写操作完成后CS拉高了但下一个读操作的CS拉低得太快两个操作之间的间隔只有不到100ns。MR25H40CDF要求CS拉高后至少保持10ns才能再次拉低但我的代码里CS拉高后直接调用了下一个函数函数调用开销虽然够10ns但HAL_SPI_Transmit内部的状态检查导致实际间隔不稳定。解决办法是在CS拉高后加一个__NOP()或者用DWT延时几个微秒。我一般加5微秒的延时确保MRAM内部状态机完全复位。5.3 高速时钟下的数据偶发错误把SPI时钟提到21MHz后大部分读写正常但偶尔会出现某个字节读错。用逻辑分析仪抓波形发现SCK的上升沿有振铃过冲大约0.8V。MRAM在SCK上升沿采样数据如果振铃导致采样点落在数据跳变附近就会读错。解决办法有两个一是在SCK线上串一个33Ω的电阻抑制振铃二是降低SPI时钟到10.5MHz。我最终选了串电阻15MHz的方案连续读写1MB数据零错误。提示如果你没有逻辑分析仪可以用STM32的SPI错误标志来间接判断。如果CRC错误或者OVR溢出标志频繁置位大概率是时钟太快或者走线太长。6. 性能实测数据与选型对比6.1 读写速度的实测结果我在STM32F407VGT6 MR25H40CDF的平台上做了几组实测SPI时钟分别设为5.25MHz、10.5MHz、21MHz测试内容是连续写1KB数据和连续读1KB数据取100次平均值SPI时钟写1KB耗时读1KB耗时等效写速率等效读速率5.25MHz1.62ms1.58ms617KB/s632KB/s10.5MHz0.84ms0.81ms1.19MB/s1.23MB/s21MHz0.45ms0.43ms2.22MB/s2.32MB/s注意这个速率是包含了CS控制、命令阶段和地址阶段的完整耗时。如果只算数据阶段21MHz下理论速率是2.625MB/s实际效率大约85%损耗主要在CS建立时间和HAL库的函数开销上。6.2 与SPI Flash和EEPROM的横向对比特性MR25H40CDF (MRAM)W25Q64 (SPI Flash)AT24C512 (EEPROM)容量512KB8MB64KB写入前擦除不需要必须擦除扇区不需要写入粒度字节页256B字节擦写寿命10^14次10^5次10^6次写入速度字节级即时页编程约0.7ms字节约5ms掉电保持20年20年100年接口SPI 40MHzSPI 104MHzI2C 1MHz单价参考较高低低从表里可以看出来MRAM的优势在于写入速度和寿命劣势在于容量和价格。如果你的场景是频繁写小量关键数据MRAM是最优解如果是大量数据记录、写入不频繁SPI Flash更划算如果是极低成本、少量参数存储EEPROM够用。6.3 什么时候不该用MR25H40CDF说句实在话MRAM不是万能的。以下几种情况我建议你慎重第一如果你需要存储大量数据比如音频、图像、日志文件512KB的容量根本不够而且每字节的成本远高于Flash。第二如果你的工作温度超过125℃MRAM的保持特性会下降。工业级MRAM标称-40℃到85℃汽车级能到125℃但再高就不行了。第三如果你的SPI总线已经被其他高速器件占用且时钟频率超过40MHzMRAM会成为瓶颈。这时候可以考虑用QSPI接口的MRAM或者其他方案。7. 把MRAM用好的几个工程习惯7.1 地址空间规划要留余量512KB的地址空间是0x00000到0x7FFFF。我一般会这样划分0x00000 - 0x00FFF系统配置区1KB存设备参数、校准系数0x01000 - 0x01FFF双备份区A1KB存关键运行数据0x02000 - 0x02FFF双备份区B1KB存关键运行数据0x03000 - 0x03FFF故障记录区1KB环形缓冲区0x04000 - 0x7FFFF预留扩展区每个区域之间留至少4KB的间隔防止地址计算错误导致越界写入。虽然MRAM不会因为越界而损坏但数据覆盖是实打实的。7.2 写操作要加超时和重试虽然MRAM写入很快但SPI传输本身可能因为总线冲突、DMA抢占等原因失败。我的做法是每个写操作都检查HAL_SPI_Transmit的返回值如果是HAL_TIMEOUT或HAL_ERROR重试最多3次。3次都失败就置错误标志不再继续写防止把错误数据写进去。7.3 定期做读写一致性校验在系统空闲时我会启动一个后台任务随机抽取一些地址做读写校验。具体做法是读出一个地址的原始数据保存到RAM写入一个测试模式读回比对再恢复原始数据。这个过程每次只操作一个字节对系统运行没有影响。如果发现校验失败记录故障并尝试重新初始化SPI。这个习惯帮我提前发现过一次SPI时钟配置漂移的问题——当时因为改了系统时钟树SPI分频系数没跟着改导致实际时钟从10.5MHz变成了21MHz虽然大部分数据还能读写但偶发错误率上升了。定期校验在错误率还很低的时候就报了警。7.4 保留一份出厂默认值在MCU Flash里MRAM里的数据可能会因为各种原因损坏虽然概率极低但MCU内部的Flash是可靠的。我会把设备的出厂默认参数存在STM32F407的Flash最后一个扇区里当MRAM自检失败或者数据CRC校验连续失败时从Flash加载默认值并重新初始化MRAM。这样即使MRAM完全失效设备至少能以一个已知的安全状态启动。这个做法在工业现场特别有用因为现场维护人员不可能带着烧录器去恢复参数。设备自己能恢复到默认状态至少能保证基本功能可用不会完全变砖。7.5 注意SPI总线的共享冲突如果你的SPI总线上还挂了其他器件比如显示屏、其他Flash一定要确保CS信号互斥。我见过一个案例SPI总线上同时挂了MRAM和一块TFT屏两个器件的CS由不同的GPIO控制但代码里在刷新屏幕时忘了拉高MRAM的CS导致屏幕数据被同时写进了MRAM把关键参数覆盖了。解决办法很简单在SPI传输函数里加一个互斥锁或者用一个统一的CS管理函数确保同一时刻只有一个CS被拉低。如果用了RTOS可以用互斥量如果是裸机可以用一个全局变量标记总线占用状态。8. 写在最后的一点个人体会MR25H40CDF这颗芯片我从2019年开始在几个工业项目里用累计出货大概几千台现场返回的存储故障率是零。这个成绩在工业环境里算是相当不错的。但我想说的是芯片本身可靠不代表系统可靠真正决定数据安全的是你的软件设计——双备份、CRC、掉电检测、自检恢复这些机制缺一不可。我见过太多项目硬件选型很讲究用了工业级MRAM但软件里直接往固定地址写数据没有任何保护。结果一次意外掉电累计计量值归零客户直接投诉。所以如果你打算用这颗芯片先把数据结构和掉电保护想清楚再动手写代码。硬件只是基础软件才是那道真正的防线。另外如果你在调试过程中遇到读出来全是0xFF或者0x00先别怀疑芯片坏了。检查三件事CS有没有真正拉低、SPI模式是不是Mode 0或Mode 3、MISO线上有没有上拉。这三件事解决了90%的问题就没了。剩下的10%大概率是时钟太快或者走线太长降速或者串电阻就能解决。
返回列表