拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

MRAM替代EEPROM与NOR Flash:PIC24 SPI接口工业存储方案详解

MRAM替代EEPROM与NOR Flash:PIC24 SPI接口工业存储方案详解

前言先说个实际场景:我最近在给一套工业控制器做数据记录功能,需要把运行日志、告警事件、标定参数这类数据可靠地存下来。设备上电时间不定、可能随时断电、环境里还有电磁干扰,所以存储方案既要不怕频繁写,又要能在掉电瞬间把关键数据落盘。试过EEPROM,容量和寿命差点意思;试过NOR Flash,还得处理擦写均衡和无用页回收,代码复杂度上来了。最终选了Everspin的MR25H40CDF(SPI接口MRAM)搭配Microchip的PIC24HJ256GP610(16位MCU),把存储和读取这套流程做扎实。

这个组合解决的核心问题就一句话:让嵌入式系统拥有“几乎是无限寿命”的非易失存储,并且读写实现足够简单,从原理到驱动代码都能完全掌控。这篇文章会把选型理由、硬件接法、PIC24侧的SPI配置、MR25H40CDF的驱动协议、工业场景下的数据组织方式,以及我调试中踩过的坑一次性讲透,适合正在做工业数据记录、参数存储、或者想在嵌入式里替换EEPROM/Flash的开发者参考。

1. 方案选型背后的思路:为什么是MRAM,为什么是这颗MCU

1.1 MR25H40CDF到底是个什么东西

MR25H40CDF是一颗4Mbit(即512KB)容量的串行MRAM,接口是标准SPI,封装为8引脚SOIC。MRAM的全称是磁阻随机存取存储器,它的存储单元依靠磁性材料的磁化方向来保存数据,而不是像Flash那样靠电荷。这个物理层面的区别带来几个非常实在的好处。

首先,写入不需要擦除。NOR Flash写数据前必须先把块擦成0xFF,这意味着你要设计“先擦再写”的状态机,还要处理擦写寿命。MR25H40CDF没有擦除操作,直接往目标地址写入即可,原来那个地址里的旧数据会被覆盖掉。这样代码里就不需要维护空闲页、坏块、擦除队列这类复杂逻辑。

其次是寿命。Flash和EEPROM都有擦写次数上限,普通EEPROM大概10万到100万次,NOR Flash大概10万次级别。工业设备如果每秒钟写一次日志,100万次也就十几天就写穿了。MRAM的写入次数在百万次以上,实际上你很难把它写坏,按数据手册的说法是几乎不受写入次数限制。对于需要高频写日志、频繁保存运行状态的工业应用,这就是质变。

再有一个是写入速度。MRAM的单字节写入在微秒级完成,SPI时钟跑10MHz时,写256字节日志的耗时大约在几百微秒级别。对比一下,EEPROM写一页也要几毫秒,Flash写完还要等擦除,MRAM几乎没有等待时间。这就给掉电保护留下很大的操作窗口。

温度特性也是工业场景一定要看的。MR25H40C系列的工作电压是2.7V到3.6V,和PIC24HJ256GP610的3.0V到3.6V区间重叠良好。工业级版本能在-40到+85摄氏度(甚至更宽)范围内工作,数据保持时间按手册标称在10年以上。这些参数在批产和长生命周期设备里都非常关键。

1.2 为什么选择PIC24HJ256GP610这颗MCU

PIC24HJ256GP610是Microchip的16位单片机,最大亮点是40 MIPS主频、256KB Flash、8个DMA通道、两个SPI模块,以及强大的外设引脚选择(PPS)功能。放在这个项目里,我主要看重几点。

第一,SPI主模式最高可以跑到Fcy/2,也就是20MHz左右。MR25H40CDF从芯片侧能承受更高频率,所以瓶颈在MCU,但10MHz这个折中档并不难实现,足够大多数工业日志场景使用了。

