最近在调试 STM32H743 的双 QSPI Flash 下载算法。前期因为其中一颗 W25Q256 本身存在硬件问题,导致下载失败、Verify 失败、屏幕花屏、双 Flash 读回异常等问题交织在一起,现象非常混乱。
最后还是在ai的帮助下完成了FLM的编写,于是整理出了主要过程以及分享AI调试的心得。
最后附上FLM文件。
一、资源配置
1.原理图页
本项目基于野火的STM32H743XIH6核心板进行的调试,带有两颗外部 W25Q256 QSPI Flash。
由于本项目更倾向于读速度稳定、延迟低的方向,且需要储存一部分图片资源故采用双QSPI Flash的方案。
2.cubeMX当中的配置
选择模式
我们在Connectivity中找到QUADSPI,按照上图进行配置
因为板子上有两颗 QSPI Flash:U7 = Bank1 Flash;U8 = Bank2 Flash;每颗 W25Q256 本身都是标准 Quad SPI Flash,也就是每颗 Flash 有 4 根数据线。
所以 CubeMX 不能选普通的单 Bank Quad SPI,而要选:
Dual Bank with Quad Lines
Bank1 用 4 根 QSPI 数据线,Bank2 也用 4 根 QSPI 数据线;两颗 Flash 并行工作,MCU 把它们当成一个8bit 总线,64MB的更大的外部 Flash。
又因为两颗 Flash不是各自独立片选,而是共用同一个片选信号;所以两颗 Flash 必须同时被选中、同时接收命令、同时读写数据。
因此 CubeMX 里必须选:
Enable Chip Select 1 for both banks
下方参数配置
其中重点强调一下 Flash Size:这块板的双 QSPI Flash 是由两个 32M 字节 Flash 组成 64M 字节的 8bit QSPI Flash;
计算过程是:
单颗 W25Q256 = 256 Mbit = 32 MByte
两颗 W25Q256:32MB + 32MB = 64MB
然后把 64MB 换成 2 的幂:
64MB= 64 × 1024 × 1024 Byte
= 67,108,864 Byte
= 2^26 Byte
CubeMX / HAL 的 QSPI Flash Size 规则是:
外部 Flash 容量 = 2^(FlashSize + 1)FlashSize = 25
引脚配置
对照自己的原理图调成合适的引脚即可,注意配置成高速
3.总结并检查
我们可以知道,CubeMX 主要负责这几件事:
1. 使能 QUADSPI 外设时钟;
2. 配置 QUADSPI 对应 GPIO 复用功能;
3. 配置 QSPI 时钟分频、FIFO、片选保持时间、FlashSize、Dual Flash 等寄存器初值;
4.生成MX_QUADSPI_Init()和HAL_QSPI_MspInit()。
当我们所有的配置完成后,我们可以将生成的cubeMX配置(quadspi.c/h)文件说明需求后丢给AI去检查,这样总是能发现一些微小的错误,从而减少我们的调试负担。
二、代码部分编写
1.诊断信息结构体
驱动不只是返回HAL_OK或HAL_ERROR,还会保存 QSPI 初始化过程中的关键信息
/* W25Q256 常用命令 */ #define W25Q_CMD_READ_JEDEC_ID ((uint8_t)0x9FU) /* 读取 Flash ID */ #define W25Q_CMD_READ_STATUS_REG1 ((uint8_t)0x05U) /* 读取状态寄存器1,包含 BUSY/WEL */ #define W25Q_CMD_READ_STATUS_REG2 ((uint8_t)0x35U) /* 读取状态寄存器2,包含 QE 位 */ #define W25Q_CMD_WRITE_STATUS_REG2 ((uint8_t)0x31U) /* 写状态寄存器2,用于设置 QE */ #define W25Q_CMD_VOLATILE_SR_WRITE_ENABLE ((uint8_t)0x50U) /* 允许写易失状态寄存器 */ #define W25Q_CMD_ENTER_4BYTE_ADDRESS_MODE ((uint8_t)0xB7U) /* 进入 4 字节地址模式 */ #define W25Q_CMD_FAST_READ_QUAD_OUT_4B ((uint8_t)0x6CU) /* 4 字节地址 Quad Output 快速读 */ typedef enum { BSP_QSPI_ERROR_NONE = 0, /* 无错误 */ BSP_QSPI_ERROR_HAL_NOT_ENABLED, /* HAL QSPI 模块未启用 */ BSP_QSPI_ERROR_COMMAND, /* QSPI 命令发送或接收失败 */ BSP_QSPI_ERROR_ID, /* JEDEC ID 读取或校验失败 */ BSP_QSPI_ERROR_BUSY_TIMEOUT, /* 等待 Flash 空闲超时 */ BSP_QSPI_ERROR_QE, /* Quad Enable 位设置失败 */ BSP_QSPI_ERROR_4BYTE, /* 进入 4 字节地址模式失败 */ BSP_QSPI_ERROR_MEMORY_MAPPED, /* Memory Mapped 模式开启失败 */ BSP_QSPI_ERROR_READ_TEST /* 0x90000000 映射读测试失败 */ } BSP_QSPI_ErrorTypeDef; typedef struct { uint8_t jedec_id[QSPI_JEDEC_ID_READ_BYTES]; /* 双 Flash JEDEC ID */ uint8_t status_reg1[QSPI_STATUS_READ_BYTES]; /* 两颗 Flash 的 SR1 */ uint8_t status_reg2[QSPI_STATUS_READ_BYTES]; /* 两颗 Flash 的 SR2 */ uint8_t status_reg3[QSPI_STATUS_READ_BYTES]; /* 两颗 Flash 的 SR3 */ uint32_t memory_mapped_enabled; /* 是否已进入内存映射模式 */ uint32_t read_test_word; /* 从 0x90000000 读到的测试数据 */ BSP_QSPI_ErrorTypeDef last_error; /* 最近一次错误原因 */ } BSP_QSPI_InfoTypeDef;2. 双 Flash 忙等待
两颗 Flash 会同时参与传输,因此等待忙状态时不能只看一颗芯片
static HAL_StatusTypeDef QSPI_WaitWhileBusy(uint32_t timeout_ms) { uint32_t start_tick = HAL_GetTick(); uint8_t sr1[QSPI_STATUS_READ_BYTES] = {0U, 0U}; do { if (QSPI_ReadStatusPair(W25Q_CMD_READ_STATUS_REG1, sr1) != HAL_OK) { QSPI_SetError(BSP_QSPI_ERROR_COMMAND); return HAL_ERROR; } if (((sr1[0] & W25Q_SR1_BUSY) == 0U) && ((sr1[1] & W25Q_SR1_BUSY) == 0U)) { qspi_info.status_reg1[0] = sr1[0]; qspi_info.status_reg1[1] = sr1[1]; return HAL_OK; } } while ((HAL_GetTick() - start_tick) < timeout_ms); QSPI_SetError(BSP_QSPI_ERROR_BUSY_TIMEOUT); return HAL_TIMEOUT; }3. 开启 Quad 模式
W25Q256 要使用四线数据读取,必须打开 QE 位;两颗Flash的 QE 位都要打开
HAL_StatusTypeDef BSP_QSPI_EnableQuadMode(void) { uint8_t sr2[QSPI_STATUS_READ_BYTES] = {0U, 0U}; uint8_t write_sr2[QSPI_STATUS_READ_BYTES]; if (QSPI_ReadStatusPair(W25Q_CMD_READ_STATUS_REG2, sr2) != HAL_OK) { QSPI_SetError(BSP_QSPI_ERROR_QE); return HAL_ERROR; } if (((sr2[0] & W25Q_SR2_QE) != 0U) && ((sr2[1] & W25Q_SR2_QE) != 0U)) { qspi_info.status_reg2[0] = sr2[0]; qspi_info.status_reg2[1] = sr2[1]; return HAL_OK; } write_sr2[0] = sr2[0] | W25Q_SR2_QE; write_sr2[1] = sr2[1] | W25Q_SR2_QE; if (QSPI_CommandNoData(W25Q_CMD_VOLATILE_SR_WRITE_ENABLE) != HAL_OK) { QSPI_SetError(BSP_QSPI_ERROR_QE); return HAL_ERROR; } if (QSPI_CommandTransmit(W25Q_CMD_WRITE_STATUS_REG2, write_sr2, QSPI_STATUS_READ_BYTES) != HAL_OK) { QSPI_SetError(BSP_QSPI_ERROR_QE); return HAL_ERROR; } return HAL_OK; }4. 进入 4 字节地址模式
W25Q256 单颗容量 32MB,双 Flash 总容量 64MB,已经超过 24 位地址能覆盖的范围。因此必须进入 4-byte address mode。
HAL_StatusTypeDef BSP_QSPI_Enter4ByteAddressMode(void) { if (QSPI_CommandNoData(W25Q_CMD_ENTER_4BYTE_ADDRESS_MODE) != HAL_OK) { QSPI_SetError(BSP_QSPI_ERROR_4BYTE); return HAL_ERROR; } HAL_Delay(1U); return HAL_OK; }5. 开启 Memory Mapped 模式
Instruction:1 线发送 Address:1 线发送,32 位地址 Data:4 线读取 DummyCycles:8HAL_StatusTypeDef BSP_QSPI_EnableMemoryMappedMode(void) { QSPI_CommandTypeDef cmd; QSPI_MemoryMappedTypeDef memory_mapped_cfg; QSPI_FillBaseCommand(&cmd); cmd.Instruction = W25Q_CMD_FAST_READ_QUAD_OUT_4B; cmd.AddressMode = QSPI_ADDRESS_1_LINE; cmd.AddressSize = QSPI_ADDRESS_32_BITS; cmd.Address = 0U; cmd.DataMode = QSPI_DATA_4_LINES; cmd.DummyCycles = QSPI_MEMORY_MAPPED_DUMMY_CYCLES; memory_mapped_cfg.TimeOutActivation = QSPI_TIMEOUT_COUNTER_DISABLE; memory_mapped_cfg.TimeOutPeriod = 0U; if (HAL_QSPI_MemoryMapped(&hqspi, &cmd, &memory_mapped_cfg) != HAL_OK) { QSPI_SetError(BSP_QSPI_ERROR_MEMORY_MAPPED); return HAL_ERROR; } qspi_info.memory_mapped_enabled = 1U; return HAL_OK; }6. 总初始化流程
a. 复位 Flash
b. 读取 JEDEC ID
c. 检查 Flash 是否正常
d. 开启 Quad 模式
e. 进入 4 字节地址模式
f. 开启 Memory Mapped
g. 从 0x90000000 做读测试
HAL_StatusTypeDef BSP_QSPI_BringUpMemoryMapped(void) { uint8_t id[QSPI_JEDEC_ID_READ_BYTES] = {0U}; QSPI_ClearInfo(); if (BSP_QSPI_ResetMemory() != HAL_OK) return HAL_ERROR; if (BSP_QSPI_ReadJEDECID(id, sizeof(id)) != HAL_OK) return HAL_ERROR; if (QSPI_CheckJEDECID() != HAL_OK) return HAL_ERROR; if (BSP_QSPI_EnableQuadMode() != HAL_OK) return HAL_ERROR; if (BSP_QSPI_Enter4ByteAddressMode() != HAL_OK) return HAL_ERROR; if (BSP_QSPI_EnableMemoryMappedMode() != HAL_OK) return HAL_ERROR; if (BSP_QSPI_MemoryMappedReadTest() != HAL_OK) return HAL_ERROR; QSPI_SetError(BSP_QSPI_ERROR_NONE); return HAL_OK; }三、外部 QSPI Flash 下载算法编写
1. 调试背景
这个项目使用的是 STM32H743,并外挂两颗 W25Q256 QSPI Flash。外部 Flash 主要用于存放 TouchGFX 图片资源等数据,因此不仅要求 Keil 能把数据下载进去,还要求程序运行时能够通过 Memory Mapped 地址正确读取。由于下载算法这些文件目前使用AI编写非常方便,编译及工程部分不做过多讲述。
不过值得提一下的是,在提供的FLM编译工程中,需要重新修改路径,否则会出现报错
最开始调试时,外部 Flash 下载算法一直不稳定,主要现象包括:
下载失败 Verify 失败 屏幕花屏 双 Flash 读回数据异常后来经过硬件排查,确认其中一颗 Flash 存在问题。更换并修复 Flash 后,之前很多随机异常消失,问题也从“硬件不稳定”转变为“下载算法细节不正确”。
所以本文只记录更换好 Flash 之后,到最终成功的这段调试过程。
最终成功版本为:
WF_H743X_S13_NOHALF_MV.FLM最终现象为:
擦除成功 下载成功 Verify 成功 TouchGFX 屏幕显示正常2. Verify 成功很关键
在这个项目里,FLM 下载算法不是只要“能写进去”就算结束。
因为 TouchGFX 的图片资源会放在外部 QSPI Flash 中,程序运行时会通过类似下面的地址访问外部资源:
0x90000000也就是说,Keil 下载、Keil Verify、程序运行时读取,这三条链路必须看到同一份正确数据。
如果只是下载过程显示成功,但 Verify 失败,或者运行时 Memory Mapped 读出的数据和写入数据不一致,那么最后的表现很可能就是:
程序能跑 屏幕能亮 但图片资源异常 最终表现为花屏这也是本次调试中非常重要的判断:
屏幕花屏不是简单的颜色格式问题,而是外部 Flash 中的数据内容本身不对,或者读取路径不一致。
3. 关于使用AI调试的重点:SRAM 探针是关键
FLM 下载算法运行在 Keil 的下载流程中,失败时通常只给出类似信息:
这些信息太粗,无法判断到底失败在哪一步。
因此在 STM32H743 的 SRAM 中划出一块区域作为探针区,用来记录 FLM 运行过程中的关键状态
0x2407F000FLM 运行时,把关键状态写到这块 SRAM。下载失败后,通过 Keil 的 Memory Window 查看这个地址,就能知道算法内部执行到了哪一步。
探针主要记录:
当前阶段 错误码 操作地址 写入长度 Flash ID 状态寄存器 读回数据 Memory Mapped 读出的数据简单示例:
typedef struct { volatile uint32_t magic; volatile uint32_t stage; volatile uint32_t error; volatile uint32_t addr; volatile uint32_t size; volatile uint32_t flash_id_1; volatile uint32_t flash_id_2; volatile uint32_t status_1; volatile uint32_t status_2; volatile uint32_t readback0; volatile uint32_t readback1; volatile uint32_t readback2; volatile uint32_t readback3; } FLM_Probe_t; #define FLM_PROBE_ADDR 0x2407F000UL #define FLM_PROBE ((FLM_Probe_t *)FLM_PROBE_ADDR)在keil5调试界面读取参数:
这部分人看着可能会很费力,但是可以让ai自行设置,自行阅读有着极高的调试效率。
在关键位置写入状态:
FLM_PROBE->magic = 0x48474351; FLM_PROBE->stage = 0x01; // 进入 Init FLM_PROBE->error = 0;这个探针非常重要。它让问题从“猜测”变成了“有证据定位”。
4. 探针带来的判断
加入探针后,可以看到:
JEDEC ID 能正确读到 QSPI 初始化能执行 擦除流程能进入 写入流程能完成 Verify 一进入就失败这说明基础通信不是完全错误的。
也就是说:
不是 Flash 完全无法通信 不是 QSPI 初始化完全失败 不是单纯硬件损坏真正的问题集中在:
写入后读回不一致 Memory Mapped 读回不一致 双 Flash 数据排列可能不一致 访问宽度或分块策略有问题这个结论直接缩小了排查范围。
如果没有探针,我们可能还会继续怀疑硬件、焊接、QSPI 引脚或 Flash 芯片,即使使用AI不断修改,也会难以成功;
但探针证明,Flash ID、初始化、擦除、写入流程都能进入,关键问题在 Verify 和运行时读取一致性上。
5. 调试路线
更换好 Flash 后,我重新整理了一版标准 FLM。
最开始的结果是:
擦除后进入下载阶段失败调整 Program 写入流程后,结果变成:
下载成功 Verify 失败这说明擦除和写入大体已经能跑,问题转向读回一致性。
后续测试过几个方向
其中:
AUTO:自动路径不稳定,擦除阶段失败 IV:间接读取能跑,但 Verify 仍失败 SWAP:交换双 Flash 数据顺序后仍失败 SPLIT:复杂分裂写入反而引入新问题 P256:回到 256 字节页编程,但仍不够 MV:最接近程序运行时读取方式,最终保留最终方向逐渐明确:
不要继续堆复杂处理 保留标准页编程 取消 HALF 取消 SPLIT 使用 Memory Mapped Verify 保证 Verify 和程序运行时读取路径一致6. 最终版本
关键修改:
取消 HALF 写入策略 取消复杂 SPLIT 写入策略 不再使用 SWAP 方向 保留标准页编程 使用 Memory Mapped Verify 减少额外数据重排 保持写入和读回路径一致测试结果:
擦除成功 下载成功 Verify 成功 TouchGFX 屏幕显示正常这说明外部 Flash 中的数据已经能被 Keil Verify 正确读回,也能被程序运行时通过 Memory Mapped 地址正确读取。
四、使用条件
本 FLM 适用于:STM32H743XIH6 + 双 W25Q256 + Dual Bank Quad Lines + 共用 CS + 4-byte address mode + 0x90000000 Memory Mapped 访问方式。不同板卡、不同 Flash、不同片选连接方式不能直接套用,需要重新核对引脚、FlashSize、DummyCycles 和读写命令。