Edge detection design?

State change detection or edge detection is a common problem when you want to do one action based on one change.

Alternatives to edge detection are:

  • delay() -- to rate-limit the actions (see Examples/02.Digital/button).
  • BlinkWithoutDelay - to slow down the actions without blocking everything else
  • interrupts to detect edge changes
  • state machines to switch the focus of the processing away from re-processing old information.
  • millis() with the edge-detecting if(millis() - last >= interval){ last = millis();...} construction.

Arduino provides an example of state-change detection built-in File/Examples/02.Digital/StateChangeDetection (github) and it's tutorial:

And State-change/edge detection is also in many button libraries, since edge detection is an essential part of debouncing:

Here is an example of some scratch-built code for edge detection of a button and a potentiometer:

// https://wokwi.com/projects/383836308714551297
// For forum discussion at 
// https://forum.arduino.cc/t/edge-detection-design/1199335
// Note that the Wokwi diagram.json file setting is set to bounce:
//       "attrs": { "color": "green", "bounce": "1" }

const byte buttonPin = 2;
const byte potPin = A0;
const int potThreshold = 512;
const int potHysteresis = 100;

int buttonLevel, lastButtonLevel;
bool buttonRisingOneShot = false, buttonFallingOneShot = false;
int potLevel, lastPotLevel, potAboveThreshold;
bool potRoseToThresholdOneShot = false;
bool potFellBelowHysteresisOneShot = false;

void setup() {
  // put your setup code here, to run once:

  pinMode(buttonPin, INPUT_PULLUP);
  buttonLevel = lastButtonLevel = digitalRead(buttonPin);

  potLevel = lastPotLevel = analogRead(potPin);

  Serial.begin(115200);
}
void loop() {
  // put your main code here, to run repeatedly:

  // handle inputs
  buttonLevel = digitalRead(buttonPin);
  potLevel = analogRead(potPin);

  // handle one-shots
  // clear flags
  buttonRisingOneShot = false;
  buttonFallingOneShot = false;
  potRoseToThresholdOneShot = false;
  potFellBelowHysteresisOneShot = false;
  // set flags based on changes
  if (buttonLevel != lastButtonLevel) {
    if (buttonLevel == HIGH) {
      buttonRisingOneShot = true;
    }
    if (buttonLevel == LOW) {
      buttonFallingOneShot = true;
    }
    lastButtonLevel = buttonLevel;
  }
  if (potLevel != lastPotLevel ) {
    if (potLevel >= potThreshold && lastPotLevel < potThreshold) {
      potRoseToThresholdOneShot = true;
    }
    if (potLevel < potThreshold - potHysteresis && lastPotLevel > potThreshold - potHysteresis) {
      potFellBelowHysteresisOneShot = true;
    }
    lastPotLevel = potLevel;
    Serial.print('.');
  }
  //use one-shots
  if (buttonRisingOneShot == true) {
    Serial.print("R");
  }
  if (buttonFallingOneShot == true) {
    Serial.print("F");
  }
  if (potRoseToThresholdOneShot == true) {
    Serial.print("^");
  }
  if ( potFellBelowHysteresisOneShot == true) {
    Serial.print("v");
  }
}

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

This code focuses on setting a one-shot variable to "true" for the duration of the single loop in which a condition becomes true.

Instead of the one-shot flag being cleared after one loop(), it could persist through many loops and be cleared at the point when it is acted upon:

  if ( potFellBelowHysteresisOneShot == true) {
    Serial.print("v");
    potFellBelowHysteresisOneShot = false;
  }

The code in the loop could easily be moved into functions or structures, as is done in the many libraries

Besides buttons, or potentiometers, the edge detecting code could be applied to logical variables or conditions, such as the this essential part of Blink Without Delay:

The combination of the 'if()' and update.. is an edge-detecting construct, since previousMillis is updated to make the 'if()' condition false after it tests true, and ensures that the code within the if()'s braces only runs once.

Using edge detection (or state-change detection) is an important tool for efficiently running code just once in response to changes in a system.

1 Like

the wokwi conveniently set the button to not bounce...

change in diagram.json

    {
      "type": "wokwi-pushbutton",
      "id": "btn1",
      "top": -89.8,
      "left": 144,
      "attrs": { "color": "green", "bounce": "0" }
    }

into

    {
      "type": "wokwi-pushbutton",
      "id": "btn1",
      "top": -89.8,
      "left": 144,
      "attrs": { "color": "green", "bounce": "20" }
    }

and a simple press of the button generates

FRFRFRFRFRFRFRFRFRFRFR

so it does detect indeed all the edges but at higher level the end user only wants one click to be detected.

1 Like

You are correct. I explicitly set Wokwi for a noise-free button. I wanted to focus on the idea of edge detection. Some folks like debouncing in hardware, and I didn't want to confuse edge detection with debouncing multiple edges.

