ARTICLE DETAIL

资讯详情

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

MRAM工业嵌入式存储实战:MR25H40CDF与TM4C123GH6PZ SPI驱动详解

MRAM工业嵌入式存储实战:MR25H40CDF与TM4C123GH6PZ SPI驱动详解 1. 为什么MRAM在工业嵌入式场景里越来越受关注搞过工业数据采集或者电力终端的朋友应该都有体会选存储芯片这件事很多时候比选主控还让人头疼。EEPROM写入速度慢得让人着急NOR Flash擦除寿命有限FRAM容量又上不去铁电存储器价格还贵。这几年MRAM磁性随机存储器逐渐进入大家的视野尤其是Everspin的MR25H40CDF这颗4Mbit的SPI MRAM在工业现场越来越常见。我最早接触MR25H40CDF是在一个配电终端的项目上当时的需求很明确系统需要频繁记录电参量数据每秒要写几十次每次几十个字节而且掉电不能丢数据工作温度要求-40到85摄氏度。一开始用的EEPROM结果写入延迟直接拖垮了主循环的实时性后来换FRAM容量又不够存历史曲线。最后选了MR25H40CDF4Mbit容量、SPI接口、无限次读写寿命、纳秒级写入速度基本上把前面几个方案的痛点全解决了。这篇文章我打算把MR25H40CDF和TM4C123GH6PZ搭配使用的完整过程梳理一遍。TM4C123GH6PZ是TI的Cortex-M4F芯片80MHz主频自带多路SSI也就是SPI外设在工业控制领域用量很大。两者搭配一个负责高速运算和通信一个负责可靠存储是很典型的工业嵌入式存储方案。不管你是刚接触SPI存储的新手还是正在选型的嵌入式架构师这篇内容应该都能给你一些可以直接抄作业的参考。2. 项目整体设计与选型思路拆解2.1 为什么是MR25H40CDF而不是EEPROM或Flash先把这个选型逻辑说清楚因为很多刚入行的朋友会觉得“存储芯片不都差不多吗”实际上差别大了去了。MR25H40CDF的核心优势在于它是MRAM不是传统的电荷存储。MRAM靠磁隧道结的磁化方向来存储数据写入过程不需要先擦除也不需要高压电荷泵。这带来几个直接好处写入速度是纳秒级的没有写延迟读写寿命理论上无限不像Flash只有十万次擦除寿命功耗低写入时不需要高压。对比一下常见方案特性EEPROMNOR FlashFRAMMR25H40CDF写入速度毫秒级毫秒级需擦除微秒级纳秒级擦写寿命100万次10万次10^12次无限容量小中小4Mbit接口I2C/SPISPII2C/SPISPI掉电保持是是是是工作温度-40~85-40~85-40~85-40~125从表格能看出来MR25H40CDF在写入速度和寿命上是碾压性的优势。工业场景里数据记录往往要求“随时写、频繁写、掉电不丢”这三点MRAM全部满足。2.2 TM4C123GH6PZ的SSI外设为什么适合驱动MRAMTM4C123GH6PZ有4路SSI模块每路都支持SPI模式最高时钟可以到主频的四分之一也就是20MHz。MR25H40CDF支持最高40MHz的SPI时钟所以TM4C这边完全不会成为瓶颈。选TM4C123GH6PZ还有一个原因是它的SSI外设支持Freescale SPI模式也就是CPOL和CPHA可以灵活配置。MR25H40CDF支持SPI模式0和模式3TM4C都能覆盖。另外TM4C的SSI有独立的TX和RX FIFO深度是8在连续读写的时候可以减少中断次数提高吞吐效率。还有一点很实际TM4C123GH6PZ的GPIO可以配置成SSI的片选引脚也可以软件控制普通GPIO做片选。工业现场有时候需要多个SPI从设备共享总线软件片选就更灵活。这个后面实操部分会详细说。2.3 整体数据流设计这个项目的核心数据流其实不复杂TM4C通过SPI总线向MR25H40CDF发送命令和数据MRAM执行读写操作。但工业场景下要考虑几个额外因素。第一是掉电保护。系统检测到掉电信号后要利用电源电容的余电完成最后一次写入。MRAM写入快这个窗口完全够用。第二是数据完整性。我习惯在存储区加CRC校验每个数据块后面跟两个字节的CRC16。读取的时候先校验再使用避免读到半写状态的数据。第三是磨损均衡。虽然MRAM寿命无限但为了管理方便我还是做了简单的环形缓冲区写指针循环移动读的时候按时间戳排序。整体架构上TM4C的SSI1用来接MR25H40CDFSSI0留给调试串口或者别的外设。MRAM的存储空间划分成几个区域系统配置区、实时数据区、历史数据区、日志区。每个区域有独立的起始地址和长度用宏定义管理。3. 硬件连接与SPI底层配置细节3.1 引脚连接与硬件设计要点MR25H40CDF是8引脚SOIC封装引脚定义很标准CS、SO、SI、SCK、VDD、VSS、WP、HOLD。WP和HOLD在不需要硬件写保护和暂停功能的时候直接上拉到VDD就行。和TM4C123GH6PZ的连接方式MR25H40CDF的SCK接TM4C的SSI1ClkPB5SI接SSI1TxPB7SO接SSI1RxPB6CS接PB4配置成普通GPIO做软件片选WP和HOLD都上拉到3.3V这里有个细节要注意MR25H40CDF的供电范围是2.7V到3.6VTM4C123GH6PZ的IO也是3.3V电平匹配没问题。但如果你的系统里有5V器件一定要加电平转换MRAM的IO不耐5V。PCB布局上SPI的四根线尽量等长SCK走线远离模拟信号。MRAM的电源引脚旁边放一个0.1uF的陶瓷电容再并一个1uF的钽电容。如果MRAM和主控距离超过10厘米建议在SCK上串一个22欧姆的电阻抑制反射。注意MR25H40CDF的CS引脚内部没有上拉如果悬空可能导致误触发。一定要在CS上加一个10k的上拉电阻到VDD确保主控未初始化时MRAM不被选中。3.2 TM4C的SSI初始化代码TM4C的SSI配置分几步使能外设时钟、配置GPIO复用、配置SSI参数、使能SSI。下面是我实际项目里用的初始化代码基于TivaWare库。#include stdint.h #include stdbool.h #include inc/hw_memmap.h #include inc/hw_ssi.h #include inc/hw_gpio.h #include driverlib/ssi.h #include driverlib/gpio.h #include driverlib/sysctl.h #include driverlib/pin_map.h #define MRAM_CS_PORT GPIO_PORTB_BASE #define MRAM_CS_PIN GPIO_PIN_4 void MRAM_SPI_Init(void) { // 使能SSI1和GPIOB时钟 SysCtlPeripheralEnable(SYSCTL_PERIPH_SSI1); SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOB); // 配置SSI1引脚复用 GPIOPinConfigure(GPIO_PB5_SSI1CLK); GPIOPinConfigure(GPIO_PB6_SSI1RX); GPIOPinConfigure(GPIO_PB7_SSI1TX); GPIOPinTypeSSI(GPIO_PORTB_BASE, GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7); // 配置CS为普通GPIO输出默认拉高 GPIOPinTypeGPIOOutput(MRAM_CS_PORT, MRAM_CS_PIN); GPIOPinWrite(MRAM_CS_PORT, MRAM_CS_PIN, MRAM_CS_PIN); // 配置SSI1主机模式SPI模式016位数据1MHz时钟 SSIConfigSetExpClk(SSI1_BASE, SysCtlClockGet(), SSI_FRF_MOTO_MODE_0, SSI_MODE_MASTER, 1000000, 8); // 使能SSI1 SSIEnable(SSI1_BASE); // 清空RX FIFO中可能存在的残留数据 uint32_t dummy; while(SSIDataGetNonBlocking(SSI1_BASE, dummy)) {} }这段代码里几个关键点解释一下。SSIConfigSetExpClk的最后一个参数是数据位宽我设的是8位因为MRAM的命令和地址都是按字节操作的。时钟先设1MHz调试通了再往上提。SSI_FRF_MOTO_MODE_0对应CPOL0、CPHA0这是MR25H40CDF支持的SPI模式0。CS用软件控制所以在发送数据前后要手动拉低和拉高。这样做的好处是可以在一次CS有效期内发送多个字节比如读命令加地址加数据中间不需要切换CS。3.3 SPI时序与MRAM命令集MR25H40CDF的命令集很精简常用的就几条命令编码功能WREN0x06写使能WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据读操作时序拉低CS发送0x03发送3字节地址MR25H40CDF是4Mbit需要19位地址用3字节传输高5位忽略然后连续读取数据每读一个字节地址自动递增。读操作可以在任意字节后拉高CS结束。写操作时序拉低CS发送0x06WREN拉高CS。再拉低CS发送0x02发送3字节地址然后连续写入数据。写操作必须在CS拉高后才真正执行MRAM内部有写完成逻辑但因为是纳秒级实际上CS拉高后数据就已经写入了。提示MR25H40CDF的写操作不需要等待写完成不像Flash需要轮询状态寄存器。但为了代码兼容性我还是保留了读状态寄存器的函数万一以后换芯片不用大改。4. 完整读写实操与代码实现4.1 底层字节读写函数先封装两个最基础的函数发送一个字节和接收一个字节。TM4C的SSI是全双工的发送的同时也在接收所以读的时候发一个哑元数据就行。uint8_t MRAM_SPI_Transfer(uint8_t txData) { uint32_t rxData; // 等待TX FIFO有空位 while(SSIDataGetNonBlocking(SSI1_BASE, rxData)) {} // 发送数据 SSIDataPut(SSI1_BASE, txData); // 等待发送完成并读取接收数据 SSIDataGet(SSI1_BASE, rxData); return (uint8_t)(rxData 0xFF); } void MRAM_CS_Low(void) { GPIOPinWrite(MRAM_CS_PORT, MRAM_CS_PIN, 0); } void MRAM_CS_High(void) { GPIOPinWrite(MRAM_CS_PORT, MRAM_CS_PIN, MRAM_CS_PIN); }这里有个坑我踩过TM4C的SSI在发送数据后RX FIFO里会有对应的接收数据。如果只发不收FIFO满了之后SSIDataPut会阻塞。所以每次SSIDataPut之后必须跟一个SSIDataGet哪怕你不需要接收数据。上面的MRAM_SPI_Transfer就是标准做法。4.2 写数据函数实现写数据分两步先发WREN再发WRITE命令加地址加数据。void MRAM_Write(uint32_t addr, uint8_t *data, uint32_t len) { uint32_t i; // 发送写使能命令 MRAM_CS_Low(); MRAM_SPI_Transfer(0x06); MRAM_CS_High(); // 发送写命令和地址 MRAM_CS_Low(); MRAM_SPI_Transfer(0x02); MRAM_SPI_Transfer((addr 16) 0xFF); MRAM_SPI_Transfer((addr 8) 0xFF); MRAM_SPI_Transfer(addr 0xFF); // 连续写入数据 for(i 0; i len; i) { MRAM_SPI_Transfer(data[i]); } MRAM_CS_High(); }地址是24位的MR25H40CDF实际只用低19位高5位忽略。我传3个字节高字节的高5位是0不影响。写操作的关键是WREN必须在WRITE之前单独发一次而且WREN之后CS要拉高再拉低。有些SPI Flash允许WREN和WRITE在同一个CS周期内但MRAM要求分开。这个细节在数据手册里有写但容易忽略。4.3 读数据函数实现读数据就简单了发READ命令加地址然后连续读。void MRAM_Read(uint32_t addr, uint8_t *buffer, uint32_t len) { uint32_t i; MRAM_CS_Low(); MRAM_SPI_Transfer(0x03); MRAM_SPI_Transfer((addr 16) 0xFF); MRAM_SPI_Transfer((addr 8) 0xFF); MRAM_SPI_Transfer(addr 0xFF); for(i 0; i len; i) { buffer[i] MRAM_SPI_Transfer(0x00); } MRAM_CS_High(); }读的时候发0x00作为哑元MRAM会把对应地址的数据移出来。地址自动递增所以连续读不需要重复发地址。4.4 带CRC校验的数据块读写工业场景下我强烈建议加CRC。下面是我用的数据块结构typedef struct { uint32_t timestamp; uint16_t dataLen; uint8_t payload[58]; uint16_t crc; } DataBlock_t; // 总共66字节每个数据块66字节写入的时候先算CRC16把整个结构体写进去。读取的时候先读出来再算一次CRC和存储的CRC对比。uint16_t CRC16_Calculate(uint8_t *data, uint32_t len) { uint16_t crc 0xFFFF; uint32_t i; uint8_t j; for(i 0; i len; i) { crc ^ data[i]; for(j 0; j 8; j) { if(crc 0x0001) { crc (crc 1) ^ 0xA001; } else { crc 1; } } } return crc; } bool MRAM_WriteDataBlock(uint32_t blockIndex, DataBlock_t *block) { uint32_t addr DATA_AREA_START blockIndex * sizeof(DataBlock_t); block-crc CRC16_Calculate((uint8_t*)block, sizeof(DataBlock_t) - 2); MRAM_Write(addr, (uint8_t*)block, sizeof(DataBlock_t)); return true; } bool MRAM_ReadDataBlock(uint32_t blockIndex, DataBlock_t *block) { uint32_t addr DATA_AREA_START blockIndex * sizeof(DataBlock_t); uint16_t crcCalc; MRAM_Read(addr, (uint8_t*)block, sizeof(DataBlock_t)); crcCalc CRC16_Calculate((uint8_t*)block, sizeof(DataBlock_t) - 2); if(crcCalc ! block-crc) { return false; // 数据损坏 } return true; }CRC16用的Modbus多项式0xA001这是工业领域最常用的。计算的时候注意CRC字段本身不参与计算所以长度要减2。4.5 环形缓冲区管理历史数据区用环形缓冲区管理写指针循环移动读的时候按时间戳排序。这样避免固定地址反复写虽然MRAM寿命无限但环形缓冲区在逻辑上更清晰。#define DATA_AREA_START 0x10000 #define DATA_BLOCK_COUNT 1024 #define DATA_BLOCK_SIZE sizeof(DataBlock_t) static uint32_t writeIndex 0; void MRAM_AppendData(DataBlock_t *block) { MRAM_WriteDataBlock(writeIndex, block); writeIndex (writeIndex 1) % DATA_BLOCK_COUNT; } uint32_t MRAM_GetWriteIndex(void) { return writeIndex; }写指针存在RAM里掉电会丢。所以每次上电后要扫描一遍数据区找到时间戳最大的块把writeIndex恢复出来。这个扫描过程大概几毫秒可以接受。5. 性能优化与常见问题排查5.1 提高SPI时钟频率的实操初始化的时候SPI时钟设的1MHz实际跑起来可以往上提。MR25H40CDF支持40MHzTM4C的SSI1最高可以到20MHz主频80MHz的四分频。我实测下来10MHz很稳15MHz偶尔有误码20MHz在长走线的情况下不稳定。提升时钟的方法很简单改SSIConfigSetExpClk的参数SSIConfigSetExpClk(SSI1_BASE, SysCtlClockGet(), SSI_FRF_MOTO_MODE_0, SSI_MODE_MASTER, 10000000, 8);但要注意时钟提高后MRAM_SPI_Transfer里的等待逻辑要检查。TM4C的SSI在高速下SSIDataPut之后要等TX FIFO空SSIDataGet会阻塞直到有数据。如果发现数据错位可以在CS拉高后加几个NOP延时。实操心得SPI时钟不是越高越好。工业现场电磁干扰大10MHz是个比较稳妥的选择。如果PCB走线短且屏蔽好可以试15MHz。20MHz只在实验室环境稳定过。5.2 常见问题速查表现象可能原因排查方法解决方案读出来全是0xFFCS没拉低或MRAM没供电用示波器看CS和SCK波形检查CS引脚连接和供电读出来全是0x00SI/SO接反交换SI和SO试试核对引脚定义写入后读出来不对WREN没发或CS时序不对抓SPI波形看WREN命令确保WREN和WRITE分开CS偶尔数据错位SPI时钟太快降低时钟到5MHz测试降频或加走线屏蔽上电后数据丢失掉电时写入未完成检查掉电检测电路加掉电中断用余电完成写入CRC校验失败数据块跨页或写入中断检查写入长度是否跨边界分块写入每块加CRC5.3 掉电保护的实际处理工业现场掉电是常态掉电保护必须做。我的做法是在电源输入端加一个电压检测芯片比如TPS3809当电压低于3.0V时触发中断。中断里立刻把当前缓存的数据写入MRAM。MRAM写入快66字节的数据在10MHz SPI下大概52微秒就能写完。电源电容只要能在掉电后维持3.3V超过100微秒就够了。我一般用220uF的电解电容并联0.1uF陶瓷电容实测能撑2毫秒左右余量很足。void PowerFail_ISR(void) { // 清除中断标志 GPIOIntClear(POWER_FAIL_PORT, POWER_FAIL_PIN); // 紧急写入当前数据 MRAM_AppendData(currentBlock); // 标记系统状态 systemStatus STATUS_POWER_FAIL; }这个中断的优先级要设高一点确保能及时响应。但也不能太高避免影响其他关键中断。5.4 多设备共享SPI总线的片选管理如果系统里还有别的SPI设备比如显示屏或者无线模块就需要管理片选。我的做法是每个设备一个CS引脚操作前先拉低对应CS操作完拉高。关键是操作期间不能被其他任务打断。void SPI_SelectDevice(uint8_t devId) { // 先拉高所有CS GPIOPinWrite(MRAM_CS_PORT, MRAM_CS_PIN, MRAM_CS_PIN); GPIOPinWrite(OTHER_CS_PORT, OTHER_CS_PIN, OTHER_CS_PIN); // 拉低目标设备CS switch(devId) { case DEV_MRAM: GPIOPinWrite(MRAM_CS_PORT, MRAM_CS_PIN, 0); break; case DEV_OTHER: GPIOPinWrite(OTHER_CS_PORT, OTHER_CS_PIN, 0); break; } }如果用了RTOSSPI总线操作要加互斥锁避免任务切换导致CS时序错乱。6. 几个容易被忽略的细节和我的实操体会6.1 MRAM的写保护引脚不要悬空MR25H40CDF的WP引脚是硬件写保护低电平有效。如果你不需要硬件写保护一定要上拉到VDD。我见过一个案例WP悬空导致MRAM偶尔进入写保护状态写入失败但读正常排查了半天才发现是WP引脚的问题。HOLD引脚同理不用就上拉。这两个引脚悬空的时候MRAM的行为不确定数据手册里虽然说了内部有弱上拉但工业环境下弱上拉不可靠。6.2 状态寄存器的块保护位MR25H40CDF的状态寄存器里有块保护位BP0、BP1可以保护部分存储区不被写入。默认是全保护还是全开放我记不太清了建议上电后先读一次状态寄存器确认保护位是0然后写一次状态寄存器确保全开放。void MRAM_CheckStatus(void) { uint8_t status; MRAM_CS_Low(); MRAM_SPI_Transfer(0x05); // RDSR status MRAM_SPI_Transfer(0x00); MRAM_CS_High(); // 如果保护位被设置清除它 if(status 0x0C) { MRAM_CS_Low(); MRAM_SPI_Transfer(0x06); // WREN MRAM_CS_High(); MRAM_CS_Low(); MRAM_SPI_Transfer(0x01); // WRSR MRAM_SPI_Transfer(status ~0x0C); MRAM_CS_High(); } }6.3 数据对齐和结构体填充DataBlock_t结构体在内存里会有填充字节sizeof可能不是66。我一般用#pragma pack(1)强制1字节对齐确保写入MRAM的数据和结构体定义完全一致。#pragma pack(push, 1) typedef struct { uint32_t timestamp; uint16_t dataLen; uint8_t payload[58]; uint16_t crc; } DataBlock_t; #pragma pack(pop)不加pack的话timestamp后面可能填充2字节payload后面也可能填充导致实际大小变成68或72。跨平台移植的时候这个坑特别常见。6.4 读取速度的实测数据我实测过不同SPI时钟下的读取速度SPI时钟读1KB耗时写1KB耗时1MHz8.2ms8.5ms5MHz1.7ms1.8ms10MHz0.9ms0.95ms15MHz0.6ms0.65ms写入比读取略慢是因为每次写之前要发WREN多了一个CS周期。但整体差距很小MRAM的写入速度优势非常明显。6.5 关于MRAM的寿命虽然MR25H40CDF标称无限读写寿命但实际使用中还是要注意。无限寿命是指存储单元不会因为读写而退化但外围电路比如电荷泵、驱动电路还是有寿命的。不过对于工业应用来说每天写100万次写十年也才36亿次远低于MRAM的承受能力。我个人的建议是正常用就行不用刻意做磨损均衡。但环形缓冲区的逻辑还是保留因为它在数据管理上更方便和寿命无关。6.6 调试工具的选择调试SPI接口逻辑分析仪是必备的。我用的是Saleae Logic 88通道100MHz采样率抓SPI波形足够。关键是它的协议解码功能可以直接把SPI数据解析成命令和地址省去手动对照时序图的时间。如果预算有限也可以用示波器看波形但解码就麻烦很多。我建议至少有一个能解码SPI的逻辑分析仪调试效率会高很多。提示抓SPI波形的时候触发条件设成CS下降沿这样每次片选都能抓到完整的一帧数据。采样率至少是SPI时钟的4倍10MHz SPI需要40MHz以上的采样率。6.7 代码移植到其他平台的注意事项这套代码从TM4C123GH6PZ移植到STM32或者别的平台主要改三个地方SPI初始化、CS控制、延时函数。MRAM_SPI_Transfer的接口不用变上层的数据块读写和CRC校验逻辑完全复用。STM32的HAL库有HAL_SPI_TransmitReceive可以直接替换MRAM_SPI_Transfer。CS控制用HAL_GPIO_WritePin。整体移植工作量不大半天就能搞定。我在实际项目里还遇到过MRAM和SD卡共享SPI总线的情况。SD卡的初始化需要低速时钟MRAM可以高速所以初始化的时候先低速枚举SD卡然后切到高速操作MRAM。切换时钟的时候要注意先禁用SPI外设改完参数再使能避免时序错乱。最后分享一个小技巧如果MRAM的读写偶尔出错但降频后正常大概率是信号完整性问题。可以在SCK和MOSI上串22欧姆电阻MISO上串100欧姆电阻同时缩短走线长度。我有个项目就是SCK走线太长导致15MHz下误码串了电阻之后20MHz都稳。
返回列表