ARTICLE DETAIL

资讯详情

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

PIC32驱动SPI MRAM实现工业掉电保存:从选型到驱动实战

PIC32驱动SPI MRAM实现工业掉电保存:从选型到驱动实战 前阵子给一台工业设备做控制器升级旧方案用SPI接口的EEPROM存运行参数和故障记录。参数每隔十几秒就要更新一次照理说EEPROM的100万次写寿命应该够用实际跑了两个多月就开始出现偶发丢数排查下来才发现是频繁改写导致的坏块问题。最后我把存储芯片换成了 MR25H40CDF 这颗SPI接口的MRAM主控用的是 Microchip 的 PIC32MX675F256L。这篇文章就把整套存储与读取方案从选型、硬件、驱动到实测数据完整梳理一遍给正在做类似嵌入式数据记录项目的朋友一个可直接参考的模板。文章会重点讲清楚三件事为什么在这个场景下MRAM比EEPROM和Flash更合适PIC32MX675F256L的SPI外设到底怎么配置才算稳以及工业环境里掉电保存和故障日志应该怎么设计才不丢数据。如果你正在纠结嵌入式存储选型或者第一次在PIC32上驱动MRAM这篇内容应该能帮你省不少弯路。1. 项目整体思路与选型拆解1.1 存储介质选型为什么是 MR25H40CDF 而不是 EEPROM/FlashMR25H40CDF 是 Everspin 的 4Mbit SPI 接口 MRAM换算下来是 512KB 容量。MRAM 的核心原理是磁阻效应数据保存在磁隧道结的磁化方向上写入过程直接改变存储单元的磁化状态。这意味着它天生支持按字节覆盖写不需要像 Flash 那样先擦除整块再写。这个特性放在工业设备的参数存储场景里非常关键。设备里的运行参数比如温度阈值、补偿系数、累计运行时间更新频率高单次数据量小但要求掉电之后必须还在。EEPROM 虽然也支持字节写入但写操作要等毫秒级的时间而且寿命通常在 10 万到 100 万次之间高频次改写场景下很容易提前耗尽。NOR Flash 寿命也是 10 万次级别最关键的是写之前要整块擦除一旦在做擦除动作时掉电数据就处于不确定状态。MR25H40CDF 的写寿命标称在 10 的 14 次方这个量级基本上可以认为不需要考虑磨损问题。我打个比方Flash 和 EEPROM 像一张纸反复折叠几次就开始破损而 MRAM 更像一块白板写完擦掉再写几乎不磨损。另一个优势是写入不需要等待写完就是写完没有 EEPROM 那种“数据在飞”的时间窗口掉电也不用担心写到一半卡住。劣势也很明显成本比同容量的 SPI Flash 贵比 EEPROM 也贵普通消费类产品基本不会用。但工业控制器、医疗设备、电力仪表这类对可靠性要求高的行业多花这点钱换来的是调试和生产环节省心得多。一个典型的对比可以看下面这张表维度MR25H40CDFMRAMSPI EEPROMSPI NOR Flash写入寿命约10^14次约10^5~10^6次约10^5次写入前擦除不需要不需要但有写等待必须整块擦除单字节写入支持支持不支持按页/块擦写等待时间无3~5ms数十至数百ms掉电写入可靠性高写入原子完成中等低擦写过程掉电易损坏单价同容量高中低所以选型逻辑很清楚如果存储的是频繁改写的关键数据且掉电之后数据必须可靠MRAM 是省心方案。如果只存固件和静态资源Flash 完全够用没必要追 MRAM。1.2 主控选择PIC32MX675F256L 解决什么问题PIC32MX675F256L 是 Microchip PIC32MX 系列的一款MCU内核是 MIPS M4K最高主频 80MHz带 256KB Flash 和 64KB RAM。从规格上看它并不是性能最强的 MCU但在做工业控制器这一类场景里它有几个点非常合适。第一外设资源足够。PIC32MX675F256L 有多个 SPI 模块实际项目中我用 SPI1 接 MR25H40CDF还能留出另一路 SPI 给其它外部设备比如 ADC 或者另一个传感器。这对于一个同时要采集多路模拟量、控制输出、跟人机界面通信的控制板来说不会出现外设不够用的局面。第二外设引脚选择PPS机制灵活。PIC32MX 系列支持把 SPI 的时钟、数据线映射到不同引脚上。这意味着画 PCB 的时候不用非得把信号线绕到固定引脚布局自由度大很多。对于一个需要兼顾强电和弱电区域的设备来说这个特性非常实用可以显著减少走线交叉。第三工业级温度范围。控制设备工作环境可能是 -40 到 85℃ 甚至更高PIC32MX 系列的工业级型号覆盖了这个范围。MR25H40CDF 同样是工业级器件两者搭配不存在工作温度上的短板。还有一点MPLAB X IDE 和 MCC 工具链对 PIC32 的支持已经相当成熟寄存器操作也直接。对工程师来说从寄存器层面写 SPI 驱动并不复杂不像有些ARM平台还要过一遍HAL抽象层排问题的时候反而更容易看清时序。1.3 整体数据流设计存储驱动在系统中的位置在把代码拆开写之前我先说清楚整个系统里数据是怎么流转的。主控通过传感器采集数据经过运算后得到控制参数。一部分参数需要掉电保存比如 PID 系数、累计运行小时、故障码。另一部分是运行日志比如最近 N 条告警事件。这些数据经过序列化之后通过 SPI 写入 MR25H40CDF。上电时主控从 MRAM 里读取参数校验通过后恢复运行状态。因为 MRAM 是随机访问的我可以把它当作一块不丢失的 RAM 来用驱动层的工作量集中在 SPI 命令封装和地址管理上而不是像 Flash 那样还要设计坏块管理和擦除队列。驱动层在设计上分三层底层SPI 外设初始化、单字节收发、片选控制中间层MR25H40CDF 命令封装包括读、写、写使能、状态寄存器操作应用层把参数结构体、日志记录映射到 MRAM 地址空间这样分层的好处是如果后续换主控只需要重写底层和中间层应用层的数据结构不用动。如果换存储介质只要是 SPI 接口中间层改动也很小。2. 硬件接线与关键设计细节2.1 引脚分配与 SPI 接线方法MR25H40CDF 是 8 引脚封装常用引脚分别是 CS#、CK、SI、SO、WP#、HOLD#、VCC、VSS。对应到 PIC32MX675F256L我选用的是 SPI1 模块。具体接线关系如下表MR25H40CDFPIC32MX675F256L说明CKSCK1输出SPI 时钟SISDO1输出主发从收SOSDI1输入从发主收CS#任意 GPIO软件控制片选WP#3.3V 上拉禁用写保护HOLD#3.3V 上拉确保正常工作CS# 不建议用 PIC32 硬件 NSS 引脚来控制。原因很简单硬件 NSS 的自动行为在某些模式下会跟 SPI 收发产生耦合很容易出现“还没发完数据片选就跳了”的情况。用普通 GPIO 手动拉低、拉高时序完全可控排查问题的时候也直观。PIC32MX 系列支持 PPS 引脚映射所以 SCK1、SDO1、SDI1 这三根线不一定固定在默认引脚上需要在初始化代码里把对应引脚配置为外设功能。具体引脚号要以封装图和 PPS 映射表为准我先不写死后面代码里用注释说明。2.2 电源、去耦与特殊引脚处理MR25H40CDF 的工作电压是 3.3V 左右不能直接接 5V。如果主控供电是 5V一定要加电平转换否则不仅通信不稳定还可能损伤芯片。我当前这块板子主控和 MRAM 都是 3.3V 供电所以直接对接。电源引脚旁边放一个 100nF 陶瓷电容紧贴 VCC 和 VSS有条件的话再在稍远一点的位置放一个 10uF 钽电容或电解电容。工业环境里电源纹波通常比实验室恶劣这个措施能明显降低高频噪声耦合到 SPI 信号的概率。WP# 和 HOLD# 这两个引脚最容易踩坑。WP# 是写保护输入直接接地的话在某些状态寄存器配置下所有写命令都会被忽略表现就是“写了但是读回来还是旧值”。HOLD# 是通信暂停脚一旦被拉低MRAM 会忽略 SPI 时钟信号总线上的数据保持原状态。如果在工业现场这两个引脚悬空受到电磁干扰就可能出现偶发读写失败。这两颗引脚的推荐做法是各接一个 10kΩ 电阻上拉到 3.3V。上电过程中即使 MCU 还没初始化这两个引脚也处于确定的高电平状态不会误触发写保护或暂停。2.3 硬件设计容易踩的坑我实际画板时遇到过几个问题这里列出来提醒大家。第一个坑是片选信号离芯片太远。CS# 信号如果走线过长且旁边有电机驱动或者继电器触点很容易被干扰导致芯片误识别命令。解决办法是让 SPI 四根线尽量以一组信号线的方式走线远离功率走线必要的时候在原理图里加串联电阻做阻尼。第二个坑是 SPI 模式跟从机不匹配。MR25H40CDF 支持 SPI 模式 0 和模式 3主控必须对应配置。模式 0 是空闲时钟为低电平数据在上升沿采样模式 3 是空闲时钟为高电平采样沿也是上升沿。如果只配置了 SCK 极性没有同时确认采样沿很容易出现数据错位。第三个坑是共用一个 SPI 总线时片选干扰。如果板上还有其他 SPI 从机每个从机的 CS# 必须单独控制而且在一个从机通信结束时要把片选拉到确定电平不能浮空。浮空的片选引脚可能让多个从机同时响应总线MISO 上就会出现数据冲突。3. PIC32MX675F256L 驱动实现与读写流程3.1 SPI 外设初始化细节PIC32 的 SPI 模块初始化主要是配置 SPIxCON、SPIxBRG 和 SPIxSTAT。我用 SPI1先配置引脚复用再设置 SPI 参数。示例代码如下void mr25_spi_init(void) { // 1. 引脚复用把所用引脚配置为 SPI1 功能 // 以实际 PPS 映射为准例如 // RPB14R 0x07; // SDI1? 具体数值查阅器件手册 // 这里只演示框架 // 2. 关闭 SPI 后初始化 SPI1CON 0; SPI1STATbits.SPIROV 0; SPI1CONbits.MSTEN 1; // 主模式 SPI1CONbits.MODE16 0; // 8 位数据 SPI1CONbits.CKP 0; // 空闲时钟为低 SPI1CONbits.CKE 0; // 采样沿配置 SPI1CONbits.SMP 1; // 输入采样在数据末端 // 3. 波特率配置 SPI1BRG 1; // 4. 启动 SPI SPI1CONbits.ON 1; }波特率的计算公式是SPI 时钟频率 Fpb / (2 × (SPI1BRG 1))假设外设时钟 Fpb 配置为 40MHzSPI1BRG 取 1得到40MHz / (2 × (1 1)) 10MHz10MHz 对 MR25H40CDF 来说远低于它的最高时钟信号完整性余量很足。工业板上走线不可能像实验室那么短保守一点用 10MHz后续如果要做批量读写优化再调整 BRG 提到 20MHz 也不难。每次收发一个字节的函数可以这样写uint8_t spi_write_read_byte(uint8_t byte) { SPI1BUF byte; while (!SPI1STATbits.SPIRBF) { // 等待接收缓冲就绪 } return SPI1BUF; }这里有个细节值得注意SPI 是全双工协议每次读操作都需要主控先发一个字节来产生时钟读回来的内容在下一个字节里。所以读 MRAM 时发送地址字节的过程中MISO 上回来的是前一个字节对应的数据这个时序要在心里清楚否则容易把数据错位。3.2 MR25H40CDF 命令集与底层封装MR25H40CDF 的命令集跟常见 SPI 存储芯片类似最常用的是下面这几个命令操作码帧格式READ0x03命令 3字节地址 数据读出WRITE0x02命令 3字节地址 数据写入WREN0x06命令无后续帧WRDI0x04命令无后续帧RDSR0x05命令 状态寄存器读出WRSR0x01命令 状态寄存器写入注意地址是 3 字节因为 512KB 的容量需要 19 位地址用 24 位地址帧来覆盖整片空间。早期 1Mbit 以下容量的 MRAM 可能用 2 字节地址换成 MR25H40CDF 之后一定要改成 3 字节否则地址永远落在低 64KB 以内。写使能和写禁止是这套接口里的关键。MRAM 内部有一个写使能锁存位发送 WREN 命令之后状态寄存器里的 WEN 位置 1芯片才允许写入。写完一次之后 WEN 自动清零下一次写之前必须再发 WREN。这个机制跟 EEPROM 很像目的是防止误写。底层封装代码void mr25_write_enable(void) { MR25_CS_LOW(); spi_write_read_byte(0x06); // WREN MR25_CS_HIGH(); } void mr25_write_disable(void) { MR25_CS_LOW(); spi_write_read_byte(0x04); // WRDI MR25_CS_HIGH(); } uint8_t mr25_read_status(void) { uint8_t status; MR25_CS_LOW(); spi_write_read_byte(0x05); // RDSR status spi_write_read_byte(0x00); MR25_CS_HIGH(); return status; }写完数据之后我建议主动调一次 WRDI。虽然 WEN 会自动清零但主动清一次更保险尤其后面程序里如果还要继续写的话可以避免某些异常路径下 WEN 残留导致的意外改写。3.3 单字节与多字节读写实现单字节读和写是基础多字节连续读写则主要用在日志批量存储里。单字节读uint8_t mr25_read_byte(uint32_t addr) { uint8_t data; MR25_CS_LOW(); spi_write_read_byte(0x03); // READ spi_write_read_byte((addr 16) 0xFF); spi_write_read_byte((addr 8) 0xFF); spi_write_read_byte(addr 0xFF); data spi_write_read_byte(0x00); // 产生时钟读回数据 MR25_CS_HIGH(); return data; }单字节写void mr25_write_byte(uint32_t addr, uint8_t data) { mr25_write_enable(); MR25_CS_LOW(); spi_write_read_byte(0x02); // WRITE spi_write_read_byte((addr 16) 0xFF); spi_write_read_byte((addr 8) 0xFF); spi_write_read_byte(addr 0xFF); spi_write_read_byte(data); MR25_CS_HIGH(); mr25_write_disable(); }多字节连续读void mr25_read_bytes(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t i; MR25_CS_LOW(); spi_write_read_byte(0x03); spi_write_read_byte((addr 16) 0xFF); spi_write_read_byte((addr 8) 0xFF); spi_write_read_byte(addr 0xFF); for (i 0; i len; i) { buf[i] spi_write_read_byte(0x00); } MR25_CS_HIGH(); }连续读模式下地址会自动递增跨越芯片容量边界之后会回到 0 地址这个行为跟很多 SPI Flash 一致。实际使用时要保证 len 不会让地址超出边界否则数据位置会对不上。我一般在应用层做一次地址范围检查避免这种低级问题。多字节连续写同理把循环里的数据字节连续发出去就行。MRAM 的好处是连续写地址自动递增中间没有页缓冲的概念也不会因为跨页产生奇怪的写入失败。3.4 调用示例与性能测试举个例子设备每次上电启动时要把开机次数加 1然后把总运行时间存下来#define MAGIC_ADDR 0x00000 #define BOOT_COUNT_ADDR 0x00010 typedef struct { uint32_t magic; uint32_t boot_count; uint32_t total_seconds; uint32_t crc32; } sys_info_t; void save_sys_info(sys_info_t *info) { mr25_write_bytes(MAGIC_ADDR, (uint8_t *)info, sizeof(sys_info_t)); }这里 mr25_write_bytes 是把结构体当作连续字节写入 MRAM。因为 MRAM 支持任意字节地址写入这种做法非常直接不需要考虑页对齐。性能方面按 10MHz 时钟来算写入一个字节实际要传输 4 字节帧命令 3 字节地址大约 3.2us加上软件循环开销实测单字节随机写不到 5us。连续读 512 字节大约是 516 字节帧理论传输时间约 413us加上循环判断等开销实测在 500us 左右。这跟 EEPROM 动辄 3~5ms 的字节写等待相比完全是两个量级。4. 工业场景下的可靠性设计与实测数据4.1 掉电保存与故障记录的设计MRAM 的写入是原子的但应用层的“事务”不一定天然原子。比如同时要更新一组参数A 参数已经写进去了B 参数还没来得及写这时候正好掉电恢复之后就会出现 A 是新值、B 是旧值的情况。解决这个问题我用了双备份结构也叫“槽位法”。把参数区域分成 A、B 两个槽每槽都保存完整数据和 CRC 校验值。写入时先写 A再写 B。上电读取时先读 A如果 A 的 CRC 校验通过就采用 A如果 A 校验失败说明 A 写到一半掉了电此时读 BB 应该是完整的。这样即使掉电发生在任意时刻系统总能恢复到一个完整的数据集。这个方案不需要额外硬件只是把存储空间翻倍。MR25H40CDF 有 512KB对于几组参数来说完全不是问题。伪代码结构大致如下typedef struct { uint32_t magic; uint32_t data_len; uint8_t data[64]; uint32_t crc; } record_slot_t; #define SLOT_A_BASE 0x10000 #define SLOT_B_BASE 0x11000掉电保存流程就三步拉低 GPIO 关掉外部执行器然后按上面双槽逻辑写入数据最后把写保护开启。整个过程因为 MRAM 写速度极快可以在检测到掉电到系统完全断电之间轻松完成。4.2 写入策略与寿命管理MRAM 寿命很长不代表可以随便写。工业设备里很多“疯狂写入”其实是因为软件 bug 导致的状态反复翻转比如传感器瞬时值抖动让控制逻辑每隔几毫秒就更新一次参数。这种无意义的写入会放大系统功耗也会让日志区域在短时间内被覆盖掉。我给写入操作加了几个约束条件参数变化小于阈值时不写写入间隔小于 100ms 时丢弃本次写入日志按环形队列设计队列满之后覆盖最旧的数据。这些策略对任何存储介质都适用但对 MRAM 来说主要是为了降低总线负载和数据管理复杂度而不是保护寿命。块保护方面MR25H40CDF 的状态寄存器里有 BP0、BP1 位可以设置块的写保护范围。如果某一段地址放的是出厂校准数据不想被运行程序误改可以在初始化时给对应地址加上块保护。要修改这些数据时再临时关闭保护。4.3 实测数据与测量方法这块板子做了一系列测试记录如下连续随机地址写入 10 万次每次写后读回校验全部通过对同一地址反复写入 100 万次数据一致无写失败掉电 100 次测试每次都是在写入过程中随机断电上电后双槽恢复机制均能读到正确数据在电机启动干扰下持续读写 8 小时未出现位反转或通信错误测量方法上我在 SPI 读写函数前后翻转一个调试 GPIO用示波器观察单次读写耗时再用逻辑分析仪抓取完整帧确认命令字节、地址字节和数据的电平都符合预期。这里要特别说明数据保持特性的验证很难在短时间内完成因为 MRAM 的保持期限是按年计的。我们能做的是相信器件数据手册给出的指标同时通过双槽 CRC 机制兜底。真正需要长期验证的指标要靠设备在实际工况中积累数据。5. 常见问题排查与避坑实录5.1 读回 0xFF 或 0x00 的排查流程读回固定 0xFF 是最常见的问题通常不是 MRAM 坏了而是 SPI 通信本身没建立起来。排查顺序我固定是这么几步。第一步查供电和引脚电平。用万用表确认 VCC 有 3.3VWP# 和 HOLD# 是高电平CS# 在通信时能被拉低。第二步查SPI模式。用示波器抓 SCK看空闲电平和数据采样沿是否符合预期模式不对的话MISO 上的数据会整体错位。第三步查命令帧。用逻辑分析仪抓 MOSI 上的数据确认发送的第一个字节是 0x03后续地址字节顺序正确。第四步查 MISO 回读。MOSI 发完地址后继续发 0x00SCK 继续翻转MISO 才会把数据送出来。大部分读回 0xFF 的情况最终都定位在引脚复用没生效或者 SPI 模式配错芯片本身很少出问题。5.2 写入不生效的三大原因写入不生效比读取异常更隐蔽因为命令看起来都发出去了信号电平也正常但读回来还是旧值。第一个原因是没发 WREN。MRAM 在收到写命令之前必须要把 WEN 位置位否则写命令被忽略。检查方法很简单发完 WREN 之后读状态寄存器看 WEN 位是否为 1。如果不为 1说明 WREN 时序不对或者 CS 上沿没有正常触发锁存。第二个原因是 WP# 或者 HOLD# 被拉低。WP# 拉低会让芯片处于写保护状态HOLD# 拉低会让芯片忽略 SPI 时钟。这两个引脚的检查要放在所有软件调试之前因为信号都正常但操作无效大概率就是硬件级别的写保护被触发了。第三个原因是地址越界。如果使用的地址超过芯片容量有些器件会直接忽略写入有些会做回卷。如果应用层代码不小心把某一类数据的地址算错了写入可能落在错误区域看起来就是“没写进去”。5.3 调试利器逻辑分析仪与 GPIO 打点排查这类问题我强烈建议在开发阶段就把逻辑分析仪用起来。SPI 时序本身不复杂抓一次完整波形就能看清所有问题。我常用的做法是写一个调试宏在每次片选拉低和拉高时翻转一个 GPIO然后把这个 GPIO 也接到逻辑分析仪上。这样不仅能看 SPI 信号还能看出软件里片选保持时间够不够是否存在过短的 CS 脉冲。代码大致是#define DBG_PIN_TOGGLE() LATFbits.LATF0 !LATFbits.LATF0 // 在片选操作前后调用 MR25_CS_LOW(); DBG_PIN_TOGGLE(); // ... 发送命令 DBG_PIN_TOGGLE(); MR25_CS_HIGH();在 MPLAB X 的调试界面里同时打断点观察状态寄存器值配合逻辑分析仪的波形基本能定位所有 SPI 相关问题。5.4 常见问题速查表故障现象可能原因解决方案读回固定 0xFFSPI 模式不匹配、CS 未拉低、引脚复用未生效检查 SCK 电平与采样沿检查 PPS 配置读回数据错位字节序错、地址帧少发一个字节确认地址是 3 字节检查 SPI 位序写不进去未发 WREN、WP# 拉低、HOLD# 拉低读状态寄存器确认 WEN 位检查硬件引脚偶发读写失败干扰、CS 走线过长、电源纹波大加强去耦SPI 降频检查布局掉电后数据异常应用层事务未保证原子性使用双槽结构和 CRC 校验地址超出容量后数据错乱应用层未做边界检查写驱动时增加地址范围校验我在实际调试中还有一个习惯第一版驱动一定用最朴素的轮询方式把所有细节剥开先把 CS、模式、地址帧调通再用中断和 DMA 逐层往上加。原因很简单SPI 设备的问题本来就不多一旦混入 DMA 和多线程定位起来就麻烦很多。MR25H40CDF 本身是个很省心的存储芯片只要时序正常几乎不会掉链子多数问题还是出在初始化配置和硬件接线上。如果你也在做类似的数据记录项目建议先跑通单字节循环读写再扩展批量读写最后把日志和双槽恢复这些业务逻辑加上去。一步步来整个系统会稳很多。
返回列表