硬件原理
按键电路
开发板上的两个按键分别连接在以下引脚:
| 按键 | 引脚 | 信号 | 说明 |
|---|---|---|---|
| KEY1 | PB12 | 按下=低电平,松开=高电平 | 板载上拉电阻 |
| KEY2 | PB13 | 按下=低电平,松开=高电平 | 内部上拉(芯片内部准备好了,只需要在CubeMX中配置,无需外加电阻) |
按键电路原理:一端接地,另一端通过上拉电阻连接到GPIO引脚。按下时引脚读到低电平,松开时读到高电平。
LED 电路
| LED | 引脚 | 控制信号 |
|---|---|---|
| 🟢 绿色 | PA7 | GPIO 高电平亮 |
| 🔴 红色 | PB0 | GPIO 高电平亮 |

CubeMX 引脚配置
新建项目
芯片选择 STM32F103C8T6,步骤与之前一样,略。
配置 GPIO 引脚
一共四个引脚需要配置:两个LED(输出)、两个按键(输入)。
LED 引脚(输出):
| 引脚 | 功能 | User Label | 其他参数 |
|---|---|---|---|
| PA7 | GPIO_Output | LED_GREEN |
推挽输出、无上下拉、低速 |
| PB0 | GPIO_Output | LED_RED |
推挽输出、无上下拉、低速 |
按键引脚(输入):
| 引脚 | 功能 | User Label | 上下拉 |
|---|---|---|---|
| PB12 | GPIO_Input | KEY1 |
无(板载已有上拉) |
| PB13 | GPIO_Input | KEY2 |
Pull-up(内部上拉) |


