0

I'm using a teensy 4.0 with two mcp23s17 gpio expanders where #1 has 16 buttons wired to it, and #2 has 4 buttons and six rotary encoders. To run everything I'm using majenkos mcp23s17 library. Recently, I refactored my code, and suddenly I'm unable to setup the mcp's to have interrupts working properly.

I've tried to narrow the problem down by commenting out code, and it appears that as soon as I call gpioExpander.enableInterrupt(pin, CHANGE); in gpioSetup() the problem occurs. In detail, loop() runs fine, but as soon as I press a button, the teensy hangs. Also, to restore the teensy I have to upload the code with the call to gpioExpander.enableInterrupt(pin, CHANGE); commented out in order to have the teensy up and running again.

I've been staring myself blind on the code, and I suspect I'm missing something obvious. Your help is appreciated.

//main.cpp
#include <Arduino.h>

#include "GPIO.h"
#include "MCP23S17.h"

extern MCP23S17 gpioExpander1;
extern MCP23S17 gpioExpander2;

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

void loop() {
    GPIOtest(gpioExpander1, gpioExpander2);
}

//GPIO.h
#ifndef GPIO_H
#define GPIO_H

#include <Arduino.h>

#include "MCP23S17.h"

constexpr uint8_t address = 0;
constexpr uint8_t interruptPin1 = 8;
constexpr uint8_t interruptPin2 = 22;
constexpr uint8_t chipSelectPin1 = 7;
constexpr uint8_t chipSelectPin2 = 21;

void setGPIO1Pins(MCP23S17 &gpioExpander);
void setGPIO2Pins(MCP23S17 &gpioExpander);
void gpioSetup();
void GPIO1interrupt();
void GPIO2interrupt();
void GPIOtest(MCP23S17 &gpioExpander1, MCP23S17 &gpioExpander2);

#endif

//GPIO.cpp
#include "GPIO.h"

#include <Arduino.h>
#include <MCP23S17.h>

volatile bool interruptFlag1 = false;
volatile bool interruptFlag2 = false;

MCP23S17 gpioExpander1(&SPI, chipSelectPin1, address);
MCP23S17 gpioExpander2(&SPI, chipSelectPin2, address);

//  Set all Port Expander pins to desired mode INPUT_PULLUP
//  Set all Port Expander input pins interrupts
void setGPIOPins(MCP23S17& gpioExpander) {
    for (uint8_t pin = 0; pin <= 15; ++pin) {
        gpioExpander.pinMode(pin, INPUT_PULLUP);
        gpioExpander.enableInterrupt(pin, CHANGE); 
    }

    // set Port Expander Interrupt configuratons
    gpioExpander.setMirror(true);
    gpioExpander.setInterruptOD(false);
    gpioExpander.setInterruptLevel(LOW);
}

void gpioSetup() {
    gpioExpander1.begin();
    gpioExpander2.begin();
    digitalWrite(chipSelectPin1, HIGH);
    digitalWrite(chipSelectPin2, HIGH);

    pinMode(interruptPin1, INPUT_PULLUP);
    pinMode(interruptPin2, INPUT_PULLUP);

    attachInterrupt(digitalPinToInterrupt(interruptPin1), GPIO1interrupt, LOW);
    attachInterrupt(digitalPinToInterrupt(interruptPin2), GPIO2interrupt, LOW);

    setGPIOPins(gpioExpander1);
    setGPIOPins(gpioExpander2);

    // GPIO registers are read to be cleared
    gpioExpander1.getInterruptValue();
    gpioExpander2.getInterruptValue();
}

void GPIO1interrupt() {
    interruptFlag1 = true;
}

void GPIO2interrupt() {
    interruptFlag2 = true;
}

void GPIOtest(MCP23S17& gpioExpander1, MCP23S17& gpioExpander2) {
    if (interruptFlag1 == true) {
        Serial.println("interrupt 1 = true");
        interruptFlag1 = false;  
    }

    if (interruptFlag2 == true) {
        Serial.println("interrupt 2 = true");
        interruptFlag2 = false;
    }
}

1 Answer 1

2

Short version, try:

