ARTICLE DETAIL

资讯详情

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

工业级MRAM与MCU SPI接口设计:从驱动到掉电保护

工业级MRAM与MCU SPI接口设计:从驱动到掉电保护 1. 项目缘起与方案选型1.1 为什么要在工业场景里折腾 MRAM 和 MCU 的搭配工业现场的数据存储有个很拧巴的地方既要掉电不丢又要写得快还得扛得住高低温、震动和电磁干扰。传统方案里EEPROM 擦写寿命十万次量级写入速度慢到毫秒级NOR Flash 擦除要按扇区来写之前还得先擦掉电瞬间容易把整个扇区搞成半吊子状态。铁电存储器 FRAM 倒是快但容量小、价格高而且某些型号对供电时序挑剔得很。MR25H40CDF 这颗 MRAM 就是冲着这个痛点来的。它是 4Mbit 容量的磁阻随机存储器SPI 接口40MHz 时钟最关键的是——写入不需要擦除字节级随机写写一次的时间跟读一次差不多而且寿命标称 10^14 次以上。这意味着你可以把它当成一个“掉电不丢的 RAM”来用不用再费劲去搞磨损均衡、日志文件系统那一套。MKV46F256VLH16 则是 NXP 的 Kinetis V 系列 MCUCortex-M4 内核256KB Flash带 FPU 和 DSP 指令主频 100MHz 左右封装是 LQFP-64。它本身有 FlexSPI 或者普通 SPI 外设用来挂 MR25H40CDF 正合适。选这对组合的核心逻辑是MCU 负责采集和控制MRAM 负责高频写入的关键数据缓存和掉电保护两者通过 SPI 总线连接硬件简单软件可控。注意MR25H40CDF 的供电范围是 2.7V 到 3.6VMKV46F256VLH16 的 IO 电压通常也是 3.3V电平匹配没问题。但如果你的系统里有 5V 器件千万别直接把 5V 的 SPI 信号怼到 MRAM 上必须加电平转换。1.2 SPI 接口的硬件设计要点SPI 四根线SCLK、MOSI、MISO、CS。MR25H40CDF 支持 SPI 模式 0 和模式 3也就是 CPOL0/CPHA0 或者 CPOL1/CPHA1。MKV46F256VLH16 的 SPI 模块可以配置成任意模式我一般习惯用模式 0因为示波器抓波形的时候上升沿采样看起来比较顺眼。硬件片选和软件片选的问题这里要拎清楚。MR25H40CDF 只有一个 CS 引脚如果你在 SPI 总线上还挂了别的从设备比如 Flash 或者显示屏那就必须用 GPIO 来做片选每个从设备一根独立的 CS 线。MKV46F256VLH16 的硬件 SPI 片选引脚数量有限通常不够用所以实际项目里我都是把 SPI 配成“主机模式、软件片选”用 GPIO 手动拉低拉高。PCB 布线的时候SCLK 这根线要特别注意。40MHz 的时钟在普通 FR4 板上已经算高速信号了走线尽量短最好包地不要跟模拟信号或者大电流的功率线平行走。MISO 是 MRAM 输出到 MCU 的如果走线太长可以串一个 22Ω 到 33Ω 的电阻做阻抗匹配防止过冲。CS 线虽然频率不高但它是控制信号拉低到数据有效之间的建立时间要满足 MRAM 的时序要求一般几十纳秒就够了不用太担心。1.3 为什么不用文件系统很多嵌入式项目一提到存储就想到 FATFS 或者 LittleFS但在 MRAM 这个场景下文件系统反而是累赘。MRAM 的写入粒度是字节没有擦除概念你直接按地址写就行。文件系统带来的元数据开销、目录管理、磨损均衡逻辑在这里全是多余的。我的做法是把 MRAM 的 512KB 空间直接映射成一块线性缓冲区前 4KB 放一个简单的索引表记录每个数据块的有效标志、写入序号和 CRC 校验值后面按固定大小的块循环写入。这样掉电后重新上电扫一遍索引表就能知道哪些数据是完整的哪些是写了一半的垃圾。2. MR25H40CDF 的底层驱动与读写机制2.1 命令集与操作码解析MR25H40CDF 的命令集很精简常用的就几条命令名称操作码功能说明WREN0x06写使能写之前必须发WRDI0x04写禁止写完可以发也可以不发RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据后面跟 3 字节地址WRITE0x02写数据后面跟 3 字节地址状态寄存器的 bit0 是 WEL写使能锁存bit1 是 BP0bit2 是 BP1这些是块保护位。默认情况下块保护是关的整个 512KB 都能写。如果你不小心把 BP 位设错了写操作会被静默忽略读回来的数据还是旧的这个坑我踩过——调试了半天以为 SPI 时序有问题结果是状态寄存器被误改了。提示每次写操作之前都要发 WRENMRAM 不会自动保持写使能。写完一个字节或者一页之后WEL 位会自动清零。所以连续写多个不连续地址的时候每次都要重新发 WREN。2.2 单字节写与页写的时间账MR25H40CDF 的写操作有两种模式单字节写和页写。页写一次最多可以写 256 字节地址的低 8 位会自动回绕高位的地址不变。也就是说如果你从地址 0x0000FF 开始写 256 字节写到 0x000100 的时候不会进位到下一页而是回到 0x000000。这个行为跟 EEPROM 的页写一模一样用的时候要算好地址。时间上单字节写的典型时间是 40 微秒左右页写 256 字节也是 40 微秒左右。所以如果你要写 1KB 数据用单字节写需要 40 次操作大概 1.6 毫秒用页写只需要 4 次大概 160 微秒。差了十倍。在工业数据采集场景里如果采样率是 1kHz每毫秒都要存一次数据那必须用页写否则 SPI 总线会被写操作占满。但页写有个限制你不能跨页写。比如你要写 300 字节从地址 0x00F0 开始那前 16 字节写到 0x00FF 就结束了剩下的 284 字节得从 0x0100 重新开始。所以驱动里要做一个地址对齐的拆分逻辑把长数据拆成多个页写操作。2.3 读操作的时序与速度优化读操作就简单了发 0x03 加 3 字节地址然后 SCLK 继续给时钟MISO 上就会依次输出数据。MR25H40CDF 支持最高 40MHz 的时钟理论上 512KB 全部读一遍需要 512K × 8 / 40M ≈ 102 毫秒。实际用的时候不会全读都是按需读。如果你用 MKV46F256VLH16 的硬件 SPI配合 DMA可以做到读操作几乎不占 CPU。配置 DMA 把 SPI 的接收寄存器搬到内存缓冲区长度设成你要读的字节数启动之后 CPU 就可以去干别的。等 DMA 传输完成中断来了数据已经在缓冲区里了。这个技巧在需要快速读取大量历史数据的场景下特别有用比如设备上电后要把 MRAM 里的配置参数全部加载到 RAM 里。2.4 状态轮询与写完成判断MRAM 跟 Flash 不一样它没有“忙”状态。你发完写命令数据就进去了不需要轮询状态寄存器等写完成。但是SPI 总线上你发完最后一个字节CS 拉高之后MRAM 内部还需要一点时间来完成实际的存储单元翻转。这个时间在数据手册里叫“写周期时间”典型值 40 微秒最大值可能到 100 微秒。如果你在 CS 拉高之后立刻又拉低发下一条命令而且这条命令是读同一个地址那读回来的可能是旧数据。所以稳妥的做法是每次写操作之后要么延时 100 微秒要么发一个 RDSR 命令读状态寄存器如果读回来的数据不是 0xFFMISO 空闲时为高说明 MRAM 已经准备好接受下一条命令了。我一般用延时因为简单100 微秒对大多数应用来说不算什么。3. MKV46F256VLH16 的 SPI 主机配置与代码实现3.1 时钟与引脚初始化MKV46F256VLH16 的 SPI 模块叫 DSPIDouble SPI跟普通 SPI 比多了个 FIFO可以缓冲 4 个帧。配置步骤大概是这样的使能 SPI 模块的时钟门控在 SIM_SCGC6 寄存器里把 SPI0 的时钟打开。配置引脚复用把 PTC5、PTC6、PTC7 分别设成 SPI0_SCK、SPI0_MOSI、SPI0_MISOPTC4 设成 GPIO 做片选。设置 SPI 的时钟分频。总线时钟假设是 50MHz要得到 10MHz 的 SCLK分频系数就是 5。DSPI 的分频寄存器里PRESCALE 和 SCALE 两个字段组合起来决定最终频率。配置 CTAR 寄存器帧大小 8 位CPOL0CPHA0MSB 先出波特率分频系数设好。使能 SPI 主机模式MCR 寄存器里的 MSTR 位置 1。代码大概长这样void SPI0_Init(void) { SIM_SCGC6 | SIM_SCGC6_SPI0_MASK; SIM_SCGC5 | SIM_SCGC5_PORTC_MASK; PORTC_PCR5 PORT_PCR_MUX(2); // SCK PORTC_PCR6 PORT_PCR_MUX(2); // MOSI PORTC_PCR7 PORT_PCR_MUX(2); // MISO PORTC_PCR4 PORT_PCR_MUX(1); // CS as GPIO GPIOC_PDDR | (1 4); GPIOC_PSOR (1 4); // CS high SPI0_MCR SPI_MCR_MSTR_MASK | SPI_MCR_PCSIS(0x3F); SPI0_CTAR0 SPI_CTAR_FMSZ(7) | SPI_CTAR_CPOL(0) | SPI_CTAR_CPHA(0) | SPI_CTAR_PBR(0) | SPI_CTAR_BR(4); }BR(4) 对应的分频系数是 32如果总线时钟 50MHzSCLK 就是 1.56MHz。要跑到 10MHzBR 设成 1 或者 2具体看参考手册里的分频表。3.2 字节收发函数DSPI 的收发是分开的发一个字节的同时会收到一个字节。所以最简单的收发函数就是往 PUSHR 寄存器写数据然后等 TCF 标志位置位再从 POPR 读数据。uint8_t SPI0_Transfer(uint8_t data) { while (!(SPI0_SR SPI_SR_TFFF_MASK)); SPI0_PUSHR SPI_PUSHR_TXDATA(data) | SPI_PUSHR_PCS(0); while (!(SPI0_SR SPI_SR_TCF_MASK)); SPI0_SR | SPI_SR_TCF_MASK; return SPI0_POPR 0xFF; }注意 PCS 字段如果你用硬件片选这里要填对应的片选号。我用软件片选所以 PCS 填 0实际片选靠 GPIO 控制。3.3 MRAM 读写封装基于上面的字节收发封装 MRAM 的读写void MRAM_WriteEnable(void) { CS_LOW(); SPI0_Transfer(0x06); CS_HIGH(); } void MRAM_Write(uint32_t addr, uint8_t *buf, uint32_t len) { while (len 0) { uint32_t page_remain 256 - (addr 0xFF); uint32_t write_len (len page_remain) ? len : page_remain; MRAM_WriteEnable(); CS_LOW(); SPI0_Transfer(0x02); SPI0_Transfer((addr 16) 0xFF); SPI0_Transfer((addr 8) 0xFF); SPI0_Transfer(addr 0xFF); for (uint32_t i 0; i write_len; i) { SPI0_Transfer(buf[i]); } CS_HIGH(); delay_us(100); addr write_len; buf write_len; len - write_len; } }这个函数自动处理了跨页的问题每次写到页边界就重新发 WREN 和 WRITE 命令。delay_us(100) 是等 MRAM 内部写完成。读函数更简单void MRAM_Read(uint32_t addr, uint8_t *buf, uint32_t len) { CS_LOW(); SPI0_Transfer(0x03); SPI0_Transfer((addr 16) 0xFF); SPI0_Transfer((addr 8) 0xFF); SPI0_Transfer(addr 0xFF); for (uint32_t i 0; i len; i) { buf[i] SPI0_Transfer(0x00); } CS_HIGH(); }3.4 用 DMA 加速大数据块读写当你要读 64KB 以上的数据时用上面的循环方式 CPU 占用率会很高。MKV46F256VLH16 的 DSPI 支持 DMA 请求配置好 DMA 通道之后SPI 每发一个字节DMA 自动把下一个字节填进去接收也是自动搬走。配置 DMA 的关键点源地址是内存缓冲区目的地址是 SPI0_PUSHR传输长度设成数据长度触发源选 SPI0 的 TX FIFO 请求。接收方向反过来源地址是 SPI0_POPR目的地址是内存缓冲区触发源选 RX FIFO 请求。两个通道要同时启动否则会丢数据。我实测下来用 DMA 读 128KB 数据CPU 占用率从 80% 降到 5% 以下效果非常明显。但 DMA 配置的坑也不少比如传输长度寄存器是 32 位的但实际最大传输次数受限于 DMA 的循环计数超过 65535 次要拆成多轮。4. 数据存储结构与掉电保护策略4.1 索引表设计MRAM 的前 4KB 我用来放索引表。每个表项 16 字节结构如下偏移长度字段名说明0x004Magic固定值 0x4D52414D表示表项有效0x044SeqNum写入序号单调递增0x084DataAddr数据在 MRAM 中的起始地址0x0C2DataLen数据长度0x0E2CRC16前面 14 字节的校验4KB 除以 16 等于 256 个表项。每次写入新数据就找一个 Magic 无效的表项填进去或者覆盖 SeqNum 最小的表项。上电初始化的时候扫一遍索引表把所有 Magic 有效且 CRC 正确的表项读出来按 SeqNum 排序就知道最近写入了哪些数据。4.2 双缓冲与原子写工业场景最怕的是写一半掉电。MRAM 虽然写得快但如果你写 256 字节的过程中掉电那这 256 字节里可能前 100 字节是新的后 156 字节是旧的。为了避免这种半吊子状态我用双缓冲数据区分成 A 区和 B 区交替写入。索引表里记录当前有效的是 A 还是 B。写新数据的时候先写 B 区写完更新索引表指向 B然后再写 A 区写完更新索引表指向 A。这样任何时候掉电至少有一个区的数据是完整的。索引表的更新本身也要原子。一个表项 16 字节MRAM 写 16 字节是原子的吗不是SPI 传输过程中掉电可能只写了前 8 字节。所以索引表也要双份主表和备表写的时候先写备表再写主表。上电时如果主表 CRC 错就用备表恢复。4.3 CRC 校验与数据恢复CRC16 我用的是 CCITT 多项式 0x1021初始值 0xFFFF。这个在嵌入式里很常见查表法或者直接计算都行。查表法快但占 512 字节的 Flash 空间直接计算慢但省空间。MKV46F256VLH16 有 256KB Flash不差这点空间所以我用查表法。数据恢复的逻辑是这样的上电后先读主索引表逐项检查 Magic 和 CRC。如果主表整体 CRC 错切换到备表。然后根据索引表里的 DataAddr 和 DataLen把数据读出来再算一遍 CRC跟索引表里的 CRC16 比对。如果对不上说明数据区被破坏了这条记录标记为无效继续找下一条。实操心得索引表的 SeqNum 我建议用 32 位不要用 16 位。因为如果设备频繁写入16 位很快回绕回绕之后排序就乱了。32 位按每秒写 1000 次算也要 49 天才回绕实际工业场景够用了。5. 常见问题与排查实录5.1 SPI 通信失败排查表现象可能原因排查方法读回全是 0xFFMISO 没接好或 CS 没拉低示波器看 CS 和 MISO 波形读回全是 0x00MOSI 和 MISO 接反交换两根线试试写进去读出来不对写使能没发或状态寄存器被保护读状态寄存器看 WEL 和 BP 位高速时钟下数据错走线太长或没阻抗匹配降速到 1MHz 测试逐步提高偶尔丢数据写之后延时不够增加延时到 200 微秒5.2 片选信号的坑我遇到过一种情况SPI 总线上挂了 MRAM 和一个 SPI 显示屏两个设备的 CS 分别用 GPIO 控制。调试的时候发现读 MRAM 偶尔会读到显示屏的数据。后来用示波器抓波形发现是 GPIO 操作顺序的问题——我先拉低了 MRAM 的 CS然后才去配置 SPI 的数据寄存器这中间有几十纳秒的窗口显示屏的 CS 还是低电平SPI 时钟一出来两个设备同时响应MISO 上就打架了。解决办法很简单先拉高所有从设备的 CS然后配置 SPI 数据最后拉低目标设备的 CS。顺序不能乱。5.3 电源纹波导致的写失败MRAM 的写操作对电源比较敏感。如果 3.3V 电源上有大的纹波比如电机启动或者继电器吸合的时候写操作可能会失败。我实测过电源纹波超过 100mV 峰峰值的时候写失败率明显上升。对策是在 MRAM 的 VCC 引脚旁边放一个 100nF 加一个 10uF 的电容越近越好。如果条件允许再串一个磁珠做高频隔离。软件上每次写完之后读回来校验一遍如果不对就重写最多重试三次。5.4 温度对时序的影响工业级温度范围是 -40°C 到 85°C。MR25H40CDF 标称是工业级但 SPI 时序参数在高温下会变差。比如 tCLH时钟高电平时间在 85°C 时可能比 25°C 时短几个纳秒。如果你在常温下调通了 40MHz到了高温环境可能就不稳了。稳妥的做法是留余量常温下跑 30MHz高温下也够用。或者用示波器在温箱里实测波形确认时序满足数据手册的要求。5.5 初始化时 MRAM 无响应有时候上电后 MRAM 完全不响应读回来全是 0xFF。这种情况我遇到过一次原因是 MRAM 的电源上升时间太慢MCU 已经开始发 SPI 命令了MRAM 还没完成上电复位。MR25H40CDF 的上电复位时间典型值是 1 毫秒如果你的电源上升时间超过 10 毫秒就要在初始化代码里加延时等电源稳定后再操作 SPI。6. 性能实测与优化建议6.1 读写速度实测数据我用 MKV46F256VLH16 在 10MHz SCLK 下实测了一组数据操作类型数据量耗时平均速率单字节写1 字节140 微秒7.1 KB/s页写256 字节180 微秒1.42 MB/s连续读1KB820 微秒1.25 MB/sDMA 读64KB52 毫秒1.26 MB/s单字节写之所以慢是因为每次都要发 WREN 命令加 100 微秒延时。页写把 256 字节打包成一次操作效率高很多。读操作受限于 SPI 时钟10MHz 下理论极限是 1.25 MB/s实测基本跑满。6.2 提高写入吞吐量的技巧如果你的应用需要高频写入比如每毫秒写一次 64 字节的数据那单字节写肯定来不及。我的做法是在 RAM 里开一个 4KB 的环形缓冲区采集任务只管往缓冲区里扔数据另一个低优先级的任务负责把缓冲区里的数据攒够 256 字节再一次性页写到 MRAM。这样 SPI 总线的利用率高CPU 也不会被写操作卡住。缓冲区的大小要算好假设写入速率是 64 字节/毫秒MRAM 页写 256 字节需要 180 微秒加上索引表更新大概 200 微秒。每 4 毫秒攒够 256 字节写一次花 200 微秒占用率 5%完全没问题。缓冲区 4KB 可以撑 64 毫秒足够应对突发延迟。6.3 降低功耗的配置如果设备是电池供电的MRAM 的待机电流只有几十微安但 SPI 总线上如果一直有时钟信号功耗会上去。我的做法是每次读写完成后把 SPI 模块的使能位关掉SCLK 引脚配置成 GPIO 输出低电平。MRAM 的 CS 保持高电平它自己会进入低功耗待机模式。MKV46F256VLH16 本身也有低功耗模式比如 VLPRVery Low Power Run和 STOP 模式。在 STOP 模式下SPI 模块的时钟被关掉但 GPIO 状态保持。唤醒之后重新初始化 SPI 就能继续用。实测下来STOP 模式下整机功耗可以降到 200 微安以下。6.4 长期可靠性验证工业设备要求 MTBF平均无故障时间至少几万小时。MRAM 的写入寿命是 10^14 次按每秒写 1000 次算可以写 3000 多年完全不用担心寿命问题。但 SPI 接口的可靠性需要验证比如反复插拔、震动、温度循环。我做过一个简单的加速测试让设备连续写入 72 小时每秒写 100 次总共写了 2592 万次。期间每写 1000 次读回来校验一次没有出现数据错误。温度方面在 -40°C 和 85°C 各放了 24 小时读写正常。这个测试虽然不算严格但至少说明这套方案在工业环境下是靠谱的。最后分享一个小技巧如果你在调试的时候发现 MRAM 读写不稳定先别急着改代码拿示波器看一下 SCLK 和 MOSI 的波形。很多时候问题出在信号完整性上比如过冲、振铃、上升沿太缓。在 SCLK 上串一个 33Ω 电阻往往能解决大部分玄学问题。
返回列表