首页 / 资讯中心 / 文章详情

工业嵌入式存储选型:MRAM与PIC18F87K22实战指南

工业嵌入式存储选型:MRAM与PIC18F87K22实战指南 ★ FEATURED ARTICLE
1. 为什么在工业场景里我会优先考虑 MRAM 而不是 EEPROM做工业嵌入式这行十几年最怕听到客户说一句话“现场数据又丢了。”尤其是那些部署在配电柜、远程泵站、冷链仓储里的设备断电是家常便饭有些场合甚至是每天定时拉闸。早些年我用 24C 系列 EEPROM 存参数用着用着就发现两个绕不开的坎一是写入速度慢字节写要等 5ms 左右页写也就那样频繁记录运行日志的时候 CPU 大量时间耗在等写完成上二是擦写寿命有限标称 100 万次实际在高温高湿环境下打个对折都算乐观日志写得勤一点两三年就开始出现坏块。后来接触到 MR25H40CDF 这颗 MRAM才算真正把“频繁写 断电不丢 长寿命”这三件事同时解决掉。MRAM 的存储机理和 Flash、EEPROM 完全不同它靠的是磁性隧道结的磁化方向来记录 0 和 1没有浮栅、没有电荷泵、没有擦除动作。带来的直接好处就是写入不需要先擦除字节级随机写写一个字节和写一整页在时间上没有本质差别而且官方标称的耐久度是 10^14 次以上量级这个数字对绝大多数工业应用来说基本等于“无限次”。MR25H40CDF 的容量是 4Mbit也就是 512KB组织成 512K × 8 位。接口是标准的 SPI支持 Mode 0 和 Mode 3最高时钟能跑到 40MHz。这个容量放在今天看不算大但对于存配置参数、校准系数、运行日志、故障快照、掉电前的现场状态这些用途绰绰有余。我手上一个远程监控终端项目用它存每 10 秒一条的运行记录每条 32 字节算下来能存 16000 多条按环形覆盖的方式写跑了大半年一点问题没有。选它还有一个很现实的原因SPI 接口意味着几乎任何 MCU 都能直接挂。这次我搭配的是 PIC18F87K22一颗 8 位机里比较能打的型号自带 MSSP 模块支持 SPI 主模式128KB Flash、4KB RAM引脚多、外设全工业现场用得很广。8 位机配 512KB 的 MRAM听起来有点“小马拉大车”但实际用下来完全够因为 MRAM 的读写是流式的MCU 只管把字节推出去不需要自己做复杂的块管理。提示MRAM 虽然写入不需要擦除但它并不是“随便写不用管”的存储器。上电后的写保护配置、状态寄存器轮询、以及 SPI 时序的建立/保持时间这几处如果处理不好照样会出现写不进去或者读出来全是 0xFF 的情况。后面我会把这些坑一个个拆开讲。这篇文章我打算按实际项目的推进顺序来写先讲硬件怎么连、SPI 模式怎么定再讲 PIC18F87K22 的 MSSP 初始化细节然后是 MR25H40CDF 的读写指令集和状态机接着是数据可靠性的处理校验、掉电保护、磨损均衡其实不需要但环形管理要做最后是实测中踩过的坑和排查过程。如果你手上正好有这两颗料或者正在选型工业存储方案这篇可以直接拿去抄作业。2. 硬件连接与 SPI 模式选择别小看那两根线的走法2.1 MR25H40CDF 的引脚定义与 PIC 端映射MR25H40CDF 是 8 引脚封装引脚定义很标准CS片选低有效、SO数据输出从机到主机、SI数据输入主机到从机、SCK时钟、VDD、VSS另外还有 WP写保护和 HOLD保持两个引脚。这里有个细节很多人会忽略WP 和 HOLD 在 MR25H40CDF 上是复用功能的具体行为取决于状态寄存器的配置位。如果你不打算用硬件写保护和总线暂停功能这两个脚必须拉到 VDD不能悬空。我见过有人把 HOLD 悬空结果 SPI 通信时好时坏查了两天才发现是 HOLD 引脚被干扰拉低芯片进入了保持状态SO 变成高阻。PIC18F87K22 这边我用的是 MSSP1 模块映射到 RC3SCK、RC4SDI/SI、RC5SDO/SO片选用 RB0 普通 IO 控制。这里要注意 PIC18F87K22 的 MSSP 模块在 SPI 主模式下SDO 是输出、SDI 是输入和 MRAM 的 SI/SO 是交叉连接的别接成同名对同名。MR25H40CDF 引脚方向PIC18F87K22 引脚说明CS输入RB0软件控制片选低有效SCK输入RC3/SCKMSSP1 时钟输出SI输入RC5/SDO主机输出到从机输入SO输出RC4/SDI从机输出到主机输入WP输入VDD不用硬件写保护则拉高HOLD输入VDD不用总线暂停则拉高VDD电源3.3V注意电压域VSS地GND就近接地2.2 电压域与电平匹配的坑MR25H40CDF 的工作电压是 2.7V 到 3.6V典型 3.3V。PIC18F87K22 有宽压版本但我用的是 3.3V 供电的那档所以电平直接匹配不需要电平转换。如果你手上是 5V 供电的 PIC18 型号那就必须加电平转换否则 MRAM 的输入脚会被 5V 打坏。这一点在选型阶段就要确认清楚别等板子打回来才发现。另外MRAM 的 VDD 去耦电容我建议放 0.1μF 加 1μF 组合尽量靠近芯片引脚。MRAM 在写入瞬间的电流会有小幅波动虽然不像 Flash 编程那样有大的尖峰但工业现场电源纹波本来就大多一颗电容不亏。2.3 SPI 模式 0 还是模式 3MR25H40CDF 支持 SPI Mode 0CPOL0CPHA0和 Mode 3CPOL1CPHA1。这两种模式的区别在于空闲时时钟是高还是低以及数据在哪个边沿采样。我一般选 Mode 0原因是 PIC18F87K22 的 MSSP 在 Mode 0 下配置最直观SCK 空闲为低数据在 SCK 上升沿被从机采样下降沿输出。Mode 3 也能用但如果你系统里还挂了别的 SPI 从设备统一模式能省很多事。配置 MSSP1 的关键寄存器是 SSP1CON1 和 SSP1STAT。Mode 0 对应 SSP1CON1 的 CKP0空闲低SSP1STAT 的 CKE0数据在时钟从空闲态到有效态的边沿发送。实际写出来就是// PIC18F87K22 MSSP1 初始化SPI 主模式Mode 0时钟 Fosc/16 SSP1STAT 0x40; // CKE0SMP0在数据输出末尾采样 SSP1CON1 0x21; // SSPEN1CKP0SPI 主模式 Fosc/16 SSP1ADD 0x00; // 不用地址掩码 TRISC3 0; // SCK 输出 TRISC5 0; // SDO 输出 TRISC4 1; // SDI 输入 TRISB0 0; // CS 输出 CS 1; // 初始拉高不选中时钟分频这里我选 Fosc/16。假设系统时钟 64MHz分频后 SCK 是 4MHzMRAM 完全能吃下。为什么不直接上 40MHz因为 PIC18F87K22 的 MSSP 在 64MHz 系统时钟下最高也就 Fosc/4 即 16MHz而且长走线、工业干扰环境下时钟太快容易出错。4MHz 是个稳妥的折中实测读写 512KB 全片也就一百多毫秒对工业应用完全够。注意SSP1STAT 的 SMP 位决定采样时刻。SMP0 是在数据输出时间的末尾采样SMP1 是在中间采样。高速时用 SMP0 更稳低速时两者都行。我统一用 SMP0。3. PIC18F87K22 的 MSSP 驱动封装把字节收发做扎实3.1 单字节收发的底层函数SPI 通信的本质就是“换字节”主机发一个字节出去同时从机回一个字节进来。PIC18F87K22 的 MSSP 用 SSP1BUF 寄存器完成这件事写进去启动发送读出来拿到接收数据。判断完成靠 SSP1IF 中断标志或者 PIR1 的 SSP1IF 位。我习惯用查询方式简单可靠unsigned char SPI1_ExchangeByte(unsigned char txData) { SSP1BUF txData; // 写入待发送数据启动传输 while (!PIR1bits.SSP1IF); // 等待传输完成 PIR1bits.SSP1IF 0; // 清标志 return SSP1BUF; // 读取接收到的数据 }这个函数看着简单但有两个地方容易出问题。第一如果 SCK 配置错误或者 CS 没拉低SSP1IF 永远不置位程序就死在这里。所以实际项目里我会加超时保护unsigned char SPI1_ExchangeByte_Timeout(unsigned char txData, unsigned int *timeout) { SSP1BUF txData; while (!PIR1bits.SSP1IF) { if (--(*timeout) 0) return 0xFF; // 超时返回 } PIR1bits.SSP1IF 0; return SSP1BUF; }第二SSP1BUF 是双缓冲的读之前必须确认 SSP1IF 已经置位否则读到的是上一次的残留数据。这个坑我在早期调试时踩过读出来的 ID 总是差一位后来发现是没等标志就提前读了。3.2 片选控制的时序要求MR25H40CDF 对 CS 的时序有明确要求CS 拉低到第一个 SCK 上升沿之间要有至少 5ns 的建立时间最后一个 SCK 下降沿到 CS 拉高之间也要有至少 5ns 的保持时间。在 4MHz 时钟下一个时钟周期 250ns这些时间自然满足。但如果你把时钟提到 16MHz 以上就要注意在 CS 操作前后加几个 NOP 或者用示波器确认。我的做法是把片选操作封装成宏并在拉低和拉高前后各加一个空操作保证时序余量#define MRAM_CS_LOW() do { CS 0; NOP(); } while(0) #define MRAM_CS_HIGH() do { NOP(); CS 1; } while(0)3.3 多字节读写与地址递增MR25H40CDF 的读指令READ0x03和写指令WRITE0x02都支持地址自动递增。发完 3 字节地址后可以连续读或写任意多个字节芯片内部地址指针会自动加一到 0x7FFFF 后回卷到 0x00000。这个特性让批量读写非常方便void MRAM_ReadBytes(unsigned long addr, unsigned char *buf, unsigned int len) { unsigned int i; MRAM_CS_LOW(); SPI1_ExchangeByte(0x03); // READ 指令 SPI1_ExchangeByte((addr 16) 0xFF); // 地址高字节 SPI1_ExchangeByte((addr 8) 0xFF); // 地址中字节 SPI1_ExchangeByte(addr 0xFF); // 地址低字节 for (i 0; i len; i) { buf[i] SPI1_ExchangeByte(0x00); // 发哑元收数据 } MRAM_CS_HIGH(); }写操作稍微复杂一点因为写之前要发 WREN0x06指令使能写锁存写完还要等状态寄存器的 WIP 位清零void MRAM_WriteBytes(unsigned long addr, const unsigned char *buf, unsigned int len) { unsigned int i; MRAM_CS_LOW(); SPI1_ExchangeByte(0x06); // WREN MRAM_CS_HIGH(); MRAM_CS_LOW(); SPI1_ExchangeByte(0x02); // WRITE 指令 SPI1_ExchangeByte((addr 16) 0xFF); SPI1_ExchangeByte((addr 8) 0xFF); SPI1_ExchangeByte(addr 0xFF); for (i 0; i len; i) { SPI1_ExchangeByte(buf[i]); } MRAM_CS_HIGH(); MRAM_WaitWriteComplete(); // 等 WIP 清零 }3.4 状态寄存器轮询WIP 位到底要不要等MRAM 的写操作是异步完成的写入指令发完后芯片内部还在把数据写进存储阵列这时候状态寄存器的 WIPWrite In Progress位是 1。理论上你必须等 WIP 变 0 才能发下一条写指令否则新指令会被忽略。但 MRAM 的写入速度极快一个字节的写入时间在几十纳秒量级实际上等你发完 CS 拉高再发下一条指令WIP 早就清零了。不过我在代码里还是保留了轮询原因是工业现场温度范围宽-40℃ 到 85℃ 都要工作低温下写入时间会变长。轮询函数这样写void MRAM_WaitWriteComplete(void) { unsigned char status; do { MRAM_CS_LOW(); SPI1_ExchangeByte(0x05); // RDSR status SPI1_ExchangeByte(0x00); MRAM_CS_HIGH(); } while (status 0x01); // WIP 位 }这里有个细节RDSR 指令读出来的状态寄存器bit0 是 WIPbit1 是 WEL写使能锁存bit2 是 BP0bit3 是 BP1。BP0/BP1 是块保护位可以保护部分地址区域不被写入。如果你不需要块保护初始化时把状态寄存器写成 0x00 就行。4. MR25H40CDF 的指令集与初始化流程4.1 上电后的第一件事读 ID 确认通信每次上电我做的第一件事不是急着读写数据而是读 MRAM 的设备 ID。MR25H40CDF 支持 RDID0x9F指令返回 1 字节的制造商 ID 和 2 字节的设备 ID。制造商 ID 是 0xE0设备 ID 是 0x22 和 0x08具体以数据手册为准。读 ID 能一次性验证三件事SPI 模式对不对、片选有没有生效、芯片有没有坏。unsigned char MRAM_ReadID(unsigned char *idBuf) { MRAM_CS_LOW(); SPI1_ExchangeByte(0x9F); // RDID idBuf[0] SPI1_ExchangeByte(0x00); // 制造商 ID idBuf[1] SPI1_ExchangeByte(0x00); // 设备 ID 高字节 idBuf[2] SPI1_ExchangeByte(0x00); // 设备 ID 低字节 MRAM_CS_HIGH(); return (idBuf[0] 0xE0) ? 1 : 0; }如果读出来全是 0xFF说明 SO 线没接好或者 CS 没拉低如果读出来全是 0x00说明 SI 线可能短路到地。这两种情况我都遇到过排查起来其实很快关键是养成上电先读 ID 的习惯。4.2 状态寄存器配置与写保护MR25H40CDF 的状态寄存器只有 1 个字节可写的位包括 WEL、BP0、BP1。WEL 是写使能锁存每次写操作前必须通过 WREN 指令置 1写完成后自动清零。BP0/BP1 用来保护地址区域BP1BP0保护区域00无保护010x60000 - 0x7FFFF100x40000 - 0x7FFFF11全片保护工业项目里我一般把关键参数放在低地址区日志放在高地址区然后把 BP 位设成 0b01保护高地址区的日志不被误写。但要注意一旦设了块保护写保护区域内的数据就写不进去了调试阶段容易忘了这回事以为芯片坏了。4.3 初始化流程的完整顺序把上面的东西串起来上电初始化按这个顺序走配置 PIC18F87K22 的 MSSP1 为 SPI 主模式Mode 0Fosc/16。配置 CS、SCK、SDO 为输出SDI 为输入。CS 拉高延时 1ms 等 MRAM 内部上电稳定。读 RDID确认返回 0xE0。读状态寄存器确认 WIP0。如需块保护发 WREN 后写状态寄存器。开始正常读写。这个顺序里第 3 步的延时容易被省掉。MRAM 的上电时间是微秒级但 PIC 的 MSSP 初始化也需要时间加 1ms 延时能避免上电瞬间的时序竞争。我在一个低温测试项目里-40℃ 下如果不加这个延时读 ID 会偶发失败加了之后就稳了。5. 数据可靠性设计校验、环形日志与掉电保护5.1 为什么 MRAM 也需要数据校验有人会问MRAM 不是不会丢数据吗为什么还要校验这里要区分两件事存储单元的可靠性和数据传输的可靠性。MRAM 的存储单元确实很稳10^14 次写入寿命、数据保持 20 年以上但 SPI 传输过程中如果受到工业干扰字节可能传错。另外如果 MCU 在写数据的过程中掉电可能只写了一半留下一条不完整的记录。所以校验和掉电保护还是要做。我的做法是每条记录加 2 字节 CRC16 和 1 字节状态标志。状态标志用 0xA5 表示“这条记录有效”写入顺序是先写数据、再写 CRC、最后写状态标志。这样即使掉电发生在写状态标志之前这条记录也不会被误认为是有效的。typedef struct { unsigned long timestamp; unsigned char data[24]; unsigned short crc; unsigned char valid; // 0xA5 表示有效 } LogRecord;5.2 环形日志的地址管理512KB 的空间我划出前 64KB 存配置参数和校准系数后 448KB 做环形日志区。环形日志的关键是维护一个写指针每次写完一条记录指针后移到末尾回卷到起始地址。写指针本身也要存我把它存在配置区的固定位置每次上电读出来。这里有个细节写指针的更新和日志记录的写入不是原子的。如果写完日志、还没更新写指针就掉电下次上电会覆盖同一条记录。解决办法是写指针用“双备份 序号”的方式存两个位置轮流写每个位置带一个递增序号上电时取序号大的那个。这个思路和很多文件系统的超级块管理是一样的。5.3 掉电检测与紧急保存PIC18F87K22 有内置的欠压复位BOR和低压检测LVD模块。我把 LVD 配置成在电源跌落到 2.9V 时触发中断中断里立刻把关键状态写进 MRAM。因为 MRAM 写入不需要擦除、速度快从检测到掉电到写完几十字节在毫秒级时间内就能完成而电源上的大电容能撑住这几十毫秒。void __interrupt(high_priority) HighISR(void) { if (PIR2bits.LVDIF) { PIR2bits.LVDIF 0; MRAM_WriteBytes(EMERGENCY_ADDR, (unsigned char*)g_state, sizeof(g_state)); // 其他紧急处理 } }这个机制我在一个冷链监控项目里实测过直接拔电源每次都能把最后的状态存下来。注意 LVD 的阈值要选得比 BOR 高一点否则 BOR 先复位了中断根本没机会执行。6. 实测踩坑记录从读不出 ID 到稳定运行6.1 第一个坑读 ID 返回 0xFF板子第一次上电读 ID 返回全 0xFF。按经验0xFF 通常意味着 MISO 线上没有数据回来。我先用示波器看 SCK有波形频率也对再看 CS拉低正常最后看 SO发现 SO 一直是高电平没有翻转。查原理图发现 SO 和 SI 接反了——我把主机的 SDO 接到了从机的 SO 上等于两个输出对顶。改过来之后ID 正常读出 0xE0。这个坑的教训是SPI 的 SI/SO 命名是从从机角度定义的主机和从机之间必须交叉连接。很多新手会按同名连接结果就是读不到数据。6.2 第二个坑写入后读出来是旧数据ID 读出来后我写了一段测试数据再读出来发现还是旧数据。排查发现是 WREN 指令没有正确发送。WREN 必须在 CS 拉低后单独发一个字节然后 CS 拉高再重新拉低发 WRITE 指令。我一开始把 WREN 和 WRITE 放在同一个 CS 周期里结果 WREN 被忽略了。MRAM 的写使能锁存是在 CS 上升沿锁存的所以 WREN 必须独立一个 CS 周期。6.3 第三个坑高速时钟下的偶发错误把 SCK 从 4MHz 提到 10MHz 后连续读写测试跑一万次会出现几次数据错误。用示波器看波形发现 SCK 上升沿有过冲SO 线上的数据在采样点附近有振铃。这是典型的信号完整性问题。解决办法是在 SCK 和 SO 线上串 22Ω 电阻并把走线缩短。改板后 10MHz 下跑十万次无错误。后来我干脆把时钟定在 4MHz工业现场稳定压倒一切。6.4 第四个坑低温下写不进去产品做高低温测试-40℃ 下写日志偶尔失败。查下来是 MRAM 在低温下写入时间变长而我的代码在写完后没有等 WIP 清零就发下一条指令。加上MRAM_WaitWriteComplete()之后问题消失。这也印证了前面说的WIP 轮询在宽温场景下不能省。7. 几个让项目更稳的工程习惯7.1 地址空间规划表项目一开始就把地址空间规划好写进文档后面维护会轻松很多。我的习惯是地址范围用途大小0x00000 - 0x000FF设备信息与版本256B0x00100 - 0x00FFF配置参数3.5KB0x01000 - 0x01FFF校准系数4KB0x02000 - 0x0FFFF预留56KB0x10000 - 0x7FFFF环形日志区448KB7.2 读写函数的返回值设计底层读写函数我全部返回状态码而不是 void。这样上层调用能知道这次操作到底成没成方便做重试和错误统计。比如MRAM_ReadBytes返回 0 表示成功返回 1 表示超时返回 2 表示 CRC 错误。工业设备最怕的就是“悄悄失败”有返回值就能在日志里记下来。7.3 定期自检设备运行过程中我每隔 24 小时做一次存储自检在预留区写一个测试模式读回来比对然后擦掉MRAM 不需要擦直接覆盖。这个自检能提前发现存储器的潜在问题比等用户报故障再查要好得多。7.4 关于 MRAM 的寿命别被数字冲昏头10^14 次这个数字确实很吓人但要注意它是“每个存储单元”的耐久度而且是在特定测试条件下的标称值。实际使用中高温会加速老化频繁写入同一地址总归不是好事。所以我在做环形日志时尽量让写入地址均匀分布不要盯着一个地址反复写。虽然 MRAM 不像 Flash 那样有明确的擦写次数限制但均匀写入总归是更稳妥的工程习惯。8. 这套方案适合什么场景不适合什么场景MR25H40CDF 加 PIC18F87K22 这套组合最适合的是中小容量的工业数据存储场景设备配置参数、校准数据、运行日志、故障快照、掉电现场保存。它的优势是写入快、寿命长、接口简单、宽温可靠。512KB 的容量对于存文本日志和参数来说够用但如果你要存图像、音频或者大量历史数据这个容量就捉襟见肘了那种场景应该考虑更大容量的存储方案。另外PIC18F87K22 是 8 位机处理能力有限如果你需要跑复杂的文件系统或者网络协议栈建议换 32 位平台。但如果你只是做数据采集、逻辑控制加存储这套组合的性价比和可靠性都很能打。我在好几个远程终端项目里用这套方案现场跑了三年多没有因为存储问题返修过。最后分享一个我在调试阶段常用的小工具写一个简单的串口命令解析器通过串口发指令来读写 MRAM 的任意地址比如read 0x10000 64读 64 字节write 0x10000 01 02 03写几个字节。这个工具在排查数据问题时特别有用不用反复烧程序直接在线看存储内容。代码量不大但能省下大量调试时间。
阅读完成 · 觉得有帮助?
咨询建站