1. 项目缘起与整体设计思路工业现场的数据存储有个很尴尬的夹层用 EEPROM 吧容量小、写入慢频繁记录日志很快就到寿命上限用 SD 卡或者 eMMC 吧文件系统复杂、掉电容易损坏、还要跑一套驱动栈对实时性是个负担。我这次要聊的组合就是专门填这个夹层的方案——MR25H40CDF这颗 4Mbit 的 SPI 接口 MRAM磁性随机存储器配上MK64FN1M0VDC12这颗 Kinetis K64 系列的 Cortex-M4 MCU做工业与嵌入式场景下的数据存储与读取。先说清楚这套组合到底解决什么问题。MR25H40CDF 的核心卖点是非易失 近乎无限的擦写寿命 纳秒级写入。它不像 Flash 那样需要先擦后写、也不像 EEPROM 那样写入要等几毫秒。你往里面写一个字节它当场就存住了掉电也不丢。这对工业设备意味着什么意味着你可以把关键状态、故障快照、累计计量值、掉电前的最后一批采样数据直接当成 RAM 一样随手写进去不用做复杂的磨损均衡也不用担心写坏。MK64FN1M0VDC12 这边1MB Flash、256KB SRAM、120MHz 主频带多个 SPI 外设SPI0/SPI1/SPI2还有 FlexBus、DMA 这些外设。选它来驱动 MR25H40CDF主要看中的是它的 SPI 速率和 DMA 能力——MRAM 本身支持很高的 SPI 时钟如果 MCU 这边用 DMA 搬运就能做到几乎不占 CPU 的连续数据记录。整体设计思路我拆成三层来看物理层MR25H40CDF 走标准 SPI 四线SCLK、MOSI、MISO、CS供电 3.3V和 K64 的 IO 电平天然匹配不需要电平转换。驱动层K64 的 SPI 外设配置成主机模式CPOL/CPHA 按 MRAM 的 Mode 0 来设速率先保守跑 10MHz 验证稳定后再往上顶。应用层把 MRAM 当成一块线性的 512KB 存储空间来管理前面放元数据区魔数、版本、写指针后面放环形日志区掉电重启后靠元数据恢复写位置。为什么不用文件系统因为在这个场景里文件系统带来的抽象成本远大于收益。工业设备要的是确定性——我知道第 N 个字节写在哪里读回来就是什么中间不经过任何缓存和索引层。MRAM 的随机访问特性刚好支持这种“裸地址读写”的用法。提示MR25H40CDF 是 512K x 8 的组织方式也就是 512KB 字节寻址空间地址线是 19 位0x00000 到 0x7FFFF。别和 4Mbit 这个标称容量搞混换算过来就是 512KB。2. 核心器件解析与选型背后的考量2.1 MR25H40CDF 到底强在哪很多人第一次接触 MRAM 会把它和 FRAM铁电存储器搞混。两者都是非易失、都快、都耐写但机理不同。MRAM 用的是磁性隧道结MTJ来存数据靠磁化方向表示 0 和 1FRAM 靠铁电材料的极化方向。实际用下来MRAM 在抗辐射、抗磁场干扰、高温稳定性上更有优势工业级温度范围-40 到 85℃内表现很稳。MR25H40CDF 的关键参数我列一下这些是选型时必须核对的参数项数值说明容量4Mbit / 512KB字节寻址接口SPI Mode 0/3支持最高 40MHz供电2.7V ~ 3.6V典型 3.3V写入寿命10^14 次以上基本可以忽略磨损数据保持20 年以上断电不丢写入方式无需擦除直接写这是和 Flash 最大的区别封装8-SOIC / 8-DFN工业板常用 SOIC这里最值得说的是“无需擦除”。Flash 写之前必须把整个扇区擦成 0xFF擦除动作慢且伤寿命。MRAM 是字节级直接覆盖写你写 0x5A 进去它立刻变成 0x5A不需要任何前置操作。这个特性直接简化了驱动逻辑——没有擦除状态机没有等待轮询写命令发完数据就完事。2.2 MK64FN1M0VDC12 的 SPI 资源盘点K64 这颗片子我用得比较多它的 SPI 外设在参考手册里叫 DSPI即 SPI 模块有几个点要提前搞清楚SPI0 和 SPI1 是高速 SPI挂在系统时钟上可以跑到很高的分频SPI2 相对弱一些。每个 SPI 都有独立的PCS片选信号支持硬件片选和软件片选两种模式。支持FIFO深度是 4 级配合 DMA 可以做到连续传输不中断。时钟极性和相位CPOL/CPHA可配覆盖 Mode 0~3。我一般把 MRAM 挂在SPI0上因为它的时钟源更干净分频选项多。K64 的 SPI 波特率计算公式是SPI 波特率 SPI 模块时钟 / (Prescaler × (1 波特率分频值))假设 SPI0 模块时钟是 60MHz我想跑 15MHz那就选 Prescaler2分频值1得到 60/(2×2)15MHz。这个计算在配置寄存器的时候必须算准否则实际速率和预期差一截调试时会很迷惑。2.3 为什么是这两个器件配对有人会问MRAM 这么贵为什么不用便宜的 SPI NOR Flash答案在写入模式上。NOR Flash 适合“写一次读很多次”的场景比如存固件、存配置。但工业设备经常要高频记录运行数据比如每 10ms 记一次电机电流一天就是 864 万次写入。NOR Flash 的扇区擦写寿命通常只有 10 万次几天就写废了。MRAM 的 10^14 次寿命按这个频率能写几千年。而 K64 这边它的 DMA 和 SPI FIFO 组合能让 MRAM 的写入几乎不占 CPU。我实测过用 DMA 往 MRAM 连续写 512KBCPU 占用率不到 5%主循环该干嘛干嘛。这个组合在“高频记录 实时响应”的工业场景里是很务实的搭配。3. 硬件连接与 SPI 时序实操要点3.1 硬件连线与去耦MR25H40CDF 的引脚定义很标准CS、SOMISO、SIMOSI、SCK、VDD、VSS加上 WP 和 HOLD这两个在标准 SPI 模式下可以拉高不用。接线时我踩过的坑主要在两个地方第一是去耦电容。MRAM 在写入瞬间的电流会有小波动如果 VDD 上的去耦不到位高速写入时会出现偶发数据错误。我的做法是在芯片 VDD 引脚旁边放一个 0.1uF 的陶瓷电容紧贴引脚再并一个 1uF 的钽电容做低频储能。这两个电容的走线要短地要就近打过孔。第二是CS 信号的处理。K64 的 SPI 支持硬件片选但我更推荐用软件片选普通 GPIO 控制 CS。原因是硬件片选在 FIFO 和 DMA 模式下片选的拉低拉高时机由外设控制有时候和你的命令序列对不齐尤其是 MRAM 这种“命令 地址 数据”的多段传输。用 GPIO 手动控制 CS你能精确掌握每一帧的边界调试起来心里有底。注意MRAM 的 CS 拉低后第一个时钟沿开始采样命令字节。如果 CS 在传输中间被意外拉高整个命令会作废芯片回到空闲态。所以软件片选时要确保一次完整操作期间 CS 保持低电平。3.2 SPI 模式与时序确认MR25H40CDF 支持 SPI Mode 0CPOL0, CPHA0和 Mode 3CPOL1, CPHA1。我一般用Mode 0因为它在空闲时 SCK 为低第一个边沿是上升沿采样和大多数逻辑分析仪的默认触发方式一致抓波形方便。时序上要关注几个时间参数CS 建立时间CS 拉低到第一个 SCK 边沿之间至少要等几个纳秒K64 的 GPIO 翻转速度足够一般不用刻意加延时。SCK 频率初次调试建议从 1MHz 开始确认读写正常后再逐步提高到 10MHz、20MHz。我实测 MR25H40CDF 在 3.3V 下跑 20MHz 很稳40MHz 在长走线时会有边沿变缓的问题。命令间间隔连续操作之间CS 拉高后要留一点时间tSHSL典型值几十纳秒K64 的 GPIO 操作天然满足。3.3 读写命令集速查MR25H40CDF 的命令集不复杂常用的就几条命令操作码说明WREN0x06写使能写之前必须发WRDI0x04写禁止READ0x03读数据后跟 3 字节地址WRITE0x02写数据后跟 3 字节地址RDSR0x05读状态寄存器WRSR0x01写状态寄存器注意地址是3 字节虽然实际只用到 19 位但高位补 0 即可。读操作是“发命令 发地址 连续读”地址会自动递增所以读一整块数据只需要一次 CS 拉低。写操作稍微特殊每次写之前必须先发 WREN0x06否则写命令会被忽略。这是很多新手第一次调不通的原因——直接发 WRITE 命令数据写不进去读回来还是旧值。WREN 之后 CS 要拉高再拉低然后发 WRITE 命令和地址数据。4. 驱动实现与 DMA 加速方案4.1 基础读写函数实现先给一个最朴素的字节读写实现用轮询方式方便验证硬件。这里以 K64 的寄存器操作为例实际项目里可以用 SDK 的 SPI 驱动封装。// 软件片选控制 #define MRAM_CS_LOW() (GPIOB_PDOR ~(1 10)) #define MRAM_CS_HIGH() (GPIOB_PDOR | (1 10)) // SPI 单字节收发轮询 uint8_t spi_transfer(uint8_t data) { while (!(SPI0_SR SPI_SR_TFFF_MASK)); // 等待发送 FIFO 有空位 SPI0_PUSHR data | SPI_PUSHR_PCS_MASK; // 写入数据 while (!(SPI0_SR SPI_SR_RFDF_MASK)); // 等待接收完成 return SPI0_POPR 0xFF; } // 读 MRAM void mram_read(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LOW(); spi_transfer(0x03); // READ 命令 spi_transfer((addr 16) 0xFF); // 地址高字节 spi_transfer((addr 8) 0xFF); spi_transfer(addr 0xFF); for (uint32_t i 0; i len; i) { buf[i] spi_transfer(0x00); } MRAM_CS_HIGH(); } // 写 MRAM void mram_write(uint32_t addr, const uint8_t *buf, uint32_t len) { MRAM_CS_LOW(); spi_transfer(0x06); // WREN MRAM_CS_HIGH(); // 短暂延时确保 WREN 生效 for (volatile int i 0; i 10; i); MRAM_CS_LOW(); spi_transfer(0x02); // WRITE 命令 spi_transfer((addr 16) 0xFF); spi_transfer((addr 8) 0xFF); spi_transfer(addr 0xFF); for (uint32_t i 0; i len; i) { spi_transfer(buf[i]); } MRAM_CS_HIGH(); }这段代码能跑通但效率不高。每次spi_transfer都在等 FIFO 标志CPU 全程被占住。写 512KB 数据要花不少时间而且主循环被阻塞。4.2 用 DMA 把 CPU 解放出来K64 的 SPI 支持 DMA 请求发送和接收各有一个 DMA 通道。我的做法是把“命令 地址 数据”拼成一个连续的发送缓冲区然后启动 DMA 发送同时启动接收 DMA 把 MISO 上的数据收回来。这样一次传输几百字节CPU 只需要在开头配置和结尾检查中间完全不管。具体配置步骤配置 DMA 通道 0 为 SPI0 发送请求源源地址是发送缓冲区目的地址是SPI0_PUSHR。配置 DMA 通道 1 为 SPI0 接收请求源源地址是SPI0_POPR目的地址是接收缓冲区。设置传输长度启动两个通道。在 DMA 完成中断里拉高 CS处理数据。这里有个细节SPI 的发送和接收是同步的你发一个字节就必然收一个字节。所以读操作时发送缓冲区里命令和地址之后要填 0x00 占位接收缓冲区里前几个字节是无效的对应命令和地址阶段真正的数据从第 4 个字节开始。提示DMA 传输期间 CS 必须保持低电平。如果传输长度很大要注意 MRAM 的地址自动递增不会越界回绕超过 0x7FFFF 后行为未定义所以单次传输长度要控制在剩余空间内。4.3 环形日志区的设计把 MRAM 当线性空间用还不够工业场景需要“循环记录、掉电可恢复”。我的做法是在 MRAM 开头留 64 字节做元数据区偏移 0x00魔数 0x4D52414DMRAM用于判断是否首次使用。偏移 0x04写指针记录下一个要写入的位置。偏移 0x08记录总条数。偏移 0x0C每条记录的长度。日志区从 0x100 开始到 0x7FFFF 结束。每次写一条记录先更新数据再更新写指针。写指针到达末尾就回绕到 0x100。掉电重启后读魔数确认有效再读写指针恢复状态。这个设计的精髓在于写指针的更新顺序。一定要先写数据再更新指针。如果反过来掉电时可能出现指针已经前进但数据没写完的情况恢复后会读到垃圾。先写数据后更新指针最坏情况是最后一条记录不完整但指针指向的位置之前的数据都是可信的。5. 常见问题与排查技巧实录5.1 读回来全是 0xFF 或 0x00这是最常见的现象。排查顺序我总结成一张表现象可能原因排查方法读回全 0xFFMISO 没接好或芯片没供电万用表量 VDD示波器看 MISO 是否有翻转读回全 0x00CS 没拉低或 SCK 没输出逻辑分析仪抓 CS 和 SCK读回数据错位CPOL/CPHA 设错改成 Mode 3 试试写入无效忘了发 WREN检查写函数是否先发 0x06偶发错误去耦不足或速率太高降速到 1MHz 复测我遇到最多的是忘了 WREN。因为读操作不需要 WREN很多人调通读之后就以为写也一样结果写不进去。记住MRAM 的每一次写操作前面都必须有一次独立的 WREN。5.2 高速下的数据错误把 SPI 提到 20MHz 以上后如果出现偶发位错误先别怀疑芯片。检查这几项走线长度SCK 和 MOSI 的走线如果超过 10cm边沿会变缓建议缩短或加串联电阻匹配。地回路MRAM 的地和 MCU 的地要低阻抗连接最好在同一层铺地。CS 时序软件片选时CS 拉低到第一个 SCK 之间如果太快芯片可能还没准备好。加几个 NOP 延时试试。我实测下来在 4 层板上走线 5cm 以内20MHz 完全稳定。40MHz 需要更严格的阻抗控制一般工业板不追求这个速率。5.3 掉电后数据恢复异常如果重启后读到的写指针是乱值检查元数据区的写入逻辑。我的经验是元数据区每次更新都要做校验。比如写指针旁边放一个 CRC8恢复时先校验校验不过就用默认值重新初始化。这样即使掉电时元数据写了一半也能安全恢复。另外MRAM 虽然写入快但也不是瞬间完成。数据手册里写的写入时间是纳秒级但那是芯片内部完成的时间SPI 传输本身还是要花时间的。所以掉电检测中断里要确保最后一条记录的 SPI 传输已经完成再让系统断电。如果电源掉得太快可以在电源端加一个大电容给 MCU 留出几十毫秒的“善后时间”。6. 工业场景下的扩展用法6.1 故障快照记录工业设备出故障时最宝贵的是故障前那几秒的运行数据。用 MRAM 做环形缓冲以 1kHz 的频率记录关键变量电流、电压、温度、位置缓冲区存最近 10 秒的数据。一旦触发故障立即停止写入把这段数据锁定。事后读出来就是一份完整的故障现场快照。这个用法对 MRAM 的写入速度要求很高。1kHz 意味着每 1ms 写一次每次可能几十字节。用 DMA 的话CPU 几乎无感。我算过每次写 32 字节1ms 一次平均带宽才 32KB/s对 20MHz 的 SPI 来说绰绰有余。6.2 累计计量与参数存储电表、水表、流量计这类设备需要累计计量值。这个值要频繁更新而且掉电不能丢。传统做法是用 EEPROM但 EEPROM 写入慢只能每隔一段时间存一次掉电时会丢最后一段。用 MRAM 可以每次计量都写掉电时数据是完整的。参数存储也是类似。设备的校准参数、配置项存在 MRAM 里读写都快而且不怕频繁修改。我一般把参数区放在 MRAM 的前半部分日志区放在后半部分互不干扰。6.3 与 RTOS 的配合如果项目跑了 RTOS比如 FreeRTOSMRAM 的读写要注意临界区保护。SPI 总线是共享资源多个任务同时访问会冲突。我的做法是给 MRAM 操作加一个互斥信号量谁用谁申请。DMA 传输期间任务可以挂起等待完成信号不阻塞其他任务。另外如果用了文件系统或者日志库要确认它们的底层写入是不是线程安全的。有些库内部有缓存掉电时会丢数据。用 MRAM 的话最好绕过缓存直接写到底层。7. 一些实测数据与经验值最后分享几组我实测的数据供参考写入速度20MHz SPIDMA 模式连续写 512KB 耗时约 210ms平均带宽约 2.4MB/s。读取速度同样条件下连续读 512KB 耗时约 205ms带宽约 2.5MB/s。CPU 占用DMA 模式下传输期间 CPU 占用低于 5%轮询模式下CPU 占用接近 100%。功耗MRAM 写入时电流约 15mA读取时约 8mA待机时低于 100uA。这些数据在不同板子上会有差异但量级可以参考。如果你的应用对速度要求不高轮询模式也够用如果要高频记录DMA 是必须的。我个人在实际操作中的体会是这套组合最大的价值不是性能参数而是确定性。你知道数据写进去就一定在掉电也不丢不需要复杂的掉电保护逻辑不需要磨损均衡算法。对于工业设备来说这种简单可靠比什么都重要。踩过的坑主要集中在 WREN 遗漏和高速下的信号完整性上把这两点处理好剩下的就是常规的 SPI 调试了。
阅读完成 · 觉得有帮助?