/*
Forum: https://forum.arduino.cc/t/need-beginner-help-with-debounce-logic/1425853
Wokwi: https://wokwi.com/projects/454699917472227329
Copied from https://wokwi.com/projects/454412444481892353 created by ec2021
Supersedes previous version https://wokwi.com/projects/454183828831326209
Copious assistance from the forum contributors
Latest notes 2026/01/31
Presettable six digit "totalizer" with debounced inputs
Changes by ec2021
Example for reading buttons with "acceleration" of returned
(btn.down() == True)
The six digit counter / "totalizer" can be preset up or down
by pressing either of two buttons. So as to rapidly preset a desired
count start value, the increment and decrement values are calculated
based on the duration of time the respective button is held down.
Minor modifications by jg1 include:
Comments added or reorganized for my own edification
added LiquidCrystal I2C
count range arbitrarily limited to six digits
limit maximum rate consistent with six digit limit
wrap through zero in both directions whether counting or setting
right justify count value and add leading zeroes
Count on / set switch distinguishes the active counting state
vs presetting the count value.
Setting the count value is processor-intensive.
The totalizer will be mostly at rest when the slide switch is "on"
which will be its usual, actual counting state.
While in that stat, the prox sw will be used to increment
the count value (increment only).
While in that state, the up / down switches are ignored.
While in that state, addional time-consuming actions can be performed.
Future features contemplated but not yet implemented include:
writing to EEPROM or offboard nonvolatile storage
thermal input for motor temperature LCD display
power on reset / erase EEPROM (easter egg)
*** it should NOT be easy to reset counter to zero ***
*** if implemented, consider some way to "undo" it ***
Certain features such as LCD "delta" display and
serial USB read / write are temporary,
for debugging or personal development purposes,
and will eventually be removed.
*/
#include <LiquidCrystal_I2C.h>
#include <EEPROM.h>
#include "OneWire.h"
#include "DallasTemperature.h"
#define I2C_ADDR 0x27
#define LCD_COLUMNS 16
#define LCD_LINES 2
#define ONE_WIRE_BUS 9
// Create a new instance of the oneWire class to communicate with any OneWire device:
OneWire oneWire(ONE_WIRE_BUS);
// Pass the oneWire reference to DallasTemperature library:
DallasTemperature sensors(&oneWire);
// define LCD parameters
LiquidCrystal_I2C lcd(I2C_ADDR, LCD_COLUMNS, LCD_LINES);
char countTxt[16]; // specific for this particular 2 line x 16 char LCD
char decrTxt[16]; // specific for this particular 2 line x 16 char LCD
/*******************************************************
ec2021 comments
In this version,
the struct paramType and the declaration of the array param[] was
added and replace the data in the array buttonPin[] and in the call of init()
in setup()
This allows use of a for-loop to intialize all buttons
/*******************************************************
*/
struct paramType {
byte pinNo;
unsigned long minIntv;
unsigned long maxIntv;
unsigned long decr;
};
constexpr paramType param[] =
{
{3, 50, 400, 100}, //button0 -> the up toggle sw
{4, 50, 400, 100}, //button1 -> the down toggle sw
{2, 50, 400, 0}, //button2 -> the prox sw
{5, 50, 400, 0} //button3 -> the count on / set toggle sw
};
constexpr int buttons = sizeof(param) / sizeof(param[0]);
constexpr byte countSetIndex {buttons - 1}; // Addresses the parameters of the count on/ set toggle sw in the array
const long blinkinterval = 1000; // interval at which to blink the onboard LED (milliseconds)
unsigned long previousMillis = 0; // will store last time onboard LED was updated
long lastCount = -1; // flag to indicate count change (it's never negative)
int led0State = LOW; // led0State used to set the onboard LED
int counterOnState = HIGH;
const int ledOnboard = LED_BUILTIN; // onboard LED pin designation
const int proxPin = 2; // magnetic prox sensor input pin designation
const int counterOnPin = 5; // counter "on" toggle switch pin designation
/*******************************************************
ec2021 comments
AccelBtnClass provides the ability to set a minimum and maximum interval
to receive true from the function btn.down() while the button is pressed.
The time to return true is decremented by a given value "decrement" in btn.init(...).
Once the button has been released the interval is reset to maxInterval
again.
/*******************************************************
*/
class AccelBtnClass {
private:
byte pin;
byte state;
byte lastState;
byte reportedState;
unsigned long lastChange;
unsigned long lastPressTime;
unsigned long lastCheckTime;
unsigned long startDownTime;
unsigned long accelInterval;
unsigned long minInterval;
unsigned long maxInterval;
unsigned long decrement;
public:
init(paramType params) {
init(params.pinNo, params.minIntv, params.maxIntv, params.decr);
};
init(byte p, unsigned long minIntv, unsigned long maxIntv, unsigned long decr) {
pin = p;
pinMode(pin, INPUT_PULLUP);
lastPressTime = 0;
lastCheckTime = 0;
minInterval = min(minIntv, maxIntv);
maxInterval = max(minIntv, maxIntv);
accelInterval = maxInterval;
decrement = decr;
reportedState = LOW;
};
unsigned long getLastPressTime() {
return lastPressTime;
};
unsigned long getLastCheckTime() {
return lastCheckTime;
};
void reset() {
lastPressTime = 0;
}
boolean isReleased() {
return state;
}
unsigned long getDownInterval() {
return lastCheckTime - startDownTime;
}
boolean getDebouncedState() {
byte actState = digitalRead(pin);
if (actState != lastState) {
lastChange = millis();
lastState = actState;
};
if (actState != state && millis() - lastChange > 30) {
state = actState;
if (!state) lastPressTime = lastChange;
}
return state;
}
boolean pressed() {
boolean actState = getDebouncedState();
if (reportedState != actState) {
reportedState = actState;
return !reportedState;
}
return false;
}
boolean down() {
if (!digitalRead(pin)) {
if (millis() - lastCheckTime >= accelInterval) {
state = LOW;
lastCheckTime = millis();
if (accelInterval == maxInterval) {
startDownTime = lastCheckTime;
}
if (accelInterval >= minInterval + decrement) {
accelInterval -= decrement;
}
return true;
} else {
return false;
}
} else {
accelInterval = maxInterval;
state = HIGH;
return false;
}
}
};
AccelBtnClass button[buttons];
/*******************************************************
// ec2021 comments
// The following declaration creates a pointer to an AccelBtnClass object
// The pointer will be set to the instance of the button[] array that
// belongs to the slide switch so that it's easier to read the code
// It's meant as an example: The same could be done for the other buttons as well ...
/*******************************************************
*/
AccelBtnClass *CountSetBtn;
long Count = 0;
long increment = 1;
long decrement = 1;
void setup() {
// initialize the external switches as input, and choose the internal pullup resistor:
pinMode(proxPin, INPUT_PULLUP); // not sure if I need any of this
pinMode(counterOnPin, INPUT_PULLUP); // given the button class above
pinMode(ledOnboard, OUTPUT);
// LCD Initialization
lcd.init();
lcd.backlight();
// Print some things that won't change
lcd.setCursor(0, 0);
lcd.print("Count: not set");
Serial.begin(9600);
sensors.begin();
// set a suitably low resolution. Allegedly lower = faster
// setResolution(9) = 12 bits = about 0.5 degC
// it's still too slow though. Need to relegate this to a low priority task.
sensors.setResolution(9);
/*
With paramType struct this initialization can be used
for (int i= 0; i<buttons;i++){
button[i].init(param[i].pinNo,
param[i].minIntv,
param[i].maxIntv,
param[i].decr
);
}
or as an alternative one can call the init function that
directly reads the struct as its parameter.
CountSetBtn is set to the corresponding array entry.
These calls can be used alternatively:
button[3].getDebouncedState()
button[countSetIndex].getDebouncedState()
CountSetBtn->getDebouncedState()
*/
for (int i = 0; i < buttons; i++) {
button[i].init(param[i]);
}
CountSetBtn = &button[countSetIndex];
/******************************************************************/
}
void toggleLED() { // this is kinda dumb but why not
led0State = !led0State;
}
void doNothing() { // do nothing placeholder
}
void displayCount() {
// Note regarding the following snprintf function:
// The %08 here is something hardcoded.
// It's the width of the display (16) minus the number of characters in "Count:"
// 16 - 6 = 10, so that's the remaining space at the right of that label.
// if we change the label "Count:", we will want to change the 08.
// the .6 tells the function how many digits you want (six).
// the ld means pad with zeroes (I think)
// math is hard
snprintf(countTxt, sizeof countTxt, "%08.6ld", Count);
lcd.setCursor(8, 0);
lcd.print(countTxt);
lastCount = Count;
}
void loop() {
/*
The function getDebouncedState() returns .... the debounced state of course ... ;-)
It can be used here alternatively as
if (button[3].getDebouncedState()){}
if (button[countSetIndex].getDebouncedState()){} or
if (CountSetBtn->getDebouncedState()) {}
*/
if (CountSetBtn->getDebouncedState()) {
Counting();
} else {
notCounting();
}
if (Serial.available()) { // If anything comes in Serial (USB)
Serial.write(Serial.read()); // echo it back to Serial (USB)
}
// heartbeat routine that runs all the time
// check to see if it's time to toggle the onboard LED; that is, if the difference
// between the current time and last time you toggled the LED is larger than
// the interval at which you want to toggle the LED.
unsigned long currentMillis = millis();
unsigned long currentMicros = micros(); // not used
unsigned long currentSecs = currentMillis / 1000;
if (currentMillis - previousMillis >= blinkinterval) {
previousMillis = currentMillis;
toggleLED();
digitalWrite(ledOnboard, led0State);
if (led0State) {
Serial.print("Time ");
Serial.print(currentSecs);
Serial.print("\t");
Serial.println();
} else {
doNothing();
}
}
}
void Counting() {
if (button[2].pressed()) {
Count++;
if (Count > 999999) { // wrap through zero
Count = 0;
}
displayCount();
}
}
void notCounting() {
// This routine will be called upon only when count on / off sw is in the "set" position
// It checks the up / down toggle switch action
// and increments / decrements the count value when the corresponding button is depressed.
// The resulting count value should be written to EEPROM only when
// the count on / set switch transitions from "set" to "on"
if (Count != lastCount) {
displayCount();
}
if (button[0].down()) {
Count += increment;
if (Count > 999999) { // wrap through zero
Count = 0;
}
lcd.setCursor(0, 1);
lcd.print("delta: ");
snprintf(decrTxt, sizeof decrTxt, "%10.4ld", increment); // this still needs work because math is hard
lcd.setCursor(6, 1);
lcd.print(decrTxt);
// The following line changes the increment depending
// on the duration the button is held down
increment = min(pow(2, button[0].getDownInterval() / 1000), 2047); // limit max delta
}
if (button[1].down()) {
Count -= decrement;
if (Count < 0) { // wrap through zero
Count = 999999;
}
lcd.setCursor(0, 1);
lcd.print("delta: ");
snprintf(decrTxt, sizeof decrTxt, "%10.4ld", decrement); // this still needs work because math is hard
lcd.setCursor(6, 1);
lcd.print(decrTxt);
// The following line changes the decrement depending
// on the duration the button is held down
decrement = min(pow(2, button[1].getDownInterval() / 1000), 2047); // limit max delta
}
// The following lines set the increment to 1
// when button[0] is not down
if (button[0].isReleased()) {
increment = 1;
}
// The following lines set the decrement to 1
// when button[1] is not down
if (button[1].isReleased()) {
decrement = 1;
}
/* sensors fetch. This takes a long time, causing the accel rate to be reduced by half.
Omitted for now. Add it as a low priority task (somehow)
// Send the command for all devices on the bus to perform a temperature conversion:
sensors.requestTemperatures();
// Fetch the temperature in degrees Celsius for device index:
float tempC = sensors.getTempCByIndex(0); // the index 0 refers to the first device
// Fetch the temperature in degrees Fahrenheit for device index:
float tempF = sensors.getTempFByIndex(0);
*/
/*
// Print the temperature in Celsius in the Serial Monitor:
Serial.print("Temperature: ");
Serial.print(tempC);
Serial.print(" \xC2\xB0"); // shows degree symbol
Serial.print("C | ");
// Print the temperature in Fahrenheit
Serial.print(tempF);
Serial.print(" \xC2\xB0"); // shows degree symbol
Serial.println("F");
*/
}
Up toggle sw
down toggle sw
Count on / set sw
prox sw
on
set