attachInterrupt(digitalPinToInterrupt(interruptPin1), GPIO1interrupt, FALLING);
attachInterrupt(digitalPinToInterrupt(interruptPin2), GPIO2interrupt, FALLING);

FALLING (instead of LOW) being the active ingredient. I'm assuming FALLING is supported. If not you can try synthesizing this with CHANGE and reading the port to see whether or not it is currently LOW.

And then call .getInterruptValue(); to clear the MCP23S17's interrupt condition where you clear the global volatile flags:

void GPIOtest(MCP23S17& gpioExpander1, MCP23S17& gpioExpander2) {
    if (interruptFlag1 == true) {
        Serial.println("interrupt 1 = true");
        interruptFlag1 = false;
        gpioExpander1.getInterruptValue(); // <------
    }

    if (interruptFlag2 == true) {
        Serial.println("interrupt 2 = true");
        interruptFlag2 = false;
        gpioExpander2.getInterruptValue(); // <------
    }
}

Long version:

This a guess, since I'm unable to test. But this is maybe not a bad guess:

You have configured the MCP23S17 device to drive active low on/during an MCP23S17 interrupt condition:

    gpioExpander.setMirror(true);
    gpioExpander.setInterruptOD(false);
    gpioExpander.setInterruptLevel(LOW);

So, were an MCP23S17 interrupt to occur (gpioExpander.enableInterrupt(pin, CHANGE);), the interrupt flag within the MCP23S17 would be set, and INTA/INTB of the MCP23S17 would become active low.

The MCP23S17 datasheet says:

The interrupt condition is cleared after the LSb of the data is clocked out during a read command of GPIO or INTCAP

You have configured your interrupt pins for interrupting on a LOW level interrupt:

attachInterrupt(digitalPinToInterrupt(interruptPin1), GPIO1interrupt, LOW);

So, if I understand this correctly, your Teensy continually re-enter the GPIO#interrupt() functions until something read causes the interrupt condition to be cleared. You are not clearing the interrupt condition inside the ISR, you're setting a flag to do that later in the main line of execution, which is reasonable by itself. One thing I can guess is that had you'd previous had something to clear the interrupt condition inside the interrupt and it is now outside the interrupt, namely your call to .getInterruptValue(), which reads the INTCAP register mentioned above. Or perhaps you'd be using one of the functions that reads a pin/port that would have cleared the interrupt condition. Clearing the global variable alone will not clear the interrupt condition, and so you're going to go right back into ISR which will reassign the global variable.

An AVR will execute a single instruction outside of the ISR before reentering the ISR. So the effect of remaining in the interrupt condition is to make your code slow, potentially very slow. I don't know off the top of my head what the Teensy 4.x core will do in this case. Either way, it would lock up or possibly look locked up.

If you were reading/getting-interrupt-value inside the ISR it would have cleared it and so it would not re-entered the ISR repeatedly. Or rather, it wouldn't do that in definitely (or just for long). It possible that the Teensy is so much faster that it might re-enter a few times until the MCP23S17 got around to acknowledging that the interrupt condition had be cleared. I'm not advocating for this so, so I'll leave it at that.

I believe ultimately what you want is to set a FALLING edge interrupt:

attachInterrupt(digitalPinToInterrupt(interruptPin1), GPIO1interrupt, FALLING);

Anyway, with FALLING the initial falling edge of the INTA/INTB line sets the interrupt condition in the Teensy (not MCP23S17) but it is cleared when your ISR executes. So you will not keep re-entering the ISR. So your ISR will enter once to set the global volatile flag. Later when you pick up on the set value of your global volatile flag(s), you need to call .getInterruptValue() (or otherwise read a pin) to clear the interrupt condition in your MCP23S17 (not Teensy) which will de-assert/raise INTA/INTB, at which point the ISR in the Teensy is ready for to get trigger by another falling edge of INTA/INTB.

It's also probably possible to do this with a LOW-level interrupt, but you'd have keep un-configuring them in the ISR and re-configuring them after you clear the MCP23S17's condition. Not sure that would be recommended. But, I think it's possible.

1
  • You've really pinpointed where I went wrong. Your keen eye has helped me in the right direction. Appreciate it a lot!
    – Erik
    Oct 8, 2022 at 13:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.