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

文章详情

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

MR25H40CDF在STM32上的工业级数据存储实践与调试

MR25H40CDF在STM32上的工业级数据存储实践与调试 这篇不是我突发奇想写的是今年实实在在调完的一块板子。工业控制项目里数据存储一直是让人头大的环节掉电要存参数、运行日志要可靠、校准系数不能丢、现场维护还要能追根溯源。我在基于STM32F215RE的工业控制板上改用MR25H40CDF这颗 SPI MRAM 之后以前围着 Flash 和 EEPROM 打转的那些妥协基本都能删掉了。这篇把选型逻辑、硬件接线、寄存器驱动到现场调试的坑一次性说透给在嵌入式应用里做数据存储的朋友当个参考。先说结论MR25H40CDF 是一片 4Mb也就是 512KB的串行 MRAMSPI 接口能像 SRAM 一样随时读随时写却又和 Flash 一样掉电不丢数据。它很适合做工业现场的“非易失参数区”“运行日志盘”这类活。下面的内容我会尽量按我实际调试的顺序来讲硬件、驱动、校验、案例、踩坑一个不落。1. 为什么工业数据存储最后选了 MRAM 而不是 Flash 和 EEPROM1.1 Flash/EEPROM 的三个老大难问题做工业设备的工程师基本都跟 Flash 和 EEPROM 打过交道它俩的问题不是不能忍而是忍得难受。第一是写寿命。普通 SPI NOR Flash 的擦写次数一般标称十万次EEPROM 更差常见的是十万到一百万次。听起来不少但工业设备一开机就是七年十年系统如果每 5 秒存一次运行状态一天就是 17280 次一年超过六百万次。你算一下就知道指望 Flash 干这个活纯粹是在赌概率。第二是写入要有擦除和页编程流程。NOR Flash 写之前必须先擦除擦除以 sector 为最小单位通常是 4KB。这意味着你只想改一个字节参数也得先把整个 sector 读出来、擦掉、再把整块写回去。这个时刻系统是脆弱的一旦掉电轻则丢一个扇区的数据重则把关键配置文件弄坏。第三是磨损均衡。为了分摊擦写次数你得在软件里维护块映射、写计数、垃圾回收这些逻辑。这不是不能写但增加了复杂度也增加了出 bug 的概率。工业现场最怕的就是这种“平时不坏坏一次就致命”的隐性逻辑。1.2 MRAM 的底牌为什么它能同时做到掉电保存和无限写入MRAM 的核心存储单元用的是磁隧道结数据不是靠电荷保存而是靠磁性方向保存。这带来的结果很明显没有电荷泄漏问题不需要刷写掉电之后状态自然保持。写入的时候它不像 Flash 那样需要先擦除再编程而是直接改变磁化方向基本上就是“写就写了不存在等待”。所以对软件来说MRAM 的写操作体验更像 SRAM只是多了一个片选和命令的过程。这对工业场景来说太友好了你不用去设计磨损均衡算法不用在掉电窗口里跟擦除操作赛跑写坏了才真的是奇迹。另外MR25H40CDF 在写操作完成之后也没有“忙”这一说至少在这个系列里发完写命令数据就落定了。这跟 Flash 那种要轮询状态寄存器等 WIP 位清零是完全不同的体验。1.3 MRAM 不是没有短板价格、容量、接口我也得把丑话说在前面。MRAM 不是万能的它最大的两个短板是价格和容量。同样容量的情况下MRAM 比 NOR Flash 贵不少。512KB 的 MR25H40CDF 在批量采购时单价大概是同容量 NOR Flash 的几倍具体看渠道和量级。所以它不适合做那种“塞一部固件进去”的代码存储那是 NOR Flash 的天下。容量方面MR25H40CDF 是 4Mb也就是 512KB这个容量对“参数 日志 掉电保存标志位”完全是够用的但你要想存音频、存图片、存几百条带时间戳的长日志那就有点挤了。接口上它是标准 SPIMode 0 和 Mode 3 都支持最大串行时钟标称可以到 50MHz。我实际没跑满因为 STM32F215RE 的 SPI 外设时钟还到不了这个极限下面会细说。1.4 和 FRAM、电池供电 SRAM 的横向对比既然提到非易失存储器顺便把 FRAM 和电池 SRAM 也拉出来对比一下方便大家选型。指标MRAM (MR25H40CDF)FRAM电池供电 SRAM容量密度中常见几百KB低常见几KB到几百KB中写耐久近乎无限近乎无限无限依赖电池写前擦除无无无数据保持10年以上10年左右依赖电池写入速度SPI最高标称50MHz高但接口通常较慢并行/SPI很快维护成本低低要换电池、有安全风险从这个表能看出来MRAM 几乎就是“工业非易失参数存储”的六边形战士。FRAM 在写入耐久上和 MRAM 很像但容量和访问速度往往不如电池 SRAM 速度快但维护电池是工业设备最反感的事情。MRAM 把这些问题一次都解决了。2. MR25H40CDF 与 STM32F215RE 的硬件连接每一根线都讲究2.1 引脚功能与供电别把 WP 和 HOLD 当摆设MR25H40CDF 是 8 脚封装典型引脚包括CS#片选低有效SCKSPI 时钟SI数据输入MOSISO数据输出MISOWP#写保护输入HOLD#保持输入低电平暂停通信VDD/VSS电源和地这里我要特别强调 WP# 和 HOLD# 这两个脚。很多工程师第一次用 SPI MRAM 会直接忽略它们悬空处理。如果你只是跑跑 Demo 可能感觉不到问题但在工业现场这两个脚悬空等于埋雷。HOLD# 一旦被噪声拉到低电平MRAM 会暂停当前的 SPI 通信时钟和片选都识别不到表现就是数据读错、命令无响应。正确做法是给 WP# 和 HOLD# 都接 10kΩ 上拉电阻到 VDD确保它们默认处于非使能状态。哪怕你软件里根本不用写保护功能WP# 也一定要固定高电平。供电方面MR25H40CDF 支持 2.7V 到 3.6V 工作电压我按 3.3V 系统设计。VDD 边要放一个 100nF 高频去耦电容再并一个 1μF~10μF 的钽电容或陶瓷电容做低频去耦。位置要尽量靠近 VDD 引脚这是老生常谈但确实见过不少人把电容放得很远导致 SPI 高速通信时电源纹波过大。2.2 STM32F215RE 的 SPI 引脚映射与电路连接我使用的是 STM32F215RE 的 SPI1接线如下MR25H40CDF 引脚STM32F215RE 引脚说明SCKPA5 (SPI1_SCK)SPI 时钟SIPA7 (SPI1_MOSI)主机输出到从机输入SOPA6 (SPI1_MISO)从机输出到主机输入CS#PA4 (普通 GPIO)软件控制片选WP#3.3V通过 10kΩ 上拉HOLD#3.3V通过 10kΩ 上拉VDD3.3V配去耦电容VSSGND共地我把 CS# 接到了普通 GPIO 而不是 SPI 硬件 NSS 脚原因后面调试部分会详细讲。简单说就是硬件 NSS 在 STM32 上容易被自动拉低拉高不如 GPIO 直来直去稳定。STM32F215RE 的 SPI1 在 APB2 总线上PA5/PA6/PA7 的复用功能是 AF5。如果你用 CubeMX 生成初始化代码选 SPI1 之后把 NSS 配成软件模式片选脚自己拉 GPIO 即可。2.3 走线方面的建议SPI 高速下别乱飞线如果你的板子只是实验验证杜邦线连接 1MHz 以下没有问题。但 MR25H40CDF 理想工作频率在 30MHz 以上这时候走线就要注意了。SCK、SI、SO 三条线尽量等长少打过孔不要跨分割区。CS# 线可以稍微长一点但不要让 CS# 在 SCK 跳变时同时跳变否则会引起误触发。SO 和 MISO 之间的串联电阻我习惯加一个 22Ω~33Ω如果信号过冲明显可以调大。电源线和信号线不要平行走太远。我第一版 PCB 因为布局问题把 MRAM 放得离 MCU 很远SCK 走线绕了大半个板子结果跑到 15MHz 以上 MISO 波形就糊了。后来把器件挪近SCK 走线缩短到 2cm 以内问题直接消失。PCB 上距离就是时序裕量这钱省不得。3. SPI 驱动与 MRAM 命令集寄存器级读写实现3.1 初始化 SPI1参数怎么配才符合 MRAM 时序MR25H40CDF 支持 SPI Mode 0 和 Mode 3。我用的是 Mode 0CPOL 0CPHA 0也就是时钟空闲为低、第一个边沿采样数据。STM32F2 的 SPI 外设时钟有限尤其考虑到工业环境我把波特率设置得比较保守。下面代码里我用了 8 分频假设 APB2 时钟是 60MHz那么 SPI 时钟就是 7.5MHz这个频率下时序非常干净信号完整性完全不用担心。后续如果要压榨性能可以逐步升到 15MHz但建议配合示波器验证 MISO 波形的建起时间。/* SPI1 初始化MR25H40CDF 工作在 Mode 0 */ static void MRAM_SPI_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; SPI_InitTypeDef SPI_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_SPI1_CLK_ENABLE(); /* PA4 - CS普通推挽输出 */ GPIO_InitStruct.Pin GPIO_PIN_4; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_PULLUP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); /* PA5 SCK, PA6 MISO, PA7 MOSI 复用为 SPI1 */ GPIO_InitStruct.Pin GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_PULLUP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; GPIO_InitStruct.Alternate GPIO_AF5_SPI1; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); 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_8; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; HAL_SPI_Init(hspi1); }初始化完成后CS 脚要拉高MRAM 处于待机状态不能乱发命令。3.2 MR25H40CDF 的基本命令集一张表看懂串行 MRAM 的命令集和 SPI NOR Flash 很相似这对从 Flash 迁移过来的工程师是好事。核心命令如下命令名命令码功能备注WREN0x06写使能写操作前必须发WRDI0x04写禁止上电默认禁止写RDSR0x05读状态寄存器1 字节WRSR0x01写状态寄存器用于配置保护READ0x03读数据3 字节地址 数据WRITE0x02写数据3 字节地址 数据MR25H40CDF 的容量是 4Mb所以地址只需要 19 位也就是 24 位地址的高 3 位必须为 0。操作地址范围是 0x00000 到 0x7FFFF。有一点和 Flash 不同MRAM 没有“擦除”命令也没有页面大小的限制。你可以在任意地址开始连续写任意长度的数据只要不越过 0x7FFFF 这个边界。这一点极大简化了驱动设计。3.3 单字节读写封装最基础的底层函数先封装一个 SPI 收发函数。注意这里收发的字节数是 1HAL 库超时时间我给了 10ms如果实际 SPI 时钟很慢可以适当调大。uint8_t MRAM_SpiTransmitReceive(uint8_t txData) { uint8_t rxData 0; HAL_SPI_TransmitReceive(hspi1, txData, rxData, 1, 10); return rxData; } static void MRAM_CS_Low(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); } static void MRAM_CS_High(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); }写使能是写操作的前置条件。MRAM 上电后默认写禁止所以每次写之前必须先发 0x06 命令。void MRAM_WriteEnable(void) { MRAM_CS_Low(); MRAM_SpiTransmitReceive(0x06); /* WREN */ MRAM_CS_High(); } void MRAM_WriteDisable(void) { MRAM_CS_Low(); MRAM_SpiTransmitReceive(0x04); /* WRDI */ MRAM_CS_High(); }单字节写命令序列WREN - CS 低 - 0x02 - 地址高字节 - 地址中字节 - 地址低字节 - 数据 - CS 高。void MRAM_WriteByte(uint32_t addr, uint8_t data) { if (addr 0x7FFFF) return; MRAM_WriteEnable(); MRAM_CS_Low(); MRAM_SpiTransmitReceive(0x02); /* WRITE */ MRAM_SpiTransmitReceive((addr 16) 0xFF); MRAM_SpiTransmitReceive((addr 8) 0xFF); MRAM_SpiTransmitReceive(addr 0xFF); MRAM_SpiTransmitReceive(data); MRAM_CS_High(); }单字节读命令序列CS 低 - 0x03 - 地址高字节 - 地址中字节 - 地址低字节 - 发一个空时钟接收数据 - CS 高。uint8_t MRAM_ReadByte(uint32_t addr) { uint8_t data 0; if (addr 0x7FFFF) return 0; MRAM_CS_Low(); MRAM_SpiTransmitReceive(0x03); /* READ */ MRAM_SpiTransmitReceive((addr 16) 0xFF); MRAM_SpiTransmitReceive((addr 8) 0xFF); MRAM_SpiTransmitReceive(addr 0xFF); data MRAM_SpiTransmitReceive(0x00); MRAM_CS_High(); return data; }3.4 多字节读写日志块搬移的基础实际工程里很少单字节读写参数更多是整块读写。比如一开机就要把 128 字节的配置参数从 MRAM 搬到 RAM 里或者把系统状态几百字节一次性写入 MRAM。猝发访问不需要额外命令地址连续增加就行。void MRAM_ReadBuffer(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t i; if (addr len 0x80000) len 0x80000 - addr; MRAM_CS_Low(); MRAM_SpiTransmitReceive(0x03); MRAM_SpiTransmitReceive((addr 16) 0xFF); MRAM_SpiTransmitReceive((addr 8) 0xFF); MRAM_SpiTransmitReceive(addr 0xFF); for (i 0; i len; i) { buf[i] MRAM_SpiTransmitReceive(0x00); } MRAM_CS_High(); } void MRAM_WriteBuffer(uint32_t addr, const uint8_t *buf, uint32_t len) { uint32_t i; if (addr len 0x80000) len 0x80000 - addr; MRAM_WriteEnable(); MRAM_CS_Low(); MRAM_SpiTransmitReceive(0x02); MRAM_SpiTransmitReceive((addr 16) 0xFF); MRAM_SpiTransmitReceive((addr 8) 0xFF); MRAM_SpiTransmitReceive(addr 0xFF); for (i 0; i len; i) { MRAM_SpiTransmitReceive(buf[i]); } MRAM_CS_High(); }这里有个细节读数据时主机需要持续输出时钟所以MRAM_SpiTransmitReceive(0x00)是空操作目的是产生时钟让 MRAM 把数据移出到 MISO 线上这是 SPI 全双工的标准玩法。3.5 为什么不需要轮询忙标志写到这里我要特意提一下MR25H40CDF 这类 MRAM没有像 Flash 那样忙状态标志要轮询。为什么因为它根本没有擦除和编程的等待时间。写命令接收完毕磁隧道结的翻转已经完成数据就固定下来了。软件不需要反复读状态寄存器等 WIP 清空。很多从 Flash 驱动迁移过来的同事习惯性在写操作后面加一个while(MRAM_IsBusy());这就是没必要。你只需要在写命令结束后把 CS 拉高等几个微秒让片选时序恢复就可以进行下次操作。不过这并不代表你可以完全不管状态寄存器。如果你要用写保护功能还是需要通过 WRSR 配置状态寄存器里的 WPEN 位以及 HOLD/WP 功能。我实际项目里没有用硬件写保护因为软件层已经做了 CRC 校验和故障隔离没必要再挂一层保护。4. 数据完整性设计工业现场真正考验你的地方4.1 写保护逻辑和状态寄存器默认配置够用但要懂MR25H40CDF 的状态寄存器可以通过 RDSR 读取里面有几位的含义和 Flash 的状态寄存器不完全一样尤其涉及 WP# 和 HOLD# 的组合逻辑。我用到的配置很简单默认不使能 WPEN这样 WP# 引脚的电平不会对写操作产生影响。如果你需要更强硬的保护可以把状态寄存器配置为 WPEN1此时 WP# 引脚低电平会禁止写操作。这个特性适合“数据写死之后不希望被误写”的场景但会给调试带来额外麻烦。工业现场一般建议调试阶段不启用写保护量产阶段再根据产品定义决定。具体状态寄存器的每一位我建议以 MR25H40CDF 数据手册第 4 章为准。我在项目里只用默认值主要是图省心。4.2 掉电、总线毛刺和软件兜底MRAM 虽然不怕掉电丢数据但这不代表你的存储流程不需要掉电保护设计。原因很简单掉电可能发生在写命令中间。举个例子你正在给 MRAM 写一条日志写到第 3 个字节时突然断电。MR25H40CDF 接受的是一条完整命令还是一半命令由片选时序决定。如果 CS# 在命令中途被拉高比如 MCU 掉电导致 GPIO 输出混乱MRAM 会丢弃当前不完整的操作已写入的字节保留不会有中间态破坏整块数据。但问题在于如果你写的是一个 16 字节的日志条目掉电时可能只写入了前 3 个字节这条日志就是不完整的。工业日志分析系统读到这条残缺记录如果设计不当可能会把后面所有日志都当成损坏。所以我的建议是每条日志记录都带一个头部字段包含长度和 CRC16 校验值。读取时先校验头部如果长度或 CRC 不匹配就默认这条日志无效滑动到下一个槽位继续扫描。这种设计比“整块校验失败了就全丢”要健壮得多。另外MCU 侧的电源监控也很重要。STM32F215RE 我建议在掉电检测中断里尽量不执行 SPI 写操作因为有可能会写到一半总线电平被拉垮反而制造残缺记录。合理设计是掉电时只更新 RAM 里的状态等下次上电时再补写 MRAM。MRAM 的好处恰恰就是写入速度极快、没有擦除窗口补写操作任何时候执行都行。4.3 校验和与冗余用什么 CRC 算法CRC16 是工业界最常见的校验方式。我推荐用 CRC16-CCITT (多项式 0x1021)实现简单、覆盖长数据时误判率低。uint16_t crc16_ccitt(uint16_t crc, const uint8_t *data, uint32_t len) { uint32_t i; uint8_t byte; while (len--) { byte *data; crc ^ (uint16_t)(byte 8); for (i 0; i 8; i) { if (crc 0x8000) { crc (uint16_t)((crc 1) ^ 0x1021); } else { crc (uint16_t)(crc 1); } } } return crc; }如果你嫌软件计算 CRC 慢STM32F215RE 里有硬件 CRC 外设但它通常是固定多项式未必是你要的 CCITT。我的做法是软件跑 CRC16-CCITT对 128 字节参数区来说开销几乎可以忽略。4.4 写入频率高到一定程度还是要考虑缓冲策略虽然 MRAM 耐久度接近无限但 MCU 访问 MRAM 是要占用 SPI 总线时间的。比如你 10ms 写一次 16 字节日志SPI 时钟 7.5MHz一次写操作大概要传 8 个字节命令 地址 数据耗时大约 8.5μs占 CPU 时间很小。但如果系统里 SPI 总线还挂了其他传感器那么频繁写 MRAM 会占用总线带宽。我的建议是运行期高频数据先在 RAM 里聚合满 64 字节或满一个小缓冲之后一次性写入 MRAM。既减少了 SPI 占用也天然形成了日志“块”结构方便加 CRC 和长度头。5. 一个真实存储方案参数区与日志区的完整实现5.1 存储布局512KB 怎么分MR25H40CDF 共 0x80000 字节。我按功能把空间划分成三个区避免互相干扰区域地址范围大小用途参数区0x00000 - 0x0FFFF64KB设备参数、校准系数日志区0x10000 - 0x7EFFF448KB循环运行日志标识区0x7F000 - 0x7FFFF4KB固件版本、存储格式标识参数区采用“双备份 版本号”策略。在头部记录一个 4 字节 magic 和 1 字节版本号。每次修改参数时先写备份区校验成功后更新主区。下次启动时读取两个区以版本号高且校验正确的为准。这能有效防止参数写入一半时发生意外。5.2 日志区采用环形缓冲日志区我设计成这样在日志区头部四个字节存当前写偏移量currentOffset每条日志固定 64 字节或者变长但带长度字段日志写满后回卷覆盖最旧数据考虑到掉电可能造成尾部残留残缺记录我扫描日志时从头部偏移开始先检查每条记录的 magic 和 CRC不合法就标记“无效记录”并跳过继续往后找有效记录。这个过程开机时做一次512KB 扫描时间在毫秒量级完全可以接受。以下是日志写入的示意代码typedef struct { uint16_t magic; uint16_t length; uint16_t crc; uint32_t timestamp; uint8_t data[56]; } LogEntry; void Log_WriteEntry(LogEntry *entry) { uint32_t offset; uint32_t addr; entry-magic 0x4C47; entry-length sizeof(LogEntry); entry-crc crc16_ccitt(0, (uint8_t *)entry 4, sizeof(LogEntry) - 4); /* 读当前写偏移 */ MRAM_ReadBuffer(LOG_OFFSET_POS, (uint8_t *)offset, 4); addr LOG_DATA_START offset; if (addr sizeof(LogEntry) LOG_END) { addr LOG_DATA_START; /* 回卷 */ } MRAM_WriteBuffer(addr, (uint8_t *)entry, sizeof(LogEntry)); /* 更新偏移量 */ offset (uint32_t)(addr - LOG_DATA_START sizeof(LogEntry)); if (offset sizeof(LogEntry) LOG_END - LOG_DATA_START) { offset 0; } MRAM_WriteBuffer(LOG_OFFSET_POS, (uint8_t *)offset, 4); }这里有一点要注意magic和length放在记录最前面就是为了掉电时能快速判断“这条记录是否被完整写入”。如果只写了半个记录magic是对的但 length 不合法或者 CRC 不对读取端直接跳过不会影响后续记录。5.3 掉电恢复上电自检流程上电自检我固定在main()里做这几件事初始化 SPI1。读参数区主区和备份区比较 magic、版本和 CRC。如果主区合法则加载参数否则加载备份区并用备份区恢复主区。扫描日志区定位当前有效写偏移。把关键参数复制到 RAM 镜像运行期直接操作 RAM关键节点再写回 MRAM。这套流程下来MRAM 容量足够、逻辑简单还比以前的 Flash 方案省掉了磨损均衡模块的维护成本。5.4 实测性能一次日志写入到底多快我用 STM32F215RE 在 7.5MHz SPI 时钟下实测写一条 64 字节日志包括命令头、地址和 CRC 计算大约耗时 120μs。如果 SPI 提升到 15MHz耗时能压到 70μs 以内。这个速度在工业现场做事件记录绰绰有余。耐久度方面MRAM 标称写入次数在 10^14 次以上这个数字基本意味着你写一辈子都摸不到上限。所以循环日志可以大胆采用频繁覆盖策略不用担心把存储介质写坏。6. 调试记录我踩过的坑希望你绕开6.1 HOLD 脚悬空害我调了一天第一块样板回来我按 Flash 的惯性把 HOLD# 和 WP# 直接悬空。刚开始单读没问题但一连续读就出现随机数。用示波器抓 SCK 和 MISO发现 SCK 偶发毛刺再一查HOLD# 引脚电平在跳变。原来是相邻信号线串扰把 HOLD# 拉低了MRAM 进入保持状态后面的传输全部乱套。处理方式把 WP# 和 HOLD# 都接 10kΩ 上拉到 VDD重新跑 100 万次随机读写零错误。这个案例写在这里是真心建议大家别省这两个电阻。6.2 硬件 NSS 的坑CS 被 SPI 外设自动拉高我之前图省事把 MRAM 的 CS# 接到了 STM32 的 NSS 引脚想依赖 SPI 外设自动管理片选。结果发现寄存器配置和时序匹配没处理好CS# 在传输过程中被自动拉高导致 MRAM 认为命令提前结束写操作偶发失败。后来我把 CS# 挪到普通 GPIO直接用HAL_GPIO_WritePin控制问题立刻消失。嵌入式圈子里一直有句话能用软件控制的片选就不要依赖硬件的 NSS。这话我现在的体会更深了。6.3 电源纹波造成的偶发写错误第二板调试时写操作偶尔回读不一致大概几个小时内出现一两次非常难复现。后来在 VDD 引脚上用示波器一看发现 SPI 通信瞬间电源纹波有 100mV 左右的跌落超过了 MRAM 的容忍范围。解决方法是给 VDD 增加 10μF 电容并把去耦电容从 100nF 换成 100nF 1nF 并联。之后连续 48 小时压力测试没有再出现写错误。工业应用里的电源设计优先做好比加什么软件重试都管用。6.4 波形检查的三个关键点调试时一定要在示波器上看这几个点SCK 上沿/下沿的过冲如果超过 VDD0.3V建议增大串联电阻或者降低 SPI 时钟。MISO 数据建立时间SCK 采样沿到来前MISO 是否已经稳定如果临界可以降低分频。CS# 和 SCK 的时序关系CS# 拉低后到 SCK 第一个沿之间至少留一个 tCSS 时间这个参数在 MRAM 数据手册里有明确值。我实测在 7.5MHz 下CS# 先低 500ns 再发起第一个 SCK波形余量非常大各种极端电压下都没问题。6.5 整片健康度检查量产前必做每块新板子回来我会先跑一遍 MRAM 全片烧写测试从地址 0 到 0x7FFFF依次写入递增模式再读回来比对。如果全片都过说明焊接、电源、SPI 通信链路基本没问题。这个测试跑一次大约几十秒却能提前把坏焊、虚焊、地址线问题全部暴露出来省去后面在产线上排查环境的痛苦。具体模式我常用三种全 0x55、全 0xAA、地址递增。地址递增模式能检查 CS# 和地址解码逻辑全 0x55/0xAA 能检查数据线粘连和焊接短路。别一个个去读写一个循环就完了效率很高。另外如果板子上有多个 SPI 设备切记 MRAM 的片选必须独立不能和别的设备共用一个 CS。否则两条命令打架MRAM 数据被别人命令污染这种问题查起来极费时间。一点收尾的体会这套方案从前期的 QSPI Flash 方案迁移到 MRAM大概用了一周时间改动量比预想中小得多。主要的减负在于不用再维护磨损均衡和页擦除逻辑日志系统也简单了不止一个档次。MR25H40CDF 配合 STM32F215RE在工业设备里做参数存储和运行日志已经在我手头几个项目里稳定跑了几个月零数据丢失。最后再啰嗦一句MRAM 写不坏但 SPI 链路和电源会坏所以在硬件上做足保护比在软件里堆了多少重试机制都管用。
返回列表