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

资讯详情

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

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

MRAM替代EEPROM与NOR Flash:PIC24 SPI接口工业存储方案详解 前言先说个实际场景我最近在给一套工业控制器做数据记录功能需要把运行日志、告警事件、标定参数这类数据可靠地存下来。设备上电时间不定、可能随时断电、环境里还有电磁干扰所以存储方案既要不怕频繁写又要能在掉电瞬间把关键数据落盘。试过EEPROM容量和寿命差点意思试过NOR Flash还得处理擦写均衡和无用页回收代码复杂度上来了。最终选了Everspin的MR25H40CDFSPI接口MRAM搭配Microchip的PIC24HJ256GP61016位MCU把存储和读取这套流程做扎实。这个组合解决的核心问题就一句话让嵌入式系统拥有“几乎是无限寿命”的非易失存储并且读写实现足够简单从原理到驱动代码都能完全掌控。这篇文章会把选型理由、硬件接法、PIC24侧的SPI配置、MR25H40CDF的驱动协议、工业场景下的数据组织方式以及我调试中踩过的坑一次性讲透适合正在做工业数据记录、参数存储、或者想在嵌入式里替换EEPROM/Flash的开发者参考。1. 方案选型背后的思路为什么是MRAM为什么是这颗MCU1.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这颗MCUPIC24HJ256GP610是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脚是SOMISO数据输出3脚是WP#写保护低有效4脚是VSS5脚是SIMOSI数据输入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#接到VDDWP#也接到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.3VMR25H40CDF也是3.3V所以不需要电平转换直接相连即可。连线关系如下MRAM的CS#接MCU的普通IOSCK接MCU的SPI时钟输出脚SIMOSI接MCU的SDO输出SOMISO接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对应CPOLCKE的定义则与常见的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如果Fcy40MHz目标F_SCK10MHz那么SPI1BRG1。对于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模块已使能并且发送缓冲区为空。第二等待接收完成的标志是SPIRBFSPI1STAT的bit0但有些情况下SPIROV接收溢出会被置位导致读到的数据错位所以驱动里要检查并清掉溢出标志。第三CS#的电平控制不要放在SPI函数里应当由上层时序来控制这样才能把多条指令组合成完整的MRAM操作事务。4. MR25H40CDF驱动协议实现与读写流程4.1 指令集与状态寄存器MR25H40CDF是标准的SPI接口MRAM操作码和常见的串行MRAM一致核心就六个指令。WREN0x06是写使能WRDI0x04是写禁用RDSR0x05是读状态寄存器WRSR0x01是写状态寄存器READ0x03是读数据WRITE0x02是写数据。指令操作码说明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 512KB4Mbit 4,194,304位 524,288字节 512KB。是的512KB地址需要19位。但要注意许多MRAM产品型号命名MR25H40代表“4兆位”可能是4,194,304位所以字节容量512KB。这和我之前记得的4Mb512KB一致。地址是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开始回卷逻辑上可能把前面的数据覆盖掉因此上层必须保证不越界。为了方便我在写函数里检查addrlen是否超过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的CNChange Notification中断方案把稳压电源输出的3.3V通过电阻分压到比较器一端比较器另一端接基准电压当3.3V降到阈值以下时比较器输出一个下降沿给MCU的外部中断引脚。MCU收到这个中断后立即把当前的运行状态、还没来得及落盘的关键参数压缩成一条日志写入MRAM然后停在原地等待系统彻底断电。关键点是写入必须“短平快”。如果一次要写入几百字节MRAM在10MHz SPI下也就几十微秒完全赶得上。但这个时候再去初始化外设、打印调试信息就来不及了所以掉电中断里的写日志函数必须全部用预先初始化好的SPI和纯RAM变量不能依赖不了中断时的外设状态。另一个细节是掉电中断触发后要先把全局中断关掉防止其他高优先级中断打断写入流程。如果MCU本身有BOR欠压复位模块也可以直接用但阈值往往不可调而且复位前的执行时间很短。我的经验是对要求高的项目外部掉电检测电路比MCU内部BOR可靠得多。5.4 要不要用DMA来加速PIC24HJ256GP610的DMA在大量连续数据传输时很有用。比如每秒要记录256字节的日志SPI时钟10MHzCPU参与传输的话每字节都要等待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等兼容型号代码和电路基本不用大改。
返回列表