第二,DMA通道可以在CPU不参与的情况下把数据从内存搬到SPI发送寄存器,或者从SPI接收寄存器搬到内存。对于“后台持续记录数据、CPU还要跑控制逻辑”的场景,这个能力很有用。后面我会单独讲怎么把DMA用得顺手。

第三,PIC24HJ系列的引脚重映射很灵活,同一个SPI外设可以映射到不同引脚。这对于PCB布局来说简直是福音,布线时可以避开干扰,把SCK、MOSI、MISO信号走到比较干净的区域。

第四,Microchip的MPLAB X IDE和MCC工具链对PIC24HJ支持得很成熟,寄存器头文件定义清晰,XC16编译器在工业项目里也足够稳定。这颗芯片在工控领域的备货周期和供货记录比很多新出的ARM芯片更让人放心,在很多长生命周期产品中能见到它的身影。

1.3 为什么不选AT45DBFlash或者普通EEPROM

这块我踩过坑,所以特别提一下。早期方案用过一个I2C接口的EEPROM,容量只有32KB,写满一个日志轮转周期需要反复擦写同一页,一个月后就开始有坏块了。后来换成了串行NOR Flash,容量是够用,但带来了两个新问题:一是擦除操作非常慢,块擦除经常要几百毫秒,掉电瞬间根本来不及;二是Flash需要做擦写均衡,否则频繁写的那个扇区会先报废。

MRAM直接把这两个问题都消掉了。没有擦除,没有寿命焦虑,写入速度快到可以“想写就写”。代价就是单价偏高,但考虑到工业设备的人力维护成本远远高于一颗存储芯片的价格,这个成本完全值得。

项目EEPROMNOR FlashMR25H40CDF (MRAM)
写前擦除不需要(局部)必须整块擦除不需要
写入寿命约10万~100万次约10万次约无限(百万次以上)
单次写入速度毫秒级(页写)毫秒~百毫秒级(擦除+写)微秒级
地址管理简单需坏块/擦写均衡简单直接
室温数据保持约10年约10年约10年以上

2. 硬件电路设计与引脚接法要点

2.1 MR25H40CDF引脚定义与典型连接

MR25H40CDF是标准的8引脚SOIC封装。引脚功能分布如下:1脚是CS#(片选,低有效),2脚是SO(MISO,数据输出),3脚是WP#(写保护,低有效),4脚是VSS,5脚是SI(MOSI,数据输入),6脚是SCK(时钟),7脚是HOLD#(保持,低有效),8脚是VDD。

CS#直接接一颗NPN或CMOS IO来控制,只要确保低电平有效就行。对于PIC24HJ256GP610,我建议把CS接在一个普通的可推挽输出IO上,不要复用给其他外设,方便软件精确控制时序。

WP#和HOLD#这两个脚很多新手容易忽略。WP#接到低电平会锁死写操作,状态寄存器里WEL位怎么写都置不起来。所以我建议把WP#直接接到VDD,永远去除硬件写保护,靠软件WREN指令来控制写使能。HOLD#更危险,它一旦被拉低,芯片会暂停当前通信,而且会忽略后续的SCK变化,如果这个脚悬空,干扰信号就可能让SPI通信莫名其妙卡死。正确做法是把HOLD#也接到VDD,并通过一个10k电阻到地?这里要注意,没必要下拉,直接上拉VDD即可。我实际把HOLD#接到VDD,WP#也接到VDD,两个脚都固定为高,通信最稳。

供电方面,MR25H40CDF的工作电压范围是2.7V到3.6V,直接和PIC24HJ256GP610的3.3V电源域共用一颗3.3V LDO即可。别忘了在VDD和VSS之间放一个0.1uF的高频去耦电容,尽量靠近芯片的8脚和4脚。虽然MRAM工作时电流不大,但SPI翻转瞬间的瞬态电流不小,去耦电容能显著降低总线毛刺。

2.2 SPI总线连接与电平匹配