两个关键点:
- KEY1(PB12)——板子自带了外部上拉电阻,所以 CubeMX 里
Pull选No pull - KEY2(PB13)——这个引脚板子没有外部上拉,需要在 CubeMX 里把
Pull设为 Pull-up,这样芯片内部会开启上拉电阻
配置调试接口
这一步绝对不能忘!——System Core → SYS → Debug → Serial Wire。否则烧录一次后 SWD 就被禁用了。
生成代码
Project Manager 设置:
| 设置项 | 值 |
|---|---|
| Project Name | KEY |
| Toolchain / IDE | CMake |
点击 Generate Code,用 VS Code 打开。
VS Code 中编写代码
认识 main.h 中的宏定义
CubeMX 根据 User Label 生成了以下宏:
1 | // LED |
核心逻辑设计
这次要实现两种不同的按键逻辑:
| 按键 | 功能 | 实现方式 |
|---|---|---|
| KEY1 | 按下→绿灯亮,松开→绿灯灭 | 每个循环读取 KEY1 电平,直接控制绿灯 |
| KEY2 | 每按一次→红灯翻转(亮↔灭) | 检测到按键后翻转红灯,等待松开再继续 |
main.c 代码
打开 main.c,在 while(1) 中编写:
1 | /* USER CODE BEGIN WHILE */ |
代码讲解
用到的函数:
| 函数 | 作用 |
|---|---|
HAL_GPIO_ReadPin(端口, 引脚) |
读取引脚电平:GPIO_PIN_SET(高)或 GPIO_PIN_RESET(低) |
HAL_GPIO_WritePin(端口, 引脚, 电平) |
设置引脚高/低电平 |
HAL_GPIO_TogglePin(端口, 引脚) |
翻转引脚电平 |
KEY1 的逻辑——直接读取直接控制:
每次 while 循环都判断 KEY1 的电平。按下就读到低电平→亮,松开读到高电平→灭。反应很及时,适合开关类控制。
KEY2 的逻辑——边缘触发(按一下变一次):
KEY2 要的是”按一下变一次”,而不是”按住的时候一直变”。所以多了三步:
- 消抖——机械按键按下瞬间会有抖动(几ms内电平反复跳变),加 10ms 延时后再读一次确认,防止误触发
- 翻转——确认按下后执行一次
TogglePin - 等待松手——用 while 循环等待按键恢复高电平,防止一次按压被 while 循环反复检测到,造成很短的时间内多次翻转,肉眼看上去效果很差。
解读 MX_GPIO_Init —— 输出与输入引脚的配置
这个项目有四种引脚(两个输出、两个输入),MX_GPIO_Init() 比前两篇复杂了不少:
1 | static void MX_GPIO_Init(void) |
新知识点:输入模式配置
输出引脚没什么新内容,重点是两个输入引脚的配置:
GPIO_MODE_INPUT —— 输入模式
之前两篇都是 GPIO_MODE_OUTPUT_PP(输出),这次改成了 GPIO_MODE_INPUT(输入)。输入模式下,GPIO 不再输出电压,而是读取外部信号的电平高低,通过 HAL_GPIO_ReadPin() 获取。
Pull 参数 —— 上下拉电阻的选择
这是本次最有价值的部分——两种按键的上拉电阻配置方式不同:
| 按键 | Pull 参数 | 原理 |
|---|---|---|
| KEY1 (PB12) | GPIO_NOPULL |
板子自带了外部上拉电阻,芯片内部不需要再配 |
| KEY2 (PB13) | GPIO_PULLUP |
板子没有外部上拉,需要开启芯片内部上拉电阻 |
为什么需要上拉?看按键电路:
1 | 无上拉: PB12 -------- 按键 ---- GND |
- 按键松开:上拉电阻把电平拉到 3.3V,读到高电平
- 按键按下:引脚直接接 GND,读到低电平(电流经过按键到地,不走电阻)
如果没有上拉电阻,按键松开时引脚悬空,电平是浮动的,读到的值可能随机变化。
内部上拉 vs 外部上拉
| 对比 | 外部上拉 | 内部上拉 |
|---|---|---|
| 实现 | 板子上焊接电阻 | 芯片内部集成,通过软件配置开启 |
| 阻值 | 可按需选择(通常 4.7kΩ~10kΩ) | 固定约 40kΩ(STM32F1) |
| 优点 | 阻值精确、驱动能力强 | 省元件、省PCB空间 |
| 缺点 | 占PCB空间、增加BOM成本 | 阻值偏大、抗干扰稍弱 |
KEY1 使用外部上拉是因为外部上拉相比内部上拉能提供更稳定的高电平,而 KEY2 使用内部上拉则是为了节省板子空间和成本。
GPIO 模式表(更新版)
加上输入模式后,现在接触到的 GPIO 模式更丰富了:
| 模式 | 含义 | 本篇中的用途 |
|---|---|---|
GPIO_MODE_OUTPUT_PP |
推挽输出 | 驱动 LED |
GPIO_MODE_INPUT |
数字输入 | 读取按键状态 |
GPIO_NOPULL |
无上下拉 | 输出引脚 / 外部已有上拉的输入引脚 |
GPIO_PULLUP |
内部上拉 | 没有外部上拉的输入引脚 |
编译 & 烧录
- VS Code 底部工具栏点击齿轮图标 → 生成
- 连接 ST-Link,点击运行和调试 → 选择 STM32Cube:STM32 Launch STLink GDB Server
- 点击继续按钮
如果遇到烧录问题(连不上芯片),参考波特律动 FAQ 的解决方法。
效果展示
烧录成功后:
- 按下 KEY1 → 🟢 绿灯亮
- 松开 KEY1 → 🟢 绿灯灭
- 按一下 KEY2 → 🔴 红灯翻转(亮↔灭),每按一次变一次
效果展示视频链接:
https://www.bilibili.com/video/BV1dFg96MEJq/?vd_source=5a111e8d49f70e532d36ddb73be3d0aa
进阶思路
当前代码的局限性
仔细观察 KEY2 的处理逻辑,会发现一个问题:按下 KEY2 还没松开的时候,程序卡死在 while 循环里了。
1 | while (HAL_GPIO_ReadPin(KEY2_GPIO_Port, KEY2_Pin) == GPIO_PIN_RESET) {} |
这行代码的意思是”在这里空转,直到 KEY2 松开”。在这期间:
- ❌ KEY1 按了没反应(while 循环不检测 KEY1)
- ❌ 其他任何外设都无法响应
- ❌ 如果一直不松手,程序就一直卡着
这在只有两个按键、两个LED的 demo 里问题不大,但如果在实际项目里——比如同时要检测多个传感器、控制电机、处理通信——这种”阻塞式等待”的方式就行不通了。
怎么解决?
有以下几种方案,由简单到专业:
方案一:状态机 + 非阻塞延时
不用 while 傻等,而是用变量记录按键状态和时间,每次循环只检查”按键是不是还被按着”。这样即使 KEY2 没松开,main loop 也能去干别的事。
1 | // 伪代码思路 |
方案二:定时器轮询
把按键检测放到定时器中断里,每隔 10ms 采样一次电平,用状态机判断按下和松开。主循环完全解放出来做正事。
方案三:外部中断(EXTI)
最专业的方法。按键按下时触发中断,CPU 暂停当前任务来处理按键,处理完再回去。不需要任何轮询,效率最高。
这也是嵌入式开发的典型学习路径——先学会在 main loop 里轮询,遇到阻塞问题后,自然就会去学中断和定时器。




