【嵌入式学习笔记】Key模块解析

前言

本系列学习笔记是本人跟随米醋电子工作室学习嵌入式的学习笔记,自用为主,不是教学或经验分享,若有误,大佬轻喷,同时欢迎交流学习,侵权即删。

一、GPIO 引脚的三种:输入模式

在连接按键之前,我们必须了解单片机 GPIO (通用输入/输出) 引脚作为输入时,有三种基础的"性格设定":上拉 (Pull-up)下拉 (Pull-down)浮空 (Floating)。它们决定了引脚在没有明确外部信号时(比如按键没按下)的默认电平状态,这对于稳定读取按键至关重要。

1.1. 上拉 (Pull-up) ⬆️ ------ 默认"开"

它是啥? 想象引脚内部有一根"橡皮筋"轻轻地把它往"高电平"(通常是电源 VCC)拉。当没有更强的力量(比如按键按下接地)作用时,它就保持在高电平。

为啥用? 防止引脚"悬空"乱飘。就像一个开关,默认是开着的,只有你按下时才关断。

何时用? 当你需要引脚在空闲时稳定地输出高电平,或者连接的按键按下时会把引脚拉到低电平 (GND)。这是最常见的按键连接方式之一。

1.2. 下拉 (Pull-down) ⬇️ ------ 默认"关"

它是啥? 和上拉相反,这次"橡皮筋"是把引脚往"低电平"(地线 GND)拉。默认状态是低电平。

为啥用? 同样是为了防止悬空,提供一个明确的默认状态。

何时用? 当你需要引脚空闲时稳定在低电平,或者连接的按键按下时会把引脚接到高电平 (VCC)。相对上拉来说,用得稍微少一些。

1.3. 浮空 (Floating / High-Impedance) 👻 ------ "随波逐流"

它是啥? 引脚内部既不上拉也不下拉,像个与世无争的"隐士"。它的电平完全由外部连接决定。如果什么都没接,它的状态就像空气中的羽毛,极其不稳定,容易受到各种电磁干扰。

为啥用? 有些特定场景需要引脚呈现高阻态,不影响外部电路(比如模拟信号输入,或者需要外部电路精确控制电平)。但在简单的数字输入,尤其是按键检测中,极少使用浮空模式,因为它太容易"受惊"了。

注意! 直接用浮空模式连接按键通常是不可靠的,你需要外部电路(比如外部上拉或下拉电阻)来明确默认电平。

✨ 小结

  • ⬆️上拉: 默认高电平 (VCC),按键通常接到 GND
  • ⬇️下拉: 默认低电平 (GND),按键通常接到 VCC
  • 👻浮空: 无默认电平,易受干扰,一般不直接用于按键。

正确选择和配置输入模式是按键能够被可靠读取的前提。对于大多数简单的按键电路,使用单片机内部的上拉或下拉功能是最方便有效的方式。

二、常规按键驱动(无组件库)代码解析

2.1. 代码整体说明

该代码是嵌入式中最基础的按键驱动实现方式,无依赖第三方库,通过 "状态对比法" 检测按键按下事件,实现按键 1 控制 LED0 翻转功能。核心逻辑为 "读取硬件状态→对比历史状态→识别按键事件→执行业务逻辑",适合入门理解按键驱动的底层原理。

2.2. 完整代码逐行解析

cpp 复制代码
#include "key_app.h"

// 全局变量:存储按键状态(核心状态变量)
uint8_t key_val, key_old, key_down, key_up;

/**
 * @brief 读取按键硬件状态(底层GPIO操作)
 * @return 按键编号(1-6对应6个按键,0表示无按键按下)
 */
uint8_t key_read()
{
	uint8_t temp = 0;  // 初始值0(无按键按下)
	
	// 逐个读取GPIO引脚电平,判断按键是否按下
	// 硬件逻辑:按键为上拉输入,按下时引脚为低电平(GPIO_PIN_RESET)
	if(HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_15) == GPIO_PIN_RESET ) temp = 1;
	if(HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_13) == GPIO_PIN_RESET ) temp = 2;
	if(HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_11) == GPIO_PIN_RESET ) temp = 3;
	if(HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_9) == GPIO_PIN_RESET ) temp = 4;
	if(HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_7) == GPIO_PIN_RESET ) temp = 5;
	if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET ) temp = 6;

	return temp;
}

