一 本章简介
理论学了再多,都不如亲手跑一遍来得实在。
还记得第一次学 C 语言时,在电脑上运行 Hello World 那一刻吗?屏幕上出现那行文字的瞬间,可以说是初步用代码控制了电脑!在嵌入式的世界里,我们首先要控制的不再是屏幕上的文字输出,而是一块实实在在的硬件。
本章我们将带你完成嵌入式开发的 初体验 ——从打开一个现成的例程工程,到成功编译、下载、运行,再到小小地修改一下代码,感受那种【我改了代码,板子就听话了】的成就感。整个过程就像学开车:完全不需要你懂发动机原理,只要能启动、挂挡、踩油门、把车开起来就行。原理,我们后面再慢慢学。
本章不会讲太多深奥的原理,重点是让你熟悉开发工具的操作流程,建立对整个开发过程的直观认知。你可以把本章当作一次 跟着做 的实验课,看一步就做一步,跑通了就好。
TIP
给有经验工程师的话
如果你已经有其他单片机的开发经验(如51、AVR、ESP32等),本章可以帮你快速熟悉STM32的开发流程和Keil MDK的使用。你可以重点关注:
- STM32的工程配置特点(Device选择、编译器版本、调试器配置)
- HAL库的代码风格和函数命名规则
- Keil MDK与其他IDE(如Arduino IDE、PlatformIO)的差异
建议你快速浏览本章,然后就直接跳到第八章【认识GPIO】深入学习STM32的底层原理。
NOTE
本章与后续章节的关系
- 第四章(本章): 快速体验完整流程,使用现成例程,目标是"跑起来"
- 第八章【认识GPIO】: 深入讲解GPIO的工作原理、寄存器结构、电气特性
- 第九章【新建点灯工程】: 从零开始创建工程,学习寄存器级编程
如果把学习比作爬山,本章是坐缆车直达山顶看风景,让你知道山顶的样子;后面的章节则是教你如何一步步爬上去。
IMPORTANT
在开始本章之前,请确保你已经完成了第三章【学习前的准备工作】中的环境搭建,特别是:
- 已正确安装 Keil MDK 及 STM32F4 设备支持包(DFP)。
- 已准备好硬件:天空星核心板(STM32F407版本) + 天空星筑基学习板 + DAPLink 调试器。
- 已从 Gitee 仓库下载了配套例程(或至少下载了本章要用的 LED 闪烁例程)。
如果以上任何一步还没完成,请先返回第三章查漏补缺。环境没搭好就开始写代码,就像没装轮子就想开车一样,注定是跑不起来的。
1.1 学习目标
完成本章学习后,你将能够:
| 序号 | 学习目标 | 重要程度 |
|---|---|---|
| 1 | 学会打开一个现成的 Keil MDK 工程文件 | ⭐⭐⭐⭐⭐ |
| 2 | 熟悉 Keil MDK 的主要界面布局和常用功能按钮 | ⭐⭐⭐⭐⭐ |
| 3 | 学会编译工程并能看懂编译输出信息 | ⭐⭐⭐⭐⭐ |
| 4 | 学会将程序下载到天空星开发板中运行 | ⭐⭐⭐⭐⭐ |
| 5 | 学会简单修改代码(如修改延时时间),重新编译下载,观察效果变化 | ⭐⭐⭐⭐ |
| 6 | 初步体验 Keil 的在线调试功能(单步、断点、变量观察) | ⭐⭐⭐ |
| 7 | 建立对嵌入式开发流程的整体认知:"写代码 → 编译 → 下载 → 验证" | ⭐⭐⭐⭐⭐ |
1.2 重点提示
本章只是 体验 ,不求甚解:本章的目标是让你快速建立信心,看到 我的代码能控制硬件 这个结果。关于GPIO是什么、寄存器怎么配置、HAL库的原理等问题,我们会在第八章和第九章讲解。现在你只需要 照着做 就好。
不要怕点错:Keil MDK 的界面可能看起来复杂,按钮密密麻麻的,但大部分按钮点错了也不会造成什么严重后果。就像你刚拿到一个新遥控器,多按按总会知道哪个键是干嘛的。大胆去探索,别怕。
先跑通再理解:本章的目标是先让程序跑起来。至于代码为什么这样写、寄存器是什么、HAL 库怎么回事……我们后续章节再详细讲解。现在你只需要会 照着操作 就够了。
遇到问题先检查硬件连接:下载失败、无法识别调试器等问题,90% 是因为线没插好、SWD 接线错误。很多初学者花几个小时排查软件问题,最后发现是一根线松了。所以,出了问题,先检查线,再检查软件。
路径不要有中文和空格:在第三章中我们已经强调过了,工程存放路径不要包含中文和空格。如果你把例程放在了类似
C:\Users\张三\桌面\我的工程\这样的目录下,编译和下载过程中可能会出现各种莫名其妙的错误。先用现成工程建立正反馈:很多初学者一上来就想从零创建工程、自己配时钟、自己配GPIO,结果还没看到LED亮起来,就先被各种配置项劝退了。本章刻意选择 先用现成工程跑通 ,就是为了先建立信心。等你看到了成果,再回头学原理和工程搭建,会轻松很多。
看到现象后要学会建立对应关系:本章虽然不深入讲原理,但建议你在操作时始终思考:我改了哪一行代码?硬件现象发生了什么变化? 这种 代码-现象 的对应关系,是后续调试和开发能力的起点。
1.3 名词解释
本章会涉及到一些术语,这里提前做一个简单解释,方便你在后面阅读时不会一头雾水。这些概念在后续章节会有更详细的讲解,现在只需要有个大概印象即可。
| 术语 | 解释 |
|---|---|
| 编译 (Compile/Build) | 把你写的 C 语言代码 翻译 成单片机能看懂的机器码(二进制文件)。就像把中文翻译成英文一样。 |
| 下载 / 烧录 (Download/Flash) | 把编译好的机器码通过调试器传输到单片机的 Flash(闪存)中保存。断电后代码不会丢失。 |
| 调试 (Debug) | 连接调试器后,可以控制单片机一步一步地执行代码,查看变量的值,分析程序是否按照预期运行。就像给程序装了一个"慢动作摄像机"。 |
| 工程 / 项目 (Project) | 一组相关的代码文件、配置文件、库文件的集合,Keil 用 .uvprojx 文件来管理这些文件之间的关系。 |
| GPIO | 通用输入输出端口,单片机上可以被软件控制为高电平或低电平的引脚,用于连接LED、按键等外设。 |
| HAL库 | STM32的硬件抽象层库,封装了底层寄存器操作,让我们可以用更简单方便的函数来控制硬件。 |
| Flash (闪存) | 单片机内部用来存储程序代码的非易失性存储器。断电不丢失数据,就像 U 盘一样。 |
| RAM (随机存取存储器) | 单片机内部用来存储运行时数据(变量等)的存储器。断电后数据丢失,就像电脑内存条一样,只要一断电,里面的数据就需要重新填充。 |
TIP
如果你现在看到这些术语还是一头雾水,完全没关系!本章你只需要知道"编译"、"下载"、"调试"这三个最基本的概念就够了。其他的概念会在实际操作中逐渐理解。
1.4 工程链接
仓库工程地址:天空星筑基学习板例程仓库
本章所使用的例程路径:0_example/GPIO/gpio-led-pb2-project
打包好的工程压缩包:gpio-led-pb2-project.zip 【推荐直接下载这个】
二 嵌入式开发的整体流程
在动手之前,我们先从宏观上理解一下嵌入式开发到底在做什么。了解全貌之后,后面每一步操作你都会知道自己在做什么、为什么要做。
2.1 嵌入式开发 vs 电脑编程
如果你学过 C 语言(哪怕只是在电脑上跑过 Hello World),你就已经走过了一次 编程 的完整流程。让我们把两种开发方式做个对比:
| 步骤 | 电脑编程(如 Dev-C++) | 嵌入式开发(如 Keil + STM32) |
|---|---|---|
| 1. 写代码 | 在 IDE 里写 C 代码 | 在 Keil 里写 C 代码 |
| 2. 编译 | 点击"编译"按钮 | 点击"编译"按钮 |
| 3. 运行 | 点击"运行",程序在电脑上执行 | 点击"下载",将编译好的程序传到单片机中执行 |
| 4. 查看结果 | 在控制台看到 printf 输出 | 看到 LED 灯亮了 / 串口输出了数据 |
| 5. 调试 | 设断点、单步执行、看变量 | 同样可以设断点、单步、看变量(需要调试器) |
你会发现,流程几乎是一样的!唯一的区别是:电脑编程的程序在电脑上运行,嵌入式的程序在单片机上运行。由于单片机是一块独立的小芯片,我们没法直接在上面 点击运行 ,所以需要借助一个调试器(DAPLink) 把编译好的程序 搬运 到单片机里面去。
2.2 开发流程图
用一张图来展示完整的开发流程:

