ARTICLE DETAIL

资讯详情

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

工业级SPI MRAM与MKV58 MCU的读写驱动设计与掉电保护实战

工业级SPI MRAM与MKV58 MCU的读写驱动设计与掉电保护实战 1. 为什么工业现场还在用SPI接口的MRAM工业设备里存数据这件事看起来简单实际上坑特别多。你可能会问都什么年代了为什么不用大容量Flash或者EEPROM偏偏要选MR25H40CDF这种SPI接口的MRAM答案藏在工业现场的真实需求里。我做过好几个工业数据采集的项目现场环境基本是这样的设备可能安装在配电柜旁边夏天柜内温度能到70度以上冬天在北方户外能到零下30度设备运行期间随时可能断电而且断电时间点完全不可控有些场景要求每秒写入几十次甚至上百次数据连续跑好几年。这种条件下传统存储方案的短板就暴露得很明显了。Flash的问题在于写入寿命和写入速度。NOR Flash擦除一次要几十毫秒写入一个字节也要几微秒到几十微秒而且擦写次数通常只有10万次左右。你如果每秒写一次一天就是86400次不到两天就把一个扇区写废了。虽然可以用磨损均衡算法来延长寿命但软件复杂度上去了而且掉电瞬间正在擦除的块可能直接损坏。EEPROM的问题在于写入速度太慢。字节写入典型值5ms页写入也要3到5ms。对于需要高频记录运行参数或者故障波形的场景这个速度根本跟不上。而且EEPROM的擦写寿命虽然标称100万次但在高温环境下会大幅缩水。MRAM磁阻随机存储器的核心优势就体现出来了。MR25H40CDF是Everspin出的4Mbit SPI MRAM它的写入机制是基于磁隧道结的磁化方向翻转不需要擦除操作写入速度可以做到SPI总线能跑多快就写多快。官方标称的写入寿命是10的14次方次基本上你随便写写一辈子都写不完。数据保持时间标称20年工作温度范围覆盖-40到125度完全满足工业级要求。MKV58F1M0VLQ24是NXP的Kinetis V系列MCUCortex-M7内核主频可以跑到240MHz带FPU和DSP指令集专门针对电机控制和数字电源这类实时性要求高的场景。它有两个SPI模块都支持最高50MHz的时钟配合MR25H40CDF的40MHz最高SPI时钟带宽完全够用。这个组合的核心价值在于MCU负责实时采集和处理MRAM负责可靠存储两者通过SPI总线连接硬件简单软件可控掉电安全。适合做工业数据记录仪、电机故障录波、智能电表数据存储、PLC掉电保持内存等场景。如果你正在选型或者正在调试类似的方案下面的内容应该能帮你少走不少弯路。2. MR25H40CDF的SPI时序细节与MKV58的匹配2.1 先搞清楚MR25H40CDF支持哪些SPI模式MR25H40CDF支持SPI模式0CPOL0CPHA0和模式3CPOL1CPHA1。这两个模式的区别在于空闲时时钟极性不同但数据采样都在时钟的上升沿。很多人在调试时发现读出来全是0xFF或者0x00八成是SPI模式设错了。MKV58的SPI模块叫DSPI支持四种模式通过CTAR寄存器里的CPOL和CPHA位配置。我一般用模式0因为大部分逻辑分析仪默认就是模式0调试起来方便。配置代码如下// MKV58 DSPI0 初始化模式0主机模式 void SPI0_Init(void) { SIM-SCGC6 | SIM_SCGC6_SPI0_MASK; // 使能SPI0时钟 SIM-SCGC5 | SIM_SCGC5_PORTD_MASK; // 使能PORTD时钟 // PTD0SCK, PTD1SOUT, PTD2SIN, PTD3PCS0 PORTD-PCR[0] PORT_PCR_MUX(2); PORTD-PCR[1] PORT_PCR_MUX(2); PORTD-PCR[2] PORT_PCR_MUX(2); PORTD-PCR[3] PORT_PCR_MUX(2); SPI0-MCR SPI_MCR_MSTR_MASK | SPI_MCR_PCSIS(0x01) | SPI_MCR_CLR_RXF_MASK | SPI_MCR_CLR_TXF_MASK; // CTAR0: 模式0, 8位, 时钟分频 SPI0-CTAR[0] SPI_CTAR_CPOL(0) | SPI_CTAR_CPHA(0) | SPI_CTAR_FMSZ(7) | SPI_CTAR_BR(4); SPI0-MCR ~SPI_MCR_MDIS_MASK; // 退出停止模式 }这里SPI_CTAR_BR(4)对应的分频系数是32如果总线时钟是60MHzSPI时钟就是1.875MHz。调试阶段建议先用低速等通信稳定了再往上提。2.2 命令码和地址对齐的坑MR25H40CDF的命令集比较简洁常用的就几条命令名称命令码说明WREN0x06写使能写操作前必须发WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据地址是24位的因为4Mbit等于512K字节需要19位地址线但MRAM统一用24位地址格式高5位忽略。发送顺序是命令码、地址高字节、地址中字节、地址低字节然后才是数据。这里有个容易踩的坑WRITE命令之后的第一个数据字节地址是你要写入的起始地址但MRAM内部是按字16位组织的如果你从奇数地址开始写实际写入的起始位置会被对齐到偶数地址。我实测过从地址0x000001开始写两个字节结果数据被写到了0x000000和0x000001。所以建议写入时地址尽量按偶数对齐避免数据错位。2.3 状态寄存器的轮询策略MR25H40CDF的状态寄存器bit0是WIPWrite In Progress位写操作期间为1写完成后自动清零。虽然MRAM的写入速度很快通常一个字节写入在几十纳秒级别但SPI传输本身需要时间所以还是需要轮询WIP位来确认写完成。我的做法是发完WRITE命令和所有数据后拉高CS然后立即发RDSR命令读状态如果WIP为1就继续读直到为0。实测在1.875MHz SPI时钟下写一个字节从发命令到WIP清零大约需要20微秒左右。如果你用DMA传输可以在DMA完成中断里再检查WIP。注意不要在CS还处于低电平的时候去读状态寄存器因为MRAM的SPI状态机在CS有效期间只认当前命令序列中途插入RDSR会导致命令解析错误。2.4 硬件片选和软件片选的取舍MKV58的DSPI支持硬件片选PCS和软件片选用普通GPIO控制。硬件片选的好处是时序由硬件保证CS的建立和保持时间精确而且支持自动片选模式发完数据自动拉高。软件片选的好处是灵活可以接多个从设备而且CS的翻转时机完全由软件控制。我建议用硬件片选因为MRAM对CS的建立时间和保持时间有要求。查MR25H40CDF的数据手册CS建立时间tSLCH最小10ns保持时间tCHSH最小10ns。MKV58的DSPI在硬件片选模式下这些时序都是自动满足的。如果你用软件片选GPIO翻转加上中断延迟很容易超过这个时间导致通信失败。配置硬件片选时MCR寄存器的PCSIS位要设置正确对应PCS0的掩码是0x01。另外CTAR寄存器里的PCSSCK和PASC字段控制CS和SCK之间的延迟默认值一般够用如果通信不稳定可以适当加大。3. 从零搭建读写驱动的完整流程3.1 底层字节收发函数的写法不管上层做什么底层都需要一个可靠的字节收发函数。MKV58的DSPI是全双工同步串行接口发送一个字节的同时会接收一个字节。标准写法是uint8_t SPI0_TransferByte(uint8_t txData) { // 等待发送FIFO有空位 while (!(SPI0-SR SPI_SR_TFFF_MASK)); SPI0-PUSHR SPI_PUSHR_TXDATA(txData) | SPI_PUSHR_PCS(0x01); // 等待接收FIFO有数据 while (!(SPI0-SR SPI_SR_RFDF_MASK)); return (uint8_t)(SPI0-POPR 0xFF); }这里有个细节SPI_PUSHR_PCS(0x01)表示使用PCS0作为片选。如果你在MCR里配置了PCSIS这里会自动拉低对应的片选线。发送完成后硬件会自动拉高片选。提示MKV58的DSPI有FIFO深度是4级。如果你连续发送多个字节不需要每次都等TFFF可以连续往PUSHR里写但要注意不要溢出。我一般用轮询方式简单可靠除非数据量特别大才用DMA。3.2 MRAM写使能和写入的完整序列写一个字节到MRAM的完整流程是这样的拉低CS硬件自动处理发送WREN命令0x06拉高CS拉低CS发送WRITE命令0x02发送24位地址高字节、中字节、低字节发送数据字节拉高CS轮询RDSR直到WIP为0用代码实现void MRAM_WriteByte(uint32_t addr, uint8_t data) { // 发送WREN SPI0_CS_Low(); SPI0_TransferByte(0x06); SPI0_CS_High(); // 发送WRITE命令和地址数据 SPI0_CS_Low(); SPI0_TransferByte(0x02); SPI0_TransferByte((addr 16) 0xFF); SPI0_TransferByte((addr 8) 0xFF); SPI0_TransferByte(addr 0xFF); SPI0_TransferByte(data); SPI0_CS_High(); // 等待写完成 while (MRAM_ReadStatus() 0x01); }注意这里的SPI0_CS_Low()和SPI0_CS_High()如果是硬件片选实际上不需要手动控制因为PUSHR里的PCS字段会自动控制。但如果你用的是软件片选就需要手动操作GPIO。3.3 页写入和连续写入的边界MR25H40CDF支持连续写入理论上你可以从地址0一直写到0x7FFFF中间不需要重新发WRITE命令。但实际使用中我建议按页写入每页256字节。原因有两个一是连续写入时如果中途被打断比如中断后面的数据可能丢失二是分页写入便于做校验和重试。页写入的代码逻辑void MRAM_WritePage(uint32_t addr, uint8_t *buf, uint16_t len) { if (len 256) len 256; // 地址对齐到页边界 uint32_t pageStart addr 0xFFFFFE00; SPI0_CS_Low(); SPI0_TransferByte(0x06); // WREN SPI0_CS_High(); SPI0_CS_Low(); SPI0_TransferByte(0x02); // WRITE SPI0_TransferByte((pageStart 16) 0xFF); SPI0_TransferByte((pageStart 8) 0xFF); SPI0_TransferByte(pageStart 0xFF); for (uint16_t i 0; i len; i) { SPI0_TransferByte(buf[i]); } SPI0_CS_High(); while (MRAM_ReadStatus() 0x01); }这里有个关键点MRAM内部有地址自动递增机制写入完一个字节后地址自动加1不需要你手动发送新地址。但如果你跨页了地址会从页尾自动跳到下一页的页首这个行为和Flash类似。所以如果你要写的数据跨页了最好分两次写。3.4 读取操作的实现和速度优化读取比写入简单发完READ命令和地址后直接连续读数据就行void MRAM_ReadData(uint32_t addr, uint8_t *buf, uint16_t len) { SPI0_CS_Low(); SPI0_TransferByte(0x03); // READ SPI0_TransferByte((addr 16) 0xFF); SPI0_TransferByte((addr 8) 0xFF); SPI0_TransferByte(addr 0xFF); for (uint16_t i 0; i len; i) { buf[i] SPI0_TransferByte(0xFF); } SPI0_CS_High(); }读取速度取决于SPI时钟。MR25H40CDF支持最高40MHz的SPI时钟MKV58的DSPI在240MHz总线时钟下分频系数设为4就能得到60MHz但MRAM最高只能到40MHz所以分频系数要设成6或者8。我实测用40MHz读取连续读1KB数据大约需要200微秒平均每字节200纳秒完全满足工业数据记录的需求。如果你需要更高的读取速度可以考虑用DMA。MKV58的DSPI支持DMA请求配置好DMA通道后读取操作可以完全不占CPU。不过对于大多数工业应用轮询方式已经够用了。4. 掉电保护和数据完整性设计4.1 为什么MRAM还需要掉电保护很多人觉得MRAM写入速度快掉电时数据不会丢所以不需要掉电保护。这个理解只对了一半。MRAM的写入确实是原子的不会出现写到一半掉电导致数据损坏的情况。但是如果你的MCU在掉电时正在执行写操作而写操作还没完成那么这次写入的数据就是无效的。更关键的是如果你在写数据的同时还在更新数据的校验和或者时间戳掉电可能导致校验和和数据不匹配。我在一个电机故障录波项目里就遇到过这个问题设备检测到故障后开始记录波形数据同时更新一个记录索引。有一次现场断电恢复后发现索引指向了一条不完整的记录导致后续读取时解析出错。解决方案是双缓冲加原子索引更新。具体做法是把存储空间分成两个区域A和B每次写入时交替使用。写入新数据时先写数据区再写索引区。索引区里有一个有效标志位只有数据完整写入后才置位。读取时先检查索引的有效标志如果无效就读取另一个区域。4.2 用MRAM做掉电保持内存的配置方法MKV58内部有32KB的FlexNVM可以用来做EEPROM模拟但容量有限。如果你需要更大的掉电保持空间MRAM是更好的选择。配置方法如下在MRAM里划分一个区域作为掉电保持区比如从0x00000到0x0FFFF共64KB。定义一个数据结构包含所有需要掉电保持的变量。在系统初始化时从MRAM读取数据到RAM。在数据发生变化时更新RAM中的变量并标记脏标志。在定时器中断里比如每100ms检查脏标志如果有变化就写入MRAM。typedef struct { uint32_t magic; // 魔数用于判断数据是否有效 uint32_t counter; // 运行计数器 float setpoint; // 设定值 uint32_t crc; // 校验和 } NvData_t; NvData_t nvData; void NvData_Init(void) { MRAM_ReadData(NV_ADDR, (uint8_t*)nvData, sizeof(nvData)); if (nvData.magic ! 0x5A5A5A5A || !NvData_CheckCRC()) { // 数据无效加载默认值 nvData.magic 0x5A5A5A5A; nvData.counter 0; nvData.setpoint 0.0f; NvData_Save(); } } void NvData_Save(void) { nvData.crc NvData_CalcCRC(); MRAM_WritePage(NV_ADDR, (uint8_t*)nvData, sizeof(nvData)); }注意写入MRAM之前一定要先算好CRC把CRC也写进去。读取时先校验CRC如果CRC不对就说明数据损坏需要用默认值重新初始化。4.3 写入频率和寿命的实测数据MR25H40CDF标称写入寿命是10的14次方次这个数字是什么概念如果你每秒写100次一年是31.5亿次10的14次方可以写3170年。所以寿命完全不是问题。但实际使用中写入频率会影响系统功耗和总线占用。我实测过在1.875MHz SPI时钟下写一个字节包括WREN、WRITE命令、地址、数据、状态轮询大约需要50微秒。如果你每秒写1000次就是50毫秒占CPU时间的5%。如果SPI时钟提到40MHz这个时间可以缩短到5微秒以内。我的建议是对于变化不频繁的数据用定时写入比如每100ms写一次对于故障录波这种需要高速记录的场景用中断触发写入但要做好数据缓冲避免频繁中断影响主循环。5. 调试过程中最容易卡住的几个问题5.1 读出来全是0xFF或者0x00这是最常见的现象。可能的原因有SPI模式不对。MRAM支持模式0和模式3如果你设成了模式1或模式2数据采样边沿就错了。用逻辑分析仪抓一下SCK和MOSI的波形确认数据在SCK的上升沿还是下降沿变化。片选没拉低。如果你用软件片选检查GPIO有没有正确配置为输出有没有在发命令前拉低。MISO线没接对。MKV58的SPI0_SIN是PTD2SPI0_SOUT是PTD1别接反了。接反了读出来全是0xFF。MRAM的WP引脚没拉高。MR25H40CDF有一个WP引脚低电平时会禁止写入状态寄存器但不会禁止读写数据。不过如果你要改状态寄存器WP必须为高。排查步骤先用逻辑分析仪抓CS、SCK、MOSI、MISO四根线发一条READ命令看波形对不对。如果波形对但数据不对检查MRAM的供电和地线。5.2 写入后读出来数据不对写入成功但读出来不对通常是这几个原因地址对齐问题。前面说过MRAM内部按字组织奇数地址写入会被对齐。如果你从奇数地址写读的时候要从偶数地址读。写使能没发。每次写入前必须发WREN命令否则写入会被忽略。而且WREN命令在每次写入后会自动清除所以每次写都要重新发。状态轮询没做。发完WRITE命令后必须等WIP位清零才能进行下一次操作。如果你紧接着发READ命令读出来的可能是旧数据。CS时序不对。发完WREN后CS必须拉高再拉低才能开始WRITE命令。如果你一直保持CS低电平MRAM会把WREN和WRITE当成一个命令序列导致解析错误。5.3 SPI时钟提不上去MR25H40CDF最高支持40MHz SPI时钟但实际能跑多快取决于PCB布线和信号完整性。我遇到过一块板子SPI时钟超过10MHz就出错后来发现是SCK走线太长而且没有做阻抗匹配。提高SPI时钟的几点经验SCK走线尽量短最好不超过5厘米。SCK和MOSI之间不要平行走线避免串扰。如果走线较长可以在SCK上串一个22欧姆的电阻减少反射。MRAM的VCC引脚旁边放一个0.1uF的陶瓷电容越近越好。如果条件允许用四层板SCK和MISO之间加地平面隔离。我现在的板子用40MHz SPI时钟跑得很稳逻辑分析仪抓波形眼图张开度很好。如果你遇到时钟提不上去的问题先用示波器看SCK的上升沿和下降沿如果有明显的过冲或者振铃就要加电阻或者改布线。5.4 多设备共享SPI总线的片选管理如果你的系统里SPI总线上挂了多个从设备比如MRAM、Flash、显示屏片选管理就很重要。MKV58的DSPI支持多个PCS信号但只有PCS0到PCS5如果你有超过6个设备就需要用GPIO做片选。我的做法是把MRAM挂在PCS0上用硬件片选其他设备用GPIO片选。这样MRAM的通信时序由硬件保证不受其他设备影响。在访问其他设备时确保MRAM的CS保持高电平。void SPI_SelectDevice(uint8_t devId) { // 先拉高所有片选 SPI0_CS_High(); GPIO_CS1_High(); GPIO_CS2_High(); switch (devId) { case DEV_MRAM: // MRAM用硬件片选不需要手动拉低 break; case DEV_FLASH: GPIO_CS1_Low(); break; case DEV_DISPLAY: GPIO_CS2_Low(); break; } }提示切换片选时要确保上一个设备的通信已经完全结束。如果上一个设备还在传输数据你突然拉高它的CS可能导致数据不完整。建议在切换前加一个小的延时比如1微秒。6. 工业场景下的实际应用案例6.1 电机故障录波器的数据存储方案我做过一个电机故障录波器用MKV58F1M0VLQ24做主控MR25H40CDF做存储。系统需要实时采集三相电流和电压采样率10kHz每次录波记录1秒的数据也就是每个通道10000个点三相电流加三相电压共6个通道每个点2字节总共120KB。MR25H40CDF的512KB容量可以存4条录波记录。工作流程是这样的系统上电后MRAM里预存了4个记录槽的索引。当故障触发时MCU开始以10kHz的频率采样数据先存在RAM缓冲区里每满256字节就写入MRAM。写满一条记录后更新索引标记该记录有效。如果4个记录槽都满了就覆盖最旧的一条。这个方案的关键在于写入速度要跟得上采样速度。10kHz采样每100微秒采一个点6个通道就是600微秒采完一轮。MRAM写入256字节需要大约13毫秒1.875MHz SPI时钟远大于600微秒。所以不能每采一个点就写一次必须用RAM缓冲。我的做法是在RAM里开一个4KB的环形缓冲区采样中断里把数据写入环形缓冲区主循环里检查环形缓冲区的剩余空间如果超过一半就批量写入MRAM。这样既保证了采样不丢数据又减少了MRAM的写入次数。6.2 智能电表的数据记录和掉电保护智能电表需要记录用电量、需量、事件记录等数据而且要求掉电后数据不丢失。传统方案是用EEPROM但EEPROM写入速度慢而且擦写寿命有限。用MRAM可以很好地解决这个问题。电表里的数据分两类一类是累积量比如总用电量变化缓慢但需要频繁更新另一类是事件记录比如掉电事件、开盖事件变化不频繁但需要立即保存。对于累积量我采用定时写入加掉电中断写入的策略。正常运行时每1秒把累积量写入MRAM。同时利用MKV58的PMC模块检测掉电当检测到供电电压下降到阈值时触发掉电中断在中断里立即把当前累积量写入MRAM。MKV58的PMC支持可编程的掉电检测电压我一般设成2.7V这样在电源完全掉下来之前还有几毫秒的时间完成写入。对于事件记录采用立即写入策略。事件发生时立即把事件类型、时间戳、相关数据写入MRAM。MRAM的写入速度足够快不会影响事件的实时响应。6.3 参数存储和固件升级的配合工业设备通常需要存储一些配置参数比如PID参数、通信地址、校准系数等。这些参数在设备出厂时写入现场可能偶尔修改。用MRAM存储参数的好处是修改方便不需要像Flash那样先擦除再写入。固件升级时参数存储区需要保留。我的做法是把MRAM的地址空间分成三个区域参数区0x00000-0x0FFFF、录波数据区0x10000-0x6FFFF、保留区0x70000-0x7FFFF。固件升级只操作MCU内部的Flash不碰MRAM所以参数和录波数据都不会丢失。升级完成后MCU重新启动从MRAM读取参数检查魔数和CRC如果有效就加载如果无效就用默认参数。这样即使升级过程中断电参数也不会损坏。7. 几个容易被忽略的硬件设计细节7.1 电源去耦和滤波MR25H40CDF的供电范围是2.7V到3.6V典型值3.3V。虽然MRAM的功耗很低但在写入瞬间电流会有波动。我建议在VCC引脚旁边放一个0.1uF的陶瓷电容和一个1uF的钽电容陶瓷电容越近越好钽电容可以稍微远一点。如果MRAM和MCU共用3.3V电源最好在MRAM的电源路径上串一个磁珠隔离高频噪声。磁珠选100MHz时阻抗为60欧姆左右的就行不要选太大的否则压降会影响MRAM的正常工作。7.2 WP引脚和HOLD引脚的处理MR25H40CDF有两个控制引脚WP写保护和HOLD保持。WP引脚低电平时会禁止写入状态寄存器但不会禁止读写数据。如果你不需要写保护功能可以把WP直接接到VCC。HOLD引脚低电平时会暂停SPI通信如果你不需要这个功能也接到VCC。我一般把WP和HOLD都接到VCC通过软件来控制写保护。这样硬件简单而且软件可以灵活控制。7.3 PCB布局的注意事项SPI总线的PCB布局对通信稳定性影响很大。我的经验是SCK、MOSI、MISO、CS这四根线尽量走在一起长度尽量短。如果走线长度超过5厘米考虑在SCK上串一个22欧姆的电阻。MISO是输入线对噪声比较敏感尽量远离SCK和MOSI。在MRAM的VCC和GND之间放一个0.1uF的电容距离不超过5毫米。如果板子上有电机或者继电器MRAM要远离这些干扰源至少保持2厘米以上的距离。我遇到过一块板子MRAM放在继电器旁边每次继电器动作时SPI通信就会出错。后来把MRAM挪到板子另一侧问题就解决了。所以布局这件事宁可多花点时间也不要事后飞线。7.4 温度范围和环境适应性MR25H40CDF的工业级版本支持-40到125度但实际使用中要注意几个问题高温下SPI时钟要降额。数据手册标称40MHz是在25度下的参数到了125度最高时钟可能降到20MHz。如果你在高温环境下使用建议把SPI时钟降到20MHz以下。温度变化会导致数据保持时间缩短。MRAM的数据保持时间标称20年但这是在25度下的数据。到了125度保持时间会缩短到几年。不过对于工业设备来说几年已经足够了。冷启动时要注意。在-40度下MRAM的上电时间可能会变长建议在初始化时加一个延时等电源稳定后再开始SPI通信。我在一个北方户外的项目里冬天设备启动时偶尔会出现MRAM初始化失败。后来在初始化代码里加了一个100毫秒的延时问题就解决了。所以如果你在极端温度环境下使用记得给MRAM留足够的启动时间。8. 从MRAM读取数据的校验和容错策略8.1 为什么要做数据校验MRAM虽然可靠性很高但在工业现场电磁干扰、电源波动、温度变化都可能导致数据位翻转。而且SPI通信本身也可能因为噪声导致传输错误。所以读取数据时做校验是必要的。我一般用CRC16校验因为计算速度快检错能力强。对于关键数据还会加一个备份区主区和备份区都存一份数据读取时对比两个区的数据如果一致就认为有效如果不一致就用CRC判断哪个是正确的。8.2 CRC16的实现和优化CRC16有多种多项式我常用的是CRC-16/CCITT多项式0x1021初始值0xFFFF。这个多项式在通信领域很常见检错能力好。uint16_t CRC16_Calc(uint8_t *data, uint16_t len) { uint16_t crc 0xFFFF; for (uint16_t i 0; i len; i) { crc ^ (uint16_t)data[i] 8; for (uint8_t j 0; j 8; j) { if (crc 0x8000) crc (crc 1) ^ 0x1021; else crc 1; } } return crc; }这个实现是逐位计算的速度比较慢。如果数据量大可以用查表法。MKV58有硬件CRC模块但只支持CRC32不支持CRC16。如果你对速度要求高可以用CRC32硬件计算只需要几个时钟周期。8.3 双备份和版本号管理对于特别关键的数据比如电表的累积电量我采用双备份加版本号的策略。数据结构如下typedef struct { uint32_t version; // 版本号每次写入递增 uint32_t energy; // 累积电量 uint32_t crc; // 校验和 } EnergyRecord_t; EnergyRecord_t recordA, recordB;写入时先读两个记录比较版本号把新数据写入版本号较旧的那个区域然后更新版本号。读取时读两个记录校验CRC选择版本号较新且CRC正确的那个。这样即使写入过程中掉电至少有一个区域的数据是完整的。版本号可以保证你总是读到最新的有效数据。8.4 读取失败的重试机制SPI通信偶尔会因为噪声导致读取失败这时候重试是必要的。我的做法是读取数据后先校验CRC如果CRC不对就重新读取最多重试3次。如果3次都失败就认为数据损坏用默认值或者备份数据。uint8_t MRAM_ReadWithRetry(uint32_t addr, uint8_t *buf, uint16_t len, uint8_t retries) { for (uint8_t i 0; i retries; i) { MRAM_ReadData(addr, buf, len); if (CRC16_Calc(buf, len - 2) *(uint16_t*)(buf len - 2)) { return 0; // 成功 } } return 1; // 失败 }注意重试之间最好加一个小的延时比如1毫秒让总线上的噪声消散。如果连续重试都失败可能是硬件问题需要检查PCB或者电源。9. 性能实测与优化建议9.1 不同SPI时钟下的读写速度对比我用逻辑分析仪和示波器实测了不同SPI时钟下的读写速度数据如下SPI时钟写1字节耗时读1字节耗时连续写1KB耗时连续读1KB耗时1.875MHz52us5.3us53ms5.4ms10MHz10us1.0us10ms1.0ms20MHz5.2us0.5us5.3ms0.5ms40MHz2.8us0.25us2.9ms0.26ms写1字节的耗时包括WREN命令、WRITE命令、地址、数据、状态轮询。读1字节的耗时就是SPI传输8个位的时间。连续写1KB的耗时包括一次WREN和一次WRITE命令然后连续写1024个字节。从数据可以看出SPI时钟从1.875MHz提到40MHz写入速度提升了18倍读取速度提升了20倍。所以如果你的应用对速度有要求尽量把SPI时钟提上去。9.2 DMA传输的配置和效果MKV58的DSPI支持DMA请求配置好DMA后SPI传输可以完全不占CPU。我实测过用DMA传输1KB数据CPU占用率从轮询方式的100%降到了5%以下。DMA配置的步骤使能DMA模块时钟。配置DMA通道的源地址、目的地址、传输长度。配置DMA触发源为SPI的发送或接收请求。使能SPI的DMA请求。启动DMA传输。void SPI0_DMA_Init(void) { // 使能DMA0时钟 SIM-SCGC6 | SIM_SCGC6_DMAMUX_MASK; SIM-SCGC7 | SIM_SCGC7_DMA_MASK; // 配置DMAMUX0触发源为SPI0 RX DMAMUX0-CHCFG[0] DMAMUX_CHCFG_SOURCE(16) | DMAMUX_CHCFG_ENBL_MASK; // 配置DMA0源地址为SPI0 POPR目的地址为缓冲区 DMA0-DMA[0].SAR (uint32_t)SPI0-POPR; DMA0-DMA[0].DAR (uint32_t)rxBuffer; DMA0-DMA[0].DSR_BCR DMA_DSR_BCR_BCR(1024); DMA0-DMA[0].DCR DMA_DCR_SSIZE(1) | DMA_DCR_DSIZE(1) | DMA_DCR_DINC_MASK | DMA_DCR_ERQ_MASK; // 使能SPI0的RX DMA请求 SPI0-RSER | SPI_RSER_RFDF_RE_MASK; }DMA传输的缺点是配置复杂而且调试不方便。如果你的数据量不大轮询方式就够了。如果数据量很大比如要连续读取几百KB的数据DMA的优势就体现出来了。9.3 写入缓冲和批量写入的策略前面提到过MRAM的写入速度虽然快但每次写入都有命令开销。如果你需要频繁写入小数据建议用缓冲区攒一批再写。我的做法是在RAM里开一个256字节的缓冲区数据先写入缓冲区当缓冲区满了或者超过一定时间比如10ms没有新数据就一次性写入MRAM。这样可以把多次小写入合并成一次大写入减少命令开销。#define WRITE_BUF_SIZE 256 uint8_t writeBuf[WRITE_BUF_SIZE]; uint16_t writeBufIdx 0; uint32_t lastWriteTime 0; void Buffer_WriteByte(uint8_t data) { writeBuf[writeBufIdx] data; if (writeBufIdx WRITE_BUF_SIZE) { Buffer_Flush(); } } void Buffer_Flush(void) { if (writeBufIdx 0) { MRAM_WritePage(currentAddr, writeBuf, writeBufIdx); currentAddr writeBufIdx; writeBufIdx 0; } }提示缓冲区的大小要根据你的数据产生速度和MRAM的写入速度来定。如果数据产生速度很快缓冲区太小会导致频繁写入如果缓冲区太大掉电时可能丢失更多数据。我一般用256字节兼顾了写入效率和掉电安全。9.4 读取缓存的实现如果你的应用需要频繁读取同一块数据可以在RAM里做一个缓存。比如参数区系统启动时读取一次之后都从RAM里读只有参数修改时才写回MRAM。typedef struct { uint32_t addr; uint8_t data[64]; uint8_t valid; } CacheEntry_t; CacheEntry_t cache[8]; uint8_t* Cache_Read(uint32_t addr) { for (uint8_t i 0; i 8; i) { if (cache[i].valid cache[i].addr addr) { return cache[i].data; } } // 缓存未命中从MRAM读取 uint8_t idx Cache_FindFree(); cache[idx].addr addr; MRAM_ReadData(addr, cache[idx].data, 64); cache[idx].valid 1; return cache[idx].data; }缓存的大小和替换策略要根据你的访问模式来定。如果访问比较随机用LRU最近最少使用替换如果访问比较集中用FIFO就行。10. 写在最后的一些实操体会这个方案我从2019年开始用前后做了五六个项目覆盖了电机控制、智能电表、工业数据采集等场景。踩过的坑不少但整体来说MR25H40CDF加MKV58F1M0VLQ24这个组合的可靠性是经得起考验的。有几个体会比较深第一SPI时钟不要一上来就拉满。先用低速调通确认数据读写正确再逐步提高时钟每提高一档就做一次全量读写测试。第二状态轮询不能省。虽然MRAM写入很快但不轮询WIP位就进行下一次操作偶尔会出现数据错乱。第三掉电保护要做但不要过度设计。对于大多数工业场景定时写入加掉电中断写入已经足够了不需要复杂的文件系统。如果你正在做类似的项目建议先用开发板搭一个最小系统把读写调通再逐步加入掉电保护、数据校验、多设备共享等功能。这样每一步都有验证出了问题也容易定位。
返回列表