/*
Forum: https://forum.arduino.cc/t/need-beginner-help-with-debounce-logic/1425853
Wokwi: https://wokwi.com/projects/453676692512664577 (for latest see updates below)
ec2021
Example for reading buttons with "acceleration" of returned
(btn.down() == True)
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.
2026/01/20
A counter has been added that is incremented and decremented by pressing the two
buttons. To further speed up the process, the increment and decrement values are
calculated based on the time the respective button is held down.
2026/01/25
Forum: same as above
Wokwi: https://wokwi.com/projects/454183828831326209
Copious assistance from the forum contributors
Minor modifications by jg1 include:
LiquidCrystal I2C
count range arbitrarily limited to six digits
limit maximum rate consistent with six digit limit
wrap around zero in both directions
right justify count value and add leading zeroes
Started to add code to separate the active counting state
from setting the count value.
Setting the count value is processor-intensive.
The Arduino will be mostly at rest when the slide switch is "on"
i.e. the actual counting state.
When it is in the "on" state the prox sw will be used to increment
the count value, and the up / down switches will be ignored.
While in the "on" state time-consuming actions can be performed,
such as writing the count value to EEPROM and reading the thermometer.
Wokwi: https://wokwi.com/projects/454412444481892353
Changes by ec2021
*/
#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
/*******************************************************
In version https://wokwi.com/projects/454412444481892353
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 to use 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
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];
// 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 ment 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);
pinMode(counterOnPin, INPUT_PULLUP);
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);
// the following button[i].init procedure ONLY applies to buttons that you want to repeat
// otherwise apply the idea only to specific buttons
// as shown immediately below these commented lines
// for (int i = 0; i < buttons; i++) {
// Pin No, minInterval, maxInterval, decrement
// button[i].init(buttonPin[i], 50, 400, 100);
// }
/*******************************************************************
With paramType struct the original 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