STM32 零基础可移植教程 04:按键输入,为什么按下去读到的是 0 或 1
前面两篇,我们都是在控制输出:LED 亮灭、蜂鸣器响不响。
这一篇开始做输入。
输入里最适合新手上手的就是按键。它看起来也很简单:按下去,程序读一下,然后做动作。但真正写的时候,很多人会卡在这些地方:
-
CubeMX 里 GPIO 到底选
Input还是EXTI; -
按键为什么有时候按下读到 0,有时候按下读到 1;
-
Pull-up、Pull-down、No pull 到底怎么选;
-
HAL_GPIO_ReadPin()返回的值怎么判断; -
换一块开发板,按键代码为什么反了。
这篇只做一个目标:
bash
用轮询方式读取一个按键:按下时 LED 亮,松开时 LED 灭
按键消抖、短按/长按、外部中断,我们后面单独讲。这一篇先把"读到电平并判断按下/松开"这件事讲透。
本篇目标
最终现象:
bash
按住按键,LED 亮
松开按键,LED 灭
本篇用到的外设:
bash
GPIO Input
GPIO Output
本篇跑通标准:
-
Keil 编译通过;
-
程序能下载到开发板;
-
按键按下和松开时,LED 状态能跟着变化;
-
能说清楚自己的按键是"按下为低电平"还是"按下为高电平"。
本篇暂时不处理按键抖动。你可能会看到按键边沿不够干净,但"按住亮、松开灭"应该是稳定可见的。
准备工作
你需要准备:
|
项目
|
说明
|
| --- | --- |
|
STM32 开发板
|
任意 STM32 开发板
|
|
下载器
|
ST-LINK/V2 或板载 ST-LINK
|
|
LED
|
板载 LED 或外接 LED,沿用第 02 篇即可
|
|
按键
|
板载按键或外接独立按键模块
|
|
原理图
|
最好找到 LED 和按键对应的原理图
|
如果你的开发板自带用户按键,优先用板载按键。常见名称可能叫:
bash
KEY
KEY0
WK_UP
USER Button
BUTTON
不同开发板的按键引脚不一样,不要死记。一定以你的原理图为准。

硬件连接
按键最常见的连接方式有两种。
第一种:按键一端接 GPIO,一端接 GND。
bash
GPIO ---- 按键 ---- GND
这种情况下,通常要给 GPIO 开上拉:
bash
松开:GPIO 被上拉到高电平,读到 1
按下:GPIO 被接到 GND,读到 0
所以这种按键是:
bash
低电平有效
第二种:按键一端接 GPIO,一端接 3.3V。
bash
GPIO ---- 按键 ---- 3.3V
这种情况下,通常要给 GPIO 开下拉:
bash
松开:GPIO 被下拉到低电平,读到 0
按下:GPIO 被接到 3.3V,读到 1
所以这种按键是:
bash
高电平有效
这就是为什么有的教程里写"按下等于 0",有的教程里写"按下等于 1"。不是谁写错了,而是硬件接法不一样。

该电路是一个典型的按键输入检测电路,主要用于单片机或数字逻辑芯片的GPIO口输入。
电路中将PA0引脚通过一个4.7kΩ的上拉电阻R15连接到3.3V电源,确保在按键未按下时PA0保持稳定的高电平状态。
同时,PA0与按键之间串联了一个1kΩ的电阻,用以限制电容放电电流,保护GPIO口。
按键KEY1采用低电平有效的方式工作。
当按键按下时,PA0通过1kΩ电阻和按键开关被拉低至GND,此时PA0读取为低电平(0);释放按键后,PA0恢复为高电平(1)。为了抑制按键抖动和外部高频干扰,电路中还加入了一个104(100nF)的滤波电容C56,与电阻共同构成RC滤波网络,提高信号稳定性。
CubeMX 配置步骤
1. 复制上一篇工程
建议从上一篇蜂鸣器工程或第 02 篇 LED 工程复制一份,改名为:
bash
04_key_input
如果你保留 LED 代码,这一篇可以直接复用 app_led.h/.c。
如果你想从干净工程开始,也可以只配置一个 LED 输出和一个按键输入。
2. 配置 LED 输出引脚
LED 的配置和第 02 篇一样:
-
找到 LED 对应 GPIO;
-
设置为
GPIO_Output; -
User Label 填
LED; -
根据有效电平设置初始输出电平。
如果你已经从第 02 篇工程复制过来,这一步通常已经配置好了。

