Using MCP23017 functions inside interrupt causes Arduino Pro Micro to stop

I'm making a custom steering wheel for my racing sim and I encountered an unusual behavior. I am using the interrupt pin on my Pro Micro to read rotary encoders hooked up on two MCP23017 modules. The interrupt seems to be working fine when only a serial output is in the callback function. But as soon as try to get the last interrupt pin or read any of the pins on the expander, the entire code stops. The same methods work fine in the main loop though.

#include <Encoder.h>

#include <Joystick.h>
#include <Adafruit_MCP23X17.h>

Adafruit_MCP23X17 mcp1;
Adafruit_MCP23X17 mcp2;

int mcp1Buttons[8] = {2, 3, 6, 7, 10, 11, 14, 15};
int mcp1LastState[8] = {HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH};

int mcp2Buttons[10] = {2, 5, 8, 9, 10, 11, 12, 13, 14, 15};
int mcp2LastState[10] = {HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH};


Joystick_ Joystick(JOYSTICK_DEFAULT_REPORT_ID, JOYSTICK_TYPE_GAMEPAD,
                   32, 0,                 // Button Count, Hat Switch Count
                   true, true, false,     // X and Y, but no Z Axis
                   true, true, false,     // Rx and Ry, but no Rz Axis
                   false, false,          // No rudder or throttle
                   false, false, false);  // No accelerator, brake, or steering

#define DEADZONE 10

void setup() {
  Serial.begin(9600);
  Serial.println("Serial started");

  if (!mcp1.begin_I2C(0x20)) {
    Serial.println("Error starting mcp 0x20");
  }
  if (!mcp2.begin_I2C(0x21)) {
    Serial.println("Error starting mcp 0x21");
  }
  for (int i = 0; i < 16; i++) {
    mcp1.pinMode(i, INPUT_PULLUP);
    mcp2.pinMode(i, INPUT_PULLUP);
  }

  Joystick.begin();
  Joystick.setXAxisRange(-512, 512);
  Joystick.setYAxisRange(-512, 512);
  Joystick.setRxAxisRange(-512, 512);
  Joystick.setRyAxisRange(-512, 512);

  pinMode(7, INPUT_PULLUP);

  mcp1.setupInterrupts(true, false, LOW);
  mcp1.setupInterruptPin(0, LOW);
  mcp1.setupInterruptPin(4, LOW);
  mcp1.setupInterruptPin(8, LOW);
  mcp1.setupInterruptPin(12, LOW);
  mcp1.clearInterrupts();

  mcp2.setupInterrupts(true, false, LOW);
  mcp2.setupInterruptPin(0, HIGH);
  mcp2.setupInterruptPin(3, HIGH);
  mcp2.setupInterruptPin(6, HIGH);
  mcp2.clearInterrupts();

  attachInterrupt(digitalPinToInterrupt(7), pin7Interrupt, CHANGE);
}

void loop() {
  int leftX = analogRead(A0) - 512;
  int leftY = analogRead(A1) - 512;
  int rightX = analogRead(A2) - 512;
  int rightY = analogRead(A3) - 512;

  if (abs(0 - leftX) <= DEADZONE) {
    leftX = 0;
  }
  if (abs(0 - leftY) <= DEADZONE) {
    leftY = 0;
  }
  if (abs(0 - rightX) <= DEADZONE) {
    rightX = 0;
  }
  if (abs(0 - rightY) <= DEADZONE) {
    rightY = 0;
  }

  Joystick.setXAxis(leftX);
  Joystick.setYAxis(leftY);
  Joystick.setRxAxis(rightX);
  Joystick.setRyAxis(rightY);

  for (int i = 0; i < 8; i++) {
    int state = mcp1.digitalRead(mcp1Buttons[i]);
    if (state != mcp1LastState[i]) {
      if (state == LOW) {
        Joystick.pressButton(mcp1Buttons[i]);
        mcp1LastState[i] = LOW;
      }
    }
    if (state == HIGH) {
        Joystick.releaseButton(mcp1Buttons[i]);
        mcp1LastState[i] = HIGH;
      }
  }

  for (int i = 0; i < 10; i++) {
    int state = mcp2.digitalRead(mcp2Buttons[i]);
    if (state != mcp2LastState[i]) {
      if (state == LOW) {
        Joystick.pressButton(mcp2Buttons[i] + 8);
        mcp2LastState[i] = LOW;
      }
      else if (state == HIGH) {
        Joystick.releaseButton(mcp2Buttons[i] + 8);
        mcp2LastState[i] = HIGH;
      }
    }
  }
}