上图由AI生成
这个流程你会在今后的学习和工作中反复经历,次数多到数不清。所以现在先建立一个整体印象就好。
2.3 各步骤对应的工具
| 步骤 | 使用的工具 | 说明 |
|---|---|---|
| ① 写代码 | Keil MDK / VS Code / CLion | 代码编辑器,本章用 Keil 来演示,另外两种请自行探索 |
| ② 编译 | Keil 内置的 ARM Compiler | 把 C 代码翻译成二进制 |
| ③ 下载 | DAPLink 仿真器 + Keil | 通过 SWD 接口把程序写入芯片 |
| ④ 运行 | 单片机自己运行 | 下载完成后自动或手动复位运行 |
| ⑤ 调试 | DAPLink + Keil 调试界面 | 断点、单步、查看变量和寄存器 |
NOTE
如果你觉得上面的信息有点多,没关系!记住一句话就够了:写代码 → 编译 → 下载 → 看效果。就这四步,循环往复,就是嵌入式开发的日常。
2.4 本章你真正要建立的能力
从学习目标上看,本章是在教你如何打开工程、如何点按钮、如何下载程序;但从能力培养的角度看,本章真正想让你建立的是下面这三种意识:
2.4.1 流程意识
很多初学者一遇到问题,就会立刻怀疑代码写错了。但实际工程里,问题可能出在很多环节:
- 代码没成功保存
- 工程没编译成功
- 编译成功但没重新下载
- 下载成功但板子没复位运行
- 程序已经运行,但你观察错了现象
所以我们首先要建立一个清晰的流程意识:
改代码 → 保存 → 编译 → 下载 → 观察现象 → 判断结果
以后无论做LED、串口、SPI、I2C,还是更复杂的电机控制、传感器采集,这个基本流程都不会变。
2.4.2 排错意识
本章虽然是入门体验,但你已经会接触到最典型的嵌入式问题:
- 工具链问题
- 工程配置问题
- 调试器连接问题
- 供电问题
- 代码逻辑问题
这几类问题,几乎贯穿整个嵌入式职业生涯。越早建立正确的排错思路,后面越省时间。
2.4.3 结果导向意识
嵌入式开发和纯软件开发有一个很大的不同:最终结果一定会体现在真实硬件上。
你写得再漂亮,如果LED不亮、串口没数据、电机不转,那这个功能就不能算完成。
所以从现在开始,我们就要养成一个习惯:
- 不只看代码
- 不只看编译通过
- 一定要看板子上的真实现象
这是从 会写代码 走向 会做产品 的重要一步。
三 获取例程工程
3.1 下载例程
我们为天空星筑基学习板准备了一系列配套例程,目前还不全,会努力持续更新的。
Gitee 仓库地址:https://gitee.com/lcsc/fdb
下载方式(任选其一):
方式一:直接下载 ZIP 压缩包(推荐新手)
- 用浏览器打开上面的仓库地址
- 点击页面右上角的 克隆/下载 → 下载ZIP
- 等待下载完成
方式二:使用 Git 克隆(推荐有 Git 基础的同学)
如果你安装了 Git,可以在命令行中执行:
git clone https://gitee.com/lcsc/fdb.git3.2 例程目录结构
下载解压后,进入仓库目录,你会看到类似这样的结构:
fdb/ ← 仓库根目录
├── 0_example/ ← 例程总目录
│ ├── GPIO/ ← GPIO相关例程
│ │ ├── gpio-led-pb2-project/ ← LED闪烁例程(本章使用)★
│ │ │ ├── Core/ ← 核心用户代码
│ │ │ │ ├── Inc/ ← 头文件
│ │ │ │ └── Src/ ← 源文件(main.c在这里)
│ │ │ ├── Drivers/ ← HAL库驱动文件
│ │ │ ├── MDK-ARM/ ← Keil工程文件 ★
│ │ │ │ ├── project.uvprojx ← 双击这个文件打开工程!
│ │ │ │ └── ...
│ │ │ ├── .eide/ ← EIDE工程配置
│ │ │ ├── CMakeLists.txt ← CLion/CMake工程配置
│ │ │ └── project.ioc ← STM32CubeMX配置文件
│ │ └── ...
│ ├── UART/ ← 串口相关例程
│ ├── LCD/ ← 2.0寸屏幕相关例程
│ └── ...
├── 1_tutorial-code/ ← 教程配套代码【后续复杂了会对应到章节中】
└── ...2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
本章我们使用 0_example/GPIO/gpio-led-pb2-project 这个 LED 闪烁例程。
TIP
你可能注意到了,同一个工程里有 MDK-ARM、.eide、CMakeLists.txt 三种工程配置文件。这是因为我们的例程做了三种 IDE 的兼容——你可以用 Keil、VS Code + EIDE、CLion 中的任何一种打开同一份代码。本章我们只用 Keil,其他两种以后再说。
3.3 确认你的例程可以正常工作
在正式开始之前,我们需要确认一件事:你从仓库下载的例程是否完整、路径是否正确。
快速检查清单:
- [ ] 例程文件夹中有
MDK-ARM目录,里面有project.uvprojx文件 - [ ] 例程文件夹中有
Core/Src/main.c文件 - [ ] 例程文件夹中有
Drivers目录,里面有 HAL 库文件 - [ ] 工程存放路径中没有中文和空格
如果以上都确认无误,我们就可以开始了!
TIP
工程师常用的小习惯:先做最小验证
很多有经验的工程师拿到一个新项目时,不会立刻开始大改代码,而是会先做一次最小验证:
- 工程能不能打开
- 能不能编译通过
- 能不能下载成功
- 板子有没有基础现象
这样做的好处是:
- 先确认开发环境本身没问题
- 建立一个稳定的起点
- 后面如果改坏了,知道该回退到哪里,当然你最好还是用git来管理你的工程
本章其实也在带你建立这个习惯。
四 打开并熟悉 Keil MDK
4.1 打开工程
Step 1:进入例程目录,找到 0_example\GPIO\gpio-led-pb2-project\MDK-ARM\ 文件夹。
Step 2:找到后缀为 .uvprojx 的文件(如 project.uvprojx),这就是 Keil MDK 的工程文件。

Step 3:双击该文件,Keil MDK 会自动启动并打开工程。

TIP
如果双击后没有自动打开 Keil,而是弹出了 选择打开方式 的对话框,这说明 .uvprojx 文件还没有和 Keil 关联。你可以:
方法一:在弹出的对话框中手动选择 Keil 安装目录下的 UV4.exe,然后勾选 始终使用此应用打开 .uvprojx 文件 。
方法二:先启动 Keil MDK,然后通过菜单 Project -> Open Project... 手动浏览到 project.uvprojx 文件并打开。
工程打开后,你应该能看到 Keil MDK 的主界面,左边是工程文件树,中间是代码编辑区域。如果你看到了这个画面,恭喜,你已经成功迈出了第一步!

