搞嵌入式开发的人,估计都经历过这样的场景:代码写了一堆,编译报错一屏,Flash下载卡死,进调试刚跑两步又跳进HardFault。Keil作为最常用的IDE,报错能力却相当“原生态”,很多提示又短又抽象,对新手极不友好。这篇文章把我这几年用Keil调试时踩过的常见错误整理一下,逐个讲清楚报错背后的原因、排查思路、解决步骤,穿插一些常规文档里不会写的实战经验。内容覆盖编译、下载、运行、环境配置四个阶段,适合正在用Keil做STM32、C51或者其它ARM项目,以及被各种报错折磨得想砸电脑的朋友。
1. 编译链接阶段的报错处理
先说编译期的报错,这类错误相对好排查,因为编译器会给出具体的行号和符号名。但有些报错信息本身比较绕,比如链接阶段的L6218E,很多人都被它坑过。
1.1 L6218E: Undefined symbol 未定义符号
现象:编译通过,链接时报错,输出类似这样:
.\Objects\project.axf: Error: L6218E: Undefined symbol HAL_UART_Init (referred from main.o).看到这段报错,说明main.c里调用了HAL_UART_Init这个函数,但链接器在整个工程里都找不到它的实现。
根本原因,无非以下几类:
- 调用的函数有声明(头文件里有),但实现它的源文件没被添加到工程里。
- 函数实现了,但它所在的源文件因为条件编译被整体屏蔽了(比如
#if 0,或某个宏未定义导致#ifdef分支不成立)。 - 使用了静态库(.lib/.a),但库文件没有正确添加或路径不对。
- 函数名拼写不一致,声明和定义的参数列表不同导致无法匹配。
排查思路,我建议按这个顺序走:
第一步,双击报错信息,定位到是哪个文件引用了这个符号。接着去全工程搜索这个符号的定义,看看它究竟在哪个文件里。
第二步,检查定义它的文件是否在工程树中。如果文件没有加入工程,右键点击工程名选择“Add Existing Files to Group”,把它加进去。
第三步,如果文件在,看它是否被条件编译屏蔽。在定义处打断点或看编译预处理结果,或者在代码里临时去掉#ifdef确认。
一个我踩过的坑:写了一个bsp_uart.c,里面实现了UART初始化函数,也用#ifdef BSP_UART_ENABLE包着。结果在C/C++选项卡的Preprocessor Symbols里宏名拼成了BSP_UART_ENABEL,排查了半小时才发现是少了一个字母。这类低级错误,把宏定义和条件编译里逐字符比对一下,往往能救命。
1.2 error: #20 identifier "xxx" is undefined
这个报错意味着某个标识符(变量名、结构体类型、宏等)在当前文件里没有被定义。
常见触发场景:
- 头文件没有包含,或者包含顺序不对。比如在A.h里引用了B.h中的类型,但A.h没包含B.h,导致A.h编译不过。
- 结构体类型先用了,后定义了。
- 宏定义在某个头文件中,但使用的.c文件没有包含它。
- 在AC5和AC6切换后,某些编译器内置的宏不可用(比如
__CC_ARM、__GNUC__这类编译器相关宏)。
解决办法:定位到报错行,看是哪个标识符,然后全工程搜索它的定义位置。确认它所在的头文件是否有被包含。如果牵扯到循环包含(A包含B,B又包含A),需要重新设计头文件的包含路径,或者用前置声明解决。
注意:在Keil里C文件默认不检查头文件的依赖关系,所以改完头文件后,建议点击“Rebuild”而不是“Build”,确保所有文件重新编译。
1.3 semihosting(半主机)相关报错与printf重定向问题
用STM32做串口打印时,几乎都会遇到printf重定向。不少人在链接时会看到提示:
Error: L6915E: Library reports error: __use_no_semihosting was requested, but _ttywrch was referenced.这行报错是ARM编译器对semihosting机制的保护。所谓semihosting,是ARM调试时的一种机制,允许开发板上的代码通过调试器直接访问PC主机上的文件、终端等资源。标准库里的printf输出,默认会走semihosting这条路径,在调试环境下能用,但脱离调试器或者没实现相关接口就会出问题。所以链接器才会要求你显式声明不使用半主机。
两种常见解决思路:
方案一,最简单粗暴:在Options for Target里的Target选项卡勾选Use MicroLIB。MicroLIB是ARM编译器提供的一个精简版C库,默认不依赖semihosting,也不需要额外写底层函数,printf重定向只需要重新实现fputc即可。这种方式适合大多数单片机项目,缺点是部分C标准库功能(比如浮点printf支持)会有缩减。
方案二,不勾选MicroLIB,自行禁用semihosting并补全底层接口。在任意一个源文件中加上这段:
#pragma import(__use_no_semihosting) struct __FILE { int handle; }; FILE __stdout; void _sys_exit(int x) { x = x; } int fputc(int ch, FILE *f) { // 这里换成你的串口发送函数 while((USART1->SR & 0X40) == 0); USART1->DR = (uint8_t) ch; return ch; }这个方案保留了完整C库功能,但代码量多。项目里如果只需要简单打印,选MicroLIB就够了。如果要做复杂文件操作、浮点解析等,才建议走完整方案。
2. 烧录下载阶段的报错处理
编译链接通过只是第一步,烧录下载这一关也能卡掉很多人。最常见的报错就是Flash Download failed、找不到设备、RDDI错误这几类。
2.1 Flash Download failed - "Cortex-M3/M4"
这是Keil下载程序时最具代表性的报错,常见信息:
Flash Download failed - "Cortex-M4" Error: Flash Download failed - "Cortex-M4"原因分析:
- 芯片型号选错,导致加载的Flash算法不匹配。
- Flash编程算法(Programming Algorithm)里没添加对应器件的算法文件。
- 芯片Flash被读保护,调试器无法擦除。
- SWD接口接触不良,导致下载过程中断。
- 目标板供电不稳。
排查步骤:
先去Options for Target的Device选项卡,确认芯片型号是否与实际一致。接着看Debug选项卡,确认调试器类型(ST-Link、J-Link、CMSIS-DAP等)和接口设置。然后点开Utilities选项卡,点击Settings进入Download页面,确认右下角的Flash Download区域里,Programming Algorithm列表是否存在对应芯片的算法。
常见问题在于,很多人选了STM32F103C8,但Programming Algorithm里只有512KB容量的STM32F10x高密度Flash算法,选成中等密度算法就能解决。
如果确认算法无误仍报错,多半是读保护引起。此时可以用STM32CubeProgrammer或者命令行工具做全片擦除,先解除保护再下载。
提示:下载失败时,如果提示“Cannot access target”,先按住目标板复位键,点下载的同时松开复位,很多SWD被禁用或时钟异常的场景能靠这种“碰运气”方式救回来。
2.2 No ULINK Device found / Cannot access target
这种报错一般出现在点击下载的瞬间,Keil弹出来:
No ULINK Device found. Cannot access target. Please verify power, connection and target settings.几个最容易忽略的细节:
- 调试器没被电脑识别,先去设备管理器看是否有感叹号设备。ST-Link识别不到,一般是驱动没装好或固件损坏。
- SWDIO、SWCLK、GND三条线没接对。SWD两根线最容易接反,很多杜邦线颜色一样,插的时候特别容易出错。
- 目标板供电不足。有些板子只靠调试器的3.3V输出供电,如果板子外设多,功耗大会导致电压跌落。
- Debug设置里选了错误的调试器,比如用J-Link却选了ST-Link。
- SWD速率太高。个别板子的线长或者布局不好,5MHz、10MHz可能连不上,降到1MHz、100kHz反而稳定。
解决思路:打开Options for Target -> Debug -> Settings,看右边SW Device窗口能否扫描到目标芯片。如果显示No target detected,先查接线和供电;如果能看到芯片ID,说明连接本身没问题,可以试试降低Max Clock。也可以用示波器量SWCLK引脚是否有波形来判断调试器是否在输出时钟。
2.3 RDDI-DAP Error
这个报错在ST-Link上尤其常见:
RDDI-DAP Error: An error occurred while trying to read Corex-M4 ID.RDDI-DAP是CMSIS-DAP协议中的一个调试接口,出现这个报错意味着调试器和目标芯片之间的DAP握手失败了。常见原因:目标板供电异常、SWD线受到干扰、ST-Link固件版本过旧或者不兼容、个别情况下是目标芯片进入了低功耗模式导致SWD不可用。
解决步骤:
- 检查ST-Link的驱动和固件。Keil安装目录下通常有ST-Link Upgrade工具,能看当前固件版本。固件太老建议升级,兼容性问题能解决不少。
- 单独给目标板供电,不要依赖调试器供电。
- 拔掉其它连接到调试器和目标板的线路,排除干扰。
- 降低SWD速率,试100kHz、1MHz。
2.4 Cannot Load Flash Programming Algorithm
这个报错通常出现在使用外部存储或非标准芯片时,Keil在下载阶段找不到匹配的Flash编程算法:
Cannot load flash programming algorithm!原因:Keil的Flash算法文件(.flm)没安装,或者器件配置里选择了一个不存在的算法。
如果是标准STM32芯片,去Pack Installer里确认对应器件的DFP(Device Family Pack)是否完整安装。如果是自己做的板子,用到了外部SPI Flash或者自定义存储,需要自行编写或者导入匹配的Flash算法文件,并在Utilities选项卡里手动添加。
我遇到过的情况:换了一台新电脑,装了最新版Keil,下载STM32F103时提示找不到算法。查了一圈发现是Pack安装了一半,有文件损坏。卸载重装对应DFP包后问题消失。如果是Pack导致的问题,与其折腾半天,不如果断重装一次。
3. 调试运行阶段的报错处理
烧录进去只是第一步,真正让开发进度停滞的往往是运行阶段的调试问题。死机、跑飞、变量看不到、断点不生效,每一个都能让人崩溃。
3.1 程序跑飞,死在HardFault_Handler
这是嵌入式调试里最经典、也是占比最高的问题。现象就是代码一运行,还没干什么就跳进HardFault_Handler的死循环。硬件异常里除了HardFault,还有BusFault、UsageFault、MemManage Fault,但默认中断向量表里往往都汇总到HardFault。
触发HardFault的常见原因:
- 空指针或者野指针解引用,比如未初始化指针就直接赋值。
- 数组越界写,把栈或者堆的数据改坏了。
- 栈溢出,嵌套调用过深,或者局部变量太大。
- 外设寄存器访问了不存在的地址,或者时钟没打开就操作外设。
- 中断优先级配置错误,导致中断无法正常嵌套。
- 非对齐访问,某些Cortex-M内核不支持非对齐访问,或者需要特殊配置才支持。
定位HardFault最高效的方式:让程序停在HardFault_Handler里,然后打开Peripherals -> Core Peripherals -> Fault Reports窗口,查看异常状态寄存器的值。再打开Registers窗口,找到Current PC值和LR(链接寄存器)值,这个PC值基本能指示发生异常时执行的指令位置。
还有一个非常实用的技巧:在HardFault_Handler里,查看SP(栈指针)指向的栈内存。因为异常发生时,内核会把一系列寄存器压栈,栈里保存着发生异常前那一刻的R0-R3、R12、LR、PC、PSR。手动从栈内存中把PC找出来,比任何猜测都靠谱。具体操作:进入HardFault后,在Memory窗口里看SP指向的连续32字节存储,倒数第9-12字节对应的就是保存的LR和PC。很多教程里讲的方法不直观,但这个栈回溯法是我实际用过最有效的。
3.2 error 65: access violation at 0x...: no 'read' permission
调试运行中,Keil有时会直接弹出一行:
*** error 65: access violation at 0x2000A000 : no 'read' permission翻译过来就是:程序试图访问0x2000A000这个地址,但调试器认为它没有读取权限。这个地址通常是存储映射外的区域,或者该外设/内存区域当前不可用。
常见场景:
- 访问了芯片不存在的SRAM地址。比如STM32F103C8只有20KB SRAM,地址范围是0x20000000到0x20004FFF,如果程序访问0x2000A000就会触发这个错误。
- 外设没使能就访问它的寄存器。比如串口1的时钟没开,就去读USART1的SR寄存器。
- FreeRTOS环境下,任务里访问了不属于当前任务栈的地址,通常是任务栈溢出或者栈指针错误。
- 在调试模式下查看了一个被优化掉或者不在当前作用域的变量,调试器尝试读取它的存储位置但权限不允许。
排查方法:定位到报错前的最后一条有效代码。看它访问的地址是否在芯片手册标注的合法范围内。检查对应外设的时钟是否已使能。用Memory窗口手动输入报错的地址,看能不能读取,如果读不到说明地址本身不合法。
经验补充:在新版Keil MDK里,如果勾选了“Use Memory Protection Unit”相关的配置,访问权限检查会更严格。但大多数项目默认不开启,遇到这个报错优先怀疑代码本身。
3.3 变量显示“not in scope”或者值不更新
调试时在Watch窗口添加一个变量,结果Keil显示not in scope,或者在程序运行到某处时变量值始终不变。这种问题出现的频率极高。
原因有两类:
第一,优化导致变量被寄存器替代或直接优化掉了。编译器开了-O2或-O3优化后,局部变量的生命周期和存储位置会发生很大变化,调试器无法准确跟踪。解决办法:把对应源文件的优化等级单独调低,或者给变量加volatile关键字,更彻底的办法是暂时关闭优化用于调试,发布时再开。
第二,变量作用域问题。在Watch窗口里添加的是某个函数内部的局部变量,但当前程序执行点不在这个函数里,调试器自然无法访问它。这种情况不算错误,但新手经常会疑惑。可以在函数内部打断点,等程序运行到断点时再查看。
实操建议:不要在Optimization级别为Level 3的时候花太多时间在单步调试上,先临时改成Level 0(或-O0),确认逻辑没问题再开优化。这能省去大量无意义的排查时间。
3.4 断点无法命中
现象:在代码某一行打了断点,点击全速运行,程序却没有在断点处停止。
原因排查:
- 程序根本没执行到那一行。可以先在更早的位置打断点确认执行路径。
- 断点打在了不可执行的行,比如变量声明、只有注释的行。编译器没有为这些行生成实际指令,断点会被忽略或自动移动到相邻指令。
- 优化导致代码行与指令行的映射关系错乱。断点位置和实际执行指令对不上。
- Keil支持硬件断点的数量有限(取决于调试器,一般2-6个),断点太多时后面的断点可能不生效。软件断点需要修改Flash内容,在代码区写保护时会失效。
最隐蔽的一种情况:在中断服务函数里打的断点,如果中断一直被更高优先级的中断阻塞,或者中断标志没清除,就永远进不去。这种问题靠看寄存器最快,直接在中断标志位相关的寄存器上打断点,确认它是否被置位。
4. 工程与环境配置的常见坑
4.1 Pack安装失败,器件列表里找不到芯片
换了新电脑、装了新版Keil,打开工程却提示找不到目标芯片。这是因为Keil MDK从5.0版本开始,把器件支持包(Device Family Pack,简称DFP)从安装包里拆了出来,需要单独安装。
排查步骤:打开Pack Installer,搜索芯片型号,看有没有安装完成或者是否存在版本冲突。如果Pack Installer在线安装总失败,可以去官网手动下载对应型号的DFP包,然后双击导入。注意,DFP版本过高或过低也可能导致器件覆盖异常,一般选择和工程创建时接近的版本。
另一个容易忽略的问题:Keil 5和Keil 4对器件包的管理机制不同。老工程用Keil 5打开,有时需要把Legacy Device支持包也装上,否则部分旧型号芯片会找不到。安装完别忘了在Pack Installer的左侧列表里确认对应器件的状态。
4.2 AC5与AC6编译器差异导致的报错
把老工程从Keil 5的AC5编译器切换到AC6(默认的armclang),经常会出现一堆莫名其妙的报错。比如__CC_ARM宏不再可用、#pragma语法不兼容、内联汇编写法不一样、变量定义必须放在语句开头等。
解决思路:如果没有特殊需求,老工程继续用AC5也行。AC5在Keil MDK 5.37以下版本都还支持。但如果你用到了新版的CMSIS或者某些库已经只支持AC6,那就需要做迁移。迁移时把编译器的Warning Level调高,先把所有警告处理干净,再处理错误。AC6对代码规范要求更严,语法问题、隐式类型转换在AC5下可能只是警告,在AC6下直接报错。
一个我经常用的技巧:在Options for Target -> C/C++选项卡的Misc Controls里添加:
-Wno-XXX忽略特定的非关键警告。但这条只能临时应急,不能作为长期方案。根本办法还是把代码修规范。
4.3 更改Pack包默认路径
默认情况下,Keil的Pack会被安装到系统盘:
C:\Users\用户名\AppData\Local\Arm\Packs如果C盘空间不足,或者公司电脑有权限限制,就需要改路径。
步骤:
打开Keil -> Tools -> Manage Pack Installer,在Pack Installer窗口里,点击菜单栏的File -> Manage Packs,或者直接打开Keil目录下的TOOLS.INI文件,在[UV2]节里找到类似:
PACKPATH=C:\Keil_v5\Packs把PACKPATH改成目标路径,比如D:\Keil\Packs。改完后重启Keil,在Pack Installer里重新安装或刷新Pack。
但要注意,已经添加到工程里的Pack路径是工程级保存的,如果只改全局路径,老工程可能仍然指向旧路径。这种情况可以在工程的Options for Target -> Target选项卡里,检查RTE和Pack相关的路径设置。重新加载Pack并重新构建一次,一般能解决。
5. 常见错误速查表与调试经验总结
把平时最常遇到的错误整理成了一个速查表,方便下次遇到问题时快速定位。
5.1 Keil常见错误速查表
| 报错信息(关键词) | 常见原因 | 快速解决方向 |
|---|---|---|
| L6218E: Undefined symbol | 函数未实现/文件未添加/宏屏蔽 | 搜索符号定义,检查工程文件与条件编译 |
| error: #20 identifier undefined | 头文件未包含/类型未定义 | 搜索定义位置,检查包含顺序 |
| L6915E: __use_no_semihosting | printf重定向缺少底层实现 | 勾选MicroLIB或实现fputc |
| Flash Download failed | Flash算法缺失/芯片选型错误/读保护 | 核对Device、添加算法、解除保护 |
| No ULINK Device found | 接线问题/驱动缺失/速度过高 | 检查SWD接线、设备管理器、降低速率 |
| RDDI-DAP Error | ST-Link固件/供电/线材干扰 | 升级固件、单独供电、降低SWD速率 |
| Cannot Load Flash Algorithm | 算法文件缺失/自定义Flash | 重新安装DFP包/手动加入算法文件 |
| HardFault_Handler | 野指针/数组越界/栈溢出 | 看Fault Reports、栈回溯、查PC值 |
| error 65: access violation | 非法地址/外设时钟未开 | 核对地址范围、使能外设时钟、查栈 |
| not in scope | 优化过度/变量不在作用域 | 关优化、加volatile、添加全局变量 |
| 断点无法命中 | 优化/断点过多/路径错误 | 调低优化、检查硬件断点数量、确认执行路径 |
5.2 我用Keil调试时的一些心得
聊几点肺腑之言,都是实际项目中总结出来的习惯,算不上什么高深技巧,但真的能少踩很多坑。
第一,第一次烧录前,先花30秒检查调试器设置。芯片型号、调试器类型、SWD速率、Flash算法,这四个东西确认一遍再烧录。我见过太多人拿着STM32F407的板子,工程里选的却是F103,然后到处问为什么下载失败。类型选错,算法加载就会出问题,这是很低级但也很常见的失误。
第二,学会看反汇编和寄存器,不要只靠printf。遇到HardFault,与其在代码里加打印然后全速跑,不如先把Fault Reports窗口打开,把PC、LR、栈里的数据记录下来。很多情况下,一个栈回溯就能把问题定位到具体的函数调用链上。
第三,不要过度依赖调试器。有些bug是时序相关的,全速运行正常,单步运行就消失。这种时候,传统的LED翻转、串口打印反而更有效。调试器是工具,不是目的。
最后再说一个小技巧:Keil的“Debug (printf) Viewer”窗口可以直接显示printf的调试输出,省去外接串口线的麻烦。如果调用了printf但屏幕没显示,通常是因为工程设置里没勾选“Use MicroLIB”,或者初始化串口时中断没开。这个小窗口在排查逻辑问题时非常顺手,值得一试。