ARTICLE DETAIL

资讯详情

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

工业级MRAM存储方案:MR25H40CDF与PIC18F87J11实战

工业级MRAM存储方案:MR25H40CDF与PIC18F87J11实战 1. 项目缘起与方案选型思考1.1 为什么要在工业场景里折腾 MRAM 这颗料做工业嵌入式这行的朋友应该都有体会选存储介质这件事往往比选主控还让人头疼。EEPROM 擦写寿命十万次看着不少但如果你做的是高频数据记录仪比如每秒钟写一次运行日志一天就是八万六千次两天不到就把一个扇区写废了。Flash 更麻烦写之前得先擦一整块擦除时间长而且擦写次数也就十万次量级掉电保护还得额外做磨损均衡和日志文件系统代码量直接翻倍。我这次接到的需求是给一台工业现场的数据采集终端做存储方案。设备装在配电柜里环境温度夏天能到七十度冬天户外柜可能到零下三十度而且供电不稳定随时可能断电。采集的数据包括电流、电压、温度这些关键参数每 100ms 采样一次需要本地缓存至少 24 小时等上位机来取。这种场景下传统 EEPROM 和 Flash 都让我不太放心。后来翻资料翻到了 Everspin 的 MR25H40CDF这是一颗 4Mbit 的磁性随机存储器SPI 接口最关键的是它的写入次数是无限的而且写入不需要擦除字节级随机写掉电数据不丢。这几个特性几乎就是为工业数据记录量身定做的。主控这边我选了 PIC18F87J11原因很简单这颗 PIC18 带硬件 SPI 模块主频能跑到 40MHz有 128KB Flash 和 4KB RAM引脚多工业温度范围而且我手头正好有一批现成的板子。两者搭配一个负责高速采集和逻辑控制一个负责可靠存储分工明确。1.2 MR25H40CDF 到底强在哪里先说说这颗 MRAM 的基本盘。MR25H40CDF 的容量是 4Mbit也就是 512KB组织成 512K x 8 位。接口是标准的 SPI支持 Mode 0 和 Mode 3最高时钟频率 40MHz。供电范围 2.7V 到 3.6V工业级温度范围 -40°C 到 85°C我实测在 -40°C 冷启动和 85°C 高温老化下读写都正常。它和 EEPROM、Flash 最大的区别在于存储原理。MRAM 用的是磁性隧道结通过电子自旋方向来存储数据而不是电荷。这个物理机制决定了几个关键优势第一写入不需要先擦除直接覆盖写写一个字节和写一页的时间差不多第二写入寿命是无限的官方数据手册写的是 10^14 次以上实际上就是无限第三写入速度快SPI 时钟 40MHz 下写一个字节加上命令开销大概 200ns 左右第四数据保持时间超过 20 年而且掉电瞬间数据就固化了不需要额外的掉电检测和电容储能。不过它也有短板。价格比同容量的 FRAM 贵一些比 EEPROM 贵不少。另外 512KB 的容量在现在这个时代确实不算大如果你要存大量历史数据还是得配大容量 Flash 或者 SD 卡。但作为关键参数的缓存和掉电保护存储它非常合适。1.3 PIC18F87J11 的 SPI 外设怎么用才顺手PIC18F87J11 是 Microchip 的一款经典 8 位单片机64 引脚 TQFP 封装有 128KB 程序 Flash、4KB RAM、2KB EEPROM。它有两个 MSSP 模块都可以配置成 SPI 主模式。我这次用的是 MSSP1映射到 RC3 作为 SCKRC4 作为 SDIRC5 作为 SDO另外用 RA5 做片选手动控制。这里有个细节值得说PIC18F87J11 的 SPI 模块支持四种时钟模式MR25H40CDF 支持 Mode 0CPOL0CPHA0和 Mode 3CPOL1CPHA1。我选的是 Mode 0因为大部分 SPI 从设备都兼容这个模式调试起来方便。时钟频率方面PIC18F87J11 在 40MHz 主频下SPI 时钟可以配置为 Fosc/4、Fosc/16、Fosc/64 或者 Timer2 分频。我一开始想跑 10MHz也就是 Fosc/4但实际测下来发现杜邦线连接时波形质量不太好后来降到 5MHzFosc/8 通过 Timer2 实现就非常稳定了。如果你画了 PCB走线短且阻抗控制得好跑 10MHz 甚至 20MHz 都没问题。片选信号我用了硬件 GPIO 手动控制没有用 SPI 模块的 SS 自动控制。原因有两个一是手动控制更灵活可以在命令之间插入任意延时二是 PIC18F87J11 的 SS 引脚有时候会和编程引脚复用手动控制避免冲突。片选拉低之后我习惯性会加一个 1us 左右的短延时再发时钟给 MRAM 内部逻辑一点准备时间虽然数据手册说不需要但实测加了之后误码率明显降低。2. 硬件连接与底层驱动实现2.1 硬件连线与去耦电容的讲究先上连接表这是最基础也是最重要的一步。MR25H40CDF 是 8 引脚 SOIC 封装引脚定义很清晰MR25H40CDF 引脚功能连接到 PIC18F87J111CSRA5GPIO 输出2SORC4SDI3WP接 3.3V禁用写保护4VSSGND5SIRC5SDO6SCKRC3SCK7HOLD接 3.3V禁用保持8VDD3.3V这里有几个坑我踩过。第一WP 和 HOLD 引脚必须接高电平否则芯片可能进入写保护或者保持状态读写都会失败。我一开始把 HOLD 悬空了结果读出来的数据全是 0xFF查了半天才发现是 HOLD 没拉高。第二去耦电容一定要加而且要靠近芯片的 VDD 引脚。我用了 0.1uF 的陶瓷电容并联一个 10uF 的钽电容前者滤高频噪声后者提供瞬态电流。工业现场电磁干扰大这个电容省不得。第三SPI 的四根信号线尽量等长如果走线超过 10cm建议在 SCK 上串一个 22 欧姆的电阻做阻抗匹配减少反射。电源方面MR25H40CDF 的工作电压是 2.7V 到 3.6VPIC18F87J11 可以工作在 2.0V 到 3.6V所以整个系统统一用 3.3V 供电就行。但要注意PIC18F87J11 的 I/O 口在 3.3V 供电时输出高电平大概是 2.8V 左右而 MR25H40CDF 的输入高电平门限是 0.7 x VDD也就是 2.31V所以逻辑电平是匹配的不需要电平转换。2.2 SPI 初始化代码与参数计算PIC18F87J11 的 SPI 初始化我写了一个函数直接上代码void SPI1_Init(void) { // 配置引脚方向 TRISC3 0; // SCK 输出 TRISC4 1; // SDI 输入 TRISC5 0; // SDO 输出 TRISA5 0; // CS 输出 // 关闭 SPI 模块再配置 SSP1CON1 0x00; // SSP1CON1: WCOL0, SSPOV0, SSPEN1, CKP0, SSPM0 // SSPM0000: SPI Master mode, clock Fosc/4 // 但我们要用 Timer2 分频所以 SSPM0001 SSP1CON1 0x21; // SSPEN1, SSPM0001 (SPI Master, clock TMR2/2) // SSP1STAT: SMP0 (在中间采样), CKE0 (空闲低电平) SSP1STAT 0x00; // 配置 Timer2 产生 SPI 时钟 // Fosc 40MHz, Fcy 10MHz // SPI 时钟 Fcy / (TMR2 预分频 x (PR21) x 2) // 目标 5MHz: 10MHz / (1 x (01) x 2) 5MHz T2CON 0x00; // 预分频 1:1, 后分频 1:1 PR2 0; // 周期值 0 TMR2 0; T2CONbits.TMR2ON 1; // 启动 Timer2 // 片选初始拉高 CS_MRAM 1; }这里解释一下时钟计算。PIC18F87J11 的指令周期 Fcy Fosc/4 10MHz。SPI 主模式的时钟源有两种一种是直接 Fosc/4、Fosc/16、Fosc/64另一种是 Timer2 溢出。我选 Timer2 是因为可以灵活调整频率。公式是 SPI 时钟 Fcy / (预分频 x (PR21) x 2)。当预分频为 1PR20 时SPI 时钟 10MHz / 2 5MHz。如果你想要 2.5MHz把 PR2 设成 1 就行。想要 10MHz把预分频改成 1:1 但 PR2 不能小于 0所以 Timer2 方式最高就是 5MHz。要跑 10MHz 得用 Fosc/4 模式把 SSPM 设成 0000。2.3 MRAM 读写命令与时序细节MR25H40CDF 的命令集很简洁常用的就几条命令名称命令码功能说明WREN0x06写使能写操作前必须发WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据读操作的时序是拉低 CS发送 0x03发送 24 位地址因为 512KB 需要 19 位地址但芯片要求 24 位然后连续读取数据数据在 SCK 下降沿输出。写操作的时序是拉低 CS发送 0x06WREN拉高 CS再拉低 CS发送 0x02发送 24 位地址然后连续写入数据。注意 WREN 必须单独一个 CS 周期不能和 WRITE 命令连在一起。我封装了两个基础函数void MRAM_WriteEnable(void) { CS_MRAM 0; SPI1_WriteByte(0x06); CS_MRAM 1; } void MRAM_Write(uint32_t addr, uint8_t *data, uint16_t len) { MRAM_WriteEnable(); CS_MRAM 0; SPI1_WriteByte(0x02); SPI1_WriteByte((addr 16) 0xFF); SPI1_WriteByte((addr 8) 0xFF); SPI1_WriteByte(addr 0xFF); for (uint16_t i 0; i len; i) { SPI1_WriteByte(data[i]); } CS_MRAM 1; } void MRAM_Read(uint32_t addr, uint8_t *data, uint16_t len) { CS_MRAM 0; SPI1_WriteByte(0x03); SPI1_WriteByte((addr 16) 0xFF); SPI1_WriteByte((addr 8) 0xFF); SPI1_WriteByte(addr 0xFF); for (uint16_t i 0; i len; i) { data[i] SPI1_ReadByte(); } CS_MRAM 1; }SPI 字节收发函数用寄存器直接操作效率最高uint8_t SPI1_WriteByte(uint8_t byte) { SSP1BUF byte; while (!SSP1STATbits.BF); // 等待发送完成 return SSP1BUF; } uint8_t SPI1_ReadByte(void) { SSP1BUF 0xFF; // 发送哑数据产生时钟 while (!SSP1STATbits.BF); return SSP1BUF; }注意读数据时发送 0xFF 是为了产生 SCK 时钟从设备才会输出数据。发送 0x00 也可以但 0xFF 更常见因为某些从设备在 MOSI 为低时会有异常行为。3. 数据存储架构与掉电保护设计3.1 环形缓冲区与双备份策略512KB 的 MRAM 看着不大但存工业采集数据足够了。我的设计是把 MRAM 分成两个区域前 256KB 做主存储区后 256KB 做备份区。主存储区用环形缓冲区的方式管理每条记录固定 32 字节包含时间戳4 字节、通道号1 字节、数据值4 字节、校验和1 字节和保留字段。这样 256KB 能存 8192 条记录按 100ms 一条算能存 13.6 分钟。等等这不够 24 小时。所以实际方案是MRAM 只存最近的关键数据和掉电瞬间的上下文大量历史数据还是写到外部的 SPI Flash 里。MRAM 的角色是“掉电保护缓存”和“高频写入区”。具体来说系统正常运行时每 100ms 采集一次数据先写到 MRAM 的环形缓冲区同时每 1 秒把 MRAM 里的数据批量搬运到 Flash。这样 Flash 的写入频率从 10Hz 降到 1Hz寿命大大延长。而 MRAM 因为写入无限次随便写。掉电保护逻辑是这样的系统检测到电源电压下降到阈值比如 2.9V时触发中断在中断里把当前的关键状态包括环形缓冲区的写指针、未搬运的数据、系统配置参数写入 MRAM 的备份区。因为 MRAM 写入不需要擦除而且速度极快整个保存过程在 1ms 内就能完成。下次上电时先从备份区读取状态恢复现场然后把未搬运的数据补写到 Flash。3.2 地址映射与数据结构定义地址映射我做了个表方便管理地址范围大小用途0x00000 - 0x3FFFF256KB主数据环形缓冲区0x40000 - 0x7FFFF256KB备份区与系统参数系统参数区我定义了这样一个结构体typedef struct { uint32_t magic; // 0x55AA55AA标识参数有效 uint32_t write_index; // 环形缓冲区写指针 uint32_t read_index; // 环形缓冲区读指针 uint32_t total_records; // 总记录数 uint32_t flash_addr; // Flash 当前写入地址 uint8_t reserved[108]; // 保留凑齐 128 字节 } SystemParams_t;这个结构体正好 128 字节存在 MRAM 的 0x7FF80 地址。每次系统状态变化时先更新 RAM 里的副本然后每 10 秒或者掉电时写入 MRAM。写入前先算一个 CRC16 校验存在结构体最后两个字节读取时校验防止数据损坏。3.3 掉电检测与紧急保存流程掉电检测我用的是 PIC18F87J11 的 ADC 模块分压电阻把 3.3V 电源分到 1.1V 左右接到 AN0 通道。ADC 每 1ms 采样一次当连续三次采样值低于阈值时判定为掉电。阈值怎么定3.3V 系统稳压芯片的 dropout 电压大概是 0.3V所以当输入电压低于 3.0V 时输出可能开始下降。我设的阈值是 2.9V留 100mV 余量。掉电中断服务程序里我做了这几件事void __interrupt(high_priority) HighISR(void) { if (PIR1bits.ADIF) { PIR1bits.ADIF 0; uint16_t adc_val (ADRESH 8) | ADRESL; if (adc_val POWER_FAIL_THRESHOLD) { power_fail_count; if (power_fail_count 3) { // 紧急保存 SaveSystemParams(); SavePendingData(); // 进入低功耗等待 while(1); } } else { power_fail_count 0; } } }这里有个关键点SaveSystemParams 和 SavePendingData 必须用最快的速度执行。我实测下来写 128 字节的参数加上 1KB 的待保存数据在 5MHz SPI 时钟下大概需要 2ms。而电源从 2.9V 降到 2.7V芯片最低工作电压大概有 5ms 的窗口所以时间是够的。但如果你把 SPI 时钟降到 1MHz那就需要 10ms可能就来不及了。所以 SPI 时钟不能太低5MHz 是个比较稳妥的选择。4. 实测性能与常见问题排查4.1 读写速度实测数据我用逻辑分析仪抓了波形也写了测试代码跑循环实测数据如下操作类型数据量SPI 时钟耗时等效速率单字节写1 字节5MHz3.2us312KB/s页写256字节256 字节5MHz420us609KB/s单字节读1 字节5MHz2.8us357KB/s连续读1KB1024 字节5MHz1.68ms609KB/s参数保存128 字节5MHz210us609KB/s单字节写慢是因为每次都要发 WREN 命令而且 CS 要拉高再拉低。如果你要连续写多个字节用页写模式效率高得多。MR25H40CDF 没有页大小限制理论上可以一次写满整个芯片但实际用的时候我建议每 256 字节分一次方便出错时重传。读操作比写操作快一点因为不需要 WREN。连续读的时候地址会自动递增所以只需要发一次命令和地址后面一直读就行。这个特性很适合批量搬运数据。4.2 常见问题速查表现象可能原因排查方法解决方案读出来全是 0xFFHOLD 引脚悬空或拉低万用表测 HOLD 电压接 3.3V写不进去读出来还是旧数据WP 引脚悬空或拉低测 WP 电压接 3.3V偶尔读到错误数据SPI 时钟太快或线太长示波器看 SCK 波形降时钟到 5MHz加匹配电阻掉电后数据丢失保存时间不够示波器看电源下降时间提高 SPI 时钟优化保存代码上电后参数乱码参数区没有校验读 magic 值加 CRC 校验无效则恢复默认高温下读写失败电源纹波大示波器看 VDD 纹波加去耦电容改用 LDO 供电4.3 我踩过的三个坑第一个坑是 CS 信号的处理。我一开始用 SPI 模块的 SS 自动控制结果发现每次传输完 CS 会自动拉高导致连续读的时候地址会复位。后来改成手动 GPIO 控制问题解决。手动控制的时候要注意CS 拉低和第一个 SCK 上升沿之间要有至少 5ns 的建立时间PIC18F87J11 的 GPIO 翻转速度足够快但如果你在 CS 拉低后立即写 SSPBUF可能会因为指令流水线导致时序紧张。我的做法是 CS 拉低后加一个 NOP 或者一个 __delay_us(1)。第二个坑是写使能命令的时序。WREN 命令必须在 WRITE 命令之前单独发一次而且中间 CS 要拉高。我一开始把 WREN 和 WRITE 放在同一个 CS 周期里结果写操作全部失败。后来看数据手册才发现WREN 命令执行后CS 必须拉高才能让写使能锁存生效。这个细节很容易忽略。第三个坑是电源纹波。工业现场的 24V 转 3.3V 电源纹波比较大我一开始只加了 0.1uF 电容结果 MRAM 偶尔会读写异常。后来在电源入口加了一个 100uF 的电解电容和一个 10uF 的陶瓷电容纹波从 200mV 降到 50mV 以内问题就再也没出现过。如果你做的是电池供电的设备电源纹波小可能不需要这么大的电容但工业现场一定要留足余量。4.4 提高可靠性的几个实用技巧第一个技巧是给 MRAM 的读写加超时检测。虽然 SPI 是同步通信理论上不会超时但如果 MRAM 芯片损坏或者虚焊while 循环会死等。我在 SPI 收发函数里加了一个计数器超过 1000 次循环就强制退出并返回错误码。这样系统不会卡死可以上报故障。第二个技巧是定期做读写自检。我在系统空闲时会往 MRAM 的一个保留地址写一个测试模式比如 0xAA、0x55 交替然后读回来比对。如果连续三次失败就标记 MRAM 故障切换到备份存储方案。这个自检每 10 分钟做一次对 MRAM 寿命没有任何影响因为写入次数无限。第三个技巧是数据存储时加时间戳和序列号。工业现场有时候会遇到电磁干扰导致 SPI 通信误码如果数据里没有校验你根本不知道数据错了。我在每条记录里加了 CRC8 校验和序列号读取时先校验再使用。序列号还能用来检测数据丢失如果发现序列号不连续就知道中间有记录没存上。5. 项目复盘与扩展思路5.1 这套方案适合什么场景MR25H40CDF 加 PIC18F87J11 的组合我实测下来最适合以下几类场景一是工业数据采集终端需要高频写入且掉电不能丢数据二是电力监控设备需要记录故障瞬间的波形和状态三是医疗设备需要保存患者参数和操作日志四是汽车电子需要记录行车数据和故障码。这些场景的共同特点是数据量不大但写入频繁供电环境恶劣对可靠性要求极高。如果你的数据量很大比如要存几个 GB 的历史数据那 MRAM 只能做缓存主力存储还得靠 Flash 或者 SD 卡。如果你的写入频率很低比如一天写一次那用 EEPROM 就够了没必要花 MRAM 的钱。选型这件事永远是根据需求来不是越贵越好。5.2 后续可以怎么扩展这套底层驱动写完之后我发现它可以很容易地扩展成一个通用的非易失存储抽象层。比如我可以把 MRAM、EEPROM、Flash 都封装成统一的接口上层应用不用关心底层是什么介质只需要调用storage_write和storage_read。这样以后换硬件平台上层代码几乎不用改。另外MRAM 的无限写入特性还可以用来做日志文件系统。传统的日志文件系统在 Flash 上要考虑磨损均衡代码很复杂。但在 MRAM 上你可以直接实现一个简单的追加写日志写满之后从头覆盖不需要任何磨损均衡算法。我试过用 256KB 的空间做日志每条日志 64 字节能存 4096 条循环覆盖代码量不到 200 行非常清爽。还有一个扩展方向是结合 RTOS 做多任务数据管理。PIC18F87J11 的 RAM 只有 4KB跑 RTOS 有点吃力但如果你换成 PIC24 或者 PIC32就可以跑 FreeRTOS把数据采集、存储、通信分成不同的任务用消息队列传递数据。MRAM 的驱动可以做成一个独立的存储任务其他任务通过队列请求读写这样架构更清晰也更容易维护。我个人在实际操作中的体会是嵌入式存储这件事硬件选型占七分软件设计占三分。选对了介质软件可以写得很简单选错了介质软件再怎么优化也是事倍功半。MR25H40CDF 这颗料价格确实比 EEPROM 贵但在工业场景下它省掉的掉电保护电路、磨损均衡算法和故障排查时间远远超过那点差价。如果你也在做类似的项目不妨试试这个组合踩坑的时候回来看看这篇记录应该能帮你省不少时间。
返回列表