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:
- AceButton
- Button.h
- Bounce2
- OneButton & here
- toggle.h
- and many others from Signal Input/Output - Arduino Reference
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");
}
}
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.