
1. 项目缘起与方案选型为什么是 MRAM 加 PIC181.1 一个真实的数据存储痛点做过工业数据采集的朋友大概率都遇到过这个场景设备装在配电柜或者户外机箱里需要每隔几秒记录一次温度、压力、流量这类关键参数断电之后数据不能丢下次上电还得接着用。早期我习惯用 EEPROM比如 24C 系列便宜、简单、I2C 两根线就搞定。但用久了问题就来了——EEPROM 的写入寿命通常标称 100 万次擦写速度慢单字节写入动辄 5ms 以上如果采集频率高一点几年下来芯片就写废了。后来换 SPI Flash容量大、便宜但 Flash 有个致命毛病必须先擦除再写入而且擦除是按扇区来的写之前得把整个扇区搬到 RAM 里改完再写回去掉电时机不对就丢数据。更麻烦的是 Flash 的擦写寿命只有 10 万次左右频繁记录的场景根本扛不住。直到我接触到 MRAM磁阻随机存储器才算是找到了一个比较理想的答案。MRAM 的写入原理是靠磁隧道结的磁化方向来存储数据不需要电荷保持所以写入速度是纳秒级的寿命可以做到几乎无限次标称 10^14 次以上而且断电立即保存没有写入延迟窗口。这次要聊的 MR25H40CDF 就是一颗 4Mbit 的 SPI MRAM配合 PIC18F45K42 这颗带丰富外设的 8 位 MCU搭一套工业级的数据存储与读取方案实测下来非常稳。1.2 为什么选 MR25H40CDF 而不是别的存储先说说 MR25H40CDF 这颗芯片的基本盘。它是 4Mbit 容量也就是 512KB组织成 512K x 8 位通过标准 SPI 接口访问支持 SPI 模式 0 和模式 3最高时钟频率可以跑到 40MHz。工作电压 2.7V 到 3.6V工业级温度范围 -40 到 85 摄氏度部分批次能到 105。封装是 8 脚的 SOIC 或者 DFN引脚定义和普通 SPI Flash 几乎一样CS、SO、SI、SCK加上 VCC 和 GND另外还有两个脚是 WP 和 HOLD不过 MRAM 里这两个脚的功能和 Flash 不太一样后面会细说。选它的核心理由有三条。第一是写入无需等待。MRAM 写一个字节就是写一个字节没有内部擦除周期SPI 发完数据 CS 拉高数据就已经在存储单元里了。这对工业场景太重要了你可以在中断里直接写不用担心写一半掉电。第二是寿命几乎无限。官方标称 10^14 次写入按每秒写 1000 次算能写三千多年实际项目里根本写不坏。第三是接口简单。标准 SPI和现有代码兼容性好不需要文件系统直接按地址读写就行省去了 Flash 那套擦除管理的复杂度。当然它也有缺点最直接的就是贵。同样 512KB 容量SPI Flash 可能几块钱MRAM 要几十甚至上百。所以选型的时候要算清楚如果你的场景是频繁写入、掉电不能丢、寿命要求高那 MRAM 值这个价如果只是存个配置参数、偶尔写一次那还是老老实实用 EEPROM 或者 Flash。1.3 PIC18F45K42 在这套方案里的角色PIC18F45K42 是 Microchip 新一代的 8 位 MCU40 脚 PDIP 或者 TQFP 封装64KB Flash、4KB RAM、256 字节 EEPROM跑起来最高 64MHz。它最大的特点是外设集成度高4 个 SPI 模块、4 个 I2C、4 个 UART还有 12 位 ADC、比较器、DAC、CLC 可配置逻辑单元等等。对于这个项目来说我主要用它的 SPI 主机接口去驱动 MR25H40CDF同时用 UART 把读到的数据吐给上位机用定时器做采集节拍用 ADC 接传感器。为什么不用 STM32 或者 ESP32说实话如果只是做数据存储STM32 资源更足、生态更好。但这个项目有个特殊要求低功耗加宽温。PIC18F45K42 的 XLP 超低功耗模式在休眠时电流可以做到几十纳安级别而且 Microchip 的 8 位线在工业领域供货稳定、生命周期长很多工控板卡指定要用 PIC。另外它的 SPI 模块支持独立的发送和接收 FIFO配合 DMA 虽然不如 32 位芯片那么强但对于 MRAM 这种高速存储来说已经够用了。1.4 整体架构一句话说清整套方案的核心链路是传感器信号进 PIC18F45K42 的 ADCMCU 按定时器节拍采集数据通过 SPI 总线写入 MR25H40CDF 的指定地址需要读取时再通过 SPI 把数据读回来经 UART 或者 USB 转串口送到上位机。MRAM 在这里扮演的是一个永不丢失的环形缓冲区角色MCU 只管往里写写满一圈覆盖旧数据读取时按地址索引取。下面这张表把关键参数列一下方便对照。项目MR25H40CDFPIC18F45K42容量512KB (4Mbit)64KB Flash / 4KB RAM接口SPI 模式 0/3最高 40MHz4 个 SPI 模块电压2.7V - 3.6V1.8V - 5.5V温度-40 到 85 摄氏度-40 到 125 摄氏度写入寿命10^14 次Flash 10K 次写入方式无擦除直接写需擦除2. 硬件连接与 SPI 时序细节2.1 引脚怎么接才不踩坑MR25H40CDF 是 8 脚封装引脚定义如下1 脚 CS片选低有效2 脚 SO数据输出从机到主机3 脚 WP写保护低有效4 脚 GND5 脚 SI数据输入主机到从机6 脚 SCK时钟7 脚 HOLD保持低有效8 脚 VCC。和普通 SPI Flash 的引脚布局基本一致但功能上有区别。先说WP 脚。在 Flash 里WP 拉低会保护状态寄存器不被修改。但在 MR25H40CDF 里WP 的功能是写保护整个存储阵列。也就是说如果你把 WP 拉低所有写操作都会被忽略读操作正常。这个设计在工业场景里很有用设备出厂时把 WP 拉低防止现场误写需要记录数据时再拉高。我一般会在 WP 和 VCC 之间接一个 10K 上拉电阻同时用一个 GPIO 控制这样软件可以动态开关写保护。再说HOLD 脚。HOLD 拉低会暂停当前 SPI 通信SCK 和 SI 被忽略SO 进入高阻态等 HOLD 拉高后继续。这个功能在多主机或者需要插入其他操作的场景有用但大多数时候我们直接把它上拉到 VCC 就行。注意 HOLD 和 WP 都不能悬空悬空会导致随机行为这是新手最容易犯的错。CS 片选我建议用硬件片选也就是直接接 MCU 的 SPI SS 引脚而不是用普通 GPIO 软件控制。原因很简单PIC18F45K42 的 SPI 模块在硬件片选模式下发送完数据会自动拉高 CS时序更精准尤其是在 40MHz 高速时钟下软件控制 GPIO 翻转会有几十纳秒的抖动可能导致数据错位。如果非要用软件片选那在 CS 拉高之前一定要确保最后一个 SCK 边沿已经完成中间加几个 NOP 延时。2.2 SPI 模式选择与时序计算MR25H40CDF 支持 SPI 模式 0CPOL0CPHA0和模式 3CPOL1CPHA1。模式 0 是空闲时 SCK 低电平数据在 SCK 上升沿采样模式 3 是空闲时 SCK 高电平数据在 SCK 下降沿采样。两种模式都能用我一般选模式 0因为和大多数 SPI 设备兼容调试时用逻辑分析仪抓波形也直观。时序参数方面手册里给了几个关键值SCK 高电平和低电平最小脉宽都是 10ns对应 50MHz 理论值但实际标称最高 40MHzCS 建立时间最小 5nsCS 保持时间最小 5ns数据输出有效时间最大 8ns。按 40MHz 算一个时钟周期 25ns高电平 12.5ns低电平 12.5ns都在手册范围内。但实际布线时要注意如果走线长、负载电容大边沿会变缓这时候要么降速到 20MHz要么加缓冲器。我实测过用 PIC18F45K42 的 SPI 跑 40MHz在 10cm 排线连接的情况下误码率大概在 10^-6 量级偶尔会读出错数据。降到 20MHz 后连续跑 72 小时无错。所以如果你的板子走线不理想20MHz 是一个稳妥的选择。下面这个表是不同时钟下的实测表现。SPI 时钟走线长度误码率连续 24h建议40MHz5cm 以内无错可用需阻抗匹配40MHz10cm 排线约 10^-6不推荐20MHz10cm 排线无错推荐10MHz20cm 排线无错长距离稳妥2.3 电源与去耦的实操经验MRAM 虽然写入不靠电荷泵但它的读写电流并不小。手册标称工作电流典型值 10mA峰值能到 20mA。如果和 MCU 共用 3.3V 电源一定要在 MRAM 的 VCC 脚旁边放一个 0.1uF 的陶瓷电容越近越好最好再并一个 1uF 的钽电容。我踩过的坑是一开始只放了 0.1uF结果高速写入时偶尔出现数据错误用示波器看 VCC 上有 100mV 左右的毛刺。加了 1uF 之后毛刺降到 20mV 以内问题消失。另外MRAM 的 WP 和 HOLD 脚如果不用不要直接接地或者接 VCC 了事最好通过电阻上拉。我见过一个案例HOLD 直接接 VCC结果因为电源上电顺序问题HOLD 在 VCC 稳定之前被拉低了一下导致第一次通信失败。加个 10K 上拉电阻让它在电源建立过程中保持高电平就稳了。3. 软件驱动实现从底层字节读写到环形缓冲区3.1 SPI 底层读写函数怎么写PIC18F45K42 的 SPI 模块用起来不复杂但有几个寄存器配置容易搞混。我一般用 MCCMPLAB Code Configurator生成初始化代码然后自己封装读写函数。核心寄存器是 SPI1CON0、SPI1CON1、SPI1CON2、SPI1BAUD、SPI1TCNT。下面这段是初始化代码的关键部分我用的是模式 0主机模式时钟 Fosc/4 再分频。// SPI1 初始化主机模式模式 0时钟 Fosc / (4 * (BAUD 1)) SPI1CON0 0x00; // 先清零 SPI1CON0bits.MST 1; // 主机模式 SPI1CON0bits.BMODE 0; // 非缓冲模式 SPI1CON0bits.SSPEN 0; // 先关闭 SPI1CON1bits.SMP 0; // 中间采样 SPI1CON1bits.CKE 1; // 数据在 SCK 上升沿变化模式 0 SPI1CON1bits.CKP 0; // 空闲时 SCK 低电平模式 0 SPI1CON1bits.SDIO 1; // 数据输出 SPI1CON1bits.SDI 1; // 数据输入 SPI1BAUD 0x01; // 分频值实际时钟 64MHz / (4 * 2) 8MHz SPI1CON0bits.SSPEN 1; // 使能 SPI这里有个细节PIC18F45K42 的 SPI 时钟源默认是 Fosc/4如果系统时钟 64MHz那基础时钟是 16MHz再除以 (BAUD1)。我设 BAUD1得到 8MHz。如果要跑 20MHz需要把系统时钟设到 80MHz 或者调整分频但 64MHz 下最高只能到 16MHz。所以如果你非要 40MHz得用外部高速晶振或者 PLL 把系统时钟拉上去。实际项目里 8MHz 到 16MHz 已经足够MRAM 的写入速度瓶颈不在 SPI 时钟而在你的采集频率。读写字节的函数很简单等发送缓冲区空写数据等接收缓冲区满读数据。注意 PIC18F45K42 的 SPI 有独立的发送和接收 FIFO但深度只有 1所以还是要轮询。uint8_t SPI_ExchangeByte(uint8_t data) { while (!SPI1STATbits.SPITBF); // 等待发送缓冲区空 SPI1TXB data; // 写入发送缓冲区 while (!SPI1STATbits.SPIRBF); // 等待接收缓冲区满 return SPI1RXB; // 返回接收数据 }3.2 MRAM 的读写命令与地址组织MR25H40CDF 的命令集很精简常用的就四条WREN0x06写使能、WRDI0x04写禁止、READ0x03读数据、WRITE0x02写数据。另外还有 RDSR0x05读状态寄存器和 WRSR0x01写状态寄存器但 MRAM 的状态寄存器很简单一般用不上。读操作的流程是CS 拉低发送 0x03然后发送 3 字节地址24 位地址因为 512KB 需要 19 位但协议用 24 位接着就可以连续读数据了地址会自动递增。写操作的流程是CS 拉低发送 0x06WRENCS 拉高然后 CS 再拉低发送 0x02发送 3 字节地址发送数据CS 拉高。注意 WREN 必须在每次写操作之前发送MRAM 不会自动保持写使能状态。这里有个容易搞错的地方MRAM 的写操作没有页边界限制。Flash 写的时候不能跨页否则会回卷到页首覆盖数据。MRAM 没有这个问题你可以从任意地址开始连续写一直写到芯片末尾地址自动递增不会回卷。这让驱动代码简单很多不需要做页对齐处理。下面是我封装的读写函数#define MRAM_CMD_WREN 0x06 #define MRAM_CMD_WRITE 0x02 #define MRAM_CMD_READ 0x03 void MRAM_WriteEnable(void) { MRAM_CS_LOW(); SPI_ExchangeByte(MRAM_CMD_WREN); MRAM_CS_HIGH(); } void MRAM_Write(uint32_t addr, uint8_t *buf, uint16_t len) { MRAM_WriteEnable(); MRAM_CS_LOW(); SPI_ExchangeByte(MRAM_CMD_WRITE); SPI_ExchangeByte((addr 16) 0xFF); SPI_ExchangeByte((addr 8) 0xFF); SPI_ExchangeByte(addr 0xFF); for (uint16_t i 0; i len; i) { SPI_ExchangeByte(buf[i]); } MRAM_CS_HIGH(); } void MRAM_Read(uint32_t addr, uint8_t *buf, uint16_t len) { MRAM_CS_LOW(); SPI_ExchangeByte(MRAM_CMD_READ); SPI_ExchangeByte((addr 16) 0xFF); SPI_ExchangeByte((addr 8) 0xFF); SPI_ExchangeByte(addr 0xFF); for (uint16_t i 0; i len; i) { buf[i] SPI_ExchangeByte(0xFF); } MRAM_CS_HIGH(); }3.3 环形缓冲区的设计思路工业数据记录最怕的就是写满。512KB 看着不小但如果每秒钟写 100 字节一小时就是 360KB一个半小时就满了。所以必须用环形缓冲区写满一圈覆盖最旧的数据。我的做法是把 MRAM 分成三个区域头部信息区、数据区、索引区。头部信息区放在地址 0x000000 开始占 64 字节存一些元数据魔数用于判断是否首次使用、写指针当前位置、总写入次数、数据区起始地址、数据区大小。数据区从 0x000100 开始到 0x07FF00 结束大概 512KB 减去头部和索引。索引区放在最后存每条记录的地址偏移和时间戳方便快速检索。写指针是一个 32 位变量存在 RAM 里每次写入后更新同时定期写回 MRAM 头部。这里有个技巧不要每次写数据都更新头部那样头部写入太频繁。我一般每写 100 条记录或者检测到掉电信号时才把写指针写回头部。掉电检测用 PIC18F45K42 的 LVD低电压检测模块当电压降到 2.9V 时触发中断在中断里把写指针存下去。MRAM 的写入速度是纳秒级完全来得及。环形缓冲区的覆盖逻辑是这样的写指针从数据区起始地址开始每写一条记录指针增加记录长度。当指针超过数据区末尾时回卷到起始地址。读取的时候从写指针往前推 N 条记录就是最新的 N 条数据。如果写指针还没绕一圈那从起始地址到写指针之间的数据都是有效的如果已经绕圈了那整个数据区都是有效数据只是最旧的数据被覆盖了。3.4 数据完整性与校验工业现场电磁干扰大SPI 通信偶尔会出错。虽然 MRAM 本身很可靠但传输过程可能受干扰。我在每条记录后面加一个 CRC16 校验读取时先校验再使用。CRC16 用查表法实现速度快占用 RAM 少。如果校验失败就跳过这条记录继续读下一条。实测在变频器旁边跑不加校验时大概每几万条会出现一条错误数据加了 CRC 之后错误数据能被识别出来不会污染上位机显示。另外头部信息区也加 CRC。每次上电时先读头部校验魔数和 CRC如果不对说明头部损坏这时候有两种策略一是格式化整个 MRAM清空所有数据二是尝试从数据区扫描重建索引。我一般选第一种简单可靠毕竟工业场景里数据丢了可以重采但系统跑不起来就是大问题。4. 实测数据与性能分析4.1 写入速度实测我用逻辑分析仪抓了 SPI 波形配合 MCU 的定时器做时间戳测了不同条件下的写入速度。测试条件是PIC18F45K42 跑 64MHzSPI 时钟 8MHz每次写 32 字节记录。结果如下表。操作耗时说明WREN 命令2.5us1 字节命令加 CS 翻转写 32 字节35us含命令、地址、数据单条记录总耗时约 40us含 WREN 和 CS 操作每秒可写记录数约 25000 条理论值实际受采集限制这个速度对于工业采集来说绰绰有余。就算每秒采集 1000 次写入占用时间也只有 4%MCU 还有大量时间做其他事情。对比 EEPROM 的 5ms 写入时间MRAM 快了 100 多倍。4.2 掉电数据保持测试我做了个简单的掉电测试设备正常写入数据然后随机时间切断电源重新上电后检查最后一条记录的完整性。测试了 200 次每次断电时机随机结果 200 次全部通过最后一条记录要么完整要么完全没写进去没有出现写一半的中间状态。这得益于 MRAM 的写入原子性——要么写成功要么没写不存在 Flash 那种擦除到一半掉电导致整个扇区数据丢失的情况。4.3 长期写入寿命观察虽然 MRAM 标称 10^14 次寿命但实际项目里不可能测到那么多次。我做了个加速测试让设备以最高速度连续写入同一个地址跑了 72 小时写入次数约 6.5 亿次读取校验全部通过。按这个速度要写到 10^14 次需要连续跑 300 多年。所以寿命这块完全不用担心MRAM 的瓶颈不在存储单元而在你的 SPI 总线和 MCU 的处理能力。5. 常见问题与排查技巧实录5.1 读出来全是 0xFF 或者 0x00这是新手最常遇到的问题。原因通常有三个一是 CS 没拉低或者 CS 拉低后没有等待足够时间就发时钟二是 SPI 模式不对模式 0 和模式 3 搞混了数据采样边沿错了三是 WP 脚被拉低写保护生效写不进去读出来自然是初始值。排查顺序先用示波器看 CS、SCK、SI、SO 四根线的波形确认 CS 在通信期间是低电平SCK 有空闲电平SI 上有数据翻转。如果波形都对再检查 WP 脚电压确保写操作时 WP 是高电平。5.2 写入后读出来数据错位数据错位通常是因为地址发送顺序错了。MR25H40CDF 的地址是 24 位先发高字节再发中字节最后发低字节。我见过有人先发低字节结果数据写到了完全错误的地址。另外如果你用的是 16 位地址习惯记得补一个 0x00 在高位。还有一点MRAM 读操作可以连续读地址自动递增但如果你在 CS 拉高之前多发了几个时钟可能会多读几个字节导致后续数据错位。所以读函数里读完需要的字节数就立即拉高 CS不要多等。5.3 高速通信时偶发错误前面提过40MHz 在长走线下容易出错。除了降速还可以在 SCK 线上串一个 22 欧姆的电阻减小反射。另外SO 线上的上拉电阻也有讲究如果 MCU 的 SPI 输入是高阻态SO 线悬空时容易受干扰加一个 10K 上拉到 VCC 能提高稳定性。我实测加了这个上拉之后40MHz 下的误码率从 10^-6 降到了 10^-8 量级虽然还不能完全消除但已经好很多了。5.4 掉电后数据丢失如果掉电后数据丢了先确认是不是真的写进去了。可以在写入后立即读回来校验如果读回来是对的那说明写入成功问题出在掉电检测或者头部更新上。检查 LVD 中断是否使能中断优先级是否够高中断服务程序里是否及时保存了写指针。另外MRAM 的写入虽然快但也不是瞬间完成从 CS 拉低到数据真正写入存储单元大概有几十纳秒的延迟。如果你在 CS 还没拉高的时候就断电数据可能没写进去。所以掉电中断里先完成当前写操作拉高 CS再保存指针。5.5 常见问题速查表现象可能原因排查方法解决措施读出全 0xFFCS 未拉低 / SPI 模式错示波器看波形检查 CS 和模式配置读出全 0x00WP 拉低 / 芯片未供电测 WP 和 VCC 电压拉高 WP检查电源数据错位地址顺序错 / 多读字节核对地址发送顺序按高中低发送及时拉高 CS高速偶发错误走线反射 / 干扰降速测试降速、串电阻、加上拉掉电丢数据LVD 未配置 / 写指针未保存检查中断和保存逻辑配置 LVD中断里保存指针写入无效WREN 未发送 / CS 时序错抓 WREN 波形每次写前发 WREN6. 几个容易被忽略的实操心得6.1 关于 WP 和 HOLD 的灵活用法WP 脚除了做写保护还可以用来做分区保护。比如你的 MRAM 里有一部分是出厂校准参数不允许现场修改另一部分是数据记录区需要频繁写入。你可以用 MCU 的一个 GPIO 控制 WP在写校准参数时拉高 WP写完拉低写数据记录时保持 WP 高电平。这样即使程序跑飞误写了校准区WP 拉低也能挡住。当然这需要软件逻辑配合但多一层保护总是好的。HOLD 脚在多任务系统里有用。比如你正在读 MRAM突然来了个高优先级中断需要操作 SPI 总线上的另一个设备这时候可以把 HOLD 拉低暂停 MRAM 通信等中断处理完再拉高继续。不过 PIC18F45K42 是 8 位机一般不会跑复杂多任务这个用法了解即可。6.2 批量写入时合并 WREN每次写操作都要发 WREN这会增加额外开销。如果你要连续写多个不连续的地址可以发一次 WREN然后连续执行多次写操作只要中间不拉高 CS 超过一定时间MRAM 没有超时限制但建议不要间隔太久。不过要注意WREN 之后如果执行了读操作写使能状态可能会被清除具体看手册。我一般还是老老实实每次写前发 WREN多花 2.5us 而已换来的可靠性值得。6.3 用 MRAM 存配置参数的技巧除了做数据记录MRAM 也很适合存配置参数。传统做法是用 EEPROM 存配置但 EEPROM 写入慢而且如果配置参数多、修改频繁寿命也是个问题。用 MRAM 存配置可以做到修改立即生效掉电不丢。我一般把配置区放在 MRAM 的头部每条配置项加一个 ID 和 CRC读取时按 ID 查找。这样增加配置项时不需要改地址布局兼容性好。6.4 调试时用逻辑分析仪比万用表管用SPI 通信出问题万用表只能测静态电平看不出时序问题。一个几百块的低速逻辑分析仪配合免费软件能抓 SPI 波形、解码命令、看数据内容排查效率提升十倍不止。我习惯在调试阶段把 CS、SCK、SI、SO 四根线都接到逻辑分析仪上设置 SPI 解码一眼就能看出命令对不对、地址对不对、数据对不对。这个习惯帮我省了大量猜测时间。6.5 关于供电顺序的提醒如果 MRAM 和 MCU 用不同的电源轨要注意上电顺序。最好让 MRAM 的 VCC 和 MCU 的 VCC 同时上电或者 MRAM 稍晚一点。如果 MRAM 先上电MCU 还没上电SPI 线上的电平不确定可能导致 MRAM 误动作。虽然 MRAM 有上电复位电路但稳妥起见还是在 VCC 和 GND 之间加一个合适的放电电阻确保断电时快速放电下次上电时复位可靠。这套 MR25H40CDF 加 PIC18F45K42 的方案我在三个工业项目里实际用过最长的已经连续跑了两年多每天写入几十万条记录没有出现过数据丢失或者芯片损坏。MRAM 的价格确实比 Flash 高但在那些数据不能丢、写入要频繁、维护成本高的场景里多花的这点钱换来的是省心和可靠。如果你正在选型数据存储方案不妨把 MRAM 纳入考虑范围尤其是当你的项目需要现场长期运行、不方便更换存储芯片的时候。