Why delay in loop? - working program to map switches to keyboard strokes

With the help of some examples, I wrote a program to map switches to keyboard strokes.

It's aimed for an arduino micro that has five footswitches connected to the pins 2 to 6. The program works, but I need a delay in the loop otherwise keystrokes are triggered more then once. I tested how low can I set that delay and found 12 as working value. While the program does work, I still wonder, why I need that delay.

// Including the keyboard library
#include <Keyboard.h>
// Including the mouse library
#include <Mouse.h>                                  

// The Values for my Keys
const char keyValueA = 'w';
const char keyValueB = 's';
const char keyValueC = 'q';
const char keyValueD = 'e';
const char keyValueE = KEY_SCROLL_LOCK;

// Time for delay. The script needs a delay, otherwise it triggers over
// and over again. Values that worked:
// 50 - OK
// 40 - OK
// 30 - OK
// 20 - OK
// 12 - OK
// 11 - triggers double extremly rarely
// 10 - triggers double rarely
//  9 - triggers double sometimes
//  8 - triggers double often
//  7 - triggers double
//  6 - triggers double
//  5 - triggers double
const int delayValue = 12;

// Declaring variables for the pins
const int pinA = 2;
const int pinB = 3;
const int pinC = 4;
const int pinD = 5;
const int pinE = 6;

// Variables for the previous status of the switch
int previousSwitchStateA = HIGH; 
int previousSwitchStateB = HIGH;
int previousSwitchStateC = HIGH;
int previousSwitchStateD = HIGH;
int previousSwitchStateE = HIGH;

void setup()
{
  // Setting up the internal pull-ups resistors and also setting the 
  // pins to inputs.
  pinMode(pinA, INPUT_PULLUP);
  pinMode(pinB, INPUT_PULLUP);                        
  pinMode(pinC, INPUT_PULLUP);
  pinMode(pinD, INPUT_PULLUP);
  pinMode(pinE, INPUT_PULLUP);
  // as seen in this example:
  // https://www.instructables.com/Arduino-Programmable-Button-Panel-As-Keyboard/
  digitalWrite(pinA, HIGH);
  digitalWrite(pinB, HIGH);
  digitalWrite(pinC, HIGH);
  digitalWrite(pinD, HIGH);
  digitalWrite(pinE, HIGH);
  // start the keyboard
  Keyboard.begin();
}

// the permanent running loop
void loop()
{
  //checking the states of the switches
  int switchStateA = digitalRead(pinA);
  int switchStateB = digitalRead(pinB);
  int switchStateC = digitalRead(pinC);
  int switchStateD = digitalRead(pinD);
  int switchStateE = digitalRead(pinE);

  // Checking if the first switch has been pressed and the variable for a
  // pressed key not already set
  if ( (switchStateA == LOW) && (switchStateA != previousSwitchStateA) )
  {
    // press the key
    Keyboard.press(keyValueA);
    delay(delayValue);
  }

  if ( (switchStateA == HIGH) && (switchStateA != previousSwitchStateA) )
  {
    // release the key
    Keyboard.release(keyValueA);
    delay(delayValue);
  }

  // Checking if the second switch has been pressed and the variable for a
  // pressed key not already set
  if ( (switchStateB == LOW) && (switchStateB != previousSwitchStateB) )
  {
    // and it's currently pressed:
    Keyboard.press(keyValueB);
    delay(delayValue);
  }

  if ( (switchStateB == HIGH) && (switchStateB != previousSwitchStateB) )
  {
    // and it's currently released:
    Keyboard.release(keyValueB);
    delay(delayValue);
  }

  // Checking if the third switch has been pressed and the variable for a
  // pressed key not already set
  if ( (switchStateC == LOW) && (switchStateC != previousSwitchStateC) )
  {
    // and it's currently pressed:
    Keyboard.press(keyValueC);
    delay(delayValue);
  }

  if ( (switchStateC == HIGH) && (switchStateC != previousSwitchStateC) )
  {
    // and it's currently released:
    Keyboard.release(keyValueC);
    delay(delayValue);
  }

  // Checking if the fourth switch has been pressed and the variable for a
  // pressed key not already set
  if ( (switchStateD == LOW) && (switchStateD != previousSwitchStateD) )
  {
    // and it's currently pressed:
    Keyboard.press(keyValueD);
    delay(delayValue);
  }

  if ( (switchStateD == HIGH) && (switchStateD != previousSwitchStateD) )
  {
    // and it's currently released:
    Keyboard.release(keyValueD);
    delay(delayValue);
  }

  // Checking if the fifth switch has been pressed and the variable for a
  // pressed key not already set
  if ( (switchStateE == LOW) && (switchStateE != previousSwitchStateE) )
  {
    // and it's currently pressed:
    Keyboard.press(keyValueE);
    delay(delayValue);
  }

  if ( (switchStateE == HIGH) && (switchStateE != previousSwitchStateE) )
  {
    // and it's currently released:
    Keyboard.release(keyValueE);
    delay(delayValue);
  }

  // save the states
  previousSwitchStateA = switchStateA;
  previousSwitchStateB = switchStateB;
  previousSwitchStateC = switchStateC;
  previousSwitchStateD = switchStateD;        
  previousSwitchStateE = switchStateE;
}