/**
 * @brief 按键任务函数(需周期性调用,如10ms/次)
 * @note 核心逻辑:通过"当前状态-历史状态"对比识别按键事件
 */
void key_task()
{
	// 1. 读取当前按键状态
	key_val = key_read();
	
	// 2. 计算按键按下/松开事件(位运算核心逻辑)
	// key_down:仅当"当前按下"且"历史未按下"时为非0(按下事件)
	key_down = key_val & (key_old ^ key_val);
	// key_up:仅当"当前松开"且"历史按下"时为非0(松开事件)
	key_up = ~key_val & (key_old ^ key_val);
	
	// 3. 更新历史状态(为下一次对比做准备)
	key_old = key_val;
	
	// 4. 业务逻辑:按键1按下时,翻转LED0状态
	if(key_down == 1)
	{
		ucLed[0] ^= 1;  // ^=1 表示翻转(1→0,0→1)
	}
}

2.3. 核心变量与逻辑说明

变量名 作用
key_val 本次扫描读取的当前按键状态(1-6 表示对应按键按下,0 表示无按键)
key_old 上一次扫描的历史按键状态(用于与当前状态对比)
key_down 按键按下事件标志(仅在 "从无到有" 时为对应按键编号,其余时间为 0)
key_up 按键松开事件标志(仅在 "从有到无" 时为对应按键编号,其余时间为 0)

关键位运算解析(事件识别核心)

  • key_old ^ key_val:异或运算,结果为 1 的位表示 "状态变化"(按下 / 松开);
  • key_val & (key_old ^ key_val):仅保留 "状态变化且当前按下" 的位→识别按下事件;
  • ~key_val & (key_old ^ key_val):仅保留 "状态变化且当前松开" 的位→识别松开事件。

三、使用组件库的核心思想

在嵌入式开发中,外设管理(如按键、LED、传感器等)是常见需求。直接从零开发不仅耗时,还容易忽略边界场景(如按键消抖、多事件识别)。高效的开发方式是优先选用成熟的开源组件库

  • 成熟库经过多场景验证,稳定性更高,能减少重复造轮子的成本;
  • 通用性强的库往往适配多种硬件环境,便于移植和维护;
  • 可基于库的设计思想学习外设管理的核心逻辑(如状态机、事件驱动),提升自身设计能力。

本次学习的 easy_buttonGitHub 地址)是一款针对嵌入式按键管理的轻量库,解决了传统按键驱动在组合按键、资源占用、多事件识别等方面的不足,非常适合多按键场景(如键盘、控制面板)。

3.1.基于 easy_button 库的按键驱动代码解析

整体框架说明

该代码基于easy_button库实现了 6 个按键的管理,支持单击 / 双击控制 LED、组合键(复制 / 粘贴 / 剪切)功能。核心逻辑分为参数配置硬件适配事件处理初始化周期性扫描 五部分,充分复用了easy_button库的事件驱动机制。

核心代码解析

