Keil 编译 STM32F103 最常见 20 个坑
本文总结了使用Keil开发STM32F103时最常见的20个问题,涵盖编译链接错误、HardFault定位、外设配置失误等典型场景,结合底层机制分析原因并提供实用解决方案,帮助开发者快速排查和规避嵌入式开发中的高频陷阱。
Keil 编译 STM32F103 最常见 20 个坑:从编译失败到运行崩溃的实战避坑指南
你有没有遇到过这样的场景?
代码写得飞起,信心满满点下“Build”——结果跳出一行红字: Error: L6218E Undefined symbol 。
或者,终于下载进去了,串口却死活不输出 printf 的内容,查了三天才发现是 MicroLIB 没勾 ?
更离谱的是,程序一上电就进 HardFault_Handler 死循环,调了半天发现只是数组越界了一位……
欢迎来到嵌入式开发的真实世界 🙃。
在使用 Keil MDK 开发 STM32F103 的过程中,这些“看似低级、实则致命”的问题几乎每个工程师都踩过。它们不一定是技术难题,但排查起来极其耗时,尤其对新手而言,常常陷入“不知道错在哪”的困境。
今天,我们就来一次把 Keil 编译 STM32F103 最常见的 20 个坑 彻底讲透。不是照本宣科地贴错误码,而是结合真实项目经验,告诉你:
- 这个问题到底 为什么会发生 ?
- 它背后涉及哪些 底层机制 ?
- 如何快速定位并 永久规避 ?
准备好了吗?我们直接开干 🔧。
1. 链接报错: L6218E Undefined symbol —— 函数声明了却找不到实现?
这是最让人抓狂的一类错误之一:明明写了函数,也包含了头文件,为什么还说“未定义”?
实际案例
// delay.h
void delay_ms(uint32_t ms);
// main.c
#include "delay.h"
int main(void) {
delay_ms(1000); // 编译通过,链接时报错!
return 0;
}
看起来没问题吧?但如果你忘了把 delay.c 加入 Keil 工程……恭喜, L6218E 就找上门了。
背后发生了什么?
Keil 的构建流程分为两步:
1. 编译阶段 : .c → .o ,只检查语法和声明。
2. 链接阶段 : armlink 把所有 .o 文件拼成一个可执行文件,这时才真正去找函数体。
所以,即使你只声明没实现,编译器也不会报错——因为它以为“别人会实现”。只有到了链接器手里,它才发现:“哎,这人 promised 有个 delay_ms ,结果 nowhere to be found!”于是怒抛 Undefined symbol 。
常见原因
| 原因 | 说明 |
|---|---|
| ❌ 源文件未添加到工程 | .c 文件存在但没加入 Keil 工程 |
| ❌ 文件被排除编译 | 右键文件 → “Excluded from Build” 打钩了 |
| ❌ 函数名拼写/大小写错误 | C 区分大小写, Delay_MS() ≠ delay_ms() |
| ❌ 库未正确链接 | 使用 HAL 或自定义库时路径或依赖缺失 |
快速解决方法
✅ 三步排查法 :
1. 在 Project 窗口中确认 .c 文件确实在工程里;
2. 右键该文件 → Properties → 确保 “Always Build” 启用;
3. 全局搜索函数名,看是否拼写一致(建议用 Keil 自带的 Symbol Browser)。
💡 小技巧:在
Options for Target → C/C++ → Define中加个宏,比如DEBUG_LINK_CHECK,然后在.c文件中用#pragma message("Compiling delay.c")输出日志,确保文件真的参与了编译。
2. printf 不输出?别急,先看看 MicroLIB 勾了没!
想用 printf 打印调试信息,结果串口一片寂静。检查 UART 配置、波特率、引脚都没问题……最后发现: MicroLIB 没开 !
为什么必须开 MicroLIB?
ARMCC 默认使用的标准库是为通用操作系统设计的,比如 Linux。它依赖 _sys_write 这样的系统调用来输出数据——但在单片机上,哪来的“系统”?根本跑不通。
而 MicroLIB 是 Keil 提供的一个轻量级替代品,专为嵌入式系统优化:
- 不依赖操作系统
- 支持用户重定向 fputc
- 占用 Flash 和 RAM 极小
但它默认是关闭的!
怎么启用?
打开: Project → Options → Target → Use MicroLIB ✅ 勾上!
然后重写 fputc :
#include <stdio.h>
int fputc(int ch, FILE *f) {
while (!(USART1->SR & USART_FLAG_TXE)); // 等待发送寄存器空
USART1->DR = (uint8_t)ch;
return ch;
}
现在你就可以愉快地 printf("Hello, STM32!\n"); 了 ✅。
⚠️ 注意事项:
- 如果没开 MicroLIB,哪怕写了fputc也不会生效!
-scanf同理,需要重写fgetc
3. 启动文件丢了?程序连 main 都进不去!
有没有试过程序烧进去后完全没反应?调试器一跟,停在 Reset_Handler 外面飘着?
大概率是你缺了那个不起眼的小文件: startup_stm32f103xe.s
它到底干了啥?
这个汇编文件,是整个程序的生命起点。上电后 CPU 第一件事就是执行它里面的代码。主要做四件事:
- 设置初始栈指针 SP
- 从向量表第一个字读取初始值(通常是0x2000_5000) - 复制 .data 段
- 把 Flash 中已初始化的全局变量搬到 RAM - 清零 .bss 段
- 所有未初始化变量设为 0 - 跳转到 Reset_Handler → __main → main
如果这个文件没了,上面这些事全都没做。你的全局变量就是随机值,堆栈可能指向非法地址,程序自然“跑飞”。
常见翻车现场
- 新建工程时忘记添加启动文件
- 选错了型号对应的启动文件(比如用了
startup_stm32f103rb.s却实际是 RC 型号) - 文件被误删或路径错误
解决方案
- 打开
\Keil\ARM\PACK\ARM\CMSIS\...\Device\ARM\STM32F1xx\Source\Templates\arm
找到对应芯片容量的启动文件:
-startup_stm32f103xb.s→ 64KB Flash
-startup_stm32f103xe.s→ 512KB Flash - 添加进工程,并确保参与编译(右键 → Options → Always Build)
🛑 特别提醒:不同 Flash 容量的启动文件中
.stack和.heap大小不同,选错可能导致内存溢出!
4. 下载失败:“Flash Algorithm Download Failed”?
激动人心的时刻来了——点击 “Download”,结果弹窗:
Flash Algorithm Download Failed
怎么回事?ST-Link 明明连上了啊!
根本原因
Keil 并不能直接操作 Flash。它需要一个“中间人”—— Flash Algorithm ,也就是一段运行在调试器上的小程序,负责控制擦除、写入、校验等操作。
每个 Flash 算法都是针对特定芯片定制的。例如:
- STM32F103RB(128KB Flash)
- STM32F103RC(256KB Flash)
如果你的工程配置和实际芯片不匹配,Keil 就找不到合适的算法,自然下载失败。
怎么修复?
进入: Project → Options → Debug → Settings → Flash Download
👉 点击 “Add” → 选择正确的 Flash 算法(如 STM32F10x High-density Flash)
如果列表为空?说明你没装对应的 Device Family Pack (DFP) 。
解决方案:
1. 打开 Pack Installer (Tools → Pack Installer)
2. 搜索 STM32F1 Series Device Family Pack
3. 安装最新版本
⚠️ 注意事项:
- 自制最小系统板要确保供电稳定(≥2.7V),否则也可能导致下载失败
- BOOT0 引脚必须拉低才能正常进入 ISP 模式
5. 系统时钟还是 8MHz?HAL_Delay() 时间严重不准!
你写了 HAL_Delay(1000) ,结果等了快 9 秒才过去……是不是觉得 HAL 库有 bug?
别急,多半是你没让系统时钟跑到 72MHz。
默认状态有多慢?
STM32F103 上电后,默认使用内部 8MHz HSI 作为系统时钟(SYSCLK)。虽然能工作,但性能只有满血版的 1/9。
要想达到标称的 72MHz,必须开启外部晶振(HSE)并通过 PLL 倍频。
正确配置方式
方式一:使用 HAL 库自动初始化
RCC_OscInitTypeDef osc = {0};
RCC_ClkInitTypeDef clk = {0};
osc.OscillatorType = RCC_OSCILLATORTYPE_HSE;
osc.HSEState = RCC_HSE_ON;
osc.PLL.PLLState = RCC_PLL_ON;
osc.PLL.PLLSource = RCC_PLLSOURCE_HSE;
osc.PLL.PLLMUL = RCC_PLL_MUL9; // 8MHz * 9 = 72MHz
HAL_RCC_OscConfig(&osc);
clk.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
clk.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
clk.AHBCLKDivider = RCC_SYSCLK_DIV1;
clk.APB1CLKDivider = RCC_HCLK_DIV2; // PCLK1 = 36MHz
clk.APB2CLKDivider = RCC_HCLK_DIV1; // PCLK2 = 72MHz
HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_2);
方式二:手动寄存器操作(裸机常用)
RCC->CR |= RCC_CR_HSEON;
while(!(RCC->CR & RCC_CR_HSERDY));
RCC->CFGR |= RCC_CFGR_PLLSRC | RCC_CFGR_PLLMULL9;
RCC->CR |= RCC_CR_PLLON;
while(!(RCC->CR & RCC_CR_PLLRDY));
RCC->CFGR |= RCC_CFGR_SW_PLL;
while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
💡 提示:可以用
SystemCoreClock变量查看当前主频。
6. HardFault 死循环?教你三招精准定位元凶
程序突然卡在一个无限循环里:
void HardFault_Handler(void) {
while(1);
}
这不是普通的 bug,这是 CPU 在喊救命: 我遇到无法恢复的异常了!
常见诱因
| 类型 | 示例 |
|---|---|
| 内存访问违规 | 访问 NULL 指针、越界数组 |
| 栈溢出 | 递归太深、局部变量过大 |
| 未对齐访问 | 强制将奇地址转成指针 |
| 中断未实现 | NVIC 使能了中断但没写 ISR |
排查大法
方法一:看 LR(R14)寄存器
在 Keil 调试模式下,打开 Registers 窗口,找到 LR(Link Register)。
它的值决定了异常来源:
- 0xFFFFFFF1 :来自线程模式,使用 MSP
- 0xFFFFFFF9 :来自中断,使用 PSP
方法二:分析 Fault Status 寄存器
__IO uint32_t *CFSR = (__IO uint32_t*)(0xE000ED28); // Configurable Fault Status Register
uint32_t cfsr = *CFSR;
if (cfsr & (1 << 0)) // 处理 BUSFAULT
if (cfsr & (1 << 8)) // 处理 MEMMANAGE
if (cfsr & (1 << 16)) // 处理 USAGEFAULT
方法三:增强版 HardFault 处理器
替换默认实现,让它帮你停下来等你查:
void HardFault_Handler(void) {
__disable_irq();
__asm("tst lr, #4");
__asm("ite eq");
__asm("mrseq r0, msp");
__asm("mrsne r0, psp");
__asm("b hard_fault_catch"); // 跳转到 C 函数
}
void hard_fault_catch(uint32_t *sp) {
// sp[0]: R0, sp[1]: R1, ..., sp[6]: R6, sp[8]: LR, sp[9]: PC!
while(1) {
// 断点打在这,查看 sp[9] 即出错指令地址
}
}
🎯 经验之谈:90% 的 HardFault 是因为指针乱指或栈不够。建议在 startup 文件中把
Stack_Size改成0x00000800(2KB)以上。
7. 全局变量不是 0?.bss 段没清零!
你以为全局变量默认是 0?错了,有时候它是“上次留下的垃圾”。
问题重现
int g_count; // 期望为 0
int main() {
if (g_count != 0) {
LED_ON(); // 居然亮了?!
}
g_count++;
}
为什么会这样?因为你没有执行 .bss 清零操作。
启动流程关键一步
.bss 段存放的是未初始化的静态变量。它们不会占用 Flash 空间,但需要在启动时手动清零。
这一步由启动文件中的 __main 完成:
LDR R0, =__bss_start__
LDR R1, =__bss_end__
MOVS R2, #0
B.W __loop_bss_init__
__loop_bss_init__:
CMP R0, R1
ITE LT
STRPL R2, [R0], #4
BPL __loop_bss_init__
但如果启动文件没加,或者你用了裸机启动没调 __main ,那 .bss 就不会清零!
✅ 解决方法 :
- 确保启动文件存在且参与编译
- 不要绕过 Reset_Handler 直接跳 main
8. IAP 升级后中断失效?VTOR 没改!
做 IAP(在线升级)的同学注意了:如果你把应用程序从 0x08000000 搬到了 0x08004000 ,记得告诉 CPU 新的中断表在哪!
否则,中断一触发,CPU 还去老地方找入口,结果跳到一片空白区,直接 HardFault。
正确做法
#define APPLICATION_ADDR (0x08004000)
SCB->VTOR = APPLICATION_ADDR; // 更新向量表偏移
也可以用 CMSIS 函数:
NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x4000);
⚠️ 注意事项:
- 必须在main()开头尽早设置
- 若使用 FreeRTOS,需同步更新 PendSV 和 SysTick 的优先级
9. 调试时变量“消失”?优化等级太高了!
你在调试,想看看某个变量的值,结果 Keil 显示 <optimized out> 。
不是编译器坏了,是你开了 -O2 或 -O3 。
为什么会被优化?
编译器认为:
- 这个变量只在一处使用 → 直接内联
- 是临时计算值 → 放进寄存器,不占内存
- 根本没被修改 → 缓存起来不用重新读
结果就是:你看不到它。
解决方案
方案一:调试用 -O0
Project → Options → C/C++ → Optimization 设为 Level 0
方案二:标记 volatile
volatile uint32_t debug_flag = 0; // 强制保留
这样即使优化也会保留内存访问。
✅ 建议:调试阶段一律用
-O0;发布版本再切-O2,并充分测试。
10. 中断优先级混乱?分组没设对!
两个中断同时来,谁先谁后?这取决于 NVIC 优先级分组 。
Cortex-M3 支持 4 位优先级字段,可以拆成:
- 抢占优先级(Preemption Priority)
- 子优先级(Subpriority)
但如果不统一设置,就会出现“高优先级没打断”的诡异现象。
正确配置
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 2bit 抢占,2bit 子
NVIC_InitTypeDef nvic;
nvic.NVIC_IRQChannel = USART1_IRQn;
nvic.NVIC_IRQChannelPreemptionPriority = 1;
nvic.NVIC_IRQChannelSubPriority = 3;
NVIC_Init(&nvic);
⚠️ 关键原则: 整个项目只能有一种分组方式 ,否则行为不可预测!
11~15:那些容易忽略的配置细节
| 坑 | 问题 | 解决方案 |
|---|---|---|
| GPIO 时钟没开 | 配置 PA5 为输出,但灯不亮 | RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); |
| core_cm3.o 缺失 | 提示 __enable_irq 未定义 |
添加 CMSIS-Core 库(通常在 Manage Run-Time Environment 中勾选) |
| SWD 引脚被重映射 | 下载失败 | 检查 AFIO->MAPR 是否禁用了 SWD/JTAG |
| 堆太小 | malloc 失败 | 修改 startup 文件中 Heap_Size 至 0x0400 (1KB)以上 |
| 无执行区域警告 | L6312W Image with no execution region | 检查 scatter file 或 target 地址是否为 0x08000000 |
16~20:高级陷阱,专治“我以为没问题”
| 坑 | 问题 | 深度解析 |
|---|---|---|
| SysTick 不触发 | HAL_Delay() 卡住 |
检查 SysTick_Config(SystemCoreClock / 1000) 返回值是否为 0;确认 SysTick_Handler 是否被覆盖 |
| 外部 SRAM 读写失败 | FSMC 接口不通 | 地址线/数据线是否接反?Bank 是否配置正确?时序参数是否匹配? |
| 低功耗模式刚进就醒 | STOP 模式立即退出 | IO 浮空产生漏电流;外设时钟未关闭;WKUP 引脚干扰 |
| CAN 启动失败 | CAN 初始化返回 ERROR | 上电后电源不稳定,建议延时 100ms 再初始化 |
| USB 不识别 | 主机看不到设备 | DP 引脚必须接 1.5kΩ 上拉至 3.3V !否则主机无法检测设备插入 |
🛠️ 特别强调:USB DP 上拉电阻是硬性要求!很多开发板都自带了,但自己画板时千万别忘了!
实战排查流程图:UART 无输出怎么办?
假设你遇到最常见问题: 串口没输出 。
不要瞎猜,按这个顺序一步步来:
UART 无输出?
├─ 是不是用了 printf?
│ ├─ 是 → MicroLIB 开了吗?
│ └─ 否 → 跳过
├─ fputc 写了吗?
│ └─ 检查函数签名是否正确
├─ TX 引脚配置对了吗?
│ └─ 必须是复用推挽输出(Alternate Function Push-Pull)
├─ 时钟配了吗?
│ └─ APB2 时钟使能 + GPIOA 时钟使能
├─ 波特率对吗?
│ └─ 常见 115200,误差不超过 3%
├─ 系统时钟是 72MHz 吗?
│ └─ 影响 USARTDIV 计算
└─ 物理连接 OK 吗?
└─ 交叉线?电平匹配?GND 共地?
按照这个逻辑,99% 的问题都能定位出来。
工程结构最佳实践
一个清晰的 Keil 工程应该长这样:
Project/
├── Core/
│ ├── cmsis_core.h
│ └── startup_stm32f103xe.s
├── Drivers/
│ ├── STM32F1xx_HAL_Driver/
│ └── my_drivers/ (gpio.c, usart.c...)
├── Middleware/
│ └── fatfs/, freertos/...
├── User/
│ ├── main.c
│ ├── it.c (中断服务例程)
│ └── config.h (宏定义集中管理)
└── Output/
├── firmware.hex
└── listing.map
推荐配置
| 项目 | 建议值 |
|---|---|
| Optimization | Debug: -O0 ,Release: -O2 |
| Use MicroLIB | ✅ 勾选 |
| One ELF Section per Function | ✅ 启用(利于优化) |
| Browse Information | ✅ 生成(方便跳转) |
| Output: Create Hex File | ✅ 生成 |
| Listing: Generate Map File | ✅ 生成(查内存分布) |
最后一点真心话
这 20 个坑,每一个我都亲手踩过,有的甚至反复踩。
但正是这些“痛”,让我学会了如何真正理解一个嵌入式系统的启动、链接、运行全过程。
与其说是“避坑指南”,不如说是一份 从失败中提炼出来的系统认知地图 。
下次当你看到 L6218E 或 HardFault 时,别慌。打开这篇文档,顺着机制一层层往下查,你会发现:原来所谓的“玄学问题”,不过是某个环节没到位罢了。
🚀 终极建议: 永远用 STM32CubeMX 生成基础工程 ,再导入 Keil。它可以帮你避开 80% 的低级配置错误。
毕竟,我们的时间,不该浪费在重复踩坑上。
更多推荐



所有评论(0)