MCP23017 issues with multiple pins

For other supported Arduino products from Adafruit: Shields, accessories, etc.

Moderators: adafruit_support_bill, adafruit

Please be positive and constructive with your questions and comments.
Locked
User avatar
shanmcarthur
 
Posts: 9
Joined: Fri Apr 14, 2023 3:23 am

MCP23017 issues with multiple pins

Post by shanmcarthur »

I am using the MCP23017 GPIO expander with 4 pins attached. I am using the interrupt mechanism, but still using a loop on my MCU, much like the example does. The challenge I have is that I am wanting my code to trigger a single time for a single press of the button. My inner loop is fast and I don't want to trigger the code twice if the button is held down between loops. In essence, I only want to be triggered when the button goes from HIGH to LOW on the expander. What I am noticing is that since I call the clearInterrupts() call in my code that responds when the interrupt pin on my MCU goes low, it tells the expander module to clear the interrupt, but if the button remains pressed, the expander throws the interrupt low again and my code will get triggered another time. How can I achieve a single interrupt for a single button press?

Here is my serial output (for when I press and hold the button on pin 8):
MCP23xxx Interrupt Test!
Interrupt detected on pin: 8
Pin states at time of interrupt: 0b111000000000
Pin 8 was pressed
Interrupt detected on pin: 8
Pin states at time of interrupt: 0b111000000000
Pin 8 was pressed
Interrupt detected on pin: 8
Pin states at time of interrupt: 0b111000000000
Pin 8 was pressed
Interrupt detected on pin: 8
Pin states at time of interrupt: 0b111000000000
Pin 8 was pressed
Interrupt detected on pin: 8
Pin states at time of interrupt: 0b111000000000
Pin 8 was pressed

Here is my code:

Code: Select all

// NOTE: This is a simple example that only reads the INTA or INTB pin
//       state. No actual interrupts are used on the host microcontroller.
//       MCP23XXX supports the following interrupt modes:
//           * CHANGE - interrupt occurs if pin changes to opposite state
//           * LOW - interrupt occurs while pin state is LOW
//           * HIGH - interrupt occurs while pin state is HIGH
#include <Arduino.h>
#include <Adafruit_MCP23X17.h>

#define CHECK_PIN_PRESSED(var,pos) (!((var) & (1<<(pos))))

#define BUTTON_PIN 1   // MCP23XXX pin used for interrupt

#define INT_PIN 4      // microcontroller pin attached to INTA/B

int clicks = 0;

Adafruit_MCP23X17 mcp;

void setup() {
  Serial.begin(115200);
  //while (!Serial);
  Serial.println("MCP23xxx Interrupt Test!");

  // uncomment appropriate mcp.begin
  if (!mcp.begin_I2C()) 
  {
    Serial.println("Error.");
    while (1);
  }

  // configure MCU pin that will read INTA/B state
  pinMode(INT_PIN, INPUT);

  mcp.setupInterrupts(false, false, LOW);

  // configure button pins for input with pull up
  mcp.pinMode(8, INPUT_PULLUP);
  mcp.pinMode(9, INPUT_PULLUP);
  mcp.pinMode(10, INPUT_PULLUP);
  mcp.pinMode(11, INPUT_PULLUP);

  // enable interrupts on button press
  mcp.setupInterruptPin(8, LOW);
  mcp.setupInterruptPin(9, LOW);
  mcp.setupInterruptPin(10, LOW);
  mcp.setupInterruptPin(11, LOW);
}

void loop() 
{
  if (!digitalRead(INT_PIN)) 
  {
    uint16_t allPins = mcp.getCapturedInterrupt();
    uint8_t lastPin = mcp.getLastInterruptPin();

    Serial.print("Interrupt detected on pin: ");
    Serial.println(lastPin);
    Serial.print("Pin states at time of interrupt: 0b");
    Serial.println(allPins, 2);

    if (CHECK_PIN_PRESSED(allPins, 8))
    {
      Serial.println ("Pin 8 was pressed");
    }

    if (!(allPins & (1<<(9))))
    {
      Serial.println ("Pin 9 was pressed");
    }

    if (!(allPins & (1<<(10))))
    {
      Serial.println ("Pin 10 was pressed");
    }

    if (!(allPins & (1<<(11))))
    {
      Serial.println ("Pin 11 was pressed");
    }

    delay(250);  // debounce
    // NOTE: If using DEFVAL, INT clears only if interrupt
    // condition does not exist.
    // See Fig 1-7 in datasheet.
    mcp.clearInterrupts();  // clear
  }
}