4.2 Keil MDK 界面介绍
打开工程后,你会看到 Keil MDK 的主界面。第一次看到可能会觉得按钮好多、信息好杂,别慌,我们一个区域一个区域来认识。
我们首先双击工程窗口 project->Application/User/Core 下面的 main.c ,打开Keil的编辑区

整个界面可以分为五个核心区域:
4.2.1 菜单栏
位于窗口最上方,包含 File(文件)、Edit(编辑)、View(视图)、Project(工程)、Flash(烧录)、Debug(调试)等菜单。

初学者最常用的操作及快捷键:
| 菜单路径 | 功能 | 快捷键 | 使用频率 |
|---|---|---|---|
| File -> Save All | 保存所有文件 | Ctrl+Shift+S | ⭐⭐⭐⭐⭐ |
| Project -> Build Target | 编译工程 | F7 | ⭐⭐⭐⭐⭐ |
| Project -> Rebuild all target files | 全部重新编译 | - | ⭐⭐⭐ |
| Flash -> Download | 下载程序到芯片 | F8 | ⭐⭐⭐⭐⭐ |
| Debug -> Start/Stop Debug Session | 进入/退出调试模式 | Ctrl+F5 | ⭐⭐⭐⭐ |
TIP
记住三个最常用的快捷键就够了:F7(编译)、F8(下载)、Ctrl+F5(调试)。这三个按键你以后会按到手指起茧。
4.2.2 工具栏
位于菜单栏下方,是一排图标按钮,提供了菜单中常用功能的快捷访问。

编译相关按钮:
| 序号 | 功能 | 快捷键 | 说明 |
|---|---|---|---|
| ① | 编译当前文件 | Ctrl+F7 | 只编译当前打开的文件 |
| ② | 编译整个工程(Build) | F7 | 最常用! 只编译修改过的文件 |
| ③ | 全部重新编译(Rebuild) | - | 重新编译所有文件,通常在切换编译器或修改配置后使用,或者你怀疑有奇怪问题的时候进行一次全编译. |
下载和调试按钮:
| 图标 | 功能 | 快捷键 | 说明 |
|---|---|---|---|
| ④ | 下载程序 | F8 | 将编译好的程序下载到单片机 |
| ⑤ | 调试模式 | Ctrl+F5 | 进入/退出在线调试 |
| ⑥ | 工程配置 | Alt+F7 | 打开 Options for Target 配置窗口 |
NOTE
Build(F7)和 Rebuild 的区别:
- Build(F7):增量编译。只编译你修改过的文件,速度快。日常开发中 99% 的时候用这个。
- Rebuild:全量编译。把所有文件都重新编译一遍,速度慢但更彻底。通常在切换编译器版本(AC5↔AC6)、修改了全局配置、或者遇到莫名其妙的编译问题时使用。
打个比方:Build 就像 只重新阅读了书架上的一本你刚做过批注的书 ,Rebuild 就像 把书架上所有书都重新阅读一遍 。
4.2.3 工程窗口
位于界面左侧,以树形结构显示工程的文件组织。这是你浏览工程文件的主要入口。
project/ ← 编译目标(一般只有一个)
├── Application/MDK-ARM/ ← 虚拟文件夹(存放启动文件)
│ └── startup_stm32f407xx.s ← 启动文件(一般不改)
├── Application/User/Core/ ← 用户代码组
│ ├── main.c ← 主程序文件 ★(初学者写代码主要在这里)
│ ├── stm32f4xx_it.c ← 中断服务函数(ISR)
│ └── stm32f4xx_hal_msp.c ← HAL 库的 MSP(低层硬件初始化)
├── Drivers/CMSIS/ ← ARM 内核 & CMSIS(一般不改)
│ └── system_stm32f4xx.c
├── Drivers/STM32F4xx_HAL_Driver/← STM32 HAL 驱动(一般不改,或由 CubeMX 管理)
│ ├── stm32f4xx_hal.c
│ ├── stm32f4xx_hal_gpio.c
│ └── ...
└── CMSIS/ ← CMSIS 组件(统一软件接口层,可暂时不关心)2
3
4
5
6
7
8
9
10
11
12
13
14
常用操作:
- 双击文件名 → 在编辑区打开该文件
- 右键文件名 → 出现上下文菜单(打开、移除、添加文件等)
- 点击
各个文件夹前的 + / - 号 → 展开/折叠文件树
对于初学者来说,你目前只需要关注 main.c 这一个文件就够了。其他文件我们在后续章节中会逐步讲解。
4.2.4 编辑区
位于界面中央,是你编写和查看代码的区域。

编辑区的重要功能:
| 功能 | 说明 |
|---|---|
| 语法高亮 | 不同类型的代码用不同颜色显示,方便阅读,比如说这里注释是绿色的,代码是黑色的 |
| 行号 | 左侧显示行号,方便定位代码位置 |
| 代码折叠 | 点击行号旁边的 ± 号,可以折叠/展开代码块 |
| 设置断点 | 在行号左边单击,出现红色圆点,表示设置了断点[只在调试状态下生效] |
| 跳转到定义 | 右键一个函数名 → Go to Definition,可以跳转到函数定义的位置 |
| 代码补全 | 输入几个字母后会弹出补全提示(虽然没有 VS Code 那么智能,但也勉强能用) |
TIP
设置断点是在线调试时非常重要的功能。你在某一行代码行号的左边点一下,出现一个红色圆点,程序运行到这里就会暂停,你可以查看此时变量的值。
4.2.5 输出窗口(Build Output)
位于界面下方,显示编译信息、错误、警告以及下载过程中的日志,这里我们先按一下 F7 进行一次编译:

编译成功时,你会在 Build Output 中看到类似这样的信息:
"project\project.axf" - 0 Error(s), 0 Warning(s).0 Error(s) 表示没有错误,0 Warning(s) 表示没有警告。看到这两个 0,就说明编译通过了,可以下载了。
如果有错误,会显示错误信息,双击错误信息可以直接跳转到出错的代码行,非常方便。
4.3 工程配置界面(Options for Target)
这是 Keil 中非常重要的一个配置窗口,用于设置目标芯片、编译选项、调试器参数等。虽然我们提供的例程已经配置好了,但了解这个界面对你排查问题很有帮助。
TIP
给工程师的一个提醒
有时候很多现场问题最后都不是代码逻辑错了,而是工程配置被改了、编译器版本变了、调试器选错了、下载算法不匹配。也就是说,工程文件本身就是产品的一部分。
所以建议你从一开始就把 Options for Target 当成正式工程配置来对待,而不是把它当成一个可用可不用的窗口。
打开方式(三选一):
- 点击工具栏的魔术棒图标 ,如下图里蓝色标那个位置
- 右键 Target 1 → Options for Target...,如下图里红色标1,2的那个方式
- 快捷键
Alt+F7

打开后,页面如下图所示:

4.3.1 Device 选项卡
显示当前选择的目标芯片型号。天空星低配版使用的是 STM32F407VET6,高配版是 STM32F407VGT6。

NOTE
如果你在这里看不到任何芯片,说明你还没有安装 STM32F4 的设备支持包(DFP)。请返回第三章安装。
4.3.2 Target 选项卡
配置存储器布局、编译器版本、是否使用微库等核心参数。

