ARTICLE DETAIL

资讯详情

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

STM32L021K4与MR25H40CDF的SPI接口驱动设计与工业掉电保护实践

STM32L021K4与MR25H40CDF的SPI接口驱动设计与工业掉电保护实践 1. 项目缘起与方案选型思路1.1 为什么要在工业场景里盯上 MRAM 这颗料做工业嵌入式的人都有一个共同的痛点设备装到现场之后最怕的就是掉电丢数据。传统的方案无非几种EEPROM 写入慢、擦写寿命有限FRAM 容量小、价格高带电池的 SRAM 又要考虑电池寿命和环保问题。我最早接触 MR25H40CDF 这颗料是因为一个电力监测终端的项目现场要求每 100ms 记录一次电参量掉电后最近 10 分钟的数据不能丢而且设备预期寿命是 10 年以上。算一笔账100ms 一次一天就是 864000 次写入一年 3.15 亿次。EEPROM 那种百万次擦写寿命的器件几个月就废了。FRAM 虽然寿命够但 512Kb 这个容量级别的价格实在下不去手。MRAM 的出现在这个场景里就非常合适——MR25H40CDF 是 4Mb 容量的 SPI 接口磁阻存储器理论上无限次擦写写入没有延迟掉电数据保持 20 年以上而且不需要像 Flash 那样先擦后写。这里要解释一下 MRAM 和 Flash 的本质区别。Flash 存储靠的是浮栅里的电荷写之前必须先把整个扇区擦干净擦除操作慢且伤寿命。MRAM 靠的是磁性隧道结的磁化方向来存储数据写的时候直接翻转磁矩不需要擦除步骤所以写入速度是纳秒级的而且没有擦写次数限制。你可以把它理解成一个掉电不丢的 SRAM用起来跟普通内存一样随意读写但断电后数据还在。1.2 STM32L021K4 在这套方案里扮演什么角色选 STM32L021K4 不是随便挑的。这个项目对主控的要求有几条硬指标第一功耗要低设备是电池加太阳能供电的待机电流必须控制在微安级第二要有硬件 SPI因为 MRAM 的写入速率直接取决于 SPI 时钟第三封装要小PCB 空间紧张第四成本要可控工业产品对 BOM 成本很敏感。STM32L0 系列是 ST 的低功耗产品线L021K4 是 Cortex-M0 内核32KB Flash、2KB RAMTSSOP20 封装运行模式功耗大约 100uA/MHz停止模式可以做到 1uA 以下。硬件 SPI 最高可以跑到 16MHz受限于 APB 时钟对于 MR25H40CDF 来说完全够用——这颗 MRAM 的 SPI 接口最高支持 40MHz我们在 L021 上跑 8MHz 或者 16MHz 都能稳定工作。这里有个细节值得说L021K4 的 RAM 只有 2KB这意味着你不能搞太大的缓冲区。我在实际项目里是开了一个 256 字节的环形缓冲区攒够一页再往 MRAM 里写这样既减少了 SPI 事务开销又不会把 RAM 撑爆。如果你用的是 L052 或者 L073 那种 RAM 大一些的型号缓冲区可以开大一点但 L021 这个级别就得精打细算。1.3 SPI 接口选型的几个关键考量MR25H40CDF 支持 SPI 模式 0 和模式 3也就是 CPOL0/CPHA0 或者 CPOL1/CPHA1。STM32L021 的 SPI 配置里这两个模式都支持我一般用模式 0因为大部分逻辑分析仪和示波器默认就是按模式 0 解码的调试起来方便。硬件片选还是软件片选这是个老生常谈的问题。STM32 的硬件 NSS 在实际使用中经常有坑特别是多从机场景下硬件 NSS 的管理比较死板。我建议用软件片选也就是随便找一个 GPIO 来控制 CS 引脚SPI 外设配置成 NSS 软件管理模式。这样做的好处是片选时序完全可控你可以在发送命令之前提前拉低 CS等数据完全发完之后再拉高避免因为 SPI 时钟相位问题导致 MRAM 误判命令。还有一个容易被忽略的点MR25H40CDF 的 CS 拉低之后需要等待至少 3ns 才能开始发送时钟。STM32 的 GPIO 翻转速度再快也远大于这个值所以正常操作不会有问题。但如果你在中断里操作 SPI要确保 CS 拉低和第一个时钟沿之间没有被其他中断打断太久否则 MRAM 可能会进入一个不确定的状态。我的做法是在 SPI 事务期间关掉全局中断事务结束再打开事务本身很短对系统实时性影响可以忽略。2. 硬件设计与连接细节2.1 引脚连接与 PCB 布局要点MR25H40CDF 是 8 引脚 SOIC 封装引脚定义很标准1 脚 CS、2 脚 SO、3 脚 WP、4 脚 VSS、5 脚 SI、6 脚 SCK、7 脚 HOLD、8 脚 VDD。和 STM32L021K4 的连接方式如下MR25H40CDF 引脚STM32L021K4 引脚说明CS (1)PA4 (GPIO 输出)软件片选低有效SO (2)PA6 (SPI1_MISO)主入从出WP (3)接 VDD不用的写保护直接拉高VSS (4)GND地SI (5)PA7 (SPI1_MOSI)主出从入SCK (6)PA5 (SPI1_SCK)时钟HOLD (7)接 VDD不用的保持功能直接拉高VDD (8)3.3V电源加 100nF 去耦WP 和 HOLD 这两个引脚如果你不需要硬件写保护和暂停传输功能直接拉到 VDD 就行。我见过有人把这两个脚悬空结果 MRAM 工作不稳定偶尔写入失败查了半天才发现是悬空引脚受干扰导致状态误判。所以这两个脚要么拉高要么用 GPIO 控制千万别悬空。PCB 布局上MRAM 尽量靠近 MCU 放置SPI 走线越短越好。SCK 和 MOSI 是高速信号如果走线超过 5cm建议在源端串一个 22Ω 到 33Ω 的电阻做阻抗匹配减少过冲和振铃。MISO 是从机输出一般不需要串阻。去耦电容 100nF 要放在 MRAM 的 VDD 引脚旁边距离不超过 2mm否则去耦效果大打折扣。2.2 电源与去耦的实操经验MR25H40CDF 的工作电压范围是 2.7V 到 3.6VSTM32L021K4 也是 1.65V 到 3.6V所以整个系统用 3.3V 供电完全没问题。但工业现场电源波动大我建议在 MRAM 的 VDD 上再加一个 1uF 的钽电容或者 MLCC和 100nF 并联使用。100nF 滤高频1uF 滤低频两个搭配起来效果最好。有个坑我踩过有一次用了一个 LDO 给 MRAM 供电LDO 的输出电容只有 1uF结果 MRAM 在写入的时候偶尔会复位。后来用示波器抓 VDD 波形发现写入瞬间 VDD 有一个大约 200mV 的跌落持续时间大概 50ns。原因是 MRAM 写入时电流突然增大LDO 响应不过来。解决办法是在 VDD 引脚旁边再并一个 10uF 的电容同时把 LDO 的输出电容加大到 10uF。所以电源这块别省电容该加就加。2.3 SPI 时钟速率的选择与实测MR25H40CDF 的 SPI 最高时钟是 40MHz但实际能跑多快取决于你的 MCU、PCB 走线和线缆长度。STM32L021K4 的 SPI1 挂在 APB2 上最高时钟是 32MHzSPI 分频后可以输出 16MHz、8MHz、4MHz 等。我在实验室用 10cm 的杜邦线连接时16MHz 下偶尔会出现误码降到 8MHz 就完全稳定。后来画了正式 PCB走线长度控制在 3cm 以内16MHz 跑了一周压力测试没有出现任何错误。所以我的建议是如果你还在用杜邦线调试先用 4MHz 或者 8MHz等 PCB 打样回来再尝试提高时钟。工业产品我一般最终定在 8MHz留足余量毕竟 MRAM 的写入速度已经比 EEPROM 快几个数量级了没必要为了那点速度冒可靠性的风险。3. 软件驱动实现与核心代码解析3.1 SPI 初始化与 GPIO 配置STM32L021K4 用 HAL 库配置 SPI1 的代码大概长这样。这里我用的是 SPI 模式 0软件 NSS8 位数据宽度MSB 先行SPI_HandleTypeDef hspi1; void SPI1_Init(void) { __HAL_RCC_SPI1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct {0}; // PA5 SCK, PA6 MISO, PA7 MOSI GPIO_InitStruct.Pin GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; GPIO_InitStruct.Alternate GPIO_AF0_SPI1; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // PA4 作为软件片选 GPIO_InitStruct.Pin GPIO_PIN_4; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // CS 默认拉高 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); 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; hspi1.Init.CLKPhase SPI_PHASE_1EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_4; // 32MHz/4 8MHz hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; hspi1.Init.TIMode SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE; hspi1.Init.CRCPolynomial 7; HAL_SPI_Init(hspi1); }分频系数选 4对应 8MHz 的 SPI 时钟。如果你要跑 16MHz改成SPI_BAUDRATEPRESCALER_2就行。注意 L021 的 SPI1 时钟源是 APB2默认 32MHz所以分频 2 就是 16MHz分频 4 就是 8MHz。3.2 MRAM 读写命令与底层函数MR25H40CDF 的命令集很简洁常用的就几条命令名称命令码说明WREN0x06写使能写入前必须发WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据写操作之前必须先发 WREN 命令这是 MRAM 的写保护机制。虽然 MRAM 不需要擦除但写使能这一步不能省。读操作不需要 WREN直接发 READ 命令加地址就行。地址是 24 位的因为 4Mb 容量对应 512KB 地址空间需要 19 位地址但 MRAM 用 3 个字节传输地址高 5 位忽略。下面是我封装的基础读写函数#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) #define CMD_WREN 0x06 #define CMD_WRDI 0x04 #define CMD_RDSR 0x05 #define CMD_READ 0x03 #define CMD_WRITE 0x02 void MRAM_WriteEnable(void) { uint8_t cmd CMD_WREN; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 1, 100); MRAM_CS_HIGH(); } uint8_t MRAM_ReadStatus(void) { uint8_t cmd CMD_RDSR; uint8_t status 0; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 1, 100); HAL_SPI_Receive(hspi1, status, 1, 100); MRAM_CS_HIGH(); return status; } void MRAM_Write(uint32_t addr, uint8_t *buf, uint16_t len) { uint8_t cmd[4]; cmd[0] CMD_WRITE; cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; MRAM_WriteEnable(); MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 4, 100); HAL_SPI_Transmit(hspi1, buf, len, 1000); MRAM_CS_HIGH(); } void MRAM_Read(uint32_t addr, uint8_t *buf, uint16_t len) { uint8_t cmd[4]; cmd[0] CMD_READ; cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 4, 100); HAL_SPI_Receive(hspi1, buf, len, 1000); MRAM_CS_HIGH(); }这里有个细节HAL_SPI_Transmit和HAL_SPI_Receive的超时参数我分别设了 100ms 和 1000ms。写操作因为要等 MRAM 内部完成超时给长一点读操作很快100ms 足够。实际测试下来8MHz 时钟下写 256 字节大概需要 300us 左右读 256 字节大概 260us所以超时时间给得很充裕。3.3 掉电保护与数据完整性设计工业场景最怕的就是写一半掉电数据变成半新半旧。MRAM 虽然写入速度快但如果你一次写 256 字节写到第 128 字节的时候掉电那前 128 字节是新数据后 128 字节是旧数据整个数据块就不一致了。我的做法是加一个双缓冲加校验的机制。把存储区分成 A、B 两个区每个区前面加一个 4 字节的头部包含一个递增的序列号和一个 CRC16 校验值。写入的时候先写 B 区写完校验通过后再更新 A 区的指针。读取的时候比较 A、B 两个区的序列号取序列号大的那个如果 CRC 校验失败就取另一个。typedef struct { uint32_t seq; uint16_t crc; uint16_t len; uint8_t data[250]; } DataBlock; void SafeWrite(uint8_t *data, uint16_t len) { static uint32_t seq 0; DataBlock blk; blk.seq seq; blk.len len; memcpy(blk.data, data, len); blk.crc CRC16((uint8_t*)blk, sizeof(blk) - 2); // 交替写入 A/B 区 uint32_t addr (seq % 2) ? ADDR_A : ADDR_B; MRAM_Write(addr, (uint8_t*)blk, sizeof(blk)); }这个机制的好处是即使写入过程中掉电最多丢失最后一次写入的数据之前的数据始终是完整可用的。CRC16 我用的就是标准的 CCITT 多项式 0x1021计算速度在 M0 上大概 10us 左右完全可以接受。4. 调试过程中踩过的坑与排查方法4.1 SPI 通信失败的第一排查顺序SPI 调不通是嵌入式开发的家常便饭我总结了一个排查顺序按这个顺序走基本能定位 90% 的问题。第一步用示波器或者逻辑分析仪抓 CS、SCK、MOSI、MISO 四根线。先看 CS 有没有正常拉低拉高再看 SCK 有没有时钟输出再看 MOSI 上的数据是不是你期望的。如果 CS 没动检查 GPIO 配置和代码逻辑如果 SCK 没输出检查 SPI 外设时钟使能和 SPI 使能位。第二步确认 SPI 模式。MRAM 支持模式 0 和模式 3如果你配成了模式 1 或模式 2数据肯定错。用逻辑分析仪解码的时候把解码模式设成对应的 CPOL/CPHA看看解出来的数据对不对。第三步检查 MISO 有没有数据返回。如果 MISO 一直是高电平或者低电平可能是 MRAM 没供电、CS 没接对、或者 MISO 引脚配置错了。我遇到过 PA6 被其他外设占用的情况查了半天才发现是引脚冲突。第四步降低 SPI 时钟。如果高速下不稳定降到 1MHz 试试。1MHz 能通说明是信号完整性问题检查走线、串阻、去耦电容。4.2 写入后读出来全是 0xFF 或 0x00这个问题我遇到过两次原因不一样。第一次是 WREN 命令没有发MRAM 处于写保护状态写入操作被忽略了。第二次是 CS 拉高的时机不对数据还没完全移出就把 CS 拉高了导致写入不完整。排查方法很简单写完之后立刻读回来如果读出来是 0xFF说明写入根本没生效如果读出来是部分正确部分错误说明是时序问题。对于时序问题可以在 CS 拉高之前加一个几微秒的延时确保最后一个时钟沿已经完成。HAL 库的HAL_SPI_Transmit返回之后实际上最后一个字节已经移出但为了保险我在 CS 拉高之前加了一个__NOP()空操作实测下来再没出现过写入不完整的情况。4.3 常见问题速查表现象可能原因排查方法解决方法读出来全是 0xFFMISO 没接好或 MRAM 没供电测 MRAM VDD 电压检查供电和 MISO 连线写入后读出来不变WREN 没发或 CS 时序不对抓 CS 和 SCK 波形补发 WREN调整 CS 时序高速下偶发误码信号完整性差降速测试加串阻缩短走线加去耦数据半新半旧写入过程掉电检查电源加双缓冲和 CRC 校验SPI 完全无响应引脚配置错误查 GPIO 和 SPI 初始化检查引脚复用和时钟使能4.4 几个容易被忽略的实操心得第一个心得MRAM 的 WP 引脚虽然可以拉高禁用写保护但在调试阶段我建议用 GPIO 控制这样你可以在软件里动态开启写保护防止程序跑飞误写数据。等产品定型了再决定是拉高还是保留 GPIO 控制。第二个心得STM32L021K4 的 SPI 在停止模式下会关闭如果你用 STOP 模式做低功耗唤醒之后要重新初始化 SPI。我一开始没注意这个唤醒后读 MRAM 一直失败后来在唤醒回调里加了 SPI 重新初始化就好了。第三个心得MRAM 的写入寿命虽然是无限的但 SPI 接口的引脚是有寿命的不过这个寿命是百万次级别的对于正常使用来说完全够。真正需要注意的是频繁的 SPI 事务会增加 MCU 的功耗如果你做电池设备尽量攒一批数据再写不要每来一个数据就写一次。第四个心得用逻辑分析仪调试 SPI 的时候采样率至少要设到时钟频率的 10 倍以上。8MHz 的 SPI 时钟逻辑分析仪采样率至少要 80MHz否则解出来的波形会失真误导你的判断。我一开始用 24MHz 采样率抓 8MHz SPI解出来的数据全是乱的换了 100MHz 采样率才看到真实波形。5. 性能实测与场景扩展5.1 读写速度与功耗实测数据我在实验室用 STM32L021K4 加 MR25H40CDF 做了一组实测SPI 时钟 8MHz室温 25 度供电 3.3V。写入 256 字节耗时约 310us读取 256 字节耗时约 265us。对比 EEPROM 的写入速度同样 256 字节EEPROM 需要 5ms 到 10msMRAM 快了将近 20 倍。功耗方面MRAM 写入时电流大约 3mA读取时大约 2mA待机时 10uA 左右。STM32L021K4 在运行模式下大约 1.5mA停止模式下 0.5uA。整个系统在每 100ms 写入一次的工作模式下平均电流大约 200uA用 2000mAh 的电池可以撑一年多加上太阳能板补电实际续航可以做到 3 年以上。5.2 这套方案还能用在哪些场景除了电力监测终端这套 MRAM 加低功耗 MCU 的组合还适合很多场景。比如工业数据记录仪需要频繁记录传感器数据且不能丢比如智能仪表需要掉电保存累计用量比如汽车电子需要记录故障码和运行参数比如医疗设备需要保存患者数据和设备配置。MRAM 的另一个优势是抗辐射和抗磁场干扰能力比 Flash 强所以在一些特殊工业环境下也有优势。不过要注意MRAM 虽然抗磁场但强磁场下数据还是可能翻转如果你的应用场景有强磁场比如靠近电机或者变压器建议加磁屏蔽或者做数据冗余。5.3 后续可以优化的方向如果你想让这套方案更完善有几个方向可以继续做。第一加一个文件系统层把 MRAM 当成一个小型磁盘来管理支持文件的创建、删除、读写这样上层应用就不用关心底层地址了。第二加一个磨损均衡算法虽然 MRAM 寿命无限但均衡一下总没坏处而且可以让数据分布更均匀。第三加一个掉电检测电路用比较器监测 VDD一旦低于阈值立刻触发紧急写入把关键数据保存到 MRAM。我个人在实际操作中的体会是MRAM 这颗料最大的价值不在于速度快而在于省心。你不用再纠结擦写寿命、不用再等写入完成、不用再担心掉电丢数据用起来就跟普通 RAM 一样随意。对于工业嵌入式项目来说这种省心带来的开发效率提升和现场故障率降低远比省那几毛钱的 BOM 成本更有价值。
返回列表