short answer - mechanical switches 'bounce', so will deliver multiple closures when you operate them once. What you've discovered is the cheapest, but least reliable, way to debounce a switch.

2 Likes

Ah, that makes sense. The switches are way over specced with 250V by 15A but bouncing seems to be an issue.

Note that, as your switches age, there is zero guarantee they won't bounce more, or less, with more, or less, predictability. So a fixed delay hard-coded in your software is a 'for now' solution, guaranteed to fail you when you aren't expecting it.

1 Like

I know I can have trust in murphy.

At the moment the switches will be used for gaming. To move your character in an MMORPG, steer a ship or a tank. So that is not a critical problem.

You could try it like this?

Debounced and everything.

This does NOT use hardcoded delay() for debouncing. It actually uses a digital filter to sense when the button stops bouncing. And is totally non-blocking to boot.

3 Likes

That sound like a damn good idea! All that magic happens by mechButton.h

I will give it later a try!

It works like a charm!

Thank you so much for your library and great example.

// A lot of mechanical buttons have the problem that they bounce. You can
// work around that with delay settings, to ignore the later bouncing 
// switch impulses. But it is problematic as switches age and a value that 
// worked fine might not do after some months or years. On the other hand, 
// you want to keep the value as low as possible.
//
// Thankfully there is a better way:
//
// The mechButton class, which is part of the LC_baseTools library.
// "This does NOT use hardcoded delay() for debouncing. It actually uses
//  a digital filter to sense when the button stops bouncing. And is 
//  totally non-blocking to boot." by jimLee
//
// Example:
// https://wokwi.com/projects/391133606159541249
// This Sketch uses a lot from the example. Thanks a lot to jimLee
// for his great library and example!
//
// You have to install the LC_baseTools library into your Arduino IDE to
// use the mechButton class.
//
// Including the mechButton library with a Class that creates a 
// debounced button.
#include <mechButton.h>

// Including the keyboard library for keyboard key defines
#include <Keyboard.h>

// Including the mouse library for mouse action defines
#include <Mouse.h>  

// Setting defines for the used pins to make it better readable
#define PIN_A	2
#define PIN_B	3
#define PIN_C	4
#define PIN_D	5
#define PIN_E	6

// Set define to true to enable debug
#define DEBUG_VALUE false

// The Values for my Keys
const char keyValueA = 'w';
const char keyValueB = 's';
const char keyValueC = 'q';
const char keyValueD = 'e';
const char keyValueE = KEY_SCROLL_LOCK;

// create the mechButtons objects for the callbacks later
mechButton aButton(PIN_A);
mechButton bButton(PIN_B);
mechButton cButton(PIN_C);
mechButton dButton(PIN_D);
mechButton eButton(PIN_E);


