Debouncing buttons read with a PCF 8575

Good evening everyone,
I am progressing with my Arduino controlled dishwahser and I am now implementing more functions, such as a service mode.
I realized that it would be very useful to have a function I can call that will tell me whether a switch is pressed or not, detecting the falling edge. Problem is, these buttons are read via a PCF8575. I can take the reading and detect the falling edge, but I can't seem to transofrm this into a function.
This is the code for reading from the PCF:

void writePCF(byte byte1, byte byte2)
{
  Wire.beginTransmission(PCF_address);
  Wire.write(byte1);
  Wire.write(byte2);
  Wire.endTransmission();
}

byte readDataFromPCF() {
  writePCF(255, 255); // PCF8575 require us to set all outputs to 1 before doing a read.

  Wire.beginTransmission(0x20);
  Wire.requestFrom(0x20, 2); // Ask for 1 byte from slave
  byte upperByte = Wire.read(); // read that one byte
  byte lowerByte = Wire.read();
  Wire.endTransmission();

  return lowerByte;
}

What I'd like to do is have a function (eg. called isButtonPressed()) which takes the name of the button as an input (eg. isButtonPressed(PROG_SEL_BTN)) and returns true or false, but only the first time the button has been pressed. This means that if it gets held down the function will have to return true or false only once.
The way the buttons are wired now is DELAY_TIMER_BTN on PCF pin 0, PROG_SEL_BTN on PCF pin 1, START_BTN on PCF pin 2. Is it possible to do this? Many thanks in advance

I would set a flag when you determine it has detected the edge. In your code when you process that flag simply clear it. That way the reading of the switch is independent of the functions.

A function returns every time you call it. What value will it return when it isn't returning 'true' or 'false'?

Hi Mattia9914,

would you try this ?
(here a test ) :

.


//https://wokwi.com/projects/344622706469110354


// limitations:
// no debounce, but this should not be a problem
// in case of very short press, risk of confusion on the buttons (maybe it will be almost never seen) 
// only one button pressed at a time



#define DELAY_TIMER_BTN    B00000001                  // PCF pin 0  (write  B00000001 or 1)
#define PROG_SEL_BTN       B00000010                  // PCF pin 1  (write  B00000010 or 2)
#define START_BTN          B00000100                  // PCF pin 2  (write  B00000100 or 4)

unsigned long  prevmillis;                            // tempo
byte           dataFromPCF;                           // PCF result



void setup() {                                        // eventually...
  Serial.begin(115200); 
  Serial.println(F("go ! press a button"));  
}



void writePCF(byte byte1, byte byte2)                 // your code
{
  Wire.beginTransmission(PCF_address);
  Wire.write(byte1);
  Wire.write(byte2);
  Wire.endTransmission();
}



byte readDataFromPCF() {                              // your code
  writePCF(255, 255);                                 // PCF8575 require us to set all outputs to 1 before doing a read.

  Wire.beginTransmission(0x20);
  Wire.requestFrom(0x20, 2);                          // Ask for 1 byte from slave
  byte upperByte = Wire.read();                       // read that one byte
  byte lowerByte = Wire.read();
  Wire.endTransmission();

  //return lowerByte & B00000111;                     // PCF return positive logic (1 when pressed, pulldown on pin)
  return  ~lowerByte & B00000111;                     // PCF return negative logic (0 when pressed, pullup   on pin)
}




void function1(){                                     // here your function when DELAY_TIMER_BTN is pressed
  Serial.println(F("DELAY_TIMER_BTN just pressed"));  //
}                                                     //
void function2(){                                     // here your function when PROG_SEL_BTN is pressed
  Serial.println(F("PROG_SEL_BTN just pressed"));     //
}                                                     //
void function3(){                                     // here your function when START_BTN is pressed
  Serial.println(F("START_BTN just pressed"));        //
}                                                     //



void isButtonPressed() {
  if (millis() - prevmillis > 50) {                   // wait for 50 ms period
    prevmillis = millis();                            // 50ms reached

    dataFromPCF <<= 1;                                // left shift
    dataFromPCF += readDataFromPCF();                 // add button status

    if (dataFromPCF == DELAY_TIMER_BTN) function1();  // 
    if (dataFromPCF == PROG_SEL_BTN)    function2();  //
    if (dataFromPCF == START_BTN)       function3();  //
  }
}



