ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

dsPIC33FJ256GP710A 通过 SPI 驱动 MR25H40CDF MRAM 的工业存储实战

dsPIC33FJ256GP710A 通过 SPI 驱动 MR25H40CDF MRAM 的工业存储实战 1. 项目背景与核心需求拆解1.1 为什么要在工业场景里讨论 MRAM 这件事工业现场的数据存储有个很拧巴的矛盾既要写得快、写得频繁又要求掉电不丢、寿命够长。传统方案里EEPROM 写一次要等好几毫秒擦写次数也就百万级NOR Flash 擦除要按扇区来写之前还得先擦遇到频繁记录小数据的场景寿命和实时性都扛不住。铁电存储器 FRAM 读写快、寿命高但容量普遍偏小、价格偏高而且很多型号已经停产或供货不稳。MR25H40CDF 这类磁阻随机存储器 MRAM 的出现恰好卡在这个缺口上。它的核心特点可以概括成几句话写入不需要擦除、写入速度接近 SRAM、掉电数据不丢、擦写寿命几乎无限、SPI 接口直接读写。对于工业设备里那种“每隔几十毫秒记录一次状态、一跑就是好几年”的需求MRAM 的匹配度非常高。我这次做的项目主控选的是 dsPIC33FJ256GP710A一颗带 DSP 引擎的 16 位单片机在电机控制、数字电源、工业采集这些领域用得很多。它自带 SPI 模块和 MR25H40CDF 对接非常自然。整个项目的目标很明确用 dsPIC33FJ256GP710A 通过 SPI 总线对 MR25H40CDF 进行稳定可靠的数据存储和读取并在工业级电磁环境下保证数据完整性。1.2 这颗 MRAM 和这颗 dsPIC 到底能解决什么问题先说 MR25H40CDF 的硬指标。它是 4Mbit 容量也就是 512K 字节组织方式是 512K × 8 位。SPI 接口支持模式 0 和模式 3最高时钟可以跑到 40MHz供电范围 2.7V 到 3.6V工业级温度范围 -40℃ 到 85℃。封装是 8 脚 SOIC引脚定义很标准片选 CS、时钟 SCK、数据输入 SI、数据输出 SO加上电源和地另外还有 WP 和 HOLD 两个控制脚。再说 dsPIC33FJ256GP710A。它有两个 SPI 模块SPI1 和 SPI2支持主从模式8 位或 16 位数据传输带 4 级 FIFO最高波特率可以到 30MHz在 40MIPS 下。它还有 DMA 控制器可以在 SPI 收发时减少 CPU 干预。对于这个项目来说SPI1 用来接 MRAMSPI2 可以留给其他外设互不干扰。这两颗芯片组合起来能做的事情很实际工业设备运行日志的循环记录、关键参数的掉电保存、采集数据的缓存队列、设备配置信息的快速读写。尤其是那些“写频率高、单次数据量小、不能丢”的场景MRAM 的优势非常明显。1.3 适合哪些人参考这篇内容如果你正在做工业控制、电力监测、电机驱动、数据采集这类嵌入式项目需要一颗靠谱的非易失存储那这篇内容可以直接抄作业。如果你之前只用过 EEPROM 或 Flash想了解 MRAM 的用法和坑也能从里面找到有用的东西。哪怕你用的是 STM32 或者其他平台SPI 操作 MRAM 的底层逻辑是相通的把寄存器操作换成 HAL 库调用就行。我下面会从硬件连接、SPI 配置、读写时序、数据完整性保护、常见问题排查这几个角度把整个项目拆开讲清楚。所有参数和步骤都是实际调试过的不是照着手册抄的。2. 硬件设计与 SPI 接口连接要点2.1 引脚连接与硬件片选的设计取舍MR25H40CDF 和 dsPIC33FJ256GP710A 的连接看起来简单但片选信号的处理方式会直接影响通信稳定性。先看基本连接MR25H40CDF 引脚dsPIC33FJ256GP710A 引脚说明CSRB2可配置为 SPI1 的 SS 或普通 IO片选低有效SCKRB1SPI1 SCK时钟SIRB0SPI1 SDI主出从入SORB3SPI1 SDO主入从出WP上拉到 VDD写保护不用时拉高HOLD上拉到 VDD保持不用时拉高VDD3.3V电源VSSGND地这里有个关键选择片选到底用硬件 SS 还是软件 IO 控制。dsPIC33FJ 的 SPI 模块在使能 SS 功能后如果 SS 引脚被拉低模块会自动进入从模式或者产生错误这在多设备共享 SPI 总线时很麻烦。我的做法是把 SS 功能关掉用普通 IO 口手动控制片选。这样做的好处是片选时序完全由软件掌控想什么时候拉低就什么时候拉低想保持多久就保持多久不会受 SPI 模块内部逻辑干扰。具体操作上把 RB2 配置成普通数字输出初始状态拉高。每次读写 MRAM 之前先拉低 RB2发完命令和数据之后再拉高。这个拉低到拉高的时间就是一次完整的 SPI 事务。2.2 SPI 模式选择与时序匹配MR25H40CDF 支持 SPI 模式 0CPOL0CPHA0和模式 3CPOL1CPHA1。这两种模式的区别在于时钟空闲电平和采样边沿。模式 0 下时钟空闲为低数据在上升沿采样模式 3 下时钟空闲为高数据在下降沿采样。我选的是模式 0原因是 dsPIC33FJ 的 SPI 模块在模式 0 下配置最直观而且大多数调试工具和逻辑分析仪默认也是按模式 0 解析。配置的时候要注意几个寄存器SPI1CON1设置主模式、时钟极性、时钟相位、数据位宽。SPI1CON2设置帧模式、FIFO 控制。SPI1BRG波特率发生器决定 SCK 频率。波特率的计算方式是SCK 频率 FCY / (2 × (SPI1BRG 1))。假设系统时钟 FCY 是 40MHz想要 10MHz 的 SCK那么 SPI1BRG 40M / (2 × 10M) - 1 1。实际配置下来10MHz 在普通 PCB 上跑得很稳如果走线比较长或者环境干扰大可以降到 5MHz 甚至 2.5MHz。注意MRAM 虽然标称支持 40MHz但实际能跑多快取决于 PCB 走线质量、片选信号的边沿速度、以及电源去耦是否到位。不要一上来就拉满先用低速调通再逐步提速。2.3 电源去耦与 PCB 布局的实战经验MRAM 在写入瞬间会有较大的瞬态电流如果电源去耦不到位写入过程中可能出现位翻转或者写入失败。我的做法是在 MR25H40CDF 的 VDD 和 VSS 之间放两颗电容一颗 0.1μF 的陶瓷电容紧贴引脚一颗 1μF 的陶瓷电容放在附近。这两颗电容的作用不同0.1μF 负责滤高频噪声1μF 负责提供写入瞬间的电荷缓冲。PCB 布局上SPI 的四根信号线尽量等长、尽量短避免和电机驱动、继电器控制这些大电流线路平行走线。如果实在避不开中间加地线隔离。SCK 是时钟信号最容易辐射干扰走线两侧最好包地。片选 CS 线对噪声敏感因为它一旦被误触发整个 SPI 事务就乱了所以 CS 线也要尽量短必要时加一个 100pF 的电容到地做滤波。还有一点容易被忽略WP 和 HOLD 引脚不能悬空。这两个引脚内部有弱上拉但在工业环境下悬空引脚容易耦合噪声导致意外进入写保护或保持状态。我的做法是直接用 10kΩ 电阻上拉到 VDD确保它们始终处于无效状态。3. dsPIC33FJ 的 SPI 模块配置与底层驱动3.1 SPI1 初始化流程与寄存器详解dsPIC33FJ256GP710A 的 SPI 初始化有一套固定的流程顺序不能乱。我把它整理成下面这几步关闭 SPI 模块先把 SPI1STAT 的 SPIEN 位清零确保配置过程中模块不工作。配置引脚方向SCK 和 SDO 配置为输出SDI 配置为输入CS 配置为普通 IO 输出并初始拉高。设置 SPI1CON1主模式、模式 0、8 位数据、时钟预分频。设置 SPI1CON2关闭帧模式FIFO 根据需要配置。设置 SPI1BRG写入波特率值。清除中断标志把 SPI1STAT 的中断标志位清掉。使能 SPI 模块置位 SPIEN。对应的代码大概长这样void SPI1_Init(void) { SPI1STATbits.SPIEN 0; // 关闭 SPI SPI1CON1bits.DISSCK 0; // 使能 SCK SPI1CON1bits.DISSDO 0; // 使能 SDO SPI1CON1bits.MODE16 0; // 8 位模式 SPI1CON1bits.SMP 0; // 中间采样 SPI1CON1bits.CKE 1; // 模式 0空闲低上升沿采样 SPI1CON1bits.CKP 0; // 空闲低电平 SPI1CON1bits.MSTEN 1; // 主模式 SPI1CON1bits.SPRE 0b110; // 次级预分频 2:1 SPI1CON1bits.PPRE 0b10; // 初级预分频 4:1 SPI1BRG 1; // 波特率设置 SPI1STATbits.SPIROV 0; // 清除溢出标志 SPI1STATbits.SPIEN 1; // 使能 SPI }这里 CKE 和 CKP 的组合决定了 SPI 模式。模式 0 对应 CKP0、CKE1意思是空闲低电平在时钟上升沿之前数据已经准备好上升沿采样。这个配置和 MR25H40CDF 的模式 0 时序完全匹配。3.2 字节收发函数的实现与陷阱SPI 的字节收发函数看起来简单但里面有几个坑。dsPIC33FJ 的 SPI 模块在发送一个字节的同时会接收一个字节所以收发是一体的。标准写法是uint8_t SPI1_ExchangeByte(uint8_t txData) { SPI1BUF txData; // 写入发送缓冲 while (!SPI1STATbits.SPITBF); // 等待发送完成 return SPI1BUF; // 读取接收数据 }这里有个细节等待发送完成的标志用 SPITBF 还是 SPIRBF。SPITBF 是发送缓冲满标志SPIRBF 是接收缓冲满标志。在高速通信时如果只等 SPITBF可能在数据还没完全移出的时候就拉高了片选导致最后一个位丢失。稳妥的做法是等 SPIRBF 置位表示一个完整的字节已经收发完毕。uint8_t SPI1_ExchangeByte(uint8_t txData) { SPI1BUF txData; while (!SPI1STATbits.SPIRBF); return SPI1BUF; }另外每次读写之前必须手动拉低 CS读写完成后拉高 CS。MR25H40CDF 的 SPI 事务是以 CS 的下降沿开始、上升沿结束的。如果 CS 一直保持低电平MRAM 会认为一次事务还没结束后续的命令会被当成数据继续接收整个通信就乱了。3.3 命令集与地址寻址方式MR25H40CDF 的命令集不复杂常用的就几条命令操作码说明WREN0x06写使能WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据地址是 19 位宽因为 512K 字节需要 19 根地址线。发送地址的时候分三个字节第一个字节是命令码后面三个字节是地址的高、中、低。比如要读地址 0x00000 的数据发送顺序是 0x03、0x00、0x00、0x00然后连续读出数据。写操作稍微麻烦一点必须先发 WREN 命令使能写入再发 WRITE 命令和地址然后写数据。写完数据拉高 CS 之后MRAM 内部会启动一个写入周期。这个周期很短典型值只有几十纳秒但为了保险我在写完最后一个字节后加了一个短延时再发下一条命令。4. 数据存储与读取的完整实现4.1 单字节读写与页写操作单字节读写是最基础的操作。读一个字节的流程是拉低 CS发 READ 命令发三字节地址读一个字节拉高 CS。写一个字节的流程是拉低 CS发 WREN拉高 CS拉低 CS发 WRITE 命令发三字节地址写一个字节拉高 CS。这里要注意 WREN 命令必须单独发一次而且发完之后 CS 要拉高再拉低否则 WREN 不会生效。我一开始图省事把 WREN 和 WRITE 放在同一个 CS 低电平周期里结果写不进去。后来查手册才发现WREN 需要 CS 的上升沿来锁存写使能状态。页写操作可以一次写入多个字节。MR25H40CDF 的页大小是 256 字节地址的低 8 位在页内循环高 11 位是页地址。如果一次写入超过页边界地址会自动回绕到页首覆盖之前的数据。所以做连续写入的时候要么保证不跨页要么在跨页时重新发 WRITE 命令。void MRAM_WritePage(uint32_t addr, uint8_t *data, uint16_t len) { uint16_t pageOffset addr 0xFF; if (pageOffset len 256) { len 256 - pageOffset; // 截断到页边界 } CS_LOW(); SPI1_ExchangeByte(0x06); // WREN CS_HIGH(); CS_LOW(); SPI1_ExchangeByte(0x02); // WRITE SPI1_ExchangeByte((addr 16) 0xFF); SPI1_ExchangeByte((addr 8) 0xFF); SPI1_ExchangeByte(addr 0xFF); for (uint16_t i 0; i len; i) { SPI1_ExchangeByte(data[i]); } CS_HIGH(); }4.2 状态寄存器与写保护机制MR25H40CDF 的状态寄存器有几位很关键WIP 位表示写入进行中WEL 位表示写使能锁存BP0 和 BP1 是块保护位SRWD 是状态寄存器写保护位。读状态寄存器用 RDSR 命令写状态寄存器用 WRSR 命令。WIP 位在写入周期内为 1写完自动变 0。虽然 MRAM 的写入周期很短但在连续写入的场景下还是建议每次写完检查一下 WIP 位确保上一次写入已经完成。WEL 位在 WREN 命令后置 1在 WRDI 命令或写入完成后自动清 0。块保护位 BP0 和 BP1 可以保护部分或全部存储区不被写入。在工业应用中如果有些区域存的是校准参数或设备序列号不希望被程序意外改写就可以用块保护功能把这些区域锁起来。具体保护范围是BP1BP0保护范围00无保护01保护高 1/4 区域10保护高 1/2 区域11保护全部区域设置块保护的时候要注意WRSR 命令本身也受 SRWD 位控制。如果 SRWD 置 1 且 WP 引脚为低状态寄存器就变成只读连块保护位都改不了。所以我的做法是 WP 引脚始终上拉SRWD 保持 0这样既能用块保护又不会把自己锁死。4.3 数据完整性校验与循环存储策略工业现场的数据存储最怕的是写了一半掉电或者数据在传输过程中被干扰出错。我的做法是在每个数据块后面加两个字节的 CRC16 校验。写入的时候计算 CRC 一起写进去读取的时候重新计算 CRC 和存储的值比对不一致就认为数据无效。循环存储策略是另一个重点。假设我要记录设备运行日志总共 512K 字节的空间每个日志条目 32 字节那就能存 16384 条。我用一个 4 字节的写指针记录当前写位置每次写完一条指针加 32到了末尾就回绕到开头。读的时候从指针位置往前读就能拿到最新的日志。这个写指针本身也需要保护。我的做法是双备份加 CRC在存储区的固定位置存两份写指针每份后面跟一个 CRC。每次更新指针的时候先写第一份再写第二份。读取的时候如果第一份 CRC 不对就用第二份如果两份都不对就触发异常处理。这样即使更新指针的过程中掉电至少有一份是完整的。5. 常见问题与排查技巧实录5.1 SPI 通信失败的典型原因调试 SPI 通信最直接的工具是逻辑分析仪。我遇到过几次通信失败基本都是下面这几种原因片选信号时序不对。有一次 CS 拉低之后SCK 还没稳定就开始发数据导致第一个位丢失。后来在 CS 拉低和第一个 SCK 边沿之间加了几个 NOP 延时问题解决。这个延时不需要很长几百纳秒就够但必须有。SPI 模式配置错误。MRAM 支持模式 0 和模式 3如果主控配成了模式 1 或模式 2数据采样边沿就错了读出来的全是 0xFF 或 0x00。用逻辑分析仪抓一下 SCK 和 SI 的波形对照手册上的时序图一眼就能看出来。波特率过高。在面包板上搭电路的时候10MHz 经常跑不稳降到 1MHz 就正常了。PCB 上一般能跑到 20MHz 以上但也要看走线质量。我的经验是先用 1MHz 调通功能再逐步提高波特率找到稳定工作的上限。电源噪声导致写入失败。有一次写入的数据读出来总是差几个位查了半天发现是电源纹波太大。在 MRAM 的 VDD 引脚旁边加了一颗 0.1μF 电容之后问题消失。5.2 数据读写异常的排查流程遇到数据读写异常我一般按下面这个流程排查确认硬件连接用万用表量一下 VDD 是不是 3.3VCS、SCK、SI、SO 有没有虚焊。抓波形用逻辑分析仪抓一次完整的读操作看命令码、地址、数据是否符合预期。读状态寄存器发 RDSR 命令看 WIP 和 WEL 位是否正常。降速测试把 SPI 时钟降到 1MHz排除时序问题。换地址测试换一个地址读写排除某个存储单元损坏的可能。检查块保护读状态寄存器确认 BP0 和 BP1 没有意外置位。这个流程走下来大部分问题都能定位到。如果还不行就换一颗 MRAM 芯片试试排除芯片本身损坏的可能。5.3 常见问题速查表现象可能原因解决方法读出来全是 0xFFSPI 模式错误、CS 未拉低、SO 线断路检查模式配置和硬件连接读出来全是 0x00SCK 无输出、SI 线断路检查时钟配置和 SI 连接写入后读出来不对WREN 未生效、写入周期未完成确认 WREN 后 CS 有上升沿检查 WIP 位部分数据错误电源噪声、跨页写入加强去耦检查页边界处理通信时好时坏波特率过高、走线太长降低波特率缩短走线状态寄存器读不出命令码错误、CS 时序不对核对命令码检查 CS 时序5.4 几个容易踩的坑第一个坑WREN 和 WRITE 之间不能插入其他命令。WREN 之后如果发了别的命令写使能会自动清除。所以 WREN 之后必须紧接着发 WRITE。第二个坑CS 拉高之后要等一小段时间再发下一条命令。MRAM 虽然快但内部写入周期还是需要一点时间。我一般加 10 微秒的延时实测下来很稳。第三个坑不要忽略 HOLD 引脚。HOLD 拉低会暂停通信如果这个引脚悬空被干扰拉低通信就会莫名其妙中断。一定要上拉。第四个坑地址要按 19 位处理。512K 字节的地址范围是 0x00000 到 0x7FFFF如果程序里用 16 位变量存地址超过 0xFFFF 的部分就丢了。我一开始就犯了这个错误访问高地址区域时数据全错。6. 性能优化与进阶用法6.1 利用 DMA 减少 CPU 干预dsPIC33FJ256GP710A 的 SPI 模块支持 DMA 传输。在需要连续读写大量数据的时候用 DMA 可以大幅减少 CPU 占用。配置 DMA 的步骤是设置 DMA 通道的源地址和目的地址设置传输长度使能 SPI 的 DMA 请求然后启动 DMA。具体来说读操作的时候DMA 源地址设为 SPI1BUF目的地址设为内存缓冲区写操作反过来。DMA 传输完成之后会产生中断在中断里处理数据。这样 CPU 只需要在开始和结束的时候介入中间的数据搬运全部由 DMA 完成。不过 DMA 用起来也有代价配置比较复杂而且调试的时候如果出问题定位比普通 SPI 麻烦。我的建议是数据量小的时候直接用查询方式简单可靠数据量大、对 CPU 占用有要求的时候再上 DMA。6.2 提高写入速度的几个技巧MRAM 的写入速度本身已经很快了但通过一些技巧还能再压榨一下。第一个技巧是批量写入把多个小数据攒在一起用页写命令一次写进去减少命令开销。第二个技巧是减少 WREN 次数如果连续写多个页可以在每页写之前发一次 WREN而不是每个字节都发。第三个技巧是提高 SPI 时钟在保证稳定的前提下把 SCK 频率提到 20MHz 以上写入时间能缩短一半。实测数据写 256 字节的一页在 10MHz SPI 下大约需要 210 微秒在 20MHz 下大约 110 微秒。如果换成 EEPROM同样的数据量需要 5 毫秒以上差距非常明显。6.3 在工业环境中的可靠性设计工业环境的电磁干扰比实验室恶劣得多可靠性设计必须做足。除了前面提到的电源去耦和 PCB 布局还有几个措施看门狗在 SPI 通信函数里加超时检测如果等待标志位超过一定时间就强制退出并复位 SPI 模块。数据校验所有关键数据都加 CRC 校验读取时验证不通过就重读或报错。双备份关键参数存两份互为备份。定期自检设备上电时做一次全区域读写测试确认 MRAM 工作正常。这些措施看起来麻烦但在工业现场一次数据丢失可能导致设备停机甚至安全事故投入是值得的。7. 实际调试记录与经验总结7.1 一次完整的调试过程记录项目刚开始的时候我先把 dsPIC33FJ 的 SPI1 配置好用逻辑分析仪抓波形确认 SCK、SI、SO 的时序符合模式 0 的要求。然后发 RDSR 命令读状态寄存器读出来是 0x00说明通信基本正常。接着测试单字节写入。先发 WREN再发 WRITE 和地址写一个 0x5A 进去。然后读出来结果是 0xFF。查了半天发现是 WREN 和 WRITE 之间没有拉高 CS导致 WREN 没生效。改成 WREN 之后拉高 CS再拉低发 WRITE问题解决。然后测试页写。写 256 字节的递增数据读出来比对发现第 128 字节之后的数据全错。查手册发现页大小是 256 字节但地址的低 8 位在页内循环我写的起始地址不是页对齐的导致数据回绕覆盖。改成页对齐写入之后正常。最后测试掉电保护。在写入过程中随机断电重新上电后读取数据用 CRC 校验发现大部分情况下数据完整偶尔有写入中断的情况CRC 能检测出来。配合双备份策略数据可靠性满足要求。7.2 关于 MRAM 选型的一些个人看法MR25H40CDF 这颗芯片我用下来的感受是适合那些对写入速度、擦写寿命、数据可靠性有要求的场景但成本比 EEPROM 和 Flash 高。如果你的项目只是偶尔存几个配置参数EEPROM 就够了没必要上 MRAM。但如果你需要频繁记录数据、要求掉电不丢、而且设备寿命内写入次数可能超过百万次那 MRAM 的价值就体现出来了。另外MRAM 的容量选择也要考虑。512K 字节对于大多数工业记录场景够用了但如果要存图像或者大量历史数据可能就需要更大容量的存储方案比如加一颗大容量 Flash 做数据仓库MRAM 只用来存关键指针和最新数据。7.3 后续可以扩展的方向这个项目目前实现的是基础的读写和校验功能后面还可以往几个方向扩展。一个是文件系统在 MRAM 上实现一个简单的日志文件系统方便管理大量记录。另一个是多设备共享 SPI 总线用不同的片选信号挂多颗 MRAM 或其他 SPI 设备扩展存储容量。还有一个是与上位机通信通过 UART 或 CAN 把 MRAM 里的数据传到上位机做分析和展示。我在实际使用中发现MRAM 的写入速度优势在循环记录场景下特别明显。以前用 EEPROM 的时候每次记录要等好几毫秒程序只能阻塞等待换成 MRAM 之后写入几乎不占时间程序可以继续处理其他任务。这个体验上的提升是用过之后才能感受到的。
返回列表