// Your standard sketch setup()
void setup()
{
  // start the serial should debug is enabled
  if(DEBUG_VALUE == true)
  {
    Serial.begin(9600); 
  }

  // Set up our callback. (Also calls hookup() for idling.)
  aButton.setCallback(aCallback);
  bButton.setCallback(bCallback);
  cButton.setCallback(cCallback);
  dButton.setCallback(dCallback);
  eButton.setCallback(eCallback);
}


// This is the function that's called when the A button changes state.
void aCallback(void)
{
  if(DEBUG_VALUE == true)
  {
    Serial.print("A Button just became ");
  }

  if (aButton.getState())
  {
    if(DEBUG_VALUE == true)
    {
      Serial.println("true!");
    }
    // released the pressed key
    Keyboard.release(keyValueA);
  }
  else
  {
    if(DEBUG_VALUE == true)
    {
      Serial.println("false!");
    }
    // press the key
    Keyboard.press(keyValueA);
  }
}


// This is the function that's called when the B button changes state.
void bCallback(void)
{
  if(DEBUG_VALUE == true)
  {
    Serial.print("B Button just became ");
  }

  if (bButton.getState())
  {
    if(DEBUG_VALUE == true)
    {
      Serial.println("true!");
    }
    // released the pressed key
    Keyboard.release(keyValueB);
  }
  else
  {
    if(DEBUG_VALUE == true)
    {
      Serial.println("false!");
    }
    // press the key
    Keyboard.press(keyValueB);
  }
}


// This is the function that's called when the C button changes state.
void cCallback(void)
{
  if(DEBUG_VALUE == true)
  {
    Serial.print("C Button just became ");
  }

  if (cButton.getState())
  {
    if(DEBUG_VALUE == true)
    {
      Serial.println("true!");
    }
    // released the pressed key
    Keyboard.release(keyValueC);
  }
  else
  {
    if(DEBUG_VALUE == true)
    {
      Serial.println("false!");
    }
    // press the key
    Keyboard.press(keyValueC);
  }
}


// This is the function that's called when the D button changes state.
void dCallback(void)
{
  if(DEBUG_VALUE == true)
  {
    Serial.print("D Button just became ");
  }

  if (dButton.getState())
  {
    if(DEBUG_VALUE == true)
    {
      Serial.println("true!");
    }
    // released the pressed key
    Keyboard.release(keyValueD);
  }
  else
  {
    if(DEBUG_VALUE == true)
    {
      Serial.println("false!");
    }
    // press the key
    Keyboard.press(keyValueD);
  }
}


// This is the function that's called when the E button changes state.
void eCallback(void)
{
  if(DEBUG_VALUE == true)
  {
    Serial.print("E Button just became ");
  }

  if (eButton.getState())
  {
    if(DEBUG_VALUE == true)
    {
      Serial.println("true!");
    }
    // released the pressed key
    Keyboard.release(keyValueE);
  }
  else
  {
    if(DEBUG_VALUE == true)
    {
      Serial.println("false!");
    }
    // press the key
    Keyboard.press(keyValueE);
  }
}


// the permanent running loop
void loop()
{
  // Let all the idlers have time to do their thing.  
  idle();
}

1 Like

Well, thank you!

Glad it worked!

-jim lee

1 Like

@jimLee

I tried to understand how your mechButton.h-library works.

hm .... ---- .... "a digital filter to sense when the button stops bouncing"

I looked into your libraries code but it is above my head. So please as a first step can you explain in normal words what in principle is your digital filter doing?

What I am assuming is: measuring time between LOW/HIGH-changes and if the time-difference becomes smaller always only wait for this smaller amount of time until reading the IO-pin again.

This assummes that the time-differencies between changes of the logic level between LOW/HIGH become smaller and smaller. The "B"'s shall symbolise the Bouncing

LOW/HIGH , HIGH/LOW-changes
The below is a timeline where each dot symbolises a small amout of time that has passed by

