做 Cortex-M 项目的人,Keil5 基本是每天第一个打开、最后一个关掉的软件。它的脾气也挺固定:编译过了啥事没有,一旦出问题就是两种最抓狂的形态——一是在 Objects 目录里翻不到那个.axf文件,二是点下载直接弹窗Flash Download failed - could not load file "....\xxx.axf",偶尔还捎带一句Target DLL has been cancelled或者could not load file "....axf"。这两个现象看着像两个 bug,其实在多数情况下是同一条链路上前后两段断掉的结果:前面链接阶段没跑完,后面烧录阶段又去找那个根本不存在或者已经过时的可执行文件。
这篇内容围绕 Keil5 编译不产出 .axf、以及下载时报 Flash Download failed 这一整条链路来讲,从构建流程、工程配置、Flash 算法、驱动与硬件连接几个角度把原因摊开,每一个结论都配上可复现的操作步骤和判断依据。适合刚上手 STM32 / Cortex-M 的朋友,也适合已经能跑通工程、但一换电脑或者一换芯片就卡住的工程师。我会尽量按"先分流、再定位、最后改配置"的顺序写,你能直接照着顺序查,不用靠运气反复点 Rebuild。
1. 先把链路拆开:.axf 文件和 Flash Download failed 到底是什么关系
1.1 .axf 在 Keil 构建流程里的位置
Keil MDK 的编译不是一步到位的,它走的是 Arm 工具链的标准流程:每个 .c 文件先由 armcc / armclang 编译成 .o 目标文件,汇编文件由 armasm 处理成 .o,然后由链接器 armlink 把所有 .o、库文件、分散加载文件(scatter file,.sct)拼在一起,产出一个 ELF 格式的可执行文件,扩展名就是 .axf。这个 .axf 里面包含代码段、数据段、符号表、地址信息,芯片能跑起来的东西本质上都在里面。
再往下才是烧录阶段:Keil 的调试器(ULINK、ST-Link、J-Link、CMSIS-DAP 的 .dll 驱动)需要读这个 .axf,把里面的 load 段提取出来,配合 Flash 编程算法写进芯片的 Flash。所以顺序是死的:编译成功 → 链接成功 → 生成 .axf → 下载器读取 .axf → 写 Flash。任何一环断了,后面就不可能有结果。理解了这条链,很多事情就顺了:如果编译日志里明明有 Error,你跳过它去点下载,那下载器只能去找上一次残留的旧文件,或者干脆找不到,于是弹出 could not load file。
我在早期最容易犯的错,就是看到 "Build started" 底下飘红一堆错误,随手把窗口一关就当它编过了。实际上 Keil 在编译失败时并不会自动清空 Objects 目录,上一次成功生成的 .axf 还躺在那里,你点下载,它会用旧文件烧进去,然后你调试半天发现代码改动完全没生效。这种"看起来成功了但其实是旧程序"的情况,比直接报错更消耗时间。
1.2 两条报错链路的分界点
判断是编译问题还是下载问题,有一个非常快的分界线,就是看 Output 窗口最底部那行汇总。编译阶段的汇总长这样:
".\Objects\demo.axf" - 0 Error(s), 0 Warning(s). Build Time Elapsed: 00:00:07只要出现0 Error(s),并且那一行确实打印出了 .axf 的路径,说明链接成功了,文件一定存在,问题必然在下载侧。反过来,如果底部是2 Error(s), 1 Warning(s)或者Build aborted,那就压根没进下载阶段,报错也是编译错误,先别看什么 Flash 算法。
这里有个细节值得强调:链接成功时会打印Program Size: Code=xxxx RO-data=xxxx RW-data=xxxx ZI-data=xxxx,这四组数字是判断链接是否真的完成的关键证据。很多朋友只盯着 Error 计数,其实这个 Program Size 才是"链接器真的跑完了"的签名。缺了这行,即便没看到 Error,也可能是因为工程里没有设置输出可执行文件,或者链接器被跳过了。
1.3 为什么两类报错总是一起出现
因为它们共享同一个名字:.axf。下载器在配置里记着"我要烧Objects\demo.axf",这个路径写在工程文件(.uvprojx)的 Output 配置里。如果你在 Target 选项卡里改过输出目录、改过 Name of Executable、或者把 Objects 文件夹整体删掉重建,下载器仍然按老路径去找,找不到就报 could not load file。而如果编译同时又失败,这两件事就会叠加在一起,出现"编译报错 + 下载报错"的双黄蛋。
我遇到过一次很典型的:为了清理工程,我把整个 Objects 目录删了,然后在 Keil 里直接点下载,弹窗就是 could not load file "...\Objects\demo.axf"。当时的直觉是"文件被删了,那我重新编译不就行了",重新编译确实生成了新文件,但下载还是失败——因为我在删目录之前改过工程名,输出文件名跟着变了,而 Debug 页里的一些残留配置还指着旧名字。这个坑的教训是:清理工程要用 Keil 自带的 Clean Targets,不要手工删目录,手工删很容易和工程配置不同步。
判断顺序建议固定下来:先看编译汇总有没有
0 Error(s)和Program Size,确认 .axf 真实存在;再去查下载配置。顺序反了,你会花大量时间在 Flash 算法上瞎折腾,而真正的问题只是少写了一个分号。
2. 排查顺序怎么设计才不会来回折腾
2.1 三分钟分流法
我把排查压缩成三个动作,熟练之后基本三分钟内能定性。
第一步,打开工程目录,直接去 Objects(或者你自己设置的输出目录)里看有没有 .axf,右键看修改时间。如果时间戳是几分钟前,说明编译是成功的,问题在下载侧;如果时间戳是昨天甚至更早,那今天这次编译根本没产出文件,老老实实滚回去看 Output 窗口。
第二步,在 Keil 里按 F7(Build),看底部汇总行。不要看中间的警告,直接拉到底。有没有0 Error(s),有没有 Program Size,这两个信息决定了后面所有分支。
第三步,切到 Options for Target 的 Debug 页,看右侧调试器下拉框选的是不是你现在插的那根硬件。我见过太多次,同事借走一块 ST-Link 之后,自己的工程里还选着 ST-Link Debugger,插的却是 J-Link,点下载就弹 Target DLL has been cancelled。选错驱动不会立刻报"设备未找到",它会先尝试加载 DLL,加载不上就 cancel,报出来的信息还挺吓人。
2.2 环境层面先排掉三类低级坑
有些问题跟代码一点关系都没有,纯粹是环境。
第一类是路径。Keil 的 Arm 工具链对非 ASCII 路径的支持一直不算好,尤其是老版本 MDK 和 51 的 C51 工具链。如果工程放在D:\学习资料\STM32项目\模板\,或者放在C:\Users\张三\Desktop\,编译时可能报找不到文件、链接时打不开输出,甚至程序烧进去能跑但字符串乱码。经验做法是把工程放到纯英文短路径下,比如D:\work\stm32_demo\。这条不是玄学,中间工具链调用时会走命令行,编码转换出问题的概率实实在在存在。
第二类是杀毒软件。实时防护会扫描新生成的可执行文件,.axf 是 ELF 格式,某些引擎会把它当成可疑对象隔离掉,表现就是编译时明明没报错,转头去目录里找文件没了。如果你的环境里装了带主动防御的安全软件,把工程目录加入信任列表,能省掉很多莫名其妙的麻烦。
第三类是驱动占用。ST-Link 的驱动同一时间只能被一个程序占用,如果后台挂着 STM32CubeProgrammer、STM32CubeIDE 或者厂商的烧录工具,Keil 点下载就会失败。这类失败的报错信息五花八门,有可能是 Target DLL has been cancelled,有可能是连不上 SW 设备。下载前把其他工具关干净,是最省事的做法。
2.3 什么时候该怀疑硬件
纯粹从报错文字上不太容易区分软件问题和硬件问题,我给一个粗略的经验判断:如果同一个工程、同一台电脑,换一块板子就正常,那基本是板子的问题;如果换电脑或者换线正常,那是接口和线材问题;如果所有板子都不行、但换一个工程就行,那问题在工程配置。
硬件侧最常见的三种情况:SWDIO / SWCLK 接反或虚焊;目标板没供电或者共地没接;程序里把 SWD 引脚复用成了普通 GPIO,或者进 STOP / STANDBY 低功耗模式。第三种最阴,因为它只在程序已经烧进去之后才出现,表现为"上一版能连,这一版连不上"。这时候要在 Debug 页把 Reset 方式改成 Connect under Reset,或者下载时按住复位键再松开。
3. 编译期:.axf 生成不出来的典型成因
3.1 编译错误和链接错误的区别
很多人把所有红字统称"编译报错",但处理思路完全不同。编译错误(Compiler)发生在单个源文件上,前面会带文件名和行号,比如:
..\User\main.c(58): error: #20: identifier "GPIO_InitTypeDef" is undefined这类问题的根因通常是头文件没包含、宏拼错、结构体名写错、外设库版本和芯片型号不匹配。看到#20: identifier xxx is undefined,先查头文件包含关系和库版本,别急着怀疑工具链。
链接错误(Linker)是在所有 .o 都生成完之后才出现的,报错前缀是L加数字,比如L6218E: Undefined symbol xxx (referred from yyy.o)。这说明某个函数被调用了但没找到定义,可能原因包括:对应的 .c 文件没有加入工程组、库文件没勾选、函数名在头文件里声明了但实现被条件编译掉了。链接错误出现时,.axf 一定不会生成,因为链接器根本没完成工作。
还有一类是L6406E: No space in execution regions with .ANY selector matching xxx.o,这是内存不够,代码或堆栈把 RAM / Flash 撑爆了。处理方向是查 Map 文件(勾选 Linker 页的 Linker Listing,生成 .map),看看哪个模块吃掉了最多空间,是不是有巨大的全局数组、或者某个库把整个外设驱动都拖进来了。
3.2 头文件路径和输出目录配置
cannot open source input file "xxx.h": No such file or directory这类错误,八成是 Include Paths 没配全。在 C/C++ 选项卡里点 Include Paths 右侧的按钮,把每个用到外设头文件的目录都加进去,比如..\Libraries\STM32F10x_StdPeriph_Driver\inc、..\User、..\CMSIS\Core\Include。注意这一点:路径是用相对工程文件(.uvprojx)的位置算的,所以工程文件挪了位置,这些相对路径就会失效,全部要改。这也是从别人那里拷贝工程时最常见的问题。
输出目录在 Output 选项卡,Select Folder for Objects 能改。我建议保持默认的.\Objects\不动,因为很多模板工程、脚本、CI 配置都按这个路径找文件,你改了之后,别人接手或者用脚本打包就会踩坑。顺带把 Create HEX File 勾上,做量产或脱机烧录时用得上,它是由 fromelf 从 .axf 转换出来的,不生成 .axf 的话这一项也不会执行。
3.3 Device 选型和启动文件必须匹配
Target 选项卡里的 Device 必须和板子上的芯片严格对应。选错型号不一定立刻报错,可能编译通过、下载也成功,但中断向量表和启动文件对不上,程序一跑就进 HardFault。启动文件(startup_stm32f10x_md.s 之类)要和芯片的容量等级匹配:小容量、中容量、大容量的向量表长度不一样。把大容量的启动文件用在小容量芯片上,编译不会报错,运行会跑飞,而且这种问题往往在别的机器上又能跑,非常难查。
顺带说一下 XTAL 那个灰掉的框。有朋友问过为什么 Target 里那个 MHz 输入框是灰色的改不了,这其实是正常的:对于现在这些 Cortex-M 器件,芯片时钟由 SystemInit 里的宏(比如HSE_VALUE)和时钟树配置函数决定,Keil 里那个 XTAL 值是给老式仿真器件和某些软件仿真场景用的,器件包选好之后它就由器件数据库托管了。所以时钟不对,要去改system_stm32f1xx.c里的宏,而不是去抠那个灰框。
3.4 编译与链接错误速查表
| 报错关键字 | 出现阶段 | 大概率原因 | 处理方向 |
|---|---|---|---|
#20: identifier ... is undefined | 编译 | 头文件未包含、类型名写错、库版本不符 | 补 include、核对外设库版本 |
cannot open source input file "xxx.h" | 编译 | Include Paths 缺失或路径失效 | 补齐 C/C++ 页的 Include Paths |
L6218E: Undefined symbol | 链接 | .c 未加入工程、实现被条件编译屏蔽 | 检查工程组、宏定义 |
L6406E: No space in execution regions | 链接 | Flash / RAM 溢出 | 查 .map,削减全局数组与库范围 |
L6002U: Could not open file | 链接 | 输出目录不存在或被占用 | 清理工程、重建 Objects 目录 |
| 无报错但无 .axf | 链接 | 输出名被改、目录被手工删、链接被跳过 | 核对 Output 页配置 |
这张表里最后一行是最需要留意的,因为它没有任何红字提示,你只会看到"下载失败"。
4. 下载期:Flash Download failed 的真凶排查
4.1 "could not load file ...axf" 的四种典型场景
这个弹窗字面意思很直白:下载器在指定路径找不到 .axf。但背后的成因有四种,处理方式各不相同。
第一种,编译失败还硬点下载。Keil 会先弹一个"是否继续下载上一次的结果"的询问,你点了是,它就去找旧文件。如果目录被清理过,文件不存在,于是报错。正确做法是先解决编译错误。
第二种,输出目录或文件名被改过。你在 Output 页把 Name of Executable 从demo改成了demo_v2,下载器仍然按老名字找;或者你把 Objects 换到了别的盘符,路径里带上了空格。改完之后要做一次 Rebuild,让工程配置和实际文件对齐。
第三种,手工删了 Objects 目录。前面说过,用 Clean Targets,别用手删。
第四种,路径里含中文或过长。Windows 的路径长度限制是 260 个字符,超长时工具链调用会失败,表现可能是打不开文件,也可能悄悄生成到别的地方。把工程往浅层目录挪。
4.2 Flash 编程算法:下载失败的第一现场
真正确认芯片能不能被写进去,靠的是 Flash 编程算法(Flash Programming Algorithm)。位置在 Options for Target → Utilities → 右侧 Settings → Flash Download 页。列表里应该有一条和你芯片匹配的记录,形式类似:
STM32F10x Med-density Flash 128k 0x08000000 0x00020000如果这个列表是空的,点下载会直接失败或者提示找不到算法。点右侧的 Add,从下拉里选对应型号。这里有两个容易出错的点。
容量选偏大的算法一般没问题,它能覆盖小容量区域,比如 64KB 的芯片用 128KB 的算法通常也能烧;但如果反过来,用 64KB 的算法去烧 128KB 的芯片,超出部分写不进去,会出现烧录到一半失败或者校验不通过。所以选算法时宁可大一点,别选小了。
另一个是 "RAM for Algorithm" 的 Start 和 Size。默认通常是0x20000000和0x1000。这块 RAM 是给算法程序临时用的,芯片被编程期间,算法本身要先被下载到这段 SRAM 里执行。有些低端 M0 芯片 SRAM 只有 4KB 或 2KB,默认的 4KB 就会越界,需要把 Size 改小到0x0800甚至0x0400,同时确认 Start 落在这颗芯片真实的 SRAM 区间内。还有一些芯片有专门的 SRAM 区域(例如某些型号的 0x10000000 段),按手册填即可。改完这个参数的判断标准很简单:能正常烧录并且校验通过。
4.3 时钟速度、复位方式和 SWD 连接
Debug 页 Settings 里能看到 SW Device 的 IDCODE 和一段描述,比如ARM CoreSight SW-DP。这一栏能看到东西,说明物理连接和驱动都是通的,问题在算法或配置;如果这一栏空白或者报错,那基本是硬件或驱动层面的问题。
Max Clock 我一般从默认往下调,长排线、杜邦线、板载没有屏蔽的情况下,10MHz 很容易连不稳。降到 1MHz 甚至 500kHz,连接成功率会明显提升。等一切稳定了再往回调。
Reset 下拉框里有几个选项:Autodetect、HW RESET、SYSRESETREQ、VECTRESET、Connect under Reset。前面说过,如果程序把 SWD 引脚配成了普通 IO 或者进了低功耗,普通的连接方式会失败,这时候选 Connect under Reset,或者干脆在点下载的瞬间按住板子上的复位键,等下载开始再松手,成功率很高。还有个细节:下载配置里 Reset and Run 勾上,烧完会自动复位运行,省得你再按一次;如果发现烧录成功但程序没跑起来,先看这个勾有没有打上。
4.4 Target DLL has been cancelled 的来龙去脉
这条提示和 Flash Download failed 经常一起出现,很多人以为是同一个错误,其实它说的是调试器驱动 DLL 加载被取消了。常见来源有四个:调试器下拉框选错了硬件;驱动被其他软件占用;驱动版本和 Keil 版本不匹配;Flash 算法列表为空,Keil 在准备阶段就中止了。
按顺序排:先确认 Debug 下拉框选的是实际使用的调试器;关掉所有可能占用调试器的后台程序;在 Pack Installer 里更新一下器件包,器件包里同时包含器件描述和配套的算法文件,包损坏时空列表是很常见的现象;最后检查 Utilities 页,确认勾的是 Use Debug Driver,这样下载阶段会复用 Debug 页的配置,而不是用另一套独立设置。我遇到过最离谱的一次,是工程在 Utilities 页里被改成了 Use External Tool,结果下载时调用了一个不存在的脚本,报错信息却看着像驱动问题,查了半天。
4.5 下载失败速查表
| 弹窗内容 | 优先怀疑 | 动手方向 |
|---|---|---|
could not load file "...\xxx.axf" | 编译未产出文件 / 路径不符 | 先看 Output 汇总,核查 Output 页配置 |
Flash Download failed - "Cortex-M3" | Flash 算法未添加或与器件不符 | Add 对应算法,核对容量与起始地址 |
Target DLL has been cancelled | 调试器选错、驱动被占用、算法列表空 | 换驱动、关后台工具、更新器件包 |
| SW Device 栏空白 | 接线、供电、引脚被复用 | 查 SWDIO/SWCLK、共地、改 Connect under Reset |
| 烧写中途失败、校验不过 | RAM for Algorithm 越界、算法容量不匹配 | 缩小 Size、确认 SRAM 区间、换更大容量算法 |
| 烧写成功但不运行 | Reset and Run 未勾、BOOT 引脚状态不对 | 勾上 Reset and Run,检查启动模式引脚 |
5. 几个绕不开的进阶坑
5.1 Keil5 和 C51 共存安装引发的怪现象
不少人电脑上同时装了 MDK(做 STM32)和 C51(做 8051),两者装在不同目录是没问题的,但如果为了图省事装进同一个目录,出问题的概率就上来了。典型表现是打开 MDK 工程时找不到器件、或者编译报工具链相关的错、左侧目录树显示不全。原因是两者的工具链文件和配置文件在同一路径下互相覆盖。
比较稳的做法是把 MDK 和 C51 分别装在两个独立目录,工程也分开管理。至于授权问题,按官方渠道的正规方式处理就好,这类工具都有对应的评估方式和商用授权方案,别在这个环节给自己找麻烦。另外 C51 的免费评估版本对代码大小有限制,超限时会报链接错误,这跟芯片容量不是一回事,看到代码量相关的链接报错先想想是不是这个原因。
5.2 器件包版本和工程迁移
Keil5 从 MDK5 开始用 Pack 机制管理器件支持,器件包(DFP)版本和工程配置是绑定的。从同事那里拷来的工程,如果本机没装对应的器件包,打开时会提示缺失,或者 Device 下拉框里找不到那颗芯片。解决办法是在 Pack Installer 里搜厂商名,安装对应的 DFP。
版本不一致还会带来另一个隐蔽问题:新版本器件包里某些寄存器定义、启动文件有改动,老工程编译时可能出现"某个宏未定义"的报错。这种情况不要盲目降级,先看报错的具体符号,通常补一个宏定义或者调整包含关系就能过去。我的习惯是把工程用到的器件包版本写进项目的说明文件里,团队里所有人保持一致,能省掉很多"在我这能编译"的扯皮。
5.3 增量编译的隐形陷阱
增量编译能省时间,但它也会让你误判。有一种情况是:改了头文件里的宏定义,但因为依赖关系没被正确识别,部分 .o 没有重新编译,链接出来的 .axf 是半新半旧的,运行表现诡异。遇到"代码明明改了却没生效",第一反应应该是 Rebuild all(工具栏上那个带循环箭头的按钮),而不是继续怀疑代码。
另一个相关的小问题:如果编译输出的时间戳没有变化,说明这次编译被跳过了。Keil 在没有任何文件改动时会跳过链接,Objects 里的 .axf 还是旧的。下载前扫一眼文件时间戳,是个成本很低的好习惯。
6. 我自己的固定检查清单
6.1 新建工程时一次性配好的项
新建工程时把这几个地方配好,能规避掉后面八成的报错。工程路径放在纯英文浅层目录;Device 选择实际芯片型号,顺手把 Pack 装全;C/C++ 页的 Include Paths 一次补齐;Output 页勾上 Create HEX File,输出目录保持默认;Debug 页选对调试器并设置合理的 Max Clock;Utilities 页确保 Use Debug Driver 被勾选;Flash Download 页确认算法存在且 RAM for Algorithm 参数合法;下载配置里勾上 Reset and Run。这一套做完,之后换电脑、换同事接手,只要照着这项清单核对一遍就行。
一个容易被忽视的细节:把 Options for Target 里的每一项改完之后,记得保存工程。Keil 的配置是即时生效的,但 .uvprojx 文件不保存的话,关掉重开会回到旧配置,然后你就开始怀疑自己刚才是不是改错了地方。
6.2 交付前和接手时的双向清单
我给自己的工程做过一版自查清单,交给别人之前跑一遍,接手别人工程时也跑一遍,两边都省事。
交付方要看:所有源文件都在工程组里而不是只在硬盘上;Include Paths 全是相对路径且有效;没有残留的绝对路径;Objects 目录不进版本管理;器件包版本写清楚;有没有依赖本机特有的环境变量或脚本。
接手方要看:先 Rebuild all 一次,确认能出 .axf;再看 Output 汇总有没有 Program Size;再连板子看 SW Device 有没有 IDCODE;最后才点下载。顺序不要跳,跳过任何一步都可能把时间浪费在后面。
我在实际排查中最大的体会是,Keil 的报错信息其实相当诚实,它说的就是它遇到的问题,只是这些信息分布在不同的窗口里,你需要知道去哪个窗口看。编译问题看 Build Output,下载问题看 Debug 页和 Flash Download 列表,硬件问题看 SW Device 那一栏。把这三个地方当成固定动作,慢慢就形成肌肉记忆了,第一次碰到 could not load file 可能查半小时,第十次基本两分钟定位。最后再分享一个小习惯:给每个工程的 Objects 目录加一个说明文件,写清楚用的器件包版本和调试器型号,跨机器协作的时候,这个小小的文本比任何口头交接都管用。