I2C interrupts on a MCP23017

Status
Not open for further replies.

lodevalm

Member
Hello,

I'm using a MCP23017 to connect buttons to a teensy 4.1 and I'm getting some problems configuring the interrupts.

here is the code:

Code:
#include <Wire.h>

#define PWREN 29
#define TEENSY_INTERRUPT_PIN 4
#define TEENSY_ENCODERS_RESET_PIN 5
#define TEENSY_BUTTONS_RESET_PIN 6

#define I2C_BUTTONS 0x22

//https://github.com/blemasle/arduino-mcp23017/blob/master/src/MCP23017.h
#define I2C_IODIR_A   0x00 // Controls the direction of the data I/O for port A.
#define I2C_IODIR_B   0x01 // Controls the direction of the data I/O for port B.
#define I2C_IPOL_A    0x02 // Configures the polarity on the corresponding GPIO_ port bits for port A.
#define I2C_IPOL_B    0x03 // Configures the polarity on the corresponding GPIO_ port bits for port B.
#define I2C_GPINTEN_A 0x04 // Controls the interrupt-on-change for each pin of port A.
#define I2C_GPINTEN_B 0x05 // Controls the interrupt-on-change for each pin of port B.
#define I2C_DEFVAL_A  0x06 // Controls the default comparaison value for interrupt-on-change for port A.
#define I2C_DEFVAL_B  0x07 // Controls the default comparaison value for interrupt-on-change for port B.
#define I2C_INTCON_A  0x08 // Controls how the associated pin value is compared for the interrupt-on-change for port A.
#define I2C_INTCON_B  0x09 // Controls how the associated pin value is compared for the interrupt-on-change for port B.
#define I2C_IOCON_A   0x0A // Controls the device.
#define I2C_IOCON_B   0x0B // Controls the device.
#define I2C_GPPU_A    0x0C // Controls the pull-up resistors for the port A pins.
#define I2C_GPPU_B    0x0D // Controls the pull-up resistors for the port B pins.
#define I2C_INTF_A    0x0E // Reflects the interrupt condition on the port A pins.
#define I2C_INTF_B    0x0F // Reflects the interrupt condition on the port B pins.
#define I2C_INTCAP_A  0x10 // Captures the port A value at the time the interrupt occured.
#define I2C_INTCAP_B  0x11 // Captures the port B value at the time the interrupt occured.
#define I2C_GPIO_A    0x12 // Reflects the value on the port A.
#define I2C_GPIO_B    0x13 // Reflects the value on the port B.
#define I2C_OLAT_A    0x14 // Provides access to the port A output latches.
#define I2C_OLAT_B    0x15 // Provides access to the port B output latches.

int calls = 0;

void writeI2C(int I2Caddress, byte memAddr, byte memValue) {
  Wire.beginTransmission(I2Caddress);
  Wire.write(memAddr);
  Wire.write(memValue);
  Wire.endTransmission();
}

void interruptHandler() {
  calls++;
}

void setup() {
  Serial.begin(9600);
  pinMode(PWREN, OUTPUT);
  digitalWrite(PWREN, HIGH);

  Wire.begin(); // enable Wire bus
  Wire.setClock(400000); //I2C 400kHz

  setupI2CButtons();

  attachInterrupt(digitalPinToInterrupt(TEENSY_INTERRUPT_PIN), interruptHandler, FALLING);
}

void setupI2CButtons() {
  writeI2C(I2C_BUTTONS, I2C_IOCON_A, 0x40);      // MIRROR MODE
  writeI2C(I2C_BUTTONS, I2C_IOCON_B, 0x40);      // MIRROR MODE

  // PORT A: encoder switches
  writeI2C(I2C_BUTTONS, I2C_IODIR_A, 0xFF);    // mode (FF -> input 00; -> output)
  writeI2C(I2C_BUTTONS, I2C_GPPU_A, 0x00);     // pullup
  writeI2C(I2C_BUTTONS, I2C_GPINTEN_A, 0xFF);  // interrupt

  // PORT B: buttons
  writeI2C(I2C_BUTTONS, I2C_IODIR_B, 0xFF);    // mode (FF -> input; 00 -> output)
  writeI2C(I2C_BUTTONS, I2C_GPPU_B, 0x00);     // pullup
  writeI2C(I2C_BUTTONS, I2C_GPINTEN_B, 0xFF);  // interrupt
}

void loop() {
  Wire.requestFrom(I2C_BUTTONS, 2); //request TWO bytes from the pointed location from above
  
  Serial.print("calls:");
  Serial.print(calls);
  Serial.println(" ");

  delay(25);
}

The interrupt only works if I call Wire.requestFrom on the loop function, is there any reason for that or Am I missing some I2C configurations of the MCP?

Thanks!
 
The MCP will keep the interrupt pin low until you read the input register of the port that caused the interrupt, so you have to read the data in your ISR.
This also implies that the correct interrupt mode is LOW, not FALLING.

Pieter
 
The MCP will keep the interrupt pin low until you read the input register of the port that caused the interrupt, so you have to read the data in your ISR.
This also implies that the correct interrupt mode is LOW, not FALLING.

Pieter

Thank-you Pieter,

I've changed the code as you suggested, moving the Wire.requestFrom in the ISR and setting the interrupt mode to LOW and solved it!

