STM32H7外部QSPI Flash FLM下载算法开发与排错
发布时间:2026/11/3 12:37:19 锦皓数字建站

简介这份应用笔记面向使用STM32H7XX系列MCU、需要在KEIL-MDK环境下把程序烧录到外部SPI Flash的嵌入式开发者聚焦自定义FLM下载算法的开发与落地解决外部Flash无法直接套用MDK内建算法的问题。内容从MDK下载算法基础知识讲起依次覆盖FLM工程建立、SPI Flash下载算法制作与使用以及FLM_DEBUG调试工程建立并强调开发中关闭中断、改用查询方式、重新实现HAL_InitTick、HAL_GetTick和HAL_Delay等要点同时涉及QSPI/OCTOSPI接口函数、FlashDev.c结构体配置、时序参数调整、写保护处理与GPIO辅助观察。压缩包仅含1个docx文档约421KB篇幅精炼目录清晰便于按模块查阅。已有2499人学习适合需要为外部Flash定制烧录算法、排查下载与校验问题的工程师参考可从中获得工程搭建思路、接口实现要点和调试排错方法。1. 外部 Flash 跑程序卡住的往往不是驱动在 STM32H7 上把程序搬进外部 QSPI Flash 之后最常见的翻车点不是驱动没写对而是按下 keil mdk flash 下载按钮时弹出一句 Cannot Load Flash Programming Algorithm或者干脆报地址越界。MDK 对片内 Flash 自带下载算法外部 SPI Flash 没有得自己做一个 .FLM这段与地址无关的代码会被调试器搬进芯片内部 RAM 跑起来代替 MDK 完成初始化、擦扇区、写页、回读校验。这份 STM32H7XX 外部 FLASH FLM 下载算法笔记从模板改造、IOC 最小化配置、FlashDev.c 与 FlashPrg.c 的落地写法一路讲到算法装不上、下载一半挂死、校验不过的排查路径。它适合已经能把外部 Flash 读起来、但还没打通下载链路的 H7 开发者先跑通片内 Flash 调试的人看到这份笔记踩的坑会少一半。2. 从模板起一个 FLM 工程最小化 IOC 与不依赖 SysTick 的延时2.1 FLM 是加载进内部 RAM 里一段一段跑的所以什么都不能开很多人第一次做下载算法习惯性地把平时工程里的 BSP 一股脑塞进去结果第一次下载就卡在初始化。原因在于 FLM 的运行环境和普通固件完全不同MDK 在下载阶段把 .FLM 文件当作一段“地址无关代码”加载到芯片内部 RAM 的指定区域加载地址在调试配置里设然后按需调用里面的 Init、EraseSector、ProgramPage、Verify。此时芯片刚复位完成向量表没建立中断控制器没初始化也没有任何线程或调度器在跑。只要有一个中断触发CPU 就会跳到未定义的地址表现为下载器报通讯失败或者直接挂起。所以第一条铁律是FLM 里不要开任何中断全部改用查询方式操作外设。轮询标志位、轮询忙状态、轮询写完成这套做法在功能上完全不输中断代价只是下载时 CPU 空转而下载本来就是一件不需要考虑功耗和实时性的事。第二条更隐蔽。HAL 库大量函数内部靠“等超时”来兜底比如 QSPI 发送、Flash 写等待写法都是先记一个起始时刻然后循环判断HAL_GetTick() - start timeout。而HAL_GetTick()返回的毫秒计数来自uwTickuwTick是在 SysTick 中断服务函数里自增的。中断一关uwTick永远停在零所有带超时判断的 HAL 函数就会一直等到那个永远到不了的超时值实际表现是下载卡死在初始化阶段过好几分钟才被调试器判为失败。更麻烦的是HAL_Delay()——很多 Flash 手册要求上电后延时若干毫秒再发命令HAL_Delay 一卡整条链路就断了。结论很直接在 FLM 工程里必须重新实现HAL_InitTick、HAL_GetTick、HAL_Delay这三个函数。它们在 HAL 里都带__weak属性你在自己工程里定义一份同名同签名的实现链接器就会用你的版本覆盖库里的版本不需要改任何库文件。2.2 模板选取与工程文件组织Keil 安装路径下有一份官方模板大致位置是ARM/PACK/ARM/CMSIS/version/Device/_Template_Flash里面就是下载算法的三件套FlashOS.h、FlashDev.c、FlashPrg.c。用这份模板起步可以但更省事的是找一份已经能跑的 H7 QSPI FLM 工程当底子改——时钟树、QSPI 引脚、命令序列都是现成的你只需要把地址和容量改成自己板子的参数。工程里该加哪些文件我的习惯是这样FLM_H7_QSPI/ ├── FlashDev.c # 器件描述表告诉 MDK 这块 Flash 长什么样 ├── FlashPrg.c # 六个回调函数的实现算法核心 ├── FlashOS.h # MDK 固定头文件不要改 ├── qspi_flash.c / .h # 自己的 QSPI 底层初始化、擦、写、读 ├── bsp_clock.c / .h # 时钟配置只保留算法必需的几个时钟 └── stm32h7xx_hal_conf.h # 裁剪过的 HAL 配置关掉用不上的模块启动文件交给 Run-Time Environment 自动带入不要手动往工程里加system_stm32h7xx.c。这一点后面第 6 章会展开——调试版工程必须靠 Startup 自动带进来的那份手动加会和启动文件里的定义冲突链接时直接报重复符号。还有一个容易忽略的开关模板工程默认可能勾了位置无关代码Position Independent相关的选项。这项在普通应用工程里是好事但在 FLM 里地址无关是由 MDK 的加载机制保证的工程里再叠一层反而会让函数指针的相对偏移算错表现为调用 ProgramPage 时跳到乱七八糟的地方。新建工程后第一件事把 C/C 和 Asm 两处的 Position Independent 选项都取消掉。2.3 IOC 配置的三个原则最小化、可观测、先内部时钟用 CubeMX 生成配置时最容易犯的错是“顺手全配了”。FLM 的 IOC 只有三个原则。第一最小化。只配时钟、QSPI 或 OCTOSPI 控制器、以及它们用到的 GPIO 复用。串口、定时器、DMA、文件系统一个都不要。多一个外设就多一份初始化就多一个在下载阶段挂死的可能。第二可观测。留两三个空闲 GPIO 当“探针”在 Init、擦除循环、写等待循环里翻转电平。后面用逻辑分析仪或者示波器一夹就能看出算法到底走到哪一步、卡在等什么比纯靠调试器打断点快得多。这块算是血泪经验第一次做 FLM 时没有探针一个写等待超时的问题查了整整两天。第三时钟源先内部后外部。外部晶振在算法里要等 HSE 起振如果板子上晶振焊接不良、负载电容不匹配或者起振时间比算法里的等待时间长初始化会直接卡死而且这种卡死和下一条命令发不出去的表现几乎一样很难区分。我的做法是先用内部 HSI 把整条链路初始化、擦、写、校验跑通确认逻辑没问题再换成外部晶振重测一遍时序裕量。两份 IOC 各存一份调试时随时切换。IOC 里真正需要关注的配置项不多配置项建议取值说明系统时钟先用 HSI再切 HSE内部时钟起振确定便于隔离问题QSPI 时钟分频先慢后快首次调试用最低分频跑通后再提QSPI 模式间接写 内存映射读写擦用间接模式校验用映射模式Flash 容量与实物一致写错会导致扇区表越界调试 GPIO2~3 个在关键循环里翻转用于定位卡点生成代码后把qspi.c、gpio.c、rcc.c里真正用到的部分整理进自己的qspi_flash.c不要整个main.c搬过来——CubeMX 生成的main()里有 HAL_Init 和 SysTick 配置那套东西和 FLM 的运行环境是冲突的。2.4 重写三个 HAL 弱函数用 DWT 做毫秒基准不依赖 SysTick 中断的毫秒计时最省事的做法是借 Cortex-M7 内核自带的 DWT 周期计数器。它就是个自由运行的 32 位计数器每个 CPU 周期加一不需要任何中断参与。/* 覆盖 HAL 的弱函数不再启动 SysTick 中断改用 DWT 周期计数器 */ HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority) { (void)TickPriority; CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; /* 打开 DWT 访问权限 */ DWT-CYCCNT 0; /* 计数器清零 */ DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; /* 启动计数 */ return HAL_OK; } uint32_t HAL_GetTick(void) { /* SystemCoreClock 是当前 CPU 主频480MHz 时 1ms 对应 480000 个周期 */ return DWT-CYCCNT / (SystemCoreClock / 1000U); } void HAL_Delay(uint32_t Delay) { uint32_t start HAL_GetTick(); /* 死等减法写法天然兼容计数器回绕 */ while ((HAL_GetTick() - start) Delay) { __NOP(); } }这段代码的关键点有三个。第一HAL_InitTick里没有调用HAL_SYSTICK_Config所以 SysTick 中断从头到尾都不会开符合 2.1 节的要求。第二HAL_GetTick的除数写成SystemCoreClock / 1000U而不是硬编码换主频时不用改代码。第三DWT 的 CYCCNT 是 32 位480MHz 下大约 8.9 秒回绕一次HAL_GetTick()的返回值因此也会周期性归零用(now - start) Delay这种无符号减法写法回绕前后算出来的差值都是对的但如果哪里写成if (now start Delay)就会在回绕点翻车。还有一个细节SystemCoreClock这个全局变量要靠 SystemInit 更新而 FLM 工程里没有完整的启动流程。稳妥的做法是在Init()里显式调一次时钟配置函数然后手动把SystemCoreClock赋成实际频率别指望它自己是对的。如果这里没对上HAL_Delay(10)可能实际只延时了 3 毫秒Flash 上电时序不够写进去的数据就是错的。3. FlashDev.c 与地址空间把 4MB 外部 Flash 描述成 MDK 认得的器件3.1 存储映射起始地址先定死后面所有东西都围着它转外部 QSPI Flash 通过内存映射方式挂到总线上常见基地址是 0x90000000。这个地址不是随便挑的它由芯片的总线矩阵和 QSPI/OCTOSPI 控制器决定具体查参考手册里的存储器映射表。定下来之后三处必须严格一致第一处是FlashDev.c里的DevAdrMDK 靠它判断下载目标落在哪个地址区间。 第二处是应用工程的分散加载文件sct或者链接脚本代码段、只读数据段都要放到这个区间。 第三处是算法内部所有地址换算——EraseSector和ProgramPage拿到的adr是绝对地址而 QSPI 的擦写命令要的是相对 Flash 芯片内部的偏移所以每个函数开头都得减一次基地址。这三处只要有一处不对症状就千奇百怪地址算多了一次减法写进去的数据整体偏移链接脚本没改代码还在片内 Flash 里下载算法看起来“成功”了但程序跑的是旧代码。3.2 FlashDevice 结构体逐字段填FlashDev.c里只有一个全局常量但每个字段都直接影响下载行为值得一个一个抠。/* FlashDev.c —— 告诉 MDK 这块外部 Flash 长什么样 */ #include FlashOS.h struct FlashDevice const FlashDevice { FLASH_DRV_VERS, /* 驱动版本宏固定值不要改成数字 */ H7_QSPI_4MB, /* 出现在 MDK 算法下拉框里的名字自己起 */ EXTSPI, /* 器件类型外部 SPI Flash */ 0x90000000, /* 存储映射起始地址与链接脚本一致 */ 0x00400000, /* 总容量 4MB */ 0x100, /* 编程页大小 256 字节 */ 0, /* 保留字段必须为 0 */ 0xFF, /* 擦除后的内容NOR Flash 都是 0xFF */ 20000, /* 页编程超时单位毫秒宁大勿小 */ 50000, /* 扇区擦除超时单位毫秒宁大勿小 */ /* 扇区表成对给出「扇区大小, 起始偏移」按地址升序排列SECTOR_END 收尾 */ 0x001000, 0x000000, /* 4KB 小扇区从偏移 0 开始铺满整片 */ SECTOR_END };DevType这个字段要特别注意。FlashOS.h里定义了ONCHIP、EXT8BIT、EXT16BIT、EXT32BIT、EXTSPI几个宏外部 SPI Flash 用EXTSPI。用错类型在部分 MDK 版本上不会立刻报错但会让 MDK 在计算校验和、处理地址对齐时走错分支。两个超时字段是最容易抠得太紧的地方。有人按数据手册的典型擦除时间填个 500ms结果批量下载时偶尔报超时——Flash 的擦除时间随温度、寿命、扇区位置波动很大数据手册给的是典型值最坏值可能是典型值的四五倍。这两个字段填大了没有任何副作用只在你真的卡死时多等一会儿填小了就是随机失败的来源。我的习惯是直接给它 20 秒和 50 秒调试时反而更方便定位——真卡住了等 20 秒报错比等 500 毫秒报错更容易判断是哪一步出问题。3.3 扇区表的排列方式决定了擦除行为扇区表是FlashDevice里最需要动脑子的部分。它是一组(扇区大小, 起始偏移)对MDK 读进去之后会自己推导出每个扇区的边界然后在你调用EraseSector时传入对应的绝对地址。规则是条目按地址升序排列相邻两个条目的起始偏移之差就是前一种扇区重复的次数最后必须以SECTOR_END结束。整个表必须完整覆盖0到szDev的范围不能有空洞也不能重叠。有空洞的后果是某段地址 MDK 认为不可擦写下载时报“地址不在算法范围内”重叠的后果更糟——你只想擦一个 4KB 扇区实际把后面一大片也擦了。外部 QSPI Flash 常见两种组织整片 4KB 小扇区或者“底部若干 4KB 中间 32KB 上部 64KB”的混合结构。整片 4KB 写起来最简单0x001000, 0x000000, /* 4KB × 1024铺满 4MB */ SECTOR_END混合结构则要分段列出每一段给出该段的扇区大小和该段的起始偏移0x001000, 0x000000, /* 前 64KB 用 4KB 扇区 */ 0x010000, 0x010000, /* 中间用 64KB 块擦除速度快得多 */ SECTOR_END选哪种取决于你的下载习惯。整片 4KB 的好处是擦除粒度细改一行代码只擦一小块下载快坏处是全片擦除时命令数量多一次完整烧录可能要多花几秒。混合结构适合“程序区 数据区”的布局数据区用小扇区频繁改程序区用大块一次擦完。这里没有标准答案但有一点是确定的扇区表必须和芯片实际结构完全一致不能为了“擦得快”把 4KB 扇区谎报成 64KB——MDK 会按 64KB 的边界去发擦除命令落在错误边界的擦除会擦掉不该擦的内容。3.4 编程页大小与 MDK 的分包逻辑szPage填的是这颗 Flash 一次页编程能接受的最大字节数NOR Flash 通常是 256 字节。这个值决定了 MDK 在下载时如何切分数据——它会按szPage对齐后一段一段调用ProgramPage。注意是“最大”不是“必须”。ProgramPage收到的sz参数可能小于szPage尤其是文件末尾那一小段。所以函数里不能假设sz恒等于 256必须按实际长度处理并且要支持跨页写入——一段数据从第 200 字节开始、长度 100 字节就跨了两个物理页靠一个写命令是写不完的要自动拆成两次。填错的典型症状是“下载完成但数据错乱”szPage报大了MDK 一次给你 512 字节而 Flash 内部缓冲区只有 256 字节超出的部分被丢弃或者覆盖回页首报小了MDK 会切成很多小段功能上没问题但下载慢得离谱。4. FlashPrg.c 六个函数初始化、擦除、编程、校验的落地写法4.1 Init / UnInit上电时序与引脚状态Init是 MDK 调用的第一个函数签名固定为int Init(unsigned long adr, unsigned long clk, unsigned long fnc)。三个参数里adr是本次下载的目标起始地址clk是 MDK 传进来的 CPU 时钟频率单位 Hzfnc是功能码。返回值 0 表示成功非 0 表示失败MDK 会据此决定是否继续。clk这个参数很有用——你不需要自己去猜主频直接用它在算法内部换算超时周期。但要注意它给的是 CPU 时钟QSPI 控制器的分频还得按自己的配置算。/* FlashPrg.c —— MDK 在下载阶段回调的入口函数 */ #include FlashOS.h #include qspi_flash.h int Init(unsigned long adr, unsigned long clk, unsigned long fnc) { (void)adr; (void)fnc; SystemCoreClock clk; /* 让 HAL_Delay 的换算基准正确 */ if (qspi_flash_clock_enable() ! 0) { return 1; /* 时钟没起来直接失败 */ } if (qspi_flash_gpio_init() ! 0) { return 2; /* 引脚复用配置失败 */ } if (qspi_flash_ctrl_init() ! 0) { return 3; /* QSPI 控制器初始化失败 */ } if (qspi_flash_read_id() ! 0) { return 4; /* 读不到 ID多半是硬件或时序问题 */ } qspi_flash_exit_power_down(); /* 有些片子复位后处于深睡要发唤醒命令 */ HAL_Delay(2); /* 手册要求的上电稳定时间短一点没关系 */ qspi_flash_disable_write_protect(); /* 解除全部地址的写保护见 4.3 */ return 0; } int UnInit(unsigned long fnc) { (void)fnc; qspi_flash_deinit(); /* 引脚恢复为模拟态关掉 QSPI 时钟 */ return 0; }返回值带上不同的错误码是个好习惯。MDK 只会告诉你“算法执行失败”但如果你在Init里把每一步的返回值区分开再用调试器看一眼 CPU 停在哪一行就能立刻知道是时钟、引脚、控制器还是芯片本身的问题。同理第 2.3 节说的那几个调试 GPIO在这里也该翻一下示波器上一看就知道执行到哪一步了。UnInit相对轻松但也别省。有些下载器在一次下载结束后会反复调用 Init/UnInit 对如果 UnInit 没把引脚恢复干净下次 Init 里重新配复用就可能失败。4.2 EraseSector / EraseChip擦除是耗时最长的一步EraseSector收到的是绝对地址先换算成芯片内部偏移再发擦除命令。扇区大小要和FlashDev.c里的扇区表一致——MDK 按表里的粒度调用你你按同一个粒度去擦才不会有错位。int EraseSector(unsigned long adr) { uint32_t offset adr - QSPI_FLASH_BASE; /* 0x90000000 起的偏移 */ qspi_write_enable(); /* 擦除前必须发写使能 */ if (qspi_cmd_sector_erase(offset) ! 0) { return 1; /* 命令发送失败 */ } /* 不返回则继续轮询忙状态超时值取 FlashDev.c 里配的 50s 量级 */ if (qspi_wait_wip(ERASE_TIMEOUT_MS) ! 0) { return 2; /* 等待擦除完成超时 */ } return 0; } int EraseChip(void) { qspi_write_enable(); if (qspi_cmd_chip_erase() ! 0) { return 1; } /* 整片擦除时间最长超时要给足50000ms 起步 */ if (qspi_wait_wip(CHIP_ERASE_TIMEOUT_MS) ! 0) { return 2; } return 0; }这里有个容易忽略的点擦除命令发出之后芯片进入忙状态此时只能发读状态寄存器命令发别的命令一律被忽略或者返回错误。所以qspi_wait_wip的循环体里只做一件事——发0x05读状态寄存器检查 bit0。如果有人为了“省时间”在等待循环里顺手做点别的事比如提前准备下一段数据发过去大概率会被丢弃最后表现为随机数据错误。另一个坑是写使能。SPI Flash 的写使能位WEL在每次擦除、编程命令执行完之后会自动清零所以每发一条擦除或编程命令之前都要重新发一次0x06。忘记发的表现是命令看起来发出去了忙状态也等了但读回来数据没变——这类现象很像时序问题其实是漏了一个字节的命令。4.3 ProgramPage 与任意长度写跨页和写保护是两个独立问题ProgramPage是调用最频繁的函数签名int ProgramPage(unsigned long adr, unsigned long sz, unsigned char *buf)。它的职责是把buf里的sz字节写到adr并且保证字节数可以是任意值。底层我一般分两层写一个处理任意长度、自动跨页的hal_qspi_flash_write一个薄薄的ProgramPage只做地址换算和错误码映射。这样底层函数在调试工程里可以单独调用测试不用每次都跑完整下载流程。/* 支持任意长度、自动跨页的写函数 */ int hal_qspi_flash_write(uint32_t addr, const uint8_t *buf, uint32_t len) { while (len 0) { /* 计算当前页还剩多少字节可写避免越页 */ uint32_t page_left QSPI_PAGE_SIZE - (addr % QSPI_PAGE_SIZE); uint32_t n (len page_left) ? len : page_left; qspi_write_enable(); /* 每次写命令前都要重新使能 */ if (qspi_cmd_page_program(addr, buf, n) ! 0) { return -1; } if (qspi_wait_wip(PROGRAM_TIMEOUT_MS) ! 0) { return -2; /* 页编程超时 */ } addr n; buf n; len - n; } return 0; } int ProgramPage(unsigned long adr, unsigned long sz, unsigned char *buf) { uint32_t offset adr - QSPI_FLASH_BASE; /* sz 不一定等于 szPage文件末尾那一段通常更短 */ if (hal_qspi_flash_write(offset, buf, sz) ! 0) { return 1; } return 0; }跨页这个问题值得单独说。NOR Flash 的页编程有个物理约束一条页编程命令写入的数据不能跨越页边界一旦跨了地址会回卷到页首把前面的数据覆盖掉。表现出来就是“下载完成、校验也过、但程序跑起来某几个函数是乱的”——因为校验逻辑如果也按同样的错误方式读不一定能发现。page_left那行代码就是防这个的。写保护是另一个独立问题。很多 QSPI Flash 出厂或者复位后状态寄存器里的块保护位是置位的某些地址段处于写保护状态。此时擦除命令会被静默忽略忙状态也正常返回读回来还是老数据。所以Init里要显式做一次“解除全部写保护”先读状态寄存器判断保护位再发写状态寄存器命令清掉最后轮询确认。这套动作不能省尤其是拿到一块二手板子或者别人调过的板子时状态寄存器里可能是任何值。/* 解除写保护并等待生效Init 里调用一次即可 */ int qspi_flash_disable_write_protect(void) { qspi_write_enable(); /* 写状态寄存器同样需要 WEL */ if (qspi_cmd_write_status(0x00) ! 0) { /* 清掉 BP/TOP 等保护位 */ return -1; } return qspi_wait_wip(PROGRAM_TIMEOUT_MS); }有些型号的保护位在状态寄存器 2 或者 3 里需要0x31、0x11这类扩展命令配合具体查自己芯片的手册。写完之后一定要回读确认别假设命令一定生效。4.4 Verify读回比较注意缓存Verify的职责是把刚写进去的数据读回来和源缓冲区逐字节比较。签名int Verify(unsigned long adr, unsigned long sz, unsigned char *buf)返回 0 表示一致返回非 0 表示不一致——很多实现返回的是出错位置方便定位。实现上有两条路。第一条是走间接读模式一个字节一个字节地发读命令慢但绝对可靠。第二条是切到内存映射模式把 Flash 当成一块普通内存直接memcmp速度快一个数量级但有个前提QSPI 控制器必须真的进入了映射模式而且地址窗口配置正确。int Verify(unsigned long adr, unsigned long sz, unsigned char *buf) { uint32_t offset adr - QSPI_FLASH_BASE; if (qspi_flash_memory_mapped() ! 0) { /* 切到内存映射模式 */ return 1; } /* 映射模式下 Flash 内容可以像普通内存一样访问 */ const uint8_t *flash_ptr (const uint8_t *)(QSPI_FLASH_BASE offset); for (uint32_t i 0; i sz; i) { if (flash_ptr[i] ! buf[i]) { return (int)(offset i 1); /* 返回 1 基的出错偏移 */ } } return 0; }如果芯片开了 D-Cache这里就是最经典的陷阱映射窗口的地址可能被缓存过flash_ptr[i]读到的其实是缓存里的旧值校验永远通过但实际 Flash 里是错的。正确做法是在进入映射模式之后、比较之前把对应地址范围的数据缓存无效化对 Cortex-M7 是SCB_InvalidateDCache_by_Addr或者干脆把这块地址配成不可缓存区域。第一条路虽然慢但完全没有这个顾虑调试阶段我倾向先用间接读把逻辑跑通确认时序和命令都没问题再换成映射读提速。5. 避坑与排查五个真实翻车现场5.1 报错提示算法无法加载或者下拉框里根本找不到现象调试配置的 Flash Download 页里点 Add找不到自己编出来的算法或者添加了点下载时报 Cannot Load Flash Programming Algorithm。原因三种可能。一是 .FLM 文件没放到 MDK 会扫描的目录里放在工程输出目录是不行的二是文件名和FlashDev.c里DevName字段不一致MDK 靠文件名匹配三是 RAM for Algorithm 的 Size 设得太小算法被加载到 RAM 时空间不够调试器直接报加载失败。解决把生成出来的 .FLM 复制到 MDK 安装目录下的ARM/Flash这是全局目录所有工程都能看到比塞进某个器件包的CMSIS/Flash目录更好管理重装或者升级器件包时不会丢。然后在调试配置里把 RAM for Algorithm 的起始地址和 Size 按芯片内部 RAM 的实际情况调大一般 8KB~16KB 起步算法里如果带了较大缓冲还要再留余量。这个值给大了不会浪费什么给小了必挂。5.2 下载进度条卡在中途几十秒后报超时现象点下载后进度条走到某个百分比就不动了等很久报 timeout重试有时候又能过。原因绝大多数是擦除或编程的等待逻辑没写对。常见的是qspi_wait_wip里的超时判断用了HAL_GetTick()但HAL_InitTick没重写、SysTick 没跑HAL_GetTick()恒返回 0(now - start) timeout永远成立循环出不来。另一种是FlashDev.c里的toProg/toErase填得太小芯片还在忙MDK 已经判超时并放弃了。解决先确认三个 HAL 弱函数确实被覆盖了——在HAL_GetTick里下一个断点看能不能命中或者用调试 GPIO 在等待循环里翻转示波器上看有没有波形。确认没问题后把FlashDev.c里两个超时字段统一放大到 20000 和 50000 毫秒先用大值把功能跑通等整套流程稳定了再考虑要不要收紧。这类玄学问题九成是超时值或者时钟基准的问题别一上来就怀疑硬件。5.3 下载显示成功程序跑起来却直接进 HardFault现象MDK 提示 Programming Done、Verify OK复位后程序不跑调试器一挂上去发现停在 HardFault。原因地址映射没对齐。要么链接脚本里的代码段地址和FlashDev.c的DevAdr不一致下载算法把数据写到了 A 区间程序却从 B 区间取指要么算法里减基地址的运算多减或者少减了一次写进去的内容整体偏移了几百字节恰好落在一个“看起来有数据”的位置校验时因为比较的也是同一块错位区域反而通过了。解决下载完成后用调试器的内存窗口直接看 0x90000000 起始的几个字对照编译出来的 .map 文件里第一个函数的地址看是不是同一份内容。再检查链接脚本确认ROM区域的起始地址、大小和算法描述完全一致。这一步不要靠猜靠对地址。另外一个容易忽略的点是时钟——算法里配置的 QSPI 时钟分频和应用工程里配的可能不一样如果算法里跑得太快读出来的内容在边界上可能不稳表现出来也是随机跑飞。5.4 第一次下载正常第二次必失败断电才能恢复现象上电后第一次下载成功不断电再下一次就报错必须拔电重来。原因芯片还停留在上一次操作后的状态里。可能是 QSPI 控制器没有复位还处在内存映射模式此时发间接模式命令会被忽略也可能是写保护状态寄存器在某些操作后被重新置位还有一种情况是算法跑完之后没有正确UnInit引脚还挂在 QSPI 复用上下次Init重新配置时冲突。解决在Init开头先做一次控制器软复位——关掉 QSPI 使能位、清掉配置寄存器、等几个周期再重新初始化。同时在UnInit里把 QSPI 控制器彻底关掉、引脚切回模拟态。如果还不行在Init里加一次“退出映射模式”的显式命令有些控制器切回间接模式需要额外一步。这类问题的排查方法是第二次下载失败时不要断电直接用调试器看 QSPI 控制器的几个关键寄存器值和刚上电时的值对比差异就是线索。5.5 校验能通过但把 Flash 拆下来用编程器读内容对不上现象MDK 的 Verify 一路绿灯但用外部编程器读出来的内容和源文件不一致或者程序运行结果和预期差一点。原因Verify 走了内存映射模式读的是 D-Cache 里的旧数据。写操作通过间接模式直接进了 Flash但映射窗口那块地址在缓存里还是写之前的内容比较的时候自然全对。这个坑在开了 Cache 的 H7 上尤其常见因为默认配置下这块区域是可缓存的。解决两条路。稳妥的一条是把 Verify 改成走间接读一个字节一个字节地发读命令慢但结果可信性能优先的一条是在比较之前对目标地址范围做一次缓存无效化并且把 QSPI 映射区域配置成不可缓存或者写穿透。我一般两个都做——间接读用来做最终确认映射读加缓存无效化用来做快速预检两边的结果必须一致才认为真的写对了。从那以后我每次改完 Verify 都强制用编程器抽读一段比对不再只信 MDK 的那句 Verify OK。6. FLM_DEBUG把算法从黑匣子里拽出来单独调试正常的 FLM 工程是没法像普通工程那样下断点单步的——它不被直接烧进 Flash而是被 MDK 在下载阶段动态加载到 RAM 里执行你按 F5 启动调试跑的其实是应用工程算法根本没被加载。所以在算法开发的早期必须专门做一个可调试的版本。改造点就四个缺一不可。第一去掉 C/C 选项里的 Position Independent 相关勾选第二Asm 选项里同样去掉第三Linker 页里勾上 Use Memory Layout from Target Dialog让链接地址从 Target 对话框里取而不是用分散加载文件第四在 Manage Run-Time Environment 里把 Startup 勾上由它自动带入启动文件同时把工程里手动添加的system_stm32h7xx.c删掉——两个启动文件同时存在会报重复定义而且 RTE 带进来的那份才是和调试环境匹配的。改完之后工程就变成一个普通的、能烧到内部 Flash 或者内部 RAM 里跑的工程了。这时候可以直接在FlashPrg.c的六个函数里下断点用一个测试用的main()依次调用 Init → EraseSector → ProgramPage → Verify每步之后用调试器的内存窗口看 Flash 映射区域的内容。这个流程跑通了再切回真正的 FLM 工程编译剩下的就只是加载和打包的问题。验证顺序我一般固定成五步每步都有明确的观察点步骤验证内容观察点1Init 能否读到正确的 Flash ID返回值 ID 寄存器2单扇区擦除后读回是否全 0xFF内存窗口看映射地址3写 256 字节后再读回是否一致逐字节比对4跨页写 512 字节是否越页回卷检查第 256 字节位置5整片擦除耗时是否在超时范围内计时器测实际耗时这套顺序的价值在于把问题隔离在最小范围内。第 1 步不过说明引脚、时钟、命令序列有问题跟擦写逻辑无关第 3 步不过但第 2 步过说明是页编程命令或者写使能的问题第 4 步不过基本可以断定是跨页计算写错了。按这个顺序走每个失败都能直接定位到一小段代码不至于对着“下载失败”四个字猜。最后提醒一个容易忘的细节调试版工程里为了让printf或者日志能输出有人会顺手接上串口和重定向。这套东西在真正的 FLM 工程里必须全部拿掉因为下载阶段没有串口初始化的上下文HAL 的串口发送函数会因为时钟没配而卡在等待标志位上。我现在的习惯是每次从调试版切回 FLM 版之前先把工程完整编译一遍并检查输出目录里的 .FLM 大小——如果比上次明显大了一圈多半是哪个不该加的模块又被链进来了。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。