让你的开发板“听得懂”你按了什么!

🎯 适用对象:零基础嵌入式小白
💡 开发环境:STM32G431RBT6 + STM32CubeMX + HAL 库
✅ 前提:你已在 CubeMX 中将按键引脚配置为 GPIO_Input + Pull-up


🌟 引子:为什么按键不是“一按就灵”?

想象你家的门铃

  • 如果你按一下,它“嘀——”响一次,很舒服。
  • 但如果你按的时候手抖了一下,它“嘀嘀嘀嘀……”响十次,你会崩溃!

这就是机械按键的“抖动”问题
而我们的任务,就是让单片机像一个聪明的管家

“主人只按了一次?那我就执行一次动作,不管他手抖没抖!”


🔌 一、硬件原理图:按键是怎么连的?

在 STM32G431RBT6 开发板上,4 个独立按键通常这样连接:

         VCC (3.3V)
              │
        [内部上拉电阻] ← CubeMX 启用!
              │
              ├── PB0 ──┬── 按键1 ── GND
              ├── PB1 ──┬── 按键2 ── GND
              ├── PB2 ──┬── 按键3 ── GND
              └── PA0 ──┬── 按键4 ── GND

🧠 三种 GPIO 输入模式(笔记中的精华!)

模式 原理 类比 适用场景
上拉(Pull-up) 内部电阻接 VCC,无信号时为高电平 就像弹簧门:没人推时自动关(高电平) 按键接地(最常见!)
下拉(Pull-down) 内部电阻接地,无信号时为低电平 就像重力门:没人拉时自动开(低电平) 按键接 VCC(少见)
浮空(Floating) 无上下拉,电平不确定 就像风中旗帜:风吹哪边倒哪边 外部电路已提供确定电平

✅ 设计用的是 上拉 + 按键接地,所以:

  • 没按 → 高电平(GPIO_PIN_SET
  • 按下 → 低电平(GPIO_PIN_RESET

📚 二、HAL 库核心 API:HAL_GPIO_ReadPin()

这是你和硬件“对话”的桥梁!

函数原型:

GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);

参数说明:

  • GPIOx:端口,如 GPIOAGPIOB
  • GPIO_Pin:引脚,如 GPIO_PIN_0

返回值:

  • GPIO_PIN_SET → 高电平(3.3V)
  • GPIO_PIN_RESET → 低电平(0V)

使用场景(你笔记中的总结):

  • ✅ 按键检测:判断是否被按下
  • ✅ 传感器读取:如红外避障模块输出
  • ✅ 通信监测:如检测 UART 的 CTS 信号

实现原理(底层揭秘):

  1. 读取 输入数据寄存器(IDR)
  2. 通过位操作提取指定引脚状态

    就像从一排开关中,只看第3个是不是“关”了。


📦 三、方案一:简洁版 —— “瞬间识别按下/松开”

适合初学者快速上手,代码少、逻辑清!

🎯 生活比喻:电梯按钮

  • 你按一下“3楼”,电梯记住:“有人要去3楼!”
  • 即使你手抖按了5次,电梯也只执行一次动作。

1. 全局变量(像四个小信箱)

// key_app.h
#ifndef __KEY_APP_H
#define __KEY_APP_H

#include "stm32g4xx_hal.h"

extern uint8_t key_val;   // 现在谁在按?(0=没人,1~4=按键号)
extern uint8_t key_old;   // 上一秒谁在按?
extern uint8_t key_down;  // 谁**刚刚开始按**?(只触发一次!)
extern uint8_t key_up;    // 谁**刚刚松手**?

uint8_t key_read(void);
void key_proc(void);

#endif

2. 核心函数(完整保留的逻辑)

// key_app.c
#include "key_app.h"

uint8_t key_val = 0;
uint8_t key_old = 0;
uint8_t key_down = 0;
uint8_t key_up = 0;

/**
 * @brief 读取按键状态
 * @return 0: 无按键;1~4: 对应按键被按下
 */
uint8_t key_read(void)
{
    uint8_t temp = 0;

    if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET) temp = 1;
    if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET) temp = 2;
    if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2) == GPIO_PIN_RESET) temp = 3;
    if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) temp = 4;

    return temp;
}

/**
 * @brief 按键处理函数
 * 更新 key_down(刚按下)和 key_up(刚松开)标志
 */
void key_proc(void)
{
    key_val = key_read();                     // 读当前状态
    key_down = key_val & (key_old ^ key_val); // 刚刚按下的键
    key_up   = (~key_val) & (key_old ^ key_val); // 刚刚松开的键
    key_old = key_val;                        // 更新历史
}

🧠 位运算魔法(小白也能懂):

  • key_old ^ key_val → 找出变化了的键
  • key_val & (...) → 只保留从“没按”变成“按了” 的键 → 按下事件
  • (~key_val) & (...) → 只保留从“按了”变成“没按” 的键 → 松开事件

✅ 效果:长按不重复触发,只在“按下瞬间”执行一次!


🧠 四、方案二:状态机版 —— “智能管家模式”

适合需要去抖、长按、连按的复杂场景!

🎯 生活比喻:手机闹钟 App(经典例子!)

你的手机闹钟有三个状态:

  1. 未设定 → 你还没设时间
  2. 已设定 → 时间设好了,等待响铃
  3. 响铃中 → 到点了!叮铃铃!

