ARTICLE DETAIL

资讯详情

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

STM32H750实战:QSPI接口驱动W25Q128并挂载FATFS实现文件读写

STM32H750实战:QSPI接口驱动W25Q128并挂载FATFS实现文件读写 简介面向使用STM32CubeIDE进行嵌入式开发的工程师这份STM32H750VBT6示例工程提供了一套完整的QSPI Flash文件读写方案。工程基于HAL库实现QSPI总线驱动并集成FATS文件系统演示如何对W25Q系列Flash进行文件级读写QSPI接口吞吐高配合文件系统可显著简化大容量Flash的数据组织与访问适合需要外扩存储、记录日志或管理固件的应用场景。压缩包体积仅15.93MB共293个文件涵盖77个头文件、36个C源文件、编译目标文件、链接脚本及CubeIDE工程配置如.ioc、.cproject等类型其中QSPI底层驱动、FATFS中间件和用户示例代码完整收录并附带文本说明和JPG接线示意图便于对照学习。目前已有507人学习下载。整个工程目录结构分明导入STM32CubeIDE后即可直接编译运行省去繁琐的引脚配置时间开发者既能深读代码理解QSPI时序与FATFS挂载逻辑也能将其作为基础模板快速迁移到其他STM32H7系列产品中兼顾学习参考与工程复用价值。1. 这不是一根普通的 SPI FlashSTM32H750 用 QSPIFATS 把 W25QXX 变成可读写 U 盘STM32H750VBT6 的 128KB 内部 Flash 在带屏产品里是真的不够用字库、日志、配置备份、升级包放哪里都别扭。QSPI 外设接一颗 W25Q128再让 FATS 文件系统跑在这颗 Flash 上外部存储就能像 U 盘一样读文件、写日志、断电不丢数据。这套工程就是 STM32CubeIDE HAL 库环境下的完整实现代码里把 HAL_QSPI、FATFS 以及 TIM、I2C、UART、RCC、DMA 这些驱动都放进同一个骨架里下载后改下引脚和 Flash 容量就能用。不是简单贴个例程而是把 QSPI 读时序、W25QXX 状态寄存器、diskio 底层这几个容易翻车的位置都清一遍。2. 为什么选 QSPI 而不是普通 SPI从 4KB 扇区擦除说起2.1 QSPI 与 SPI/DSPI 的时序差异FATFS 在遍历目录和追加日志时会产生大量小数据块读取这时候普通 SPI 的 1-bit 数据线会成为明显的瓶颈。W25Q128 手册里的0xEBFast Read Quad I/O 命令命令和地址阶段用单线数据阶段切到四线同样读 256 字节时钟数比普通0x03读命令要少近四分之三。这也是 QSPI 外设和普通 SPI 外设最本质的区别QUADSPI 可以在同一个指令帧内切换 Lane 数而不是像软件模拟那样频繁翻转 IO。读命令命令/地址/数据线宽256B 页读取大致时钟数0x03 Read1-1-1约 20480x0B Fast Read1-1-1约 2064含 dummy0x6B Quad Output1-1-4约 5260xEB Quad I/O1-1-4约 520含 dummy实际项目中经常看到有人把 DSPI 和 QSPI 搞混。STM32H7 的 QUADSPI 外设里有一个 DualFlash 选项开启后会把两颗 Flash 并联一个传高字节一个传低字节整体数据宽度翻倍通常叫 Dual SPI 或 DSPI Flash 接口而 W25QXX 自身的 Quad 模式是指单颗 Flash 的四线数据读取。两者不是同一层的东西只挂一颗 W25Q128 时务必把DualFlash改为QSPI_DUALFLASH_DISABLE否则两个设备会互相拉死数据线读 ID 都读不出来。2.2 W25QXX 和 NAND Flash 的选型边界很多人第一反应是为什么不用 NAND毕竟同容量 NAND 便宜得多。但 NAND 的读写单位是页坏块管理和 ECC 要自己做FATFS 不负责 FTL 层除非在 diskio 里再包一层闪存转换层否则文件系统在 NAND 上很容易因为擦写不均匀直接报废。W25Q 系列属于 NOR Flash4KB 小扇区擦除、字节级随机读正好匹配 FATS 的按扇区逻辑16MB 容量对 W25Q128 来说单颗芯片就能同时做代码 XIP 和文件数据区。另一个实际原因在启动链路里。NOR Flash 支持线性地址读取H750 的 QUADSPI 把外部 Flash 映射到0x90000000之后可以直接从里面取指令NAND 必须先搬代码到 SRAM 才能执行。所以如果这块存储既要放 bootloader、又要放文件系统SPI NOR 几乎是唯一省事的选择。选 Winbond W25Q 系列还有一层理由是文档和生态都成熟QSPI dummy cycle、状态寄存器定义在一众国产 NOR 里算非常透明的。2.3 FATFS 扇区大小与 4KB 对齐FATFS 默认把扇区定为 512 字节但 W25Q128 的物理擦除扇区是 4096 字节。如果保持默认会出现一个簇只占物理扇区四分之一的情况文件碎片率高且每次逻辑写入都要先读改写整块写放大 4 倍。最稳妥的做法是直接把 ffconf.h 里的最小和最大扇区都改为 4096让文件系统扇区等于 Flash 物理扇区。/* ffconf.h */ #define FF_MIN_SS 4096 #define FF_MAX_SS 4096 #define FF_USE_MKFS 1 #define FF_FS_RPATH 2参数说明FF_MIN_SS和FF_MAX_SS决定 FATFS 内部缓存和扇区计算单位统一设成 4096 后每个 FAT 扇区对应 Flash 一个扇区。FF_USE_MKFS开启格式化功能第一次使用新 Flash 时调用f_mkfs才能生成 FAT16 引导区。FF_FS_RPATH开启相对路径支持日志模块按0:/log/2025这类目录写文件时能少写很多重复代码。第一次挂载时如果f_mount返回FR_NO_FILESYSTEM要先格式化static FATFS fs; static uint8_t work[4096]; if (f_mount(fs, Q:, 1) ! FR_OK) { f_mkfs(Q:, FM_FAT16, 4096, work, sizeof(work)); f_mount(fs, Q:, 1); }f_mkfs的第二个参数FM_FAT16是故意选的。16MB 的 W25Q128 用 FAT16簇大小可以直接对齐到 4KB 扇区FAT32 虽然也能建但对齐粒度更粗日志类小文件频繁追加时会多浪费很多空间。work缓冲区必须是 CCM 或普通 SRAM 里的数组不能定义在函数内部让它进栈栈不够大时格式化会直接 HardFault。3. HAL 库的 QSPI 初始化从 GPIO 到命令帧3.1 引脚复用配置D2/D3 必须上拉QUADSPI 和普通 SPI 不一样数据脚 D2、D3 在 W25QXX 里和 WP#、HOLD# 复用。如果初始化时把这两根引脚配置成浮空输入一旦电平被干扰拉低Flash 会进入 hold 状态后面的命令全部超时。我的习惯是 D0-D3 全部带上拉CS 也带上拉CLK 不带。void HAL_QSPI_MspInit(QSPI_HandleTypeDef *hqspi) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_QSPI_CLK_ENABLE(); GPIO_InitStruct.Pin QSPI_CLK_PIN | QSPI_CS_PIN; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_VERY_HIGH; GPIO_InitStruct.Alternate GPIO_AF10_QUADSPI; HAL_GPIO_Init(QSPI_CLK_PORT, GPIO_InitStruct); GPIO_InitStruct.Pin QSPI_D0_PIN | QSPI_D1_PIN | QSPI_D2_PIN | QSPI_D3_PIN; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(QSPI_D0_PORT, GPIO_InitStruct); }QSPI_CLK_PIN等宏在具体板子上会有差异烧录前一定对照原理图确认。比如有的开发板把 CLK 放在 PA6有的放在 PF10Alternate Function 都是 AF10如果复用错引脚读出来的 flash ID 通常是全0xFF或者全0x00这时候最先要查的就是引脚表而不是怀疑芯片坏了。速度等级选VERY_HIGH因为 QSPI 时钟通常跑到 100MHz 以上默认的 moderate 档位会导致传输毛刺。3.2 通信结构体里的 FlashSize 陷阱初始化结构体里最容易被复制错的就是FlashSize参数。它不是字节数而是地址位宽减一比如 W25Q128 是 16MB需要 24 根地址线所以 FlashSize 填 23写成POSITION_VAL(0x1000000) - 1。如果填成 15HAL 库会在发送地址时只发 16 位超出 64KB 的内容全部读到同一块区域。QSPI_HandleTypeDef hqspi; hqspi.Instance QUADSPI; hqspi.Init.ClockPrescaler 2; hqspi.Init.FifoThreshold 4; hqspi.Init.SampleShifting QSPI_SAMPLE_SHIFTING_HALFCLK; hqspi.Init.FlashSize POSITION_VAL(0x1000000) - 1; hqspi.Init.ChipSelectHighTime QSPI_CS_HIGH_TIME_5_CYCLE; hqspi.Init.ClockMode QSPI_CLOCK_MODE_0; hqspi.Init.FlashID QSPI_FLASH_ID_1; hqspi.Init.DualFlash QSPI_DUALFLASH_DISABLE; if (HAL_QSPI_Init(hqspi) ! HAL_OK) { Error_Handler(); }ClockPrescaler为 2 表示 QSPI 时钟 QSPI kernel clock / 2具体最终频率由时钟树决定。W25Q128 最高支持 133MHz但实际 H750 的 QUADSPI 跑太高后PCB 走线稍微长一点就会在时钟沿采样出错。SampleShifting选HALFCLK是让采样点移到时钟中间这个配置在大部分 4 层板上稳定双层板可以把翻到QSPI_SAMPLE_SHIFTING_NONE试试如果不加反而出错重点检查 CLK 信号和 D0-D3 的等长。ChipSelectHighTime关系到两次命令之间 CS 拉高的最短时长W25QXX 在快速连续命令之间需要至少几个时钟的高电平设成 5 cycle 能避免高速读写时状态机错乱。3.3 上电第一步JEDEC ID、Reset 和 QSPI 命令帧初始化完成后不要急着读写文件先发一条读 ID 命令确认链路通。下面这段代码用 HAL 库方式发送0x9F指令并读取 3 字节 JEDEC ID。static int qspi_read_jedec_id(QSPI_HandleTypeDef *h) { QSPI_CommandTypeDef cmd {0}; uint8_t id[3] {0}; cmd.Instruction 0x9F; cmd.AddressMode QSPI_ADDRESS_NONE; cmd.DummyCycles 0; cmd.DataMode QSPI_DATA_1_LINE; cmd.NbData 3; if (HAL_QSPI_Command(h, cmd, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) ! HAL_OK) { return -1; } if (HAL_QSPI_Receive(h, id, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) ! HAL_OK) { return -1; } return (id[0] 16) | (id[1] 8) | id[2]; }AddressMode和DataMode必须显式指定HAL 库不会自动推断。HAL_QSPI_Command只负责把命令、地址、dummy 周期发到总线上真正收发数据要靠后面的HAL_QSPI_Receive或HAL_QSPI_Transmit。返回0xEF4018就是 W25Q128如果读到0xFFFFFFFF先检查电源和引脚如果读到0xEF4017说明板子上焊的是 W25Q64容量宏需要跟着改。还有一个容易忽略的步骤是在初始化时对 Flash 做一次0x660x99软件复位。尤其是二手芯片或者之前跑过 QPI 模式的片子内部状态可能停留在四线指令下直接发0x9F会得不到正常响应。复位后再读 ID能避免很多“硬件明明正常但就是不通”的诡异问题。4. 让 FATFS 跑在 QSPI Flash 上diskio 改写实战4.1 块大小、扇区大小与换页边界W25Q 系列的存储层次很规律256 字节一页16 页组成一个 4KB 扇区16 个扇区组成一个 64KB 块。擦除操作最小单位是 4KB 扇区编程操作最小单位是 256 字节页但页编程之前必须处于已擦除状态。FATFS 的 diskio 接口看到的只是“扇区读写”并不知道底层的擦除周期所以 disk_write 里必须自己处理擦除。型号容量页大小扇区大小块大小W25Q324MB256B4KB64KBW25Q648MB256B4KB64KBW25Q12816MB256B4KB64KBW25Q25632MB256B4KB64KBFATFS 配置成 4KB 扇区后一个逻辑扇区正好对应 Flash 一个 4KB 物理扇区。但擦除块是 64KB也就是 16 个逻辑扇区一组。disk_ioctl里的GET_BLOCK_SIZE应该返回 16这样f_mkfs建文件系统时会把 FAT 表对齐到 64KB 边界后续写文件时文件数据簇不会跨擦除块减少一边读一边擦的惩罚。4.2 disk_read 与 disk_write 的完整实现disk_read 直接调用 Fast Read Quad I/O 命令按每 4096 字节一个逻辑扇区读取。注意count可能大于 1但为了简化这里用循环逐扇区读避免一次命令长度超过 FIFO 导致HAL_QSPI_RECEIVE超时。DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { QSPI_CommandTypeDef cmd {0}; if (pdrv ! DEV_FLASH) { return RES_PARERR; } for (UINT i 0; i count; i) { cmd.Instruction 0xEB; cmd.Address (sector i) * 4096; cmd.AddressSize QSPI_ADDRESS_24_BITS; cmd.DummyCycles 6; cmd.DataMode QSPI_DATA_4_LINES; cmd.NbData 4096; if (HAL_QSPI_Command(hqspi, cmd, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) ! HAL_OK) { return RES_ERROR; } if (HAL_QSPI_Receive(hqspi, buff i * 4096, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) ! HAL_OK) { return RES_ERROR; } } return RES_OK; }0xEB是四线读地址和数据阶段都走四线所以DummyCycles必须设置。W25Q128 在 100MHz 以下运行时 dummy cycle 为 6如果 QSPI 时钟调高后读出的数据发生移位第一步就是把DummyCycles改成 8同时检查SampleShifting。disk_write 比读麻烦得多必须先擦除再编程。下面这段代码每次处理一个 4KB 扇区先检查 WEL 写使能位再擦除扇区、等待 busy 清掉最后分 16 页写入。DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count) { QSPI_CommandTypeDef cmd {0}; uint8_t sr; if (pdrv ! DEV_FLASH) { return RES_PARERR; } for (UINT sec 0; sec count; sec) { uint32_t addr (sector sec) * 4096; qspi_write_enable(); sr qspi_read_status_register(1); if ((sr 0x02) 0) { return RES_WRPRT; } cmd.Instruction 0x20; cmd.Address addr; cmd.AddressSize QSPI_ADDRESS_24_BITS; cmd.DummyCycles 0; cmd.DataMode QSPI_DATA_NONE; cmd.NbData 0; if (HAL_QSPI_Command(hqspi, cmd, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) ! HAL_OK) { return RES_ERROR; } while (qspi_read_status_register(1) 0x01) { } for (int page 0; page 16; page) { qspi_write_enable(); cmd.Instruction 0x02; cmd.Address addr page * 256; cmd.AddressSize QSPI_ADDRESS_24_BITS; cmd.DummyCycles 0; cmd.DataMode QSPI_DATA_1_LINE; cmd.NbData 256; if (HAL_QSPI_Command(hqspi, cmd, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) ! HAL_OK) { return RES_ERROR; } if (HAL_QSPI_Transmit(hqspi, (uint8_t *)buff sec * 4096 page * 256, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) ! HAL_OK) { return RES_ERROR; } while (qspi_read_status_register(1) 0x01) { } } } return RES_OK; }qspi_write_enable本质是发送0x06写使能命令必须在每次擦除和每次页编程之前单独发一次。发送完0x06后立刻读状态寄存器 1检查 bit1 WEL 是否为 1如果为 0说明 WP# 引脚被拉低或者状态寄存器被硬件写保护了FATFS 的f_write会一直返回FR_DISK_ERR。CPU 主频较高时建议在每个 while 循环里加一个超时计数器否则 flash 异常时程序会卡死在等待 busy 位。4.3 GET_BLOCK_SIZE 与写保护导致的文件系统损坏disk_ioctl 里除了CTRL_SYNC最关键的是GET_BLOCK_SIZE。返回的块大小是“以当前逻辑扇区为单位”的擦除块大小所以 64KB 擦除块除以 4KB 扇区结果是 16。case GET_BLOCK_SIZE: *(WORD *)buff 16; return RES_OK;如果这里写成 1FATFS 会认为自己擦除块就是 4KB文件分配时会频繁跨越 64KB 物理擦除块。跨块更新文件时可能出现这样的情况FAT 表所在扇区只改了一部分但底层必须擦掉包含该扇区的整个 64KB 块擦写过程中断电FAT 表就残了。4KB 对齐只能保证逻辑扇区不跨 4KB 物理扇区但没办法保证不跨 64KB 物理擦除块所以GET_BLOCK_SIZE返回值直接影响掉电健壮性。4.4 FATFS 读文件时的底层缓冲对齐问题在 STM32H7 上如果启用了 D-CacheQSPI 的 FIFO 是 AHB 外设HAL_QSPI_Receive写入到普通 SRAM 数组时会出现 cache 不一致读出来的数据可能全是旧值。f_open 之后 f_read 第一包数据如果是 0xFF而直接读 ID 又是正常的大概率就是 buffer 没有对齐到 cache line。__attribute__((aligned(32))) static uint8_t file_buf[4096];定义文件缓冲区时用 32 字节对齐并在每次 disk_read 后面调用SCB_InvalidateDCache_by_Addr使对应缓存失效。如果项目用了 RTOSDMA 和 QSPI 共用时会更加隐蔽建议在代码里专门留一个对齐数组做中转不要直接在系统堆里动态申请。5. 烧录与调试从 Keil 的 Flash Loader 到 XIP 的最后一步5.1 处理flash download failed - target dll has been cancelled用 Keil 给 H750 下载外部 QSPI Flash 时最常遇到的就是这个报错。它不是因为目标板没连上而是 Keil 没有加载正确的 External Flash Loader。STM32H7 的内部 Flash 地址是0x08000000外部 QSPI Flash 映射地址是0x90000000Flash 算法文件不一样。排查按下面顺序来打开Options for Target - Utilities - Settings - Flash Download确认 Flash Loader 列表里有没有STM32H7x_QSPI或名称类似的算法。如果只有 STM32H750 内部 Flash 算法点击Add从 Keil 安装目录的FLASH文件夹里选择对应.FLM。确认调试器连接速度SWD 频率太高时算法文件自身都无法加载也会报这个 DLL 错误。用 STM32CubeProgrammer 的独立命令加载外部 Flash验证算法文件本身是否可用STM32_Programmer_CLI --connect portSWD \ --external-loader STM32H7x_QSPI.stldr \ --download app.hex--external-loader的参数是外部 Flash 描述文件路径在 STM32CubeProgrammer 安装目录ExternalLoader下。如果命令能正常下载说明目标板接线和时钟没问题问题就在 Keil 的算法加载器配置里。很多时候还要把 Keil 的Debug页里Download Options全勾上否则只是下载内部 Flash外部 QSPI 区域依然是空的。5.2 把代码放在0x90000000的 XIP 方式如果想让 H750 直接从 QSPI Flash 启动执行需要把链接脚本里的 ROM 起始地址改成0x90000000并在 system_stm32h7xx.c 的 SystemInit 里提前把 QUADSPI 切换到 Memory-Mapped 模式。FATFS 和 XIP 不能同时操作同一片区域初始化顺序一般是先挂载 FATFS、读取升级包、再切到 Memory-Mapped 模式运行应用代码。QSPI_MemoryMappedTypeDef memMap {0}; memMap.TimeOutActivation QSPI_TIMEOUT_COUNTER_DISABLE; memMap.TimeOutPeriod 0; memMap.DummyCycles 6; HAL_QSPI_MemoryMapped(hqspi, memMap); const uint8_t *app (const uint8_t *)0x90000000;进入 Memory-Mapped 后QUADSPI 的 FIFO 和命令控制器会被旁路CPU 访问0x90000000就像访问内部 Flash 一样但写命令不能再发送。所以在挂载 FATFS 期间千万不要切 Memory-Mapped否则disk_write会拿到一堆无响应状态并最终触发 HardFault。5.3 提升实际吞吐率QE 位、dummy cycle 和 DWT 测速W25QXX 出厂时 QE 位可能没有置位此时发送0xEB四线读命令Flash 不会切换数据线只会把 D2/D3 当作普通 IO 拉低或拉高读回来的内容错位。建议在初始化时把状态寄存器 2 的 QE 位强制置 1。void qspi_enable_quad(void) { uint8_t sr2 0x02; qspi_write_enable(); qspi_write_status_register_2(sr2); while (qspi_read_status_register(1) 0x01) { } }qspi_write_status_register_2发送命令0x31数据是 1 字节低字节 bit1 对应 QE。置位后再读状态寄存器 2 确认返回值避免某些国产替代芯片的 QE 位定义不同。另外 QPI 模式全四线指令和 Quad I/O 不是一个概念工程代码里如果看到0x38、0xFF这类进入和退出 QPI 的指令不要随意加否则 HAL 库的HAL_QSPI_Command在发送后续普通命令时会因为指令本身走四线而崩溃。调优时用 DWT 测一下实际读取速度最直观DWT-CTRL | 1; DWT-CYCCNT 0; HAL_QSPI_Receive(hqspi, buf, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); uint32_t cycles DWT-CYCCNT;记录读 4KB 所需的 cycle 数除以主频得到秒数。如果 QSPI 时钟从 50MHz 往上调后cycle 数没有按比例下降优先检查 dummy cycle 和采样移位而不是继续加高时钟分频。W25Q128 在 100MHz 时建议 dummy 保持 6超过 120MHz 改成 8否则读出的数据会整体往左偏几个 bit。本文还有配套的精品资源点击获取
返回列表