最近在参加论坛的活动,需要外部中断按键实现一些功能,而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:亲测代码功能正常,可实现几种不同的按键状态。