ARTICLE DETAIL

资讯详情

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

工业嵌入式存储选型:MRAM与PIC18F46K42实战

工业嵌入式存储选型:MRAM与PIC18F46K42实战 1. 为什么在工业现场我会优先考虑 MRAM 而不是 EEPROM做嵌入式这行十几年存储方案选型这件事上我踩过的坑比写过的驱动还多。早些年做工业数据采集终端板子上清一色挂 EEPROM比如 24C 系列便宜、好买、驱动简单I2C 两根线就搞定。但真正把设备扔到现场跑上一年半载问题就来了EEPROM 的写入速度慢得让人抓狂字节写典型 5ms 一个周期页写也就 64 字节一页遇到需要高频记录运行参数、掉电前紧急保存状态的场景根本来不及。更别提 EEPROM 的擦写寿命通常标称 100 万次看着挺多可如果每秒写一次不到两周就报废了。后来接触到MR25H40CDF这颗芯片才算真正把高频写入 掉电不丢 长寿命这三个需求同时满足了。MR25H40CDF 是 Everspin 出的 4Mbit512KB串行 MRAMSPI 接口工作电压 2.7V 到 3.6V工业级温度范围 -40℃ 到 85℃最关键的是它的写入不需要擦除、没有写延迟、寿命标称 10^14 次也就是一百万亿次量级这个数字对绝大多数工业场景来说基本等于无限次。MRAM 和 EEPROM、Flash 的本质区别在哪我用一个生活化的类比来解释Flash 和 EEPROM 像是铅笔写字写之前得先用橡皮擦干净擦除再写上去擦的过程慢且伤纸寿命有限而 MRAM 是磁性开关靠磁隧道结MTJ的磁化方向来存储 0 和 1翻转方向就是写入不需要擦除速度接近 SRAM掉电后磁性方向保持所以又是非易失的。这个原理决定了 MRAM 在写入速度、寿命、功耗上都碾压传统非易失存储。那为什么不是所有项目都用 MRAM因为贵。同样 512KB 容量MRAM 的价格可能是 SPI Flash 的十几倍甚至几十倍。所以选型的逻辑很清楚当你的应用对写入频率、掉电保存实时性、擦写寿命有硬性要求时MRAM 才值得上如果只是存个配置参数、固件备份一年写不了几次那 SPI Flash 或 EEPROM 完全够用没必要花这个钱。我这次的项目背景是工业嵌入式数据记录仪主控用PIC18F46K42需要实时记录设备运行状态、故障日志、累计运行时间而且要求任何时刻断电都不能丢最近的数据。这种场景下 EEPROM 的写入延迟和寿命都不达标SPI Flash 虽然便宜但写入前要擦除、掉电时正在擦除的扇区可能损坏所以最终选了 MR25H40CDF。下面我把整套硬件设计、SPI 驱动、读写逻辑、掉电保护策略完整拆一遍。2. PIC18F46K42 的 SPI 外设与 MR25H40CDF 的硬件对接2.1 先搞清楚 PIC18F46K42 的 MSSP 模块能力边界PIC18F46K42 是 Microchip 新一代 8 位 MCU带 64KB Flash、4KB RAM片上集成了 MSSPMaster Synchronous Serial Port模块支持 SPI 和 I2C。用它驱动 MR25H40CDF走的是标准 4 线 SPISCK、SDIMISO、SDOMOSI、CS。这里有个细节很多人会忽略PIC18F46K42 的 MSSP 在 SPI 主机模式下最高时钟是 Fosc/4。假设你用 64MHz 内部振荡器HFINTOSC 配合 PLL那 SPI 时钟最高能到 16MHz。但 MR25H40CDF 的数据手册标称最高 SPI 时钟是 40MHz部分型号 104MHz所以 MCU 这边反而是瓶颈。实际工程里我不会跑满 16MHz原因后面讲信号完整性时会说。引脚分配上我一般这样接PIC18F46K42 引脚功能接到 MR25H40CDFRC3SCKSCKRC4SDISOMISORC5SDOSIMOSIRC2普通 GPIOCS片选RC6普通 GPIOHOLD可选不用时拉高RC7普通 GPIOWP可选不用时拉高注意 CS 我用的是普通 GPIO 而不是 MSSP 的硬件 SS 引脚。为什么因为硬件 SS 在主机模式下容易受配置位影响而且多从机场景下切换麻烦。用软件控制 GPIO 做片选时序完全可控调试时也方便用逻辑分析仪抓。代价是每次读写前后要手动拉低拉高多几条指令而已对性能影响可以忽略。2.2 MR25H40CDF 的引脚与上电注意事项MR25H40CDF 是 8 脚 SOIC 封装引脚定义CS、SO、WP、VSS、SI、SCK、HOLD、VDD。其中WP写保护和 HOLD保持这两个脚如果不用必须拉高到 VDD否则芯片可能进入异常状态。我见过有人把 HOLD 悬空结果读出来的数据全是 0xFF查了半天以为是驱动问题其实是 HOLD 被干扰拉低了。上电时序上MR25H40CDF 要求 VDD 稳定后等待一段时间数据手册标称 tPU上电时间才能发命令。稳妥做法是上电后延时 1ms 再初始化 SPI 和访问芯片。另外MRAM 虽然写入不需要擦除但它对电源跌落比较敏感——如果写入过程中 VDD 突然掉到 2.7V 以下正在写的那个字节可能处于不确定状态。所以工业场景下我强烈建议在 VDD 附近加一颗 100uF 的储能电容配合掉电检测电路给 MCU 争取几十毫秒的善后时间。2.3 PCB 布局SPI 走线长度与串阻的选择SPI 在低速时随便走都能通但一旦上到 10MHz 以上走线就是传输线了。我的经验是SCK 走线尽量短最好不超过 5cm并且远离模拟信号和电源开关节点。在 SCK 和 MOSI 上串 22Ω 到 33Ω 的电阻靠近 MCU 端放置用来抑制过冲和振铃。我实测过不串阻时 16MHz 下 SCK 上升沿有明显振铃串 33Ω 后波形干净很多。CS 走线不要和 SCK 平行长距离走否则 CS 上的噪声可能被耦合进时钟。MRAM 的 VDD 去耦电容 0.1uF 必须紧贴芯片引脚另外再并一颗 1uF 的。这些细节在实验室里可能看不出问题但到了工业现场电磁环境复杂EMC 测试时就会暴露。我有个项目就是因为 SCK 走线太长又没串阻辐射发射测试超标后来重新改板加了串阻才过。3. SPI 驱动层从寄存器配置到字节读写3.1 MSSP 初始化几个容易配错的位PIC18F46K42 的 SPI 初始化涉及 SSP1CON1、SSP1STAT、SSP1ADD 等寄存器。我用的是 MCCMPLAB Code Configurator生成基础代码但生成之后一定要自己检查几个关键位// SPI 主机模式初始化Fosc/4时钟极性空闲低数据在上升沿采样 SSP1STATbits.CKE 1; // 时钟边沿空闲到有效时采样 SSP1STATbits.SMP 0; // 采样点数据末尾采样高速时用 SSP1CON1bits.CKP 0; // 时钟极性空闲低电平 SSP1CON1bits.SSPM 0b0000; // SPI 主机模式Fosc/4 SSP1CON1bits.SSPEN 1; // 使能 MSSPCKE 和 CKP 的组合决定了 SPI 模式。MR25H40CDF 支持 SPI Mode 0CPOL0, CPHA0和 Mode 3CPOL1, CPHA1。我习惯用 Mode 0即 CKP0、CKE1。这里有个坑Microchip 的 CKE 定义和标准 CPHA 是反的CKE1 对应数据在时钟第一个边沿采样也就是 CPHA0。很多人照着标准 SPI 文档配结果读出来全是错位的数据就是栽在这个反逻辑上。另外SMP 位在高速时建议置 0末尾采样给从机数据建立时间留余量。低速时置 1 也无所谓但统一用 0 更稳。3.2 单字节收发函数为什么不能只发不收SPI 是全双工主机每发一个字节同时会收到一个字节。MR25H40CDF 的命令格式是命令字节 地址 数据读操作时发完命令和地址后继续发 dummy 字节来换回数据。所以底层需要一个既能发又能收的函数uint8_t SPI_ExchangeByte(uint8_t txData) { SSP1BUF txData; // 写入发送缓冲 while(!PIR3bits.SSP1IF); // 等待传输完成 PIR3bits.SSP1IF 0; // 清中断标志 return SSP1BUF; // 读取接收缓冲 }注意读 SSP1BUF 这个动作本身会清 BF 标志所以顺序不能乱。我见过有人先读 SSP1BUF 再清 SSP1IF结果偶尔丢字节。正确顺序是等 SSP1IF、清 SSP1IF、再读 SSP1BUF。3.3 CS 控制与命令时序MR25H40CDF 的所有操作都以 CS 拉低开始、拉高结束。命令序列是CS 拉低发命令字节如 0x03 读、0x02 写、0x06 写使能发 3 字节地址24 位地址因为 512KB 需要 19 位地址高位补 0读或写数据CS 拉高写操作前必须先发 WREN0x06命令这是 MRAM 和 EEPROM、Flash 一致的地方。但 MRAM 不需要擦除命令也不需要等待写完成没有内部写周期发完数据拉高 CS 就完事了下一个操作可以立即开始。这一点在实时性要求高的场景下非常爽——EEPROM 写完要等 5msMRAM 写完下一个 SPI 周期就能读。void MRAM_WriteEnable(void) { CS_LOW(); SPI_ExchangeByte(0x06); // WREN CS_HIGH(); } void MRAM_WriteByte(uint32_t addr, uint8_t data) { MRAM_WriteEnable(); CS_LOW(); SPI_ExchangeByte(0x02); // WRITE 命令 SPI_ExchangeByte((addr 16) 0xFF); SPI_ExchangeByte((addr 8) 0xFF); SPI_ExchangeByte(addr 0xFF); SPI_ExchangeByte(data); CS_HIGH(); }4. 读写逻辑设计地址管理、页操作与数据完整性4.1 512KB 空间的地址规划思路MR25H40CDF 的 512KB 空间如果当成一个大数组随便写很快就会乱。我在项目里会做明确的分区规划类似文件系统的思路地址范围用途写入频率0x00000 - 0x000FF设备信息区序列号、固件版本出厂写一次0x00100 - 0x00FFF配置参数区改配置时写0x01000 - 0x3FFFF运行日志环形缓冲区高频写0x40000 - 0x7FFFF故障快照区故障时写为什么要分区一是逻辑清晰二是便于做磨损均衡虽然 MRAM 寿命极长但分区后日志区可以循环覆盖管理简单三是方便单独擦除或保护某个区域。比如设备信息区可以在出厂后通过 WP 引脚或软件写保护锁死防止现场误改。4.2 环形缓冲区高频写入的核心结构运行日志是写入最频繁的部分我用环形缓冲区来管理。核心是两个指针写指针write_ptr和读指针read_ptr都存在 MRAM 的固定位置每次写入后更新写指针。typedef struct { uint32_t write_ptr; // 当前写入位置 uint32_t read_ptr; // 当前读取位置 uint32_t count; // 已存记录数 } RingBufferCtrl; void RingBuffer_Write(uint8_t *data, uint16_t len) { uint32_t addr LOG_BASE (ctrl.write_ptr * RECORD_SIZE); MRAM_WritePage(addr, data, len); ctrl.write_ptr (ctrl.write_ptr 1) % MAX_RECORDS; if(ctrl.count MAX_RECORDS) ctrl.count; // 更新控制结构存在固定地址 MRAM_WritePage(CTRL_ADDR, (uint8_t*)ctrl, sizeof(ctrl)); }这里有个关键设计控制结构每次写完日志都要更新。如果控制结构更新到一半掉电指针就乱了。我的做法是控制结构存两份A/B 双备份每份带 CRC 校验读取时选 CRC 正确且版本号新的那份。这样即使掉电打断至少有一份是完整的。4.3 数据完整性CRC 校验与写入确认工业数据不能只写不验。我的做法是每条记录末尾附 2 字节 CRC16读取时校验。写入流程是写数据 → 回读 → 比对 → 确认虽然多花一倍时间但能发现 SPI 通信中的偶发错误。uint8_t MRAM_WriteVerify(uint32_t addr, uint8_t *data, uint16_t len) { MRAM_WritePage(addr, data, len); uint8_t readback[64]; MRAM_ReadPage(addr, readback, len); return (memcmp(data, readback, len) 0) ? 0 : 1; }实测下来在 SPI 时钟 8MHz、走线规范的板子上回读比对几乎不会失败。但如果走线差、时钟高偶发位翻转是有的这时候回读重写就很有必要。5. 掉电保护让最后一条数据不丢的完整方案5.1 掉电检测电路比软件判断更可靠掉电保护的核心是提前知道要掉电了。光靠软件读 VDD 是来不及的因为 ADC 采样和判断本身要时间。我的方案是用一颗电压监控芯片如 MCP1316监测 VDD当电压跌到阈值时产生中断MCU 在中断里立刻把关键数据写进 MRAM。电路上VDD 主路经过一个二极管给储能电容充电电容再给 MCU 和 MRAM 供电。正常时电容充满掉电时二极管反向截止电容里的电还能撑几十毫秒。以 100uF 电容、MCU MRAM 总电流 10mA 计算从 3.3V 掉到 2.7V 的时间大约是t C × ΔV / I 100uF × 0.6V / 10mA 6ms6ms 足够写几十条记录了。如果不够加大电容到 470uF 或 1000uF。5.2 中断服务程序里该做什么、不该做什么掉电中断用外部中断 INT 或 IOC里只做最紧急的事把当前运行状态、最后几条未落盘的日志写进 MRAM。不要在中断里做复杂计算、不要打印调试信息、不要操作其他外设。void __interrupt() ISR(void) { if(PIR0bits.INT0IF) { PIR0bits.INT0IF 0; // 紧急保存只写关键数据 SaveCriticalData(); // 标记系统异常掉电 MRAM_WriteByte(FLAG_ADDR, 0xA5); while(1); // 等待电容放电完毕 } }注意SaveCriticalData 里用的 SPI 操作必须是阻塞式的、不依赖任何可能被中断打断的状态机。我见过有人在中断里调用带 DMA 的异步写函数结果 DMA 还没完成电就没了数据全丢。5.3 上电恢复如何判断上次是否异常掉电上电初始化时先读 FLAG_ADDR。如果是 0xA5说明上次是异常掉电需要做数据恢复检查环形缓冲区的控制结构用 CRC 校验选出完整的那份重建读写指针。如果是正常关机FLAG 会被清零。这个机制我用了好几年在现场设备上验证过几百次断电测试没有丢过数据。关键点是FLAG 的写入必须在所有关键数据写完之后作为最后一步执行。顺序反了就会出现标记了异常但数据没写完的误判。6. 实测性能与几个只有踩过才知道的坑6.1 实测读写速度数据我在 8MHz SPI 时钟下实测PIC18F46K42 64MHzSPI 分频 8操作耗时说明单字节写约 3.5us含 WREN 命令 地址 数据单字节读约 2.8us含命令 地址 dummy64 字节页写约 180us连续写无内部延迟64 字节页读约 150us连续读1KB 连续写约 2.8ms平均 2.7us/字节对比同容量 EEPROM页写 64 字节要 5ms 左右MRAM 快了将近 30 倍。而且 MRAM 没有写完成等待写完立即可以读这在实时系统里价值巨大。6.2 坑一SPI 模式配错导致读出的数据整体移位前面提过 CKE 的反逻辑这里再强调一次。我第一次用 PIC18F46K42 驱动 MRAM 时按标准 SPI Mode 0 配了 CKP0、CKE0结果读出来的数据每个字节都像是左移了一位。用逻辑分析仪抓波形才发现采样边沿错了。正确配置是 CKP0、CKE1。这个坑在 Microchip 的论坛上问的人特别多本质是厂商对 CPHA 的定义和标准文档不一致。6.3 坑二CS 拉高后立即拉低MRAM 没反应过来MRAM 的 CS 有最小高电平时间要求tSHSL约 10ns 量级一般不会违反。但如果你的代码里 CS 拉高后紧接着又拉低开始下一个命令而中间没有足够的指令周期在高速 MCU 上可能出问题。我的做法是在 CS 操作后加一个 __delay_us(1)虽然理论上不需要但实测能避免一些玄学问题。6.4 坑三HOLD 引脚悬空导致随机读失败这个前面提过但值得单独说。HOLD 悬空时如果板子上有噪声耦合进来MRAM 会进入保持状态SPI 通信被暂停读出的数据就是错的。HOLD 和 WP 不用时一定要拉高最好直接接到 VDD中间不要走长线。6.5 坑四写入频率过高时的电源噪声MRAM 写入时电流会比读大一些如果电源去耦不足高频写入会导致 VDD 上有纹波严重时影响 MCU 本身。我在日志区做连续高速写入测试时发现 MCU 偶尔复位后来在 MRAM 的 VDD 脚旁边加了 0.1uF 1uF 组合问题消失。去耦电容不是随便放的要尽量靠近芯片引脚回路面积越小越好。7. 这套方案适合什么场景以及后续可以怎么扩展把 MR25H40CDF 和 PIC18F46K42 这套组合用下来我的体会是它最适合写入频繁、掉电不能丢、环境恶劣的工业数据记录场景。比如电力监测终端、工业网关的日志存储、医疗设备的运行记录、车载数据记录仪。这些场景的共同点是对数据可靠性要求高而对存储成本不那么敏感。如果你的项目只是存个配置、偶尔写一次那用 SPI Flash 或 EEPROM 更划算。选型这件事永远是需求驱动不是技术越新越好。后续扩展上有几个方向可以考虑一是把 MRAM 做成一个简单的文件系统支持按文件名读写方便上层应用二是结合 PIC18F46K42 的 DMA如果有或者用 SPI 的 FIFO 模式提升吞吐三是在 MRAM 里实现一个轻量级的事务日志保证多条记录的原子性写入。这些我在其他项目里做过部分实现有机会再单独拆解。最后分享一个我自己的习惯每次新板子回来第一件事不是跑业务代码而是写一个 SPI 压力测试程序连续读写 MRAM 几万次同时用逻辑分析仪盯着波形。这一步能提前暴露 90% 的硬件和时序问题比后面调试业务逻辑时才发现要省事得多。
返回列表