按键去抖动是Arduino程序的常见小问题。在Arduino.cc直接就有相关的教程(https://www.arduino.cc/en/Tutorial/Debounce),可以搜到下面的代码。这段代码也出现在Arduino IDE的例子代码里,菜单选择:文件-->示例-->02.Digital-->Debounce就有了。
void loop() {
// read the state of the switch into a local variable:
int reading = digitalRead(buttonPin);
// check to see if you just pressed the button
// (i.e. the input went from LOW to HIGH), and you've waited long enough
// since the last press to ignore any noise:
// If the switch changed, due to noise or pressing:
if (reading != lastButtonState) {
// reset the debouncing timer
lastDebounceTime = millis();
}
if ((millis() - lastDebounceTime) > debounceDelay) {
// whatever the reading is at, it's been there for longer than the debounce
// delay, so take it as the actual current state:
// if the button state has changed:
if (reading != buttonState) {
buttonState = reading;
// only toggle the LED if the new button state is HIGH
if (buttonState == HIGH) {
ledState = !ledState;
}
}
}
// set the LED:
digitalWrite(ledPin, ledState);
// save the reading. Next time through the loop, it'll be the lastButtonState:
lastButtonState = reading;
}
从技术层面讲,这代码当然没有任何问题,能做到去抖动。但是这只是解释原理的代码,一来它的交互场景未必和我们的设计需求相同,二来它不是工程性的代码。而且,这里的不少变量,如lastDebounceTime
、buttonState
都是全局变量,是工程代码大忌。
先看需求。仔细分析代码发现,这段代码实现的是检测按钮按下或抬起,能给出按钮按下或抬起的稳定的状态。而我们的设计需要的是检测按钮被按下了,也就是按下后抬起了,在抬起的瞬间给出结论。所以,首先要对代码做技术改造,从而形成如下的第一个版本:
boolean isKeyPressed()
{
static int btnState = HIGH;
static unsigned long lastDebounceTime = 0; // the last time the button pin was toggled
static boolean isValid = false;
static const unsigned long debounceDelay = 50;
int reading = digitalRead(btnMode);
int ret = false;
// If the switch changed, due to noise or pressing:
if ( reading != btnState ) {
lastDebounceTime = millis();
if ( isValid ) {
// was valid and released now
ret = true;
}
isValid = false;
btnState = reading;
}
if ( reading == LOW && (millis() - lastDebounceTime) > debounceDelay ) {
// been pressed longer enough
isValid = true;
}
return ret;
}
这里btnMode是按钮的引脚编号。原始的代码是无论按下还是抬起均要改变状态,而这个版本是只在按下时记下状态isValid
,然后在抬起时返回true
。其实,这里把这段代码从loop()
中提取出来,成为一个函数,并且把相关的持久存储的全局变量放进函数成为静态本地变量,这已经是向着工程化走了一步了。
这段代码技术验证完成后,随即就遇到了一个工程问题:设计中有两个按钮,都需要用这个算法来去抖动,但是代码中的引脚编号、表示状态的三个静态本地变量都是为一个按钮硬编码的,因此还需要进步做工程化改造,成为下面这样的代码:
class Button {
public:
Button(int pin, boolean pullup=true):btnPin(pin),btnState(pullup?HIGH:LOW),pressedValue(pullup?LOW:HIGH) {
pinMode(pin, pullup?INPUT_PULLUP:INPUT);
}
boolean isPressed() {
int reading = digitalRead(btnPin);
int ret = false;
// If the switch changed, due to noise or pressing:
if ( reading != btnState ) {
lastDebounceTime = millis();
if ( isValid ) {
// was valid and released now
ret = true;
}
isValid = false;
btnState = reading;
}
if ( reading == pressedValue && (millis() - lastDebounceTime) > debounceDelay ) {
// been pressed longer enough
isValid = true;
}
return ret;
}
private:
int btnPin;
int btnState;
int pressedValue;
unsigned long lastDebounceTime = 0;
boolean isValid = false;
const unsigned long debounceDelay = 50;
};
这样把按钮做成了一个类,把检测过程中的状态变量用成员变量的方式来做持久存储,多个按钮的数据就这样简单地隔开了,而且检测函数成了成员函数,用起来也很方便。由于已经是一个类了,再要进一步做成库也是轻而易举的事情了。
顺便说,这个检测算法是一个对其他任务友好的非占有型计算模型。每次调用检测函数,都不会长期占用CPU,几乎都是瞬间返回。算法本身所需的“延时”操作,是由外部大循环来实现的。在不断轮询检测按钮是否按下的间隙,CPU还有大量的机会去做其他任务的工作。在没有多任务操作系统的环境下,这样的设计是非常良好的。