最近在参加论坛的活动,需要外部中断按键实现一些功能,而ST的开发板上面只有一个按键,这里和大家分享一种外键按键:按下,抬起和常按的三种不同状态的处理方法。
初学按键检测时候,一般就是读取IO口的状态,然后使用系统的延时函数,再次判断IO口的状态,当两次状态一致时候,认为是有效触发,然后程序去执行按键处理函数。这种方法的局限性只能是实现一种按键状态的检测:比如是按键按下的状态,放到主函数中,由于延时函数的存在,很容易导致程序在这里死等,造成程序阻塞,造成其他模块运行不正常,如有交互式的模块急需处理或者是正在处理,很容易导致程序的其他模块运行不正常。
下面和大家分享一下,之前用到的一种检测按键的方法:
主要是依赖于STM32的基本定时器完成按键的按下、抬起和常按的功能;
优点:软件去抖的时间可调,长按时间可调,基本没有延时,无阻塞,可实现稳定触发;
而且,代码移植方便,只需要修改下底层的读取按键状态函数就可以,配置IO口为输入模式即可。
按键代码如下所示:
/**************************************************************************************
KeyBoard.c 按键处理部分
zhaoruicong
2025年5月31日
**************************************************************************************/
#include "define.h"
#include "global.h"
#include "keyboard.h"
#include "stdio.h"
#include "string.h"
#include "stdint.h"
void DealKey(uint8_t KeyValue);
static BUTTON_T s_KEY1; /* KEY1 键 */
static KEY_FIFO_T s_Key; /* 按键FIFO变量,结构体 */
/*
定义12个函数,判断按键是否按下,返回值1 表示按下,0表示未按下
*/
static uint8_t IsKey1(void) {if (HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_13) == GPIO_PIN_SET) return 0; return 1;}
/*******************************************************************************
函数名: InitButtonVar
输 入:
输 出:
功能说明:初始化所有的按键变量,这个函数需要在systic中断启动钱调用1次
*/
void InitButtonVar(void)
{
/* 对按键FIFO读写指针清零 */
s_Key.Read = 0;
s_Key.Write = 0;
/* 初始化Key1按键变量 */
s_KEY1.IsKeyDownFunc = IsKey1; /* 判断按键按下的函数 */
s_KEY1.FilterTime = BUTTON_FILTER_TIME; /* 按键滤波时间 */
s_KEY1.LongTime = BUTTON_LONG_TIME; /* 长按时间 */
s_KEY1.Count = s_KEY1.FilterTime / 2; /* 计数器设置为滤波时间的一半 */
s_KEY1.State = 0; /* 按键缺省状态,0为未按下 */
s_KEY1.KeyCodeDown = KEY1_DOWN; /* 按键按下的键值代码 */
s_KEY1.KeyCodeUp = KEY1_UP; /* 按键弹起的键值代码 */
s_KEY1.KeyCodeLong = KEY1_LONG_state; /* 按键被持续按下的键值代码 */
}
/*******************************************************************************
函数名: PutKey
输 入: 键值
输 出:
功能说明:将1个键值压入按键FIFO缓冲区
*/
void PutKey(uint8_t _KeyCode)
{
s_Key.Buf[s_Key.Write] = _KeyCode;
if (++s_Key.Write >= KEY_FIFO_SIZE)
{
s_Key.Write = 0;
}
}
/*******************************************************************************
函数名: GetKey
输 入:
输 出: 返回键值, KEY_NONE ( = 0) 表示无键按下
功能说明:从按键FIFO取1个键值
*/
uint8_t GetKey(void)
{
uint8_t ret;
if (s_Key.Read == s_Key.Write)
{
return KEY_NONE;
}
else
{
ret = s_Key.Buf[s_Key.Read];
if (++s_Key.Read >= KEY_FIFO_SIZE)
{
s_Key.Read = 0;
}
return ret;
}
}
/*******************************************************************************
函数名:DetectButton
输 入: 按键结构变量指针
输 出:
功能说明:检测指定的按键
*/
static void DetectButton(BUTTON_T *_pBtn)
{
/* 如果没有初始化按键函数,则报错
if (_pBtn->IsKeyDownFunc == 0)
{
printf("Fault : DetectButton(), _pBtn->IsKeyDownFunc undefine");
}
*/
if (_pBtn->IsKeyDownFunc())
{
if (_pBtn->Count < _pBtn->FilterTime)
{
_pBtn->Count = _pBtn->FilterTime;
}
else if(_pBtn->Count < 2 * _pBtn->FilterTime)
{
_pBtn->Count++;
}
else
{
if (_pBtn->State == 0)
{
_pBtn->State = 1;
/* 发送按钮按下的消息 */
if (_pBtn->KeyCodeDown > 0)
{
/* 键值放入按键FIFO */
PutKey(_pBtn->KeyCodeDown);
}
}
if (_pBtn->LongTime > 0)
{
if (_pBtn->LongCount < _pBtn->LongTime)
{
/* 发送按钮持续按下的消息 */
if (++_pBtn->LongCount == _pBtn->LongTime)
{
/* 键值放入按键FIFO */
PutKey(_pBtn->KeyCodeLong);
}
}
}
}
}
else
{
if(_pBtn->Count > _pBtn->FilterTime)
{
_pBtn->Count = _pBtn->FilterTime;
}
else if(_pBtn->Count != 0)
{
_pBtn->Count--;
}
else
{
if (_pBtn->State == 1)
{
_pBtn->State = 0;
/* 发送按钮弹起的消息 */
if (_pBtn->KeyCodeUp > 0)
{
/* 键值放入按键FIFO */
PutKey(_pBtn->KeyCodeUp);
}
}
}
_pBtn->LongCount = 0;
}
}
/*******************************************************************************
函数名:KeyPro
输 入:
输 出:
功能说明:检测所有的按键,这个函数要被systic的中断服务程序调用
*/
void KeyPro(void)
{
DetectButton(&s_KEY1); /* KEY1 键 */
}
/*
功能函数:键盘扫描
*/
void ScanKey(void)
{
ucKeyValue = GetKey();
if(ucKeyValue == 0) return;
switch(ucKeyValue)
{
case KEY1_DOWN: ucKeyValue = 1;
printf(" 按键按下 \r\n");
break;
case KEY1_UP: ucKeyValue = 2;
printf(" 按键弹起 \r\n");
break;
case KEY1_LONG_state: ucKeyValue =3;
printf(" 按键常按 \r\n");
break;
default : ucKeyValue = 0 ;
break;
}
//根据键值,进入不同的按键处理函数,按键弹起的时候不会响应蜂鸣器操作
if(ucKeyValue)
{
DealKey(ucKeyValue);
}
ucKeyValue = 0 ;
}
/*****************************************************************************
键盘处理函数
*****************************************************************************/
void DealKey(uint8_t KeyValue)
{
if(!KeyValue) return;
switch(KeyValue)
{
case 1:
break ;
default:break;
}
KeyValue = 0;
}
/**
* @brief 按键初始化
* @param 无
* @retval 无
*/
void Key_Init(void)
{
InitButtonVar();//按键初始化
}keyboard.h如下所示:
#ifndef __keyBoard_H
#define __keyBoard_H
#include "define.h"
#include "global.h"
//#include "global_def.h"
/* 按键滤波时间50ms, 单位10ms
只有连续检测到50ms状态不变才认为有效,包括弹起和按下两种事件
*/
#define BUTTON_FILTER_TIME 20 // 单位 1ms
#define BUTTON_LONG_TIME 2000 /* 持续1秒,认为长按事件 */
/*
每个按键对应1个全局的结构体变量。
其成员变量是实现滤波和多种按键状态所必须的
*/
typedef struct
{
/* 下面是一个函数指针,指向判断按键手否按下的函数 */
uint8_t (*IsKeyDownFunc)(void); /* 按键按下的判断函数,1表示按下 */
uint8_t Count; /* 滤波器计数器 */
uint8_t FilterTime; /* 滤波时间(最大255,表示2550ms) */
uint16_t LongCount; /* 长按计数器 */
uint16_t LongTime; /* 按键按下持续时间, 0表示不检测长按 */
uint8_t State; /* 按键当前状态(按下还是弹起) */
uint8_t KeyCodeUp; /* 按键弹起的键值代码, 0表示不检测按键弹起 */
uint8_t KeyCodeDown; /* 按键按下的键值代码, 0表示不检测按键按下 */
uint8_t KeyCodeLong; /* 按键长按的键值代码, 0表示不检测长按 */
}BUTTON_T;
/* 定义键值代码
推荐使用enum, 不用#define,原因:
(1) 便于新增键值,方便调整顺序,使代码看起来舒服点
(2) 编译器可帮我们避免键值重复。
*/
typedef enum
{
KEY_NONE = 0, /* 0 表示按键事件 */
/* 为了演示,需要检测USER键弹起事件和长按事件 */
KEY1_DOWN, /* KEY1键按下 */
KEY1_UP, /* KEY1键弹起 */
KEY1_LONG_state, //KEY1键 常按
}KEY_ENUM;
/*
按键FIFO用到变量
*/
#define KEY_FIFO_SIZE 30
typedef struct
{
uint8_t Buf[KEY_FIFO_SIZE]; /* 键值缓冲区 */
uint8_t Read; /* 缓冲区读指针 */
uint8_t Write; /* 缓冲区写指针 */
}KEY_FIFO_T;
void InitButtonVar(void);
void PutKey(uint8_t _KeyCode);
uint8_t GetKey(void);
void KeyPro(void);
extern void ScanKey(void);
extern void DealKey(uint8_t KeyValue);
void Key_Init(void);
#endif注意:在定时器中,实现对按键的扫描:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
/* USER CODE BEGIN Callback 0 */
/* USER CODE END Callback 0 */
/* USER CODE BEGIN Callback 1 */
if (htim->Instance == TIM10) {
Time10point++;
KeyPro();
if(Time10point >=1000)
{
Time10point = 0 ;
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
}
}
}在主程序中,实现按键的处理:
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
ScanKey();
}验证图如下所示:

使用方法:
1:按下是最基本的触发方式,弊端就是当常按按键时,按键按下的状态也会触发。
2:可以keyboard中设置去抖时间,和常按的时间,需要下定时器的中断时间即可。
3:需要在定时器中执行扫描按键函数,然后再主程序中执行键值处理函数。
4:亲测代码功能正常,可实现几种不同的按键状态。
我要赚赏金
