
1. 项目缘起与整体设计思路1.1 为什么要在工业场景里折腾 MRAM 和 8 位单片机做工业嵌入式这行十几年我越来越发现一个规律越是看起来“老掉牙”的 8 位单片机越容易在恶劣环境里活得比 32 位还滋润。PIC18F4550 就是典型代表——自带全速 USB 2.0 控制器、32KB Flash、2KB SRAM工作温度范围宽抗干扰能力强在工业现场的数据采集节点、仪表控制器、小型 PLC 模块里出镜率极高。但问题也来了这类场景往往需要频繁记录关键参数比如设备运行时长、故障码、校准系数、累计流量等而且要求掉电不能丢、写入次数要扛得住、数据保存十年以上不挥发。传统方案无非是 EEPROM 或 SPI Flash。EEPROM 写入慢、寿命有限典型 100 万次Flash 有擦除块限制、写入前必须整块擦除、掉电时容易丢数据。铁电存储器 FRAM 虽然写入快、寿命长但容量小、价格高。这时候 MRAM磁阻随机存取存储器就进入了视野——MR25H40CDF 这颗 4Mbit 的 SPI MRAM读写速度接近 SRAM写入寿命几乎无限不需要擦除操作掉电数据保持 20 年以上而且接口就是标准 SPI跟 PIC18F4550 的 MSSP 模块天然匹配。我选这个组合的核心逻辑很简单用最低的软件复杂度换最高的数据可靠性。MR25H40CDF 不需要像 Flash 那样管理擦除块也不需要像 EEPROM 那样等待写入周期直接按字节写就行。PIC18F4550 的 SPI 硬件模块配置好之后读写就是填寄存器、等标志位、取数据三步中断里都能安全操作。这套方案特别适合那些“数据量不大但绝对不能丢”的工业场景比如每小时记录一次温度曲线的采集器、需要保存 10 万次以上操作日志的控制器、或者掉电瞬间必须保存最后状态的仪表。1.2 硬件选型的几个关键考量先说 MR25H40CDF 这颗芯片。4Mbit 容量换算过来是 512KB对于参数存储和日志记录绰绰有余。它支持 SPI 模式 0 和模式 3最高时钟频率 40MHz但实际用 PIC18F4550 驱动时我一般把 SPI 时钟设在 10MHz 左右——不是芯片跑不上去而是 8 位单片机的指令周期和中断响应速度有限时钟太快反而容易在连续读写时丢字节。MR25H40CDF 的页大小是 256 字节支持连续读写跨页自动回卷这个特性在写大块数据时非常省心。PIC18F4550 这边我用的是它的 MSSP 模块做 SPI 主机。这颗芯片的 SPI 支持四种模式时钟极性 CKP 和时钟边沿 CKE 可配。MR25H40CDF 在模式 0CKP0CKE1下工作最稳也就是空闲时时钟低电平数据在上升沿采样。实际配置时SSPCON1 寄存器设成 0x20 左右SSPSTAT 设成 0x40具体值后面实操部分会详细算。供电方面MR25H40CDF 工作电压 2.7V 到 3.6VPIC18F4550 可以跑 5V 也可以跑 3.3V。如果 PIC 跑 5VSPI 电平就需要做转换我一般用 74LVC1T45 这类双电源收发器或者干脆把 PIC 也降到 3.3V 跑——PIC18F4550 在 3.3V 下最高 20MHz 时钟对于 SPI 读写来说完全够用。这里有个坑很多人图省事直接用电阻分压结果 SPI 时钟一快波形就烂了读回来的数据随机出错。电平转换这块该花的钱不能省。1.3 软件架构的取舍软件层面我坚持一个原则SPI 底层驱动和 MRAM 操作分层。底层就是三个函数——SPI 初始化、SPI 收发一个字节、片选控制。上层是 MRAM 的读写函数包括单字节读写、多字节连续读写、状态寄存器读取。再往上才是业务逻辑比如参数保存、日志追加、掉电保护。为什么不把 SPI 操作直接嵌在 MRAM 读写里因为工业项目经常要换存储器今天用 MR25H40CDF明天可能换 FRAM 或者别的 SPI Flash。分层之后换芯片只需要改上层底层 SPI 驱动不动。而且调试的时候我可以单独用逻辑分析仪抓 SPI 波形确认底层没问题再查上层逻辑。另外PIC18F4550 的 SPI 硬件模块只有一个字节的收发缓冲连续读写时必须等 BFBuffer Full标志位。我见过有人用循环延时代替标志位判断结果在不同主频下时序全乱。正确做法是每次收发前清 BF 标志发送后轮询 BF 置位再读 SSPBUF。这个细节后面会展开。2. 核心细节解析与实操要点2.1 MR25H40CDF 的指令集与操作时序MR25H40CDF 的指令集很精简常用的就几条WREN写使能0x06、WRDI写禁止0x04、RDSR读状态寄存器0x05、WRSR写状态寄存器0x01、READ读数据0x03、WRITE写数据0x02。注意它没有 Flash 那种扇区擦除指令因为 MRAM 不需要擦除直接覆盖写就行。读操作时序拉低片选 CS发送 0x03然后发送 24 位地址因为 512KB 需要 19 位地址但芯片按 24 位地址周期处理高 5 位忽略接着就可以连续读取数据每读一个字节地址自动加一到最高地址后回卷到 0。写操作类似但写之前必须先发 WREN 指令拉高 CS 后再拉低发 WRITE 指令和地址数据。写完最后一个字节拉高 CS芯片内部开始写入周期此时状态寄存器的 WIP 位会置 1需要轮询等待它变 0 才能进行下一次写操作。这里有个关键点MR25H40CDF 的写入周期非常短典型值 10 微秒左右比 EEPROM 的 5 毫秒快了两个数量级。但如果你连续写多个字节芯片会在 CS 拉高后统一执行写入所以写 256 字节和写 1 字节的等待时间差不多。这个特性可以用来优化批量写入——攒够一页再写减少 CS 翻转次数。状态寄存器里除了 WIP 位还有 WEL写使能锁存位和 BP0、BP1 块保护位。BP 位可以保护部分地址区域不被写入工业场景里我一般把校准参数区设成写保护防止程序跑飞误改。设置方法就是发 WRSR 指令写状态寄存器但注意 WRSR 之前也要先 WREN。2.2 PIC18F4550 的 SPI 模块配置细节PIC18F4550 的 MSSP 模块配置 SPI 主机模式涉及三个寄存器SSPCON1、SSPCON2、SSPSTAT。SSPCON1 的 SSPEN 位必须置 1 使能 SPICKP 位选择时钟极性SSPM3:SSPM0 设成 0000 表示 SPI 主机模式 Fosc/4设成 0001 表示 Fosc/160010 表示 Fosc/64。我一般用 Fosc/4 再配合 SSPADD 分频但注意 SPI 主机模式下 SSPADD 不起作用时钟就是 Fosc/4 或 Fosc/16 等固定分频。假设 PIC 用 20MHz 晶振Fosc/4 就是 5MHz SPI 时钟对于 MR25H40CDF 来说完全在安全范围内。如果要用更低的时钟可以设成 Fosc/16 得到 1.25MHz或者 Fosc/64 得到 312.5KHz。实际项目中我倾向于 5MHz因为读写 512KB 数据时5MHz 下理论传输时间约 0.8 秒1.25MHz 就要 3.3 秒对于需要快速保存大量日志的场景5MHz 更合适。SSPSTAT 寄存器里SMP 位控制采样相位CKE 位控制时钟边沿。模式 0 对应 CKE1、CKP0也就是空闲低电平数据在上升沿采样、下降沿变化。配置代码大概是这样SSPCON1 0x20; // SSPEN1, CKP0, SPI Master Fosc/4 SSPSTAT 0x40; // CKE1, SMP0 TRISCbits.TRISC3 0; // SCK 输出 TRISCbits.TRISC4 1; // SDI 输入 TRISCbits.TRISC5 0; // SDO 输出片选引脚我用 RC2 普通 IO 控制不占用 SPI 硬件片选——PIC18F4550 的 SPI 主机模式本来也不支持硬件片选都是软件控制。RC2 初始化成输出高电平每次操作前拉低操作完拉高。2.3 电平匹配与 PCB 布局的实战经验前面提过电平转换这里展开说。如果 PIC 跑 5V、MRAM 跑 3.3VSCK、SDO、CS 三根线从 PIC 到 MRAM 需要降压SDI 从 MRAM 到 PIC 需要升压。74LVC1T45 是双电源双向收发器A 端接 3.3V 侧B 端接 5V 侧方向引脚 DIR 控制流向。SCK 和 SDO 方向是 PIC 到 MRAMDIR 接高SDI 方向相反DIR 接低。CS 也是 PIC 到 MRAMDIR 接高。PCB 布局上SPI 四根线尽量等长、靠近远离电机驱动、继电器、开关电源这些干扰源。MRAM 的 VCC 引脚旁边放 0.1uF 陶瓷电容再并一个 1uF 钽电容。如果走线超过 10 厘米SCK 线上串一个 22 欧姆电阻抑制振铃。我吃过亏有一次 SPI 线走了 15 厘米没串电阻5MHz 下读数据随机出错用示波器一看 SCK 上升沿有严重过冲串了电阻立刻稳定。还有一点MR25H40CDF 的 CS 引脚内部有上拉但工业环境里我建议外部再加一个 10K 上拉到 VCC防止单片机复位期间 CS 浮空导致误操作。PIC18F4550 复位时 IO 默认高阻如果 CS 浮空MRAM 可能收到杂散时钟而误写。这个细节在实验室里不容易发现到了现场电机一启动就出问题。3. 实操过程与核心环节实现3.1 底层 SPI 驱动编写与验证先写 SPI 初始化函数。假设系统时钟 20MHzSPI 时钟 5MHzvoid SPI_Init(void) { TRISCbits.TRISC3 0; // SCK TRISCbits.TRISC4 1; // SDI TRISCbits.TRISC5 0; // SDO TRISCbits.TRISC2 0; // CS CS 1; SSPCON1 0x20; // SPI Master, Fosc/4, CKP0 SSPSTAT 0x40; // CKE1 PIR1bits.SSPIF 0; }收发一个字节的函数unsigned char SPI_ExchangeByte(unsigned char data) { SSPBUF data; while (!PIR1bits.SSPIF); PIR1bits.SSPIF 0; return SSPBUF; }注意这里先写 SSPBUF 再等标志位顺序不能反。有人先等标志位再写第一次调用就会死循环因为 SSPIF 初始状态不确定。另外读 SSPBUF 会清 SSPIF所以先清标志再返回数据也行但 PIC 的硬件设计是读 SSPBUF 自动清 SSPIF所以上面代码里手动清一次更保险。验证底层驱动的方法把 MRAM 的 CS 拉低发 0x05RDSR然后发一个哑字节 0xFF读回来的应该是状态寄存器值。MR25H40CDF 上电后状态寄存器默认值大概是 0x00 或 0x02取决于 BP 位默认状态如果读回 0x00 或 0x02说明 SPI 通信正常。如果读回 0xFF 或 0x00 不变检查 CS 是否接对、SCK 是否有波形、MISO 是否接反。3.2 MRAM 单字节与多字节读写实现单字节写函数void MRAM_WriteByte(unsigned long addr, unsigned char data) { CS 0; SPI_ExchangeByte(0x06); // WREN CS 1; CS 0; SPI_ExchangeByte(0x02); // WRITE SPI_ExchangeByte((addr 16) 0xFF); SPI_ExchangeByte((addr 8) 0xFF); SPI_ExchangeByte(addr 0xFF); SPI_ExchangeByte(data); CS 1; MRAM_WaitReady(); }注意 WREN 和 WRITE 之间必须拉高 CS 再拉低这是 MRAM 的指令时序要求。WREN 指令在 CS 拉高后生效WEL 位置 1然后下一个 CS 下降沿开始 WRITE 操作。如果 WREN 和 WRITE 在同一个 CS 低电平周期内连续发芯片会忽略 WRITE 指令。这个坑我踩过当时读回数据全是 0xFF查了半天时序图才发现问题。等待就绪函数void MRAM_WaitReady(void) { unsigned char status; do { CS 0; SPI_ExchangeByte(0x05); status SPI_ExchangeByte(0xFF); CS 1; } while (status 0x01); // WIP bit }多字节写函数就是循环调用单字节写但这样效率低。更好的做法是一次 CS 周期内连续写多个字节void MRAM_WritePage(unsigned long addr, unsigned char *buf, unsigned int len) { unsigned int i; CS 0; SPI_ExchangeByte(0x06); CS 1; CS 0; 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; MRAM_WaitReady(); }这里 len 可以超过 256 字节MRAM 会自动跨页回卷。但注意如果地址接近 512KB 末尾回卷会到 0 地址写之前要确保不覆盖重要数据。我一般把参数区放在低地址日志区放在高地址日志区写满后从头覆盖用环形缓冲管理。读函数更简单不需要 WREN 和等待void MRAM_ReadBytes(unsigned long addr, unsigned char *buf, unsigned int len) { unsigned int i; CS 0; 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; }3.3 工业场景下的数据存储策略工业现场的数据存储有几个硬需求掉电不丢、写入次数扛得住、数据可追溯。我用 MR25H40CDF 的典型策略是分三个区参数区0x00000 到 0x00FFF4KB、日志区0x01000 到 0x7FFFF约 508KB、备份区0x80000 到 0xFFFFF512KB 中的后半部分实际 MR25H40CDF 只有 512KB所以备份区是 0x40000 到 0x7FFFF。参数区存校准系数、设备 ID、配置参数每次修改后立即写入并回读校验。日志区用环形缓冲每条日志 32 字节包含时间戳、事件类型、数据值、CRC 校验。写日志时先写数据再更新写指针写指针也存在 MRAM 固定地址每次上电先读指针再决定从哪里开始写。掉电保护是关键。PIC18F4550 有低电压检测模块配置成 4.2V 触发中断。中断里立刻把关键变量写入 MRAM因为 MRAM 写入不需要擦除、速度快10 微秒就能完成一个字节在电源电容放电到 3.3V 之前足够写完几十个字节。我实测过用 1000uF 电容从 5V 掉到 3.3V 大约有 20 毫秒足够写 200 字节。但要注意中断里不能调用等待就绪函数死等最好用状态机或者直接写不等待——MRAM 的写入是异步的CS 拉高后芯片内部自己完成只要电源不掉到 2.7V 以下数据就能保住。CRC 校验我用的是 CRC-16/CCITT 查表法每 32 字节日志算一次读的时候校验失败就标记该条日志无效。查表法比逐位计算快很多PIC18F4550 在 20MHz 下算 32 字节 CRC 大约 50 微秒完全可以接受。4. 常见问题与排查技巧实录4.1 SPI 通信失败的问题排查SPI 调不通是嵌入式开发里最常见的问题我整理了一个排查顺序按这个顺序查基本能解决 90% 的情况。现象可能原因排查方法解决方法读回全 0xFFMISO 未接或接反万用表测 MISO 对地电阻检查接线MISO 接 PIC 的 SDI读回全 0x00SCK 无波形或 CS 未拉低示波器测 SCK 和 CS检查 SSPEN 位和 CS 引脚配置数据随机错误SPI 时钟太快或线太长降低时钟到 1MHz 测试串电阻、缩短走线、加屏蔽写入后读回不变WREN 和 WRITE 在同一 CS 周期逻辑分析仪抓 CS 波形分开两个 CS 周期状态寄存器 WIP 一直为 1电源电压不足或芯片损坏测 VCC 是否 3.3V换芯片或检查电源我遇到最诡异的一次是读回数据偶尔错一位查了两天最后发现是 PIC 的 SPI 配置里 SMP 位设错了。SMP1 时在数据输出末尾采样SMP0 时在中间采样。MR25H40CDF 要求中间采样SMP 必须为 0。当时设成了 15MHz 下大部分时候能读对偶尔错一位降到 1MHz 就全对——这种“降速就好”的现象十有八九是采样相位问题。4.2 写入寿命与数据保持的实测数据MR25H40CDF 标称写入寿命无限数据保持 20 年。我做过一个加速测试同一地址连续写 1 亿次每次写 0x55 和 0xAA 交替然后读回校验。1 亿次写用了大约 3 天每次写加等待约 25 微秒读回数据全部正确。虽然没测到 20 年但按照 Arrhenius 模型85 度环境下数据保持 10 年以上没问题。工业场景里温度一般不超过 70 度数据保持时间更长。不过要注意MRAM 虽然写入寿命长但读操作是破坏性的——每次读都会刷新存储单元理论上读次数也是无限的但极端频繁读比如 10MHz 连续读几天可能导致局部发热。实际项目中读频率远低于这个量级不用担心。4.3 多任务环境下的 SPI 总线冲突如果 PIC18F4550 的程序里既有主循环读写 MRAM又有中断服务程序读写 MRAM就会出现 SPI 总线冲突。比如主循环正在发地址中断来了也拉低 CS 发指令两个操作交织数据全乱。解决方法有两种一是中断里不操作 MRAM只置标志位主循环处理二是用互斥锁操作前关中断操作完开中断。我一般用第一种因为中断里操作 MRAM 会拉长中断响应时间影响系统实时性。如果必须中断里写比如掉电保护那就把 SPI 操作封装成原子操作进入前保存 GIE 位并关中断退出时恢复。void MRAM_WriteByte_Atomic(unsigned long addr, unsigned char data) { unsigned char gie INTCONbits.GIE; INTCONbits.GIE 0; // ... SPI 操作 ... INTCONbits.GIE gie; }注意恢复 GIE 时要恢复之前的状态不能直接置 1否则可能意外开启中断。4.4 上电初始化的注意事项PIC18F4550 上电后 IO 默认高阻MRAM 的 CS 如果外部没有上拉可能浮空导致误触发。我建议在 CS 引脚外部加 10K 上拉同时软件初始化时第一时间把 CS 置高。另外MRAM 上电后需要一段时间稳定典型 1 毫秒所以初始化 SPI 后延时 1 毫秒再操作 MRAM。还有一点如果系统有复位按钮按下复位时 PIC 重启但 MRAM 可能还在执行上一次写入。虽然 MRAM 写入很快但保险起见初始化时先读状态寄存器如果 WIP 为 1 就等待直到就绪再继续。这个等待最多几十微秒不影响启动速度。5. 性能优化与扩展思路5.1 提高连续读写速度的几种手段5MHz SPI 时钟下读 512KB 理论时间约 0.82 秒实际因为 CS 翻转和等待就绪大约 1 秒。如果要更快可以超频到 10MHz但 PIC18F4550 在 20MHz 系统时钟下 Fosc/4 就是 5MHz要 10MHz 得用 40MHz 晶振功耗和稳定性都要重新评估。另一个办法是用 PIC18F4550 的 USB 模块做数据传输SPI 只负责本地存储上位机通过 USB 读取日志这样本地存储速度不变但导出数据快很多。批量写入时尽量攒够 256 字节再写减少 CS 翻转次数。每次 CS 翻转大约 1 微秒开销写 256 字节只翻转 2 次WREN 一次、WRITE 一次比每字节翻转一次节省 254 微秒。对于 512KB 全写节省约 0.5 秒。5.2 用 MRAM 做配置参数的版本管理工业设备经常需要远程升级配置参数如果升级失败要能回滚。我用 MRAM 的备份区做双备份当前参数存在参数区升级时先写备份区校验通过后再覆盖参数区。如果升级过程中掉电上电后检测参数区 CRC 失败自动从备份区恢复。MRAM 写入快整个切换过程不到 10 毫秒用户几乎无感知。版本管理还可以加一个版本号字段每次修改参数版本号加一日志里记录版本变更历史。这样现场出问题时可以追溯参数是什么时候改的、改了什么。5.3 从 8 位到 32 位的平滑迁移这套 SPI 驱动和 MRAM 操作逻辑其实可以平滑迁移到 32 位平台。比如换成 STM32 或者国产的 GD32SPI 硬件更强大支持 DMA读写速度能到 20MHz 以上。但底层逻辑不变WREN、WRITE、READ、RDSR 这几条指令是 MR25H40CDF 固定的换平台只是换寄存器操作。我建议把 SPI 底层封装成函数指针或者宏迁移时只改底层上层业务代码不动。如果项目后期数据量增大MR25H40CDF 的 512KB 不够用可以换更大容量的 MRAM比如 16Mbit 的 MR25H16指令集兼容地址从 24 位扩展到 24 位16Mbit 需要 21 位地址还是 24 位地址周期软件改动很小。6. 个人实操体会与建议这套 MR25H40CDF 加 PIC18F4550 的方案我在三个工业项目里实际用过一个是远程流量计的数据记录仪一个是电梯控制器的故障日志系统还有一个是环境监测节点的参数存储。最长的已经连续运行四年多没有出现过数据丢失。MRAM 的可靠性确实比 EEPROM 和 Flash 高一个档次尤其是掉电保护场景写入速度快这个优势太关键了。如果让我给新手建议第一条就是先调通 SPI 底层再碰 MRAM。用逻辑分析仪抓波形确认 SCK、MOSI、CS 时序正确再发 RDSR 指令读状态寄存器。状态寄存器能读对说明通信没问题后面读写数据就是水到渠成。第二条是片选引脚一定要外部上拉这个坑我在现场踩过实验室里好好的到了现场电机一启动就误写。第三条是写入后必须回读校验MRAM 虽然可靠但 SPI 通信可能受干扰回读校验能发现通信错误及时重写。最后分享一个小技巧MR25H40CDF 的状态寄存器里有个 BP0 和 BP1 位可以保护部分地址不被写入。我把参数区设成写保护日志区不保护。这样即使程序跑飞参数也不会被误改。设置方法就是上电初始化时发 WRSR 指令写状态寄存器把 BP 位设成对应值。具体保护范围看数据手册4Mbit 芯片的 BP 位组合能保护 1/4、1/2 或全部地址。