嵌入式开发实战| Key模块深度解析
本节为按键模块的学习,包含详细知识点和代码。上拉:默认电平状态为高电平,按下时引脚拉向低电平。(常用)下拉:默认电平状态为低电平,按下时引脚拉向高电平。浮空:无默认电平,易受干扰,一般不直接用于按键。(特定场景需要引脚呈现高阻态时使用)总的来说,正确选择和配置输入模式是按键能够被可靠读取的前提。对于大多数简单的按键电路,使用单片机内部的上拉或下拉功能是最方便有效的方式。上拉电阻究竟有什么作用呢?首
西门子–嵌入式赛道系列文章目录
第一章 任务调度器从原理到代码实现
第二章 GPIO与LED模块全解析:从原理到实战
插叙 GPIO知识点补充
第三章 Key模块深度解析
文章目录
摘要
本节为按键模块的学习,包含详细知识点和代码。
一、单片机GPIO引脚知识点补充
1. 介绍
GPIO引脚作为输入时,有三种基础模式:
- 上拉:默认电平状态为高电平,按下时引脚拉向低电平。(常用)
- 下拉:默认电平状态为低电平,按下时引脚拉向高电平。
- 浮空:无默认电平,易受干扰,一般不直接用于按键。(特定场景需要引脚呈现高阻态时使用)
总的来说,正确选择和配置输入模式是按键能够被可靠读取的前提。对于大多数简单的按键电路,使用单片机内部的上拉或下拉功能是最方便有效的方式。
2. 对比
思考问题:为什么在使用上拉电阻的时候会带着一个电容呢?
而在蓝桥杯单片机的那块板子原理图却没有使用呢?
其实原因是在于,加入滤波电容之后可以在硬件层做到一定的消抖处理,确保按键检测更加精准。正是因为蓝桥杯单片机并没有滤波电容,所以我们需要在软件层对按键进行消抖延时处理!
3.对上拉电阻的详细介绍
思考问题:上拉电阻究竟有什么作用呢?
首先,上拉电阻可以确保没有外部信号的时候默认处于高电平,对于稳定读取按键至关重要。
其次,上拉电阻可以提高驱动能力。
当输出为高电平时,P-MOS管导通,N-MOS管截止,内部电阻和外部上拉电阻并联相当于阻值更小,则与外部电路连接时分压更小,故驱动能力会更强,具体可参考视频:
爱上半导体
关于MOS管的描述在上一节可直接跳转
二、按键函数编写操作指南(简单版)
1.配置MX

2.改为上拉电阻模式