.......B...................................B....................B..........B......B..B............

Additional question if my assumption is correct:
with how many milliseconds do you start ?

Have you ever encountered a switch that shows a different behaviour
where times between bounces look like this:
.......B...B.......B.........................B.B...................B..........B......B..B............

Which means the time-differencies decrease and increase with the result that the approach like described above does not work.

best regards Stefan

It's a method I was taught by an old ASIC programmer back in the day. Really simple.

When the button gets it's first wakeup call {idle();} it reads it's input pin and saves that as the initial condition of the saved state.

From then on every time it gets an idle call, it reads the pin and compares it to the saved state.

If it is different it bumps up the "change" counter. If the counter is greater than a set value X. It swaps the saved state, zeros the change counter and calls the callback, if there is one.

If it is the same as the saved state, it zeros out the "change" counter and exits.

Basically it needs to see x readings in a row different than the saved state before calls it it a "real" change of state.

If the switch is cheap and very bouncy, just pump up the value for X. But I've never run across this. Mainly because when I code, EVERYTHING runs on the idle loop. And the more you have running on there, the longer time passes between state checks.

Make sense?

-jim lee

P.S. Looked back at the code. It's been awhile since I been in there.

The default number of conflicting readings in a row is 4.
There is also a 1ms timer running that won't allow readings to occur faster that 1ms per reading.

1 Like

Hi @jimLee

thank you for answering. I am sorry I do not really understand it yet

I thought explained in normal words I would understand it.
Though I discovered it would have to be with much much more details to make it really understandable

only half way.

SO I try to explain what I assume how it works with using variables and mentioning what happens in setup(), what happends in loop() etc.

You have a function that must be called as often as possible in function loop()

This function is named "idle()"

Inside setup() the first an initial call of function idle() is made.
as an example let's say the input-pin with the bouncy button is configured as INPUT_PULLUP and is connected to ground.

When the initial call to idle() happens in function setup()
let's assume the button is unpressed which means the IO-pin reads a HIGH

not sure what you mean with "condition" here.
To me a condition is something like

if (myValue == 10) {
}

right now I assume that you mean save the logic level that the IO-pin detects

When the initial call to idle() happens in function setup()
let's assume the button is unpressed which means the IO-pin reads a HIGH

in this case this means

myInitial_IO_Pin_State = HIGH;

with each successive call of function idle() the actual state is read in again

myActual_IO_PinState = digitalRead(myButtonPin);
if (myActual_IO_PinState != myInitial_IO_Pin_State) {
  my_X_Counter++; 
  if (my_X_Counter > 10) {
    myInitial_IO_Pin_State = myActual_IO_PinState ;
    // call callbackfunction
  }
}
if (myActual_IO_PinState != myInitial_IO_Pin_State) {
  my_X_Counter++; 
  if (my_X_Counter > 10) {
    myInitial_IO_Pin_State = myActual_IO_PinState ;
    // call callbackfunction
  }
}
else {
  my_X_Counter = 0;
  // exiting WHAT?
}

what do you mean with "before calls" ??
related to my example-code: Does the "saved state" mean the variable

myActual_IO_PinState 

?
or the variable

myInitial_IO_Pin_State 

?

There is a "saved state" for which to me it is unclear at what time it point is it saved?
and there are following up readings

So how many variables is this using?

If you could write down an example not quoting your full code
but instead much much simpler very similar to what I have written above
I hope that then I will be able to understand it

best regards Stefan

I'm not sure how to answer you.

in setup() the only thing we call is setCallback(). This is optional call and basically tells the object that you want automatic running using the idle() call, and to use the callback you passed in as your result of a detected pin change.

False. idle() is typically NOT called in setup(). The only initialization in setup() is the adding of an optional callback function.

The remainder of initialization is done the first time getState() is called.

What you have here is pretty close to my getState() call. Missing the initialization block and the timer.

It should be

 if (myActual_IO_PinState != mySaved_IO_PinState) {
}

-jim

1 Like

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