西门子–嵌入式赛道系列文章目录

第一章 任务调度器从原理到代码实现
第二章 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.注意事项

如果出现找不到文件所在位置时需要手动添加,保证文件路径的最里层文件夹添加进去了,可参考如下步骤:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Logo

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

更多推荐