Debouncing trouble of switches connected to four MCP23017s

Status
Not open for further replies.

sixeight

Well-known member
I am struggling to get my foot switches working properly on my MIDI foot controller. I find that I am changing the debouncing time all the time, but the debouncing is not working steadily when the loop takes slightly too long. I would like somebody to look at my code and see if I am missing something, or if I can simplify things.

Here is my setup:
I have four display boards with three displays and four switches connected to one MCP23017 on each board. The boards are connected over i2c with 5k pull-up resistors. I also have a shared INTA_PIN connected from each MCP23017 through a diode to pin 2 on the Teensy. For this pin I use an internal pullup resistor of the Teensy. The displays are working perfectly, but the switches give me trouble all the time.

I am struggling to keep the loop time down as my project has 13 displays controlled over i2c, 12 neopixel LEDs, 3 serial midi ports, usbmidi and advanced support for many midi devices. Therefore a loop can take up to 100 ms, which then gives me trouble in debouncing the switches.

Here is how the code is working. I am reading two registers from the MCP23017: INTCAPA and GPIOA. The first will give the state at the time of interrupt, the second will give the current state. I have to read both, otherwise I am really missing switch presses or releases. I want to keep the number of i2c reads to a minimum, as they take up a lot of time.

The order of events is:
* Check INTA_PIN is low (MCP23017 is triggered by a button state change)
* Read INTCAPA of the current board
* Check if it clears the interrupt or select the next board. This board will be checked on the next loop cycle.
* If it clears the interrupt set switch_triggered or switch_released and start debouncing
* When debounce timer expires and INTA_PIN is still high check GPIOA to check if we have not missed a release. Update switch_pressed and switch_released accordingly

Here is the part of my code that does the debouncing of displays:
Code:
#include <i2c_t3.h>
#include <LiquidCrystal.h>
#include "lcd_lib.h"

#define INTA_PIN 2 // Digital Pin 2 of the Teensy is connected to INTA of the MCP23017 on the display boards

#define NUMBER_OF_DISPLAY_BOARDS 4
LiquidCrystal_MCP23017 lcd[NUMBER_OF_DISPLAY_BOARDS] = {
  LiquidCrystal_MCP23017 (0x20, DISPLAY1),
  LiquidCrystal_MCP23017 (0x21, DISPLAY1),
  LiquidCrystal_MCP23017 (0x22, DISPLAY1),
  LiquidCrystal_MCP23017 (0x23, DISPLAY1),
};

uint8_t switch_pressed = 0; //Variable set when switch is pressed
uint8_t switch_released = 0; //Variable set when switch is released
uint8_t previous_switch_pressed = 255;

#define DEBOUNCE_TIME 50 // Debounce time for display boards
uint32_t Debounce_timer = 0;
bool Debounce_timer_active = false;

uint8_t Current_board = 0; // The current display board that is read for switches pressed
uint32_t time_switch_pressed;

void inta_pin_interrupt() {
  time_switch_pressed = micros(); // Store the time switch was pressed. It will be used for tap tempo if the switch is programmed that way.
}

#define SERIAL_STARTUP_TIME 3000 // Will wait max three seconds max before serial starts
uint32_t serial_timer;

void setup() {
  // put your setup code here, to run once:
  pinMode(INTA_PIN, INPUT_PULLUP);
  attachInterrupt(INTA_PIN, inta_pin_interrupt, FALLING);

  Serial.begin(115200);
  serial_timer = millis();
  while ((!Serial) && (serial_timer - millis() < SERIAL_STARTUP_TIME)) {}; // Wait while the serial communication is not ready or while the SERIAL_START_UP time has not elapsed.
  Serial.println("Debugging started...");

  for (uint8_t i = 0; i < NUMBER_OF_DISPLAY_BOARDS; i++) {
    lcd[i].begin (16, 2);
  }
}

void loop() {
  // Reset switch variables
  switch_pressed = 0;
  switch_released = 0;

  if (!Debounce_timer_active) {
    if (digitalRead(INTA_PIN) == LOW) {
      check_switches_on_current_board(true); // Read the state as it was when the interrupt was triggered
      if (digitalRead(INTA_PIN) == LOW) { // Check if INT_A pin is still LOW
        // we did not find the source of the interrupt and we will need to read the next board
        Select_next_board();
      }
      else {
        // Start timer
        Debounce_timer = millis();
        Debounce_timer_active = true;
      }
    }
  }
  else if (millis() - Debounce_timer > DEBOUNCE_TIME) { // Check if debounce timer expires
    check_switches_on_current_board(false); //read the current state
    Debounce_timer_active = false;
  }

  if (switch_released > 0) {
    Serial.println("Switch released: " + String(switch_released));
  }

  // Now check for Long pressing a button
  if (switch_pressed > 0) {
    Serial.println("Switch pressed: " + String(switch_pressed));
  }

  // Now simulate the rest of the loop
  delay(100);
}

