ARTICLE DETAIL

资讯详情

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

工业嵌入式非易失存储选型:MRAM替换SRAM与Flash的实战指南

工业嵌入式非易失存储选型:MRAM替换SRAM与Flash的实战指南 1. 为什么工业现场还在用并行SRAM而MRAM已经悄悄换了赛道做工业嵌入式这行的朋友大概都有过这样的经历设备跑在现场突然断电重启之后发现标定参数丢了、运行日志断了、最后一段采集数据没了。回头一查问题出在存储介质上——要么是SRAM靠纽扣电池续命电池没电数据就没了要么是Flash写入速度跟不上高频采集场景下丢帧严重要么是EEPROM擦写寿命有限几年下来坏块越来越多。我最早接触非易失存储方案的时候用的还是经典的并行SRAM加电池的方案。那个板子上常年焊着一颗CR2032客户每隔两三年就得换一次电池维护成本高不说电池在低温环境下容量骤降北方冬天户外设备经常出问题。后来换成SPI Flash容量大了、成本低了但新的麻烦又来了Flash写入前必须擦除整个扇区写入延迟动辄几毫秒到几十毫秒对于需要高频记录运行数据的场景根本扛不住。而且Flash的擦写寿命通常只有十万次量级按每秒写一次算不到三天就能把某个扇区写废。MRAM磁性随机存储器的出现算是给这个困局提供了一个新解法。它的核心原理是用磁性隧道结的磁化方向来存储数据位而不是电荷。这意味着它天生具备非易失性——断电数据不丢同时又拥有接近SRAM的读写速度写入不需要擦除操作字节级随机写入擦写寿命理论上无限。Everspin的MR25H40CDF就是这类器件里比较有代表性的一颗4Mbit容量SPI接口工业级温度范围SOIC-8封装引脚少、布线简单特别适合替换那些SRAM电池或者小容量Flash的方案。STM32F767ZG这颗MCU大家应该不陌生Cortex-M7内核216MHz主频2MB Flash512KB SRAM外设资源丰富带多个SPI接口其中SPI1挂在APB2总线上最高时钟可以跑到54MHz。用它来驱动MR25H40CDF硬件上几乎不需要额外器件软件上HAL库的SPI驱动也足够成熟。但实际调试下来有几个地方如果不注意很容易翻车SPI模式配置、片选时序、写入后的等待策略、DMA传输的对齐问题这些细节在数据手册里写得比较含蓄真正踩过一遍才知道疼。这篇文章主要面向正在做工业数据采集、设备参数存储、黑匣子日志记录的嵌入式工程师尤其是那些正在评估MRAM替换传统存储方案的团队。我会从硬件连接、SPI底层配置、读写时序、性能实测、常见问题排查几个维度把MR25H40CDF配STM32F767ZG这套组合的完整落地过程讲清楚。不管你是第一次接触MRAM还是已经在用但遇到了一些奇怪的现象应该都能从里面找到有用的东西。2. MR25H40CDF的SPI通信底层模式、时序与片选的那些坑2.1 SPI模式选择为什么Mode 0和Mode 3都能用但建议统一用Mode 0MR25H40CDF支持SPI Mode 0CPOL0CPHA0和Mode 3CPOL1CPHA1。数据手册里明确写了这两种模式都兼容很多工程师看到这里就随便选一个觉得反正都能通。但实际项目里我建议统一用Mode 0原因有两个。第一STM32F767的SPI外设在Mode 0下的时序余量更大。Mode 0是空闲低电平第一个边沿上升沿采样数据在下降沿变化。STM32的SPI在空闲状态下SCK保持低电平第一个时钟上升沿之前MOSI就已经稳定了这个建立时间在54MHz下大约有18.5ns对于MR25H40CDF的输入建立时间要求典型值5ns来说绰绰有余。而Mode 3是空闲高电平第一个边沿下降沿采样STM32在切换空闲电平时会有额外的延迟高速下反而容易出问题。第二如果你板上还有其他SPI从设备比如某些ADC或者Flash它们可能只支持Mode 0。统一模式可以避免在片选切换时反复改SPI配置减少出错概率。我在一个项目里同时挂了MR25H40CDF和一颗SPI温度传感器传感器只认Mode 0MRAM两种都行最后统一用Mode 0代码干净很多。配置代码大概长这样hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; // CPOL 0 hspi1.Init.CLKPhase SPI_PHASE_1EDGE; // CPHA 0 hspi1.Init.NSS SPI_NSS_SOFT; // 软件片选 hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_4; // 54MHz hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; hspi1.Init.TIMode SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE;这里有个细节BaudRatePrescaler设成4SPI1挂在APB2上F767的APB2时钟最高108MHz分频后是27MHz。等等这里要算清楚——STM32F767的SPI1时钟源是APB2默认情况下APB2的时钟是HCLK的一半HCLK是216MHz所以APB2是108MHz。分频系数4得到27MHz。如果你把系统时钟超到240MHzAPB2可能到120MHz分频4就是30MHz。MR25H40CDF的最高SPI时钟是40MHz所以27MHz到30MHz都在安全范围内。我实测过40MHz也能跑但工业现场温度范围宽留点余量更稳妥。2.2 片选信号硬件片选和软件片选的取舍STM32的SPI支持硬件NSS和软件NSS两种模式。硬件NSS由外设自动控制片选引脚看起来省事但实际用起来限制很多。比如F767的SPI1硬件NSS是PA4如果你板上PA4被其他功能占用了就没法用。而且硬件NSS在多从机场景下需要额外的逻辑电路来译码反而更麻烦。我所有项目里一律用软件片选也就是随便选一个GPIO在SPI传输前后手动拉低拉高。这样做的好处是片选引脚可以任意分配多从机时每个从机一个片选代码逻辑清晰。但要注意几个点片选拉低到第一个SCK边沿之间要有足够的建立时间。MR25H40CDF要求CS下降沿到SCK上升沿至少5ns。STM32的GPIO翻转速度在50MHz以上时加上SPI外设启动传输的延迟这个时间通常够但如果你在片选拉低后立刻调用HAL_SPI_Transmit中间没有其他操作实际测下来建立时间大约在20ns左右没问题。片选拉高之前要确保最后一个SCK边沿已经完成。HAL库的SPI传输函数在返回前会等待TXE和BSY标志所以传输完成后SCK已经停止再拉高片选是安全的。片选拉高后到下一次拉低之间MR25H40CDF需要至少18ns的片选高电平时间tSHSL。如果你在循环里连续读写两次操作间隔太短可能违反这个要求。我一般会在片选拉高后加一个__NOP()或者几个空指令确保满足时序。软件片选的代码模式#define MRAM_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define MRAM_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) void MRAM_WriteEnable(void) { uint8_t cmd 0x06; // WREN MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 1, 100); MRAM_CS_HIGH(); }2.3 命令集与地址格式24位地址怎么发MR25H40CDF的容量是4Mbit也就是512KB需要19位地址线。但SPI接口按字节传输所以地址用3个字节24位表示高5位忽略。命令格式是1字节命令码 3字节地址 数据。读命令是0x03写命令是0x02写使能是0x06读状态寄存器是0x05写状态寄存器是0x01。这里有个容易搞错的地方地址的字节顺序。MR25H40CDF要求地址是MSB first也就是先发高字节再发中字节最后发低字节。比如你要访问地址0x012345发送顺序是0x01、0x23、0x45。如果你用STM32的HAL_SPI_Transmit发送一个4字节数组数组内容应该是{0x03, 0x01, 0x23, 0x45}。这个顺序在数据手册的时序图里画得很清楚但如果不仔细看很容易按小端序发成{0x03, 0x45, 0x23, 0x01}结果读出来的数据全是乱的。我一般会封装一个地址转换函数void MRAM_BuildCmdAddr(uint8_t *buf, uint8_t cmd, uint32_t addr) { buf[0] cmd; buf[1] (addr 16) 0xFF; buf[2] (addr 8) 0xFF; buf[3] addr 0xFF; }2.4 状态寄存器WEL位和BP位的作用MR25H40CDF的状态寄存器只有几个位有意义WELWrite Enable Latch、BP0、BP1、WPEN。WEL位在每次写操作前必须置1写操作完成后自动清零。如果你发完WREN命令后没有立刻发写命令或者中间插了其他操作WEL可能会超时失效。数据手册里没有明确写超时时间但我实测下来WREN之后如果超过大约100us没有发起写操作WEL就会自动清零写操作会被忽略。BP0和BP1是块保护位可以保护部分地址区域不被写入。默认情况下BP0和BP1都是0整个存储区可写。如果你需要保护固件参数区可以把BP位置1对应的地址范围就变成只读。WPEN位配合WP引脚使用硬件写保护。我一般不用硬件WP因为PCB上多一根线就多一个故障点软件写保护足够灵活。读状态寄存器的代码uint8_t MRAM_ReadStatus(void) { uint8_t cmd 0x05; uint8_t status; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 1, 100); HAL_SPI_Receive(hspi1, status, 1, 100); MRAM_CS_HIGH(); return status; }3. STM32F767ZG的SPI外设配置从CubeMX到寄存器级调优3.1 CubeMX配置的隐藏陷阱NSS引脚和GPIO速度用CubeMX配置SPI1的时候默认会把NSS引脚PA4配置成SPI功能。但如果你用软件片选这个引脚应该配置成GPIO输出而不是SPI_NSS。CubeMX有时候会自动把PA4设成SPI_NSS即使你在SPI配置里选了软件NSS。这个坑我踩过代码里手动拉PA4但CubeMX生成的初始化代码把PA4配成了复用功能结果片选根本不受控MRAM一直处于选中状态读出来的数据全是0xFF。正确的做法是在CubeMX的SPI配置里NSS选Disable然后在GPIO配置里手动把PA4设成GPIO_Output初始电平高。生成代码后检查MX_GPIO_Init和MX_SPI1_Init确保PA4没有被配成AF模式。另一个容易忽略的是GPIO速度。SPI的SCK、MOSI、MISO引脚应该配置成Very High速度片选引脚也建议Very High。如果片选引脚速度设成Low拉低和拉高的边沿会变缓在27MHz SCK下可能导致建立时间不够。我实测过片选设成Low时读写偶尔会出错改成Very High后稳定。3.2 SPI时钟相位与数据采样点的实测对比前面说了Mode 0和Mode 3都能用但实际波形上还是有差异。我用示波器抓过两种模式下的SCK和MOSI波形。Mode 0下MOSI在SCK下降沿变化上升沿采样数据眼图很宽大约有半个时钟周期的窗口。Mode 3下MOSI在SCK上升沿变化下降沿采样眼图也差不多。但在27MHz下Mode 0的上升沿更陡峭因为STM32的SPI在空闲低电平时第一个上升沿是从低到高的完整摆幅而Mode 3的空闲高电平在第一个下降沿时如果之前SCK已经稳定在高电平边沿质量会略差一点。这个差异在常温下不明显但在-40度低温下Mode 3的边沿可能会变缓导致采样错误。所以工业级产品我强烈建议用Mode 0。3.3 DMA传输的配置要点对齐与中断优先级如果你需要高速连续读写MRAM比如做数据记录仪每次写几百字节到几KB用DMA可以大幅降低CPU占用。STM32F767的SPI1支持DMATX用DMA2 Stream3RX用DMA2 Stream2。配置DMA时要注意几个点数据对齐SPI数据寄存器是16位但如果你用8位数据模式DMA的源地址和目的地址都应该是字节对齐。HAL库的HAL_SPI_Transmit_DMA内部会处理但如果你自己写DMA配置要确保DMA_MEMORY_DATA_ALIGNMENT_BYTE。中断优先级DMA传输完成中断的优先级要低于SPI中断否则可能在SPI还没发完最后一个字节时DMA就报了完成。我一般把DMA中断设成优先级5SPI中断设成优先级4。片选控制DMA传输是异步的你不能在调用HAL_SPI_Transmit_DMA后立刻拉高片选否则传输会被截断。正确的做法是在DMA传输完成回调里拉高片选。void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { MRAM_CS_HIGH(); mram_tx_done 1; } }3.4 读写速度实测27MHz下到底能跑多快我用逻辑分析仪抓过实际读写速度。单字节写操作WREN 写命令 地址 数据总共需要约1.2us其中SPI传输时间约0.9us片选和命令间隔约0.3us。连续写256字节用DMA传输总时间约85us平均每字节0.33us折合传输速率约3MB/s。读操作更快因为不需要WREN连续读256字节约78us约3.3MB/s。这个速度对于大多数工业数据采集场景足够了。比如你每秒采集1KB数据写入时间只占0.03%CPU几乎无感。但如果你要做高速波形记录比如每秒1MB那27MHz的SPI就成了瓶颈需要考虑并行接口的MRAM或者用FPGA做缓冲。4. 数据存储与读取的实战代码从单字节到块传输4.1 单字节读写最基础但最容易出错的操作单字节写MRAM的完整流程是拉低片选 - 发WREN命令 - 拉高片选 - 拉低片选 - 发写命令地址数据 - 拉高片选。注意WREN和写命令之间必须拉高片选这是很多新手容易漏掉的。如果你在WREN之后不拉高片选直接发写命令MRAM会把WREN当成数据的一部分写操作不会执行。void MRAM_WriteByte(uint32_t addr, uint8_t data) { uint8_t buf[5]; // 第一步WREN MRAM_CS_LOW(); buf[0] 0x06; HAL_SPI_Transmit(hspi1, buf, 1, 100); MRAM_CS_HIGH(); // 第二步写命令地址数据 MRAM_BuildCmdAddr(buf, 0x02, addr); buf[4] data; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, buf, 5, 100); MRAM_CS_HIGH(); }单字节读就简单多了拉低片选 - 发读命令地址 - 接收数据 - 拉高片选。uint8_t MRAM_ReadByte(uint32_t addr) { uint8_t buf[4]; uint8_t data; MRAM_BuildCmdAddr(buf, 0x03, addr); MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, buf, 4, 100); HAL_SPI_Receive(hspi1, data, 1, 100); MRAM_CS_HIGH(); return data; }这里有个细节HAL_SPI_Transmit和HAL_SPI_Receive分开调用时中间片选不能拉高。上面的代码里片选在Transmit之前拉低在Receive之后拉高中间没有拉高所以是连续的。但HAL库的Transmit和Receive函数内部会检查SPI状态如果前一个传输还没完全结束可能会返回HAL_BUSY。我一般会在Transmit后加一个while等待BSY标志清零或者直接用HAL_SPI_TransmitReceive一次性完成。4.2 页写与连续写地址自动递增的边界MR25H40CDF支持连续写地址在每次写入后自动递增。但要注意地址边界当地址到达0x7FFFF512KB的最后一个字节时下一个地址会回绕到0x00000。如果你写的数据长度超过了剩余空间会覆盖开头的数据。这个行为在数据手册里叫roll over做环形缓冲区的时候可以利用但做参数存储的时候要避免。连续写的代码void MRAM_WriteBuffer(uint32_t addr, uint8_t *data, uint32_t len) { uint8_t cmd[4]; // WREN MRAM_CS_LOW(); cmd[0] 0x06; HAL_SPI_Transmit(hspi1, cmd, 1, 100); MRAM_CS_HIGH(); // 写命令地址数据 MRAM_BuildCmdAddr(cmd, 0x02, addr); MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 4, 100); HAL_SPI_Transmit(hspi1, data, len, 1000); MRAM_CS_HIGH(); }注意HAL_SPI_Transmit的最后一个参数是超时时间单位毫秒。写大块数据时要把超时设大一点比如len4096时27MHz下需要约1.5ms超时设10ms比较安全。4.3 结构体存储对齐与字节序的处理实际项目里经常需要存储结构体比如设备参数、校准系数。直接把结构体指针传给MRAM_WriteBuffer看起来很方便但有几个坑结构体对齐STM32默认按4字节对齐结构体里可能有填充字节。这些填充字节的内容是不确定的写进去再读出来可能不一致。建议用__attribute__((packed))取消对齐或者手动序列化成字节数组。字节序STM32是小端MRAM按字节存储如果你在PC上解析数据要注意大小端转换。我一般统一按小端存储PC端解析时做转换。浮点数float在内存里是IEEE 754格式直接按字节存储没问题但要注意不同编译器可能对浮点数的处理有差异。建议用memcpy转成uint32_t再存。typedef struct __attribute__((packed)) { uint32_t magic; float kp; float ki; float kd; uint16_t calib_offset; uint8_t reserved[6]; } DeviceParams_t; void SaveParams(DeviceParams_t *params) { MRAM_WriteBuffer(PARAM_ADDR, (uint8_t*)params, sizeof(DeviceParams_t)); } void LoadParams(DeviceParams_t *params) { MRAM_ReadBuffer(PARAM_ADDR, (uint8_t*)params, sizeof(DeviceParams_t)); }4.4 掉电保护写入时机与校验策略MRAM虽然是非易失的但写入过程中如果掉电数据可能处于中间状态。比如你写一个结构体写到一半断电读出来就是半新半旧的数据。解决办法是加一个写入标志和校验和。我的做法是在参数区开头放一个magic number和CRC。写入时先写数据区再写CRC最后写magic。读取时先检查magic再校验CRC如果不对就加载默认参数。这样即使写入过程中掉电最多是magic没写成功读取时会认为参数无效不会用到损坏的数据。#define PARAM_MAGIC 0x5A5A1234 typedef struct __attribute__((packed)) { uint32_t magic; uint32_t crc; DeviceParams_t params; } ParamBlock_t; void SaveParamsSafe(DeviceParams_t *params) { ParamBlock_t block; block.magic 0; block.params *params; block.crc CRC32((uint8_t*)block.params, sizeof(DeviceParams_t)); // 先写magic0再写数据和CRC最后写magic MRAM_WriteBuffer(PARAM_ADDR, (uint8_t*)block, sizeof(ParamBlock_t)); uint32_t magic PARAM_MAGIC; MRAM_WriteBuffer(PARAM_ADDR, (uint8_t*)magic, 4); }5. 工业场景下的可靠性设计温度、寿命与数据完整性5.1 宽温下的SPI时序余量计算MR25H40CDF的工业级版本支持-40到85度汽车级到125度。在-40度下MRAM的读写时间会略微增加数据手册里给的时序参数是在25度下测的低温下tV有效数据输出时间可能增加10%到20%。27MHz的SCK周期是37ns半周期18.5ns。MRAM在-40度下的tV最大值约12ns加上PCB走线延迟和STM32的输入建立时间余量大约还有5ns。如果你把SCK提到40MHz周期25ns半周期12.5ns余量就只剩不到1ns了低温下可能出错。所以工业产品我建议SCK不超过30MHz最好在27MHz。如果你必须用40MHz要在高低温箱里做完整测试确保全温度范围内不出错。5.2 擦写寿命与数据保持MRAM的真实表现MRAM的擦写寿命理论上无限Everspin官方给出的数据是10^15次写入。按每秒写1000次算可以写3千万年。数据保持时间官方标称20年实际在85度下可能缩短到10年。对于工业设备通常5到10年的生命周期完全够用。但要注意一个点MRAM的数据保持依赖于磁性材料的稳定性强磁场环境可能影响数据。如果你的设备附近有大功率电机或者磁铁建议加屏蔽或者做磁场测试。我有个客户做电机控制MRAM离电机很近后来发现偶尔有数据翻转加了坡莫合金屏蔽片后问题解决。5.3 数据完整性校验CRC与ECC的选择前面提到了CRC校验这里展开说一下。CRC32可以检测出所有单比特、双比特错误以及大部分多比特错误。计算CRC32的代码可以用查表法速度快占用Flash约1KB。如果你对速度要求不高也可以用逐位计算法代码小但慢。uint32_t CRC32(const uint8_t *data, uint32_t len) { uint32_t crc 0xFFFFFFFF; for (uint32_t i 0; i len; i) { crc ^ data[i]; for (int j 0; j 8; j) { crc (crc 1) ^ (0xEDB88320 -(crc 1)); } } return ~crc; }如果你需要更强的纠错能力可以考虑ECC。但MRAM本身出错的概率极低加ECC有点过度设计。我一般只用CRC做完整性校验检测到错误就重新加载默认值或者从备份区恢复。5.4 多分区管理参数区、日志区、备份区的划分512KB的MRAM如果只存参数太浪费了。我一般会划分成几个区分区起始地址大小用途参数区0x000004KB设备参数、校准系数参数备份区0x010004KB参数的双备份日志区0x02000256KB运行日志、故障记录数据缓冲区0x42000252KB采集数据缓存参数区用双备份写入时先写备份区再写主区读取时如果主区CRC错误就从备份区恢复。日志区用环形缓冲区写满后从头覆盖。数据缓冲区根据具体应用定义。这种分区方式的好处是各功能互不干扰参数写入不会影响日志记录。坏处是需要一套地址管理逻辑代码量增加。我一般会写一个简单的分区管理模块把地址映射和读写封装起来。6. 调试实录那些让我熬夜的MRAM读写异常6.1 读出来全是0xFF片选和SPI模式的排查链路第一次调MRAM的时候读出来全是0xFF当时第一反应是芯片没焊好。用万用表量了电源和地正常。用示波器抓SCK和MOSI有波形。再抓MISO一直是高电平。这就奇怪了MISO一直高说明从机没有驱动数据线。排查过程检查片选用示波器抓CS引脚发现CS在传输期间一直是低电平看起来正常。但仔细看CS拉低的时间比SCK开始的时间早了太多大约有1us。这不对MRAM要求CS拉低后尽快开始SCK如果CS提前太久MRAM可能进入低功耗模式需要额外的唤醒时间。检查SPI模式CubeMX里配的是Mode 0但代码里手动改了CPOL和CPHA翻代码发现没有。检查MISO引脚配置CubeMX里MISO配成了复用推挽但GPIO速度设成了Low。改成Very High后MISO有波形了但数据还是不对。检查地址字节序用逻辑分析仪抓了完整的命令序列发现地址发成了小端序。改成大端序后数据正确。这个问题的根因是地址字节序但中间被片选时序和GPIO速度干扰了排查方向。后来我养成了一个习惯先用逻辑分析仪抓完整波形对照数据手册的时序图逐项检查不要凭感觉猜。6.2 写入后读出来不变WEL位超时的隐蔽问题有一次写参数写完之后读出来还是旧值。用逻辑分析仪抓波形发现WREN命令发了写命令也发了但MRAM没有执行写入。读状态寄存器发现WEL位是0。原因WREN和写命令之间隔了太长时间。我的代码里WREN之后调用了MRAM_CS_HIGH()然后做了一些其他操作比如计算CRC再拉低片选发写命令。这中间隔了大约200usWEL已经超时清零了。解决办法WREN之后立刻发写命令中间不要插入任何耗时操作。如果必须计算CRC先算好再发WREN。void MRAM_WriteBufferFast(uint32_t addr, uint8_t *data, uint32_t len) { uint8_t cmd[4]; MRAM_BuildCmdAddr(cmd, 0x02, addr); MRAM_CS_LOW(); uint8_t wren 0x06; HAL_SPI_Transmit(hspi1, wren, 1, 100); MRAM_CS_HIGH(); __NOP(); __NOP(); // 短暂延时满足tSHSL MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 4, 100); HAL_SPI_Transmit(hspi1, data, len, 1000); MRAM_CS_HIGH(); }6.3 DMA传输丢数据对齐与中断优先级的坑用DMA写1KB数据发现最后几个字节偶尔丢失。排查发现是DMA传输完成中断和SPI传输完成中断的优先级问题。DMA中断优先级高于SPI中断时DMA报完成时SPI可能还有最后一个字节在移位寄存器里没发完此时拉高片选会导致最后一个字节被截断。解决办法把DMA中断优先级设得比SPI低或者在DMA完成回调里先检查SPI的BSY标志等BSY清零后再拉高片选。void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { while (__HAL_SPI_GET_FLAG(hspi, SPI_FLAG_BSY)); MRAM_CS_HIGH(); } }6.4 低温下读写失败时序余量的实测数据有个项目在北方户外运行冬天-30度时偶尔出现参数读取失败。把设备拿回实验室常温下怎么都复现不了。后来放到高低温箱里-30度下跑了一晚上抓到了错误波形。对比常温和低温的SCK波形发现低温下SCK的上升沿变缓从10%到90%的上升时间从2ns增加到5ns。27MHz下半周期18.5ns上升沿占了5ns留给数据建立的时间只剩13.5ns。MRAM在低温下的tV增加到12ns加上PCB延迟余量几乎为零。解决办法把SCK降到18MHz分频系数6半周期27.8ns上升沿5ns余量充足。降速后读写速度降到2MB/s但对于参数存储和日志记录完全够用。7. 性能优化与进阶玩法让MRAM跑得更快更稳7.1 命令合并减少片选切换次数每次片选切换都有开销大约0.3us。如果你要写多个不连续的地址可以把WREN和写命令合并成一次片选操作吗不行WREN和写命令之间必须拉高片选。但你可以把多个写命令合并如果地址是连续的一次片选发完所有数据。如果地址不连续只能分开。读操作可以合并一次片选发读命令地址然后连续读多个地址的数据地址自动递增。如果你要读多个不连续的地址可以发多次读命令但片选不用拉高中间直接发新的读命令地址。MRAM支持在片选保持低电平的情况下连续发多个命令。void MRAM_ReadMultiple(uint32_t *addrs, uint8_t *buf, uint32_t count) { uint8_t cmd[4]; MRAM_CS_LOW(); for (uint32_t i 0; i count; i) { MRAM_BuildCmdAddr(cmd, 0x03, addrs[i]); HAL_SPI_Transmit(hspi1, cmd, 4, 100); HAL_SPI_Receive(hspi1, buf[i], 1, 100); } MRAM_CS_HIGH(); }7.2 双缓冲日志写入与读取的并行处理做数据记录仪的时候如果一边写日志一边读日志SPI总线会冲突。我的做法是用双缓冲MRAM里开两个日志区交替写入。写的时候只写当前区读的时候读另一个区。写满一个区后切换到另一个区同时把写满的区通过串口或者USB上传。这样SPI总线的占用是分时的不会冲突。但要注意两个区的地址管理以及切换时的原子性。我一般用一个状态变量记录当前写区切换时先写状态变量到MRAM再切换。7.3 与Flash的混合架构MRAM做缓存Flash做容量如果你的应用需要大容量存储比如几MB到几十MBMRAM的成本会很高。这时候可以用MRAM做缓存Flash做容量。高频写入的数据先写到MRAM积累到一定量后再批量写入Flash。这样既保证了高频写入的性能又降低了成本。架构大概是MRAM开一个4KB的缓冲区每写满4KB就触发一次Flash写入。Flash写入期间新的数据继续写到MRAM的另一个缓冲区。双缓冲切换。这个方案我在一个振动监测项目里用过每秒采集10KB数据MRAM缓冲Flash存储连续跑了半年没出问题。7.4 低功耗设计MRAM的待机电流与唤醒时间MR25H40CDF的待机电流典型值只有几微安读写电流约10mA。对于电池供电的设备可以在不读写的时候把片选拉高MRAM自动进入待机模式。唤醒时间很短片选拉低后大约100ns就能开始读写不需要额外的唤醒命令。但要注意片选拉高后MRAM需要tSHSL18ns才能接受下一次片选拉低。如果你在低功耗模式下频繁唤醒读写这个时间可以忽略。但如果你的唤醒周期很短比如每秒唤醒1000次片选切换的开销会累积影响功耗。我一般建议唤醒周期不低于1ms这样MRAM的平均功耗可以控制在几十微安。8. 从选型到量产一些掏心窝子的经验8.1 MRAM、FRAM、nvSRAM的选型对比特性MRAM (MR25H40CDF)FRAMnvSRAM接口SPISPI/I2C并行/SPI容量4Mbit通常1Mbit以下4Mbit写入速度字节级无延迟字节级无延迟字节级无延迟擦写寿命10^1510^1410^15功耗中低高成本中高高高温度范围-40~85-40~85-40~85选型建议如果容量需求在4Mbit以内SPI接口工业温度MRAM是首选。FRAM容量小成本更高适合超低功耗场景。nvSRAM需要电池或电容备份维护麻烦逐渐被MRAM替代。8.2 PCB布局SPI走线与去耦电容的放置SPI走线尽量短SCK和MOSI不要平行走太长避免串扰。MISO是输入对干扰敏感走线要远离SCK。片选走线如果太长可能引入延迟建议不超过5cm。去耦电容MR25H40CDF的VCC引脚旁边放一个0.1uF陶瓷电容再并一个1uF。电容尽量靠近引脚回路面积最小。我见过一个板子去耦电容放在离芯片2cm远的地方读写偶尔出错挪近后问题消失。8.3 量产测试如何快速验证MRAM的读写功能量产时每块板子都要测MRAM不能只测一两块。我写了一个简单的测试固件上电后往MRAM的测试区写一个递增序列读回来对比通过则亮绿灯失败亮红灯。测试区选在日志区的末尾不影响正常功能。测试时间大约100ms产线工人按一下按钮就能完成。测试固件通过串口输出结果方便记录。8.4 常见问题速查表现象可能原因排查方法读出全0xFF片选未拉低、SPI模式错、MISO未配置示波器抓CS和SCK读出全0x00MISO对地短路、芯片未供电万用表量MISO对地电阻写入后读不变WEL超时、写保护位设置读状态寄存器偶尔数据错误时序余量不足、电源纹波大高低温测试、示波器抓电源DMA传输丢数据中断优先级、对齐问题检查DMA配置和回调低温下失败SCK太快、时序余量不够降SCK频率我在实际项目中用这套组合替换了原来的SRAM电池方案BOM成本降低了约15%板子面积缩小了20%最重要的是再也不用担心电池没电导致数据丢失。客户反馈设备运行两年多没有出现过一次参数丢失或者日志断档。这个方案后来用在了三个产品线上从环境监测到电机控制都跑得很稳。如果你正在评估MRAM方案我的建议是先买几片样片在现有板子上飞线搭个测试电路用逻辑分析仪把读写时序抓一遍确认没问题再改板。MRAM的SPI接口和Flash兼容软件改动量不大主要是命令集和时序的差异。改完之后你会发现那些因为存储介质导致的奇怪问题一下子就少了很多。
返回列表