3. 配置按键输入引脚
找到按键对应的 GPIO 引脚。
假设你的按键接在 PA0,就在 CubeMX 里点击 PA0,选择:
bash
GPIO_Input
如果你的按键接在 PB12、PC0、PE4,就配置对应引脚。
这一篇先不用外部中断,所以不要选 GPIO_EXTI。我们先用最直观的轮询方式读取按键。

4. 给按键引脚起名字
进入:
bash
System Core -> GPIO
找到按键引脚,把 User Label 改成:
bash
KEY1
这样 CubeMX 生成代码后,会在 main.h 里生成类似:
bash
#define KEY1_Pin GPIO_PIN_0
#define KEY1_GPIO_Port GPIOA
后面应用代码就使用 KEY1_Pin 和 KEY1_GPIO_Port,不把具体引脚写死。

5. 设置 Pull-up / Pull-down
这是按键输入最容易配错的地方。
如果你的按键是:
bash
GPIO ---- 按键 ---- GND
就配置:
bash
GPIO Pull-up/Pull-down: Pull-up
如果你的按键是:
bash
GPIO ---- 按键 ---- 3.3V
就配置:
bash
GPIO Pull-up/Pull-down: Pull-down
如果开发板原理图里已经有外部上拉或外部下拉电阻,也可以选择 No pull-up and no pull-down,但新手阶段建议按原理图确认,不要猜。
6. 生成 Keil 工程
配置完成后点击:
bash
GENERATE CODE
打开 Keil 工程,先编译一次确认没有错误。

Keil 工程生成和编译
打开 Keil 后,先编译:
bash
Build / F7
确认输出里没有错误:
bash
0 Error(s)

如果这里报错,先不要怀疑按键逻辑。优先检查:
-
CubeMX 是否重新生成成功;
-
app_led.c是否还在工程里; -
新增的
app_key.c后面是否加入工程; -
KEY的 User Label 是否写对。
完整代码
这一篇我们继续使用应用层封装。
新增两个文件:
bash
Core/Inc/app_key.h
Core/Src/app_key.c
如果你复用第 02 篇 LED 代码,还需要保留:
bash
Core/Inc/app_led.h
Core/Src/app_led.c
1. 新建 Core/Inc/app_key.h
在 Core/Inc 目录下新建:
bash
app_key.h
写入下面代码:
bash
#ifndef APP_KEY_H
#define APP_KEY_H
#include "main.h"
typedefenum
{
APP_KEY_RELEASED = 0,
APP_KEY_PRESSED = 1
} App_KeyState;
void App_Key_Init(void);
App_KeyState App_Key_Read(void);
#endif
这里没有直接返回 GPIO_PinState,而是自己定义了:
bash
APP_KEY_RELEASED
APP_KEY_PRESSED
这样代码读起来更接近业务含义。
2. 新建 Core/Src/app_key.c
在 Core/Src 目录下新建:
bash
app_key.c
写入下面代码:
bash
#include "app_key.h"
/*
* KEY_GPIO_Port and KEY_Pin are generated by CubeMX in main.h.
* Set the key pin User Label to KEY in CubeMX.
*/
#ifndef KEY_GPIO_Port
#error "KEY_GPIO_Port is not defined. Set the key pin User Label to KEY in CubeMX."
#endif
#ifndef KEY_Pin
#error "KEY_Pin is not defined. Set the key pin User Label to KEY in CubeMX."
#endif
/*
* Many key circuits are active-low:
* released -> GPIO_PIN_SET
* pressed -> GPIO_PIN_RESET
*
* If your key is active-high, change this macro to GPIO_PIN_SET.
*/
#ifndef APP_KEY_PRESSED_LEVEL
#define APP_KEY_PRESSED_LEVEL GPIO_PIN_RESET
#endif
void App_Key_Init(void)
{
}
App_KeyState App_Key_Read(void)
{
GPIO_PinState level = HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin);
if (level == APP_KEY_PRESSED_LEVEL)
{
return APP_KEY_PRESSED;
}
return APP_KEY_RELEASED;
}
这里最关键的是:
bash
#define APP_KEY_PRESSED_LEVEL GPIO_PIN_RESET
它默认认为按键是低电平有效,也就是按下读到 0。
如果你的按键是高电平有效,就改成:
bash
#define APP_KEY_PRESSED_LEVEL GPIO_PIN_SET
注意,这一篇暂时没有消抖。App_Key_Read() 只是读取当前电平并判断按下/松开。
3. 把 app_key.c 加入 Keil 工程
在 Keil 工程树里右键:
bash
Application/User/Core
选择:
bash
Add Existing Files to Group 'Application/User/Core'
然后添加:
bash
Core/Src/app_key.c
如果你从干净工程开始,还要确认 app_led.c 也已经加入 Keil 工程。