void check_switches_on_current_board(bool check_interrupt_state) {
  uint8_t new_switch_pressed = 0;
  bool updated;

  // Read the buttons on this board
  if (check_interrupt_state) {
    updated = lcd[Current_board].updateButtonInterruptState() && (digitalRead(INTA_PIN) == HIGH);
    Serial.println("*** interrupt button_state board" + String(Current_board) + ": " + String(lcd[Current_board].readButtonState()));
  }
  else {
    updated = lcd[Current_board].updateButtonCurrentState();
    Serial.println("*** current button_state board" + String(Current_board) + ": " + String(lcd[Current_board].readButtonState()));
  }
  if (updated) {
    uint8_t button_state = lcd[Current_board].readButtonState();
    if (button_state & 1) new_switch_pressed = Current_board + 1; // Switch is in bottom row (1-4)
    if (button_state & 2) new_switch_pressed = Current_board + NUMBER_OF_DISPLAY_BOARDS + 1; // Switch is in second row (5-8)
    if (button_state & 4) new_switch_pressed = Current_board + (NUMBER_OF_DISPLAY_BOARDS * 2) + 1; // Switch is in third row (9-12)
    if (button_state & 8) new_switch_pressed = Current_board + (NUMBER_OF_DISPLAY_BOARDS * 3) + 1; //Switch is in top row (13 - 16)

    if (new_switch_pressed != previous_switch_pressed) { // Check for state change
      switch_released = previous_switch_pressed;
      previous_switch_pressed = new_switch_pressed; // Need to store the previous version, because switch_pressed can only be active for one cycle!
      switch_pressed = new_switch_pressed; // switch_pressed must be set here, so accidental presses of switches below will not result in a command being executed.
    }
  }
  Serial.println("******* Switches read on board " + String(Current_board) + "!!! ********");
}

void Select_next_board() {
  Current_board++;
  if (Current_board >= NUMBER_OF_DISPLAY_BOARDS) Current_board = 0;
}

And here is my lcd_lib.h library. It is based upon F.Mathilda's LiquidCrystal library.:
Code:
// Please read VController_v2.ino for information about the license and authors

#ifndef LCDLIB_H
#define LCDLIB_H

// Start of my MCP23017 library - a derived class from FMathilda's LiquidCrystal library.

// Connections to MCP23017
// GPIO B to 8 data bits of three displays
// GPIO A pin 1-4 to switch 1-4
// GPIO A pin 5 to Rs pin of three displays
// GPIO A pin 6,7 and 8 to the E pin of display 1,2 and 3

#include <Print.h>
#include <i2c_t3.h>
#include <LCD.h>
//#include <I2CIO.h>

// Define pins on MCP23017 for the displays
#define GPIO_RS_PIN B00010000 // Pin 5 of GPIO A is the Rs pin
#define DISPLAY1 B10000000 // Pin 8 of GPIO A is the E pin for display 1
#define DISPLAY2 B01000000 // Pin 7 of GPIO A is the E pin for display 2
#define DISPLAY3 B00100000 // Pin 6 of GPIO A is the E pin for display 3
#define DISPLAY_ALL B11100000 // Pin 6-8 for all displays
#define SWITCH_PINS B00001111 // Pin 1-4 are switch pins

// MCP23017 registers
#define MCP23017_IODIRA 0x00
#define MCP23017_IPOLA 0x02
#define MCP23017_GPINTENA 0x04
#define MCP23017_DEFVALA 0x06
#define MCP23017_INTCONA 0x08
#define MCP23017_IOCONA 0x0A
#define MCP23017_GPPUA 0x0C
#define MCP23017_INTFA 0x0E
#define MCP23017_INTCAPA 0x10
#define MCP23017_GPIOA 0x12
#define MCP23017_OLATA 0x14

