ARTICLE DETAIL

资讯详情

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

STM32H743驱动FM25CL64铁电存储器:SPI、DMA与Cache踩坑实录

STM32H743驱动FM25CL64铁电存储器:SPI、DMA与Cache踩坑实录 从抽屉里翻出一片 FM25CL64 的时候我以为是十分钟就能搞定的活手头这块 STM32H743 板子已经在跑 LVGL 和 JPEG 解码显示侧顺顺当当现在无非是加个 SPI 铁电存储器存参数。结果从傍晚折腾到凌晨两点读回来的数据不是 0xFF 就是 0x00偶尔写进去了重启又不见。排查到最后发现问题根本不在 FM25CL64 本身而是我拿 F4 时代的 SPI 驱动思路硬套到 H7 上再加上对铁电存储器的理解还停留在“高级 EEPROM”的层面。这篇踩坑记录写给所有准备在 STM32H743 上接 FM25CL64 的朋友也顺便把 H7 上 SPI、DMA、Cache 这几个容易翻车的地方一起捋清楚。1. FM25CL64 到底是个什么东西1.1 一张表看懂 FRAM 和 EEPROM/Flash 的区别FM25CL64 是 Cypress现在归 Infineon出的一款 SPI 接口铁电存储器容量 64Kbit也就是 8K 字节。单看容量和封装它和常见的 24C64 EEPROM、W25Q 系列 Flash 很像但内部原理完全不同。我一开始就吃了“想当然”的亏所以先把三者的差别列出来。维度FM25CL64 铁电24Cxx EEPROMW25Qxx NOR Flash容量64Kbit / 8KB64Kbit / 8KB常见 8MB 起擦除不需要擦除不需要擦除必须先擦除按扇区/块写入粒度任意字节、任意长度按页写跨页要处理按页编程受页大小限制写入等待无总线周期内完成约 5ms 轮询 ACK页编程几十微秒到几毫秒擦写寿命10^10 次10^6 次量级10^5 次量级掉电保持10 年以上100 年左右10~20 年最核心的区别是FM25CL64 写入不需要等待。这一点让它的驱动代码可以和 Flash/EEPROM 完全不同也是这次踩坑的第一个根源。如果你按照 Flash 的习惯写完一个字节或一块数据后去轮询状态寄存器、等 busy 位、甚至延时几毫秒虽然不会出错但完全浪费了这颗芯片的价值。1.2 铁电存储器的“瞬写”原理FRAM 靠的是铁电晶体材料的电滞回线。写入数据时铁电晶体的极化方向在电场作用下翻转这个物理过程非常快并且不需要像 Flash 那样靠电荷泵产生高压、也不需要先擦除再写。厂家喜欢叫它“非易失性 RAM”正是因为它在总线周期内就能完成写入掉电后又不会丢数据。放到编程层面你可以把 FM25CL64 理解成一颗带 SPI 接口的 SRAMCS 拉低发命令发地址传数据CS 拉高数据就存进去了。没有页边界没有擦除周期没有写使能前置流程。我建议把它当作“SPI 接口的 RAM”来写代码而不是“EEPROM”。不过有两点要特别注意第一FM25CL64 的地址只有 13 位有效地址范围 0x0000~0x1FFF超出部分会回卷第二它没有页的概念但你连续写超过 8192 字节时地址会从 0 重新开始继续覆盖前面的数据。很多“写不进去”的怪现象其实就是地址越界回卷后数据被覆盖了。2. 先把 H7 的 SPI 外设这些坑踩平2.1 时钟树APB2 的 100MHz 和芯片的 40MHz 上限STM32H743 跑 480MHz 时APB1 和 APB2 默认都是 100MHz而 SPI1 挂在 APB2 上。FM25CL64G 的最高 SPI 时钟是 40MHz所以分频系数至少要到 /4也就是 25MHz。如果你照搬 F4 的配置上来就设 SPI_BAUDRATEPRESCALER_2SPI 时钟就是 50MHz已经超过芯片规格了。这里有个比较隐蔽的点H743 的 SPI 外设时钟源不一定是 PCLK它可以从 PCLK1/PCLK2、PLL1Q、PLL2P 等时钟源中选择。CubeMX 默认使用 PCLK如果 PCLK2100MHzSPI1 的时钟分频只能是 2 的幂没法配出 2.5 或 3 这种系数所以低分频档位下很容易踩到 50MHz 超频。想要 SPI 跑满 40MHz得自己给 SPI123 选择 PLL1Q 之类的时钟源并让 PLL1Q 输出 80MHz然后 /2 分频。RCC_PeriphCLKInitTypeDef PeriphClkInit {0}; PeriphClkInit.PeriphClockSelection RCC_PERIPHCLK_SPI1; PeriphClkInit.Spi123ClockSelection RCC_SPI123CLKSOURCE_PLL1Q; HAL_RCCEx_PeriphCLKConfig(PeriphClkInit);注意这段代码改的是“时钟源选择”PLL1Q 具体是多少还得在 RCC 的 PLL 配置里确认。我调试时图省事直接用了 PCLK2 /4 分频也就是 25MHz稳定性完全够。先把功能跑通再考虑去冲 40MHz 更合理。2.2 硬件 NSS 和软件 NSS我建议直接软件 NSSH7 的 SPI 支持 NSS 输出、NSS 脉冲等一堆高级玩法但在驱动 FM25CL64 这种简单器件时这些功能反而容易添乱。FM25CL64 的时序要求很简单CS 拉低开始一次事务CS 拉高结束一次事务。中间除了命令、地址、数据以外没有其他小动作。如果使用硬件 NSSCS 的拉高拉低由 SPI 外设自动控制看起来省事可一旦 SPI 的 FIFO、EOT 标志、DMA 完成时机没配合好CS 可能提前被释放最后一个字节就不完整。我在 F4 上用过硬件 NSS印象一般在 H7 上干脆放弃了这种配置。CubeMX 里把 NSS 设为 SPI_NSS_SOFTCS 引脚用一个普通 GPIO 控制代码里手动拉低拉高逻辑一目了然排查问题也方便。#define FRAM_CS_LOW() HAL_GPIO_WritePin(FRAM_CS_GPIO_Port, FRAM_CS_Pin, GPIO_PIN_RESET) #define FRAM_CS_HIGH() HAL_GPIO_WritePin(FRAM_CS_GPIO_Port, FRAM_CS_Pin, GPIO_PIN_SET)2.3 H7 的“传输完成”标志位和 F4 不一样这是我在寄存器层面踩得最深的一个坑。F1/F4 时代判断 SPI 发送完成大家习惯等 BSY 标志清零但 H7 的 SPI 带了 FIFOBSY 的含义和时序都有变化。我拿旧代码改过来的第一版驱动就是在 SPI 发送完最后一个字节后不久就拉高了 CS结果最后一个字节经常丢。H7 上更可靠的传输完成标志是 EOTEnd Of Transfer。如果你用 HAL 库的阻塞接口HAL_SPI_Transmit、HAL_SPI_TransmitReceive 内部会等待 EOT所以安全性有保障但如果你是从别的平台移植寄存器版驱动千万别照搬“等 TXE 和 BSY”那套否则就得体会“最后一个字节随机消失”的玄学。实际上我后来干脆只封装了一个简单的底层字节发送函数所有 FM25CL64 操作都用 HAL 库完成代码短、逻辑清晰也避免了踩 H7 底层标志位的坑。对大多数应用来说HAL 库的性能开销完全可接受尤其是驱动这种本来就不需要高速刷写的存储芯片。3. 驱动代码从初始化到能读能写下面给出一份我在 H7 上实际验证过的驱动基于 CubeMX 生成的 SPI1软件 NSS。硬件连接如下信号STM32H743 引脚备注SPI1_SCKPA5AF5SPI1_MISOPA6AF5SPI1_MOSIPA7AF5FRAM_CSPA4普通 GPIO 输出CubeMX 中 SPI1 配置为全双工主机、8 位数据、Motorola 格式、CPOLLow、CPHA1Edge也就是 SPI Mode 0。预分频先选 /4得到 25MHz。GPIO Speed 设到 HIGH 或 VERY_HIGH低速档位在 25MHz 下波形会明显变差误码是随机出现的很难查。3.1 SPI 初始化代码示例void MX_SPI1_Init(void) { 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; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; hspi1.Init.TIMode SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE; hspi1.Init.CRCPolynomial 7; hspi1.Init.CRCLength SPI_CRC_LENGTH_8BIT; hspi1.Init.NSSPMode SPI_NSS_PULSE_DISABLE; if (HAL_SPI_Init(hspi1) ! HAL_OK) { Error_Handler(); } }GPIO 配置这里不多说CubeMX 里把 PA5/PA6/PA7 设为 SPI1 的 AF5PA4 设为 GPIO 输出初始电平为高。3.2 阻塞版读写函数FM25CL64 的命令不多最常用的就是 READ 0x03 和 WRITE 0x02。下面是读写函数命令头 3 个字节读操作先发命令和地址再连续接收写操作先发命令和地址再连续发送数据。#define FRAM_CMD_READ 0x03 #define FRAM_CMD_WRITE 0x02 #define FRAM_CMD_RDSR 0x05 #define FRAM_CMD_WRSR 0x01 #define FRAM_CMD_RDID 0x9F void fram_read(uint16_t addr, uint8_t *buf, uint16_t len) { uint8_t header[3]; header[0] FRAM_CMD_READ; header[1] (addr 8) 0xFF; header[2] addr 0xFF; FM25CL64_CS_LOW(); HAL_SPI_Transmit(hspi1, header, 3, 100); HAL_SPI_Receive(hspi1, buf, len, 100); FM25CL64_CS_HIGH(); } void fram_write(uint16_t addr, const uint8_t *buf, uint16_t len) { uint8_t header[3]; header[0] FRAM_CMD_WRITE; header[1] (addr 8) 0xFF; header[2] addr 0xFF; FM25CL64_CS_LOW(); HAL_SPI_Transmit(hspi1, header, 3, 100); HAL_SPI_Transmit(hspi1, (uint8_t *)buf, len, 100); FM25CL64_CS_HIGH(); }这里有三点补充说明。第一HAL_SPI_Transmit 和 HAL_SPI_Receive 连续使用时CS 一直保持在低电平两次传输之间 SCK 只是停了一下。FM25CL64 是纯同步器件只要 CS 没拉高它不关心时钟间不连续所以这种做法没问题。第二地址用 uint16_t 传进来但实际有效地址只有 13 位也就是 0x0000~0x1FFF。要防止调用方传入超过 8191 的地址尤其在做循环日志、地址累加这类功能时超过边界会回卷覆盖旧数据。我建议调用前主动做一次限制或者分两段处理。第三写数据前不需要发 WREN 使能指令。FM25CL64 手册里写得很明确WRITE 命令本身不需要先写使能。我也测试过发了 WREN 再写也不会出错因为这颗芯片兼容了 EEPROM 那套命令格式但既然不需要代码里就不写减少一次 SPI 事务。3.3 读 ID 和基本自检想验证 SPI 通路是否正常可以用 0x9F 命令读 ID。但注意FM25CL64 的 RDID 并不像 W25Q 那样有统一且稳定的 JEDEC ID不同批次、不同供应商出来的值可能不一样。网上有人读到 04 23 00 01也有人读到完全不同的值。所以不要把 ID 校验写死成“必须等于某个数”一旦换一批芯片程序就卡死了。void fram_read_id(void) { uint8_t tx FRAM_CMD_RDID; uint8_t id[4] {0}; FM25CL64_CS_LOW(); HAL_SPI_Transmit(hspi1, tx, 1, 100); HAL_SPI_Receive(hspi1, id, 4, 100); FM25CL64_CS_HIGH(); printf(FRAM ID: %02X %02X %02X %02X\r\n, id[0], id[1], id[2], id[3]); }更靠谱的自检方案是写入一个固定 pattern比如 0x5A 0xA5 0x55 0xAA读回来对比。能通过这个测试基本就能排除接线和时序问题。3.4 升级 DMA 版本之前先解决 D-Cache阻塞版读写函数适合小数据量比如存参数、存状态。如果想用它做数据记录频繁搬大量数据就得考虑 DMA 版本。但 H7 上 DMA 不能直接“拿来就用”最大的坑是 D-Cache。H743 内部 D1 域的 AXI SRAM 默认带 Cache如果开启了 D-CacheDMA 把数据写进内存后CPU 可能读到的是 Cache 里的旧数据反过来CPU 写了发送缓冲区如果数据还躺在 Cache 里没回写DMA 从内存读到的是旧数据。所以 DMA 接收完成后要 InvalidateDMA 发送前要 Clean。// 发送前把缓存中的数据写回内存 SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, len); // DMA 接收完成后让缓存失效强制 CPU 从内存重新读取 SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, len);更省心的做法是在 MPU 配置里把 DMA 缓冲区所在的 SRAM 区域设为 Non-cacheable。这样就不需要手动 Clean/Invalidate但要注意 Non-cacheable 区域不要放性能敏感的代码或数据。另外 H7 的 DMA1/DMA2 能不能访问某些 RAM 块还真有讲究。比如 DTCM 是 Cortex-M7 直连的紧耦合内存DMA 通常碰不到它。如果你一开始把 DMA 缓冲区定义在 DTCM可能会出现 DMA 传输永远不完成的神奇现象。这类问题最容易出现在 CubeMX 生成的链接脚本默认把变量放到 DTCM 的情况下。我自己的经验是先用阻塞版把功能跑通然后如果确实需要提高吞吐再规划 DMA 缓冲区和 Cache/MPU 方案。不要一上来就把阻塞、DMA、Cache 三个变量搅在一起出了问题根本分不清是谁的锅。4. 踩坑实录症状、原因、解决办法4.1 故障现象速查表这一节是我最想分享的。调试 FRAM 的过程中我先后遇到过下面这些现象很多问题排查到最后原因都写在数据手册的角落里。故障现象可能原因解决与排查方向读回全为 0xFFMISO 虚焊、引脚复用错误、CS 一直高示波器量 CS/MISO检查 GPIO AF 配置读回全为 0x00MOSI 没接好、SPI 时钟配置异常检查 MOSI 波形确认 SPI Mode 0数据错位写 1 读 2CPOL/CPHA 配置不对确认是 Mode 0 还是 Mode 3统一 CubeMX 和手册能读不能写读回旧值WP# 引脚悬空/拉低、状态寄存器写保护WP# 上拉到 VCC检查状态寄存器高速时偶尔出错SPI 时钟超频、GPIO 速度档位过低降低分频GPIO Speed 拉高最后一个字节丢失让 CS 拉高太早数据还在 FIFO用 HAL 库等 EOT或检查寄存器版 BSY 判断程序跑起来后随机写错中断里调用阻塞 SPI事务被撕裂加互斥保护不要在中断里做长事务上电后第一次读写异常CS 上电默认电平不确定初始化时先拉高 CS硬件加 10k 上拉4.2 典型误区把 RDID 当作强校验我第一次调通 SPI 后兴冲冲地拿 0x9F 命令去读 ID准备写一个“读到正确 ID 才继续”的初始化流程。结果发现手头上几片 FM25CL64 读出来的 ID 竟然不完全一样这就很尴尬了。铁电存储器的 RDID 指令存在但它的返回值和厂商、批次、内部版本号都可能有关网上能搜到的例程也经常是直接打印很少有人把它作为严格判据。如果你只是想验证“芯片在不在、SPI 通不通”用 pattern 回读更靠谱。随手找几个字节写进去再读出来比纠结 ID 数值更省时间。如果产品量产时必须要校验芯片型号建议在软件里做成“打印但不拦截”的方式至少别让一板 ID 差异导致整机无法开机。4.3 WP# 引脚和上电时序这类硬件坑FM25CL64 的写保护和 EEPROM 不太一样。它有一个 WP# 引脚结合状态寄存器里的 WPEN 位来实现整片写保护。如果 WPEN 为 1 且 WP# 被拉低写操作会被拒绝但读操作正常表现就是“能读不能写”。我当时换了一根杜邦线把 WP# 接到 3.3V问题立刻消失。所以硬件设计上WP# 直接接 VCC 或者用 10k 电阻上拉是最省事的不要让它悬空。悬空引脚在某些环境下容易被干扰拉低造成偶发性写保护这类问题最难查。上电时序也不能忽略。H7 复位后GPIO 在初始化完成前是浮空输入如果这时 CS 被外部干扰拉到低电平SPI 引脚上又刚好有脉冲FRAM 可能收到一段无效时钟。虽然概率不高但我在做掉电保存功能时吃过亏。解决办法很朴素初始化代码一开始就把 CS 对应的 GPIO 配置为推挽输出并输出高电平然后再初始化 SPI 外设。硬件上在 CS、SCK 加 10k 上拉也能提高稳性。4.4 中断里调用 SPI 的问题H7 上的系统里如果还有 LVGL、JPEG 解码、网络协议栈这些任务SPI 总线就必须考虑并发访问。我在调试日志功能时一开始为了省事直接在定时器中断里调用 fram_write结果系统运行一段时间后偶发写入错误查了半天才发现是另一路中断优先级更高把我的 SPI 事务切成了一段一段CS 时序被拉长不说数据也可能在 FIFO 中被打乱。后来我在驱动层加了一个简单的 busy 标志和互斥锁所有读写操作都从这里过外部模块不能在中断里直接调用。FRAM 本身足够快一个完整事务也就几微秒到几十微秒完全没必要为了“省时间”在中断里做。如果有人必须要在中断上下文里写至少保证 CS 的拉低拉高和 SPI 数据发送在同一个不可被抢占的临界区内。5. 这些坑踩完之后能用 FM25CL64 做什么5.1 掉电保存现场读取恢复FM25CL64 最典型的应用方向是掉电保存。H743 内部有 PVD可编程电压检测器可以配置一个电压阈值当供电电压掉到这个阈值以下时触发中断。我在一个数据采集项目里是这么用的正常运行状态一直记录在 RAM 里PVD 中断触发后把一组关键变量十几到几十字节一次性写入 FM25CL64然后立刻进入低功耗或复位流程。FRAM 的优势在这里体现得很明显写入一个几十字节的结构体只需要一次 SPI 事务按 25MHz 算加上命令和地址总耗时不超过 10 微秒。如果是 EEPROM写一页数据可能需要等待几毫秒掉电瞬间那点时间根本不够。要注意的是PVD 阈值要选得比 FRAM 最低工作电压高留出足够余量。比如 3.3V 供电FRAM 最低工作电压一般 2.7VPVD 阈值设到 2.9V 左右触发中断后到电压跌破 2.7V 之间还有一段窗口足够完成写入。5.2 高频状态记录寿命真的够吗FRAM 的寿命是 10^10 次写入很多人一听觉得“永远写不坏”但实际还是要算账的。写入频率理论使用寿命1 次/秒约 317 年10 次/秒约 31.7 年100 次/秒约 3.17 年1000 次/秒约 115 天如果只是每秒记录几次运行状态、故障码、开机次数用 FRAM 基本是终身免维护的。但如果是高频数据采集比如每秒写上千条日志就得考虑寿命问题或者选更大容量的 FRAM 型号比如 FM25V10 这类 1Mbit 产品。FM25CL64 的 8KB 更适合做参数型存储不太适合做海量日志。不过有一个好处值得提FRAM 不需要磨损均衡。传统 EEPROM/Flash 做频繁写入时还得想办法让写均匀分布到不同地址FRAM 的寿命足够高很多场景直接固定地址反复写就行代码能省不少事。当然如果写入频率特别高还是建议做循环覆盖写顺便也能解决地址回卷带来的覆盖问题。5.3 和 H743 的配合一个大缓存一个小仓库最后说点切身体会。H743 本身资源非常强大 RAM、高主频、外设丰富适合跑 LVGL、JPEG 解码这类重度显示负载。但显示任务越重系统越需要一个可靠的“非易失参数仓库”。FM25CL64 作为一个小容量仓库刚好补上掉电丢失这块短板校准参数、设备序列号、运行模式、最近一次状态这些数据分开存放每次开机读一遍修改时写一写完全够用。我这次踩坑后把驱动代码整理成了独立模块上层只暴露 fram_read 和 fram_write 两个函数。以后换其他 SPI FRAM只要改底层命令和地址位宽上层逻辑不用动。这也是踩坑后最大的收获外设本身的坑是可以靠代码结构和经验来避免的。我个人在实际调试中的体会是遇到“明明很简单却搞不定”的芯片先别怀疑芯片坏把时钟树、GPIO 复用、芯片手册的命令时序、H7 特有的缓存和 DMA 行为这几样逐个确认一遍比反复改代码盲试高效得多。最后再分享一个小技巧如果读写结果忽好忽坏先把 SPI 时钟降到 1MHz 跑一遍排除速率因素后再逐步升频。这个问题定位方式帮我省了至少一个晚上希望你用不上。
返回列表