| 配置项 | 说明 | 备注 |
|---|---|---|
| ARM Compiler | 编译器版本选择 | 推荐选 Use default compiler version 6,AC5是老版本,正在被逐步淘汰。 |
| Use MicroLIB | 默认不勾选 | 勾选后,编译器会使用专为嵌入式优化的微型C库(减小代码体积)。 如果你在项目中重定向了 printf 函数通过串口打印,不勾选此项会导致程序卡死在 __main 函数或者直接进入 HardFault(因为标准库默认需要半主机模式支持)。 |
| Floating Point Hardware | 是否开启 硬件浮点 | Single Precision:选了这个,编译器会自动调用浮点汇编指令来执行带小数的数学运算。 NOT Used:选了这个,遇到带小数的乘除法,编译器会调用庞大的C语言软件库(软浮点)去模拟计算。速度极慢,且极其消耗 CPU 资源。 |
| IROM1 | Flash(代码存储区)的起始地址和大小 | Start: 0x08000000, Size: 0x80000 (512K) IROM (Internal Read-Only Memory) 是代码存放在单片机内部Flash的物理地址。STM32的Flash都是从 0x08000000 开始的。F407VET6的Flash大小为512KB。 |
| IRAM1 | RAM1(数据存储区)的起始地址和大小 | Start: 0x20000000, Size: 0x1C000 (112K) IRAM (Internal Random Access Memory) 是单片机的运行内存。F407的SRAM1从这里开始,共112KB,全局变量、堆(Heap)、栈(Stack)默认都在这。 这一块内存挂载载AHB总线上,CPU和DMA都可以无缝访问。如果是串口DMA接收缓冲、ADC DMA缓冲,务必放在这个区域。 |
| IRAM2 | RAM2(数据存储区)的起始地址和大小 | Start: 0x2001C000, Size: 0x4000(112K) 这是紧挨着SRAM1的第二块内存(总计112+16=128KB的常规SRAM) 它可以被用来在低功耗模式下独立保持数据,节省功耗。 |
TIP
STM32F407的 隐藏 内存(CCM RAM)
这个芯片不仅仅只有上面提到的 128KB 内存!它还有额外的 64KB CCM RAM (Core Coupled Memory)。
- 物理地址:
0x10000000 - 大小:
0x10000(64KB)
为什么上面的表格没有写 IRAM3 指向这里? 因为 CCM RAM 直接挂在CPU内部的数据总线上,普通的DMA控制器无法访问它! 如果Keil自动把你的 串口DMA接收数组 分配到了CCM RAM里,DMA传输就会彻底失效(毕竟DMA访问不了嘛),这是很多工程师查了几天几夜都查不出的Bug。
最佳实践: 我们在Keil的Target里通常不把CCM配置进默认的IRAM中,而是通过代码中的属性宏定义,手动将那些 只被CPU高频计算,不需要DMA搬运 的数据(比如 RT-Thread/FreeRTOS的任务栈、DSP算法的大型浮点矩阵)强制分配在这64KB里,从而榨干F407的最后一滴性能!
4.3.3 Output 选项卡
配置编译输出选项。

两个值得关注的选项:
- Create HEX File:勾选后编译时会额外生成一个
.hex文件。.hex是一种通用的固件格式,可以被第三方烧录工具(如 STM32CubeProgrammer)识别。如果你只用 Keil 下载,不勾也没关系。 - Browse Information:勾选后支持 Go to Definition (跳转到函数定义)等代码导航功能。强烈建议勾选,虽然会让编译速度稍慢一点,但能大大提升代码阅读效率。
4.3.4 C/C++ 选项卡
配置 C/C++ 编译器的相关选项。

| 配置项 | 说明 |
|---|---|
| Define | 预定义的全局的宏。你会看到里面有 STM32F407xx,这个宏告诉 HAL 库 我用的是 STM32F407 这款芯片 |
| Include Paths | 头文件搜索路径。编译器会到这些路径下寻找 #include 引用的头文件,我们后面如果要添加自己的头文件路径,这个后面是会经常使用的 |
| Optimization | 优化等级。Level 0(-O0)不优化,适合调试阶段;Level 2(-O2)、Level 3(-O3)优化程度更高,适合发布时用,这里选择的-O1优化等级 |
| Language C | C 语言标准。 |
IMPORTANT
初学者不要随意修改 C/C++ 选项卡中的内容! 我们提供的例程已经配置好了正确的宏定义和头文件路径。如果你不小心删除了 STM32F407xx 这个宏定义,或者修改了 Include Paths,编译就会报出一大堆 找不到头文件 的错误。如果遇到这种情况,重新从仓库下载例程就好。
4.3.5 Debug 选项卡(重点)
配置调试器,这是初学者出问题最多的地方。

配置步骤(以我们的 DAPLink 调试器为例):
Step 1:在标号2的选择框中选择 CMSIS-DAP Debugger(DAPLink 遵循 CMSIS-DAP 协议)。
如果你用的是 ST-Link,就选 ST-Link Debugger;如果用的是 J-Link,就选 J-LINK / J-TRACE Cortex。
Step 2:点击右边的 Settings 按钮。

Step 3:在弹出的窗口中确认以下信息:

| 项目 | 期望看到的值 | 看不到怎么办 |
|---|---|---|
| 左侧调试器识别 | LCKFB DAPLink CMSIS-DAP | 重新插拔 USB 线,或检查驱动安装 |
| Port | 选择 SW(SWD 模式) | 手动下拉选择 SW |
| Max Clock | 默认 10MHz 即可 | 如果连接不稳定,可降低到 1MHz |
| 右侧 SW Device → IDCODE | 0x2BA01477 | 检查 DAPLink 和天空星之间的 SWD 接线 |
当你在 SW Device 中看到 IDCODE 时,说明调试器和芯片之间的通信已经建立,一切正常!
IMPORTANT
看不到 IDCODE 的常见原因(按照排查优先级排列):
- DAPLink 和天空星之间的 SWD 线没接好——最常见!检查 SWDIO、SWCLK、GND 三根线是否接对、接牢。
- 天空星没有供电——确保板子通过 Type-C 或 DAPLink 获得了 3.3V/5V 供电。
- USB 线经过了扩展坞/HUB,而这个HUB质量不太好——尽量直接连电脑 USB 口。
Step 4:切换到 Flash Download 标签页。
将 Reset and Run 勾选上。这样程序下载完成后,单片机会自动复位并开始运行,你不需要手动按板子上的复位键。

Step 5:点击 OK 保存设置。
WARNING
一定要记得点 OK! 如果你直接关闭窗口而不是点 OK,前面所有的设置都不会保存,下次打开还得重新来一遍。这是 Keil 的一个 经典坑,很多初学者都会踩。
4.3.6 Utilities 选项卡
这里确保 Use Debug Driver 被勾选上。它的作用是让 Keil 在下载固件时,使用你在 Debug 选项卡中选择的那个调试器(也就是我们刚才配置的 DAPLink)。

NOTE
以上这些配置,在我们提供的例程中基本都已经设置好了(除了 Debug 选项卡中的调试器选择,可能需要你根据自己的调试器型号调整)。所以如果一切正常,你可能不需要修改任何东西,直接跳到下一节开始编译。了解这些配置的目的是:万一出了问题,你知道去哪里检查。
五 编译工程
配置确认无误后,终于到了激动人心的时刻——编译!
5.1 什么是编译?
简单来说,编译就是把你写的 C 语言代码"翻译"成单片机能理解的机器码。
如果你用HAL库的话,你写的代码是这样的(人类能看懂):
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_2);编译后变成了这样的(机器能看懂,能执行的):
0x4803 // LDR r0, [pc, #12] ; r0 <- address (literal) (加载 GPIOB 基地址或指针)
0x2104 // MOVS r1, #4 ; r1 <- 0x04 (构造 pin2 的位掩码 1<<2)
0x6A42 // LDR r2, [r0, #36] ; r2 <- *(r0 + 36) (读取寄存器值,偏移 36 字节)
0x4051 // EORS r1, r2 ; r1 <- r1 XOR r2 (用掩码与寄存器做异或,计算切换后值)
0x6241 // STR r1, [r0, #36] ; *(r0 + 36) <- r1 (把新值写回寄存器)2
3
4
5
编译器做的就是这个 翻译 工作。翻译出来的结果保存在一个 .axf 文件中(如果你勾选了 Create HEX File,还会额外生成一个 .hex 文件,这个文件就是我们最常用的,带地址信息的固件文件)
我们进入Keil的在线调试后,就能看到上面的汇编信息了:

5.2 执行编译
方法一(推荐):按快捷键 F7。
方法二:点击工具栏的 Build 按钮(🔧 形状的图标)。
方法三:菜单 Project -> Build Target。

5.3 查看编译结果
编译过程中,下方的 Build Output 窗口会实时显示编译进度。
编译成功时,你会看到类似这样的输出:
Build target 'project'
compiling main.c...
compiling stm32f4xx_it.c...
compiling stm32f4xx_hal_msp.c...
compiling stm32f4xx_hal.c...
compiling stm32f4xx_hal_gpio.c...
compiling stm32f4xx_hal_cortex.c...
compiling stm32f4xx_hal_rcc.c...
linking...
Program Size: Code=3236 RO-data=440 RW-data=12 ZI-data=1644
FromELF: creating hex file...
"project\project.axf" - 0 Error(s), 0 Warning(s).
Build Time Elapsed: 00:00:012
3
4
5
6
7
8
9
10
11
12
13
关键信息解读:
| 信息 | 核心含义 | 详细解释与工程经验 |
|---|---|---|
0 Error(s) | 编译成功 | 代码没有语法等致命错误,这是生成 .hex 固件的硬性前提。 |
0 Warning(s) | 零警告 | 代码存在不规范但不致命的问题。建议: 实际工程中,请养成 将警告视为错误 的习惯,很多莫名其妙的死机 Bug 往往就隐藏在 Warning 中(比如隐式类型转换丢失精度)。 |
Code=3236 | 代码段大小 | 占用 3236 字节。存放的是你写的 C 代码被翻译后的汇编机器指令。 |
RO-data=440 | 只读数据 | Read-Only data,占用 440 字节。存放程序中的常量、const 修饰的变量、字符串字面量(比如 printf("Hello"); 里的 "Hello")。 |
RW-data=12 | 已初始化读写数据 | Read-Write data,占用 12 字节。存放非零初始化的全局变量和静态变量。比如 int a = 10;。 |
ZI-data=1644 | 未初始化读写数据 | Zero-Initialized data,占用 1644 字节。存放未初始化或初始化为0的全局变量/静态变量(比如 int b;)。注意:MCU的堆栈(Heap & Stack)默认也分配在这一段里! |
Build Time... | 编译耗时 | 本次编译花费了 1 秒。 |
很多初学者看到上面的参数,不知道怎么计算芯片实际消耗了多少 Flash(闪存)和 RAM(内存)。其实对于 STM32F407 来说,有一套固定的公式:
1. 烧录到 Flash 里的体积(ROM占用)
- 公式:
Flash占用 = Code + RO-data + RW-data - 计算:3236 + 440 + 12 = 3688 字节(约 3.6 KB)
- 解析:掉电后数据不能丢失,所以机器码(Code)、常量(RO-data)必须存放在 Flash 中。有趣的是,RW-data 的初始值(比如那个
int a = 10;里面的10)也必须保存在 Flash 中,否则单片机一断电再重启,去哪找这个初始值呢?
2. 运行时消耗的内存(SRAM占用)
- 公式:
RAM占用 = RW-data + ZI-data - 计算:12 + 1644 = 1656 字节(约 1.6 KB)
- 解析:单片机上电复位后,执行进入
main()函数之前,底层汇编的启动文件(startup_stm32f407xx.s)会做两件极其重要的事情:- 从 Flash 中把
RW-data的初始值 搬运 到 RAM 里; - 把 RAM 中属于
ZI-data的区域全部清零。 做完这些,你的全局变量才能正常工作!
- 从 Flash 中把
TIP
工程师视角:关注编译输出是个好习惯
很多初学者编译完只看"0 Error"就完事了,但有经验的工程师还会关注 Program Size 这一行。原因是:
- Flash 快满了 → 需要优化代码体积,或者换更大 Flash 的芯片型号,比如说我们天空星高配版的芯片就是青春版FLASH的两倍
- RAM 快满了 → 可能导致栈溢出、堆分配失败等运行时崩溃
- 代码突然变大 → 可能不小心引入了不需要的库文件
养成每次编译后扫一眼 Program Size 的习惯,能帮你提前发现很多潜在问题。在产品开发中,Flash 和 RAM 的余量管理是非常重要的工程实践。
5.4 如果编译出错了怎么办?
别慌!编译出错是家常便饭,就算是工作多年的老工程师也经常编译出一堆错误。
常见错误及解决方法:
| 错误信息 | 可能原因 | 解决方法 |
|---|---|---|
fatal error: 'stm32f4xx_hal.h' file not found | 头文件路径没配置好 | 检查 C/C++ 选项卡的 Include Paths |
error: expected ';' after expression | 代码语法错误,少了分号 | 双击错误信息跳转到出错行,检查代码 |
error: use of undeclared identifier 'xxx' | 使用了未定义的变量或函数 | 检查是否有拼写错误,或是否忘记包含头文件 |
undefined symbol xxx | 链接时找不到某个函数的实现 | 检查对应的 .c 文件是否加入了工程 |
Error: L6050U: The code size exceeds... | 代码超过了免费版的 32KB 限制 | 使用社区版(免费且不限代码大小)【只限非商业用途】 |
TIP
排错的基本套路:
- 看错误信息中的文件名和行号,双击可以直接跳转到出错的代码位置。
- 看错误描述,尝试理解它在说什么。
- 如果看不懂,把错误信息复制到搜索引擎搜索,通常能找到解答。
- 如果搜不到,把错误信息发给 AI(如 DeepSeek、豆包、ChatGPT),让它帮你分析。
- 如果都解决不了,重新从仓库下载例程,对照原始代码看你改了什么。
不过对于本章来说,如果你是直接打开我们提供的例程,不修改任何代码的话,理论上是不会出现编译错误的。如果出错了,大概率是环境问题,请回到第三章检查。
六 下载程序到开发板
编译通过后,下一步就是把编译好的程序 搬运 到单片机里面去。
6.1 检查硬件连接
在下载之前,请确保硬件连接正确。这一步非常重要,80% 的下载失败问题都出在硬件连接上。
NOTE
关于硬件连接的详细说明
如果你对天空星核心板、筑基学习板、DAPLink调试器的接口位置和连接方式还不熟悉,建议先回顾:
- 第一章【天空星筑基学习板套件介绍】 - 了解各个硬件的作用和接口位置
- 第三章【学习前的准备工作】 - 了解DAPLink连接方式,跳转链接。
CAUTION
供电安全提醒
- DC 电源口的电压范围是 8V~24V,不要接超过 24V 的电源,也尽量不要接反极性。
- 插拔排线时建议先断电,养成 先断电再接线 的好习惯,可以避免很多不必要的硬件损坏。
连接检查清单:
| 序号 | 检查项 | 状态 |
|---|---|---|
| 1 | DAPLink 通过 USB 线连接到电脑,电脑设备管理器里面能看到设备 | □ |
| 2 | DAPLink 和天空星通过 SWD 排线正确连接 | □ |
| 3 | 天空星开发板已供电(DAPLink 供电或 Type-C 供电或筑基学习板DC接口供电均可) | □ |
| 4 | DAPLink 上的指示灯正常亮起,天空星核心板上面电源灯(红灯)正常亮起 | □ |
TIP
关于供电方式:
天空星核心板插在筑基学习板上使用时,可以通过筑基学习板的 DC 电源口(8V~24V)或排针电源口供电。DAPLink 的 3.3V 供电可以给天空星核心板供电,但如果筑基学习板上有较多外设在工作(如电机、以太网等),建议使用筑基学习板自己的电源口独立供电,DAPLink 的供电能力很有限。
不过在学习初期,仅仅点个灯的话,DAPLink 供电完全够用。
6.2 执行下载
确认硬件连接无误后,开始下载!
方法一(推荐):按快捷键 F8。
方法二:点击工具栏的 Download 按钮(向下箭头 ⬇️ 图标)。
方法三:菜单 Flash -> Download。

