1. 项目缘起与方案选型1.1 为什么要在工业场景里折腾 MRAM 和 8 位单片机做工业设备和嵌入式采集终端的兄弟应该都有个共同的痛点数据存储这块选型永远在“掉电丢数据”和“擦写寿命不够”之间反复横跳。我手头这个项目是一个典型的工业现场数据记录仪要求每 200ms 采集一次传感器数据断电后关键配置和最近一段时间的运行日志不能丢而且设备预期寿命是 10 年以上工作温度范围覆盖 -40℃ 到 85℃。一开始考虑过几种方案。EEPROM 擦写寿命一般标称 100 万次按 200ms 写一次算一天就是 43.2 万次两天多就把一个扇区写废了就算做磨损均衡也撑不了太久。FRAM 铁电存储器倒是擦写寿命够10^12 次以上但容量普遍偏小、价格偏高而且供货周期不太稳定。NOR Flash 擦写寿命 10 万次左右还得整块擦除写之前要搬数据逻辑复杂掉电窗口期还容易出问题。最后锁定了MRAM磁阻随机存储器具体型号是MR25H40CDF4Mbit 容量SPI 接口擦写寿命标称 10^14 次以上写入不需要擦除字节级随机写入掉电数据保持 20 年以上。这个特性对工业数据记录来说几乎是量身定做的。主控这边选的是PIC18F46208 位单片机64KB Flash、3968 字节 RAM、1KB EEPROM自带 MSSP 模块支持 SPI 主模式工作温度 -40℃ 到 125℃ 的工业级版本。选它不是因为性能多强而是因为便宜、皮实、开发工具链成熟而且这个项目对算力要求不高主要逻辑就是定时采集、打包、存储、偶尔读出来通过串口上报。用 8 位机把成本压下来把预算留给 MRAM 和工业级外围电路这个取舍在量产项目里是很现实的。提示MRAM 虽然贵但在“高频写入 掉电保持 长寿命”这三个条件同时成立时综合成本反而比 EEPROM 磨损均衡 超级电容的方案更低因为省掉了电容、省掉了复杂的均衡算法也省掉了现场返修的成本。1.2 整体架构长什么样整个系统的数据流是这样的传感器通过 ADC 或者 I2C 接口把数据给到 PIC18F4620单片机在定时器中断里做采集主循环里做数据打包和存储管理通过 MSSP 模块的 SPI 接口和 MR25H40CDF 通信。MRAM 里划分了几个区域配置区、日志区、校准数据区。配置区和校准数据区是低频写入日志区是高频循环写入。上位机通过 UART 转 USB 和单片机通信可以下发读取指令把 MRAM 里的数据读出来做分析。整个链路不复杂但每个环节都有坑尤其是 SPI 时序和 MRAM 的写入时序后面会详细说。这里先给一个整体的引脚分配表方便对照PIC18F4620 引脚功能连接到 MR25H40CDFRC3 (SCK)SPI 时钟SCKRC5 (SDO)SPI 数据输出SIRC4 (SDI)SPI 数据输入SORA5片选软件控制CSRC2状态指示无RC6/RC7UART上位机片选这里我特意用了普通 GPIO 而不是 MSSP 的硬件 SS 引脚原因后面讲 SPI 配置的时候会展开简单说就是硬件 SS 在多主或者需要精细控制时序的场景下不够灵活。2. MR25H40CDF 与 PIC18F4620 的核心细节拆解2.1 MR25H40CDF 的存储结构与指令集MR25H40CDF 是 4Mbit 的 MRAM换算成字节是 512KB地址线需要 19 位2^19 524288。它内部按页组织每页 256 字节总共 2048 页。SPI 接口支持模式 0 和模式 3最高时钟频率 40MHz不过在 3.3V 供电和较长走线的情况下我实际跑在 10MHz 比较稳。指令集和标准 SPI Flash 很像但有几个关键区别。读指令是 0x03写指令是 0x02写使能是 0x06读状态寄存器是 0x05写状态寄存器是 0x01。注意 MRAM 没有擦除指令这是它和 Flash 最大的区别——你不需要先擦后写直接写就行。这个特性简化了驱动逻辑但也意味着你不能像操作 Flash 那样“先擦一整块再慢慢写”每次写都是独立的字节操作。状态寄存器的 bit0 是 WIPWrite In Progress写操作发起后需要轮询这个位直到它变 0 才能发下一条指令。虽然 MRAM 的写入速度很快字节写入时间在纳秒级但 SPI 传输本身需要时间加上内部时序实际轮询还是必要的。地址是 3 字节传输高位在前。比如要访问地址 0x000100先发 0x00再发 0x01再发 0x00。这个顺序搞反了会写到错误的地址而且因为 MRAM 写入不需要擦除写错了就是直接覆盖数据就没了调试的时候特别容易在这里翻车。2.2 PIC18F4620 的 MSSP 模块配置要点PIC18F4620 的 MSSP 模块可以配置成 SPI 主模式或从模式这里用主模式。关键寄存器是 SSPCON1、SSPSTAT、SSPBUF 和 SSPADD主模式下 SSPADD 不用。SSPCON1 的 bit5 是 SSPEN使能位bit4 是 CKP时钟极性bit3-0 是 SSPM3:SSPM0模式选择。SPI 主模式且时钟由单片机产生的配置是 SSPM 0b0000 或 0b0001区别在于时钟空闲电平。MR25H40CDF 支持模式 0CPOL0CPHA0和模式 3CPOL1CPHA1我一般用模式 0配置成 SSPM 0b0000CKP 0。SSPSTAT 的 bit7 是 SMP采样相位bit6 是 CKE时钟边沿选择。模式 0 下 SMP 0CKE 0。这几个位配错了数据会整体偏移一位读出来的全是乱码而且现象很迷惑——有时候读 ID 是对的读数据就错因为 ID 指令的时序容错性高一些。时钟频率由 SSPADD 决定公式是 Fosc / (4 * (SSPADD 1))。假设晶振 40MHz要得到 10MHz 的 SPI 时钟SSPADD 40M / (4 * 10M) - 1 0。如果要 5MHzSSPADD 1。这里注意 PIC18F4620 的 SPI 时钟是 Fosc/4 的整数分频不能任意设置所以选晶振的时候要提前算好。注意PIC18F4620 的 MSSP 在发送和接收时共用 SSPBUF写 SSPBUF 启动发送读 SSPBUF 获取接收数据。发送和接收是同时进行的所以读一个字节实际上是“发一个哑字节同时收一个字节”。这个机制如果不理解写驱动的时候会卡很久。2.3 硬件片选与软件片选的取舍前面提到我用了普通 GPIO 做片选这里展开说一下原因。MSSP 的硬件 SS 引脚在从模式下有用主模式下它只是一个普通输入而且如果配置成从模式使能SS 拉低会触发接收容易误触发。更重要的是MRAM 的指令序列是“片选拉低 - 发指令 - 发地址 - 发/收数据 - 片选拉高”整个序列必须原子化中间不能被中断打断。用软件 GPIO 控制片选可以在序列开始前关中断结束后开中断保证时序完整。如果用硬件 SS虽然也能工作但在多任务或者中断频繁的场景下片选信号可能被意外拉高导致指令序列断裂MRAM 进入不确定状态。工业现场电磁干扰大片选线走线长的话更容易出问题。所以我的习惯是SPI 主模式下片选一律用软件 GPIO硬件 SS 引脚悬空或者配置成普通 IO。片选的操作也有讲究。拉低之后不能立刻发时钟要等几个纳秒让 MRAM 检测到片选有效拉高之前要确保最后一个时钟沿已经完成数据已经锁存。在 10MHz 时钟下一个时钟周期是 100nsPIC18F4620 的指令周期是 100ns40MHz 晶振4 分频所以一个 NOP 差不多就是一个时钟周期。我一般在片选拉低后加两个 NOP拉高前也加两个 NOP实测下来很稳。3. 实操过程与核心环节实现3.1 硬件连接与电源处理先看硬件。MR25H40CDF 是 3.3V 供电PIC18F4620 我用的也是 3.3V 版本注意不是 5V 版本5V 版本和 3.3V 的 MRAM 直接连会烧 MRAM。电源这块MRAM 的 VCC 和 VSS 之间必须加 0.1uF 陶瓷电容越靠近芯片越好我一般放在芯片背面。如果走线超过 5cm再加一个 1uF 的钽电容。SPI 四根线SCK、SI、SO、CS尽量等长如果做不到等长至少保证 SCK 和 CS 不要有太大的长度差。SCK 是时钟边沿敏感走线长了会有反射和串扰导致数据采样错误。我在一个项目里因为 SCK 走了 15cm 的排线10MHz 下误码率很高降到 5MHz 才稳定。后来改成短线加地线隔离10MHz 就没问题了。CS 线要加上拉电阻10K 到 47K 都行保证单片机复位或者 IO 高阻时 MRAM 不会被误选中。SI 和 SO 不需要上拉但如果有长线可以加 22 欧姆的串联电阻做阻抗匹配。提示MRAM 的写保护引脚WP和保持引脚HOLD如果不用WP 接 VCCHOLD 接 VCC。这两个引脚悬空的话MRAM 可能进入写保护或者保持状态表现为写不进去或者读出来全是 0xFF排查起来很费时间。3.2 SPI 底层驱动代码实现底层驱动我分成几个函数SPI 初始化、SPI 收发一个字节、MRAM 写使能、MRAM 读状态、MRAM 读数据、MRAM 写数据。下面逐个说。SPI 初始化void SPI_Init(void) { TRISC3 0; // SCK 输出 TRISC5 0; // SDO 输出 TRISC4 1; // SDI 输入 TRISA5 0; // CS 输出 CS 1; // 片选拉高不选中 SSPCON1 0x00; // 先清零 SSPSTAT 0x00; SSPCON1bits.SSPM 0b0000; // SPI 主模式时钟 Fosc/4 SSPCON1bits.CKP 0; // 空闲低电平 SSPSTATbits.CKE 0; // 空闲到活跃沿发送 SSPSTATbits.SMP 0; // 中间采样 SSPADD 0; // 40MHz / (4*(01)) 10MHz SSPCON1bits.SSPEN 1; // 使能 MSSP }这里 SSPADD 的计算前面说过了40MHz 晶振下 SSPADD0 得到 10MHz。如果晶振是 10MHzSSPADD0 得到 2.5MHz要得到 5MHz 就得 SSPADD0 但晶振换 20MHz或者用 PLL。PIC18F4620 有 4 倍频 PLL可以把 10MHz 晶振倍到 40MHz这样 SSPADD0 就是 10MHz。SPI 收发一个字节uint8_t SPI_ExchangeByte(uint8_t data) { SSPBUF data; while(!SSPSTATbits.BF); // 等待接收完成 return SSPBUF; }这个函数是阻塞式的发一个字节同时收一个字节。注意 BF 位是 SSPSTAT 的 bit0接收缓冲满时置 1。在高速时钟下这个轮询很快不会成为瓶颈。MRAM 写使能void MRAM_WriteEnable(void) { CS 0; NOP(); NOP(); SPI_ExchangeByte(0x06); CS 1; NOP(); NOP(); }写使能指令是 0x06发完之后 MRAM 内部的写使能锁存器置位下一次写操作才有效。注意每次写之前都要发写使能MRAM 不会记住上一次的写使能状态。这个和 EEPROM 类似但比 Flash 简单Flash 还要考虑扇区擦除的写使能。MRAM 读状态uint8_t MRAM_ReadStatus(void) { uint8_t status; CS 0; NOP(); NOP(); SPI_ExchangeByte(0x05); status SPI_ExchangeByte(0xFF); CS 1; NOP(); NOP(); return status; }读状态指令是 0x05后面跟一个哑字节返回状态寄存器的值。bit0 是 WIP写操作进行中。虽然 MRAM 写入很快但保险起见写完一个字节后还是轮询一下 WIP确保内部时序完成。MRAM 写数据void MRAM_WriteData(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t i; MRAM_WriteEnable(); CS 0; NOP(); NOP(); SPI_ExchangeByte(0x02); SPI_ExchangeByte((addr 16) 0xFF); SPI_ExchangeByte((addr 8) 0xFF); SPI_ExchangeByte(addr 0xFF); for(i 0; i len; i) { SPI_ExchangeByte(buf[i]); } CS 1; NOP(); NOP(); while(MRAM_ReadStatus() 0x01); // 等待写完成 }地址是 24 位但 MR25H40CDF 只有 19 位地址高 5 位是无关位发 0 就行。写指令 0x02 后面跟 3 字节地址然后连续发数据地址会自动递增。MRAM 支持页内连续写跨页的话地址会回绕到页首这个和 Flash 类似但 MRAM 不需要擦除所以跨页写不会出错只是地址回绕。我一般保证一次写的长度不超过页边界避免回绕带来的逻辑混乱。MRAM 读数据void MRAM_ReadData(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t i; CS 0; NOP(); NOP(); SPI_ExchangeByte(0x03); SPI_ExchangeByte((addr 16) 0xFF); SPI_ExchangeByte((addr 8) 0xFF); SPI_ExchangeByte(addr 0xFF); for(i 0; i len; i) { buf[i] SPI_ExchangeByte(0xFF); } CS 1; NOP(); NOP(); }读指令 0x03后面跟地址然后连续读地址自动递增。读操作不需要写使能也不需要等待 WIP。3.3 数据存储区的划分与管理512KB 的 MRAM 不能随便乱写我把它划分成几个区域区域起始地址大小用途写入频率配置区0x000004KB设备参数、阈值极低校准区0x010004KB传感器校准系数极低日志区0x02000480KB运行日志、采集数据高备份区0x7A00024KB配置和校准的备份极低日志区采用循环写入的方式一个写指针从 0x02000 开始写到 0x7A000 后回绕到 0x02000。每条日志记录包含时间戳、传感器数据、状态标志固定 32 字节。480KB / 32 15360 条记录按 200ms 一条算可以存 51 分钟左右。如果现场需要更长的记录时间可以降低采集频率或者压缩数据。配置区和校准区各存两份一份在主地址一份在备份地址读取的时候两份都读做校验如果主份损坏就用备份恢复。MRAM 本身可靠性很高但工业现场有强电磁干扰SPI 传输过程中可能出错所以软件层面的校验还是必要的。每条日志记录的最后 2 个字节是 CRC16 校验读取的时候先校验再使用。CRC16 用查表法实现速度快占用空间小。PIC18F4620 的 Flash 有 64KB放一个 512 字节的 CRC 表绰绰有余。注意日志区的写指针本身也要存储我把它存在配置区的一个固定地址每次写日志前先更新写指针再写日志。这样如果写日志过程中掉电写指针已经更新下次上电会从新的位置继续写不会覆盖旧数据。但这样会有一个记录的空洞读取的时候遇到全 0xFF 的记录就跳过。3.4 掉电保护与数据完整性工业现场掉电是常态掉电保护必须做。硬件上我在电源输入端加了一个 1000uF 的电解电容配合一个二极管做隔离掉电后电容能维持单片机工作 50ms 左右。这 50ms 足够单片机完成当前写操作并保存关键状态。软件上掉电检测用 ADC 监测电源电压当电压低于 3.0V 时触发中断中断里立刻停止采集把当前写指针和状态标志写入配置区然后进入低功耗模式。注意这个中断的优先级要最高不能被其他中断打断。MRAM 的写入本身是原子的一个字节的写入要么完成要么没完成不会出现半写状态。但 SPI 传输是多字节的如果传输过程中掉电可能只写了一部分。所以我在每条记录的最后写一个“有效标志”只有整条记录写完了才写这个标志。读取的时候先检查有效标志无效就跳过。这个“有效标志”我放在记录的第一个字节写记录的时候先写 0x00写完其他数据后再改成 0xA5。读取的时候如果第一个字节不是 0xA5就认为这条记录无效。这样即使掉电导致记录不完整也不会把脏数据当成有效数据。4. 常见问题与排查技巧实录4.1 SPI 通信失败的症状与排查SPI 通信失败是最常见的问题症状五花八门我整理了一个排查表症状可能原因排查方法解决方法读出来全是 0xFF片选没拉低、MRAM 没供电、SPI 模式错用示波器看 CS 和 SCK检查片选 GPIO 配置、供电、SPI 模式读出来全是 0x00SO 线没接好、MRAM 损坏量 SO 线通断重新焊接、更换 MRAM读出来数据错位SPI 模式错、采样相位错对比示波器波形和手册调整 CPOL/CPHA写进去读出来不对地址发错、写使能没发读状态寄存器检查地址字节顺序、每次写前发写使能偶尔出错时钟太快、走线太长、干扰降低时钟测试降频、加屏蔽、加滤波电容我遇到最多的是“读出来全是 0xFF”十次有八次是片选没拉低。因为 MRAM 的 CS 是高电平有效不选中低电平选中如果 GPIO 初始化的时候忘了拉高上电后 CS 是低电平MRAM 一直被选中但 SCK 没有时钟MRAM 处于等待状态读出来就是 0xFF。这个用示波器一看就知道CS 一直是低电平。还有一个坑是 SPI 模式。MR25H40CDF 支持模式 0 和模式 3但有些批次的芯片对模式 0 的时序要求更严格如果 CKP 和 CKE 配错读 ID 可能正常读数据就错。我的经验是先用模式 0如果读数据不稳定换模式 3 试试。模式 3 是 CPOL1CPHA1空闲高电平第二个边沿采样。4.2 写入失败的几种典型情况写入失败比读取失败更难排查因为读操作不依赖写使能写操作依赖。常见情况有第一种写使能没发或者发完被其他操作打断。MRAM 的写使能是一个锁存器发 0x06 之后置位执行一次写操作后自动清零。如果发完写使能后片选被拉高又拉低或者中间插入了其他指令写使能可能失效。我的做法是写使能和写数据之间不插入任何其他操作片选保持低电平一气呵成。第二种WIP 位没等。虽然 MRAM 写入很快但 SPI 传输 3 字节地址加 N 字节数据需要时间如果写完立刻发下一条指令MRAM 可能还在处理上一条。我一般在写操作后轮询 WIP直到它为 0。实测下来10MHz 时钟下写 32 字节数据WIP 置位时间大约 2us轮询几次就过去了。第三种地址越界。MR25H40CDF 是 512KB地址范围 0x00000 到 0x7FFFF。如果地址超过 0x7FFFF高位的地址会被忽略实际写入的地址是 addr 0x7FFFF。这个在调试的时候容易忽略以为写到了 0x80000实际写到了 0x00000把配置区覆盖了。所以地址计算一定要用 uint32_t并且做边界检查。第四种电源电压不够。MRAM 的写入电压要求比读取高如果电源电压低于 2.7V写入可能失败但读取正常。工业现场如果电源纹波大或者用了长线供电到了 MRAM 这边电压可能跌到 2.5V 以下。我的做法是在 MRAM 的 VCC 引脚就近加一个 10uF 的钽电容配合 0.1uF 陶瓷电容保证写入瞬间的电流供应。4.3 数据保持与寿命的实测数据MRAM 标称数据保持 20 年擦写寿命 10^14 次。这个数字看起来很夸张但实际使用中影响寿命的主要是写入频率和温度。我在实验室做了一组加速测试85℃ 下连续写入 100 万次然后读取校验没有发现错误。常温下写入 1000 万次也没有错误。当然这个测试样本只有 5 片不能代表所有批次但至少说明 MRAM 的可靠性远高于 EEPROM 和 Flash。数据保持方面我在 85℃ 下烘烤了 1000 小时相当于常温下 10 年左右的等效时间读取数据没有丢失。这个测试还在继续目前没有发现问题。相比之下EEPROM 在 85℃ 下数据保持时间会大幅缩短有些型号标称 85℃ 下只有 1 年工业现场根本扛不住。提示MRAM 的寿命虽然长但也不是无限的。10^14 次是按字节算的如果频繁写同一个字节理论上可以写 10^14 次。但实际使用中SPI 传输和单片机处理的时间远大于 MRAM 的写入时间所以瓶颈不在 MRAM而在单片机和通信。按 200ms 写一次算一天 43.2 万次10^14 次可以写 6 亿多天完全不用担心。4.4 工业现场的电磁干扰与防护工业现场电磁干扰大SPI 通信容易受干扰。我遇到过几次现场数据出错排查下来都是干扰导致的。防护措施有几个第一SPI 走线尽量短最好在同一块 PCB 上不要用排线连接。如果必须用排线SCK 和 CS 各用一根地线隔离SI 和 SO 也各用一根地线隔离形成“信号-地-信号-地”的结构。第二MRAM 的电源引脚加磁珠和电容滤除高频干扰。磁珠选 100MHz 下阻抗 600 欧姆的电容用 0.1uF 和 10uF 并联。第三软件上做数据校验和重传。每条记录写完后立刻读回来校验如果校验失败就重写。读取的时候如果 CRC 校验失败就跳过这条记录并在日志里记录错误次数。如果错误次数超过阈值触发报警。第四SPI 时钟不要太高。10MHz 在实验室没问题但现场如果干扰大降到 5MHz 甚至 2.5MHz 更稳。速度换可靠性在工业场景里是值得的。5. 性能优化与扩展思路5.1 提高写入效率的几个技巧虽然 MRAM 写入很快但 SPI 传输和单片机处理还是瓶颈。我做了几个优化第一批量写入。不要每采集一个数据就写一次 MRAM而是先在 RAM 里缓存攒够 32 字节或者 64 字节再一次性写入。这样减少了片选操作和写使能操作的次数整体效率提高很多。PIC18F4620 有 3968 字节 RAM缓存 64 字节绰绰有余。第二用 DMA。PIC18F4620 没有 DMA但可以用 MSSP 的中断来做后台传输。不过 8 位机的 MSSP 中断处理开销不小实测下来还不如阻塞式快。所以这个项目里我没用中断直接用阻塞式简单可靠。第三减少 WIP 轮询。MRAM 写入很快如果连续写多个字节可以写完最后一个字节后再轮询 WIP中间的字节不需要每次轮询。我试过写 32 字节只在最后轮询一次和每字节轮询一次数据都正确但速度快了大约 20%。第四地址递增模式。MRAM 支持连续写地址自动递增所以写连续地址的时候不需要每次发地址只需要发一次起始地址然后连续发数据。这个在写日志的时候特别有用日志记录是连续存放的一次写多条记录只需要发一次地址。5.2 从 8 位机升级到 32 位平台的考虑这个项目用的是 PIC18F46208 位机够用但不够快。如果数据量更大或者需要更复杂的处理可以考虑升级到 32 位平台比如 STM32 或者 ESP32。升级的时候SPI 驱动逻辑基本不变但要注意几点第一32 位平台的 SPI 时钟更高STM32 可以到 50MHz 以上ESP32 可以到 80MHz。MR25H40CDF 最高支持 40MHz所以时钟要限制在 40MHz 以内。高速时钟下PCB 走线要按高速信号处理阻抗匹配、等长、包地都要考虑。第二32 位平台的 GPIO 翻转速度更快片选的控制更精细。但也要注意太快了反而可能违反 MRAM 的时序要求比如片选拉低到第一个时钟沿之间的时间MRAM 要求最小 5ns太快了可能不满足。所以高速平台上反而要加延时。第三32 位平台通常有 DMA可以用 DMA 做 SPI 传输进一步降低 CPU 占用。但 DMA 和片选的控制要配合好片选拉低、启动 DMA、DMA 完成中断里拉高片选这个流程要仔细调试。5.3 文件系统与数据管理如果数据量很大裸写 MRAM 管理起来麻烦可以考虑上文件系统。但 8 位机上跑文件系统不现实资源不够。32 位平台上可以跑 FatFS 或者 LittleFS把 MRAM 当成块设备上层用文件系统管理。这样读写数据就像读写文件一样方便不用自己管理地址和指针。不过文件系统也有开销FatFS 需要至少 512 字节的扇区缓冲LittleFS 需要更多的 RAM。而且文件系统的写入不是原子的掉电可能导致文件系统损坏。所以如果对掉电保护要求高还是裸写更可靠自己管理地址和校验。我的建议是数据量小、实时性要求高、掉电保护要求高用裸写数据量大、需要灵活管理、掉电保护要求一般用文件系统。这个项目属于前者所以用裸写。6. 实操心得与避坑清单6.1 调试工具与手段调试 SPI 和 MRAM最有用的工具是逻辑分析仪和示波器。逻辑分析仪可以抓 SPI 的时序看指令、地址、数据对不对。示波器可以看信号质量有没有过冲、振铃、毛刺。我用的是一款 8 通道的逻辑分析仪配合开源软件抓 SPI 很方便。如果没有逻辑分析仪可以用单片机的 UART 打印调试信息。比如每次读写 MRAM 之前把指令、地址、数据通过 UART 发到电脑上看流程对不对。这个方法虽然原始但很有效尤其是在没有专业工具的情况下。还有一个技巧是用 MRAM 的读 ID 指令。MR25H40CDF 有一个读 ID 指令 0x9F返回厂商 ID 和设备 ID。厂商 ID 是 0xE0设备 ID 是 0x13。上电后先读 ID如果读对了说明 SPI 通信基本正常再往下调试。如果读 ID 都不对那就先解决 SPI 通信问题不要急着读写数据。6.2 常见坑与避坑清单我整理了一个避坑清单都是实际踩过的坑片选初始化忘了拉高上电后 MRAM 一直被选中读出来全是 0xFF。SPI 模式配错读 ID 正常但读数据错位。地址字节顺序搞反写到了错误的地址覆盖了配置区。写使能没发或者发完被中断打断写不进去。WIP 没等连续写的时候后一条覆盖前一条。电源电压不够写入失败但读取正常。走线太长高速时钟下数据出错。WP 和 HOLD 引脚悬空MRAM 进入写保护或保持状态。日志写指针没保存掉电后从头写覆盖了旧数据。CRC 校验没做脏数据被当成有效数据。这些坑每一个都花了我至少半天时间排查。现在有了这个清单新项目直接对照检查能省很多时间。6.3 代码组织与可移植性最后说一下代码组织。SPI 底层驱动和 MRAM 驱动我分成两个文件spi.c 和 mram.c。spi.c 只负责 SPI 初始化、收发字节、片选控制不涉及 MRAM 的具体指令。mram.c 调用 spi.c 的接口实现 MRAM 的读写、写使能、读状态等。这样分层的好处是如果换单片机或者换 SPI 接口只需要改 spi.cmram.c 不用动。mram.c 里的地址和长度都用 uint32_t避免 8 位机上 int 是 16 位导致的溢出。PIC18F4620 的 int 是 16 位如果地址用 int超过 32767 就溢出了。这个坑我在早期项目里踩过地址算着算着就变成负数了写到了错误的地方。还有所有的延时都用 NOP 或者空循环不要用库函数的 delay因为库函数的 delay 在不同优化等级下时间不一样。SPI 的时序要求很严格延时不准会导致通信失败。我一般在片选操作前后加固定的 NOP数量根据时钟频率调整10MHz 下加 2 个 NOP5MHz 下加 4 个 NOP以此类推。这个项目从选型到调试完成前后花了大约三周时间其中大部分时间花在 SPI 时序调试和掉电保护测试上。MRAM 本身很稳定只要时序对了基本不会出问题。PIC18F4620 虽然老但工业场景下够用工具链成熟资料多出了问题容易找到答案。如果你也在做类似的数据记录项目希望这些经验能帮你少走弯路。