按键接到 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 配置成内部上拉输入。

05-01-key接线图

松开按键时,按键触点断开,内部上拉把引脚维持在高电平;按下按键时,触点把引脚接到 GND,引脚变成低电平。

按键状态 引脚连接 GPIO 读到的电平
松开 内部上拉,没有接地 高电平,通常读到 1
按下 通过按键接到 GND 低电平,通常读到 0

这里没有“按下=1”的规定。10 只描述引脚电平,究竟哪个电平代表“按下”,由接线方式决定。当前接法是低电平有效:读到 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);
}

这两个函数回答一个即时问题:这一刻,PB11PB1 是高电平还是低电平?

如果要得到更符合人话的结果,可以在接口里把低电平翻译成“按下”:

uint8_t Key_A_IsPressed(void)
{
    return Key_A_ReadLevel() == Bit_RESET;
}

这样,电路细节留在按键模块里面,主循环只需要判断 Key_A_IsPressed()。但这个函数每调用一次,仍然只是读取当下状态。手指按住时,它会一直返回 1;松开后,它会返回 0。

直接读取电平,程序会怎样运行

把按键状态直接反映到 LED,可以先验证输入链路。假设三颗外接 LED 接在 PC13PC14PC15,都按点灯实验里的写法配成输出。

(补一句:这三颗灯直接接在 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 和按键正好把这两个方向摆在了一起。至于如何把连续电平整理成一次可靠操作,后面的按键专题会继续处理。

posted @ 2026-09-22 08:45  Zw-awa  阅读(0)  评论(0)    收藏  举报