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 第一件事就是执行它里面的代码。主要做四件事:

  1. 设置初始栈指针 SP
    - 从向量表第一个字读取初始值(通常是 0x2000_5000
  2. 复制 .data 段
    - 把 Flash 中已初始化的全局变量搬到 RAM
  3. 清零 .bss 段
    - 所有未初始化变量设为 0
  4. 跳转到 Reset_Handler → __main → main

如果这个文件没了,上面这些事全都没做。你的全局变量就是随机值,堆栈可能指向非法地址,程序自然“跑飞”。

常见翻车现场

  • 新建工程时忘记添加启动文件
  • 选错了型号对应的启动文件(比如用了 startup_stm32f103rb.s 却实际是 RC 型号)
  • 文件被误删或路径错误

解决方案

  1. 打开 \Keil\ARM\PACK\ARM\CMSIS\...\Device\ARM\STM32F1xx\Source\Templates\arm
    找到对应芯片容量的启动文件:
    - startup_stm32f103xb.s → 64KB Flash
    - startup_stm32f103xe.s → 512KB Flash
  2. 添加进工程,并确保参与编译(右键 → 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% 的低级配置错误。

毕竟,我们的时间,不该浪费在重复踩坑上。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