ARTICLE DETAIL

资讯详情

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

STM32+MRAM实战:工业数据不掉电存储方案设计

STM32+MRAM实战:工业数据不掉电存储方案设计 做工业数据记录的人应该都体会过这种尴尬EEPROM 标称一百万次擦写系统里一条运行日志如果每秒写一次用不了几天就开始担心寿命NOR Flash 倒是耐磨但写入前要先擦一整块断电那一下你永远失去了最后一组关键参数。我这个项目里用了一颗 Everspin MR25H40CDF只有 8 个引脚的 4Mbit SPI MRAM把它挂在 STM32L496AG 的 SPI1 上做成了一个既能频繁写入、又能在掉电瞬间抢存数据的存储模块。这篇文章不打算泛泛介绍 MRAM 有多先进而是把我实际接线、写驱动、做掉电保护、踩坑排查的完整过程交代清楚。无论你是刚接触非易失存储的嵌入式新人还是正在给工业产品选疲劳性存储方案的老工程师都可以直接对着这套思路用。1. 项目整体设计与应用场景梳理1.1 MR25H40CDF 这颗 MRAM 到底特别在哪MR25H40CDF 是 Everspin 的串行 SPI MRAM容量 4Mbit也就是 512KByte。它本质上是一种磁性随机存储器存储单元靠磁性隧道结的磁化方向来保持数据而不是靠电荷。这一点和 Flash、EEPROM 有本质区别也直接决定了它的三个核心优势。第一是写入不需要擦除。EEPROM 和 Flash 写入前都有擦除动作Flash 还必须按扇区或块擦除所以写入延迟大、寿命受擦写次数限制。MRAM 的写入直接翻转单元的磁化方向没有擦除阶段数据在写指令完成后立刻就是非易失状态不需要等电荷泵升压、不需要等编程时间。第二是寿命极高MR25H40C 系列的数据手册给的写周期寿命是 10 的 14 次方次以上实际工作中基本可以认为写不坏。第三是写入速度快单字节写操作从发指令到数据落盘就是一条 SPI 事务的时间读跟普通 SPI NOR Flash 没什么区别。这么说吧工业设备里最常见的场景是“运行时频繁更新状态掉电前要保存现场”。EEPROM 怕频繁写Flash 怕断电丢最后一批数据MRAM 正好两个问题都收掉了。4Mbit 的容量虽然不算大但用来存参数表、报警记录、运行日志、掉电痕迹这些结构化数据绰绰有余。CDF 后缀对应的是 8 引脚 DFN 封装焊盘很小但后面我会讲具体的焊接和引脚处理办法。这里必须提一句最容易误解的地方MRAM 不是 SRAM 的替代品它访问起来仍然是 SPI 串行方式但内部写入机理不同。所以你不能指望它像 MikroBus 上那种并行 SRAM 一样随心所欲地按字节无限爆发写而是要遵守 SPI 指令时序。不过只要把时序吃透它比 Flash 好伺候得多。1.2 为什么偏偏选 STM32L496AG 来搭档STM32L496AG 是 STM32L4 系列里一颗很强悍的低功耗 Cortex-M4F MCU主频最高 120MHz内置 1MB Flash 和 320KB SRAM工作温度范围覆盖工业级。AG 这个尾缀通常对应 UFBGA 封装和较宽的温度等级具体引脚以选型手册为准。这颗芯片在工业数据采集和设备控制上非常常见因为外设丰富、功耗控制灵活尤其是它有多个 SPI、支持 DMA还有内置的 PVD 掉电检测、RTC、独立看门狗跟 MRAM 搭在一起能拼出一个完整的掉电安全存储系统。用 STM32L496AG 跑 MR25H40CDF最顺手的连接方式就是 SPI 全双工模式L496 的 SPI1 挂在 APB2 总线上时钟可以跑到 MRAM 支持的 40MHz 附近但实际工程里我通常先跑 10MHz 调试验证时序后再逐步提频。提频不是越高越好后面我会讲信号完整性的问题。选这颗 MCU 还有一个很现实的原因L4 系列的库函数和 CubeMX 生态很成熟SPI 驱动代码写起来不费劲。再加上它自带 PVD 掉电检测可以在主电源开始跌落时立刻中断把关键数据写入 MRAM。这就是“处理器负责抢时间MRAM 负责不丢数据”的经典组合。如果换一颗没有 PVD 的 MCU你还得外挂一个电压比较器来做掉电信号电路上多一堆东西。1.3 这套组合适合用在哪些产品里我实际做过的场景主要有四类你可以对照自己的产品看看有没有重合。第一类是运动控制器和伺服驱动器。这类设备需要实时记录当前位置、速度、报警状态运行中可能每毫秒更新一次而且现场经常忽然断电。以前用 EEPROM 做参数区没问题但日志区一天写几千上万次传统 EEPROM 撑不住。换成 MRAM 之后日志频率可以提得很高而且掉电后上次位置和报警码都能完整找回省去了上电重新找零位的麻烦。第二类是电力仪表和数据采集终端。电网设备对数据记录要求很苛刻事件时间戳、电压暂降波形、操作记录都要长期保存而且不允许因频繁写入导致寿命耗尽。MRAM 加上 STM32L496 的 RTC 时间戳可以做出非常可靠的电能质量事件记录模块。第三类是医疗设备和工业仪表。校准参数、操作员操作履历、自检结果这些数据修改频繁但单次数据量不大MRAM 天然适合。第四类是物联网边缘节点。设备在野外或配电柜里工作供电质量差经常瞬间断电。把采集的最后一批传感器数据先写进 MRAM主控再从容地整理上传数据不丢体验会好非常多。2. 硬件接口与接线细节别让引脚悬空毁了你的调试图2.1 封装引脚功能与关键引脚处理MR25H40CDF 是 DFN-8 封装引脚看起来不多但有两个引脚很容易被忽略就是 HOLD# 和 WP#。HOLD# 是通信暂停脚拉低后芯片会把当前的 SPI 传输状态挂起SCK 变化不影响内部状态WP# 是写保护脚配合状态寄存器的 WPEN 位可以锁住状态寄存器。这两个引脚如果悬空在工业现场受到干扰时可能被误拉低表现就是通信时好时坏、数据错位、写操作静默失败。所以硬件上我的做法很简单两个引脚都通过 10KΩ 电阻上拉到 VDD并且在 PCB 上用网标引出方便调试时飞线。如果你希望软件完全控制也可以把它们接到 STM32L496 的两个 GPIO驱动初始化时输出高电平。从抗干扰角度讲直接上拉最省事因为这样芯片上电就是正常工作状态不会出现启动时序问题。引脚功能对照如下引脚名方向接法说明CS#输入接 STM32 任意 GPIO推荐软件控制不推荐硬件 NSSSCK输入接 SPI1_SCK 引脚如 PA5SI输入接 SPI1_MOSI 引脚主发从收SO输出接 SPI1_MISO 引脚主收从发WP#输入10KΩ 上拉到 3.3V禁止悬空HOLD#输入10KΩ 上拉到 3.3V禁止悬空VDD电源3.3V就近加 0.1μF 陶瓷电容VSS地接 GND建议正下方铺实心地过孔散热2.2 电源、去耦与 DFN 焊接经验MR25H40CDF 工作电压是 3.3V范围一般是 2.7V 到 3.6V所以一定要确保 VDD 在芯片写操作期间不低于下限。这个看似简单的问题实际出的麻烦最多。曾经有一块板子SPI 读写偶尔失败查了半天最后发现是 PCB 上 3.3V 走线太长MRAM 写数据时瞬间电流把电压拉到了 2.5V 以下。加了一颗 100μF 钽电容放在芯片边上再配合 0.1μF 高频去耦问题就消失了。DFN-8 封装没有引脚伸出来是底部焊盘和四周小焊脚的结构。手工焊接时先给焊盘上一点锡用镊子夹住芯片对准烙铁温度 350°C 左右先固定一对对角脚再从侧面拖焊其余引脚。最难的是判定有没有虚焊尤其是 SO 脚因为引脚在芯片底下眼睛看不太到。我通常焊完后用万用表二极管档量 CS、SCK 对地再实测上电后 RDSR 能不能读到状态寄存器用功能验证代替目检效率高很多。另外DFN 的散热焊盘如果和地相连铺铜时要开多个过孔方便焊接时把热量带走。但注意过孔不要打在焊盘中心否则焊锡会被吸走造成芯片贴不平焊完一量某个引脚还是虚的。2.3 与 STM32L496AG 的 SPI 接线参考我用的 STM32L496AG 引脚方案是 SPI1 默认引脚组PA5 做 SCKPA7 做 MOSIPA6 做 MISO片选用 PA4。这只是参考如果你这些引脚被占用完全可以换到 SPI2 或 SPI3只要 CubeMX 里把功能映射对就行。CS# 我坚持用 GPIO 软控不用 SPI_NSS 硬件管理。原因很简单硬件 NSS 在某些模式下会自动拉低或自动控制片选和 MRAM 的指令窗口配合起来很麻烦尤其是一个 SPI 总线上只挂一个设备时软件控制反而更直观想拉低就拉低想拉高就拉高不会有隐含状态。片选信号必须是在 CS 拉低期间完整包住整条指令所以代码里我会保证 CS 低电平覆盖所有发送字节然后再拉高。接线距离方面SPI 时钟上了 10MHz 之后杜邦线已经不可靠我的习惯是 PCB 上走线控制在 5cm 以内并且避免和电源线、继电器驱动线长距离并行。SCK 和 MOSI 走线之间留一点间距否则容性耦合会导致从设备采样点偏移。两板之间用排线连接时给 SCK、CS 各加一个 33Ω 的串联电阻能明显改善过冲。3. MR25H40CDF 指令集与核心机制为什么它比 Flash 好写3.1 从操作码理解 MRAM 和 Flash 的本质区别我建议你先把 MR25H40 的操作码表打印出来放在手边写驱动的时候会发现它非常接近 SPI NOR Flash但有一个关键区别没有“擦除指令”。标准 Flash 用 0x20 擦除扇区、0xD8 擦除块、0xC7 擦除全片MRAM 全都不要。你直接发 WRITE 指令它就把数据写进去了旧值直接被覆盖。经常用到的操作码如下指令名称操作码功能说明WREN0x06写使能写状态寄存器前必须先发CS 必须拉高生效WRDI0x04写禁止关闭写使能锁存RDSR0x05读状态寄存器读取 WEL、WPEN 等状态WRSR0x01写状态寄存器配置写保护位READ0x03读数据最常用的连续读指令FAST_READ0x0B快速读多一个虚拟字节适合高速大块读WRITE0x02写数据连续写地址自动递增SFDP0x5A读 JEDEC 参数可用来探测芯片能力部分批次支持WREN 之后必须把 CS 拉高一次锁存器才会真正置位。这一点我一开始吃过亏以为可以在一条 CS 低电平周期里连续发 0x06、0x02、地址和数据后来发现有些批次能成功有些批次静默失败。正规流程是CS 低发 0x06CS 高CS 低发 0x02 地址 数据CS 高。老老实实按这条纪律写不要偷懒。3.2 状态寄存器的读取和意义MR25H40 的状态寄存器只有 8 位但工程上最有用的就是 bit0 的 WEL写使能锁存位。你执行完 WREN 之后读 RDSRWEL 应该变成 1执行 WRDI 或者完成写指令后它变成 0。所以驱动里我每次写入前会先主动发一次 WREN写入完成后延时或读一次状态寄存器确认没有异常这样从逻辑上就能提前发现硬件问题。读状态寄存器的代码非常简单CS 拉低发 0x05然后从 SO 脚读一个字节CS 拉高。但要注意读取时 MOSI 必须送一个 dummy 字节SPI 是全双工协议主机发 0x05 之后还要继续发 0x00 才能把 SO 上的数据采回来。如果用 HAL 的 TransmitReceive 函数传一个长度 2 的 buffer 就可以一次完成。3.3 地址范围和页边界问题4Mbit 换算过来是 512KB地址范围 0x00000 到 0x7FFFF正好 19 位地址用 3 个字节表示时多余的高位要填 0。READ 和 WRITE 指令都是发送操作码后紧跟三个地址字节先发高位再发低位。这里有一个隐蔽的坑连续写时地址计数器会在页边界自动回卷。标准 256 字节页也就是说如果你从地址 0x00100 写入 300 字节数据写到 0x001FF 后不会继续进 0x00200而是卷回 0x00100 把刚才写的开头部分覆盖掉。所以驱动必须做分页处理按剩余页大小切成多段写事务每一段以页边界对齐。虽然 MRAM 写入不需要擦除、不会磨损但地址回卷的机制还在不能掉以轻心。如果你用 SFDP 指令读 JEDEC 参数可以从返回表里拿到页大小、容量、命令支持情况。但我的经验是第一次调试没必要把 SFDP 做成必要流程先把 READ、WRITE、RDSR、WREN 四个指令跑通后面再做拓展。SFDP 更适合做通用驱动时用来识别不同容量的 MRAM让一套代码兼容多颗芯片。4. STM32L496AG 侧 SPI 驱动实现从 CubeMX 到可落地的 C 代码4.1 用 STM32CubeMX 快速搭建工程基础如果你还没用 CubeMX这里我给的步骤可以直接抄。新建 STM32L496AG 工程选择外部高速晶振或 HSI16PLL 倍频到 120MHz。然后在 Pinout 界面找到 SPI1模式选 Full-Duplex MasterParameter Settings 里几个关键项照着设参数值说明Frame FormatMotorolaSTM32 的 SPI 标准帧格式Data Size8 BitsMR25H40 是字节寻址First BitMSB First指令和地址都是高位在前Clock PolarityLowCPOL0空闲时 SCK 低电平Clock Phase1 EdgeCPHA0第一个边沿采样Prescaler8 或 16APB2 80MHz 下得到 10MHz 或 5MHzCRCDisable不需要 CRCNSSSoftware片选由 GPIO 软件控制GPIO 方面把 PA4 配成 Output Push Pull速度选 Medium。PA5、PA6、PA7 由 CubeMX 自动设置为 SPI 功能。HOLD# 和 WP# 如果接到了 GPIO就配成 Output 推挽并在初始化时输出高电平如果直接上拉到 VDD就不用配置。生成代码后在 main.c 的 MX_GPIO_Init 之后手动把 PA4 置高保证上电时 MRAM 没有被选中。然后写一套独立的 mram 驱动文件不要跟用户业务代码混在一起。4.2 完整读写函数我调试通过的基础版本下面这段代码是实测能用的基础版本省去了复杂的 DMA 和页管理先把读写逻辑跑通。你可以把它放在 mram.c 里直接用 HAL 库编译。/* mram.h */ #ifndef MRAM_H #define MRAM_H #include main.h #define MRAM_SIZE (512U * 1024U) /* 512KByte */ #define MRAM_PAGE_SIZE 256U #define MRAM_ADDR_MASK 0x7FFFFU #define MRAM_CMD_WREN 0x06U #define MRAM_CMD_WRDI 0x04U #define MRAM_CMD_RDSR 0x05U #define MRAM_CMD_WRSR 0x01U #define MRAM_CMD_READ 0x03U #define MRAM_CMD_FAST_READ 0x0BU #define MRAM_CMD_WRITE 0x02U extern SPI_HandleTypeDef hspi1; void mram_select(void); void mram_deselect(void); uint8_t mram_read_status(void); void mram_write_enable(void); int mram_read(uint32_t addr, uint8_t *buf, uint32_t len); int mram_write(uint32_t addr, const uint8_t *buf, uint32_t len); #endif/* mram.c */ #include mram.h #include string.h #define MRAM_CS_PORT GPIOA #define MRAM_CS_PIN GPIO_PIN_4 static SPI_HandleTypeDef *mram_spi hspi1; void mram_select(void) { HAL_GPIO_WritePin(MRAM_CS_PORT, MRAM_CS_PIN, GPIO_PIN_RESET); } void mram_deselect(void) { HAL_GPIO_WritePin(MRAM_CS_PORT, MRAM_CS_PIN, GPIO_PIN_SET); } uint8_t mram_read_status(void) { uint8_t cmd MRAM_CMD_RDSR; uint8_t dummy 0x00; uint8_t val 0x00; mram_select(); HAL_SPI_TransmitReceive(mram_spi, cmd, dummy, 1, HAL_MAX_DELAY); HAL_SPI_TransmitReceive(mram_spi, dummy, val, 1, HAL_MAX_DELAY); mram_deselect(); return val; } void mram_write_enable(void) { uint8_t cmd MRAM_CMD_WREN; mram_select(); HAL_SPI_Transmit(mram_spi, cmd, 1, HAL_MAX_DELAY); mram_deselect(); } int mram_read(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t hdr[4]; if ((addr len) MRAM_SIZE || len 0U) { return -1; } hdr[0] MRAM_CMD_READ; hdr[1] (uint8_t)((addr 16) 0xFF); hdr[2] (uint8_t)((addr 8) 0xFF); hdr[3] (uint8_t)(addr 0xFF); mram_select(); HAL_SPI_Transmit(mram_spi, hdr, 4, HAL_MAX_DELAY); HAL_SPI_Receive(mram_spi, buf, len, HAL_MAX_DELAY); mram_deselect(); return 0; } int mram_write(uint32_t addr, const uint8_t *buf, uint32_t len) { uint8_t hdr[4]; uint32_t written 0; uint32_t chunk; uint32_t page_remain; if ((addr len) MRAM_SIZE || len 0U) { return -1; } while (written len) { page_remain MRAM_PAGE_SIZE - (addr % MRAM_PAGE_SIZE); chunk len - written; if (chunk page_remain) { chunk page_remain; } mram_write_enable(); hdr[0] MRAM_CMD_WRITE; hdr[1] (uint8_t)((addr 16) 0xFF); hdr[2] (uint8_t)((addr 8) 0xFF); hdr[3] (uint8_t)(addr 0xFF); mram_select(); HAL_SPI_Transmit(mram_spi, hdr, 4, HAL_MAX_DELAY); HAL_SPI_Transmit(mram_spi, (uint8_t *)(buf written), chunk, HAL_MAX_DELAY); mram_deselect(); /* 给内部写入一点裕量避免紧接着的读操作碰到内部动作 */ for (volatile uint32_t i 0; i 200; i) { __NOP(); } addr chunk; written chunk; } return 0; }简单说明几个点。读函数里先发指令头再直接用 HAL_SPI_Receive 收数据。因为全双工模式下 Receive 期间主机会持续送 0x00对 MRAM 来说那只是 dummy不影响读取。写函数强制按 256 字节页切分保证不会踩到地址回卷。写完一块后加了极短的循环延时我实测在 10MHz 时钟下不加也能读到正确数据但加上更稳妥尤其是在时钟提高到 20MHz 以后。4.3 提频和 DMA 优化的正确姿势基础功能调通后如果你的日志频率很高可以优化成 DMA 方式。做法是把 CS 拉低用 HAL_SPI_Transmit_DMA 发送指令头等发送完成中断后再启动 HAL_SPI_Receive_DMA 接收数据全部完成后在回调里把 CS 拉高。这样 CPU 在大量数据搬运时不会被 SPI 阻塞可以把时间用来处理业务逻辑。提频的时候不要一步到位。从 10MHz 提到 20MHz跑一遍连续读写测试核对整个 512KB 区域的随机数据回读稳定后再试 40MHz。MR25H40 支持的最高时钟能到 40MHz 左右但不同批次和不同 PCB 质量会有差异。信号质量主要看三样SCK 上升沿是否干净、MISO 数据建立时间是否够、CS 与 SCK 之间的建立保持是否满足手册。有逻辑分析仪或示波器就抓一下波形没有的话就靠回读测试压测。5. 掉电安全、日志方案与可靠性设计不丢数据才是关键5.1 掉电瞬间如何把数据真正写进去MRAM 的好处是写入立即非易失但在实际电路里掉电保护不是芯片一个人的事而是电源检测、MCU 调度、存储动作三者之间的配合。STM32L496 的 PVD 掉电检测模块可以编程阈值比如设成 2.9V 左右。当 VDD 掉到阈值以下PVD 中断触发主程序停止登录新任务把当前的关键状态压缩成一条记录直接写进 MRAM 预留给紧急日志的区域。写操作本身只需要几十微秒真正的问题是从电源开始跌落到 MCU 完全停止工作中间到底有多少时间。我的做法是在电源输入端放一个 220μF 到 1000μF 的电解电容具体容量看掉电检测到程序完成写入的时间。实测在 24V 转 3.3V 的板子上220μF 电容能撑约 30ms足够写完几十条日志了。如果你用的是开关电源输入侧电容效果不如输出侧直接并电容这个要测试时确认。而且这里有个小细节掉电中断里不要再去做 Flash 固件升级、不要去等待网络重连所有不必要的外设都先关掉只保留 SPI、GPIO 和必要时钟。把 120MHz 主频暂时降到最低? 不需要MCU 跑得越快写日志越早完成所以保持主频反而更好。关键是 PVD 事件响应要快不要在中断里做复杂的判断。5.2 日志循环写入方案MRAM 上不需要磨损均衡传统 NAND/NOR 存储做日志都要设计磨损均衡和坏块管理因为擦写寿命有限。MRAM 没有这个压力日志方案可以非常直接用固定的一块区域定义一个环形索引头部存起始序号后续存日志条目写完尾部就回到头部覆盖最老的条目。这个方案让代码简单很多而且寿命不用算10 的 14 次方次写入意味着哪怕你每秒写一条连续跑几百年都碰不到寿命极限。日志条目我通常定义成 16 字节对齐的结构体包含魔数、序号、RTC 时间戳、关键变量、CRC。启动时从上电后第一次读区域头部根据索引找最后一条有效记录然后继续写。掉电恢复后按魔数和序号判断哪些记录是完整的序号断了的说明掉电时正在写直接跳过。这里要强调一次MRAM 写入虽然快但如果你在掉电瞬间只写了一半字节就断电了那这条记录也是不完整的。所以日志条目要在最后一个字段放 CRC启动时先校验。这个设计和 Flash 场景一样不是写完的每条数据都一定有效而是通过校验筛选有效记录。5.3 双镜像和重启恢复策略对容错要求高的设备我还会在 MRAM 里开两个镜像区A 区和 B 区。每次启动时读 A 区头部序号再读 B 区序号大的且 CRC 通过的就是最近一份有效数据。写入时交替选择 A 或 B 作为当前目标这样即使掉电发生在写镜像过程中另一个镜像仍然完整。恢复策略是启动时比较两个镜像自动把更新的一份复制到另一份保证两边同步。这套双镜像方案在 MRAM 上实现成本很低因为容量足够而且镜像复制不需要先擦除再写扫描一遍很快。如果在 Flash 上做双镜像每次切换前都得考虑擦除哪个扇区麻烦得多。这也是我把 MRAM 用成“掉电安全的配置仓库”的原因。5.4 PVD 之外的第二重保护滤波器与禁止写入窗口PVD 中断是软件层面的硬件层面还要防一个东西电源毛刺导致 MCU 误触发复位或 SPI 时序错乱在乱写地址。我的做法是在 MRAM 的 WP# 引脚接一个 GPIO平时输出低电平只有执行写操作前才置高。这样就可以用“硬件写保护门”把误操作挡在外面。掉电瞬间PVD 一旦触发第一时间把 WP# 拉低此时即使主程序跑飞也写不了 MRAM。这个习惯是从工业设备抗干扰经验里带过来的。现场设备可能因为接触器吸合、电机启动产生强烈的电源干扰MCU 程序走飞的可能性不是没有。有了 WP# 的硬件保护即使 SPI 线上一堆毛刺MRAM 也只是处于只读状态数据区不会被污染。6. 常见问题与现场排查笔记6.1 通信时好时坏先检查引脚而不是先怀疑代码我遇到过最典型的案例是代码没问题换一块板子就数据全乱。最后发现是 DFN 封装的 MISO 引脚虚焊。DFN 焊接后引脚藏在封装下面肉眼很难判断上电后 RDSR 读到 0x00 或者 0xFF十有八九是电气连接问题。这时候先量 CS、SCK、SI、SO 四个引脚的对地二极管值再量 VDD 和地之间有没有短路。排除硬件问题再动软件。另一个常见原因是 HOLD# 引脚悬空。HOLD# 一旦被干扰拉低芯片的 SPI 状态机就会冻结表现为主机还在发数据但 SO 不再回数据读出来一大片 0x00。排查方法很简单把 HOLD# 直接短路到 VDD看问题是否消失。所以我前面反复强调 HOLD# 必须上拉这个坑我至少踩过两次。6.2 写不进去或读出还是旧值优先查 WREN 和 WP#MRAM 的写操作前必须发 WREN而且 CS 拉高一次锁存器才会真正置位。如果你发现 write 返回成功但重新读出来还是旧数据首先在写函数里加一个调试断言写完后立刻读一下状态寄存器看 WEL 位有没有在写指令前被置 1。如果 WEL 一直为 0再查 WP# 是不是被拉低了或者在 WREN 和 WRITE 之间插入了不该有的 SPI 通信。还有一点有些初始化流程里会把 GPIO 的复用功能配错导致 MOSI 没有真正输出到 SI 脚。检查 CubeMX 的引脚映射是否和硬件一致用逻辑分析仪看 MOSI 上到底有没有发出指令头。没有逻辑分析仪就退而求其次写代码时不断翻转一个调试 GPIO配合示波器的两路通道来比对待测信号也能定位问题。6.3 跨页写入导致数据被覆盖这个坑最隐蔽因为很多时候发生在大容量连续写入时。我在一个项目里把 512KB 的 MRAM 整个当作环形缓冲区写入时按简单线性地址递增没做分页结果写到每个 256 字节页末尾下一条数据就把页内开头的旧数据覆盖了整个日志区错乱得一塌糊涂。排查办法是先写一段已知模式的数据例如在地址 0x000F0 写入 64 字节 0x5A然后跨到 0x00100 继续写 64 字节 0xA5最后回读看 0x000F0 区域的数据是否还在。如果被破坏了说明没做分页处理。修复方法就是驱动里按页边界切分我在 4.2 节的代码里已经做了。我建议所有写 MRAM 的工程师都把“地址自动递增 页内回卷”当成默认行为来对待不管数据手册里怎么描述驱动层一律按 256 字节页边界做保护这样即使换其他 SPI 存储器也不会踩坑。6.4 提频后数据偶发错位的波形解释频率提到 20MHz 以上时偶发的单个字节错位很可能是 MISO 信号在高速下受到串扰。SPI 是全双工MOSI 在写数据时是高电平翻转此时 MISO 正在输出读数据两根线如果平行走线过长信号会相互耦合。处理方法是把 SCK 和 MOSI、MISO 分开走线中间加地线隔离或者把 SPI 时钟频率降回 10MHz。因为这种偶发错位非常难在静态测试中复现我通常是做长时间压力测试随机写 4KB 数据再回读比对连续跑几万次。如果出现一次失败就降低频率重新测试。用这个办法筛选出当前硬件能稳定工作的最高频率然后留出 20% 的降额作为量产标准。有一个技巧实测时可以用 DMA 连续读写一整个 512KB 区域然后对整个区域做 CRC 校验。相比单条记录读写这种大块压力测试更容易暴露信号完整性问题。6.5 快速定位问题速查表如果你在现场或者调试台上遇到类似现象可以直接按这个表对号入座。现象优先怀疑解决办法读回全 0x00MISO 虚焊或 HOLD# 拉低量引脚HOLD# 上拉读回全 0xFFSPI 模式不对或 CS 未选中检查 CPOL/CPHA量片选电平写入后读回旧值漏发 WREN 或 WP# 拉低补 WREN上拉 WP#跨页区域数据错乱地址回卷未处理按 256 字节页切分写入高速下偶发错位信号串扰或采样沿不稳降频、加大地线间距掉电后最后一条丢失PVD 响应太慢或电容不足加储能电容压缩中断处理时间最后再分享一个我自己的习惯MR25H40CDF 和 STM32L496AG 这个组合我每次移植到新产品时都会先写一个自检函数上电后对整片 MRAM 做 0x55/0xAA 翻转写入再回读通过后才进入正式业务。这个自检过程只要几百毫秒但能把封装虚焊、接线错误、SPI 配置错误全部挡在出厂前。尤其 DFN 封装的小批量手工焊接自检的价值比任何口头强调都大。MRAM 用着顺手前提是底层链路必须扎实把基础打牢后面做掉电日志、运行数据记录都会非常省心。
返回列表