如果忘了添加 .c 文件,可能会报:
bash
undefined symbol App_Key_Read
这不是函数声明错了,而是 app_key.c 没有参与编译。
main.c 调用方式
这篇我们用按键控制 LED。
按下时 LED 亮,松开时 LED 灭。
1. 在 Includes 区域添加头文件
找到:
bash
/* USER CODE BEGIN Includes */
/* USER CODE END Includes */
改成:
bash
/* USER CODE BEGIN Includes */
#include "app_led.h"
#include "app_key.h"
/* USER CODE END Includes */
2. 在初始化区域调用初始化函数
确保 MX_GPIO_Init() 已经在前面执行:
bash
MX_GPIO_Init();
然后在 USER CODE BEGIN 2 里添加:
bash
/* USER CODE BEGIN 2 */
App_LED_Init();
App_Key_Init();
/* USER CODE END 2 */
App_Key_Init() 现在是空函数,看起来好像没用。
但保留它有一个好处:后面如果你要给按键加状态机、消抖变量、长按计时,就有一个统一初始化入口。
3. 在 while 循环里读取按键
找到:
bash
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
/* USER CODE END 3 */
}
改成:
bash
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if (App_Key_Read() == APP_KEY_PRESSED)
{
App_LED_On();
}
else
{
App_LED_Off();
}
HAL_Delay(10);
/* USER CODE END 3 */
}
这里的 HAL_Delay(10) 只是让循环不要跑得太快。
注意,它不是完整消抖。真正的按键消抖,我们下一篇单独写。
编译、下载和验证
代码加完后,先编译:
bash
Build / F7
如果没有错误,再下载:
bash
Download
下载后观察现象:
bash
按住按键 -> LED 亮
松开按键 -> LED 灭