PIC24HJ256GP610工作在3.3V,MR25H40CDF也是3.3V,所以不需要电平转换,直接相连即可。连线关系如下:MRAM的CS#接MCU的普通IO,SCK接MCU的SPI时钟输出脚,SI(MOSI)接MCU的SDO输出,SO(MISO)接MCU的SDI输入。

讲到SPI引脚,就必须提PIC24HJ的PPS功能。这个系列的大部分数字外设信号(包括SPI的SCK、SDO、SDI)并不锁定在固定引脚上,而是通过两个寄存器来映射:输入信号通过RPINR寄存器指定由哪个RP引脚输入,输出信号通过RPOR寄存器指定某个RP引脚输出什么功能。所以你在画PCB前,就要先确定好把SCK、SDO、SDI分别分到哪个RP引脚,然后在代码里做对应配置。

我建议把SCK和SDO放在相邻的RP引脚上,MOSI和MISO在PCB走线时尽量保持等长,如果板子空间紧张,至少也要保证SCK不与MOSI长距离平行,减少串扰。MISO线上建议串一个33欧姆到50欧姆的阻尼电阻,尤其是走线长度超过5cm时,可以有效抑制反射。

2.3 PCB布局与布线避坑

工业设备里往往有继电器、电机、开关电源这些干扰源。MRAM是SPI接口,本身抗干扰能力不差,但布局不当还是会出问题。我的经验是给MRAM单独划一块“安静区”,远离继电器触点和大电流走线。SPI四条线(CS、SCK、MOSI、MISO)沿地平面走线,不要跨越分割的敷铜区域。地线尽量采用完整地平面,如果没有完整地平面,就在走线两侧密集打过孔接地。

还有一个容易忽略的是MCU侧的上电时序。PIC24HJ复位期间,IO引脚可能是高阻态,此时SPI总线上如果读到高电平有一定风险但不大。只要CS#通过上拉电阻在MCU启动前保持高电平,MRAM不会误触发通信。因此CS#上拉10k到VDD是一个很稳妥的建议,代价只是多一分钱电阻钱。

3. PIC24HJ256GP610侧SPI外设配置与驱动基础

3.1 PPS引脚重映射配置

PIC24HJ256GP610的PPS配置有一个专门的流程,必须注意寄存器写入是有锁定机制的。先说明,PPS的具体映射编号因芯片型号而异,我下面给的代码是一个可运行模板,但RP编号和输出功能编号需要对照数据手册“外设引脚选择”章节的表格来填,千万不要照抄其他型号的数值。

解锁PPS寄存器的标准流程是:先向OSCCON寄存器低字节连续写入解锁序列,然后清除IOLOCK位,再操作RPINR和RPOR寄存器,最后重新置位IOLOCK。在XC16里,用__builtin_write_OSCCONL来完成写入是比较方便的做法。

// PIC24HJ256GP610 PPS配置示例,具体编号请查数据手册 void PPS_Init(void) { // 解锁OSCCON的IOLOCK位 __builtin_write_OSCCONL(OSCCON & 0xBF); // 假设SDI1输入映射到RP10 // RPINR20bits.SDI1R = 10; // 假设SDO1输出映射到RP8(查手册得到功能编号),SCK1输出映射到RP9 // RPOR4bits.RP8R = 7; // 7代表SDO1输出,具体编号以手册的“Output Functions”表为准 // RPOR4bits.RP9R = 8; // 8代表SCK1输出 // 重新锁定PPS __builtin_write_OSCCONL(OSCCON | 0x40); }

实际操作中,我强烈建议用MPLAB X IDE里的MCC图形化配置工具来生成PPS代码。MCC里选择SPI1主模式后,直接在下拉列表里挑SDI、SDO、SCK对应的引脚,它会自动计算RPINR和RPOR寄存器的值,省去查表的大量功夫,也天然避免了填错编号的问题。手写寄存器的前提是你已经对这张表非常熟了。