3.代码示例
- 添加Key_app.c和Key_app.h
#include "key_app.h"
uint8_t key_val,key_down,key_up,key_old;
uint8_t key_read()
{
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;
}
void key_task(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;
if(key_down == 2)
{
ucLed[1] ^= 1;
}
}
#ifndef __KEY_APP_H__
#define __KEY_APP_H__
#include "MyDefine.h"
void key_task(void);
#endif
- 在MyDefine.h中调用
#include "main.h"
#include "scheduler.h"
#include "led_app.h"
#include "key_app.h"
extern uint8_t ucLed[6];
- 调度器中配置定时
static task_t scheduler_task[] =
{
{led_task, 1, 0},
{key_task, 10, 0},
};
- main.c中
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
led_task(); //点亮Led灯
key_task(); //按键使能
}
三、按键函数编写操作指南(高级版)
(1)前言
简单版的代码虽然简洁但是无法高效区分单击、双击、长按等常用操作,接下来的高级版可以高效处理不止这些问题。
(2)介绍
接下来提供的’ebtn库’是让你只需关注按键"事件"本身,而把底层的抖动处理、状态判断、时间计算等繁琐细节交给它来完成。以下是其的核心理念:
-
事件驱动:得到指令后执行,避免一直循环执行。
-
状态机:内部也为每个按键维护了一个"状态机"。这个状态机记录了按键当前处于哪个阶段
-
空闲状态: 按键没被按下。
-
抖动检测状态: 刚检测到按下信号,需要等待一小段时间(去抖时间)确认不是干扰。
-
按下状态(Pressed): 确认是有效按下,并且持续按着。
-
单击判断状态:按下后很快松开了,正在等待看是否会有下一次点击(用于判断连击。
-
释放状态 (Released):确认有效松开。
-
回调函数:提供两个联系方式给ebtn库。
(3)使用详解
- 在github官网中下载相关按键框架代码(需要的私我,这里就不一一演示了)
- 为了方便管理,将这些需要移植的代码统一放在一个另外的文件夹中

- 在MyApp中新建btn_app.c和btn_app.h作为按键框架的应用层,用于储存自己编写的逻辑代码

- 结合用户使用手册,可以编写如下代码
#include "btn_app.h"
#include "ebtn.h"
const ebtn_btn_param_t defaul_ebtn_param = EBTN_PARAMS_INIT(
20, // time_debounce: 按下稳定 20ms
20, // time_debounce_release: 释放稳定 20ms
50, // time_click_pressed_min: 最短单击按下 50ms
500, // time_click_pressed_max: 最长单击按下 500ms (超过则不算单击)
300, // time_click_multi_max: 多次单击最大间隔 300ms (两次点击间隔超过则重新计数)
500, // time_keepalive_period: 长按事件周期 500ms (按下超过 500ms 后,每 500ms 触发一次)
5 // max_consecutive: 最多支持 5 连击
);
typedef enum
{
USER_BUTTON_0 = 1,
USER_BUTTON_1,
USER_BUTTON_2,
USER_BUTTON_3,
USER_BUTTON_MAX,
} user_button_t;
static ebtn_btn_t btns[] =
{
EBTN_BUTTON_INIT(USER_BUTTON_0, &defaul_ebtn_param),
EBTN_BUTTON_INIT(USER_BUTTON_1, &defaul_ebtn_param),
EBTN_BUTTON_INIT(USER_BUTTON_2, &defaul_ebtn_param),
EBTN_BUTTON_INIT(USER_BUTTON_3, &defaul_ebtn_param),
};
uint8_t prv_btn_get_state(struct ebtn_btn *btn)
{
switch (btn->key_id)
{
case USER_BUTTON_0:
return (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET);
break;
case USER_BUTTON_1:
return (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET);
break;
case USER_BUTTON_2:
return (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2) == GPIO_PIN_RESET);
break;
case USER_BUTTON_3:
return (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET);
break;
default:
// 对于库内部处理组合键等情况,或者未知的 key_id,安全起见返回 0 (未按下)
return 0;
}
}
void prv_btn_event(struct ebtn_btn *btn, ebtn_evt_t evt)
{
uint16_t key_id = btn->key_id; // 获取触发事件的按键 ID
uint16_t click_cnt = ebtn_click_get_count(btn); // 获取连击次数 (仅在 ONCLICK 事件时有意义)
// uint16_t kalive_cnt = ebtn_keepalive_get_count(btn); // 获取长按计数 (仅在 KEEPALIVE 事件时有意义)
// 调试打印 (可选)
// printf("Key ID: %d, Event: %d", key_id, evt);
// 根据事件类型进行处理
switch (evt)
{
case EBTN_EVT_ONPRESS: // 按下事件 (消抖成功后触发一次)
// printf(" - Pressed\n");
// 可以在这里处理按下即触发的操作,比如点亮提示灯
if (key_id == 1) { /* Do something for KEY1 press */ }
break;
case EBTN_EVT_ONRELEASE: // 释放事件 (消抖成功后触发一次)
// printf(" - Released\n");
// 可以在这里处理释放时触发的操作
if (key_id == 1)
{
/* Do something for KEY1 release */
}
break;
case EBTN_EVT_ONCLICK: // 单击/连击事件 (在释放后,或达到最大连击数,或超时后触发)
// printf(" - Clicked (%d times)\n", click_cnt);
// --- 根据 key_id 和 click_cnt 执行不同操作 ---
if (key_id == 1)
{ // 如果是 KEY1 触发的 CLICK
if (click_cnt == 1)
{
ucLed[0] = 1; //单击点亮
}
else if (click_cnt == 2)
{
ucLed[0] = 0; //双击熄灭
}
// ... 可以继续判断 3击, 4击 ...
}
else if (key_id == 2)
{ // 如果是 KEY2 触发的 CLICK
if (click_cnt == 1)
{
// KEY2 单击
// printf(" Action: KEY2 Single Click - Toggle LED2\n");
}
}
// else if (key_id == 101) { // 如果是组合键 (KEY1+KEY2) 触发的 CLICK
// if (click_cnt == 1)
// {
// // 组合键单击
// // printf(" Action: Combo Key 101 Single Click - Reset System\n");
// }
// }
// break;
// case EBTN_EVT_KEEPALIVE: // 保持活动/长按事件 (按下持续时间超过阈值后,按周期触发)
// // printf(" - Keep Alive (Long Press, Count: %d)\n", kalive_cnt);
// if (key_id == 1) {
// // KEY1 长按
// // printf(" Action: KEY1 Long Press - Increase Volume\n");
// }
// break;
default: // 未知事件 (理论上不应发生)
// printf(" - Unknown Event\n");
break;
}
}
void app_ebtn_init(void)
{
ebtn_init(btns, EBTN_ARRAY_SIZE(btns), NULL, 0,prv_btn_get_state, prv_btn_event);
}
void btn_task(void)
{
ebtn_process(HAL_GetTick());
}
#ifndef __BTN_APP_H__
#define __BTN_APP_H__
#include "MyDefine.h"
void app_ebtn_init(void);
void btn_task(void);
#endif
4.注意事项
如果出现找不到文件所在位置时需要手动添加,保证文件路径的最里层文件夹添加进去了,可参考如下步骤:



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

所有评论(0)