如果 Keil 下载后程序没有自动运行,但你按复位键后能运行,那就先以复位后现象为准。这个问题前面已经遇到过,它不是按键代码问题。
移植到其他板子的修改点
这篇的移植点主要有 6 个。
|
要改的地方
|
为什么要改
|
在哪里改
|
| --- | --- | --- |
|
按键引脚
|
不同板子的按键接到不同 GPIO
|
CubeMX Pinout 页面
|
|
User Label
|
代码依赖 KEY_Pin 和 KEY_GPIO_Port
|
CubeMX GPIO 页面,标签设为 KEY
|
|
上拉/下拉
|
按键接 GND 还是接 3.3V 不同
|
CubeMX GPIO Pull-up/Pull-down
|
|
按下有效电平
|
有些按下读 0,有些按下读 1
| app_key.c
里的 APP_KEY_PRESSED_LEVEL
|
|
LED 引脚和有效电平
|
LED 只是用来显示按键状态
|
沿用第 02 篇的 LED 配置
|
|
是否有外部电阻
|
有外部上拉/下拉时内部 Pull 可不同
|
看开发板原理图
|
换板子的推荐顺序:
-
看原理图,确认按键接到哪个 MCU 引脚;
-
看按键另一端接 GND 还是 3.3V;
-
在 CubeMX 里把该引脚设置成
GPIO_Input; -
User Label 填
KEY; -
根据原理图选择 Pull-up 或 Pull-down;
-
根据按下电平修改
APP_KEY_PRESSED_LEVEL; -
编译下载,用 LED 观察按键状态。
常见问题排查
1. 按下按键没反应
|
优先检查
|
具体方法
|
| --- | --- |
|
按键引脚是否选错
|
回原理图确认按键接到哪个 GPIO
|
|
User Label 是否为 KEY
|
打开 main.h 看有没有 KEY_Pin 和 KEY_GPIO_Port
|
|
上拉/下拉是否选对
|
按键接 GND 通常上拉;按键接 3.3V 通常下拉
|
|
程序是否运行
|
下载后按复位键,看 LED 是否受控
|
| app_key.c
是否加入工程
|
Keil 工程树里确认有 app_key.c
|
2. 逻辑反了:松开亮,按下灭
这通常说明按键有效电平写反了。
打开 app_key.c,找到:
bash
#define APP_KEY_PRESSED_LEVEL GPIO_PIN_RESET
如果你的按键按下读到高电平,改成:
bash
#define APP_KEY_PRESSED_LEVEL GPIO_PIN_SET
也可能是 LED 的有效电平配置反了。可以先用第 02 篇的 LED 程序确认 LED 的 On/Off 没问题,再排查按键。
3. KEY_GPIO_Port is not defined
说明 CubeMX 没有生成:
bash
KEY_GPIO_Port
KEY_Pin
去 Core/Inc/main.h 看一下。如果你看到的是:
bash
#define KEY0_Pin ...
#define KEY0_GPIO_Port ...
那说明 User Label 不是 KEY。
解决方法:
-
回 CubeMX;
-
找到按键 GPIO;
-
把 User Label 改成
KEY; -
重新 Generate Code;
-
回 Keil 编译。
4. undefined symbol App_Key_Read
通常是 app_key.c 没有加入 Keil 工程。
解决方法:
-
右键
Application/User/Core; -
选择
Add Existing Files to Group; -
添加
Core/Src/app_key.c; -
重新编译。
5. 按键偶尔乱跳
这是机械按键的常见现象,叫抖动。
你按下一次,电平可能不是干干净净地从 1 变 0,而是在几毫秒内跳几下。
本篇只是让你先读到按键电平,所以没有处理抖动。下一篇我们会专门写:
bash
STM32 按键消抖:为什么按一次会触发好几次
6. 按键一直显示按下
优先检查:
-
Pull-up/Pull-down 是否选错;
-
按键模块是否接反;
-
GPIO 是否被配置成了输出;
-
外部按键模块是否已经自带上拉/下拉;
-
APP_KEY_PRESSED_LEVEL是否写反。
如果有万用表,可以直接量按键 GPIO 对 GND 的电压:
bash
松开时电压是多少
按下时电压是多少
这比猜代码快得多。
本篇小结
这一篇我们完成了最基础的 GPIO 输入:读取按键状态,并用 LED 显示出来。
你现在至少应该知道:
-
GPIO 输入不是只写代码,先要看按键硬件接法;
-
按键接 GND 时通常按下读 0,接 3.3V 时通常按下读 1;
-
CubeMX 里按键引脚要配置成
GPIO_Input; -
Pull-up 和 Pull-down 要跟硬件接法配套;
-
User Label 填
KEY后,代码里可以使用KEY_Pin和KEY_GPIO_Port; -
APP_KEY_PRESSED_LEVEL是按键移植时最关键的宏; -
本篇没有消抖,按键抖动下一篇单独处理。
下一篇我们就接着讲:
STM32 按键消抖:为什么按一次会触发好几次。
这一篇跑通以后,你就已经具备了最基础的"输入控制输出"能力:读一个按键,控制一个 LED。后面做菜单、模式切换、参数设置,都是从这个基础上长出来的。