Perhaps I should set it to bounce, and....maybe add a bit of delay?

Or maybe leave the one-shot to be cleared by the handler?

Or add an additional one-shot timer to blank-out more transitions within a time window.

Or just turn on the noise and acknowledge that edge detection is sensitive to noisy signals.

Edge detection with noisy signals does need some additional care to handle the noise.

The bouncing is actually interesting to see as it shows you do capture all the edges which is your topic of discussion.

A quick mention could be made then to say that as demonstrated, edge detection is not enough to drive a button unless you eliminate the bouncing in hardware.

1 Like

On Wokwi, I turned on the button noise, and added a commented out delay:

    //delay(4);  // uncomment for debouncing a noisy button

yes - or a FSM could be put to work if you don't want to block the code

Here's a version guarding against bounce with millis():

  if ( buttonLevel != lastButtonLevel && millis() - lastButtonChangeMs > 4) {
  ...
  lastButtonChangeMs = millis();
// https://wokwi.com/projects/383836308714551297
// For forum discussion at 
// https://forum.arduino.cc/t/edge-detection-design/1199335
// Note that the Wokwi diagram.json file setting is set to bounce:
//       "attrs": { "color": "green", "bounce": "1" }

const byte buttonPin = 2;
const byte potPin = A0;
const int potThreshold = 512;
const int potHysteresis = 100;

int buttonLevel, lastButtonLevel;
bool buttonRisingOneShot = false, buttonFallingOneShot = false;
unsigned long lastButtonChangeMs =0;
int potLevel, lastPotLevel, potAboveThreshold;
bool potRoseToThresholdOneShot = false;
bool potFellBelowHysteresisOneShot = false;

void setup() {
  // put your setup code here, to run once:

  pinMode(buttonPin, INPUT_PULLUP);
  buttonLevel = lastButtonLevel = digitalRead(buttonPin);

  potLevel = lastPotLevel = analogRead(potPin);

  Serial.begin(115200);
}
void loop() {
  // put your main code here, to run repeatedly:

  // handle inputs
  buttonLevel = digitalRead(buttonPin);
  potLevel = analogRead(potPin);

  // handle one-shots
  // clear flags
  buttonRisingOneShot = false;
  buttonFallingOneShot = false;
  potRoseToThresholdOneShot = false;
  potFellBelowHysteresisOneShot = false;
  // set flags based on changes
  // if (buttonLevel != lastButtonLevel) {
  if ( buttonLevel != lastButtonLevel && millis() - lastButtonChangeMs > 4) {
    if (buttonLevel == HIGH) {
      buttonRisingOneShot = true;
    }
    if (buttonLevel == LOW) {
      buttonFallingOneShot = true;
    }
    lastButtonLevel = buttonLevel;
    //delay(4);  // uncomment for debouncing a noisy button
    lastButtonChangeMs = millis();
  }
  if (potLevel != lastPotLevel ) {
    if (potLevel >= potThreshold && lastPotLevel < potThreshold) {
      potRoseToThresholdOneShot = true;
    }
    if (potLevel < potThreshold - potHysteresis && lastPotLevel > potThreshold - potHysteresis) {
      potFellBelowHysteresisOneShot = true;
    }
    lastPotLevel = potLevel;
    Serial.print('.');
  }
  //use one-shots
  if (buttonRisingOneShot == true) {
    Serial.print("R");
  }
  if (buttonFallingOneShot == true) {
    Serial.print("F");
  }
  if (potRoseToThresholdOneShot == true) {
    Serial.print("^");
  }
  if ( potFellBelowHysteresisOneShot == true) {
    Serial.print("v");
  }
}

If you think of millis() or millis() - lastButtonChangeMs>interval as a state, it is an FSM.

image graphviz...

2 Likes

In the original @DaveX writes

    //delay(4);  // uncomment for debouncing a noisy button

I've just placed such a delay right in the loop to slow the entire process down with barely a mention of why.
void loop()
{
// whatever
// whatever
// whatever


// then
  delay(50);       // poor man's debouncing
}

Here's a place I would make one if statement for whether sufficient time had elapsed, and in that body test for a change of the input:

  if ( millis() - lastButtonChangeMs > 4) {
    if ( buttonLevel != lastButtonLevel) {
      if (buttonLevel == HIGH) {
        buttonRisingOneShot = true;
      }
      if (buttonLevel == LOW) {
        buttonFallingOneShot = true;
      }
      lastButtonLevel = buttonLevel;
      lastButtonChangeMs = millis();
    }
  }

At least maybe the time check should be first, to make clear the priority given to not looking too soon at a recently transitioned input.

The only reason I looked closer at this is the leftover commented delay(4) line... I'm reading it thinking this looks like a debounced pushbutton. In the 2nd version that line should be losted.

a7

1 Like

So for debouncing we have several options:

  • hardware debounce ( here, here, or here)
  • slow the loop or a portion of the loop() with a debouncing delay()
  • use if(millis()-last>=interval){...} to rate-limit a portion of the code
  • use button library debouncing

Hello DaveX

I use the polling method for a button manager that can manage several buttons "in parallel".
The button manager checks the status of the buttons every 20msec and generates a signal for "Button has been pressed" or "Button has been released".

1 Like

I found some bounce testing code at PJRC and modified it to count and time bounces. It uses both a "leading edge detection" and a "stabilize and lag" debounce strategy.

Here it is in Wokwi:

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

The output for a few bouncy button presses:

Pushbutton bounce test:
count: 37 us: 2336 total: 37
count: 56 us: 2036 total: 93
count: 56 us: 2336 total: 149
count: 33 us: 2088 total: 182
count: 30 us: 2432 total: 212

and the code:

// ButtonBounce_measure -- measures bounces per toggle
// modified from
// https://www.pjrc.com/teensy/td_libs_Bounce.html
//
// Wokwi sim: https://wokwi.com/projects/384500431436449793
// change the Wokwi diagram.json bounce attribute to 0 for a perfect button
//      "attrs": { "color": "green", "bounce": "1" }
//      "attrs": { "color": "green", "bounce": "0" }
//
const int buttonPin = 12;

void setup() {
  pinMode(buttonPin, INPUT_PULLUP);
  Serial.begin(115200);
  Serial.println("Pushbutton bounce test:");
}

byte previousState = HIGH;         // what state was the button last time
unsigned int count = 0;            // how many times has it changed to low
unsigned long countAt = 0;         // when count changed
unsigned int countPrinted = 0;     // last count printed
unsigned long leadingEdgeUs = 0;
const unsigned long leadingEdgeInterval = 10000; // 10ms for debounce
const unsigned long printDelay = 100000UL; // 100ms for reporting

void loop() {
  unsigned long nowMicros = micros();

  byte buttonState = digitalRead(buttonPin);
  if (buttonState != previousState) {
    if (buttonState == LOW) {
      count = count + 1;
      countAt = nowMicros;

      if (nowMicros - leadingEdgeUs >= leadingEdgeInterval) {
        leadingEdgeUs = nowMicros;
      }
    }
    previousState = buttonState;
  } else {
    if (count != countPrinted) {
      // unsigned long nowMillis = millis();
      if (nowMicros - countAt > printDelay) {
        Serial.print("count: ");
        Serial.print(count - countPrinted);
        Serial.print(" us: ");
        Serial.print(nowMicros - printDelay - leadingEdgeUs);
        Serial.print(" total: ");
        Serial.println(count);
        countPrinted = count;
      }
    }
  }
}

It is interesting to use the Arduino to measure its own bounciness.

Did you include the use of regular interval read bit histories?

Use a micros bit to trigger each read, bit 9 flips every 512 micros but could read more often. It's way faster than subtracting times and doesn't keep track except for every read is X micros wide. If it was LOW and became HIGH, edge detected! Bounce is just a lot of very real edges and debounce is waiting for stable reads longer than Y ms.

So I have an unsigned integer with every bit a read once filled and the bit picture I use to find stable in 8 x 512 usec intervals is either 0 or 255 or 128 or 127. 128 is pin HIGH followed by 7 LOW reads but with 32 bit history, other patterns could be quickly detected.

I have not...could you post some code or links to the method?

Tests like if( ~(bitHistory & 0x7F))... or `if( (bitHistory & 0x7F) == 0))... do seem pretty fast.

OT: how do you generate that diagram and link to a graphviz diagram? Is there a simple copy/paste?

It is. If you use Graphviz Online and paste/edit in your .dot file on the LHS , it then translates the .dot file into its URL and renders it on the RHS.

I then pasted a screen-grab of the RHS into the forum and wrapped it in a forum image link to the online URL.

I haven't upgraded my example on hand to using micros bit timing, it's old and I dunno where newer is. This does demonstrate a use of bits as read history to identify transition then stable state for 7 half-ms reads as 0b10000000. History could be 32 bits ~ 1 number.

// add-a-sketch_button 2018 by GoForSmoke @ Arduino.cc Forum
// Free for use, Apr 30/2018 by GFS. Compiled on Arduino IDE 1.6.9.
// Update May 6/2018, Aug 11, 2018

/*  Button Debounce Example

  --- for this example connect a button between pin 7 and GND
  --- or stick a jumper in pin 7 and while holding the board steady
  --- tap the free end onto the grounded USB port box to press.
  --- Press and hold the Button to toggle led13 blinking.

  Yes I'm using a 16 bit micros timer to time fractions of millis as micros.
  The button reader only reads 1 button per call so as to not block void loop().
  Each button has a history byte that holds the last 8 reads with 256 possible
  states but only 4 of them being significant.
  0 is the button held down
  255 is the button left up
  127 is the buton changing from up to down, button just released.
  128 is the button changing from down to up, button just pressed.
  everything else is to be ignored as bounce.
*/

// button vars
byte buttonPin = 7;
byte buttonHistory;
word markButtonTime;        // 16-bit micros timers
const word waitButtonTime = 500; // micros

// added sketch task, on-off blinker vars
byte ledState, ledPin = 13; // use byte for small values, int cost 2 bytes
word startBlink, waitBlink; // 16 bit millis is good to time 65.535 seconds

void buttonTask()   // yup, this is my new button debounce compromise method.
{ // read twice per milli, bits 0 to 6 all same sets the state
  if ( word( micros()) - markButtonTime >= waitButtonTime ) // read occaisoinally
  {
    buttonHistory <<= 1; // if you don't know <<= look it up in the Arduino Reference
    // keep a browser open to that page when you use the IDE.
    buttonHistory += digitalRead( buttonPin ); // read history streams through buttonHistory
    markButtonTime = micros(); // gets the low 16 bits of micros(), time to 60 ms + margin
  }
  /*        buttonHistory bits read as values:
    0 is button held down, 8 reads of 0 in a row
    255 is button left up, 8 reads of 1 in a row
    127 is buton changing from up to down, 7 1's and 1 0 = just released
    128 is button changing from down to up, 7 0's and 1 1 = just pressed.
    everything else is to be ignored as bounce.
    Understand that 7 same bits in a row counts as pin state stable.
  */
}

void OnOffBlinker() // only blinks if there's a wait time, can be switched on/off
{
  if ( waitBlink > 0 ) // this is the on/off switch
  {
    // word( millis()) gets the low 16 bits of the 32-bit millis() return.
    if ( word( millis()) - startBlink >= waitBlink ) // difference in time by subtracting start from end
    {
      ledState = !ledState;  // ! is NOT: not_0/true becomes 0/false else 0 becomes 1.
      digitalWrite( ledPin, ledState ); // the led changes state.
      startBlink += waitBlink; // next blink starts when it should, where diff > wait.
    }
  }
  else if ( ledState > 0 ) // waitBlink == 0 turns blinking off
  {
    digitalWrite( ledPin, ledState = LOW ); //  make sure the led is OFF
  } // yes, you can set a variable during calculation in C, the write here is LOW.
}

void setup()
{
  Serial.begin( 115200 );
  for ( byte i = 0; i < 66; i++ )  Serial.println();
  Serial.println( F( "\n  Button Debounce Example, free by GoForSmoke\n" ));
  Serial.println( F( "\n-- for this example connect a button between pin 7 and GND" ));
  Serial.println( F( "--- or stick a jumper in pin 7 and while holding the board steady" ));
  Serial.println( F( "--- tap the free end onto the grounded USB port box to press." ));

  pinMode( buttonPin, INPUT_PULLUP );
  pinMode( ledPin, OUTPUT );

  buttonHistory = 255;
  waitBlink = 500;
};


void loop()
{
  buttonTask();
  /*
    0 is the button held down
    255 is the button left up
    127 is the buton changing from up to down, button just released.
    128 is the button changing from down to up, button just pressed.
    everything else is to be ignored as bounce.
  */

  switch ( buttonHistory ) 
  {
    case 128 : // pin is HIGH in bit 7, LOW for 7 reads, up to down detected
      buttonHistory = 0; // change detected, make it into no change now
      Serial.print( F( "press detected     " ));
      Serial.println( millis());
      if ( waitBlink == 0 ) // toggle action tied to button press
      {
        waitBlink = 500; // makes the blinking start
        startBlink = millis(); // gets the low 16 bits
      }
      else
      {
        waitBlink = 0; // makes the blinking stop
      }
      break;
    case 127 : // pin is LOW in bit 7, HIGH for 7 reads, down to up detected
      buttonHistory = 255; // change detected, make it into no change now
      Serial.print( F( "release detected   " ));
      Serial.println( millis());
      break;
  }

  OnOffBlinker();
}

I was using 2 bits to hold current and previous read instead of 2 bytes.

bit 0 gets current read, bit 1 is previous read

combined possibilities turn if (x == HIGH) & etc to a value:
0 = both LOW
1 = current HIGH, previous LOW
2 = current LOW, previous HIGH
3 = both HIGH

before the next read, read byte gets << 1 and &= 2 then add the read.

From there... why not use all the bits and encode time into each?
At regular intervals, shift then add the new read, nothing else fast.

Evaluation is down to bit patterns, either == key or bit logic results.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.