资讯动态

STM32H750外扩QSPI Flash与FatFs文件系统移植实战

发布时间:2026/9/10 2:47:21 来源:尧图企业网站定制
简介一套基于STM32H750VBT6微控制器的QSPI接口范例工程展示在HAL库环境下通过FATS文件系统访问W25Q系列外部Flash的实现方法适合希望为嵌入式系统扩展大容量存储、学习文件系统与存储驱动整合开发的工程师。压缩包共293个文件大小约15.93MB文件类型覆盖头文件、C源码、依赖文件、目标文件与工程配置脚本等其中头文件与C源码用于驱动逻辑和上层应用链接脚本描述内存布局构建脚本负责自动化编译整体结构清晰便于按模块查阅。已有507人学习下载。工程基于CubeIDE组织围绕QuadSPI总线的命令配置、数据读写、FatFs挂载与文件操作展开可帮助读者掌握QSPI Flash在文件系统模式下的使用要点同时模块划分明确移植时调整底层引脚与时钟参数即可复用到同系列芯片能有效缩短外置存储功能的开发周期。1. 为什么是 H750 QSPI W25QXX FATS 这套组合STM32H750VBT6 的尴尬之处在于片内 Flash 只有 128KB而它的主频、SRAM 和外设都是按旗舰给的。跑个带图形库的应用或者存几份配置、日志、OTA 镜像片内空间立刻见底。常见做法是外挂一颗 W25Q64 / W25Q128 / W25Q256 系列 SPI NOR Flash用 QSPI 四线模式把读取带宽拉上去再用 FatFsFATS把这颗 Flash 变成一个可格式化、可建文件、可掉电保存的 FAT 分区。这套组合在低成本数据采集、屏幕字库、Bootloader 应用双区方案里非常成熟也解释了为什么网上流传的工程包会直接命名为 STM32H750VBT6-QSPI-FLASH-FATS-W25QXX.rar压缩包里通常就是驱动源码、FatFs 移植层和一块可用的样板工程。“QSPI W25QXX FATS”这三个词连在一起本质是解决一件事让 H750 用最少的引脚和 BOM 成本获得一个容量可扩展、可执行、可管理文件的存储系统。2. H750 的 QSPI 外设与 W25QXX 指令模型先定底层协议2.1 为什么是 QSPI 而不是 SPI、并口或 SDIOW25Q 系列本身兼容标准 SPI、Dual SPI、Quad SPI而 QSPI 和 SPI 的差别不在协议族而在数据线的使用方式。标准 SPI 是 1 根 MOSI 1 根 MISOQSPI 把 4 根 IO 全部变成双向数据线读数据时一个时钟沿输出 4 bit同样 100MHz 的 SCK 下理论吞吐从 12.5MB/s 提升到 50MB/s。对 H750 这种主频 480MHz 的 MCU 来说瓶颈往往不在算法而在数据搬运QSPI 带宽更匹配。并口 NOR Flash 的读写延迟确实更低但 16 根数据线加 20 多根地址线会挤占大量 GPIO在 H750 这种追求小封装和低 BOM 的方案里毫无优势。SDIO 协议和 FATFS 搭配也很常见但它需要卡座、电平转换和协议栈体积与成本都偏高。QSPI W25QXX 则只需 6 根线CS、SCK、IO0-IO3PCB 上几乎不占用布局空间是中小容量扩展最均衡的选型。需要说明的是H750 的 QSPI 外设全称是 QUADSPI内部有 BK1 / BK2 两个 bank可以分别挂一颗 Flash也可以把两片组成 8 线并行模式Dual Flash吞吐还能再翻倍。工程里最常见的是单颗 BK1 方案后面代码都按这个前提写。2.2 W25QXX 的存储结构与关键指令W25Q64 / W25Q128 / W25Q256 这些器件虽然容量不同但内部结构规则一致最小可擦除单位是 4KB 扇区Sector往上还有 32KB、64KB 块擦除写入的最小单位是 256 字节页Page。下面这张表概括了最常用的参数参数典型值说明页大小256 字节Page Program 单次最多写 256B超页必须拆分扇区大小4KBSector Erase 的最小单位擦除后全为 0xFF块大小32KB / 64KB用于批量擦除速度更快但粒度粗JEDEC ID 指令0x9F返回 3 字节制造商 ID、器件类型、容量代码读状态寄存器 10x05bit0 是 WIP写/擦除期间为 1完成后为 0写使能0x06每次写、擦除前必须先发否则命令被忽略Fast Read Quad0xEB四线 IO 读QSPI 内存映射模式一般用这个NOR Flash 与 NAND Flash 在设计理念上完全不同。NOR 支持字节级随机读取读操作不需要先查坏块表也不需要 ECC 校验FatFs 可以直接像使用普通磁盘一样使用它。NAND 的优势是大容量和低成本但要有 FTL 层做坏块管理与磨损均衡MCU 上独立跑 FATFS 会遇到大量额外工程问题。所以标题里出现 W25QXX 而不是 NAND选型意图很清晰稳定性优先容量在 128Mbit 以内足够。2.3 QUADSPI 的两种工作模式把所有 QSPI 操作抽象出来H750 的 QUADSPI 外设就两个模式间接模式Indirect ModeMCU 通过寄存器下发指令、地址、数据外设自动产生时序每笔传输完成后产生标志位。优点是灵活适合页编程、扇区擦除、状态寄存器轮询这些需要读回响应的操作。内存映射模式Memory-Mapped ModeQUADSPI 把外部 Flash 映射到 0x90000000 起始的地址空间CPU 像读内部 Flash 一样直接访问。这个模式是 H750 方案的核心价值之一——代码可以从 W25QXX 上执行XIP大块数据读取也能省掉命令交互的开销。实际工程中写入和擦除必须用间接模式读取则优先走内存映射模式。切换模式要注意先把未完成的传输清干净否则会触发 Busy 标志卡死后续命令这一点在移植过程中最容易踩。3. 最小启动路径QSPI 初始化、读 ID、扇区擦/写/读3.1 初始化流程与参数计算用 HAL 库搭建最小驱动第 1 步是配置 QUADSPI 外设句柄。常见做法是在 STM32CubeMX 里把 QUADSPI 的时钟源设为 AHB 分频后的时钟然后在用户代码中做精细调整/* qspi_w25qxx.c —— 基于 HAL 库的最小驱动骨架 */ static QSPI_HandleTypeDef hqspi; void qspi_lowlevel_init(void) { hqspi.Instance QUADSPI; hqspi.Init.ClockPrescaler 1; /* QCLK AHB_CLK / (11)若AHB200MHz则QCLK100MHz */ hqspi.Init.FifoThreshold 16; /* FIFO 达到16字节即触发中断/DMA轮询时阈值不影响正确性 */ hqspi.Init.SampleShifting QSPI_SAMPLE_SHIFTING_HALFCLK; /* 数据采样延迟半个时钟周期 */ hqspi.Init.FlashSize 23; /* Flash总位数减116MB→2^24 bit→23 */ hqspi.Init.ChipSelectHighTime QSPI_CS_HIGH_TIME_5_CYCLE; /* CS 释放后保持高电平至少5个时钟周期 */ hqspi.Init.ClockMode QSPI_CLOCK_MODE_0; /* CPOL0, CPHA0对应W25QXX默认模式0和模式3均可 */ hqspi.Init.FlashID QSPI_FLASH_ID_1; /* BK1 */ hqspi.Init.DualFlash QSPI_DUALFLASH_DISABLE; if (HAL_QSPI_Init(hqspi) ! HAL_OK) error_handler(); }参数说明ClockPrescaler 是分频系数实际 QCLK 等于 AHB 时钟除以Prescaler1。不要看到 W25Q128 标称 133MHz 就把 QCLK 顶满H750 的 QSPI 输出走线在双层板上很难稳定跑 133MHz习惯上先压到 50~100MHz 跑通功能再考虑拉高。FlashSize 填的是“容量位数减 1”W25Q128 是 16MB对应 2^24 位所以填 23填错会导致地址计算溢出读写数据错位。SampleShifting 在高速模式下很关键H750 的 QSPI 采样点默认偏早遇到 FLASH 返回数据建立时间不足时会出现“读 ID 对读数据错”的诡异现象把采样点往后挪半拍通常能解决。3.2 读 JEDEC ID 验证链路初始化完成后第一件要做的就是读 ID。这一步能同时验证 GPIO 复用配置、时钟频率上限和 Flash 是否被正确选通void w25q_read_jedec_id(uint8_t *manuf, uint8_t *type, uint8_t *capacity) { uint8_t rx_buf[3]; QSPI_CommandTypeDef cmd {0}; cmd.Instruction 0x9F; /* JEDEC ID 指令 */ cmd.InstructionMode QSPI_INSTRUCTION_1_LINE; /* 普通单线发送 */ cmd.AddressMode QSPI_ADDRESS_NONE; cmd.DataMode QSPI_DATA_1_LINE; /* 返回数据用单线读足够 */ cmd.DummyCycles 0; cmd.NbData 3; cmd.DdrMode QSPI_DDR_MODE_DISABLE; cmd.SIOOMode QSPI_SIOO_INST_EVERY_CMD; if (HAL_QSPI_Command(hqspi, cmd, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) ! HAL_OK) error_handler(); if (HAL_QSPI_Receive(hqspi, rx_buf, HAL_QSPI_TIMEOUT_DEFAULT_VALUE) ! HAL_OK) error_handler(); *manuf rx_buf[0]; /* 0xEF 表示 Winbond */ *type rx_buf[1]; /* 0x40 表示 W25Q 系列 */ *capacity rx_buf[2]; }命令发送后必须调用 HAL_QSPI_Receive 而不是直接读数据寄存器因为 QUADSPI 外设会在 Command 阶段把时序配置好真正把数据搬回内存的是 Receive 函数。如果读回的 ID 是 0xFFFFFF 或 0x000000优先检查 CS 是否被拉死、QSPI 引脚是否被其他外设复用以及时钟是否超过 Flash 上限。3.3 页编程与扇区擦除的时序纪律写 Flash 最核心的纪律是写之前先擦擦之后必须等 WIP 清 0。直接向非 0xFF 区域写入结果通常是读出乱数据。先实现两个内部工具函数static void w25q_write_enable(void) { QSPI_CommandTypeDef cmd {0}; cmd.Instruction 0x06; /* WRITE ENABLE */ cmd.InstructionMode QSPI_INSTRUCTION_1_LINE; cmd.AddressMode QSPI_ADDRESS_NONE; cmd.DataMode QSPI_DATA_NONE; HAL_QSPI_Command(hqspi, cmd, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); } static void w25q_wait_wip_clear(void) { uint8_t status; uint8_t cmd 0x05; /* READ STATUS REGISTER 1 */ do { HAL_QSPI_Command(hqspi, (QSPI_CommandTypeDef){ .Instruction cmd, .InstructionMode QSPI_INSTRUCTION_1_LINE, .AddressMode QSPI_ADDRESS_NONE, .DataMode QSPI_DATA_1_LINE, .NbData 1, .DummyCycles 0, .SIOOMode QSPI_SIOO_INST_EVERY_CMD }, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Receive(hqspi, status, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); } while (status 0x01); }有了这两个函数擦除和写页就变成顺理成章的三步写使能、发命令、等待完成。void w25q_erase_sector(uint32_t sector_addr) { w25q_write_enable(); /* 必须先发0x06否则0x20被忽略 */ QSPI_CommandTypeDef cmd {0}; cmd.Instruction 0x20; /* SECTOR ERASE4KB */ cmd.InstructionMode QSPI_INSTRUCTION_1_LINE; cmd.AddressMode QSPI_ADDRESS_1_LINE; cmd.Address sector_addr 0xFFFFFF; cmd.AddressSize QSPI_ADDRESS_24_BITS; cmd.DataMode QSPI_DATA_NONE; HAL_QSPI_Command(hqspi, cmd, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); w25q_wait_wip_clear(); /* 扇区擦除典型耗时 40~400ms必须等 */ }页编程同样先使能再发 0x02 指令和地址最后用 HAL_QSPI_Transmit 写入最多 256 字节void w25q_page_program(uint32_t addr, const uint8_t *data, uint16_t len) { QSPI_CommandTypeDef cmd {0}; w25q_write_enable(); cmd.Instruction 0x02; /* PAGE PROGRAM */ cmd.InstructionMode QSPI_INSTRUCTION_1_LINE; cmd.AddressMode QSPI_ADDRESS_1_LINE; cmd.Address addr 0xFFFFFF; cmd.AddressSize QSPI_ADDRESS_24_BITS; cmd.DataMode QSPI_DATA_1_LINE; cmd.NbData len; HAL_QSPI_Command(hqspi, cmd, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Transmit(hqspi, data, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); w25q_wait_wip_clear(); }这里有一个高频踩坑点0x02 页编程不允许跨 256 字节边界如果地址是 0x1F0、长度是 32则前 16 字节落在第 0 页、后 16 字节落在第 1 页直接发送会被 Flash 内部截断。驱动层必须按 256 字节页拆分否则 FATFS 写入文件超过一页后就会出现数据错位。4. 让 FatFs 在 W25QXX 上跑起来diskio 适配与参数调优4.1 diskio.c 到底要补哪些函数FatFs 本身与存储介质无关它只认 6 个底层接口disk_status、disk_initialize、disk_read、disk_write、disk_ioctl、get_fattime。对 W25QXX 这类 NOR Flash工作量集中在 disk_write 和 disk_ioctl 上。disk_write 的输入是按逻辑扇区默认 512B对齐的而 W25Q 的物理擦除单位是 4KB所以逻辑扇区与物理扇区天然有一个“4KB 对齐”的映射关系。常见做法是把逻辑扇区地址乘以 512 得到物理字节地址然后直接用页编程写入。因为 FatFs 的写操作总是整扇区写入且从不跨扇区所以 512 字节刚好拆成两个 256 字节页不必处理跨页问题DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count) { uint32_t addr; uint32_t offset; if (pdrv ! 0) return RES_PARERR; addr (uint32_t)sector 9; /* 逻辑扇区转物理字节地址 */ for (UINT i 0; i count; i) { offset addr 0xFF; if (offset 0) { /* 地址恰好落在 256B 页边界直接写两页 */ w25q_page_program(addr, buff (i 9), 256); w25q_page_program(addr 256, buff (i 9) 256, 256); } else { /* 非页对齐保守做法是先读整页改写后再回写 */ error_handler(); /* 这在实际工程里不应出现 */ } addr 512; } return RES_OK; }disk_ioctl 是整个移植里最关键的部分它向 FatFs 报告磁盘的几何参数。W25Q128 若按 512B 扇区格式化总扇区数为 32768块大小按 4KB 物理扇区换算成 8 个逻辑扇区DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { switch (cmd) { case GET_SECTOR_COUNT: *(LBA_t *)buff 16 * 1024 * 1024 / 512; /* 32768 个逻辑扇区 */ return RES_OK; case GET_SECTOR_SIZE: *(WORD *)buff 512; return RES_OK; case GET_BLOCK_SIZE: *(DWORD *)buff 8; /* 1个物理扇区 8个逻辑扇区簇按此对齐 */ return RES_OK; case CTRL_SYNC: return RES_OK; default: return RES_PARERR; } }GET_BLOCK_SIZE 返回 8 很关键。FatFs 分配簇时若簇大小是 4KB 的整数倍文件数据就不会跨物理扇区边界避免一次文件追加触发两个物理扇区的读改写。如果这个值返回 1FatFs 在分配簇时完全按 512B 粒度操作写日志文件性能会有明显下降。4.2 ffconf.h 的推荐配置与格式化步骤FatFs 的裁剪配置都在 ffconf.h。针对 H750 W25QXX我一般用这组参数配置项推荐值说明FF_USE_MKFS1允许在板上直接 f_mkfs 格式化FF_MIN_SS / FF_MAX_SS512必须与 disk_ioctl 的扇区大小一致FF_VOLUMES1单分区剪裁减小代码体积FF_FS_RPATH2支持相对路径日志文件切换少写很多字符串处理FF_USE_FASTSEEK1大文件连续读时提速需要为文件对象提供 FAT 索引缓存FF_FS_TINY0关闭 Tiny 模式换区换文件时不用反复读写扇区缓冲格式化逻辑放在主程序初始化的后半段FATFS fs; BYTE work[4096]; /* 格式化工作区4KB 对齐到物理扇区 */ FRESULT fr f_mount(fs, 0:, 1); /* 立即挂载 */ if (fr FR_NO_FILESYSTEM) { fr f_mkfs(0:, 0, work, sizeof(work)); /* 格式化 */ if (fr ! FR_OK) { /* 格式化失败检查 disk_ioctl 的扇区大小返回值 */ } fr f_mount(NULL, 0:, 0); /* 重新挂载 */ fr f_mount(fs, 0:, 1); }格式化工作区必须 4KB 对齐到物理扇区边界很多编译器默认不会把数组放到 4KB 对齐地址这里需要手动指定对齐属性例如__attribute__((aligned(4096)))。如果不对齐f_mkfs 在擦除时会把两个物理扇区都破坏表现为格式化后文件系统偶尔挂载不上。4.3 掉电保护与 f_sync 的使用边界NOR Flash 的页编程和扇区擦除期间掉电可能留下半写入页面。FATFS 自身有 FAT 表双份备份但文件数据目录项更新不是原子的所以写文件循环里建议这样做while (data_len 0) { f_write(file, buf, write_len, bw); /* 文件尾部数据不完整时记录断点 */ osDelay(1); /* 给看门狗让路 */ } f_sync(file); /* 写完后立即刷目录项与FAT表到Flash */f_sync 每调用一次就会产生一次额外写入日志类高频小文件写入时要控制频率比如 5 秒调一次而不是每包都调。如果产品对掉电后文件完整性有严格要求还需要在写数据前先把长度和校验和记录到 Flash 的另一区域上电后做一次完整性校验。5. 内存映射模式把 FatFs 的读变成 memcpy 以及常见故障排查5.1 切换到内存映射模式并处理 Cache 一致性文件系统跑通后最值得做的一个优化是把读取路径切到内存映射模式。初始化并挂载 FATFS 后查询文件偏移对应的物理地址比较麻烦但对连续存储的文件有个技巧文件在 FAT 中若按连续簇分配可以用 disk_ioctl 的 CTRL_TRIM 配合特定手段确定起始扇区更实际的做法是直接放弃小文件的精确寻址改用“读入 SRAM 再 memcpy”的形式。代码上切内存映射模式本身很简单void qspi_enter_memory_mapped(void) { QSPI_CommandTypeDef cmd {0}; QSPI_MemoryMappedTypeDef cfg {0}; cmd.Instruction 0xEB; /* Fast Read Quad I/O */ cmd.InstructionMode QSPI_INSTRUCTION_4_LINES; cmd.AddressMode QSPI_ADDRESS_4_LINES; cmd.AddressSize QSPI_ADDRESS_24_BITS; cmd.DataMode QSPI_DATA_4_LINES; cmd.DummyCycles 6; /* 0xEB 需要6个dummy周期 */ cmd.SIOOMode QSPI_SIOO_INST_EVERY_CMD; cfg.TimeOutActivation QSPI_TIMEOUT_COUNTER_DISABLE; HAL_QSPI_MemoryMapped(hqspi, cmd, cfg); }之后访问0x90000000 offset即可读 Flash。如果开了 D-CacheFlash 内容更新后 Cache 里的旧数据会导致读取错误必须在切回间接模式写 Flash 之前执行SCB_CleanDCache()再在回到内存映射模式后执行SCB_InvalidateDCache()。很多“写自己的固件、读回却报校验失败”的问题根源不在 Flash 而在 Cache。5.2 烧录与挂载高频故障对照工程实践中下面这 4 个问题占据了 QSPI FATFS 调试时间的大头故障现象原因解决办法Keil 下载报Error: Flash Download failed - Target DLL has been cancelled没有在 Flash Download 页面添加外部 SPI Flash 编程算法在 MDK 的 Flash Download 里添加 STM32H7x 对应的 QSPI FLM 算法文件或者先用 Bootloader 把代码搬运到 SRAM 再初始化 QSPI读 ID 正确但 f_mount 返回 FR_NO_FILESYSTEMFlash 未格式化或格式化工作区没有 4KB 对齐执行 f_mkfs检查 work 数组对齐属性写入大文件后读取尾部数据错乱页编程跨越 256B 边界确认 disk_write 拆页逻辑不要依赖 Flash 自动截断系统上电后文件在但内容全 0xFF掉电时文件目录项未写回保证 f_sync 时机处理掉电保护5.3 验证与收尾技巧最后给一个适合固件量产自检的流程格式化完成后创建固定长度的测试文件写入 1000 组随机数与 CRC32 校验值再断电重启读取文件并比对全部 CRC。这个过程能同时覆盖 QSPI 读链路、FATFS 目录项更新、页编程跨页处理和掉电恢复是验收一颗 Flash 驱动是否合格最快的方式。如果这套方案里还打算把应用代码放到外部 Flash 执行记得向量表要放到 0x90000000 的偏移处且启动代码里先完成 QSPI 初始化再跳到主函数否则复位后第一条取指就会 HardFault。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价