What if I need to read from another address? Can I set another interrupt or I can only use the one on pin 4?

Daniele
 
When a pin of the MCP changes state, the interrupt output goes low. This should trigger an interrupt on the Teensy. In the ISR, you read the MCP input register, and this read request causes the MPC to make the interrupt output high again.

If you don't read the register, the interrupt pin will never go high again, so it can never trigger a “falling” interrupt more than once, and it will keep on triggering a “low” interrupt.
 
I see you've edited your reply, glad you got it working.

If you have multiple chips, you could tie all of their interrupt pins together (use open-drain output on the MCPs and input pull-up enabled on the Teensy). When an interrupt fires, you don't know which MPC triggered it, so you have to read all of them. Depending on your application, this may or may not be a good approach. Alternatively, you can use one interrupt pin for each MCP, as long as you have interrupt pins available on the Teensy, of course.

Note: in most applications, especially buttons (the humans pressing them are extremely slow in comparison to the Teensy), you really don't need to use interrupts, polling the “interrupt” pin is enough to know when the inputs changed, you don't have to handle those changes instantly inside of an ISR on the Teensy.
 
I see you've edited your reply, glad you got it working.

If you have multiple chips, you could tie all of their interrupt pins together (use open-drain output on the MCPs and input pull-up enabled on the Teensy). When an interrupt fires, you don't know which MPC triggered it, so you have to read all of them. Depending on your application, this may or may not be a good approach. Alternatively, you can use one interrupt pin for each MCP, as long as you have interrupt pins available on the Teensy, of course.

Note: in most applications, especially buttons (the humans pressing them are extremely slow in comparison to the Teensy), you really don't need to use interrupts, polling the “interrupt” pin is enough to know when the inputs changed, you don't have to handle those changes instantly inside of an ISR on the Teensy.

Yes, I've edited the reply after figuring out some mistakes :)

In my case I have 2 MCP, one for buttons and one for encoders and I have 2 interrupt pins so in one I read all the buttons and on the other I read the encoders.

I've setup the second MCP with another interrupt and also for this one on the ISR I read the value from the MCP.
The ISR fires but now I don't know how to get the information to retrive the value of the encoder.

The encoder pins are wired in this way to the MCP:

Code:
GPA0	ENC1B
GPA1	ENC1A
GPA2	ENC2B
GPA3	ENC2A
GPA4	ENC3B
GPA5	ENC3A
GPA6	ENC4B
GPA7	ENC4A
GPB0	ENC5B
GPB1	ENC5A

How can I read them on the ISR?

right now I read only the 2 bytes, in the same way of the buttons:

Code:
Wire.requestFrom(I2C_ENCODERS, 2);
uint16_t reading = 0;   // -> one line: (uint16_t)(Wire.read() << 8) | Wire.read();
reading = Wire.read();  // receive high byte (overwrites previous reading)
reading = reading << 8; // shift high byte to be high 8 bits
reading |= Wire.read(); // receive low byte as lower 8 bits
Serial.println(reading, BIN);

I use interrupts for encoders connected straight to the teensy in this way:

Code:
bool Encoder::InterruptCallback() {
  if (digitalRead(_pin2) == LOW) {
    return false;
  }

  // code removed because out of scope...

  if (digitalRead(_pin1) == digitalRead(_pin2)) {
    _value += increase;
  } else {
    _value -= increase;
  }
  
  return true;
}

How can I manage this on the MCP with Wire.read()?
Is there any better way to do that?

Thanks a lot!
 
Thanks for the help,

I will take a look at it.

Actually I'd like to implement it by myself to better understand how it works, but reading it help me to find a way to develop my own version.

The code for managing the reading starts from line 209:

Code:
void update() {
        // Only update if a pin change interrupt happened
        if (interrupt_pin != NO_PIN &&
            ExtIO::digitalRead(interrupt_pin) == HIGH)
            return;
        // Read both GPIO A and B
        uint16_t newstate = readGPIO();
        uint16_t oldstate = state;

        // If the state didn't change, do nothing
        if (newstate == oldstate)
            return;

        // Save the new state
        state = newstate;

        // For each encoder, compare the new state of its two pins
        // to the old state. Combine the four states into a 4-bit
        // number and use a lookup table to determine the delta between
        // the two encoder positions.
        for (uint8_t i = 0; i < 8; ++i) {
            uint8_t change =
                uint8_t(newstate) & 0b11; // Top two bits are new pin states
            change <<= 2;
            change |=
                uint8_t(oldstate) & 0b11; // Bottom two bits are old pin states
            auto delta =
                static_cast<EncoderPositionType>(MCP23017Encoders_lut[change]);
            if (delta != 0) { // small speedup on AVR
                positions[i] += delta;
            }
            oldstate >>= 2;
            newstate >>= 2;
        }
    }

And I need to understand that part.
 
Basically it just reads the states of all the pins, and for each pair of pins, it compares them to the previous states. This is done by combining the two new states and the two old states as a 4-bit number: "new state B | new state A | old state B | old state A". There are only 16 possibilities, so you just look them up in a table (which is at the top of the file), which gives you the delta.

See also: https://github.com/PaulStoffregen/E...27de9e3fd7fc8362c6909b009/Encoder.h#L155-L178
 
Status
Not open for further replies.
Back
Top