尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

PIC32MZ与SPI MRAM工业存储:MR25H40CDF驱动与掉电安全方案

PIC32MZ与SPI MRAM工业存储:MR25H40CDF驱动与掉电安全方案 工业设备最怕什么怕数据存不住、存了读不对。这段时间我把Everspin的MR25H40CDF4Mb SPI MRAM接到Microchip的PIC32MZ1024EFE144上用来保存设备标定参数、站点信息和运行日志从电路搭接到SPI驱动调试、再到数据完整性方案都完整跑了一遍。这篇文章把这些东西整理出来为什么要用MRAM、硬件怎么连、代码怎么写、现场怎么排查。适合正在做工业控制器、数据采集终端、电力设备或者需要频繁断电保存数据的嵌入式工程师参考。1. 为什么在工业现场我会想起MR25H40CDF1.1 工业和嵌入式存储的硬骨头嵌入式工程师对EEPROM和Flash的脾气应该都很熟悉温度范围够用、容量也凑合但一到频繁写入和掉电保存场景就开始露馅。EEPROM写一个字节要几毫秒甚至更多NOR Flash写之前还得先擦除整个扇区写次数也有限。工业现场往往不止要存一两个参数还要存校准表、事件日志、故障码、计数值这些数据的特点是写入频率高、写入时间碎片化、每次数据量不大、断电随时可能发生。用普通Flash做日志最头疼的是擦写均衡和掉电丢块。一个扇区擦一次一万次寿命每天写几百次几个月就到寿命线。EEPROM虽然按字节写但速度慢写1KB数据要几秒在掉电瞬间根本来不及保存现场。带电池的SRAM是另一个办法容量大、速度快、无限写但电池在工业高温环境下寿命不稳定维护成本也高。1.2 MRAM凭什么替代EEPROM和电池SRAMMR25H40CDF是Everspin的4Mb SPI MRAM容量算下来512KB封装很小工作电压3.3V。它最核心的特点是按字节直接写不用擦除写一个字节的时间和读差不多。耐久性标称极高在工业温度范围内读写次数可以到10的12次方级别数据保持时间按年来算。这就把EEPROM写慢、Flash擦写次数少、电池SRAM怕高温这几个痛点同时解决了。从原理上说MRAM的存储单元是磁隧道结靠磁性方向保存数据而不是靠电荷。所以它不怕辐照、不靠电池维持数据、写入过程也没有电荷泵擦除那种高电压延时。掉电后数据自然保留上电也不用初始化。对工业应用来说这种特性和普通SRAM的访问习惯很像但又不需要后备电池非常省心。1.3 PIC32MZ1024EFE144在这条链路上的角色PIC32MZ1024EFE144是Microchip的32位MCUMIPS内核主频到200MHz带FPU、DMA、CAN、USB、加密引擎片上Flash 1MB、RAM 512KB。选它来做主控是因为工业项目里除了要存数据往往还要跑控制算法、做通信协议栈、处理人机交互。如果这些工作全压在小ROM的单片机上存储逻辑很容易变成瓶颈。它和MR25H40CDF搭配很合适PIC32MZ本身就是3.3V系统直接用SPI接口挂MRAM不用电平转换。MR25H40CDF提供的是快速、耐写、非易失的“内存级”存储空间正好补充PIC32MZ片内Flash不适宜频繁写入的问题。两者组合起来可以做到控制系统上电或掉电的瞬间把关键状态安全写进MRAM下次再上电直接恢复现场。2. 硬件连接SRAM、SPI、电平、去耦一个都不能少2.1 芯片引脚和最小接线框架MR25H40CDF是标准的SPI接口存储器件引脚不多常见的几个信号分别是CS#、SCK、SI、SO、HOLD#、WP#、VDD、VSS。SI是主设备输出从设备输入接MCU的SDOSO是主设备输入从设备输出接MCU的SDISCK接SPI时钟CS#接MCU的GPIO用来控制片选。有两个引脚最容易出问题HOLD#和WP#。HOLD#拉低会让MRAM暂停通信WP#拉低会禁止写操作。如果电路上不管它们浮空状态下受干扰可能把通信“挂住”或者写命令发下去没效果。稳妥的做法是把这两个引脚直接通过10kΩ电阻上拉到3.3V或者用两个GPIO控制驱动代码里固定输出高电平。实测中悬空造成的故障最难看因为现象随机、时好时坏。VDD和VSS之间要放去耦电容常规做法是100nF贴一个另外在电源进入芯片的地方再放1uF到10uF的电容。虽然MRAM是存储器件瞬时电流不大但SPI时钟翻转时还是会有小的电流毛刺去耦做不好高速读取时偶发错位是常事。2.2 SPI引脚与PPS配置PIC32MZ EF系列的引脚和外设关系比较灵活SPI模块可以通过外设引脚选择PPS映射到不同的RP引脚上。这个功能方便了PCB走线但也要求初始化时明确配置引脚映射关系不能拿过来直接用默认引脚。我的习惯是先用MPLAB Harmony v3配置。在MHC里使能SPI2设置主机模式、SPI Mode 0、8位数据宽度、时钟频率比如10MHz然后把SDI2、SDO2、SCK2分配到你选好的RP引脚上。Harmony生成初始化代码后驱动会自动做PPS映射。如果你不喜欢图形配置也可以直接写寄存器核心两步把SPI的输入信号通过SDI2R寄存器选到某个引脚把某个RP引脚通过RPxxR寄存器选成SDO2或SCK2功能。手动配置时务必查阅对应144引脚型号的数据手册和PPS表不同封装的复用关系会有差异。2.3 PCB布局、滤波和工业环境下的余量工业板卡上跑10MHz SPIPCB布局不能太随意。MR25H40CDF和PIC32MZ之间的走线尽量短尤其SCK和SI、SO这三根线长线会带来反射和过冲。信噪比不高的时候时钟沿的振铃会造成字节移位。合适的做法是在靠近主控输出端串接22Ω到33Ω的电阻能吸收一部分过冲。地平面要完整不要在SPI信号线下面开槽或者跑其他高压线。CS信号也尽量由GPIO直接控制不要和SCK靠得太近避免时钟串扰造成片选抖动。另外要注意的是工业现场环境温度。MR25H40CDF选型时要确认工作温度等级别拿着商用级型号直接进高温控制柜。选型这一步省事后面现场调试就花时间。3. 驱动实现从0到1把MR25H40CDF跑起来3.1 SPI主机初始化和波特率计算先讲SPI主机初始化。PIC32MZ的SPI波特率计算公式是波特率 PBCLK / (2 × (BRG 1))。假如PBCLK是40MHz想跑到10MHzBRG算下来就是1。实际使用中不一定要跑极限工业应用图稳10MHz、8MHz都是常用值。用寄存器初始化时关键是把SPI配置为8位模式、主机模式、SPI Mode 0。所谓的Mode 0就是CPOL0、CPHA0即空闲时钟为低、数据在第一个时钟沿采样。很多MRAM数据手册同时支持Mode 0和Mode 3但固件里最好只锁定一种。下面是一个简化的初始化片段#define PBCLK_HZ 40000000u #define MRAM_SPI_HZ 10000000u void SPI1_Config_For_MRAM(void) { SPI1CON 0; SPI1BRG (PBCLK_HZ / (2u * MRAM_SPI_HZ)) - 1u; SPI1CONbits.MSTEN 1; // 主机模式 SPI1CONbits.MODE16 0; // 8位模式 SPI1CONbits.MODE32 0; SPI1CONbits.CKP 0; // 时钟空闲为低 SPI1CONbits.CKE 1; // 数据时序配合Mode 0具体对照手册确认 SPI1CONbits.ON 1; }如果你用Harmony驱动直接用SPI2_Open和DRV_SPI_MODE_0即可。寄存器直接配置的坑在于不同系列PIC对CKE位的定义是反的必须对照PIC32MZ EF Family Reference Manual确认。3.2 MR25H40CDF的指令集与读写时序MR25H40CDF不是Flash所以它的指令集比SPI NOR简单得多。最常用的几条指令如下指令操作码说明WREN0x06写使能WRDI0x04写失能RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据地址是24位的MR25H40CDF实际有效容量512KB所以地址范围是0x000000到0x07FFFF高位地址填0。每次通信先把CS#拉低发送操作码再发三字节地址高位在前然后传输数据最后把CS#拉高。读操作可以连续读也就是发完地址后SCK持续翻转MRAM会把连续地址的数据从SO脚送出来。写操作必须注意一点每条WRITE命令之前都要先发WREN把状态寄存器里的WEL位置1。如果跳过这一步写入命令会被忽略。很多人写SPI Flash习惯了总觉得只要初始化时写使能一次就行但MRAM的WEL是每次上电默认锁住的。3.3 读写函数样例我习惯封装三个底层函数代码被应用层调用时清晰一些。假设底层已经有一个spi2_transfer(tx, rx, len)函数CS控制用mram_cs_low()和mram_cs_high()。写使能函数static void mram_write_enable(void) { uint8_t cmd 0x06; mram_cs_low(); spi2_transfer(cmd, NULL, 1); mram_cs_high(); }写数据函数int mram_write(uint32_t addr, const uint8_t *data, uint32_t len) { uint8_t header[4]; if (addr len 0x080000u) { return -1; } mram_write_enable(); header[0] 0x02; // WRITE header[1] (uint8_t)((addr 16) 0xFF); header[2] (uint8_t)((addr 8) 0xFF); header[3] (uint8_t)(addr 0xFF); mram_cs_low(); spi2_transfer(header, NULL, 4); spi2_transfer((uint8_t *)data, NULL, len); mram_cs_high(); return 0; }读数据函数int mram_read(uint32_t addr, uint8_t *data, uint32_t len) { uint8_t header[4]; if (addr len 0x080000u) { return -1; } header[0] 0x03; // READ header[1] (uint8_t)((addr 16) 0xFF); header[2] (uint8_t)((addr 8) 0xFF); header[3] (uint8_t)(addr 0xFF); mram_cs_low(); spi2_transfer(header, NULL, 4); spi2_transfer(NULL, data, len); mram_cs_high(); return 0; }这里有个容易被忽略的问题spi2_transfer如果同时接收数据在发送header时会产生4个无用字节的接收数据如果不及时清空接收FIFO会导致后续数据错位。阻塞式SPI通常没问题但FIFO较深的MCU要小心。发送数据时尤其要确保所有字节都送出后才能拉高CS#否则最后一个字节可能卡在移位寄存器里。3.4 用DMA把吞吐再拉一层PIC32MZ内置DMA把SPI接收发送和CPU解耦之后系统性能会好看不少。但MRAM这种小器件不太需要把CPU完全抛开毕竟单次写日志一般也就几十到几百字节CPU阻塞传输在微秒级就做完了。真正需要DMA的场景是大批量数据搬移比如把采集缓存的几十KB通过MRAM保存。用DMA时要特别处理CS#的释放时机。很多工程师在DMA完成中断里直接拉高CS#但SPI模块的发送FIFO和移位寄存器之间还有数据没流完这时候拉高CS#会让最后一两个字节写不进去。稳妥做法是等待SPI模块的BUSY标志清掉或者确认发送FIFO空且移位寄存器已经结束再释放CS#。这个坑在普通EEPROM上不明显但在高速SPI模式下很容易丢尾字节。4. 数据组织光能读写不等于用得好4.1 地址空间规划MR25H40CDF有512KB空间看起来不大但规划好了足够放设备参数和日志。第一次接触MRAM的人容易把它当一个大数组随便用结果数据地址互相覆盖到掉电恢复时才发现问题。我建议在项目一开始就做分区表哪怕先用注释写在头文件里。例如0x000000~0x00FFFF64KB关键参数区双镜像备份0x010000~0x01FFFF64KB设备配置和校准表区0x020000~0x07FFFF384KB循环日志区按固定块长度循环覆盖分区的好处是避免参数覆盖日志也可以让代码里的地址管理更统一。不要靠“大约”“应该”来写地址要专门用一个枚举或者宏定义把各分区基地址和大小定义出来。4.2 日志与关键参数的分区隔离关键参数区要采取冗余策略。设备标定参数、通信波特率、站址这类数据如果写坏了设备可能直接无法启动。我的做法是保存两份或者更多份每份记录里放一个递增序号。写入时先写A区再写B区读取时比较两个区的序号和CRC选择序号更新且校验通过的那一份。日志区就不一样它追求的是写入快和可追溯。可以把日志区切成许多固定长度的块比如每块256字节。块头放时间戳、数据类型、数据长度和CRC。写完一块后把写指针挪到下一块写满整个日志区后从头覆盖。MRAM没有擦除限制所以这种覆盖操作非常顺畅不需要Flash的垃圾回收机制。4.3 掉电安全与数据完整性设计MR25H40CDF把字节写进存储单元的速度很快但SPI总线上传几百字节还是需要时间如果这个过程正好遇到掉电就有可能出现半条记录。解决办法不是要求硬件永不掉电而是在数据格式上留好校验和提交标志。我常用的方案是每条记录的结构为魔数序号长度数据CRC32结束标志。写入时先把除了结束标志以外的部分写完最后写结束标志字节。读取时如果魔数正确、CRC正确、结束标志存在这条记录就是完整的。如果结束标志缺失或CRC不对就当作无效记录。关键参数还有一种做法是使用状态位。先写一个标志无效再把参数数据写进去最后把标志改成有效。这样即使掉在任何一步启动时都不会把不完整的数据加载进控制系统。配合PIC32MZ1024EFE144的BOR和低电压监测中断可以在电压跌落早期触发紧急存储把当前运行状态写进MRAM上电后恢复现场。5. 实测数据与常见问题排查5.1 一个实际项目的读写速率参考我在一个数据采集终端上做过一组简单的实测SPI时钟10MHz每次向MRAM写入512字节日志硬件传输本身约4096位折算成时间约0.41ms加上命令和地址开销实测一次写入在0.5ms左右读操作差不多。这个速度对工业控制器来说很够用至少EEPROM做不到这个水平。如果用1MHz的I2C EEPROM写512字节仅数据字节就要约4ms还要算页写延时整体10ms都下不来。MRAM的优势在频繁断点保存的场景特别明显掉电瞬间中断服务程序里可以一口气保存几十个变量或几百字节运行状态时间窗口从“来不及”变成“来得及”。5.2 问题一读出来全FF或者说0读到全FF说明总线一个字节都没回来读到全00则可能是SI/SO接反或者SO线上被拉死。按顺序排查先看CS#有没有真的拉低用示波器确认CS#信号不是虚焊。再看SPI模式Mode 0和Mode 3时序差别不大但接错后数据全是乱的。然后是HOLD#和WP#如果悬空了MRAM有时会处于暂停或写保护状态读命令可能正常但写命令无效严重时通信都不响应。最后检查电源和GNDMRAM空焊或者供电电容缺失表现就是随机乱码。5.3 问题二偶发数据错位如果多数情况正常偶尔有字节错位优先怀疑SPI时钟频率太高或者走线太长。10MHz本身不吓人但如果在干扰强、地回路不好的板子上跑毛刺就来了。处理办法是降低SPI频率到4MHz试一次如果错误消失要么降频运行要么就要改善PCB。DMA传输场景下还要检查CS#释放时机。前面提过DMA完成不等于SPI模块已经发完最后一字节。如果CS#拉高早了发送的数据会丢失尾部读操作时表现为末尾几个字节为旧值。另外如果板上还有其他SPI设备确认是不是总线仲裁问题。MRAM通信期间不应该有第二个设备占用SCKCS#竞争和SCK毛刺都会造成错位。5.4 问题三重新上电后数据丢了MRAM是非易失存储不存在上电丢数据的问题但工程现场“丢数据”往往有三种原因写入本身没完成、读取地址不对、供电时序太差。写入没完成通常发生在掉电瞬间解决方法是做好数据校验和掉电检测把不完整的记录在下次启动时清除。读取地址不对经常出现在地址位处理错误上24位地址高字节写错访问区域完全偏移。还有一种情况是供电时序如果MCU和MRAM的供电在上电瞬间稳定时间不同早读阶段VDD偏低读出来的内容可能无效。用看门狗复位或者上电延时重新读一次能缓解这个问题。5.5 现场排查速查表故障现象可能原因处理方案读回全FFCS未拉低、通讯没建立示波器查CS和SCK波形读回全00SI/SO反接、SO被拉低检查PCB网络和焊接数据随机乱码SPI模式错误固定Mode 0并核对CKE位写不进去缺少WREN、WP#悬空每条WRITE前发WRENWP上拉尾字节丢失CS释放过早等SPI模块完全空闲再拉高CS掉电后记录损坏写入中途掉电增加魔数CRC提交标志6. 还有一些值得聊的工程细节6.1 关于PIC32MZ的看门狗与写入嵌套如果固件开了看门狗在MRAM写数据时发生看门狗复位GPIO会立刻回到复位状态。如果CS#引脚复位后默认输出低电平MRAM片选等于一直被拉低SPI总线变相被锁住。复位后重新初始化SPI和CS引脚当然能恢复但要注意上电时CS#必须初始为高电平避免MRAM在复位期间被误选中。中断嵌套也是一样。如果主程序正在写MRAM中断服务程序又发起一次写CS#会乱掉。最简单的做法是定义一把互斥锁或者临时候在临界区里做整个MRAM写事务。MRAM写数据本身很快临界区持续时间不长工业实时系统完全能接受。6.2 把MRAM接到RTOS或者嵌入式Linux上的思路如果主控是PIC32MZ裸机驱动就够了。但有些复杂系统会用RTOS甚至把MRAM挂到另一颗运行Linux的应用处理器上通过SPI访问。RTOS下要重点解决多任务访问同一SPI总线的竞争问题建议在SPI驱动层加互斥锁整个WRENWRITE过程要作为一个原子操作。嵌入式Linux下如果只是调试阶段可以直接用Linux的spidev驱动在用户态打开/dev/spidevX.0通过ioctl发起读写非常方便。但如果要把MRAM做成根文件系统的一部分就不太适合因为MRAM虽然抗写但作为日志型文件系统的介质还需额外适配。更合理的做法是把它当独立存储用自定义块格式存日志和参数。6.3 最后的一些经验体会做了几轮MRAM调试我最大的体会是MR25H40CDF本身驱动不复杂真正的工程量在数据组织上。先把分区表写好把CRC和校验机制想清楚后面就是水到渠成的事。另一个心得是设计PCB时别省HOLD#和WP#的上拉电阻别把CS#悬空交给上电时序赌运气。MRAM这种器件不像Flash那样有大量擦除时序要处理但它对SPI时序的严谨性要求一点不低只要有一次CS#释放太快就可能让你排查大半天。最后再分享一个小技巧首次调MRAM驱动时先不要上DMA和RTOS用一块固定的地址反复写读读出数据能逐字节对上再往复杂架构上扩展。这个习惯帮我绕开了很多混合在一起的干扰问题也推荐你试试。
返回列表