状态之间会转换:

  • 设定闹钟 → 从未设定 → 已设定
  • 时间到 → 从已设定 → 响铃中
  • 关闭 → 从响铃中 → 未设定

按键状态机同理!

按键的三个状态:

  1. 空闲(没按)
  2. 按下(正在按)
  3. 释放(刚松手,判断是否连按)

1. 结构体定义(每个按键都是一个“小管家”)

// key_state.h
#ifndef __KEY_STATE_H
#define __KEY_STATE_H

#include "stm32g4xx_hal.h"

typedef struct {
    GPIO_TypeDef *gpiox;      // 控制哪个端口(如 GPIOB)
    uint16_t pin;             // 控制哪个引脚(如 PIN_0)
    uint16_t ticks;           // 计时器(单位:任务周期)
    uint8_t level;            // 当前电平(1=高,0=低)
    uint8_t id;               // 按键编号(0~3)
    uint8_t state;            // 状态:0=空闲,1=按下,2=释放
    uint8_t debouce_cnt;      // 去抖计数器
    uint8_t repeat;           // 连按次数
} button;

extern button btns[4];

void key_init(void);
void key_task(button *btn);
void key_state(void);

#endif

2. 初始化与状态机函数(完整版)

// key_state.c
#include "key_state.h"

button btns[4];

void key_init(void)
{
    btns[0].gpiox = GPIOB; btns[0].pin = GPIO_PIN_0; btns[0].level = 1; btns[0].id = 0;
    btns[1].gpiox = GPIOB; btns[1].pin = GPIO_PIN_1; btns[1].level = 1; btns[1].id = 1;
    btns[2].gpiox = GPIOB; btns[2].pin = GPIO_PIN_2; btns[2].level = 1; btns[2].id = 2;
    btns[3].gpiox = GPIOA; btns[3].pin = GPIO_PIN_0; btns[3].level = 1; btns[3].id = 3;
}

void key_task(button *btn)
{
    // 读取当前电平(0=按下,1=释放)
    uint8_t gpio_level = (HAL_GPIO_ReadPin(btn->gpiox, btn->pin) == GPIO_PIN_RESET) ? 0 : 1;

    // 非空闲状态才计时
    if (btn->state > 0) {
        btn->ticks++;
    }

    // 去抖处理:连续3次读到相同变化才确认
    if (btn->level != gpio_level) {
        if (++(btn->debouce_cnt) >= 3) {
            btn->level = gpio_level;
            btn->debouce_cnt = 0;
        }
    } else {
        btn->debouce_cnt = 0;
    }

    // 状态机
    switch (btn->state) {
        case 0: // 空闲状态
            if (btn->level == 0) { // 按下了!
                btn->ticks = 0;
                btn->repeat = 1;   // 第一次按
                btn->state = 1;    // 进入“按下”状态
            }
            break;

        case 1: // 按下状态
            if (btn->level == 1) { // 松开了!
                if (btn->ticks >= 15) { // 长按(假设15*5ms=75ms)
                    btn->repeat = 0; // 防止释放时误触发单击
                }
                btn->ticks = 0;
                btn->state = 2; // 进入“释放”状态
            }
            break;

        case 2: // 释放状态
            if (btn->ticks >= 15) {
                btn->state = 0; // 真正结束,回到空闲
            } else {
                if (btn->level == 0) { // 快速连按!
                    btn->repeat++;
                    btn->ticks = 0;
                    btn->state = 1; // 直接回到“按下”
                }
            }
            break;
    }
}

void key_state(void)
{
    for (uint8_t i = 0; i < 4; i++) {
        key_task(&btns[i]);
    }
}

🧪 五、如何使用?完整示例

方法一:简洁版(推荐新手)

// main.c
#include "key_app.h"
#include <stdio.h>

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();

    while (1) {
        key_proc();  // 每10ms调用一次

        if (key_down == 1) {
            printf("✅ 按键1被按下!\n");
        }
        if (key_up == 2) {
            printf("👋 按键2被松开了!\n");
        }

        HAL_Delay(10);
    }
}

方法二:状态机版(高级玩法)

// main.c
#include "key_state.h"

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();

    key_init();

    while (1) {
        key_state();  // 每5ms调用一次

        // 检测按键0是否刚按下
        if (btns[0].state == 1 && btns[0].ticks == 0) {
            if (btns[0].repeat == 1) {
                printf("🔘 按键1单击\n");
            } else {
                printf("🔁 按键1连按第 %d 次\n", btns[0].repeat);
            }
        }

        HAL_Delay(5);
    }
}

🧠 本章口诀(背下来!)

🔘 按键接地接上拉
没按是高,按下是低!

📏 HAL_GPIO_ReadPin
RESET 就是按下了!

📦 简洁版:down/up 分明
电梯按钮一次灵!

🧠 状态机:像闹钟一样聪明
去抖长按连按全搞定!

⏱️ 每 5~10ms 扫一次
按键响应快又稳!


✅ 总结

提供的两套代码,分别代表了嵌入式按键处理的两种哲学

  • 简洁版:像“快递员”——快速送达,不啰嗦
  • 状态机版:像“智能管家”——去抖、长按、连按,样样精通

现在,即使是第一次接触单片机的小白,也能根据项目需求,选择最适合的方案!

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