6.3 下载结果
下载过程中,Build Output 窗口会显示进度信息:
Load "project\\project.axf"
Erase Done.
Programming Done.
Verify OK.
Application running ...
Flash Load finished at 14:50:092
3
4
5
6
各行含义:
| 输出信息 | 含义 |
|---|---|
Erase Done. | Flash 擦除完成(写入新程序前需要先擦除旧数据) |
Programming Done. | 编程(烧写)完成,程序已写入 Flash |
Verify OK. | 校验通过,写入的数据和编译的数据一致 |
Application running ... | 程序已开始运行(前提是勾选了 Reset and Run) |
Flash Load finished | 整个下载流程结束 |
看到 Flash Load finished 说明下载成功!🎉
此时看向你的天空星开发板——LED 灯应该开始闪烁了!
如果 LED 确实在闪烁,恭喜你,你已经成功完成了嵌入式程序下载!
NOTE
如果 LED 没有闪烁,请按以下顺序排查:
- 检查是否勾选了 Reset and Run:如果没有勾选,单片机不会自动运行。请手动按一下板子上的 复位按钮(RST),看 LED 是否开始闪烁。
- 确认 LED 位置:天空星核心板上的用户 LED 连接在 PB2 引脚(在TYPE-C口旁边,是个绿灯),检查例程代码中操作的是否是这个引脚。
- 重新下载一次:偶尔会出现下载看似成功但程序没有正常写入的情况,重新按 F8 下载试试。
6.4 常见下载问题及解决
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| No CMSIS-DAP device found | DAPLink 未被电脑识别 | ① 重新插拔 USB 线 ② 换一个 USB 口(别用扩展坞) ③ 看设备管理器里面有没有新设备出来 |
| No target connected | 调试器与芯片之间通信失败 | ① 检查 SWD 接线(SWDIO、SWCLK、GND) ② 确保天空星已供电 ③ 降低 Max Clock 到 1MHz |
| Error: Flash Download failed | Flash 下载失败 | ① 检查 Debug Settings 中是否选择了正确的调试器 ② 尝试全片擦除后重新下载 ③ 检查 Flash Download 中的 Flash 编程算法是否正确 |
| 下载超时 | 通信速度过高或芯片异常 | ① 在 Debug Settings 中降低 Max Clock ② 按住天空星板子上复位键的同时点击下载 |
| Verify failed | 校验失败 | ① 尝试先全片擦除再下载 ② 检查供电是否稳定 |
TIP
万能排错大法(适用于大多数下载问题):
- 重新插拔所有 USB 线和 SWD 排线
- 换一个 USB 口(直接连电脑主板USB,不要经过扩展坞)
- 重启 Keil
- 如果还不行,重启电脑
别笑, 重启大法 在嵌入式开发中真的很管用,因为 USB 设备有时候会进入异常状态,重新枚举一下就好了。
七 简单修改代码
LED 灯闪起来了,但如果只是 照搬别人的代码运行一下 ,那和看别人的视频没什么区别。真正让你产生成就感的,是你自己修改了代码,然后看到板子的行为跟着变了。
这就像开车一样:坐在副驾驶看别人开,和自己握着方向盘,感受完全不同。
NOTE
关于代码的深入理解
本节我们会修改一些代码,但不会深入讲解每一行的含义。如果你想知道:
HAL_GPIO_TogglePin()函数内部是如何操作寄存器的?→ 请看第八章【认识GPIO】- 如何从零开始编写这些初始化代码?→ 请看第九章【新建点灯工程】
- GPIO的工作模式、上下拉、驱动能力等概念?→ 请看第八章【认识GPIO】
现在,我们只需要知道 改这里会有什么效果 就够了。
7.1 找到闪烁代码
在左侧工程窗口中,双击 main.c 文件打开它。
用鼠标滚轮往下滚,或者使用 Ctrl+G(Go to Line)跳转,找到 main 函数中的 while(1) 循环。你会看到这样的代码:

后续的所有修改请只在 USER CODE BEGIN 3 和 USER CODE END 3之间
NOTE
代码解析(简化版)
你现在不需要理解每一行代码的含义,只需要关注两行代码:
HAL_GPIO_TogglePin(CORE_LED_GPIO_Port, CORE_LED_Pin);—— 让 LED 灯状态翻转(亮变灭,灭变亮)HAL_Delay(500);—— 延时 500 毫秒(0.5 秒)
程序的逻辑是:每 500ms 翻转一次 LED → LED 亮 500ms → 灭 500ms → 亮 500ms → ……循环往复。所以你看到的闪烁周期大约是 1 秒(亮 0.5s + 灭 0.5s = 1s)。
7.2 修改延时时间
现在我们来做第一个修改:把闪烁速度变快。
把 HAL_Delay(500); 中的 500 改成 100:
HAL_Delay(100); // 延时改为100毫秒修改后按 Ctrl+S 保存文件。
7.3 重新编译并下载
- 按
F7重新编译。 - 确认编译通过(0 Error, 0 Warning)。
- 按
F8下载。

观察开发板上的 LED——你会发现闪烁速度明显变快了!现在每 0.2 秒闪一次(亮 100ms + 灭 100ms = 200ms),看起来像快速 呼吸 一样。
就这样,你修改了一个数字,单片机的行为就跟着变了! 这就是嵌入式开发的魅力:你的代码直接控制着物理世界中的硬件。
7.4 继续实验:感受不同延时的效果
趁热打铁,我们多试几个不同的延时值,直观感受一下代码和硬件之间的关系:
【注意,修改代码后一定要先编译再进行程序下载,否则天空星运行的会一直是以前的代码】
| 修改为 | 预期效果 | 直观感受 |
|---|---|---|
HAL_Delay(1000); | 每 2 秒闪一次(亮 1s + 灭 1s) | 缓慢闪烁,像呼吸灯 |
HAL_Delay(500); | 每 1 秒闪一次(默认值) | 正常闪烁 |
HAL_Delay(200); | 每 0.4 秒闪一次 | 较快闪烁 |
HAL_Delay(100); | 每 0.2 秒闪一次 | 快速闪烁 |
HAL_Delay(50); | 每 0.1 秒闪一次 | 非常快,接近"常亮"的感觉 |
HAL_Delay(10); | 每 0.02 秒闪一次 | 人眼几乎看不到闪烁了,因为频率太高了 |
TIP
有趣的物理现象:当你把延时设置到 10ms 甚至更低时,LED 看起来就像一直亮着一样,但实际上它还是在不停地亮灭。这是因为人眼的 视觉暂留 效应——当闪烁频率超过约 50Hz(每秒 50 次)时,人眼就分辨不出来了。这个原理也是电影、电视、显示器的工作基础。
7.5 进阶实验:改变闪烁模式(可选)
如果你还意犹未尽,可以尝试以下几种有趣的闪烁模式。这些实验会让你更深入地理解代码与硬件的关系。我们天空星板子上这个用户灯是高电平点亮,低电平熄灭的。
实验1:不对称闪烁(心跳模式)
让 LED 亮的时间和灭的时间不一样,模拟心跳效果:
while (1)
{
HAL_GPIO_WritePin(CORE_LED_GPIO_Port, CORE_LED_Pin, GPIO_PIN_RESET); // LED 亮
HAL_Delay(100); // 亮 100ms
HAL_GPIO_WritePin(CORE_LED_GPIO_Port, CORE_LED_Pin, GPIO_PIN_SET); // LED 灭
HAL_Delay(900); // 灭 900ms
}2
3
4
5
6
7
8