#define MCP23017_IODIRB 0x01
#define MCP23017_IPOLB 0x03
#define MCP23017_GPINTENB 0x05
#define MCP23017_DEFVALB 0x07
#define MCP23017_INTCONB 0x09
#define MCP23017_IOCONB 0x0B
#define MCP23017_GPPUB 0x0D
#define MCP23017_INTFB 0x0F
#define MCP23017_INTCAPB 0x11
#define MCP23017_GPIOB 0x13
#define MCP23017_OLATB 0x15

class LiquidCrystal_MCP23017 : public LCD
{
  public:
    // Class constructor
    // lcd_Addr is the i2C address of the MCP23017 on the display board
    // lcd_Number is the number of the display (1, 2 or 3)
    LiquidCrystal_MCP23017 (uint8_t lcd_Addr, uint8_t lcd_Number);

    virtual void begin(uint8_t cols, uint8_t rows, uint8_t charsize = LCD_5x8DOTS);
    virtual void send(uint8_t value, uint8_t mode);
    bool updateButtonInterruptState (); // Updates state of buttons, returns true when updated
    uint8_t readButtonState (); // Returns state of buttons
    bool updateButtonCurrentState (); // Reads the current state regardless of the interrupt - needed, because we are missing switch off messages

  private:
    void expanderWrite (const byte reg, const byte data );
    void expanderWriteBoth (const byte reg, const byte dataA, uint8_t dataB );
    uint8_t expanderRead (const byte reg);

    int  init();             // Initialize the LCD class and the MCP23017

    uint8_t _Addr;           // I2C Address of the IO expander
    uint8_t _Number;
    uint8_t _En;             // LCD expander word for enable pin
    uint8_t _Rs;             // LCD expander word for Register Select pin
    uint8_t _Rw;             // LCD expander word for R/W pin (not implemented)
    uint8_t _ButtonState;    // State of buttons
    uint8_t _NewButtonState; // Current state of buttons
};

LiquidCrystal_MCP23017::LiquidCrystal_MCP23017 (uint8_t lcd_Addr, uint8_t lcd_Number) {
  _Addr = lcd_Addr;
  _Number = lcd_Number;     // LCD number is 1, 2 or 3 for display 1,2 or 3
  _Rs = GPIO_RS_PIN;           // Pin 4 is the default Rs pin
  _En = lcd_Number; // Pin 7 for display 1, pin 6 for display 2 and pin 5 for display 3
  _Rw = 0;                  // RW pin is not implemented
}

// Initialization of i2c, MCP23017 ports and display
int LiquidCrystal_MCP23017::init()
{
  int status = 0;

  // initialize the MCP23017 expander
  // and display functions.
  // ------------------------------------------------------------------------
  Wire.begin();
  Wire.setClock(I2C_RATE_1200); // Set i2c clock to 1.2 MHz - the maximum speed of the MCP23017 is 1.7 MHz

  if ( Wire.requestFrom ( _Addr, (uint8_t)1 ) == 1 )
  {
    // MCP23017 has four switches connected to pin 1-4 of Port A
    // Port 5-8 of Port A and all pins of Port B are connected to the displays

    // MCP PortA pin 1-4 input and pin 5-8 output
    // MCP PortB all output
    expanderWriteBoth(MCP23017_IODIRA, SWITCH_PINS, 0x00);

    // Setup for INTA port to be triggered by switch change
    expanderWriteBoth (MCP23017_IOCONA, 0b01100000, 0b01100000); // mirror interrupts, disable sequential mode
    expanderWriteBoth (MCP23017_GPPUA, SWITCH_PINS, 0x00);   // pull-up resistor for switch pins
    expanderWriteBoth (MCP23017_IPOLA, SWITCH_PINS, 0x00);  // invert polarity of signal for switch pins
    expanderWriteBoth (MCP23017_GPINTENA, SWITCH_PINS, 0x00); // Enable interrupts for switch pins

    // read from interrupt capture ports to clear them
    expanderRead (MCP23017_INTCAPA);
    //expanderRead (MCP23017_INTCAPB);

    // setup port 1 D7 = E; D6 = RS
    expanderWrite(MCP23017_GPIOA, _Rw);

    _displayfunction = LCD_8BITMODE | LCD_1LINE | LCD_5x8DOTS;
    status = 1;
  }
  return ( status );
}

void LiquidCrystal_MCP23017::begin(uint8_t cols, uint8_t lines, uint8_t dotsize)
{

  init();     // Initialise the I2C expander interface
  LCD::begin ( cols, lines, dotsize );
}