1. 按键参数配置(defaul_ebtn_param

cpp 复制代码
const ebtn_btn_param_t defaul_ebtn_param = EBTN_PARAMS_INIT(
    20,     // 按下消抖20ms
    20,     // 释放消抖20ms
    50,     // 单击最短按下50ms
    500,    // 单击最长按下500ms
    300,    // 多击间隔300ms
    500,    // 长按周期500ms
    5       // 最多支持5连击
);
  • 作用:定义按键检测的时间规则,是easy_button库识别事件的核心依据。
  • 关键参数说明:
    • 消抖时间(20ms):过滤按键机械抖动,确保状态稳定后才识别为有效状态。
    • 单击时间范围(50~500ms):按下到释放的时间在此区间内,才视为有效单击。
    • 多击间隔(300ms):两次点击的间隔小于 300ms,视为连击(如双击、三击)。
    • 长按周期(500ms):长按状态下,每 500ms 触发一次KEEPALIVE事件(本代码未使用)。

2. 按键 ID 与硬件映射(user_button_t

cpp 复制代码
typedef enum
{
    USER_BUTTON_0 = 0,  // 对应GPIOE.15
    USER_BUTTON_1,      // 对应GPIOE.13
    USER_BUTTON_2,      // 对应GPIOE.11
    USER_BUTTON_3,      // 对应GPIOE.9
    USER_BUTTON_4,      // 对应GPIOE.7
    USER_BUTTON_5,      // 对应GPIOB.0
    USER_BUTTON_MAX,    // 按键总数(6个)
} user_button_t;
  • 作用:将 6 个物理按键(对应不同 GPIO)抽象为唯一 ID,便于软件管理。
  • USER_BUTTON_MAX用于标记按键总数,方便后续数组大小计算(如btns数组、ucLed数组)。

3. 按键数组初始化(btns

cpp 复制代码
static ebtn_btn_t btns[] = 
{
    EBTN_BUTTON_INIT(USER_BUTTON_0, &defaul_ebtn_param),
    EBTN_BUTTON_INIT(USER_BUTTON_1, &defaul_ebtn_param),
    // ... 其余4个按键初始化
};
  • 作用:静态注册按键,将每个按键 ID 与参数配置关联,是easy_button库管理按键的基础数据结构。
  • EBTN_BUTTON_INIT宏:初始化按键结构体(包含 key_id、参数指针等),库通过该数组遍历所有按键。

4. 硬件状态读取函数(prv_btn_get_state

cpp 复制代码
uint8_t prv_btn_get_state(struct ebtn_btn *btn)
{
    switch (btn->key_id)
    {
    case USER_BUTTON_0:  // 按键0 → GPIOE.15
        return !HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_15);
    // ... 其余按键的GPIO读取
    default:
        return 0;
    }    
}
  • 作用:easy_button库的核心回调函数,用于读取按键的实时硬件状态,是软件与硬件的桥梁。
  • 逻辑说明:
    • 假设按键硬件为上拉输入(未按下时引脚为高电平,按下时为低电平)。
    • HAL_GPIO_ReadPin返回GPIO_PIN_SET(高电平,未按下)或GPIO_PIN_RESET(低电平,按下)。
    • 取反(!)后,返回1表示按键按下,0表示松开,符合库对 "活跃状态" 的定义。

5. 事件处理函数(prv_btn_event

该函数是按键逻辑的核心,处理easy_button库上报的事件(ONCLICKONPRESS等)。

(1)单击 / 双击控制 LED(EBTN_EVT_ONCLICK事件)
cpp 复制代码
if (evt == EBTN_EVT_ONCLICK)
{
    uint16_t click_cnt = ebtn_click_get_count(btn); // 获取连击次数
    switch (btn->key_id)
    {
    case USER_BUTTON_0:
        if (click_cnt == 1) ucLed[0] = 1;  // 单击:LED0亮
        else if (click_cnt == 2) ucLed[0] = 0; // 双击:LED0灭
        // ... 其余按键类似
    }
}
  • 逻辑:ONCLICK事件在按键释放且符合单击时间规则时触发,通过ebtn_click_get_count获取连击次数,分别控制 LED 的亮灭。
(2)组合键逻辑(EBTN_EVT_ONPRESS事件)
cpp 复制代码
if (evt == EBTN_EVT_ONPRESS)
{
    if (prv_btn_get_state(&btns[USER_BUTTON_0]) == 1) // 按键0按下时
    {
        switch (btn->key_id)
        {
        case USER_BUTTON_1: // 0+1:复制LED状态到缓存
            for (int i=0; i<USER_BUTTON_MAX; i++)
                ucLed_copy[i] = ucLed[i];
            break;
        case USER_BUTTON_2: // 0+2:粘贴缓存到LED
            for (int i=0; i<USER_BUTTON_MAX; i++)
                ucLed[i] = ucLed_copy[i];
            break;
        case USER_BUTTON_3: // 0+3:剪切(复制后清零LED)
            for (int i=0; i<USER_BUTTON_MAX; i++)
            {
                ucLed_copy[i] = ucLed[i];
                ucLed[i] = 0;
            }
            break;
        }
    }
}
  • 逻辑:ONPRESS事件在按键按下且消抖完成后触发。当按键 0 处于按下状态时,检测其他按键的按下事件,实现复制、粘贴、剪切功能。
  • 依赖:通过prv_btn_get_state实时检查按键 0 的状态,确保组合键的有效性。

6. 初始化与周期性扫描

(1)初始化函数(app_ebtn_init

cpp 复制代码
void app_ebtn_init(void)
{
    ebtn_init(btns, EBTN_ARRAY_SIZE(btns), NULL, 0, prv_btn_get_state, prv_btn_event);
}
  • 作用:初始化easy_button库,参数包括:
    • 静态按键数组(btns)及数量;
    • 组合按键数组(此处为NULL,未使用库自带的组合键功能,而是自定义实现);
    • 状态读取回调(prv_btn_get_state)和事件回调(prv_btn_event)。

(2)周期性扫描任务(btn_task

cpp 复制代码
void btn_task(void)
{
    ebtn_process(HAL_GetTick());
}
  • 作用:调用库的核心处理函数ebtn_process,传入系统时间(HAL_GetTick()获取 ms 级时间)。
  • 机制:ebtn_process会周期性扫描所有按键状态,与历史状态对比,根据参数配置判断并触发事件(如ONCLICKONPRESS),最终调用prv_btn_event处理。
  • 建议:任务周期一般为 5~20ms(与消抖时间匹配),确保状态检测及时。

核心设计思想

  1. 硬件与软件解耦 :通过prv_btn_get_state隔离硬件 GPIO 操作,库只关心 "1(按下)/0(松开)" 的逻辑状态,便于移植到不同硬件。
  2. 事件驱动 :基于库的事件机制(ONCLICKONPRESS等),上层逻辑无需关心扫描细节,只需响应事件,简化代码。
  3. 灵活复用 :通过参数配置(defaul_ebtn_param)调整按键特性,无需修改核心逻辑;组合键功能通过自定义判断实现,比库自带组合键更灵活。

ebtn 库使用总结

  • 🧩定义参数与按键: 使用宏精心配置按键行为 (时间、ID)。
  • 📞提供回调函数: 实现 `get_state_fn` (读硬件) 和 `evt_fn` (处理事件)。
  • 🚀初始化: 调用 `ebtn_init` 注册按键和回调,并配置组合键。
  • ⏱️周期处理: 必须 在定时器中断 (最佳) 或主循环中,以固定间隔 (如 5-20ms) 调用 `ebtn_process(HAL_GetTick())`。
  • 🎉处理事件: 在 `evt_fn` 中根据 `key_id` 和 `evt` 类型编写简洁的事件响应代码,避免阻塞。

遵循这五个步骤,你就能充分利用 ebtn 库,告别繁琐的按键处理逻辑,专注于实现更有趣的应用功能!

相关推荐
huangjiazhi_2 小时前
arduino uno单片机+AM2032 DHT22 Sensor温湿度开发
单片机·嵌入式硬件
找方案2 小时前
all-in-rag 学习笔记:索引构建与优化 —— 解锁 RAG 高效检索的核心密码
人工智能·笔记·学习·all-in-rag
XFF不秃头2 小时前
力扣刷题笔记-和为 K 的子数组
c++·笔记·算法·leetcode
lpfasd1232 小时前
《魔力创业》精读笔记:从理念到中国实践的落地指南
笔记
福尔摩斯张2 小时前
嵌入式硬件篇:常见单片机型号深度解析与技术选型指南
网络·数据库·stm32·单片机·网络协议·tcp/ip·mongodb
h7997102 小时前
mysql 查询语句解析笔记(按执行顺序理解)
数据库·笔记·mysql
WarPigs2 小时前
Unity NetCode for GameObject笔记
笔记·unity·游戏引擎
颜大哦2 小时前
大模型学习笔记
笔记·学习
lpfasd1232 小时前
《复利效应》精读笔记
笔记