void pin7Interrupt() {
  Serial.println("Interrupt");
  uint8_t lastPin = mcp1.getLastInterruptPin();
  if(lastPin != 255) {
    if (mcp1.digitalRead(0) != mcp1.digitalRead(1)) {
      Serial.println("1");
    }
    else {
      Serial.println("-1");
    }
  }
}

I'm less than sanguine about expecting to get away with doing I2C I/O inside an interrupt being a reasonable expectation.

1 Like

That's also a no-no. Don't rely on luck.

Just set a flag in the interrupt routine. Then have loop() check that flag and read the chip.

However, I don't think the interrupt line will change until the chip has been read, so there may be no advantage to using an interrupt at all, because the interrupt will not be missed if not read in a short time. So why not simply poll the interrupt pin in loop().

1 Like

Got it. I'll try that later today

What do you mean? I'm new to interrupts.

Then what's the point of an interrupt? (Maybe I misunderstand you.)

You mean in general?

Where external signals could be very short, if the MCU is polling the signal, it could miss some signal changes entirely if the signal is being polled. Or it may be important to measure the time that changes occur in the signal, or the period between changes. Polling could introduce errors in those timings.

These situations are a good examples of the use of interrupts.

But in the case of the MCP23xxx and PCF85xx chips INT pins, you could connect them to an interrupt pin of the MCU, but you can also use them simply as an indicator signal on any MCU input pin, and the MCU can poll it. The chip will set the INT pin active when a change is detected on one of its input pins, and won't clear the INT pin until the chip gets communicated to over the i2c bus. So even if the MCU doesn't poll the pin immediately, it will pick up the change when it does next poll the pin.

If you need to read it first, then get the interrupt? But I could've misunderstood you.

Sorry, I'll clarify that. The interrupt pin of the MCP chip goes active (== LOW, I think) as soon as a change on the chip's inputs is detected. But even if that change is very brief, the interrupt pin stays active. It only goes inactive/cleared (== HIGH) when the chip's data gets read over the i2c bus.

If you connect the MCP interrupt to an interrupt pin on the MCU, you would set up the interrupt routine to be executed only when the pin falls from HIGH to LOW (FALLING) and not on RISING, to avoid reading the chip a second time, unnecessarily.

1 Like

Now I follow you :upside_down_face:

It turns out that using the interrupt pins on the mcp causes another unrelated problem on its own so I decided to directly poll the pins instead. I don't mind missing some steps as long as the direction readings are fairly accurate.

for (int i = 0; i < 4; i++) {
  int a = mcp1.digitalRead(mcp1EncA[i]);
  int b = mcp1.digitalRead(mcp1EncA[i + 1]);
  if (a != mcp1EncALastState[i]) {
    if (a == LOW && b == HIGH) {
      Serial.println("1");
    }
    else if (a == HIGH && b == HIGH) {
      Serial.println("-1");
    } 
    mcp1EncALastState[i] = a;
  }
}

This is what I tried, with the arrays containing the pin numbers and their last state. But for some reason, the output is always 1 followed by -1 no matter which direction I turn. What did I miss?

a != mcp1EncALastState[i]
The last state of 'a' is actually in mcp1EncALastState[i-1] and you only need to check if
a != b to know the direction.

However, if are are trying to read an encoder, I recommend using the MCP interrupts.

It is always a good idea to provide detailed description of your project.
In your case this means

  • how many encoders do you have connected?
  • How many IO-pins are connected to whatever hardware?

to see if you have enough direct IO-pins left to connect your encoders.

There is one variant to read in encoders based on a timer-interrupt.
This enables to use any direct IO-pin of the microcontroller for the encoders.

And hence the Catch 22. You can't conduct I2C transactions to read the chip's data in the ISR since doing so also relies on interrupts, which are disabled in an ISR.

I'm using 7 encoders and most of the direct IO pins are occupied by potentiometers for analog input.

Tell me more about this

Ah, I get it now. Thank you for the quick explanation

Can they all operate at the same time?

Technically yes but for the use case, unlikely.

If it's only one at a time or if it's two at a time and thery are on different MCPs then use the MCP interrupt capability

The problem is that sometimes the encoders get stuck in between clicks and cause the rest of the encoders on that mcp to stop working.