STM32H7xx Keil MDK外部QSPI Flash FLM下载算法实战

发布时间:2026/11/3 4:16:35
STM32H7xx Keil MDK外部QSPI Flash FLM下载算法实战 简介面向STM32H7XX系列MCU开发者的外部Flash下载算法应用笔记聚焦KEIL-MDK环境下为SPI/QSPI外置Flash定制FLM下载算法的完整思路。内容从MDK下载算法基础知识切入覆盖FLM工程建立、SPI Flash算法制作与使用、FLM_DEBUG调试工程搭建并展开说明关闭中断、查询方式操作、重实现HAL_InitTick/HAL_GetTick/HAL_Delay、QSPI/OCTOSPI接口函数以及FlashDev.c设备结构体配置等关键环节也给出利用GPIO观察下载进度和排查问题的调试方法。资源包为1个docx文档大小约421KB篇幅集中、便于按章节查阅。已有2499人学习适合需要将程序烧录到外部Flash并完成可靠调试的嵌入式工程师、STM32学习者参考可帮助梳理从工程模板修改到算法验证的落地路径并补充时序调整、错误处理与加载地址配置等实践要点。1. 外部 Flash 烧不进去问题不在板子板子上挂一颗 QSPI Flash应用整个搬进去跑 XIPKeil MDK 里点下载进度条卡在 0% 不动弹一句 Cannot Load Flash Programming Algorithm换 J-Link 接上去单步能读能写一到烧录就哑火。硬件没坏坏在认知MDK 只认器件包DFP里自带的那几个片上 flash 算法/Keil/ARM/PACK/Keil/STM32H7xx_DFP/x.x.x/CMSIS/Flash下没有你手上这颗外部 SPI Flash 的条目它就不认识这块地址空间。这份 STM32H7XX KEIL MDK 外部 FLASH FLM 下载算法工程笔记干的就是补上这块空白写一份与地址无关的算法工程实现初始化、擦除、编程、读取、校验让 MDK 在下载阶段把算法搬进芯片内部 RAM 执行替你操作外部 Flash。适合已经能把 H7 跑起来、准备把程序放进外部 Flash 做 ISP 或 XIP 的开发者也适合想搞明白 MDK 下载黑匣子里到底发生了什么的熟手。2. FLM 的运行链路算法怎么被塞进内部 RAM 替你烧外部 Flash2.1 从点击下载到算法在 RAM 里跑起来很多人第一次做 FLM把它当成一个普通固件工程来编编完发现生成的 .FLM 只有几十 KB既没有中断向量表也没有 main一脸懵。这玩意儿本来就不是拿来独立运行的它是一段被调试器按需搬运、按需调用的位置无关代码。完整链路是这样MDK 在下载前先把 .FLM本质是一个特殊链接的 ELF加载到芯片内部 RAM 的某个地址这个地址就是你工程里配置的算法加载地址工程侧对应 Flash Download 页里的「RAM for Algorithm」。搬完之后调试器通过调试协议在目标 RAM 里找到算法的入口符号依次调用Init、EraseSector、ProgramPage、Verify、UnInit参数用寄存器传。算法里做的是 QSPI/OCTOSPI 命令收发真正干活的是外部 Flash 自己。这里有个关键约束决定了后面所有的写法算法运行期间芯片里没有你的应用中断向量表指向的位置是无效的。所以算法内部一个中断都不能开全部查询方式轮询SysTick 中断当然也不能开但 HAL 库的HAL_Delay、HAL_GetTick又强依赖 SysTick这就得自己接管。这条约束是后面 3.3 节所有改写的原因先记住。还有一点常被忽略算法被要求「与地址无关」因为 MDK 可能会把它加载到不同地址、不同工程复用。所以模板工程里那些 Position Independent 选项、绝对地址跳转在 FLM 工程里是要反过来处理的这在第 6 章改调试工程时会再撞上一次。2.2 FlashDevice 结构体MDK 认识外部 Flash 的唯一凭据FlashDev.c里就一个结构体常量但它决定了 MDK 眼里你的 Flash 长什么样——多大、从哪开始、按多少字节编程、擦除粒度多大、擦完是什么值。写错了后面代码再对也白搭。/* FlashDev.c告诉 MDK 这块外部 Flash 的全部物理属性 */ struct FlashDevice const FlashDevice { FLASH_DRV_VERS, /* 驱动版本固定宏不要改 */ H7B0_Ext_QSPI_Flash, /* 设备名会显示在 Flash Download 列表里 */ EXTSPI, /* 设备类型外部 SPI Flash */ 0x90000000, /* 器件起始地址QSPI 内存映射基地址 */ 0x00400000, /* 器件总容量 4MB */ 0x100, /* 编程页大小 256 字节必须与硬件页一致 */ 0, /* 保留必须为 0 */ 0xFF, /* 擦除后的内容NOR Flash 通常是 0xFF */ 20000, /* ProgramPage 超时单位 ms调试期可放大 */ 50000, /* EraseSector 超时单位 ms调试期可放大 */ /* 扇区表必须覆盖整个 4MB最后以 SECTOR_END 收尾 */ { 0x010000, 0x000000 }, /* 64KB 扇区起始偏移 0 */ { 0x010000, 0x010000 }, /* ... 中间省略 60 条每 64KB 一条 ... */ { 0x010000, 0x3F0000 }, /* 最后一条偏移 0x3F0000 */ { SECTOR_END } };逐个成员说清楚容易踩的地方。DevAdr是绝对地址QSPI 内存映射一般从 0x90000000 起写错一位MDK 就会去擦别的地址后果你懂的。szDev是总容量sectors[]里的条目累加必须能覆盖到szDev覆盖不到的区域 MDK 会认为不可编程下载大固件时到某个偏移就报错。sectors[]数组上限是SECTOR_NUM512 条。如果你的 Flash 最小擦除单位是 4KB、总容量 4MB那就是 1024 个扇区装不下。常见做法是把扇区表写粗一点用 64KB 条目描述硬件上按 4KB 擦的区域MDK 请求擦 64KB 时你在EraseSector里循环擦 16 次 4KB 就行——擦除粒度由你的代码决定不是由表硬性绑定。这个技巧能省掉一大堆表项也能避免表写漏。szPage是编程页大小必须等于 Flash 硬件页。MDK 的ProgramPage每次最多给你szPage字节且地址按页对齐你的驱动不需要处理跨页。但如果szPage写成 512 而硬件页是 256写进去的数据就会在页内回卷现象是「校验失败但每隔 256 字节有一半数据是对的」非常经典。valEmpty填0xFF还是0x00取决于器件NOR Flash 基本都是 0xFFEEPROM 类可能是 0x00。填错的表现是 MDK 认为空片不空会先擦一遍才写速度慢一倍但功能正常属于不容易发现的性能坑。2.3 FlashPrg.c 的六个入口函数与调用时序FlashPrg.c是算法的主体MDK 只认这六个函数名和它们的签名签名对不上链接会失败返回值约定错了现象会很怪。函数签名调用时机返回约定Initint Init(unsigned long adr, unsigned long clk, unsigned long fnc)算法加载后、任何操作前0 成功1 失败UnInitint UnInit(unsigned long fnc)本次下载结束0 成功EraseChipint EraseChip(void)勾选整片擦除时0 成功1 失败EraseSectorint EraseSector(unsigned long adr)按扇区擦除0 成功1 失败ProgramPageint ProgramPage(unsigned long adr, unsigned long sz, unsigned char *buf)写一页0 成功非 0 为失败地址Verifyunsigned long Verify(unsigned long adr, unsigned long sz, unsigned char *buf)校验0 成功非 0 为首个不一致地址Init的三个入参里adr是本次操作的起始地址clk是 MDK 传下来的时钟频率很多工程直接忽略用自己的固定分频fnc是功能码编程、擦除、校验的组合掩码。真正干活的是在这里把 QSPI 初始化好、把 Flash 从掉电/深睡里唤醒、解除块保护。Verify的返回值约定最容易翻车返回 0 表示全部一致返回非 0 表示第一个不一致的地址。我见过有人写成「一致返回 1不一致返回 0」结果就是每次下载都报校验失败但程序其实是好的白白排查半天。函数体里比对时用一个volatile指针读回别让编译器优化掉。这几个函数在算法里是地址无关的内部不能调用应用层的绝对地址函数。所以驱动层要写成「自己带一个 QSPI 句柄、自己带时钟配置」的自包含形式这一点在 4.4 节展开。3. 工程搭建从模板改出一个能编译出 .FLM 的工程3.1 起步模板与器件配置不建议从零建工程。Keil 安装路径下有个通用模板/Keil/ARM/PACK/ARM/CMSIS/version/Device/_Template_Flash里面就是FlashDev.c、FlashPrg.c、FlashOS.h三件套骨架。但通用模板的器件配置是空的时钟、QSPI 都要自己从零填容易漏。更省事的办法是找一份已经跑通的同系列 H7 FLM 工程当模板改把FlashDev.c的结构体按自己的器件重写FlashPrg.c的函数骨架保留只换驱动实现。改的时候按这个顺序来出错好定位第一步在 Keil 的 Target 页把器件选成对应的 STM32H7 型号但不要勾选「Use Memory Layout from Target Dialog」FLM 工程需要一个手工 scatter 或者干脆把 IROM1 指到 RAM 地址。IROM1 的 Start 填算法的加载地址比如 AXI SRAM 的 0x24000000Size 填你留给算法的 RAM 大小常见 32KB 或 64KB。这个值会直接映射到工程侧 Flash Download 里的 RAM for Algorithm两边必须一致。第二步Output 页勾上「Create Executable」输出名改成有辨识度的比如H7B0_Ext_QSPI_Flash。生成的 .FLM 就是最终产物不需要 .hex。第三步C/C 页的优化等级建议先用-O1调试通过之后再往上调。-O2以上时如果驱动里某个查询循环写得不够严谨容易被优化成死循环或者被整个删掉现象是「同一份代码 -O0 能跑-O2 卡死」这类玄学问题在 FLM 里特别多。第四步把System_stm32h7xx.c这类文件先加进来让工程能编过等到第 6 章改调试工程时再按需删除。3.2 IOC 最小化配置只留时钟和 QSPI/OCTOSPI用 CubeMX 配 IOC 的时候要克制。原则是最小化只配下载算法必须的组件——系统时钟、QSPI 或 OCTOSPI 外设、必要的 GPIO。别把 UART、USB、RTOS、DMA 全勾上多一个外设就多一份初始化时间也多一份在 RAM 里跑飞的风险。时钟这块推荐先用内部晶振HSI把算法跑通。原因很实际外部晶振的起振时间不确定HSE 起振失败会让Init直接卡死在 HAL 的超时等待里而外部 Flash 的工作时钟本来就可以在 QSPI 分频里单独调低不需要系统主频跑满。等算法功能验证完了再换外部晶振、提高 QSPI 时钟出问题也知道是哪一个变量改的。工程里可以准备两份 IOC一份内部晶振一份外部晶振切换时直接换配置文件别在一个文件里来回改。QSPI 配置上有几个参数建议一开始就固定下来时钟分频先用 4 分频甚至 8 分频别一上来就跑最高速指令模式单线、地址模式按器件支持选Dummy Cycles 严格按器件手册来多一个少一个都会读出错数据。这些参数后面在内存映射模式读回校验时会再核对一次。另外建议多配几个空闲 GPIO 出来做状态指示。这是个土办法但极其有效在Init开头拉高一个脚在EraseSector里翻转一下在ProgramPage里再翻转一下用示波器或者逻辑分析仪看波形一眼就能判断算法有没有真正跑起来、卡在哪个阶段。调试 FLM 时你没有任何 printf 可用串口初始化在算法里是负担GPIO 翻转几乎是唯一可观测的手段血泪经验。配完 IOC 生成代码把生成的main.c之外的文件时钟、QSPI、GPIO 初始化部分加到 FLM 工程里。main.c不要整个加进来里面通常有while(1)和中断初始化会污染算法。3.3 三个 sysTick 依赖函数的替换实现HAL 库把HAL_InitTick、HAL_GetTick、HAL_Delay定义成了__weak重新实现一遍链接器就会用你的版本。这是整个 FLM 开发里最关键的一步因为 HAL 里所有HAL_QSPI_*的超时判断、HAL_Delay的等待全都挂在这三个函数上而它们默认依赖 SysTick 中断——算法里中断是禁区。我一般用 SysTick 寄存器轮询的方式实现不碰中断也不依赖 DWT/* 覆盖 HAL 弱定义SysTick 只用作计时器永远不开中断 */ HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority) { /* 这里什么都不做SysTick 在 HAL_Delay 里临时启用 */ return HAL_OK; } uint32_t HAL_GetTick(void) { /* 用一个软件累加的毫秒计数由 HAL_Delay 维护 */ return s_ms_tick; } void HAL_Delay(uint32_t Delay) { while (Delay--) { /* 单次计数 1ms重装载值 内核时钟 / 1000 - 1 */ SysTick-LOAD (SystemCoreClock / 1000U) - 1U; SysTick-VAL 0U; /* CLKSOURCE1 表示用内核时钟ENABLE1 启动TICKINT 保持 0 */ SysTick-CTRL SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_ENABLE_Msk; /* 死等 COUNTFLAG 置位计满 1ms 自动清零并停止 */ while ((SysTick-CTRL SysTick_CTRL_COUNTFLAG_Msk) 0U) { __NOP(); } s_ms_tick; } }三个函数的职责要分清。HAL_InitTick里不启动定时器是因为算法在真正需要延时之前可能还有别的初始化提前开 SysTick 也无害但没必要HAL_GetTick返回软件累加的s_ms_tick它只在HAL_Delay里增长所以「时间」只在延时期间推进这对 FLM 场景够用因为所有超时判断都在 QSPI 收发前后中间必然有延时调用HAL_Delay是核心每次循环重装 SysTick死等COUNTFLAG一个字节一个字节地把毫秒数数出来。参数上要注意SystemCoreClock这个变量。它由SystemInit根据实际时钟树算出来如果你在Init里改了时钟配置而没有更新这个变量算出的重装载值就是错的延时会整体偏快或偏慢表现为「QSPI 擦除偶尔超时」。稳妥做法是在Init里时钟配完之后手动调一次SystemCoreClockUpdate()。如果不想动 SysTick另一个方案是用 DWT 的 CYCCNT 周期计数器。好处是精度高坏处是 CYCCNT 是 32 位主频越高回绕越快需要处理回绕而且某些调试器会独占 DWT得先置位CoreDebug-DEMCR的 TRCENA。两个方案都行选一个自己看得懂的别混用——我见过HAL_GetTick用 DWT、HAL_Delay用 SysTick 的写法两边时基不一致超时判断永远不触发。3.4 产物落地.FLM 放哪、工程侧怎么挂上去编出来的 .FLM 有两个存放位置任选其一放进器件包目录/Keil/ARM/PACK/Keil/STM32H7xx_DFP/version/CMSIS/Flash好处是和官方算法并列显示坏处是器件包一升级就被覆盖得重放。放进 Keil 安装目录下的ARM/Flash好处是独立于器件包、升级不受影响推荐这个。放好之后打开应用工程Options for Target → Debug → Settings → Flash Download 页点 Add列表里就能看到你FlashDev.c里写的设备名。添加进来后下方的 RAM for Algorithm 起始地址和 Size 必须填对。Size 给太小是最常见的报错来源提示「算法文件无法加载」因为算法本身的代码段加数据段放不下。给 32KB 起步保守点给 64KB反正 RAM 够。还有一个容易漏的配置Programming Algorithm 列表里你的外部 Flash 算法和片上 Flash 算法可以共存。下载时 MDK 按地址匹配落到 0x90000000 区间的走你的算法落到 0x08000000 的走片上算法。如果你的程序是「引导区在片上、应用在外部」的结构两个都要加上并且注意各自的起始地址不要重叠。4. QSPI/OCTOSPI 驱动层五个必须自己写的接口函数4.1 解除写保护与自动轮询不少 SPI Flash 出厂时状态寄存器里的块保护位是开着的尤其是用了很久的样片或者被别的平台写过保护。你在算法里直接擦命令发下去返回成功读回来数据没变现象就是「擦除返回 0 但内容还是旧的」。所以Init里第一件正事是解保护这个逻辑要单独封装成两个函数/* 清除状态寄存器里的块保护位让整片地址可写 */ int hal_qspi_flash_set_wps_status(void) { uint8_t sr 0; /* 读状态寄存器 1 */ if (qspi_read_status(sr) ! 0) return -1; /* 清掉 BP0~BP2 和 SRWD 位注意有的器件还需要清状态寄存器 2 */ sr ~(SR_BP_MASK | SR_SRWD); if (qspi_write_status(sr) ! 0) return -1; /* 写状态寄存器本身也需要时间立刻回读会读到旧值 */ return hal_qspi_flash_auto_polling_wps_status(); } /* 轮询状态寄存器 WIP 位直到写保护位真正生效 */ int hal_qspi_flash_auto_polling_wps_status(void) { uint32_t timeout WPS_POLL_TIMEOUT; /* 建议 1000 次以上 */ uint8_t sr 0; do { if (qspi_read_status(sr) ! 0) return -1; if ((sr SR_WIP) 0) return 0; /* WIP 清零表示内部操作完成 */ } while (--timeout); return -1; }注意两个细节。一是写状态寄存器命令本身是异步的命令发完器件内部还在写必须轮询 WIP 位有的器件叫 BUSY等它清零才能发下一条命令否则下一条擦除命令会被直接丢弃且不报错。二是有些器件的块保护位分布在状态寄存器 1 和 2 两个字节里只清一个是不够的具体看手册判断标准是解保护后用0x03读回整片随便挑几个地址试擦擦完变 0xFF 才算真解开了。轮询超时值别省。NOR Flash 的写状态寄存器时间通常在毫秒级给 1000 次读状态足够但读状态本身走的是 QSPI 命令如果时钟配得很低一次读可能要几十微秒1000 次就是几十毫秒。这个循环里不要调HAL_Delay用纯计数轮询快得多。4.2 任意长度写页边界拆分与地址推进ProgramPage每次最多给你一页szPage但驱动层最好实现一个支持任意长度的写函数因为Init阶段可能要写一些配置数据长度不一定是页的整数倍。核心是按页拆分/* 任意长度写内部按页拆分跨页自动推进地址 */ int hal_qspi_flash_write(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t off, page_left, chunk; /* FLM 传进来的是绝对地址QSPI 命令要的是相对偏移 */ if (addr EXT_FLASH_BASE) return -1; off addr - EXT_FLASH_BASE; if (off len EXT_FLASH_SIZE) return -1; /* 越界直接拒 */ while (len 0U) { /* 本页还能写多少字节 */ page_left QSPI_PAGE_SIZE - (off % QSPI_PAGE_SIZE); chunk (len page_left) ? len : page_left; if (qspi_page_program(off, buf, chunk) ! 0) return -1; /* 等这次页编程真正完成否则下一段会覆盖失败 */ if (qspi_wait_busy() ! 0) return -1; off chunk; buf chunk; len - chunk; } return 0; }参数说明addr用绝对地址和ProgramPage的入参保持一致内部减掉EXT_FLASH_BASE转成 24 位偏移len可以任意长函数保证不跨页写。逻辑上有三层保护起始地址必须落在器件范围内、长度不能越界、每次写之前算清本页剩余空间。这里有个容易翻车的点页编程命令0x02在页边界不会自动跳转而是回卷到本页开头继续写。也就是说如果你一次发 300 字节到页内偏移 200 的位置前 56 字节落在本页尾部剩下 244 字节会覆盖本页开头——数据全乱但命令本身返回成功。所以按页拆分不是优化是必须。写完每页之后还要等 WIP 清零别连着发器件内部写周期没结束时会丢命令。页大小这个常量要和FlashDev.c里的szPage用同一个宏别在两处各写一个数字。改器件的时候只改一处另一处忘改就是数据错乱而且这种错乱是间歇性的很难查。4.3 扇区擦除与 memory-mapped 校验的切换擦除相对简单难点在「擦多大」。你的sectors[]表里写了 64KBMDK 就会以 64KB 对齐的地址请求擦除而器件最小擦除单位可能是 4KB所以函数里要循环/* 擦除一个扇区块按器件最小擦除粒度循环直到覆盖请求的块大小 */ int hal_qspi_flash_erase_sector_block(uint32_t addr, uint32_t block_size) { uint32_t off, i; if (addr EXT_FLASH_BASE) return -1; off addr - EXT_FLASH_BASE; if ((off % QSPI_SECTOR_SIZE) ! 0U) return -1; /* 必须按最小粒度对齐 */ for (i 0; i block_size; i QSPI_SECTOR_SIZE) { if (qspi_sector_erase(off i) ! 0) return -1; if (qspi_wait_busy() ! 0) return -1; /* 等本次擦除完成 */ } return 0; }block_size由调用方EraseSector根据sectors[]表推算通常固定传 64KB 或 4KB。对齐检查一定要做地址没对齐就发擦除命令器件的实际行为是忽略低位、擦掉一个完整扇区把相邻数据一起干掉而且返回成功。整片擦除EraseChip的处理要谨慎。SPI Flash 通常有 chip erase 命令0xC7或0x60但 4MB 器件整片擦除可能要几十秒远超toErase给的 50 秒预算也不方便中途报进度。我的做法是EraseChip里按扇区循环擦本质和EraseSector一样只是范围覆盖全片。缺点是慢优点是可控、可中断、可加 GPIO 翻转看进度。校验这边用内存映射读回最方便。QSPI 切到内存映射模式后0x90000000 起始的地址空间就是 Flash 内容的镜像直接按字节比对/* 读取并校验切到内存映射模式直接按地址比对 */ unsigned long Verify(unsigned long adr, unsigned long sz, unsigned char *buf) { unsigned long i; if (hal_qspi_flash_memory_mapped() ! 0) return adr; /* 切模式失败 */ for (i 0; i sz; i) { /* volatile 防止编译器把读操作优化掉 */ if (*(volatile unsigned char *)(adr i) ! buf[i]) { return adr i; /* 返回第一个不一致的地址 */ } } return 0; /* 0 表示校验通过 */ }hal_qspi_flash_memory_mapped()里做的事是配置一条快速读命令比如0xEB四线读指定 dummy cycles 和地址/数据线宽然后调HAL_QSPI_MemoryMapped。dummy cycles 必须和器件手册一致配少了读出来整体错位现象是「每 16 字节里有几个字节是下一个字节的值」非常有辨识度。5. 避坑与排查五个真实翻车现场的定位思路5.1 加载与运行期算法根本没跑起来现场一点下载立刻弹 Cannot Load Flash Programming Algorithm!现象是下载还没开始就报错日志里连擦除都没发生。原因基本是两个一是 RAM for Algorithm 的 Size 给小了算法代码段放不下二是起始地址和你的应用占用的 RAM 重叠加载时把应用的数据段踩了或者反过来。解决方式是回到 FLM 工程看 map 文件里 Code RO Data RW Data 的总大小Size 至少给它的 1.5 倍起始地址挑一块应用不用的 RAM 区H7 上 AXI SRAM 前 64KB 一般没人动用它比较稳。改完重新编译 .FLM 并重新添加Keil 会缓存算法信息不重新添加可能还是旧值。现场二算法加载成功一进 Init 就 HardFault 或者直接超时现象是下载进度条动了一下就报错用调试器 attach 上去看 PC 停在一个异常处理里。原因通常是算法里开了中断——CubeMX 生成的代码默认会HAL_Init()里配 NVIC、开 SysTick 中断而算法运行期间中断向量表无效一进中断就跑飞。解决方式是把生成代码里的 NVIC 配置、HAL_Init里开中断的部分全部去掉只保留时钟和外设初始化。另外检查一下.sct或者启动文件里有没有使能全局中断的语句__enable_irq()这类调用在 FLM 里一律删掉。现场三擦了也写了校验也过了复位后程序不跑这个最迷惑因为 MDK 全绿。原因多半在FlashDev.cDevAdr和实际的内存映射地址不一致或者sectors[]表没有覆盖到固件真正落的那段地址。表现是固件的前半段被正确烧写后半段 MDK 认为「不在我的管辖范围」直接跳过校验也只校验了写过的那部分。解决办法是打开下载日志比对实际写入的地址范围和sectors[]累加出来的范围两者必须完全一致。另一个可能是valEmpty填错导致 MDK 判断空片逻辑出错这个改起来只需一个字节。5.2 擦写与校验跑起来了但结果是错的现场四Verify 报校验失败但读出来数据明明是对的现象是每次下载都红字提示校验错误给了个地址用调试器看那个地址的内容和源文件一模一样。原因基本确定是 D-CacheH7 带 D-Cache内存映射区 0x90000000 如果没有配置成 non-cacheableCPU 读的是缓存里的旧值而算法刚写完的数据还在总线上没落到缓存。解决方式有两种在Init里调SCB_DisableDCache()直接关掉简单粗暴FLM 场景够用或者配一段 MPU region 把 0x90000000 起始的地址空间设成 Normal Non-cacheable。关缓存最省事验证阶段先用这个性能敏感了再上 MPU。现场五小固件能烧大固件烧到一半失败现象是 100KB 以内一切正常超过某个大小就报超时或者校验错。三个原因按概率排一是toProg/toErase给得太小大扇区擦除实际耗时超过预算MDK 提前放弃二是驱动里qspi_wait_busy的超时循环次数不够器件还在写你就发下一条命令三是 QSPI 时钟配得太高长距离走线或者劣质排线下误码率上来了。解决顺序是先把两个超时值放大到 20000/50000不行再把 QSPI 分频从 2 改成 4 试最后才怀疑硬件。这三步顺序很重要反着来会浪费大量时间在换板子上。6. 二次开发把 FLM 改成可调试工程的完整改造清单正常的 FLM 工程没法像普通工程那样下断点单步因为它的入口不是main是调试器按符号调用的那几个函数。想验证FlashPrg.c里某一行到底有没有执行、adr参数传进来是什么值就得把它改造成「能在 RAM 里跑起来、能被 J-Link attach」的可调试工程。改造清单有五条缺一条都调不起来。第一去掉 C/C 页里的 Position Independent Code 选项第二去掉 Asm 页里的 Read Only / Read-Write Position Independent 选项第三使能 Linker 页的 Use Memory Layout from Target Dialog第四在 Manage Run-Time Environment 里使能 Startup 组件第五删掉工程里手动加入的System_stm32h7xx.c改用 Startup 自动带进来的那份。改造完还得加一个main.c当入口把算法函数挨个调一遍/* 调试专用 main顺序调用 FLM 各函数配合断点逐段验证 */ int main(void) { uint8_t pattern[256]; uint32_t i, ret; SystemCoreClockUpdate(); for (i 0; i sizeof(pattern); i) pattern[i] (uint8_t)i; ret Init(EXT_FLASH_BASE, 0, 0); /* 1. 初始化 解保护 */ if (ret ! 0) { while (1); } /* 断点停这里看返回值 */ ret EraseSector(EXT_FLASH_BASE); /* 2. 擦一个扇区 */ if (ret ! 0) { while (1); } ret ProgramPage(EXT_FLASH_BASE, 256, pattern); /* 3. 写一页 */ if (ret ! 0) { while (1); } ret Verify(EXT_FLASH_BASE, 256, pattern); /* 4. 读回校验 */ if (ret ! 0) { while (1); } UnInit(0); while (1); /* 到这里说明全链路通了 */ }这段代码的价值在于把「MDK 调用算法」这个过程还原成了你能单步的普通函数调用。每个if (ret ! 0)后面挂一个while(1)跑起来停在哪一个就说明那一步失败比在下载日志里猜快十倍。断点可以下在hal_qspi_flash_write内部直接看拆页算出来的chunk对不对也可以下在SetWPS之后用调试器的 Memory 窗口看 0x90000000 一块是不是全 0xFF。这里有个我踩过的坑要提醒改造前的 FLM 工程 IROM1 指向 RAM 地址改造后由于要使能 Use Memory Layout from Target Dialog链接脚本会重新生成如果器件选型里 RAM 区的划分和你想的不一样改完可能链接失败显示空间不足。这时候别急着改 scatter先回 Target 页确认 IRAM1 的起始和大小FLM 场景下我一般把 IROM1 和 IRAM1 都指向同一块 RAM让代码和数据挤在一起简单直接。我现在的习惯是任何一块新外部 Flash 上手先不动应用工程单独建一个 FLM_DEBUG 工程用上面这个 main 把 Init → Erase → Program → Verify 跑通一遍把每一步的耗时用 GPIO 翻转记下来对着 datasheet 核对是否合理4KB 擦除一般在几十毫秒量级256 字节页编程在几百微秒量级数字对上了再去挂到应用工程里下载。这个流程看着多花半小时但省掉的是「下载报错 → 怀疑硬件 → 换板子 → 换半天发现是扇区表写漏了」这种一整天的消耗。从那以后我每次给新型号适配 FLM都强制先走一遍这个 DEBUG 工程不再直接拿下载器试。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询