// Display low-level stuff
// send is called from the base LCD library
void LiquidCrystal_MCP23017::send(uint8_t value, uint8_t mode)
{
  // Is it a command or data
  // -----------------------
  if ( mode == DATA )
  {
    mode = _Rs;
  }

  // Now we toggle the enable bit(s) high and vrite the data in the first write cycle
  expanderWriteBoth(MCP23017_GPIOA, (mode | _En), value); // Enable bit high and value
  expanderWrite(MCP23017_GPIOA, (mode));       // Enable bit low
}

// update button state
bool LiquidCrystal_MCP23017::updateButtonInterruptState () {
  bool updated = false;
  _NewButtonState = expanderRead(MCP23017_INTCAPA) & SWITCH_PINS; // INTCAPA remembers the state at the time of the interrupt
  //_NewButtonState = expanderRead(MCP23017_GPIOA) & SWITCH_PINS; // GPIO gives the current state 
  if (_NewButtonState != _ButtonState) {
    _ButtonState = _NewButtonState;
    updated = true;
  }
  return updated;
}

// Read button state (unbounced)
uint8_t LiquidCrystal_MCP23017::readButtonState () {
  return _ButtonState;
}

bool LiquidCrystal_MCP23017::updateButtonCurrentState () {
  bool updated = false;
  //_NewButtonState = expanderRead(MCP23017_INTCAPA) & SWITCH_PINS; // INTCAPA remembers the state at the time of the interrupt
  _NewButtonState = expanderRead(MCP23017_GPIOA) & SWITCH_PINS; // GPIO gives the current state 
  if (_NewButtonState != _ButtonState) {
    _ButtonState = _NewButtonState;
    updated = true;
  }
  return updated;
}

// **** MCP23017 expander writing and reading
// write a byte to a ports of the MCP23017 expander
void LiquidCrystal_MCP23017::expanderWrite (uint8_t reg, uint8_t data )
{
  Wire.beginTransmission (_Addr);
  Wire.send (reg);
  Wire.send (data);
  Wire.endTransmission ();
}

// write two bytes to both ports of the MCP23017 expander
void LiquidCrystal_MCP23017::expanderWriteBoth (uint8_t reg, uint8_t dataA, uint8_t dataB )
{
  Wire.beginTransmission (_Addr);
  Wire.send (reg);
  Wire.send (dataA);  // port A
  Wire.send (dataB);  // port B
  Wire.endTransmission ();
}

// read a byte from the MCP23017 expander
uint8_t LiquidCrystal_MCP23017::expanderRead (uint8_t reg)
{
  Wire.beginTransmission (_Addr);
  Wire.send (reg);
  Wire.endTransmission ();
  Wire.requestFrom (_Addr, (uint8_t) 1);
  return Wire.read();
}

#endif
 
Last edited:
Code:
    expanderWriteBoth (MCP23017_GPPUA, SWITCH_PINS, 0x00);   // pull-up resistor for switch pins

According to the datasheet:

Code:
bit 7-0
PU7:PU0:
These bits control the weak pull-up resistor
s on each pin (when configured as an input)
<7:0>.
1
= Pull-up enabled.
0
= Pull-up disabled.

your expander is not using the pullups, either enable them from your sketch by writing to the wire bus the chip's register or by modifying the library file

registers 0x0C (bank A pins 0-7) and 0x0D (bank B pins 8-15), have to be 0xFF if you want all pins to be pulled HIGH. 0x00 means NO pullups.

a debouncing timer is useless if the pin is left floating...

nice debouncing code, usually a simpler approach would be to just add a 10ms delay
 
Last edited:
Code:
expanderWriteBoth (MCP23017_GPPUA, SWITCH_PINS, 0x00);   // pull-up resistor for switch pins

SWITCH_PINS = B00001111. So the command above does enable the pull-up resistor on the switch pins and disables it on all the other pins which I use for the displays. expanderWriteBoth writes to GPPUA and GPPUB!

Without the pull-ups the sketch would do nothing at all. That is not the case.

But thanks for looking at it.
 
>>> * Check if it clears the interrupt or select the next board. This board will be checked on the next loop cycle.

This logic would lead up to a 300 ms delay in finding the correct board. I think this would feel very sluggish to the user. I would say you need to find the correct board in the current loop.

An idea: if you wire the interrupt pins from 3 of the io expanders via resistors to a common Analog pin, you could find the correct board with one analog read. The idea being to form a sort of D/A converter with resistors. ( 1 k pullup to analog pin, one expander via a 1k, 2nd via a 2k and 3rd via a 3k for example ).

Ron
 
