按键接到 STM32 后:GPIO 输入到底读到了什么
经典三问
Q: GPIO 输入是什么?
A: GPIO 输入是让芯片读取外部引脚当前电平的工作方式。芯片读到的是高电平或低电平,不是“按钮”或“按了一次”这些带有含义的动作。
Q: 为什么需要 GPIO 输入?
A: 按键、开关、数字传感器和驱动器故障信号,都需要先通过 GPIO 输入变成程序能够读取的 0 或 1。
Q: GPIO 输入常见使用场景?
A: 读取按键和拨码开关、判断模块的 DO 输出、检测限位开关、读取驱动器的报警脚,以及判断某条控制线当前是否有效。
LED 是 STM32 把电平送出去,按键则是外部电路把电平送进 STM32。
按钮按下时,电路发生了什么
现在使用两个轻触按键:A 接 PB11,B 接 PB1。每个按键的另一端接 GND。GPIO 配置成内部上拉输入。

松开按键时,按键触点断开,内部上拉把引脚维持在高电平;按下按键时,触点把引脚接到 GND,引脚变成低电平。
| 按键状态 | 引脚连接 | GPIO 读到的电平 |
|---|---|---|
| 松开 | 内部上拉,没有接地 | 高电平,通常读到 1 |
| 按下 | 通过按键接到 GND |
低电平,通常读到 0 |
这里没有“按下=1”的规定。1 和 0 只描述引脚电平,究竟哪个电平代表“按下”,由接线方式决定。当前接法是低电平有效:读到 0 就表示按键处于按下状态。
先把 PB11 和 PB1 配成输入
标准外设库初始化 GPIO 输入时,先打开 GPIOB 时钟,再给引脚选择输入模式:
#include "stm32f10x.h"
void Key_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
GPIO_Mode_IPU 的全称是 input pull-up,也就是上拉输入。上拉的作用不是“把按键按下”,而是在按键松开、外部没有把引脚接到地时,给引脚一个确定的默认高电平。
输入模式下,GPIO_Speed 不决定按键反应有多快。这个成员是 GPIO 初始化结构体的一部分,真正决定输入状态的,是引脚模式和外部接线。
GPIO_ReadInputDataBit() 读到的是什么
读取 GPIO 时,库函数只返回当前引脚的电平:
uint8_t Key_A_ReadLevel(void)
{
return (uint8_t)GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
}
uint8_t Key_B_ReadLevel(void)
{
return (uint8_t)GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1);
}
这两个函数回答一个即时问题:这一刻,PB11 或 PB1 是高电平还是低电平?
如果要得到更符合人话的结果,可以在接口里把低电平翻译成“按下”:
uint8_t Key_A_IsPressed(void)
{
return Key_A_ReadLevel() == Bit_RESET;
}
这样,电路细节留在按键模块里面,主循环只需要判断 Key_A_IsPressed()。但这个函数每调用一次,仍然只是读取当下状态。手指按住时,它会一直返回 1;松开后,它会返回 0。
直接读取电平,程序会怎样运行
把按键状态直接反映到 LED,可以先验证输入链路。假设三颗外接 LED 接在 PC13、PC14、PC15,都按点灯实验里的写法配成输出。
(补一句:这三颗灯直接接在 PC13~PC15 上,我做过,点亮都正常。后来翻数据手册第 20 页的注才知道,这三个脚不能作为电流源驱动 LED,而且同一时间只能有一个作输出——按手册这么接是出规格的,可能会让灯偏暗、几颗一起用时亮度不匀。这里我不改接线,只是把这条限制写出来。)
把 led.c 里的引脚从一颗改到三颗,输出也跟着改成三颗一起动:
#include "stm32f10x.h"
#include "led.h"
void LED_Init(void)
{
GPIO_InitTypeDef gpio;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
gpio.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
gpio.GPIO_Mode = GPIO_Mode_Out_PP;
gpio.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOC, &gpio);
LED_Off();
}
void LED_On(void)
{
GPIO_SetBits(GPIOC, GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15);
}
void LED_Off(void)
{
GPIO_ResetBits(GPIOC, GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15);
}
下面这段 main() 就是按键和这三颗灯的连接:
int main(void)
{
Key_Init();
LED_Init();
while (1)
{
if (Key_A_IsPressed())
{
LED_On();
}
else
{
LED_Off();
}
}
}
按住 A,PB11 维持低电平,程序就持续读到“按下”,LED 保持点亮;松开 A,PB11 回到高电平,LED 熄灭。程序没有把按键变成一个计数,也没有把一次按下保存成事件,它只是不断观察输入电平,并让输出跟着变化。
| GPIO 工作方式 | 芯片做的事 | 典型对象 | 程序使用的库函数 |
|---|---|---|---|
| 输出 | 芯片主动改变引脚电平 | LED、继电器使能、电机方向 | GPIO_SetBits()、GPIO_ResetBits() |
| 输入 | 芯片读取引脚当前电平 | 按键、开关、传感器 DO | GPIO_ReadInputDataBit() |
输出是“我把什么电平送出去”,输入是“外面给了我什么电平”。同一个引脚不能只看代码里的函数名判断功能,最终还要看它连接的电路。
从当前电平到一次按键
实际工程通常不希望按键一直按住时重复执行同一个动作,因此会把“当前电平”再整理成“一次按下”。我会在发现低电平后等一小段时间,再确认按键还按着,最后等它松手:
uint8_t Key_GetNum(void)
{
uint8_t KeyNum = 0;
volatile uint32_t i;
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == Bit_RESET)
{
for (i = 0; i < 72000U; i++)
{
}
while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == Bit_RESET)
{
}
for (i = 0; i < 72000U; i++)
{
}
KeyNum = 1;
}
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == Bit_RESET)
{
for (i = 0; i < 72000U; i++)
{
}
while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == Bit_RESET)
{
}
for (i = 0; i < 72000U; i++)
{
}
KeyNum = 2;
}
return KeyNum;
}
这里的空 for 循环只是让程序暂时等一小段时间,再重新确认按键状态。它被放在按键确认前后,避免刚碰到或刚松开时立刻做出判断。72000 只是一个直观的循环次数,实际等待多久还会受主频和编译器优化影响。按键函数还会等待松手,避免手指一直按着时反复返回同一个编号。
这段写法能把“持续的低电平”整理成一次返回值。
输入模式一共有四种
上拉不是唯一的选择。GPIO_Mode 这一格在输入方向上能填四个枚举常量,区别在于引脚内部接了什么:
GPIO_Mode |
名字 | 引脚在做什么 | 什么时候用 |
|---|---|---|---|
GPIO_Mode_IPU |
上拉输入 | 内部接一个上拉电阻,外面没人驱动时读到高电平 | 按键另一端接 GND(当前接法) |
GPIO_Mode_IPD |
下拉输入 | 内部接一个下拉电阻,外面没人驱动时读到低电平 | 按键另一端接 VCC |
GPIO_Mode_IN_FLOATING |
浮空输入 | 内部什么都不接,电平完全由外部电路决定 | 外部已经有明确的上拉或下拉,或者由另一个推挽输出驱动它 |
GPIO_Mode_AIN |
模拟输入 | 关掉数字输入通路,把引脚交给 ADC | 读 AO 那种连续变化的电压 |
选哪一种,要看外面接了什么——引脚内部那一侧改变不了外部电路。当前接法里按键另一端接 GND,所以用上拉:松开时内部把引脚拉到高电平,按下时按键把引脚拉到地。
下面这个例子就把模式换掉:同样是这个按键,改成浮空输入会怎样。
浮空输入为什么没有固定答案
把 GPIO_Mode_IPU 改成 GPIO_Mode_IN_FLOATING,再松开按键,PB11 就没有内部上拉提供默认电平。此时引脚既没有明确接高,也没有明确接低,周围的电线、手指和电磁干扰都可能影响它。
浮空输入不是“自动随机数”,而是电路没有为引脚规定稳定的 0 或 1。按键松开时,如果你希望程序稳定读到高电平,就使用内部上拉或外部上拉;如果电路需要默认低电平,就使用下拉方案。输入模式的选择必须和接线一起看。
GPIO 输入的本质
从按键这个例子可以把 GPIO 重新整理一遍:
- GPIO 是芯片和外部电路之间的一组数字引脚;
- 输出模式让芯片驱动引脚电平,输入模式让芯片读取引脚电平;
- 按键本身只负责接通或断开电路,程序看到的是电平变化;
- 高电平和低电平没有固定的“好”和“坏”,有效含义由电路决定;
- “按下”“松开”“一次操作”都是程序在电平变化之上整理出来的意义。
当你看到一个按键读值不对时,先沿着这条链路检查:按键有没有把引脚接到正确的位置,输入有没有默认上拉或下拉,读取的引脚是否和接线一致,最后才去看上层动作有没有重复执行。
最后橘猫说
按键没有把“我被按了一次”直接送进芯片,它只改变了 GPIO 引脚上的电平。当前接法中,松开是高电平,按下是低电平;GPIO_ReadInputDataBit() 负责读出这个瞬间的 0 或 1,Key_A_IsPressed() 只是把电平翻译成更好用的名字。GPIO 输出负责把电平送出去,GPIO 输入负责把外部电平读进来,LED 和按键正好把这两个方向摆在了一起。至于如何把连续电平整理成一次可靠操作,后面的按键专题会继续处理。

按键的一端接在引脚上,另一端接到哪里,芯片看到的并不一样。同一颗按键、同一段程序,只是换一种输入模式,松手之后的读数为什么就没了定论?
浙公网安备 33010602011771号