State Change Detection (Edge Detection) for pushbuttons

This thread is about the built-in example "State Change Detection (Edge Detection) for pushbuttons" tutorial and the diagram and StateChangeDetection.ino code it offers:

/*
  State change detection (edge detection)

  Often, you don't need to know the state of a digital input all the time, but
  you just need to know when the input changes from one state to another.
  For example, you want to know when a button goes from OFF to ON. This is called
  state change detection, or edge detection.

  This example shows how to detect when a button or button changes from off to on
  and on to off.

  The circuit:
  - pushbutton attached to pin 2 from +5V
  - 10 kilohm resistor attached to pin 2 from ground
  - LED attached from pin 13 to ground through 220 ohm resistor (or use the
    built-in LED on most Arduino boards)

  created  27 Sep 2005
  modified 30 Aug 2011
  by Tom Igoe

  This example code is in the public domain.

  https://docs.arduino.cc/built-in-examples/digital/StateChangeDetection/
*/

// this constant won't change:
const int buttonPin = 2;  // the pin that the pushbutton is attached to
const int ledPin = 13;    // the pin that the LED is attached to

// Variables will change:
int buttonPushCounter = 0;  // counter for the number of button presses
int buttonState = 0;        // current state of the button
int lastButtonState = 0;    // previous state of the button

void setup() {
  // initialize the button pin as a input:
  pinMode(buttonPin, INPUT);
  // initialize the LED as an output:
  pinMode(ledPin, OUTPUT);
  // initialize serial communication:
  Serial.begin(9600);
}


void loop() {
  // read the pushbutton input pin:
  buttonState = digitalRead(buttonPin);

  // compare the buttonState to its previous state
  if (buttonState != lastButtonState) {
    // if the state has changed, increment the counter
    if (buttonState == HIGH) {
      // if the current state is HIGH then the button went from off to on:
      buttonPushCounter++;
      Serial.println("on");
      Serial.print("number of button pushes: ");
      Serial.println(buttonPushCounter);
    } else {
      // if the current state is LOW then the button went from on to off:
      Serial.println("off");
    }
    // Delay a little bit to avoid bouncing
    delay(50);
  }
  // save the current state as the last state, for next time through the loop
  lastButtonState = buttonState;


  // turns on the LED every four button pushes by checking the modulo of the
  // button push counter. the modulo function gives you the remainder of the
  // division of two numbers:
  if (buttonPushCounter % 4 == 0) {
    digitalWrite(ledPin, HIGH);
  } else {
    digitalWrite(ledPin, LOW);
  }
}

Per these posts and these guidelines, if you've got something to say about the official example, its schematics, its explanation, or you have a better way of doing it, please share.

Some threads:

The first thing is the wiring. I thought LarryD's diagram is excellent:

From: Share tips you have come across - #999 by LarryD -- There's some discussion there and at https://forum.arduino.cc/t/wiring-a-button-pull-down-bad-vs-input-pullup-best/116298566)

Rigging the State Change Detection example to use the S3/INPUT_PULLUP scheme instead of the S1/ external pulldown scheme would be easier, safer, and cheaper. However, the Active-LOW logic is a little bit more harder to explain than Active-HIGH button logic.

Problem with your Fritzing picture: Won't work if the "TAC" button is turned 90 degrees, pull down (or up) resistor should be connected to the same pin as the wire to input. Beginners should avoid those 4 legged buttons but they're in every "starter" kit.

Hey, it's not my F'zing diagram. It's a direct link to the diagram in the official documentation for the state-change example as linked the File/Examples/02Digital/StateChangeDetection code in everyone's IDE.

And yes, a four-wire, two-terminal device adds ambiguity to the learning process.

Hooking from led - avoid the dim - #15 by ruilviana had a good image:

Well Dave, do you think "F'ing " is a proper word for a "family" forum?

Yes, and Ive seen the same or similar "Fritz" too many times, it's a problem for newbies.
I said "your" because YOU posted it without reference.

How about I add a 'z'?

Sorry, I had reference links to the tutorial and code just before the image and code. It's now edited to make the reference more clear.

So, if the button on the first picture were oriented like your second one the 5V bus would have a 10k load and the input pin would still be floating. That was my point.

You're correct. My bad.

At least this code might work. I would have sworn that not only does the offered example edge detector do a few annoying things to keep things simple, it just plain doesn't work.

Musta been another similar state change or button press counting example. I feel sorry for all ppl who stumble into example code that just does not work.

a7

I did happen to wire it up sloppily in Wokwi earlier today, falling for the floating input pin, and thought it was the debouncing that wasn't working.

If they were aiming for making things simple, the extra divide-by-4 chunk makes things needlessly complicated.

Here's a Wokwi of the Arduino Official code:

imagehttps://wokwi.com/projects/391475222059330561

The essential trick behind edge detection is remembering a value from before and comparing it to the currentValue, and the extra bits in the official sketch might distract from that.

If I was going to use this active-HIGH circuit and leave in some extra bits like debouncing and doing something interesting with the LED, I'd highlight those extra bits as separate features:

/*
  State change detection (edge detection)
  (with debouncing and LED toggling)
  https://wokwi.com/projects/391475222059330561

  Modified per https://forum.arduino.cc/t/state-change-detection-edge-detection-for-pushbuttons/1231632/10?u=davex
  to focus on the change detection, 
  and to use millis() for debouncing the change detection instead of delay();

  Often, you don't need to know the state of a digital input all the time, but
  you just need to know when the input changes from one state to another.
  For example, you want to know when a button goes from OFF to ON. This is called
  state change detection, or edge detection.

  This example shows how to:
  1) detect when a button or button changes from off to on and on to off.
  2) debounce an input with millis()
  3) toggle a LED based on its state

  The circuit:
  - pushbutton attached to pin 2 from +5V
  - 10 kilohm resistor attached to pin 2 from ground
  - LED attached from pin 13 to ground through 220 ohm resistor (or use the
    built-in LED on most Arduino boards)

  created  27 Sep 2005
  modified 30 Aug 2011
  by Tom Igoe
  modified 04 Mar 2023 by DveX for https://forum.arduino.cc/t/state-change-detection-edge-detection-for-pushbuttons/1231632/10
  This example code is in the public domain.
  
  Based on:
  https://docs.arduino.cc/built-in-examples/digital/StateChangeDetection/
*/

// this constant won't change:
const int buttonPin = 2;  // the pin that the pushbutton is attached to
const int ledPin = 13;    // the pin that the LED is attached to

// Variables will change:
int buttonState = 0;        // current state of the button
int lastButtonState = 0;    // previous state of the button
uint32_t lastChangeMillis = 0; // previous time of change for debouncing
uint32_t debounceInterval = 50;

void setup() {
  // initialize the button pin as a input:
  pinMode(buttonPin, INPUT);
  // initialize the LED as an output:
  pinMode(ledPin, OUTPUT);
  // initialize serial communication:
  Serial.begin(115200);
}


void loop() {
  // read the pushbutton input pin:
  buttonState = digitalRead(buttonPin);
  uint32_t currentMillis = millis();

  // compare the buttonState to its previous state
  if (buttonState != lastButtonState ) { // if the state has changed
    if (currentMillis - lastChangeMillis >= debounceInterval) { // limit rate using time stamps
      lastChangeMillis = currentMillis; // record the time for future debouncing
      // save the current button state as the real last state, for next time through the loop
      lastButtonState = buttonState;
      // act on the detected state change:
      if (buttonState == HIGH) {
        // if the current state is HIGH then the button went from off to on:
        Serial.print("HIGH ");
        toggleLed();
      } else {
        // if the current state is LOW then the button went from on to off:
        Serial.print("LOW ");
      }
    }
  }
}

void toggleLed(void){
  if (digitalRead(ledPin) == LOW) { // toggle the LED as needed
    digitalWrite(ledPin, HIGH);
  } else {
    digitalWrite(ledPin, LOW);
  }
}

Since the structure of the edge debouncer is

  if (buttonState != lastButtonState ) { // if the state has changed
    if (currentMillis - lastChangeMillis >= debounceInterval) { // limit... 


// stuff

    }
  }

The two conditions are in a logical and test. Since and commutes, there is no reason one can't write

  if (currentMillis - lastChangeMillis >= debounceInterval) { // limit... 
    if (buttonState != lastButtonState ) { // if the state has changed

// same stuff

    }
  }

Since forever I do the time test first.

I think there may be a good reason to do.

If the first sketch presented is simply state change detectation, a loop-level,"poor man's" debouncing can be placed outside the context of the edge detector, and excused momentarily with some hand waving.

I usually put it at the bottom.

Then, the Ugly Truth can be revealed, and a proper debouncing mechanism can envelop the edge detector, accomplishing the same thing as the crude debounce, only better, as now it no longer blocks.

It's also handy to do the time test first, as it affords the occasional opportunity to out-dent the millis() time limited block.

I do this at the top of throttled loops meant to run at, say, 50 Hz.

a7

They are indeed a logical and, and your reasons make sense. but there could be reasons to do one or the other first. The simplicity of explaining a loop-level delay() or taking the next step into an encompassing if(millis() -last >= interval){...} is good. But swapping the logic can have an impact on the responsiveness the debouncing/rate limiting, switching between edge detection and polling.

If you stretched out the delay/rate-limit interval beyond usual debouncing times, the outer polling test might cause problems. For a ridiculous motivating example, consider building a Skinnerian pigeon feeder button to rate-limit the pigeon self-feedings to no more than 1 feeding per 10 minutes (600000ms). You'd likely want an instantaneous response and to mark the time interval from the actual feeding. The Arduino stock example could do this by increasing the delay to delay(300000) (half because it would take 5 minutes to reset). Or a little more clearly, one could move the delay up to the leading edge of the button press and delay(600000). Polling with either a blocking or non-blocking outer test would likely miss pigeon pecks and starve the pigeons.

I mean to say that the ordering of the clauses could possibly matter, depending on the application.

Active-LOW Wokwi example, with bonus code for falling-edge rate-limited pigeon feeding:

image
https://wokwi.com/projects/391482018625678337

Haha, the only sense I can make of the rest of your post is that this is not what you mean to say.

But I will admit that you have made me think.

Both the version where the delay() is within the edge detecting and the version where there is no delay at all will leave the response near instant and the pigeons happy.

In both they will get fed, but not again for the brain dead delay() period or the millis() lockout period.

The bad guy here is the poor man. A delay() every loop means the pigeons will wait, on average, 1/2 the brain dead period. Worse, if you really mean pigeons, they gonna be unhappy (hungry) because the tiny window of observation such a loop affords means they'd have to be lucky, or peck and hold down the button. Pigeons are smart, but they may not be equipped to press and hold. I bet monkeys would figure out that pressing and holding the button would deliver the reward - it reminds me of trying to get some device with two or three buttons to work in the absence of the instructions… it's virtually impossible to test every "combination" some clever engineer might have come up with.

I don't think you say that

  if (A)
    if (B)
      C();

is different if tests A and B are swapped, at least not unless we worry about the costs of doing A and B and whether any side effects need to be considered.

But this just shows to go you that it is probably necessary for noobs to see this and have it explained and experience the results of not doing it, or doing it wrong before it can be truly considered locked-in knowledge.

Of course some of my knowledge is locked in so well it has become virtually inaccessible. :expressionless:

I have always argued for the no delay near instant response, and I have usually expressed that preference in the context of regular normally open switches operated by humans.

Other signals that need debouncing or deglitching or whatever you wanna call it may need some of the more elaborate or obscure methods.

a7

These details probably belong on separate thread about the Debounce on a pushbutton tutorial's Debounce.ino example. Maybe a thread like Debounce Time Code Explanation or Leading edge debouncing or something new.

But I'd still quibble a bit with:

The side effects do need to be considered, or rather the effects of the placement of the lastMillis=currentMillis update within which set of braces for which condition. The timestamp reset needs to be updated inside the inner set of braces of the two conditions to work properly, not wrapped around the outside:

if (A){
  if (B){
      C();
      // update the time here inside the (A AND B) 
      lastMillis = currentMillis;
  }
  //lastMillis = currentMillis; // not here inside only A
}

If you put it outside you get a non-blocking, but unwanted behavior of C() like the brain-dead delay(600000) on every loop. It also needs to be initialized strangely (lastMillis = -interval) to duplicate the delay(600000) behavior that would feed both the t=1s & t=601s button-pecking pigeons.

Transforming the example's delay() to use millis() needs some special care to get the same behavior.

If you do that, you no longer have

   if (A)
      if (B)
         C();

and the two conditions are no longer performing an and statement.

Taking part of the roll of function C() out gives you

   if (A) {
      if (B)
        C();
     whatever();   
  }

The flowchart on the left is the one with the time update where the pigeons want it. The one on the right shows the movement to the X box of what will now make the system inconsistent and sluggish.

If tests A and B are arranged sub-optimally. Here it matters; when the simple structure obtains, the order of the tests does not matter.

I can't say for sure I never suggested or implied that the alternate placement was correct, With test A being time, not activity.

I think.

a7

I thought the fuzzy bit were in these words:

Your pseuedocode in that post clearly has the // same stuff comment, but from the "envelop" and "out-dent the millis() time-limited block" language it might not be excruciatingly clear that the timestamp setting needs to be within the inner scope and not outside in the enveloping scope of the time-limiting block.

(I'm sure I agree with you. Is there anything left of this horse?)

LOL, I'm sure you aren't just trying to make it stop.

You are too kind. I could have made it clear, period. Instead of sharing how my own brain works, something I have yet to learn is no a good idea.

No, haha. The takeway is as simple as "start a lockout timer when the thing you want to lock out happens".

Reset the debounce timer when you see an edge.

I recently tore apart a debounce library that looked plausible, but had one or more of these errors. The only context it ever worked correctly in had a loop throttle of some kind…

OK, found it. One error, which is that it acts on the positive edge, not both. So only one edge invokes the lockout period…

So instead of

if (buttonState != lastButtonState ) {

there was

if (!buttonState && lastButtonState ) {

I won't say how long I looked at the function before I even noticed, so closely to the idiomatic pattern did it hew.

The function
bool pushButton::wasPressed()
{
  bool answer = false;
  static uint64_t then = millis();

  estadoAtual = digitalRead(_pin);
  if(!estadoAtual && estadoAnterior)
  {
    if(millis() - then > debounce)
    {
      answer = true;
      then = millis();
    }
  }
  estadoAnterior = estadoAtual;

  return answer;
}

a7

Well they have four legs for a good reason, physical stability in the face of frequently applied finger pressure - 2 leg version would likely fatigue and break with a much shorter useful life.

The way to wire these switches without worry is diagonal, corner-to-corner, then you don't have to remember which way they are commoned up internally.

How about if, contrary to the Arduino tutorial, you don't rely on how the button pins are internally connected to rig the pull-down resistor? I think it would be clearer and cleaner to connect the pull-up/pulldown or current limiting resistors directly to the Arduino pins.