按键去抖动是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;
}

从技术层面讲,这代码当然没有任何问题,能做到去抖动。但是这只是解释原理的代码,一来它的交互场景未必和我们的设计需求相同,二来它不是工程性的代码。而且,这里的不少变量,如lastDebounceTimebuttonState都是全局变量,是工程代码大忌。

先看需求。仔细分析代码发现,这段代码实现的是检测按钮按下或抬起,能给出按钮按下或抬起的稳定的状态。而我们的设计需要的是检测按钮被按下了,也就是按下后抬起了,在抬起的瞬间给出结论。所以,首先要对代码做技术改造,从而形成如下的第一个版本:

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还有大量的机会去做其他任务的工作。在没有多任务操作系统的环境下,这样的设计是非常良好的。