/*
Forum: https://forum.arduino.cc/t/need-beginner-help-with-debounce-logic/1425853
Wokwi: https://wokwi.com/projects/455495395049322497
Sketch from
https://wokwi.com/projects/454699917472227329
but with the function printActCount() added
(called in the first line in loop() )
duplicate Wokwi with diagram for the Nano is here:
https://wokwi.com/projects/455415060881775617
It's intended mostly for pinout verification.
Make no code changes there. Make changes here, then copy (if needed)
Presettable six digit counter / "totalizer" with debounced inputs
and persistent onboard memory
Copied from https://wokwi.com/projects/454412444481892353 created by ec2021
Example for reading buttons with "acceleration" of returned (btn.down() == True)
Supersedes previous version https://wokwi.com/projects/454183828831326209
Generous assistance from the forum contributors
ec2021, alto777, J-M-L, dougp, many others
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 state, 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
because the machine can only cycle so fast
Future features contemplated but not yet implemented include:
EEPROM wear leveling - or - offboard nonvolatile storage (FRAM)
power on reset / erase EEPROM
(easter egg to notice an unlikely combination of inputs on boot)
*** it should NOT be easy to reset counter to zero ***
*** if implemented, consider some way to "undo" it ***
Certain features such as serial USB read / write are temporary,
for debugging or personal development purposes,
and will eventually be removed.
notes / comments updated 2026/02/08
*/
#include <LiquidCrystal_I2C.h>
#include <EEPROM.h>
#include "EEClass.h"
#include <EEPROMWearLevel.h>
#include <eewl.h>
#include <DallasTemperature.h> // which includes <OneWire.h>
constexpr uint32_t maxWriteCount {10};
constexpr uint32_t maxCount {999999};
//Declare the myEE class
EEClass myEE;
#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]; // (to be removed, no longer used)
char lcdTemp[17]; // 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 magnetic prox sw
{5, 50, 400, 0} //button3 -> the count on / set toggle sw (slide sw shown on Wokwi)
};
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
// long Count; // the counter value
// ##############################
int32_t Count; // use fixed width Count instead? Signed, so that < 0 can be detected?
long increment = 1;
long decrement = 1;
const long maxDelta = 2047; // max accel rate
const long blinkinterval = 1000; // interval at which to blink the onboard LED (milliseconds)
const int ledOnboard = LED_BUILTIN; // onboard LED pin designation
unsigned long previousMillis = 0; // will store last time onboard LED was updated
long lastCount = -1; // effectively a flag to indicate count change (it's never negative)
long lastStoredCount; // the last Count value stored in EEPROM (not implemented yet)
long addrPtr = 0; // Fixed EEPROM address containing countAddr to be incremented each boot
long countAddr; // EEPROM count address
int led0State = LOW; // led0State used to set the onboard LED
int proxPin = 2; // the magnetic prox sw input input pin
int countUpPin = 3; // the "count up preset" toggle sw input pin
int countUpState = HIGH;
int countDownPin = 4; // the "count down preset" toggle sw input pin
int countDownState = HIGH;
int counterOnPin = 5; // the counter "on" / "set" slide sw
int counterOnState = HIGH;
unsigned long eepromTimer; // An idle timer. Write updates to EEPROM after a delay.
bool updateEEPROM = false; // flag to indicate Count needs to be written to EEPROM
/*******************************************************
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;
}
}
};
/*******************************************************
// 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 button[buttons];
AccelBtnClass *CountSetBtn;
void setup() {
// initialize external switches / buttons as inputs, and choose the internal pullup resistor
pinMode(proxPin, INPUT_PULLUP);
pinMode(countUpPin, INPUT_PULLUP);
pinMode(countDownPin, INPUT_PULLUP);
pinMode(counterOnPin, INPUT_PULLUP);
pinMode(ledOnboard, OUTPUT);
// LCD Initialization
lcd.init();
lcd.backlight();
// init the myEE class
myEE.init(maxCount, maxWriteCount);
// Display the previously stored count value
// If it had never been stored (blank EEPROM) it will display -0000001
lcd.setCursor(0, 0);
lcd.print("Count: ");
// EEPROM.get(countAddr, Count);
myEE.readActCount(); // ##############################
displayCount();
// the following only matters after a reboot, otherwise it will cause a
// needless EEPROM write if the count / set sw is in the "set" position on boot
// and neither up / down button is pressed (their normal resting state).
// The code considers that state to indicate user inactivity, which is
// when we write to EEPROM if the count changed and timer expired...
lastCount = Count;
lastStoredCount = Count;
Serial.begin(9600);
sensors.begin();
// set a suitably low resolution. Allegedly lower = faster
// setResolution(9) = 12 bits = about 0.5 degC
// I believe 9 is the default anyway so this is probably superfluous
sensors.setResolution(9);
// I think this is supposed to result in not holding things up
// it's not that important
sensors.setWaitForConversion(false);
/*
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
// consider using dtostrf() instead, as in printTemperature
// because I unnerstand it better
snprintf(countTxt, sizeof countTxt, "%08.6ld", Count);
lcd.setCursor(8, 0);
lcd.print(countTxt);
}
void getTemperature() { // Send temperature conversion request
sensors.requestTemperatures(); // this takes a few millis so do it when idle
// ******* if no actual temp sensor is present it takes way too much time so omit for now ********
// edit: fixed maybe, using sensors.setWaitForConversion(false); in setup() above
}
void printTemperature() {
float tempF = sensors.getTempFByIndex(0);
// now for the fun formatting part
dtostrf(tempF, 14, 1, lcdTemp); // width 14, 1 decimal
lcdTemp[14] = '\xDF'; // the degree symbol °
lcdTemp[15] = 'F'; // F for Fahrenheit
lcdTemp[16] = '\0'; // end of string (?)
lcd.setCursor(0, 1);
lcd.print(lcdTemp);
}
void EEPROMwriteAfterDelay() {
// If updateEEPROM flag has been set, wait a while (ten seconds) first,
// then do it, then indicate it's been done.
// Actions that change the Count value are responsible for restarting
// the idle timer: eepromTimer = millis() accomplishes that.
if (!updateEEPROM) return; // no need to, we outta here
if (Count == lastStoredCount) return; // don't write if it hasn't changed
if (millis() - eepromTimer < 10000) return; // value needs to be written, but not yet!
// EEPROM.put(countAddr, Count);
myEE.updateActCount(Count); // ##############################
lastStoredCount = Count;
Serial.print("Wrote to EEPROM, value: "); // everything will eventually be displayed on the LCD
Serial.println(Count); // so we can get rid of this later
updateEEPROM = false; // ok ok, get off my back
}
void loop() {
printActCount();
/*
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()) {}
jg1: I reverted to the below for debugging purposes
Besides, I'm not too concerned about that switch bouncing.
Nothing bad will happen if it does.
Extraneous counts, etc can't happen unless an up / down button is pressed
at the same time the switch moves, and that's unlikely given the hardware.
Tested in hardware... if you hold an up / down sw at the same time you move
the count on / off switch, the existing delta value remains the same,
which is pretty cool. I don't want to change that.
If you hold the prox switch down at the same time you move the count
on / off switch, exactly nothing happens. That's good.
counterOnState = digitalRead(counterOnPin); // get count on / set sw state
if (counterOnState) {
Counting();
} else {
notCounting();
}
jg1 note to self 2/07/2026: try implenting the getDebouncedState again
... I may have been mistaken about my reasons for reverting to digitalRead().
... 2/08/2026 tested ok. I think it's a better idea than the above. Mi gusta.
*/
if (CountSetBtn->getDebouncedState()) {
Counting();
} else {
notCounting();
}
EEPROMwriteAfterDelay(); // write to EEPROM but only after some idle time
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
// this is the most basic Arduino newbie code
// 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 currentSecs = currentMillis / 1000;
if (currentMillis - previousMillis >= blinkinterval) {
previousMillis = currentMillis;
toggleLED();
digitalWrite(ledOnboard, led0State);
if (led0State) {
Serial.print("Time ");
Serial.print(currentSecs);
Serial.print("\t\t");
Serial.print("millis ");
Serial.print(currentMillis);
Serial.println();
} else {
doNothing();
printTemperature(); // write temperature to LCD here - shouldn't take long
}
}
}
void Counting() { // meat of the program runs here.
// it is what counts the actual machine cycles
if (button[2].pressed()) { // magnetic prox switch sensed
Count++;
if (Count > 999999) { // wrap through zero
Count = 0;
}
displayCount();
if (lastCount != Count) {
updateEEPROM = true; // update needs doing
eepromTimer = millis(); // ... after some quiet time
}
lastCount = Count;
Serial.print("Prox sw detected, count to be written: ");
Serial.println(Count);
//
// additional code can be added here, such as reading the thermometer,
// or other time-consuming things that can occur between machine cycles.
//
getTemperature();
doNothing(); // it's what I do best
}
}
void notCounting() {
// This routine will be called upon only when count on / off sw is in the "set" position.
// It means the machine is not running production, and the operator either wants to turn
// the counter off for maintenance - or - to preset a new count value prior to resuming
// production on the machine.
// 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 up / down toggle switch is released, and a few seconds have transpired
// after releasing the up / down toggle switch (green / red buttons on the Wokwi)
if (Count != lastCount) { // LCD display the count value only if it has changed.
displayCount();
}
if (button[0].isReleased() && button[1].isReleased()) {
if (Count != lastCount) { // If the Count has changed,
updateEEPROM = true; // update needs doing
eepromTimer = millis(); // ... after some quiet time
lastCount = Count;
Serial.print("An up / down button was pressed and released, count to be written: ");
Serial.println(Count);
// additional code can be added here, such as reading the thermometer
// or other time-consuming things.
getTemperature();
doNothing(); // it's what I do best
}
}
if (button[0].down()) { // increment preset
Count += increment;
if (Count > 999999) { // wrap through zero
Count = 0;
}
// The following changes the increment (delta) depending
// on the duration the button is held down
increment = min(pow(2, button[0].getDownInterval() / 1000), maxDelta);
// The following resets the idle time to address the possibility
// a count up / down button is pressed and released then
// pressed again before the time expires.
// Otherwise the count would be written to EEPROM while still
// actively setting the counter, which is kind of needless.
eepromTimer = millis();
}
if (button[1].down()) { // decrement preset
Count -= decrement;
if (Count < 0) { // wrap through zero
Count = 999999;
}
// The following changes the decrement (delta) depending
// on the duration the button is held down
decrement = min(pow(2, button[1].getDownInterval() / 1000), maxDelta);
// The following resets the idle time to address the possibility
// a count up / down button is pressed and released then
// pressed again before the time expires.
// Otherwise the count would be written to EEPROM while still
// actively setting the counter, which is kind of needless.
eepromTimer = millis();
}
// The following lines reset the increment (delta) to 1
// when button[0] is not down
if (button[0].isReleased()) {
increment = 1;
}
// The following lines reset the decrement (delta) to 1
// when button[1] is not down
if (button[1].isReleased()) {
decrement = 1;
}
}
void printActCount(){
constexpr byte pin = 6;
static boolean first = true;
static unsigned long changeTime = 0;
static byte BtState;
static byte lastBtState = HIGH;
if (first){
first = false;
pinMode(pin, INPUT_PULLUP);
};
byte actBtState = digitalRead(pin);
if (actBtState != lastBtState){
lastBtState = actBtState;
changeTime = millis();
}
if (actBtState != BtState && millis()-changeTime >30){
BtState = actBtState;
if (!BtState){
Serial.print("EEPROM ActCount = ");
Serial.println(myEE.readActCount());
}
}
}
Up toggle sw
down toggle sw
Count on / set sw
prox sw
on
set