这段代码会让 LED 快闪一下然后暗很久,像心跳一样("嘀——————嘀——————")。
实验2:SOS求救信号
用LED发出摩尔斯电码的SOS信号(· · · — — — · · ·):
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
// S: 三短
for(int i = 0; i < 3; i++) {
HAL_GPIO_TogglePin(CORE_LED_GPIO_Port, CORE_LED_Pin);
HAL_Delay(200); // 短信号
HAL_GPIO_TogglePin(CORE_LED_GPIO_Port, CORE_LED_Pin);
HAL_Delay(200);
}
HAL_Delay(400); // 字母间隔
// O: 三长
for(int i = 0; i < 3; i++) {
HAL_GPIO_TogglePin(CORE_LED_GPIO_Port, CORE_LED_Pin);
HAL_Delay(600); // 长信号
HAL_GPIO_TogglePin(CORE_LED_GPIO_Port, CORE_LED_Pin);
HAL_Delay(200);
}
HAL_Delay(400); // 字母间隔
// S: 三短
for(int i = 0; i < 3; i++) {
HAL_GPIO_TogglePin(CORE_LED_GPIO_Port, CORE_LED_Pin);
HAL_Delay(200);
HAL_GPIO_TogglePin(CORE_LED_GPIO_Port, CORE_LED_Pin);
HAL_Delay(200);
}
HAL_Delay(2000); // 单词间隔,然后重复
}
/* USER CODE END 3 */
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
实验3:呼吸灯效果(挑战)
虽然真正的呼吸灯需要用PWM(脉冲宽度调制)来实现,但我们可以用快速闪烁来模拟:
while (1)
{
// 逐渐变亮(通过增加亮的时间比例)
for(int i = 1; i <= 10; i++) {
HAL_GPIO_WritePin(CORE_LED_GPIO_Port, CORE_LED_Pin, GPIO_PIN_RESET);
HAL_Delay(i);
HAL_GPIO_WritePin(CORE_LED_GPIO_Port, CORE_LED_Pin, GPIO_PIN_SET);
HAL_Delay(10 - i);
}
// 逐渐变暗(通过减少亮的时间比例)
for(int i = 10; i >= 1; i--) {
HAL_GPIO_WritePin(CORE_LED_GPIO_Port, CORE_LED_Pin, GPIO_PIN_RESET);
HAL_Delay(i);
HAL_GPIO_WritePin(CORE_LED_GPIO_Port, CORE_LED_Pin, GPIO_PIN_SET);
HAL_Delay(10 - i);
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
TIP
实验建议:
- 这些实验代码可以直接替换
while(1)循环中的内容 - 每次修改后记得保存(Ctrl+S)→ 编译(F7)→ 下载(F8)
- 观察LED的变化,思考代码和硬件行为的对应关系
- 尝试修改延时参数,看看效果有什么变化
NOTE
关于GPIO电平与LED亮灭的关系
这里用到了 HAL_GPIO_WritePin() 函数,它可以直接控制 LED 的亮灭:
GPIO_PIN_RESET(低电平)→ LED 灭(具体取决于电路设计)GPIO_PIN_SET(高电平)→ LED 灭
关于为什么低电平是亮、高电平是灭,这和LED的驱动电路设计有关。天空星核心板上的LED和筑基学习板上面的灯点亮方式不是一样的。
想深入了解?
- LED驱动电路的原理?→ 第八章【认识GPIO】会详细讲解推挽输出、开漏输出、驱动能力等概念
- 如何计算LED的限流电阻?→ 第八章会结合电路图讲解
- GPIO的输出模式有哪些?→ 第八章会讲解推挽、开漏、复用等模式
7.6 工程实践提醒:关于 HAL_Delay 的局限性
本章我们用 HAL_Delay() 来实现延时,这在学习阶段完全没问题。但在实际产品开发中,HAL_Delay() 有一个很大的缺点:它是阻塞式的。
什么意思呢?当程序执行到 HAL_Delay(500) 时,CPU 会在这里 干等 500ms,什么事都不做。就像你在等外卖的时候,站在门口一动不动地等,既不看手机也不做家务——这显然是浪费时间的(或者说浪费生命,不要浪费大好时光啊)。
在实际产品中,单片机通常需要同时处理很多任务:
- 采集传感器数据
- 刷新显示屏
- 响应按键输入
- 处理通信数据
- ……
如果用 HAL_Delay() 来做延时,CPU 在等待期间就无法处理其他任务,整个系统会变得 卡顿 。
更好的做法(后续章节会详细讲解):
- 使用硬件定时器(TIM) 来实现非阻塞延时
- 使用SysTick 计时 + 状态机来管理多任务
- 使用RTOS(实时操作系统) 来实现真正的多任务并发
NOTE
现在你不需要理解上面这些概念,只需要记住一点:HAL_Delay() 适合学习和简单实验,但不适合用在正式产品中。后续学到定时器和中断时,你会理解为什么,以及如何用更优雅的方式实现同样的功能。
这个提醒不是为了让你现在就去学定时器,而是为了在你脑海中种下一颗种子:同一个功能,可以有不同层次的实现方式。随着学习的深入,你会逐渐掌握更高级的方法。
八 进入调试模式(初步体验)
下载程序让 LED 闪烁只是开发的一部分。在实际开发中,你更多的时间会花在调试(Debug) 上——当程序没有按照预期工作时,你需要像 侦探 一样,一步一步地排查代码,找出问题所在。
Keil 提供了非常强大的在线调试功能。虽然我们会在后续章节深入讲解,但这里先让你简单体验一下,感受调试的威力。
NOTE
本节是可选内容,如果你已经很累了,可以直接跳过本章小结。调试功能我们后面还会详细讲。
8.1 进入调试模式
方法一:按快捷键 Ctrl+F5。
方法二:点击工具栏的调试按钮。

方法三:菜单 Debug -> Start/Stop Debug Session。
进入调试模式后,你会注意到界面发生了明显变化:
- 工具栏多出了一排调试控制按钮(运行、暂停、单步等)
- 左侧可能会出现 Registers(寄存器) 窗口
- 代码行号旁边出现了一个黄色箭头 ➤,指示当前程序执行到的位置

8.2 基本调试操作
上方截图中,我们先重点介绍一下前7个控制按钮,其他的功能我们在后续专题章节中再介绍吧:
- ①:复位 (Reset) ,这个是软件复位,点击后它会重新设置CPU的程序计数器(PC指针)到复位向量指向的地址,并执行启动代码,重新初始化全局/静态变量,基本等同于你按下了板子上的复位按钮。
- ②:全速运行 (Run),点击后,程序会以最快的速度(MCU内部所设置的速度)从当前位置开始执行,直到遇到你设置的断点(Breakpoint),或者你手动按下“停止”按钮,或者程序结束/卡死。
- ③:停止运行 (Stop) ,这个按钮在程序停止状态下是无效的,只有单片机内部程序在正常运行时你才可以单击让程序停止下来,这个的原理是调试器像MCU核心发送了一个Halt(暂停)指令。
- ④:单步执行-进入函数 (Step) ,字面意思,可以让程序进去执行函数内部的语句,如果你这个函数以及是最底层的没有再调用函数的话,等同于下面的⑤单步执行-跳过函数 (Step Over) 。
- ⑤:单步执行-跳过函数 (Step Over) ,单击这个按钮,可以单步执行通过这个函数,不会进入函数内部。
- ⑥:单步执行-跳出函数 (Step Out) ,如果你在调试时进入了函数内部进行单步调试,但是你已经不关心后面函数的执行情况了,就可以点这个让单片机把剩下部分直接全速执行完,并跳出这个函数。
- ⑦:运行到光标处 (Run to Cursor Line),用鼠标选择好光标位置后(直接把鼠标选定到对应函数,蓝色光标就会移动到对应位置了),此时单击这个,程序就会全速运行,直到运行到这段代码。
8.3 设置断点
断点(Breakpoint)是调试中最常用的功能。你可以在任意一行代码处设置断点,单片机程序运行到这里就会自动暂停。

设置方法:在代码行号左边的灰色区域单击,会出现一个红色圆点 🔴,表示断点已设置。再次单击可以取消。
实验:
- 在
HAL_GPIO_WritePin(...)这一行设置一个断点 - 按
F5让程序全速运行 - 程序每次运行到这一行就会自动暂停
- 再按
F5,程序会继续运行,直到再次遇到这个断点 - 每次暂停时观察 LED 的状态——你会发现它交替地亮灭
8.4 查看变量值
调试时,你还可以查看变量的实时值。
方法一:鼠标悬停
- 把鼠标放到代码中的某个变量上,会弹出一个小窗口显示当前值【只有程序停止时才能用】。

方法二:Watch 窗口
- 在代码中选中一个变量名
- 右键 → Add 'xxx' to... → Watch 1
- 在 Watch 窗口中可以看到变量的值,并且每次程序暂停时会自动更新

方法三:Locals 窗口
- 菜单
View -> Watch Windows -> Locals,会自动显示当前函数中所有局部变量的值

8.5 退出调试模式
调试完毕后,再次按 Ctrl+F5(或点击调试按钮),即可退出调试模式,界面恢复到正常编辑状态。
8.6 工程师视角:调试的价值
很多初学者觉得调试功能 可有可无 ——程序能跑就行了,为什么还要一步一步地看?
但在实际工作中,调试能力可能是区分 初级工程师 和 中高级工程师 的最重要技能之一。原因很简单:
- 程序不按预期工作是常态。在实际项目中,代码一次写对的概率远比你想象的低。
printf调试有局限性。嵌入式系统不像电脑,很多时候你没有串口输出,或者问题出在中断里、出在时序上,printf根本来不及打印。- 调试器能看到代码看不到的东西。比如寄存器的实时值、内存的实际内容、程序真正的执行路径——这些信息在排查硬件相关问题时至关重要。
所以,虽然本章只是让你 体验 了一下调试,但建议你从现在开始就有意识地多用调试功能。后续章节中,我们会结合具体案例,教你如何用调试器高效地定位问题。
TIP
一个实用的调试习惯
当你修改了代码但现象没有变化时,不要急着怀疑代码逻辑。先进入调试模式,在你修改的那行代码处设一个断点,看程序是否真的执行到了那里。
很多时候你会发现:程序根本没有走到你改的那行代码——可能是条件判断没满足,可能是函数没被调用,也可能是你改错了文件。
这个简单的排查步骤,能帮你节省大量的时间。
九 本章小结
恭喜你完成了嵌入式开发的初体验!🎉
让我们回顾一下本章的学习成果:
| 完成项 | 状态 |
|---|---|
| 理解了嵌入式开发的整体流程(写代码→编译→下载→运行) | ✅ |
| 学会了从仓库获取例程工程 | ✅ |
| 学会了在 Keil MDK 中打开工程 | ✅ |
| 熟悉了 Keil MDK 的五大界面区域 | ✅ |
| 了解了 Options for Target 中的关键配置 | ✅ |
| 学会了编译工程(F7)并看懂编译输出 | ✅ |
| 学会了下载程序(F8)到天空星开发板 | ✅ |
| 成功让 LED 灯闪烁起来 | ✅ |
| 修改了延时参数,看到了不同的闪烁效果 | ✅ |
| 初步体验了调试模式(断点、单步、查看变量) | ✅ |
你现在已经掌握了嵌入式开发最核心的工作流程。不管以后的项目多么复杂、代码多么庞大,底层的流程都是一样的:写代码 → 编译 → 下载 → 验证。万丈高楼平地起,你已经打好了第一块地基。
9.1 本章常见问题汇总(FAQ)
Q1: 编译通过了但下载时报错 "No target connected"
A: 这是最常见的问题,按以下顺序排查:
- 检查 DAPLink 和天空星之间的 SWD 排线是否接牢(尤其是 SWDIO、SWCLK、GND)
- 确保天空星已供电
- 在 Debug Settings 中确认能看到 IDCODE(0x2BA01477)
- 重新插拔 USB 线,换一个 USB 口试试
Q2: 下载成功但 LED 不闪烁
A:
- 检查 Flash Download 中是否勾选了 "Reset and Run"。如果没勾选,按一下板子上的复位按钮
- 确认例程操作的是 PB2 引脚(天空星核心板的用户 LED)
Q3: 编译时出现 "error: 'stm32f4xx_hal.h' file not found"
A: 头文件路径配置有问题。如果你没有修改过工程配置,建议重新下载例程。如果修改过,请检查 C/C++ 选项卡中的 Include Paths 是否正确。
Q4: Keil 界面字体太小/太大,怎么调?
A: 点击菜单 Edit -> Configuration,在 Colors & Fonts 选项卡中可以设置字体和字号。推荐使用 Consolas 或 Source Code Pro 字体,字号 12-14。
Q5: 每次打开工程都要重新配置调试器,有办法保存吗?
A: 配置完成后一定要点 OK 按钮来保存。如果你点的是窗口右上角的 ✕(关闭),设置将不会被保存。
Q6: 修改了代码但现象没有变化?
A: 这是初学者最容易遇到的"幽灵问题",按以下顺序排查:
- 是否保存了文件? 按
Ctrl+S确认保存。Keil 标题栏中,未保存的文件名后面会有一个*号。 - 是否重新编译了? 按
F7编译。看 Build Output 中是否有compiling main.c...,如果没有,说明 Keil 认为文件没有变化(可能是没保存)。 - 是否重新下载了? 按
F8下载。编译成功不等于下载成功,芯片里跑的还是上一次的程序。 - 是否改对了文件? 确认你改的是工程中的
main.c,而不是其他位置的同名文件。
记住这个口诀:改 → 存 → 编 → 下 → 看。
Q7: 编译时出现大量 Warning 但没有 Error,程序能正常运行吗?
A: 大多数情况下可以正常运行,但不建议忽略 Warning。Warning 是编译器在告诉你 这里可能有问题 。常见的 Warning 包括:
unused variable:定义了变量但没有使用 → 删掉不用的变量implicit declaration of function:调用了函数但没有包含对应的头文件 → 添加#includecomparison between signed and unsigned:有符号数和无符号数比较 → 统一类型
养成 零 Warning 的习惯,对你以后的工程开发很有帮助。
9.2 下一步学习建议
本章你已经学会了"照着做"来跑通一个工程。接下来,我们将逐步深入原理。
本章与后续章节的关系:
| 章节 | 定位 | 与本章的关系 |
|---|---|---|
| 第四章(本章) | 快速体验 | 使用现成例程,目标是"跑起来",建立信心 |
| 第八章【认识GPIO】 | 理论深入 | 解释本章代码背后的原理,讲清楚GPIO是什么、怎么工作的 |
| 第九章【新建点灯工程】 | 实践深入 | 教你从零开始写出寄存器控制GPIO的例程 |
学习建议:
巩固本章内容:在进入下一章之前,建议你把本章的操作再完整地做一遍。第一遍是 照着做 ,第二遍是 理解着做 。当你能不看教程就完成编译、下载、修改代码的全过程时,说明你真的掌握了。
不要跳过基础章节:第五、六、七章虽然不是直接讲STM32,但它们是理解后续内容的基础。C语言的位操作、电路的基本概念【人力有限,还在规划着写】,这些都是嵌入式工程师的必备技能。
重点关注第八章和第九章:这两章是GPIO学习的核心。第八章让你 知其然知其所以然 ,第九章让你 理解驱动GPIO的底层逻辑 。
多动手实践:看懂和会做是两回事。每学完一章,都要自己动手实践一遍,遇到问题就是学习的最好机会。
加油,嵌入式的大门已经为你敞开!
十 本节参考文档
- STM32F407x/E Datasheet(规格书)
- RM0090 Reference Manual(参考手册)
- Keil MDK 官方文档
- CMSIS-DAP 协议说明
- 天空星筑基学习板例程仓库
反馈与建议
如果你在学习过程中遇到问题,或者对本教程有任何建议,欢迎通过以下方式反馈:
- 添加开发菌企业微信,进入【立创开发板「天空星·筑基学习板」交流群】

你的反馈将帮助我们持续改进教程质量!