User avatar
dastels
 
Posts: 15656
Joined: Tue Oct 20, 2015 3:22 pm

Re: MCP23017 issues with multiple pins

Post by dastels »

Set up the interrupts for CHANGE, not LOW. Then when you process the interrupt, check the pin for a LOW value to see if it was just pressed. You might have to compensate for switch bounce by waiting briefly after the interrupt for the switch to settle.

Dave

User avatar
shanmcarthur
 
Posts: 9
Joined: Fri Apr 14, 2023 3:23 am

Re: MCP23017 issues with multiple pins

Post by shanmcarthur »

I tried that too. The challenge is that there isn't always an interrupt for it when it goes high. There doesn't seem to be a way for me to differentiate between a long press and two short presses. I only want my function triggered once for every press of the button, for quick successions as well as long holds - one call per press.

User avatar
dastels
 
Posts: 15656
Joined: Tue Oct 20, 2015 3:22 pm

Re: MCP23017 issues with multiple pins

Post by dastels »

In theory, that's what interrupting on CHANGE should do. It will let you track failing edges (presses) and rising edges (releases). Well, that's exactly what it will do with a clean digital logic signal. That's not what you have. You have a mechanical switch which is bouncy. So if you are too quick to process the interrupt and re-enable them before the switch settles (stops bouncing) you'll get spurious interrupts for the single press action. You can act on the initial falling edge interrupt and wait a while before clearing/enabling interrupts. How long depends on the switch. If you have an oscilloscope you can watch the bouncing and figure out how long it takes to settle. After that time has passed, you can check the input state and re-enable interrupts. 10 mS is generally long enough to allow a switch to settle.

As you said, if you interrupt on LOW you'll get multiple interrupts for as long as the button is pushed.

For detecting long vs. short presses, you'll have to track when the press/release events occur and figure out what your presses are. E.g. you'll have a long-press threshold. Capture the pressed time and check the time since then for as long as it's still pressed. If you get a release before the threshold you have a short press. If it stays pressed for longer than the threshold you have a long press. You can do something similar to detect sequences of multiple short presses.

Dave

User avatar
shanmcarthur
 
Posts: 9
Joined: Fri Apr 14, 2023 3:23 am

Re: MCP23017 issues with multiple pins

Post by shanmcarthur »

That helped a lot. In my existing code I did not have a delay in where I was handling the interrupt and cleared the expander's interupts, and in my isolated project based on the sample code from the library, I was using the 240ms delay that they had in the sample, plus interrupts set to LOW. When I switched to CHANGE and I set the delay to 10ms, things got a lot more reliable. Reliable enough that I don't think I need any debouncing logic...

The challenge I was having was building a good debouncing strategy when I would either have constant interrupts (using LOW) or I was missing the button release event because it was hidden by a delay then a reset of the interrupt. I will now scale up my demo to 4 buttons and see if I can build more resiliency into my interrupt handling. I think I also have to keep the work of servicing the new button presses until after the interrupt is cleared.

The explanations you have offered have been very helpful. Thank you so much for the effort!

User avatar
shanmcarthur
 
Posts: 9
Joined: Fri Apr 14, 2023 3:23 am

Re: MCP23017 issues with multiple pins

Post by shanmcarthur »

Nope, this is not working. I cannot get reliable interrupts that represent the latest pin status. And when I add a delay() into my loop that represents other work being done, I seem to miss a lot of interrupts. It was better than before, but with multiple buttons and rapid pressing of them, I cannot get a reliable signal.

Here is my reduced and complete codebase:

Code: Select all

#include <Arduino.h>
#include <Adafruit_MCP23X17.h>

// macro to detect if a button is pressed from a pin mask of all buttons
#define CHECK_PIN_PRESSED(var,pos) (!((var) & (1<<(pos))))

Adafruit_MCP23X17 mcp;

#define DEBOUNCE_TIME_MS 20
#define EXPANDER_INT_PIN 4      // microcontroller pin attached to INTA/B

int count = 0;