void loop() {
  isButtonPressed();
  //...
  
}
1 Like

I tried your wokwi, thanks for troubling yourself to make it!

The sketch works fine.

The code you posted isn't in the wokwi exsactly, at the very least I saw

    if (millis() - prevmillis > 1) {                    // wait for 50 ms period

TBH I have spent the last - never mind precisely how many - minutes trying to figure out how your *isButtonPressed*() function works.

I did finally get it, but only by looking at it on my hands and knees. I think it was just like nothing I had seen, or more accurately enough like something else entirely to make it hard to see in another way.

I wrote a drop-in that does the same thing. It uses logic to see which bits have transitioned; the function dispatch tests the relevant bit. It can be easily enhanced to handle multiple simultaneous button presses as well. Exercise for you with anywhere near as little life as I. :wink:


void isButtonPressed() {
  if (millis() - prevmillis < 50)
    return;                                         // too soon to even look again

  prevmillis = millis();                            // 50ms reached

  unsigned char newData = readDataFromPCF();

  unsigned char actOn = newData & ~dataFromPCF;     // calculate transitioning bits

  dataFromPCF = newData;                            // bump along the history

  if (actOn & DELAY_TIMER_BTN) function1();
  if (actOn & PROG_SEL_BTN)    function2();
  if (actOn & START_BTN)       function3();
}

Adding:

I finally read this at the top of the code. Usually I skip reading commentary.

// limitations:
// no debounce, but this should not be a problem
// in case of very short press, risk of confusion on the buttons (maybe it will be almost never seen) 
// only one button pressed at a time

I found it quite easy to get "button confusion" using plausible short presses. Not a debouncing issue, not sure how to catch it in the act. The algorithm I supplied has debounce built in, as does @PatMax's, but mine will never jump channels.

a7

I would like to thank everybody for helping me
Since I also needed to read multiple buttons at the same time and increase values when a button was held down for long enough, I write the following code, which works just fine for my needs



// constants won't change. They're used here to set pin numbers:
const int BUTTON_PIN = 7; // the number of the pushbutton pin
const int DEBOUNCE_TIME = 100;
const int MAX_SHORT_PRESS_TIME = 1500; // 1500 milliseconds
const int LONG_PRESS_TIME = 3000;
const int OFFSET = 7; //defines offset between position 0 of the array and the first button number (eg. here pin 7 is the first button --> offset = 7-0=7)

int lastPressedState[3] = {0, 0, 0}; // the previous state from the input pin
int currentPressedState[3];     // the current reading from the input pin
unsigned long pressedTime[3] = {0, 0, 0};
bool isPressing[3] = {0, 0, 0};
bool isLongPressed[3] = {0, 0, 0};
unsigned long releasedTime [3];

int lastHeldState[3] = {0, 0, 0};  // the previous state from the input pin
int currentHeldState[3];     // the current reading from the input pin
unsigned long pressedHeldTime[3] = {0, 0, 0};
bool isHolding[3] = {0, 0, 0};
bool isLongDetected[3] = {0, 0, 0};

void setup() {
  Serial.begin(9600);
  pinMode(7, INPUT_PULLUP);
  pinMode(8, INPUT_PULLUP);
}

void loop() {

  if (isButtonPressed(7)) {
    Serial.println("SHORT PRESS 7");
  }

  if (isButtonPressed(8)) {
    Serial.println("SHORT PRESS 8");
  }

  if (isButtonHeld(7)) {
    Serial.println("LONG PRESS 7");
  }

  if (isButtonHeld(8)) {
    Serial.println("LONG PRESS 8");
  }


  if (multipleButtonsHeld(7, 8)) {
    Serial.println("LONG PRESS BOTH");
  }
  
    if (increaseWhileHolding(7)) {
      Serial.println("Increasing");
    }
}

bool isButtonPressed(byte pin) {
  // read the state of the switch/button:
  currentPressedState[pin - OFFSET] = digitalRead(pin);

  if (lastPressedState[pin - OFFSET] == HIGH && currentPressedState[pin - OFFSET] == LOW) {
    pressedTime[pin - OFFSET] = millis();// button is pressed
  } else if (lastPressedState[pin - OFFSET] == LOW && currentPressedState[pin - OFFSET] == HIGH) { // button is released
    releasedTime[pin - OFFSET] = millis();

    long pressDuration[3];
    pressDuration[pin - OFFSET] = releasedTime[pin - OFFSET] - pressedTime[pin - OFFSET];

    if (pressDuration[pin - OFFSET] > DEBOUNCE_TIME && pressDuration[pin - OFFSET] < MAX_SHORT_PRESS_TIME) {
      lastPressedState[pin - OFFSET] = currentPressedState[pin - OFFSET];
      return true;
    }

  }

  // save the the last state
  lastPressedState[pin - OFFSET] = currentPressedState[pin - OFFSET];
  return false;
}



bool isButtonHeld(byte pin) {

  currentHeldState[pin - OFFSET] = digitalRead(pin);

  if (lastHeldState[pin - OFFSET] == HIGH && currentHeldState[pin - OFFSET] == LOW) {   // button is pressed
    pressedHeldTime[pin - OFFSET] = millis();
    isHolding[pin - OFFSET] = true;
    isLongDetected[pin - OFFSET] = false;
  } else if (lastHeldState[pin - OFFSET] == LOW && currentHeldState[pin - OFFSET] == HIGH) { // button is released
    isHolding[pin - OFFSET] = false;
  }

  if (isHolding[pin - OFFSET] == true && isLongDetected[pin - OFFSET] == false) {
    long pressDuration[3];
    pressDuration[pin - OFFSET] = millis() - pressedHeldTime[pin - OFFSET];

    if (pressDuration[pin - OFFSET] > LONG_PRESS_TIME ) {
      isLongDetected[pin - OFFSET] = true;
      lastHeldState[pin - OFFSET] = currentHeldState[pin - OFFSET];
      return true;
    }
  }

  // save the the last state
  lastHeldState[pin - OFFSET] = currentHeldState[pin - OFFSET];
  return false;
}


long buttonTimer = 0;
long buttonTime = 1000;

boolean buttonActive = false;
boolean longPressActive = false;

boolean button1Active = false;
boolean button2Active = false;

bool multipleButtonsHeld(byte button1, byte button2) {

  if (digitalRead(button1) == LOW && button1Active == false) {
    buttonTimer = millis();
    button1Active = true;
  }

  if (digitalRead(button2) == LOW && button2Active == false) {
    buttonTimer = millis();
    button2Active = true;
  }

  if (button1Active == true && button2Active == true && (millis() - buttonTimer > buttonTime) && (longPressActive == false)) {
    longPressActive = true;
    return true;
  }

  if ((digitalRead(button1) == HIGH) && (digitalRead(button2) == HIGH)) {
    longPressActive = false;
    button1Active = false;
    button2Active = false;

  }

  return false;
}

int lastStateIncrease = LOW;  // the previous state from the input pin
int currentStateIncrease;     // the current reading from the input pin
unsigned long pressedTimeIncrease  = 0;
bool isPressingIncrease = false;
bool isLongDetectedIncrease = false;
int K = 0;

bool increaseWhileHolding(byte button) {
  currentStateIncrease = digitalRead(button);

  if (lastStateIncrease == HIGH && currentStateIncrease == LOW) {       // button is pressed
    pressedTimeIncrease = millis();
    isPressingIncrease = true;
    isLongDetectedIncrease = false;
  } else if (lastStateIncrease == LOW && currentStateIncrease == HIGH) { // button is released
    isPressingIncrease = false;
    K = 0;
  }

  if (isPressingIncrease == true) {
    long pressDuration = millis() - pressedTimeIncrease;

    if (pressDuration > LONG_PRESS_TIME + K * 500) {
      K++;
      isLongDetectedIncrease = true;
      lastStateIncrease = currentStateIncrease;
      return true;
    }
  }

  // save the the last state
  lastStateIncrease = currentStateIncrease;
  return false;
}

Great, Mattia9914 !

Hi gilshultz, johnwasser

hi alto777
I know your passion for keyboard and button algorithms, I too have a lot of fun with it (my codes come from the time when microcontrollers had 16 RAM registers maximum, and we had to make short code; now we have all the space we want, but as these codes work well, I keep using them).

In WOKWI, the tempo delays (including millis() ) are not respected, that's why I modified the tempo value in the WOKWI code:

if (millis() - prevmillis > 50) { // for real UNO, 50 ms delay
if (millis() - prevmillis > 1) { // for WOKWI unknown delay, but must be short

My algorithm with shifting variable was an exercise in style, I made it from memory and as simple as possible, so that it is easy to read, but it has limitations.

OK! good exercise for me! I will give you the result of my improvement in a moment, still with Wokwi.

Imma ask you to prove that. It would be a surprise as well as making wokwi almost entirely useless.

I have found the wokwi to be 99.44 percent faithful; the few problems that I have even seen we're when dealing at a low level with the CPU registers, sophisticated timer/counter stuff for example.

And those few were fixed rapidly by Urish and/or his boys over there. :wink:

A real infidelity is how LEDs look and act. This is just a limitation of the screen drawing and timing of all that.

Which is why I stuck a servo into an LED dimming demo recently - you can see the servo go back and forth, but the supposed dimming and brightening of the LED was poorly simulate.

wokwi has been a real game-changer, now that much easier to get most things mostly working before ever introducing the inevitable excitement of actual hardware and wiring.

a7

Okay, you tell me that the timing of wokwi is accurate, I thought the opposite based on the reaction time of the leds (sometimes, on some sketch it is very slow). In my example below, a short press on Start button does not always light up the red led, while the code has no error.
I have only done 2 sketches.

Here is the third one, 8 or 16 buttons

Nice work. And wokwi. I like the bit stuff.

I found your example above to be sturdy, at least just now I cannot make it do anything wrong. I did use very short "presses". I suggest you try

  if (millis() - prevmillis < 200) return;             // sluggish too soon to even look again

to really slug those buttons, so you can def miss short presses (< 200 ms). At 20 ms, it is harder I never did make a simulated button press too short (< 20 ms), but I can see the IRL it might be too short - short stabs at a button might well just ignored except to reset the "don't look" timer mechanism.

I did move one of your calculations, then wondered why actOff would't just be symmetrical ?


  actOn   =  newData & ~prevData;                     // calculate PRESSED  transitioning bits
//  actOff  = (newData ^  prevData) & prevData;         // calculate RELEASED transitioning bits

  actOff = ~newData & prevData;        // calculate RELEASED transitioning bits

Same program operation.

a7

Yes, my example is solid, it has been implemented on thousands of devices in the last decades, if it had a defect, I would have had serious problems :wink:

You are right to increase the period from 20 to 200, but this is only valid for wokwi; I think especially IRL, with a physical processor, 20 ms is the best compromise between key reaction speed and anti-bounce. At this speed, stabbing the keys is possible, even better if you are at 10 ms.
The strongest bounces I know of are made with a bare wire rubbed on a metal part: you have to go up to 50 ms to erase the bounces, in this case.

Bravo for your remark on ActOff symmetrical of actOn! you are right, it simplifies the code, it's very clever! I will use it from now on, thanks!

Sry, I meant temporarily make it sluggish so you actually can see a missed short stab of the button.

By making a gianter window for such to occur within. Simp,y as an experiment. Way too long. I use 20-50, only my cheap arcade switches need even 20 ms to finish.

And wokwi can be slow in its tracking of the passage of real time, that would make it seem unfaithful.

Slow on a tablet. I usually see 99 or 100 percent real time performance on my desktop machine.

Many times I slow things way down and watch how they actually work even my own code will surprise me. :wink:

a7

Edit: the code has been replaced with code that works. Better. :expressionless: TBC the code I originally quoted below had a few, um, flaws. The umbrella academy caught them immediately.

OK before my beach buddy - she who must never be kept waiting - gets here to, well, you know, I have been flying about @PatMax's latest sketch at a low level.

The current isButtonPressed() takes two different ideas of what pressed (or released) means.

actOn, actOff and actTog all react immediately to any change observed on the pin. This will catch glitches, although in the case of good pushbuttons, there is no such thing as a glitch: switches do not close capriciously unless on the way to being fully closed, neither do they open without being on the way to being fully opened.

So it is actually good, in that case, to react immediealt.

But intordiced latest is the idea of a stable state (I think), wherein a pin is not considered to be HIGH until it has been HIGH for two readings.

I missed the symmetry of handling only LOW if two consecutive LOW readings come in.

The new "stable" pin readings can be used in the same manner as the "raw" readings newData and prevData, only this time with the extra fussiness of needing two in a row to trigger.

Here is the whole sketch where I leave it, soon, I hope, for the beach. :wink:

I put a huge delay() in the loop() to make it easy to see the quick response on short presses, and the verified fussier repsonse that you gotta hold down a button for. Watch it on the DELAY_TIMER_BTN button.


// BUTTONS : 1 to 16 
// reliable debounce
// buttons info : state, fronts down & up, toggle
// multiple buttons can be pressed at a time

#define DELAY_TIMER_BTN    B00000001                  // Button on A0
#define PROG_SEL_BTN       B00000010                  // Button on A1 
#define START_BTN          B00000100                  // Button on A2

unsigned long  prevmillis;                            // for debounce period
unsigned char  prevData;                              // previous reading of buttons
unsigned char  prevSta;                               // Button previous state 
unsigned char  actSta;                                // Button state (pressed, no pressed)
unsigned char  actTog;                                // Toggle
unsigned char  actOn;                                 // down front
unsigned char  actOff;                                // up front


void setup() {                                        // pullups on pins A0,A1,A2  
  PORTC |= B00000111;                                 // A3, A4, A5 as output
  DDRC  |= B00111000;
  Serial.begin(115200);                               //
  Serial.println(F("go ! press a button"));           //
}                                                     //

void isButtonPressed() {

  static unsigned int counter;
//  Serial.print(counter); Serial.print(" ");
  counter++;

  unsigned long now = millis();                       // or use global "now"

  if (now - prevmillis < 25) return;                  // too soon to even look again

  prevmillis = now;                                   // 25 ms debounce delay reached
  unsigned char newData = ~PINC & B00000111;          // 0 when pressed, internal pullup on pins A0, A1, A2

// this section reacts immediatley to a new different reading

  actOn   =  newData & ~prevData;                     // calculate PRESSED  transitioning bits
  actOff  = ~newData & prevData;

  actTog ^= actOn;                                    // apply TOGGLE 

  if (actOn & DELAY_TIMER_BTN) {Serial.print(counter); Serial.println(F(" DELAY pressed ")); }
  if (actOn & PROG_SEL_BTN)    {Serial.print(counter); Serial.println(F(" PROG pressed ")); }
  if (actOn & START_BTN)       {Serial.print(counter); Serial.println(F(" START pressed ")); }
  
  if (actOff & DELAY_TIMER_BTN){Serial.print(counter); Serial.println(F(" released DELAY")); }
  if (actOff & PROG_SEL_BTN)   {Serial.print(counter); Serial.println(F(" released PROG")); }
  if (actOff & START_BTN)      {Serial.print(counter); Serial.println(F(" released START")); }

  digitalWrite(A5, actTog & START_BTN);                // display red led toggle START/STOP 

// this section below develops the idea of a stable press (same for 2 readings)

  unsigned char staBits = ~(newData ^ prevData);

  actSta &= ~staBits;                                  // knock out the old stable bit readings
  actSta |= newData & staBits;                         // and jam the stable readings in there

  digitalWrite(A4, actSta & (PROG_SEL_BTN | DELAY_TIMER_BTN));  // one or both, green led ON 

// the stable readings could also be used with for a fussier actOn / actOff / actTog
// by basing them on stable state readings

  unsigned char fussyActOn  = actSta & ~prevSta;
  unsigned char fussyActOff = ~actSta & prevSta;

  if (fussyActOn & DELAY_TIMER_BTN)      {Serial.print(counter); Serial.println(F(" verified fussy DELAY"));}
  if (fussyActOff & DELAY_TIMER_BTN)      {Serial.print(counter); Serial.println(F(" released fussy DELAY"));}

// always...
  prevData = newData;                                 // bump along the history
  prevSta = actSta;                                   // bump along the history
}

void loop() {                                         //
  isButtonPressed();                                  // 
  delay(250); 
}                                                     //

Of course I did:

a7

1 Like

Veryyyyyyyyyy good idea! The beach inspires you! (or the buddy?) I continue to work on this idea (you have bluffed me, you have simplified the determination of ActOff and ActTog, I had not seen this possibility, bravo

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