This logic would lead up to a 300 ms delay in finding the correct board. I think this would feel very sluggish to the user. I would say you need to find the correct board in the current loop.

It does in the example I have provided. In the MIDI controller the loops get long after I have pressed a switch. The first response to a switch, even when it has to read all of the display boards, is not sluggish. My main problem is that for some reason a single press on a switch leads to a double action.

I found this general library: https://github.com/tcleg/Button_Debouncer
I have changed my code inspired by this library, but I still have the debouncing problem. i may have to read more often..

Here is my altered code:
Code:
void check_switches_on_current_board(bool check_interrupt_state) {
  uint8_t new_switch_pressed = 0;
  uint8_t new_switch_released = 0;
  bool updated;

  // Read the buttons on this board
  updated = lcd[Current_board].buttonProcess(check_interrupt_state);
  if (check_interrupt_state) {  Serial.println("*** Checking interrupt state" + String(Current_board)); }
  else {  Serial.println("*** Checking current state" + String(Current_board)); }
  
  if (updated) {
    uint8_t button_state = lcd[Current_board].buttonPressed();
    Serial.println("button_state pressed board" + String(Current_board) + ": " + String(button_state));
    if (button_state & 1) new_switch_pressed = Current_board + 1; // Switch is in bottom row (1-4)
    if (button_state & 2) new_switch_pressed = Current_board + NUMBER_OF_DISPLAY_BOARDS + 1; // Switch is in second row (5-8)
    if (button_state & 4) new_switch_pressed = Current_board + (NUMBER_OF_DISPLAY_BOARDS * 2) + 1; // Switch is in third row (9-12)
    if (button_state & 8) new_switch_pressed = Current_board + (NUMBER_OF_DISPLAY_BOARDS * 3) + 1; //Switch is in top row (13 - 16)

    if (new_switch_pressed != previous_switch_pressed) { // Check for state change
      previous_switch_pressed = new_switch_pressed; // Need to store the previous version, because switch_pressed can only be active for one cycle!
      switch_pressed = new_switch_pressed;
      switch_is_expression_pedal = false;
    }

    button_state = lcd[Current_board].buttonReleased();
    DEBUGMAIN("button_state released board" + String(Current_board) + ": " + String(button_state));
    if (button_state & 1) new_switch_released = Current_board + 1; // Switch is in bottom row (1-4)
    if (button_state & 2) new_switch_released = Current_board + NUMBER_OF_DISPLAY_BOARDS + 1; // Switch is in second row (5-8)
    if (button_state & 4) new_switch_released = Current_board + (NUMBER_OF_DISPLAY_BOARDS * 2) + 1; // Switch is in third row (9-12)
    if (button_state & 8) new_switch_released = Current_board + (NUMBER_OF_DISPLAY_BOARDS * 3) + 1; //Switch is in top row (13 - 16)

    if (new_switch_released != previous_switch_released) { // Check for state change
      previous_switch_released = new_switch_released; // Need to store the previous version, because switch_released can only be active for one cycle!
      switch_released = new_switch_released;
    }
  }
  Serial.println("******* Switches read on board " + String(Current_board) + "!!! ********");
}

And in the library:
Code:
bool LiquidCrystal_MCP23017::buttonProcess(bool useInterruptState) {
  _LastButtonState = _ButtonState;
  if (useInterruptState) _ButtonState = expanderRead(MCP23017_INTCAPA) & SWITCH_PINS; // INTCAPA remembers the state at the time of the interrupt
  else _ButtonState = expanderRead(MCP23017_GPIOA) & SWITCH_PINS; // GPIO gives the current state
  _ChangedState = _ButtonState ^ _LastButtonState;
  return (_ChangedState !=0);
}

uint8_t LiquidCrystal_MCP23017::buttonPressed() {
  return (_ChangedState & _ButtonState);
}

uint8_t LiquidCrystal_MCP23017::buttonReleased() {
  return (_ChangedState & (~_ButtonState));
}
 
Getting this to work has taken me quite a while. But I have managed it in the end:

* I found i2c speed was kept slow, because the i2c LiquidCrystal library started the i2c bus at 100 kHz and later speed commands did not seem to effect the i2c bus speed. I changed it in that library and I am running at 1500 kHz now.
* Also reading INTCAPA on the Mcp23017 was unreliable. It worked better to poll GPIOA at regular intervals.

The switches now work perfect.

Check out my project here: https://youtu.be/dFgOi_WOnbg
 
Status
Not open for further replies.
Back
Top