void setup() 
{
  Serial.begin(115200);

  if (!mcp.begin_I2C()) 
  {
    Serial.println("Error connecting to MCP23017 module.  Stopping.");
    while (1);
  }

  mcp.setupInterrupts(false, false, LOW);
  
  mcp.pinMode(8, INPUT_PULLUP);
  mcp.pinMode(9, INPUT_PULLUP);
  mcp.pinMode(10, INPUT_PULLUP);
  mcp.pinMode(11, INPUT_PULLUP);

  mcp.setupInterruptPin(8, CHANGE);
  mcp.setupInterruptPin(9, CHANGE);
  mcp.setupInterruptPin(10, CHANGE);
  mcp.setupInterruptPin(11, CHANGE);

  mcp.clearInterrupts();

  // configure MCU pin that will read INTA/B state
  pinMode(EXPANDER_INT_PIN, INPUT_PULLUP);

}

void loop() 
{
  uint16_t allPins;
  bool done = false;

  if (!digitalRead(EXPANDER_INT_PIN)) 
  {
    uint8_t iPin = 255;
    long time;
    count++;
  
    while ((iPin = mcp.getLastInterruptPin()) != 255)
    {
      time = millis();
      Serial.print ("loop ");
      Serial.print (count);
      Serial.print (" millis:");
      Serial.print (time);
      Serial.print(" Interrupt on pin: ");
      Serial.println(iPin);
      
      allPins = mcp.getCapturedInterrupt();

      Serial.print("Pins: 0b");
      Serial.print(allPins, 2);
      
      Serial.print("  Pin 8 = ");
      Serial.print(CHECK_PIN_PRESSED(allPins, 8));

      Serial.print(", 9 = ");
      Serial.print(CHECK_PIN_PRESSED(allPins, 9));

      Serial.print(", 10 = ");
      Serial.print(CHECK_PIN_PRESSED(allPins, 10));

      Serial.print(", 11 = ");
      Serial.println(CHECK_PIN_PRESSED(allPins, 11));

      delay(DEBOUNCE_TIME_MS); 
    };

    mcp.clearInterrupts();  // clear
  }
  
  // simulate other work and delays
  delay(1000);
}
And here is the end of one of my serial logs. Notice that the last 4 loops had identical data. At this point, I had pin 8 depressed and released it fully and immediately depressed it again. There was no internal interrupt that showed the button being pressed again.

loop 13 millis:29859 Interrupt on pin: 8
Pins: 0b111100000000 Pin 8 = 0, 9 = 0, 10 = 0, 11 = 0
loop 14 millis:31113 Interrupt on pin: 8
Pins: 0b111100000000 Pin 8 = 0, 9 = 0, 10 = 0, 11 = 0
loop 15 millis:33367 Interrupt on pin: 8
Pins: 0b111100000000 Pin 8 = 0, 9 = 0, 10 = 0, 11 = 0
loop 16 millis:38621 Interrupt on pin: 8
Pins: 0b111100000000 Pin 8 = 0, 9 = 0, 10 = 0, 11 = 0

I don't see the call to clearInterrupts() as being good as it will wipe any pending interrupts on the expander. You can see that the clearInterrupts() call is the very next statement from the last GetLastInterruptPin() call. There is no more optimization I can do on my side. I believe what is happening is that the I2C communication is taking time and the time between the calls is causing me to lose data. It would be a better overall system design for the expander to automatically clear the interrupt line when a call to GetLastInterruptPin() is called and there is no pending data, and to not require the remote MCU to explicitly call GetLastInterruptPin(). We should have a lossless API.

Shan

User avatar
adafruit_support_bill
 
Posts: 88091
Joined: Sat Feb 07, 2009 10:11 am

Re: MCP23017 issues with multiple pins

Post by adafruit_support_bill »

You are polling the interrupt pin instead of using it as an actual interrupt. So the probability of missed interrupts will increase as your poll-rate decreases. The 1 second delay in your loop sets an upper bound of 1Hz on your poll rate. A lot can happen in that 1 second that will be missed.

Better to handle the interrupts in real-time with an actual interrupt handler. That way you can handle pin-change events that occur while busy with other processing.

https://www.arduino.cc/reference/en/lan ... interrupt/

User avatar
shanmcarthur
 
Posts: 9
Joined: Fri Apr 14, 2023 3:23 am

Re: MCP23017 issues with multiple pins

Post by shanmcarthur »