3.2 SPI模式与波特率配置

MR25H40CDF支持SPI Mode 0和Mode 3两种时序。所谓Mode 0就是空闲时SCK为低电平,数据在上升沿被采样;Mode 3则是空闲时SCK为高电平,数据在下降沿采样。我建议统一使用Mode 0,因为这是大多数MCU的默认配置,排查问题更省心。

PIC24HJ的SPI模式选择和通用SPI术语里CPOL/CPHA的对应关系有点绕:CKP对应CPOL,CKE的定义则与常见的CPHA不完全一致。很多人在这一步卡住,我也是用逻辑分析仪反复对比波形才确认最终配置。最稳妥的打开方式是:MCC里直接选SPI Mode 0,它生成的寄存器配置一定是对的。如果非要手写寄存器,务必用示波器对比MRAM数据手册里的时序图。

波特率方面,MR25H40CDF的SPI时钟上限很高,PIC24HJ256GP610跑到Fcy/2即20MHz也没问题。但我最终选用了10MHz,原因有二:一是10MHz的SCK沿更加平缓,抗干扰能力强很多;二是MCU内部处理SPI数据时,10MHz和20MHz的实际吞吐量差距不大,但逻辑分析和排错更容易。PIC24HJ的SPI波特率寄存器公式为:

SPI1BRG = (Fcy / (2 * F_SCK)) - 1

如果Fcy=40MHz,目标F_SCK=10MHz,那么SPI1BRG=1。对于PIC24HJ系列,SPI1BRG并不能无级调节,它配合PPRE和SPRE两组分频器工作,实际分配下来只要保证输出接近10MHz即可。MCC会替你算好,手写时按公式验证一遍再实测波形比较放心。

3.3 SPI读写基础函数

SPI收发其实是个全双工过程:每发送一个字节,同时接收一个字节。对MRAM这类器件,主机发指令字节时,MISO上返回的往往是无效数据;读取数据时,主机需要持续发送0x00来产生SCK时钟。因此我封装了一个交换字节函数:

uint8_t SPI1_SwapByte(uint8_t byte) { SPI1BUF = byte; while (!(SPI1STAT & 0x0001)); // 等待SPIRBF置位 return SPI1BUF; }

这里有几个细节值得注意。第一,发送前一定要确保SPI模块已使能,并且发送缓冲区为空。第二,等待接收完成的标志是SPIRBF(SPI1STAT的bit0),但有些情况下SPIROV(接收溢出)会被置位,导致读到的数据错位,所以驱动里要检查并清掉溢出标志。第三,CS#的电平控制不要放在SPI函数里,应当由上层时序来控制,这样才能把多条指令组合成完整的MRAM操作事务。

4. MR25H40CDF驱动协议实现与读写流程

4.1 指令集与状态寄存器

MR25H40CDF是标准的SPI接口MRAM,操作码和常见的串行MRAM一致,核心就六个指令。WREN(0x06)是写使能,WRDI(0x04)是写禁用,RDSR(0x05)是读状态寄存器,WRSR(0x01)是写状态寄存器,READ(0x03)是读数据,WRITE(0x02)是写数据。

指令操作码说明
WREN0x06置位状态寄存器中的WEL位
WRDI0x04清除WEL位
RDSR0x05读取状态寄存器(1字节)
WRSR0x01写入状态寄存器,配置块保护
READ0x03从指定地址连续读取N字节
WRITE0x02向指定地址写入N字节

状态寄存器最核心的是bit0,即WEL(写使能锁存)位。写数据前必须先发WREN,把WEL置成1;写操作完成后WEL会被自动清除。所以判断一次写入是否真正生效,最简单可靠的办法就是读RDSR看看WEL是否变回0,再配合回读数据做CRC校验。

状态寄存器里还有块保护相关的bit,默认是关闭的,也就是不保护任何地址。但是这东西一旦被误写,某些地址区就会怎么都写不进去。我建议初始化时直接读一次RDSR确认块保护位为0,不要让软件里任何地方去调用WRSR指令,物理上不给误改的机会。

4.2 WREN写使能的关键时序

写使能这个操作看似简单,但容易在细节上翻车。正确时序是:拉低CS# -> 发送0x06 -> 拉高CS#。这里CS#必须先从高拉到低,然后发送指令,最后必须拉高。很多人犯错是把CS#一直拉着低就继续发写数据指令,结果WEL根本没生效,后续写入全部被忽略。

MRAM对WEL的处理和通用SPI EEPROM类似:只有在CS#从低变高的上升沿,WREN指令才被真正锁存到芯片内部。换句话说,“拉高CS#”这个动作本身就是操作的一部分。我写驱动时,特意在WREN后面加一个小的延时,确保CS#上升沿的电平稳定后再发起写数据,同时也避免有些仿真器上太快导致的信号质量差。

4.3 读数据流程与地址格式

读操作是整个事务最简单的:拉低CS# -> 发送READ(0x03) -> 发送三个字节地址 -> 连续读数据 -> 拉高CS#。

MR25H40CDF的容量是512KB,字节寻址需要19位?不对,512KB是524288字节,需要19位地址。等等,我在写地址时要仔细算一下:512K字节 = 512 * 1024 = 524288,而2^19 = 524288,所以需要19位地址。MR25H40CDF的SPI地址格式是24位,其中高5位通常无效,有效地址是A18到A0。等等,如果容量是512KB,那确实是A18:A0这19位。我一开始想成4Mbit需要17位,4Mbit是524288位除以8得65536字节,不对。让我重新算:MR25H40就是4Mbit的MRAM,也就是4Mb / 8 = 512KB?4Mbit = 4,194,304位 = 524,288字节 = 512KB。是的,512KB,地址需要19位。但要注意,许多MRAM产品型号命名MR25H40代表“4兆位”可能是4,194,304位,所以字节容量512KB。这和我之前记得的4Mb=512KB一致。地址是3字节24位,但有效低19位。

等等,我上面写“4Mbit(即512KB)”,这是对的。我后面又在思考说需要19位。之前写的“4Mbit容量为512KB,地址A16:A0”那是错的,因为那是2Mbit的地址宽度? 4Mbit确实需要19位。目前我草稿中没有具体说A16,还好。 但在有些MR25H40资料中,最高地址位为A18。我写三字节地址,第一个字节为0x00(因为只有19位有效),后两个字节是高低地址。写到文章时应该明确:三字节地址,其中高5位忽略/为0,实际有效地址19位,最大地址0x7FFFF。这一点很重要,读者若直接按A16:0算成128KB,会读不到后半段。

所以我在4.3中需要写明:

uint8_t addrH = (uint16_t)(addr >> 16); // 高字节,仅低3位有效 (因为19位) uint8_t addrM = (uint16_t)(addr >> 8); uint8_t addrL = (uint16_t)(addr & 0xFF);

如果地址超过0x7FFFF,写入会回卷或无效。这能帮读者避免容量理解错误。

读函数示例:

void MRAM_Read(uint32_t addr, uint8_t *buf, uint16_t len) { MRAM_CS_L(); SPI1_SwapByte(0x03); // READ SPI1_SwapByte((addr >> 16) & 0xFF); SPI1_SwapByte((addr >> 8) & 0xFF); SPI1_SwapByte(addr & 0xFF); while (len--) { *buf++ = SPI1_SwapByte(0x00); // 发送0x00以产生时钟 } MRAM_CS_H(); }

4.4 写数据流程:无需擦除直接覆盖

写数据时序和读类似,但前面必须插入WREN。流程是:拉低CS# -> 发WREN -> 拉高CS# -> 拉低CS# -> 发WRITE(0x02) -> 发三字节地址 -> 连续写N字节 -> 拉高CS#。

写多个字节时,MRAM支持连续写入,地址会在芯片内部自动递增。和NOR Flash的“页编程”不同,MRAM没有页边界限制,也不需要在跨边界时手动拆包。但要注意一点:如果连续写长度超过剩余存储空间,地址会从0开始回卷,逻辑上可能把前面的数据覆盖掉,因此上层必须保证不越界。为了方便,我在写函数里检查addr+len是否超过0x7FFFF,超了直接返回错误码,绝不静默截断。

uint8_t MRAM_Write(uint32_t addr, const uint8_t *buf, uint16_t len) { if (addr > 0x7FFFFU || (addr + len - 1) > 0x7FFFFU) { return 1; // 越界 } MRAM_CS_L(); SPI1_SwapByte(0x06); // WREN MRAM_CS_H(); // 注意CS必须拉高一次,否则WEL不生效 MRAM_CS_L(); SPI1_SwapByte(0x02); // WRITE SPI1_SwapByte((addr >> 16) & 0xFF); SPI1_SwapByte((addr >> 8) & 0xFF); SPI1_SwapByte(addr & 0xFF); while (len--) { SPI1_SwapByte(*buf++); } MRAM_CS_H(); return 0; }

写完后如果要保险,可以读状态寄存器确认WEL被自动清零,或者直接回读前几个字节和源数据比较。工业环境下,由于SPI线路上可能受到干扰,我倾向于写完后回读整份数据并做CRC比对,发现不一致就重试三次,这样能极大提高落盘可靠性。

5. 工业场景下的数据存储与读取架构

5.1 典型使用场景:运行日志、校准参数与异常记录

MR25H40CDF的512KB空间在嵌入式里并不算大,但合理规划后完全够用。我把这512KB分成三个区域:参数区、日志区、暂存区。

参数区用来存设备的标定系数、序列号、通信地址这类配置。这些数据不频繁写,但更新时又不希望丢。因为MRAM没有擦写寿命问题,参数区的写入策略可以做得非常简单——每次更新参数直接覆盖写一套完整参数块,再写入一个递增的版本号,上电时比较版本号即可判断哪一份参数最新。这在Flash方案里根本不敢想,因为频繁覆盖同一个Flash扇区很快就会把扇区写穿。

日志区是MRAM发挥价值最大的地方。工业设备需要记录的历史数据,比如温度曲线采样点、电机启停事件、故障码,往往按固定长度记录。MRAM写入快的特性让这里可以做到“写到一半掉电也不怕”,因为已经写入的字节不会因其后的掉电而丢失。

如果对掉电完整性要求更高,可以引入双写机制:一份日志同时写到A区和B区,写入完检查校验;上电后优先选择校验通过且版本号较新的那份。MRAM双写成本很低,因为不需要擦除,两份都写入的耗时在毫秒级以内。

5.2 环形缓冲与CRC校验的落地做法

生产环境里最常见的日志需求是“循环覆盖”:只保留最近N条记录,新记录盖掉最旧的那条。在MRAM上做这个太自然了:维护一个写指针,写到结尾再跳到开头,无需擦除旧数据,直接覆盖。我设计了一个最简单的日志头结构,固定在每个扇区的起始位置:

| 2字节魔数 | 4字节序号 | 2字节数据长度 | 2字节CRC16 | 数据... |

魔数用于快速识别这是一条有效记录还是空白区。上电时从日志区头部扫描,通过魔数和CRC判断最后一条有效记录,然后把写指针定位到它之后。这样做的好处是无论上次掉电发生在哪个位置,扫描总是能恢复。

CRC16我建议用查表法实现,对每256字节日志计算一次,成本很低。MRAM本身不容易出现位翻转,但SPI通信受到干扰时可能读出错误数据,CRC就是最后一道防线。出现CRC错误时,直接丢弃这条记录并继续向后扫描,而不是让整个系统卡死在错误数据上。

5.3 掉电保护:如何把最后的日志留在MRAM里

工业设备掉电是不可回避的。掉电瞬间,电网电压并不是瞬间归零,而是有一个几十毫秒的跌落过程,MCU可以利用这段时间把关键数据写入MRAM。

我采用的是外部比较器+MCU的CN(Change Notification)中断方案:把稳压电源输出的3.3V通过电阻分压到比较器一端,比较器另一端接基准电压;当3.3V降到阈值以下时,比较器输出一个下降沿给MCU的外部中断引脚。MCU收到这个中断后,立即把当前的运行状态、还没来得及落盘的关键参数压缩成一条日志写入MRAM,然后停在原地等待系统彻底断电。

关键点是写入必须“短平快”。如果一次要写入几百字节,MRAM在10MHz SPI下也就几十微秒,完全赶得上。但这个时候再去初始化外设、打印调试信息就来不及了,所以掉电中断里的写日志函数必须全部用预先初始化好的SPI和纯RAM变量,不能依赖不了中断时的外设状态。另一个细节是,掉电中断触发后要先把全局中断关掉,防止其他高优先级中断打断写入流程。

如果MCU本身有BOR(欠压复位)模块,也可以直接用,但阈值往往不可调,而且复位前的执行时间很短。我的经验是:对要求高的项目,外部掉电检测电路比MCU内部BOR可靠得多。

5.4 要不要用DMA来加速

PIC24HJ256GP610的DMA在大量连续数据传输时很有用。比如每秒要记录256字节的日志,SPI时钟10MHz,CPU参与传输的话每字节都要等待SPIBUF,虽然耗时不多,但会打断主循环的实时性。用DMA的话,可以把要写入的数据块放到RAM缓冲区,配置好SPI发送目的地址,然后启动DMA通道,DMA自动逐字节搬运,传完触发中断。

不过DMA驱动MRAM有一个容易忽略的点:DMA只是搬运,CS#的拉低拉高还是要CPU手动控制。一个比较顺滑的框架是:准备缓冲区 -> 拉低CS# -> 启动DMA -> DMA传输完成中断里判断发送完成标志 -> 拉高CS#。中间不要手贱去操作SPI1BUF,否则会和DMA抢外设。

对于大多数工业日志应用,数据量没那么大,CPU轮询也够用。我的建议是:先把CPU轮询版本调通、功能验证完毕,再在吞吐量不足或中断响应冲突时引入DMA。一步到位用DMA反而会增加排错难度。

6. 常见问题与排查技巧实录

6.1 读出来的数据全是0x00或0xFF

现象是读MRAM所有地址都是同一个值,要么全0要么全FF。先说全FF:大概率是芯片根本没有被选中,或者CS#没有正确拉低。检查CS#连接的MCU引脚是否配置成了数字输出,是否被其他外设复用。再说全0:常见原因一是SPI模式不对,MRAM工作在Mode 0或Mode 3,如果MCU用了其他模式,读回来的MISO数据会被当作错误时钟边沿采过去,得到全0或错位的数据;二是MISO信号线断了或者接触不良,数据一直读的是高电平或者地电平。先拿示波器或逻辑分析仪看SCK和MISO波形,MISO如果完全没有翻转,那问题基本就在硬件连接或引脚映射上。

6.2 WEL始终为0,写入总是不生效

这个现象我遇到过好几次,最后发现原因几乎都是同一类:WREN指令和WRITE指令之间,CS#没有拉高。正如前面强调的,WEL锁存依赖CS#上升沿,如果CS#一直是低,WEL只有在下一次CS#上升沿才会生效,但此时已经开始写数据了,芯片根本不会进入写状态。

另外检查WP#引脚是否被拉低。WP#一旦为低,状态寄存器位会被钳位,WEL即使置位也会立刻失效。我建议把WP#直接接VDD,并且用万用表实测该脚电压,别看了原理图就默认没接错。

6.3 SCK没有波形,SPI模块像没在工作

PIC24HJ的SPI引脚输出依赖PPS映射。如果SCK或SDO引脚上没有信号,第一件事就是确认PPS配置是否成功。常见坑是配置PPS前没有正确解锁OSCCON,或者配置完忘了重新锁定,导致后续代码又把PPS寄存器改掉了。另一个典型坑是RPOR寄存器里的功能编号写错,把SDO1输出写成了SCK1输出,两个信号就交换了。这时候看波形,SCK脚上是数据信号,MOSI脚上倒是时钟,数据完全乱掉。

用MCC生成PPS配置是省事的选择,如果手动写,就在初始化代码里加一个延时,确保OSCCON解锁序列执行完毕再操作寄存器,不要在主程序外设初始化之前就调用SPI函数。

6.4 数据错位一个半字节或者首字节丢失

这种问题基本是SPI采样沿不对。MR25H40CDF支持Mode 0和Mode 3,前者空闲时钟为低、上升沿采样,后者空闲时钟为高、下降沿采样。如果配置差了一个采样沿,表现就是首字节对不上、后续字节右移一位或者左移一位。我的调试方法是:用逻辑分析仪抓取CS#、SCK、MOSI、MISO四条线,对照MRAM数据手册里的时序图逐眼比对。如果用示波器,看不清半位偏移,就改用逻辑分析仪,慢速触发,一帧一帧看。

6.5 大容量寻址错误:只读了前128KB或前256KB

这个问题很容易被忽视。MR25H40CDF是512KB,地址需要19位,不是16位也不是17位。如果驱动里把高字节地址固定清零,地址永远不会超过0xFFFF,芯片的容量就只被使用到64KB。如果只处理了A16,那么能访问的容量上限是128KB。在驱动上一定要按24位地址格式发三字节,高字节即使一般用不到也要参与计算。

// 错误的写法 SPI1_SwapByte(0x00); SPI1_SwapByte((addr >> 8) & 0xFF); SPI1_SwapByte(addr & 0xFF); // 正确的写法 SPI1_SwapByte((addr >> 16) & 0xFF); SPI1_SwapByte((addr >> 8) & 0xFF); SPI1_SwapByte(addr & 0xFF);

6.6 问题排查速查表

现象排查方向大概率原因
读全0xFFCS#、供电、接线CS#拉低失败,芯片未被选中
读全0x00SPI模式、MISO接线时钟采样沿设置错误,或MISO断线
WEL置不上WREN时序、WP#电平CS#未拉高,或WP#接了低电平
SCK无输出PPS配置、SPIEN位引脚映射未生效,模块未使能
数据错位采样沿、字节序CKE/CKP设置错误,或地址高低字节顺序反了
高地址回卷地址位宽只发送了16位地址,越过了实际容量

写在最后的一点实操体会

这套MR25H40CDF加PIC24HJ256GP610的方案,我在两个项目里跑过量产,稳定性确实让人放心。MRAM带来的最大改变不是性能数字上的提升,而是让存储代码的思考方式变简单了:没有擦除状态机,没有磨损均衡,没有页边界,存储就是一个“按地址直接读写”的普通外设。

如果你第一次做类似的组合,我建议先不要上DMA,也不要一上来就搞复杂的双备份,先用最朴素的轮询方式把单地址读写打通,再用逻辑分析仪确认波形,然后逐步加日志结构、CRC和掉电保护。驱动跑稳之后再考虑用DMA提升吞吐量,你会发现整个调试过程会顺畅很多。

如果在调试中遇到“读正常但写不进去”这类问题,请优先怀疑WREN时序和WP#引脚,而不是急着换芯片。这个坑占了MRAM调试问题的一大半。后续如果项目需要更大容量,也可以在同系列里选MR25H256、MR25H40等兼容型号,代码和电路基本不用大改。

返回列表