ARTICLE DETAIL

资讯详情

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

MRAM与TM4C129嵌入式组合:工业级数据存储掉电保全方案

MRAM与TM4C129嵌入式组合:工业级数据存储掉电保全方案 废话不多说直接上正题。搞工业嵌入式这些年数据存储一直是个绕不开的坎儿。以前用EEPROM容量小还得慢慢磨用带电池的SRAM维护麻烦还不环保用Flash擦写寿命和掉电丢失问题让人头疼。后来把目光转向了MRAM再配上TM4C129E系列的MCU整套方案可以说既省心又抗造。这篇文章就围绕MR25H40CDF这颗SPI接口的MRAM和TM4C129ENCZAD主控聊聊怎么把数据安全、高效地存进去再读出来以及在工业现场会踩到哪些坑。1. 项目核心思路为什么这套组合适合工业场景1.1 MRAM解决了传统存储的什么痛点MR25H40CDF是Everspin公司的4Mbit串行MRAM换算下来就是512K字节走的是标准SPI接口。它的核心存储单元是磁隧道结所以天然具备非易失特性。和Flash比MRAM最大的杀手锏是不需要页擦除、可以按字节或按任意长度直接写入而且写入次数达到10的15次方量级说白了就是近乎无限次写入根本不用担心扇区被磨穿。在工业现场比如电网监测设备、伺服驱动器、协议转换网关这些设备经常需要记录故障时刻的波形快照、累计运行时间、生产计数、校准参数而且往往是机器突然断电或者现场环境剧烈变化时数据最重要。用Flash存日志有个致命问题写入次数受限制日志频繁刷写坏块管理特别痛苦。MRAM没有这个包袱写错了直接改不需要先擦除整块时序上省了erase那一步系统复杂度也降下来了。另外MRAM的读写在电气特性上和SRAM很像速度快、随机访问能力强。MR25H40CDF在数据手册上给出的SPI时钟可以跑到40MHz读、写也有相当高的速率具体看手册版本我实测在TM4C129上跑到20MHz非常稳定。整个写入过程无需等待内部擦写完成不同批次需要确认WIP位但通常微秒级就结束这一点和NOR Flash幾毫秒到几十毫秒的写周期完全是两个概念。1.2 TM4C129ENCZAD适合干什么活TM4C129ENCZAD属于TI Tiva-C系列Cortex-M4F内核主频120MHz带浮点单元。这颗芯片最亮眼的地方是接口资源特别多双CAN、双USB、以太网MACPHY集成、QEI、大量UART和I2C还有4个SSI模块就是SPI。这决定了它在工业网关、数据采集器、人机界面、电机控制这些复杂应用里非常吃香。在存储这个场景里TM4C129ENCZAD提供了几个关键能力。一是片内SRAM高达256K可以在掉电瞬间把关键数据临时放到RAM里再快速写入外部MRAM。二是有DMA控制器可以把SPI接收和发送做成不占CPU的数据流对吞吐量要求高的场合非常有用。三是它的SSI模块支持DMA触发直接省掉了逐字节操作寄存器的繁琐过程。TM4C129E系列还内置了独立的掉电检测比较器BOR配合软件可以感知电源跌落在系统彻底断电前把最关键的上下文写进MRAM。工业上讲究“最后一刻的优雅”有这层硬件支持掉电保存的设计空间就大了很多。1.3 这套组合解决的实际问题把这颗MRAM和TM4C129ENCZAD放一起最直观的好处是掉电以后数据不丢不用电池。频繁写入不磨损日志和计数器随便刷。读写速度快SPI通信加内部存储延迟都很低满足实时性要求。工作温度范围宽工业级版本可以覆盖-40到85度适合柜内无空调的恶劣环境。SPI接口简单4根信号线就搞定PCB布局也方便。2. 硬件连接与选型细节2.1 SSI引脚规划TM4C129ENCZAD有四个SSI模块我习惯用SSI0默认映射到PA2到PA5信号说明引脚MRAM引脚SCKSPI时钟PA2CLKFSS片选PA3CS#MOSI主出从入PA4SIMISO主入从出PA5SO这个映射是芯片默认的复用功能配置起来最简单。需要注意的是TM4C129的GPIO复用功能非常灵活如果你PA2被其他功能占了也可以用SSI2、SSI3的映射具体查芯片的pin mux表。但改引脚不只是改宏定义还要改GPIOPinConfigure的参数很容易记错建议设计PCB之前就把这些吃透。片选信号非常关键。在SPI模式0CPOL0/CPHA0下MRAM认为FSS低电平有效整个指令周期内FSS必须始终保持低。虽然SSI硬件会自动拉低片选但要注意指令和指令之间的间隔不能像有些Flash驱动那样连续发多个字节不清CS否则MRAM会把后面的数据误解为新的指令。对MRAM而言一条完整的指令就是“片选拉低 发送命令码 发送地址或带数据 片选拉高”。2.2 电源去耦与电平匹配MR25H40CDF的供电范围是2.7V到3.6VTM4C129ENCZAD虽然内核电压可以跑3.3V但I/O也是3.3V所以电平是兼容的不需要电平转换芯片这个省了很多事。不过电源品质要注意MRAM在写入瞬间电流会有小幅脉冲我给每颗MRAM的VDD引脚放了一个104100nF和一个10uF电容并联避免SPI高速翻转时电源跌落。如果是做多板系统或者主控板在另一个板上要特别检查地电位差。SPI信号线上最好串联33欧姆电阻靠近主控端放这样既能抑制反射还能在插拔调试时稍微保护一下主控的GPIO。MRAM的输入引脚SI、CLK、CS#和WP#都在内部有保护二极管但完全指望它扛工业现场的静电是不现实的产品化时建议在连接器侧加TVS管。MR25H40CDF有一个WP#引脚写保护和一个HOLD#引脚。这两个脚都不能悬空。WP#接高电平时只有状态寄存器里的WPEN位被置1且通过WRSR命令锁存软件写保护才生效否则WP#不阻止普通写操作。工业上如果不希望跑飞程序把MRAM内容抹掉通常会在初始化时用WRSR把WPEN配起来硬件上再把WP#拉低这样固件根本没有办法向MRAM写数据除非重新上电。HOLD#则是在SPI通信中途暂停用的平时必须接高电平我直接在板上拉10K到3.3V。2.3 引脚布局经验画PCB时把MR25H40CDF放在靠近主控SSI0引脚的一侧走线尽量等长别把SPI时钟线拖到20厘米开外。虽然SPI本身速率不至于像高速DDR那样敏感但工业现场存在电机、变频器、继电器触点的强干扰源长线SPI容易误码。实测在干扰源附近超过10cm的CLK走线偶尔会出现拉高的毛刺导致读回数据错位加了串联电阻并缩短走线后问题消失。3. 软件驱动实现与关键代码3.1 SSI初始化TM4C129的SSI驱动无非是几条标准调用。我用TivaWare库的习惯如下#include stdint.h #include stdbool.h #include inc/hw_memmap.h #include inc/hw_ssi.h #include driverlib/sysctl.h #include driverlib/gpio.h #include driverlib/ssi.h #include driverlib/pin_map.h #define MRAM_CS_PORT_PERIPH SYSCTL_PERIPH_GPIOA #define MRAM_CS_GPIO_BASE GPIO_PORTA_BASE #define MRAM_CS_PIN GPIO_PIN_3 void SSI0_Init(void) { // 使能GPIOA和SSI0外设 SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOA); SysCtlPeripheralEnable(SYSCTL_PERIPH_SSI0); while(!SysCtlPeripheralReady(SYSCTL_PERIPH_SSI0)); // 配置SSI0引脚复用 GPIOPinConfigure(GPIO_PA2_SSI0CLK); GPIOPinConfigure(GPIO_PA3_SSI0FSS); GPIOPinConfigure(GPIO_PA4_SSI0XDAT0); GPIOPinConfigure(GPIO_PA5_SSI0XDAT1); GPIOPinTypeSSI(GPIO_PORTA_BASE, GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5); // 配置为主模式模式01MHz初始化后续可提速 SSIConfigSetExpClk(SSI0_BASE, SysCtlClockGet(), SSI_CLOCK_FREQ_1MHZ, SSI_MODE_MASTER, SSI_CLOCK_POLARITY_LOW, SSI_CLOCK_PHASE_LOW); SSIEnable(SSI0_BASE); }这段代码的坑点在于SSI外设时钟的相位配置极度容易搞反。MR25H40CDF支持SPI模式0CPOL0/CPHA0和模式3CPOL1/CPHA1但是TivaWare里的POLARITY/LOW和PHASE/LOW组合对应的恰恰是模式0。如果配成mode 1POLARITY_LOW PHASE_HIGHMRAM的读时序会直接错位输出来全是移位过的数据我一开始就吃过这个亏连续读回0xFFFF排查了很久才发现是相位错了。另外SSIConfigSetExpClk这个函数传入的时钟频率是模块的时钟也就是系统时钟的分频结果。SysCtlClockGet()在跑120MHz主频时会返回120000000你不需要自己再除以分频数库内部会处理。3.2 MRAM读命令封装MR25H40CDF标准命令包括命令操作码说明WREN0x06写使能写状态寄存器前必须执行WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据任意地址任意长度WRITE0x02写数据最多一页页大小256字节RDID0x9F读工厂烧录的JEDEC ID读操作最简单。只要把CS拉低发送0x03再发三个字节的地址高字节在前低字节在后然后开始收数据。地址是24位因为4Mbit 512K字节 2的19次方字节但MRAM指令格式统一用3字节地址高5位忽略我们传0就可以。void MRAM_ReadBytes(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[4]; cmd[0] 0x03; cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; GPIOWritePin(MRAM_CS_GPIO_BASE, MRAM_CS_PIN, 0); // CS低 SSIDataPut(SSI0_BASE, cmd[0]); SSIDataPut(SSI0_BASE, cmd[1]); SSIDataPut(SSI0_BASE, cmd[2]); SSIDataPut(SSI0_BASE, cmd[3]); // 发送命令和地址后时钟继续跑接收数据 for (uint32_t i 0; i len; i) { uint32_t dummy 0xFFFFFFFF; SSIDataPut(SSI0_BASE, dummy); // 主发0xFF产生时钟 SSIDataGet(SSI0_BASE, dummy); buf[i] (uint8_t)dummy; } GPIOWritePin(MRAM_CS_GPIO_BASE, MRAM_CS_PIN, 1); // CS高 }这里有一个容易被忽视的细节TivaWare的SSIDataPut和SSIDataGet都要求FIFO不满/不空而且它们是阻塞式的调用前最好先用SSIBusy判断或等待TX FIFO有空间。上面这个写法在高频调用时其实有隐患如果SSI的TX FIFO满了SSIDataPut会一直等如果RX FIFO空了SSIDataGet也可能扑空。严谨的写法是先读状态位或者用SSIDataGetNonBlocking之类的机制。更稳妥的方式是用阻塞式带超时uint32_t ReadByteBlocking(void) { while(SSIDataGetNonBlocking(SSI0_BASE, (uint32_t*)rx_val) 0); return rx_val; }不过对MRAM这种简单外设数据量不大时直接在while循环里发送和接收也没问题。我更推荐用轮询状态位的方式因为SSIDataPut在TX FIFO满时会傻等若SPI总线上有其他干扰导致FIFO状态异常容易卡死。工业代码里任何无限等待的语句都是潜在故障点建议所有SPI收发函数都加上超时回收。3.3 MRAM写命令封装写入前必须先发WREN写使能。MR25H40CDF虽然不像Flash那样严格区分WEL位但状态寄存器里还是有WIP和WEL。放心起见写操作序列固定为CS低 - 0x06 - CS高然后重新CS低 - 0x02 - 地址 - 数据 - CS高。bool MRAM_WriteBytes(uint32_t addr, uint8_t *buf, uint32_t len) { if (len 0 || (addr len) MRAM_CAPACITY) return false; // Step1: WREN SendCmd(0x06); // Step2: Wait WIP clear WaitWIPClear(); // Step3: Write command GPIOWritePin(MRAM_CS_GPIO_BASE, MRAM_CS_PIN, 0); SSIDataPut(SSI0_BASE, 0x02); SSIDataPut(SSI0_BASE, (addr 16) 0xFF); SSIDataPut(SSI0_BASE, (addr 8) 0xFF); SSIDataPut(SSI0_BASE, addr 0xFF); for (uint32_t i 0; i len; i) { SSIDataPut(SSI0_BASE, buf[i]); } GPIOWritePin(MRAM_CS_GPIO_BASE, MRAM_CS_PIN, 1); // Step4: Wait write done WaitWIPClear(); return true; }有个地方必须强调MR25H40的WRITE指令跨页的时候需要注意虽然它是MRAM不需要擦除但页边界限制依然存在。数据手册写明单次WRITE最多256字节而且不允许跨页。如果写入长度加当前地址超过了页边界芯片会从页首回卷写入这个行为会覆盖前面的数据特别坑。所以驱动里必须强制做跨页拆包while (len 0) { uint32_t page_remain 256 - (addr 0xFF); uint32_t chunk (len page_remain) ? len : page_remain; WriteOnePage(addr, buf, chunk); addr chunk; buf chunk; len - chunk; }这是很多新手踩进去的第一个坑。MRAM虽然名号响但页写入限制和Flash是一样的只差不需要擦除这个环节。我见过有人写日志模块时不拆页写到第256字节附近数据莫名错乱排查半天才发现是这个边界回卷机制在作怪。3.4 状态寄存器轮询写状态寄存器、普通写入都要等待WIP位清零。RDSR的读法很简单uint8_t MRAM_ReadStatus(void) { uint8_t status 0; GPIOWritePin(MRAM_CS_GPIO_BASE, MRAM_CS_PIN, 0); SSIDataPut(SSI0_BASE, 0x05); SSIDataPut(SSI0_BASE, 0xFF); SSIDataGet(SSI0_BASE, status); GPIOWritePin(MRAM_CS_GPIO_BASE, MRAM_CS_PIN, 1); return status; } void WaitWIPClear(void) { // 轮询WIPbit0 // 加超时保护避免总线异常 uint32_t timeout 1000000; while ((MRAM_ReadStatus() 0x01) --timeout); if (timeout 0) { // 记录错误复位SSI或上报 } }MRAM内部写操作几乎瞬间就完成正常情况下WIP轮询一两次就清了。加超时的意义在于万一SPI通信线上有异常干扰导致读回来的状态一直是0x01或者芯片根本没应答系统不能卡死在这里。工业代码里任何循环等待都要有逃生门这是一条铁律。4. 掉电保存与可靠性设计4.1 掉电检测与关键数据保存TM4C129ENCZAD内部有BOR电路可以设置阈值检测3.3V电源跌落。比较常见的做法是把BOR中断或者一个GPIO接的电压检测芯片如TPS3803/TLV803接到主控的外部中断引脚上电源开始跌落时立刻触发中断在中断服务程序中把当前运行状态、临时变量、外部传感数据一股脑写进MRAM。这里有个性能问题如果写入数据很多比如几KB掉电瞬间的电容储能未必够用。MR25H40CDF的写入速度很快实测在20MHz SPI下写1KB数据耗时不超过1毫秒理想情况连地址加数据加时钟大概500us左右这给掉电保存提供了非常充裕的时间窗口。我在板子上放了一个200uF左右的储能电容给3.3V供电实测能把3.3V维持在有效范围以内约50毫秒足够把4KB多的关键数据全部写入MRAM。掉电顺序也值得聊。中断触发时要先把系统里所有中断源都关掉至少把优先级高于保存任务的中断关掉避免正在写MRAM的时候又被其他中断打断。写入完成后置一个标志位下次上电启动时先检查这个标志如果上次正常保存就恢复数据如果发现写了半截说明掉电保存过程出了状况要回滚到备份区。这正是MRAM的好处备份区可以频繁读写不需要做磨损均衡的算法。4.2 数据备份与一致性策略由于MRAM没有擦写寿命压力可以放心使用“双区镜像”或者“日志式”存储。比如存参数时固定用两页页A和页B。每次写新参数时先写页B然后写页A再写个标志位表示“本次更新完成”。读取时先看标志如果标志正常读页A如果页A内容不合法但页B合法就用页B并重新构建页A。这种双缓冲策略复杂度和Flash方案相比低得多因为不需要考虑坏块均衡和擦除时间。还有一点MRAM的写入是立即生效的。Flash写入虽然最终也“立即”写在Flash里但内部过程是先缓存后编程。MRAM在模式0下发送完最后一个数据字节并且CS拉高的那一刻数据已经变成了持久存储。所以同一地址连续写入不同数据不需要担心写偏了。但要注意MRAM不支持原地读取-修改-写入这种概率性问题任何想要改某几个位的操作都应该先把整个字或记录读出来改完再整字写回去。因为MRAM的写粒度虽然按字节算但硬件上是以整个16字节段为最小物理单元的有些MRAM是32字节段。虽然写单个字节不会影响邻字节内容但如果正好跨了物理段边界某些批次可能产生段内回写干扰。稳妥的做法是参数记录结构体对齐到1字节即可但同一个结构体中不同字段不要频繁分开写。4.3 文件系统取舍如果应用需要管理大量文件比如历史报表、波形文件在MRAM上挂个littlefs或者spiffs也不是不行。但说实话512KB容量不上文件系统也可以直接用固定记录索引表更可控。我习惯的做法是MRAM划分为三个区域区域偏移大小用途系统参数区0x000008KB设备配置、校准参数双备份运行记录区0x02000240KB事件日志、运行数据循环覆盖用户数据区0x1E00060KB用户自定义存储文件列表索引运行记录区采用环形缓冲每个记录固定64字节头部有魔法数、序号和时间戳。采集上来的数据在写入前先计算出CRC32放在记录尾部。读取时遇到CRC异常就视为坏记录跳过继续扫描下一个。因为MRAM不存在“编程失败导致的固定坏区”所以全盘扫描一遍逻辑上损坏的记录重新覆盖写即可不需要坏块管理。文件系统如果一定要上littlefs设计时就考虑掉电安全对MRAM这种非易失介质很合适。但littlefs默认按Flash特性做了磨损均衡这在MRAM上是没有必要的反而拖慢写入速度。真有需求的话可以把block_size配置为256字节、block_cycles调大减少元数据开销。5. 实际调试踩坑与性能实录5.1 实测性能记录在TM4C129ENCZAD跑120MHzSSI0时钟设为20MHz的前提下我做了几组实测数据操作长度实际耗时说明读256字节256B约110us含命令地址和收数SPI时钟20MHz写256字节256B约130us含WREN、写数据、WIP轮询写一条64B记录64B约40us工业日志场景读4KB4096B约1.7ms主要用于掉电恢复瞬间这个数据里最亮眼的是写一条64B记录只要几十微秒。如果换成NOR Flash即使不考虑擦除光是写入编程时间都要一百多微秒到几毫秒而且还要等写状态寄存器。所以MRAM特别适合那种“每隔几毫秒刷一条状态”的场合CPU开销几乎可以忽略。5.2 常见问题速查表现象可能原因解决办法读回全是0xFFSSI相位配置错误确认CPOL/CPHA为0/0或1/1用逻辑分析仪抓时序读回第几个字节错位TX/RX FIFO未及时清空用SSIBusy等待或加读空FIFO逻辑能读不能写没发WREN或WP引脚被拉低使能了保护检查驱动先发0x06检查硬件WP接高写入到256字节整数倍地址附近数据错乱跨页回卷驱动强制分页写掉电后数据丢失掉电瞬间存储电容不够写入未完成就掉电加大储能电容缩短短时间先关中断再写长时间高温下偶发读写错误电源纹波、长线干扰加去耦电容、缩短走线、降低SPI时钟到10MHz我在调试早期还踩过一个非常隐蔽的坑TM4C129的SSI0引脚PA3默认是GPIO我在初始化GPIO为SSI功能之后又把它拿来当普通GPIO控制片选。这个动作会导致引脚功能被复用分配覆盖出现“写着写着CS不受控”的情况。正确做法是把MRAM的CS接到一个独立的GPIO引脚不占用SSI的FSS或者占用FSS时不要既当GPIO又当SSI的自动片选用。我后来直接放弃SSI硬件自动片选自动FSS只有单字节模式才好用改用PC7拉了根普通GPIO线当CS发现控制时序反而直观得多。5.3 几个非常实用的代码细节SSI发送超时。具体写法可以这样#define SSI_TX_TIMEOUT 1000 static bool SSI_PutByteBlocking(uint8_t byte) { uint32_t timeout SSI_TX_TIMEOUT; while (SSIDataPutNonBlocking(SSI0_BASE, byte) 0) { if (--timeout 0) { return false; } // 可以延迟几个周期不要死等 } return true; }这个函数看起来只是加了超时但正是这个超时保护让系统在SPI故障时不会卡死在FIFO满的状态里。设备在现场运行总线偶尔被强干扰打断是常态没有超时的驱动跑到一半死机连看门狗都救不回来因为它是在禁止中断的临界区里死循环。读MRAM ID做自检。上电初始化时顺手读一下JEDEC ID如果读不到或者不是预期值直接报错并进入快速自检模式。RDID指令时序CS低 - 0x9F - 读4字节。其中前两字节是厂商IDMR25H40CDF读出来应该能对上Everspin的标识。有经验的工程师通过这个自检能快速判断是焊接问题、接线问题还是芯片选型问题比盲目调试快太多。写入完成检查。写完数据后除了轮询WIP还可以加上一个“读回校验”策略。对非关键数据读回校验会增加一倍时间一般我不开。但对设备标定参数这类输入容错极低的数据我开了读回校验写完读一遍不一致就重写三次三次失败就上报关键错误。MRAM因为不需要擦除写失败率很低但这种防御式编程在工业上不过分。6. 存储分区设计与容量规划心得拿MR25H40CDF的512KB容量说事很多人第一眼觉得不大。但工业设备的“数据量”和消费电子不在一个量级一条带时间戳的传感器记录一般是32到64字节512KB足够存8000到16000条。按一分钟记录一条的频率可以覆盖一周多的数据。如果还是嫌少升级到MR25H40系列更高容量的MR25H25632MB也不是难事驱动代码几乎完全复用只要把地址从3字节改成4字节模式即可。分区设计上强烈建议在头4KB放一个结构化的“设备信息块”里面包含魔数Magic Number、版本号、分区表指针、上次掉电标志。上电启动时先读这块判断存储布局是否有效。因为MCU升级固件后存储布局可能改变有了版本号分区表老固件的MRAM内容能平稳迁移。分区表我建议放在未尾的备用块里方便在线升级时不破坏主参数区。每次写分区表之前先写一个“正在更新”标志写完新表再清标志。这样即使掉电发生在更新瞬间上电后也能根据标志自动回滚旧表。7. 最后的建议调试这套方案时逻辑分析仪和好的SPI抓包工具就是命根子。别只靠示波器看波形直接上分析仪把CS、CLK、MOSI、MISO四根线同时抓下来对着数据手册的时序图逐字节核对。MRAM的时序比Flash还严格因为它的时钟沿边距要求精确到几个纳秒逻辑分析仪采样率最好选100MHz以上的。我个人在实际操作中的体会是MRAM最大的价值并不在于“快”而在于把掉电保存的问题彻底转化成纯软件问题。过去做掉电保存要设计外部SRAM加电池、要考虑掉电时序、要防止供电瞬断现在只需要保证掉电检测及时然后放心写SPI即可。MR25H40CDF和TM4C129ENCZAD这套组合让我的代码逻辑清爽了很多。如果后续想把系统往更高端推进可以考虑把SPI时钟往上提拿DMA跑读写或者换成交量更大的MR25H系列更大容量型号。不过在动这些之前先把基本功练扎实SPI相位配置对、跨页处理对、片选时序对。这三点做到位这套存储方案在工业现场真的可以做到“一劳永逸”。
返回列表