这两天在用ESP32开发一个小东西,其中用到了几个按键的单击和长按,说起来按键的检测其实比较简单,无非就是按下和释放,但是在硬件里面有一个抖动干扰问题,当我们按下某个按键时通常会得到很多次按下和释放的状态,朱镕基需要我们对按键做过滤,什么情况下是按下,什么情况下是干扰导致,一般常规的做法都是在主循环里面线检测按键状态是否按下,然后用delay()函数延时等待一段时间后再次判断按键的状态是否任然保持按下状态,如果任然按下,就认为是一次有效的按键按下事件,否则视为干扰,代码如下:
const int buttonPin = 2; // the number of the pushbutton pin
void setup() {
// initialize the pushbutton pin as an input:
pinMode(buttonPin, INPUT);
digitalWrite(buttonPin, HIGH);
}
void loop() {
// read the state of the pushbutton value:
if( digitalRead(buttonPin)==LOW){
delay(50);
if(digitalRead(buttonPin)==LOW){
//按键按下
}
}
}
这种写法有一个问题,对于要求效率的系统来说,当我们运行delay();函数的时候,整个主循环会被阻塞一段时间,导致后面的事情无法继续完成,那么有什么办法能够即能不阻塞,还能延时消抖判断呢,其实我们可用用系统自带的millis()函数来计时,在一段时间内都一直保持按下,己是我有效的按下。下面我通过这个逻辑简单的写了一个按钮单击和长按的c++类,源码如下:
//UserButton.h头文件
#ifndef UserButton_h
#define UserButton_h
#include "Arduino.h"
extern "C" {
typedef void (*PressCallback)();
}
class UserButton {
public:
/**构造函数*/
UserButton();
/**
* @brief 初始化UserButton库。
* @param pin 用于瞬时按钮输入的引脚。
* @param PressLow 当按下按钮时输入电平为LOW时设置为true,Default为true。
* @param pullup 内部上拉。默认值为true。
*/
explicit UserButton(const int pin, const bool PressLow=true, const bool pullup=true);
/**
* @brief 设置按钮单击回调函数
* @param callback 回调函数
*/
void setClickListen(PressCallback callback);
/**
* @brief 设置按钮长按回调函数
* @param callback 回调函数
*/
void setLongClickListen(PressCallback callback);
/**
* @brief 设置按钮长按释放回调函数
* @param callback 回调函数
*/
void setUnLongClickListen(PressCallback callback);
/**
* @brief 按键扫描(建议放在loop()主循环函数中调用)
*/
void buttonScan();
/**
* @brief 长按触发的持续时间
* @param duration 持续时长
*/
void setLongClickTime(uint16_t duration);
/**
* @brief 消抖时间设置
* @param duration 间隔
*/
void debounce(uint16_t interval);
private:
/**
* @brief 按钮绑定的引脚
*/
int _pin = -1;
/**
* @brief 按下标志位
*/
bool _KeyPress = false;
/**
* @brief 长按标志位
*/
bool _keyLongPress = false;
/**
* @brief 这是按下按钮时输入引脚的电平。
* LOW 如果按下按钮将输入引脚连接到GND。
* HIGH 如果按钮在按下时将输入引脚连接到VCC。
*/
uint8_t _pressed = LOW;
/**
* @brief 上次按键状态标志位。0 LOW,1 HIGH
*/
uint8_t _lastKeyState = HIGH;
/**
* @brief 当前按钮状态标志位 0 LOW,1 HIGH
*/
uint8_t _currentKeyState = HIGH;
/**
* @brief 上次按下的时间
*/
unsigned long _lastTime = 0;
/**
* @brief 按钮防抖延迟
*/
uint16_t DEBOUNCE_DELAY = 50;
/**
* @brief 长按等待时间
*/
uint16_t LONG_DURATION_DELAY = 1000;
/**
* @brief 单击回调
*/
PressCallback _callback = NULL;
/**
* @brief 长按回调
*/
PressCallback _longPressCallback = NULL;
/**
* @brief 长按释放回调
*/
PressCallback _unLongPressCallback = NULL;
};
#endif
//UserButton.cpp住文件
#include "UserButton.h"
UserButton::UserButton() {
_pin = -1;
}
/**
* @brief 初始化UserButton库。
* @param pin 用于瞬时按钮输入的引脚。
* @param PressLow 当按下按钮时输入电平为LOW时设置为true,Default为true。
* @param pullup 内部上拉。默认值为true。
*/
UserButton::UserButton(const int pin, const bool PressLow, const bool pullup) {
_pin = pin;
_pressed = PressLow ? LOW : HIGH;
pinMode(pin, pullup ? INPUT_PULLUP : INPUT);
_lastKeyState = PressLow ? HIGH : LOW;
_currentKeyState = _lastKeyState;
digitalWrite(pin, _lastKeyState);
}
/**
* @brief 设置按钮单击回调函数
* @param callback 回调函数
*/
void UserButton::setClickListen(PressCallback callback) {
_callback = callback;
}
/**
* @brief 设置按钮长按回调函数
* @param callback 回调函数
*/
void UserButton::setLongClickListen(PressCallback callback) {
_longPressCallback = callback;
}
/**
* @brief 设置按钮长按释放回调函数
* @param callback 回调函数
*/
void UserButton::setUnLongClickListen(PressCallback callback) {
_unLongPressCallback = callback;
}
/**
* @brief 长按触发的持续时间
* @param duration 持续时长
*/
void UserButton::setLongClickTime(uint16_t duration) {
LONG_DURATION_DELAY = duration;
}
/**
* @brief 消抖时间设置
* @param duration 间隔
*/
void UserButton::debounce(uint16_t interval) {
DEBOUNCE_DELAY = interval;
}
/**
* @brief 按键扫描(建议放在loop()主循环函数中调用)
*/
void UserButton::buttonScan() {
uint32_t elapsed = 0;
uint32_t millisecond = 0;
uint8_t btnState = digitalRead(_pin); //当前按键状态
//从0bit位读取上次的BUTTON_BUSY按键状态与当前读取的状态进行比较
if (btnState != _lastKeyState) {
_lastTime = millis(); //保存当前BUTTON_BUSY按下的时间轴
}
//这种写法解决开机时间计数器溢出归零后重新计数出现的提前触发事件的偏差
millisecond = millis();
if (millisecond < _lastTime) { //如果当前毫秒数小于开始计时的毫秒数。说明毫秒计数器已经溢出从新开始
Serial.printf("计时器溢出,从新计时: %ul\n", millisecond);
elapsed = ((uint32_t)-1) - _lastTime + millisecond; //用毫秒计数器最大值-开始时的计数值+当前过去的毫秒值
} else {
elapsed = millisecond - _lastTime;
}
if (elapsed >= DEBOUNCE_DELAY) { // 比较持续时间
if (btnState != _currentKeyState) {
_currentKeyState = btnState; //写入状态到0比特位
if (_currentKeyState == _pressed) { //按下状态
_KeyPress = true; //写入第n位按下标志位
}
}
}
//----------------------------------------------------/
if (_KeyPress == true) { //开关灯按钮按下
if (btnState == (_pressed == LOW ? HIGH : LOW)) { //按钮释放
if (_keyLongPress == true) {
Serial.printf("%d按钮长按释放",_pin);
if (_unLongPressCallback != NULL) {
_unLongPressCallback();
}
} else {
Serial.printf("%d按钮短按释放",_pin);
if (_callback != NULL) {
_callback();
}
}
_KeyPress = false;
_keyLongPress = _KeyPress;
_currentKeyState = btnState; //写入状态到0比特位
_lastTime = 0; //清除时间轴
} else {
//---------------------替代写法----------------------//
//这种写法解决开机时间计数器溢出归零后重新计数出现的提前触发事件的偏差
millisecond = millis();
if (millisecond < _lastTime) { //如果当前毫秒数小于开始计时的毫秒数。说明毫秒计数器已经溢出从新开始
Serial.printf("计时器溢出,从新计时: %ul\n", millisecond);
elapsed = ((uint32_t)-1) - _lastTime + millisecond; //用毫秒计数器最大值-开始时的计数值+当前过去的毫秒值
} else {
elapsed = millisecond - _lastTime;
}
if (elapsed >= LONG_DURATION_DELAY) { // 比较持续时间
if (_keyLongPress == false) {
_keyLongPress = true; //修改长按状态标志位为1
Serial.printf("%d按钮长按中...",_pin);
if (_longPressCallback != NULL) {
_longPressCallback();
}
}
}
}
}
_lastKeyState = btnState; //将当前按键状态保存
}