以下是对您提供的博文内容进行深度润色与重构后的技术博客正文。全文已彻底去除AI生成痕迹,采用真实嵌入式工程师口吻撰写,逻辑层层递进、语言自然流畅,兼具教学性、实战性与思想深度。结构上打破传统“引言-原理-步骤-总结”的模板化框架,以问题驱动、经验穿插、细节点拨的方式展开,更贴近一线开发者的真实学习路径和思考节奏。
从点亮一颗LED开始:我在Keil5里踩过的坑,和教会新人的第一课
你有没有过这样的经历?
刚拿到一块STM32F103C8T6开发板,烧完官方例程LED亮了,心里一热;
可自己新建工程,照着教程敲完main.c,编译通过、下载成功、调试器连上了……
结果PA0死活没波形——示波器探头贴上去,只有3.3V直流,像块沉默的石头。
不是代码写错了,也不是硬件坏了,而是某个被忽略的“默认配置”,在背后悄悄关掉了GPIO时钟。
这就是我带第一个实习生时的真实场景。
他花了三天查数据手册、翻HAL库源码、重装驱动……最后发现:Keil5工程向导选错芯片封装,导致启动文件里的中断向量表长度不够,复位后直接跳到非法地址,程序根本没跑起来。
而这一切,在Build Output窗口里只显示一行绿色的".axf - 0 Error(s), 0 Warning(s)"—— 完美得令人绝望。
所以今天这篇笔记,不讲“Keil5是什么”,也不罗列菜单在哪点;
我想带你回到那个最原始的状态:没有CubeMX、没有HAL、没有自动配置向导——只有一份Reference Manual、一个ST-Link、和一颗想被点亮的LED。
我们用最朴素的方式,在Keil5里亲手搭起嵌入式世界的地基。
为什么是Keil5?而不是VS Code + CMake?
先说句实在话:如果你的目标是快速做出产品原型,用CubeMX生成初始化代码+Keil5一键编译,效率远高于手写寄存器操作。
但正因如此,当系统某处突然卡死、中断不响应、ADC采样值漂移时,你才会意识到:那些被工具隐藏掉的细节,才是故障的真正源头。
Keil5的价值,从来不在它有多“智能”,而在于它足够“诚实”——
它不会替你决定RCC_CFGR里PLL倍频系数该设多少;
它不会自动帮你把.data段从Flash复制到SRAM;
它甚至会冷眼看着你把SystemCoreClock写成72000000,却实际只配出了8MHz主频,然后让你在HAL_Delay(1000)里等一辈子。
换句话说:Keil5不是保姆,它是教官。
它强迫你直面芯片手册第2章的时钟树图、第9章的存储器映射、第11章的复位与启动流程。
而这些,恰恰是所有高级功能(RTOS调度、USB枚举、I²S同步)赖以成立的物理基础。
✅ 小贴士:Arm Compiler 6(基于LLVM)相比AC5,在内联汇编兼容性和浮点运算优化上更激进,但对老项目建议仍用AC5——尤其当你需要精确控制指令周期时,AC5生成的汇编更可预测。
创建工程前,必须确认的三件事
别急着点“New Project”。在µVision界面弹出来之前,请先问自己:
1. 你的目标芯片,到底是哪一款?
STM32F103C8T6 ≠ STM32F103CBT6 ≠ STM32F103RBT6
它们的Flash容量(64KB / 128KB)、SRAM大小(20KB / 32KB)、封装引脚数(48 / 64 / 100),全都不一样。
而Keil5的Device Pack(DFP)正是按这些参数打包的。选错型号,轻则中断向量溢出,重则Flash算法加载失败。
🔍 怎么确认?看芯片丝印背面的小字,或用ST-Link Utility读取IDCODE:0x1BA01477→ Cortex-M3(F1/F2系列)0x2BA01477→ Cortex-M4(F4/F7系列)
2. 你的调试器,固件版本够新吗?
ST-Link V2出厂固件普遍是J21/J25,而Keil5 v5.38+要求最低为J37.S7。
旧固件无法识别SWD协议中的某些调试寄存器,表现就是:
- “Cannot connect to target”
- 或者连接成功,但无法设置断点、读不到寄存器值
🛠️ 解决方法:用STM32CubeProgrammer → Help → Firmware update → 选择最新V2.Jxx.Sx固件升级。
3. 你的工程路径,真的干净吗?
中文路径?空格?特殊符号?
统统不行。
Arm Compiler底层调用的是POSIX风格路径解析器,遇到C:\我的工程\STM32\这种路径,会在预处理阶段就报错:
Error: #5: cannot open source input file "system_stm32f1xx.h"这不是Keil5的问题,是编译器本身限制。
✅ 正确做法:D:\KeilProjects\F103_LED_Baremetal
新建工程:五步走,但每一步都有陷阱
打开µVision,点击Project → New µVision Project,接下来的操作看似简单,实则暗藏玄机:
第一步:保存工程文件(.uvprojx)
⚠️ 注意:此时不要急着选芯片!
先点“Save”,把工程文件存到纯英文路径下。
因为Keil5会在保存瞬间自动生成同名文件夹,并将后续所有资源默认放进去——如果路径含中文,后面所有添加的源文件都会继承这个“有毒路径”。
第二步:选择设备(Device Selection)
弹出窗口里搜STM32F103C8,选中后点OK。
这时Keil5会自动下载并安装对应DFP(需联网)。
✅ 验证是否成功:Project → Manage → Project Items → Folder Tabs → 确认Startup组里有startup_stm32f103xb.s
📌 关键细节:xb代表64KB Flash(0x08000000 ~ 0x0800FFFF),如果你用的是128KB型号(如RBT6),必须手动替换为startup_stm32f103rb.s,否则中断向量表会覆盖到Flash末尾之后的非法区域。
第三步:添加启动文件与系统初始化文件
右键Source Group 1→Add Existing Files to Group,添加:
-startup_stm32f103xb.s(来自DFP安装目录)
-system_stm32f103xx.c(同上)
-main.c(你自己写的)
💡 提醒:system_stm32f103xx.c里有一个宏定义HSE_VALUE,默认是8000000。如果你外接的是12MHz晶振,这里必须改!否则SystemCoreClock计算错误,所有依赖SysTick的函数都会失准。
第四步:配置Target选项
双击左侧Target页签,重点检查三项:
| 设置项 | 推荐值 | 为什么重要 |
|--------|--------|-------------|
| Xtal (MHz) |8| 告诉编译器外部高速晶振频率,影响SystemCoreClockUpdate()计算 |
| IRAM1 / IROM1 Base/Size |0x20000000 / 0x5000,0x08000000 / 0x10000| SRAM和Flash起始地址与大小,链接脚本依据 |
| Use Memory Layout from Target Dialog | ✅ 勾选 | 让链接器自动读取上面填的IRAM/IROM参数,避免手写scatter file出错 |
第五步:输出与调试配置
Output页:勾选Create HEX File(方便用其他烧录工具验证)Debug页:选择ST-Link Debugger→Settings→Debug→ 确保Connect模式为Under Reset(防止复位后立即运行导致调试失败)Utilities页:点击Settings→Flash Download→ 确认已勾选Reset and Run(下载完自动重启运行)
main.c怎么写?不是复制粘贴,而是理解每一行在干什么
下面这段代码,是我给新人布置的“首日作业”。它不依赖任何库,只靠CMSIS头文件和寄存器直操:
#include "stm32f1xx.h" int main(void) { // Step 1: 初始化系统时钟(HSE=8MHz, PLL=9 → SYSCLK=72MHz) // 注意:此函数会修改RCC寄存器,且假设HSE已稳定 SystemInit(); // Step 2: 使能GPIOA时钟(APB2总线) // 如果跳过这行,GPIOA->CRH写操作将被忽略!这是最常见LED不亮原因 RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // Step 3: 配置PA0为推挽输出(注意:CRH控制高8位IO,CRL控制低8位) // 先清零PA0对应的4位模式字段,再写入0b0010(推挽输出,10MHz) GPIOA->CRL &= ~(0xF << (0 * 4)); // 清除低4位 GPIOA->CRL |= (0x2 << (0 * 4)); // 写入推挽输出模式 // Step 4: 主循环翻转PA0 while (1) { // BSRR寄存器高位写1置位,低位写1复位 GPIOA->BSRR = GPIO_BSRR_BS0; // PA0 = 1 for (volatile uint32_t i = 0; i < 1000000; i++); GPIOA->BSRR = GPIO_BSRR_BR0; // PA0 = 0 for (volatile uint32_t i = 0; i < 1000000; i++); } }🧠 这段代码背后,藏着三个必须掌握的核心概念:
volatile不是可有可无的修饰符:它告诉编译器“这个变量可能被硬件异步修改”,禁止优化掉整个for循环。否则AC5可能直接删掉延时,LED变成常亮。BSRR比ODR更安全:ODR是读-改-写操作,多任务环境下可能被中断打断;而BSRR是原子写入,高位置位、低位复位互不干扰。SystemInit()不是万能的:它只配置了主频,但APB1/APB2分频系数仍为默认值(HCLK/2)。若你要用USART1(挂APB2),波特率计算必须基于PCLK2而非SYSCLK。
编译通过 ≠ 程序正确:四个必查的“静默杀手”
即使Build Output全是绿色,也别急着庆祝。以下是我在现场调试中最常遇到的四类“伪成功”现象:
| 表象 | 真实原因 | 快速验证法 |
|---|---|---|
| LED完全不亮 | GPIO时钟未使能(RCC->APB2ENR未置位) | 用万用表测PA0对地电压:若有3.3V但无波动,大概率是时钟问题 |
| LED微亮/闪烁异常 | PA0被配置为开漏输出但未接上拉电阻 | 查GPIOA->CRL低4位是否为0b0100(开漏),示波器看波形是否只能拉低不能拉高 |
| 下载成功但无法调试 | SWDIO/SWCLK引脚被复用为JTAG(PA13/PA14)且未禁用JTAG | 在main()开头加__HAL_AFIO_REMAP_SWJ_DISABLE();,或改用SWD专用引脚 |
| 变量值始终为0 | .bss段未清零(启动代码未执行或__main被跳过) | 在调试模式下单步进入Reset_Handler,确认是否跳转到了__main |
🔧 终极排查手段:打开View → Memory Windows → Memory 1,输入0x20000000,观察SRAM起始位置是否全为0。如果不是,说明.bss初始化失败。
后续可以怎么走?别停在点亮LED
当你能让PA0稳定翻转,恭喜你已经跨过了嵌入式开发的第一道门槛。
但真正的挑战才刚开始——
- 想让LED呼吸?那就得搞懂
TIM2定时器的PWM输出模式,以及如何用HAL_TIM_PWM_Start()启动通道; - 想串口打印“Hello World”?先解决
printf重定向到ITM(SWO引脚),再配置Core Debug ITM Stimulus Port; - 想接传感器?你会立刻撞上
HAL_I2C_Master_Transmit()返回HAL_ERROR,然后发现SCL线上根本没有波形——原来是GPIO_InitTypeDef里忘了设GPIO_MODE_AF_OD;
所有这些“高级功能”,其实都建立在同一个前提之上:
你知道Reset_Handler从哪里开始执行,知道栈指针怎么初始化,知道.data是怎么从Flash搬到SRAM的,也知道为什么main()之前必须先跑一段汇编。
而这,正是Keil5裸机工程教会你的第一课:
真正的掌控感,永远来自对底层细节的理解,而非对工具链的依赖。
如果你也在用Keil5做STM32开发,或者正卡在某个看似简单却反复失败的环节,欢迎在评论区留言。我们可以一起拆解那行让你失眠的寄存器配置,或者分析那段永远进不去的中断服务函数。
毕竟,每一个闪亮的LED背后,都曾有过无数次黑暗中的摸索。
而分享这些摸索的过程,就是我们这群嵌入式人,最朴素的传承方式。