按键接到 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"的规定。10 只描述引脚电平,究竟哪个电平代表"按下",由接线方式决定。当前接法是低电平有效:读到 0 就表示按键处于按下状态。

先把 PB11 和 PB1 配成输入

标准外设库初始化 GPIO 输入时,先打开 GPIOB 时钟,再给引脚选择输入模式:

c 复制代码
#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 时,库函数只返回当前引脚的电平:

c 复制代码
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 是高电平还是低电平?

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

c 复制代码
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 里的引脚从一颗改到三颗,输出也跟着改成三颗一起动:

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() 就是按键和这三颗灯的连接:

c 复制代码
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()

输出是"我把什么电平送出去",输入是"外面给了我什么电平"。同一个引脚不能只看代码里的函数名判断功能,最终还要看它连接的电路。

从当前电平到一次按键

实际工程通常不希望按键一直按住时重复执行同一个动作,因此会把"当前电平"再整理成"一次按下"。我会在发现低电平后等一小段时间,再确认按键还按着,最后等它松手:

c 复制代码
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 和按键正好把这两个方向摆在了一起。至于如何把连续电平整理成一次可靠操作,后面的按键专题会继续处理。

相关推荐
MarkZhou6241 小时前
智能蒸烤双核通信架构拆解:从冗余耦合到分层解耦
stm32·单片机·嵌入式硬件·架构·安卓·安全架构
chem41112 小时前
STM32CubeMX Linux 开发环境搭建
stm32·单片机·linux系统
无敌贵点大王12 小时前
RTThread学习记录11——RT-Thread 设备模型吃透:UART/ADC/PWM/PIN 到底有什么区别?
c语言·stm32·学习·链表
恶魔泡泡糖15 小时前
stm32F103C8T6标准库串口接收之中断方式2
stm32·单片机·嵌入式硬件
充哥单片机设计16 小时前
【STM32开源项目】智能小区充电桩
stm32·单片机·嵌入式硬件·毕业设计·小区充电桩·智能小区充电桩
充哥单片机设计17 小时前
【STM32开源项目】智能语音台灯
stm32·单片机·嵌入式硬件·毕业设计·单片机设计·智能语音台灯
Brilliantwxx19 小时前
【STM32】 SPI阻塞式通信与HAL源码 (源码分析)
stm32·单片机·嵌入式硬件
WWJA王文举21 小时前
Bootloader、App 和升级分区之间是什么关系?
stm32·freertos·嵌入式开发·在线升级
LCG元1 天前
STM32F103 NTC 热敏电阻测温实战:分压网络设计、Steinhart-Hart 标定与 ADC 采样优化实测
stm32·单片机·嵌入式硬件