In my real code I was handling this with an interrupt (more on that), but in this code, my intention was to minimize it to the minimal code so that we could easily collaborate on the forum. Your response has got me thinking (again). My code that is inside the polling of the interrupt pin is in a while loop, calling the getLastInterruptPin() method until it returns a no-pin response (255). I had assumed that the expander is implementing an interrupt queue in its memory, but I think that is a false assumption.

The challenge I have with a real interrupt on the interrupt pin is that I can't call out over I2C bus within that interrupt as I get a watchdog timer exception that then crashes the board. Apparently, calling out to the serial bus within an interrupt is not possible. So my interrupt simply incremented a counter and left it up to my main loop to poll for the details, leading me back into this little trap - any delay in the loop is going to surface as missed events. I don't have a mechanism to get the interrupt details from the expander without losing any events.

I have been thinking of the problem and possible approaches and I was thinking that a button expander would have to have a reliable buffer of events and allow it to drain over the i2c bus without losing any of those events. I don't think this chip will do that and I have sort of been playing with the concept of perhaps designing an MCU solution to that, but that would mean my project would need multiple MCUs. The other concept I am thinking about is that everything in my project needs to be threaded and non-blocking. That is going to be a tall order. Many of my sensors take a while to obtain a reading and perhaps I need to find a way to background all of those. What I worry is that this will then present a challenge and conflict on the I2C bus itself, where I have to have a mutex to control its usage too, but then if my button code is triggered I have to prioritize that traffic if I don't want to lose any event.

The only thing that I think may solve this is a multiplexer that will use a parallel interface (4 pins for address and 1 for action - rise/fall or HIGH/LOW) and set those lines then raise the interrupt pin (so 6 GPIOs in total). Then my MCU interrupt routine can trigger and read the address and action pins. I might need one more pin to ACK the signal and request the interrupt pin be lowered. The multiplexer would need to maintain a buffer of pending events. This is the design I have been having for a microcontroller version of this multiplexer problem. It will be expensive to put in projects, but I think theoretically it will work.

Who would have thought that a simple onClick() method would be SO complicated to solve in a microcontroller. I do enjoy this challenge.

User avatar
adafruit_support_bill
 
Posts: 88091
Joined: Sat Feb 07, 2009 10:11 am

Re: MCP23017 issues with multiple pins

Post by adafruit_support_bill »

There is no queue in the MCP23017. It just captures the state of the pins at the time of the interrupt. The interrupt is not re-armed until you clear it.

Hard to say without knowing the whole scope of the system, but it sounds like a dedicated microcontroller to capture and queue the button events in real-time might be a good way to go. You wouldn't need a high-powered processor, floating point hardware or gobs of memory. You just need something with enough pins to handle all your buttons. One of the ItsyBitsy boards would probably do the job: https://www.adafruit.com/?q=itsybitsy&sort=BestMatch

User avatar
shanmcarthur
 
Posts: 9
Joined: Fri Apr 14, 2023 3:23 am

Re: MCP23017 issues with multiple pins

Post by shanmcarthur »

OK, I think I have it figured out. At least with a minimal program that has no other work going on. I removed my delay() in the loop as I decided that I need to start doing background things and keep the main loop extremely fast. I switched to using an interrupt on the MCU to service the interrupt coming from the expander. I also changed the approach inside my code to read the last pin and pin states and IMMEDIATELY clear the interrupt on the expander and free up the interrupt counter on my host MCU. Even my debounce delay is outside of that critical section. My observations are now that the buttons seem responsive and accurate. The only way I can get it to not work properly is to press two buttons at the same time and even then they usually work.

My sample still doesn't do any work. So my next step is to look into ESP32 tasks and see if I can dispatch the processing for the work behind the button to a background task. I think the least performant thing in my project is the LCD output which is very slow and I will see if I can push that to a background task too. I am going to be religious on keeping my main loop extremely fast. I am hoping this approach will allow me to be successful without having a second microcontroller involved. That said, I do believe that this is necessary in the long run - and it is very consistent with a modern keyboard that has onboard processor and only sends properly processed button data to the computer over the serial port.

Thanks everyone for providing input. It was quite helpful for me. The trick of using the CHANGE interrupt instead of the LOW interrupt was very critical, as was all the other advice to keep things tight and clarification that there is no queue on the expander, which got me focusing on performance to ensure I would not be delayed and miss an event.

Locked
Please be positive and constructive with your questions and comments.

Return to “Other Arduino products from Adafruit”