RE MCP23017 and interrupts and Adafruit MCP23017 library

I have been playing around with one of these and looking at a number of examples of setting up interrupts.

From the examples I am getting the impression that the only way they will work is to pull the input pin high by an external pull-up resistor or the internal ones and then pulling the pin low to generate the interrupt.

Is that correct or is it also possible to pull the input pin low and generate the interrupt by taking it high?

Is your query about interrupts on the Arduino or the MCP20317 ?

If the former then look at https://www.arduino.cc/en/Reference/AttachInterrupt

Are you referring to the MCP23017?

In the datasheet there is an INTPOL (interrupt polarity) bit which configures if an interrupt is active high or active low.

UKHeliBob: Is your query about interrupts on the Arduino or the MCP20317 ?

If the former then look at https://www.arduino.cc/en/Reference/AttachInterrupt

Sorry I meant setting up interrupts on the MCP20317.

I am confident about how to setup interrupts on the arduino.

But I am trying to make sense of the mirroring and polarity etc on the MCP20317.

Also I am using the Adafruit MCP20317 library and, through a bit of trial and error, I have found that the only way I can re-trigger an interrupt on the MCP is to briefly connect ts reset pin to GND and then back to 5V.

Both with the example provided with the library and example I have created that sets up interrupts on the arduino in the conventional manner (not in the loop() function).

Am I missing something here?

Is there another way you are supposed to reset the interrupts through a library function call rather than GNDing the MCP reset pin?

[quote author=Nick Gammon link=msg=2577352 date=1453458835] Are you referring to the MCP23017?

In the datasheet there is an INTPOL (interrupt polarity) bit which configures if an interrupt is active high or active low.

[/quote] Excuse my momentary dyslexia - I fixed the heading of my original post.

This interrupt example from the Adafruit MCP23017 library does not seem to work for me.

It wont generate interrupts on the arduino and the LED pin does not flash.

Has any one tried it and gotten it to work on their breadboard?

Are there any known problems with this library and/or this example?

// Install the LowPower library for optional sleeping support.
// See loop() function comments for details on usage.
//#include <LowPower.h>

#include <Wire.h>
#include <Adafruit_MCP23017.h>

CMCP23017 mcp;

byte ledPin=13;

// Interrupts from the MCP will be handled by this PIN
byte arduinoIntPin=3;

// ... and this interrupt vector
byte arduinoInterrupt=1;

volatile boolean awakenByInterrupt = false;

// Two pins at the MCP (Ports A/B where some buttons have been setup.)
// Buttons connect the pin to grond, and pins are pulled up.
byte mcpPinA=7;
byte mcpPinB=15;

void setup(){

  Serial.begin(9600);
  Serial.println("MCP23007 Interrupt Test");

  pinMode(arduinoIntPin,INPUT);

  mcp.begin();      // use default address 0
  
  // We mirror INTA and INTB, so that only one line is required between MCP and Arduino for int reporting
  // The INTA/B will not be Floating 
  // INTs will be signaled with a LOW
  mcp.setupInterrupts(true,false,LOW);

  // configuration for a button on port A
  // interrupt will triger when the pin is taken to ground by a pushbutton
  mcp.pinMode(mcpPinA, INPUT);
  mcp.pullUp(mcpPinA, HIGH);  // turn on a 100K pullup internally
  mcp.setupInterruptPin(mcpPinA,FALLING); 

  // similar, but on port B.
  mcp.pinMode(mcpPinB, INPUT);
  mcp.pullUp(mcpPinB, HIGH);  // turn on a 100K pullup internall
  mcp.setupInterruptPin(mcpPinB,FALLING);

  // We will setup a pin for flashing from the int routine
  pinMode(ledPin, OUTPUT);  // use the p13 LED as debugging
  
}

// The int handler will just signal that the int has happen
// we will do the work from the main loop.
void intCallBack(){
  awakenByInterrupt=true;
}

void handleInterrupt(){
  
  // Get more information from the MCP from the INT
  uint8_t pin=mcp.getLastInterruptPin();
  uint8_t val=mcp.getLastInterruptPinValue();
  
  // We will flash the led 1 or 2 times depending on the PIN that triggered the Interrupt
  // 3 and 4 flases are supposed to be impossible conditions... just for debugging.
  uint8_t flashes=4; 
  if(pin==mcpPinA) flashes=1;
  if(pin==mcpPinB) flashes=2;
  if(val!=LOW) flashes=3;

  // simulate some output associated to this
  for(int i=0;i<flashes;i++){  
    delay(100);
    digitalWrite(ledPin,HIGH);
    delay(100);
    digitalWrite(ledPin,LOW);
  }

  // we have to wait for the interrupt condition to finish
  // otherwise we might go to sleep with an ongoing condition and never wake up again.
  // as, an action is required to clear the INT flag, and allow it to trigger again.
  // see datasheet for datails.
  while( ! (mcp.digitalRead(mcpPinB) && mcp.digitalRead(mcpPinA) ));
  // and clean queued INT signal
  cleanInterrupts();
}

// handy for interrupts triggered by buttons
// normally signal a few due to bouncing issues
void cleanInterrupts(){
  EIFR=0x01;
  awakenByInterrupt=false;
}  

/**
 * main routine: sleep the arduino, and wake up on Interrups.
 * the LowPower library, or similar is required for sleeping, but sleep is simulated here.
 * It is actually posible to get the MCP to draw only 1uA while in standby as the datasheet claims,
 * however there is no stadndby mode. Its all down to seting up each pin in a way that current does not flow.
 * and you can wait for interrupts while waiting.
 */
void loop(){
  
  // enable interrupts before going to sleep/wait
  // And we setup a callback for the arduino INT handler.
  attachInterrupt(arduinoInterrupt,intCallBack,FALLING);
  
  // Simulate a deep sleep
  while(!awakenByInterrupt);
  // Or sleep the arduino, this lib is great, if you have it.
  //LowPower.powerDown(SLEEP_1S, ADC_OFF, BOD_OFF);
  
  // disable interrupts while handling them.
  detachInterrupt(arduinoInterrupt);
  
  if(awakenByInterrupt) handleInterrupt();
}

According to the datasheet:

Input change activity on a port input pin that is enabled for IOC will generate an internal device interrupt and the device will capture the value of the port and copy it into INTCAP. The interrupt will remain active until the INTCAP or GPIO register is read. Writing to these registers will not affect the interrupt. The interrupt condition will be cleared after the LSb of the data is clocked out during a read command of GPIO or INTCAP.

So, reading the data clears the interrupt. The interrupt should stay enabled (but not active) as far as I can see.

I haven't used the Adafruit library. Dealing with the chip directly isn't that hard. I have some stuff about using the MCP23017 here.

I have some test code below which tests interrupts on the MCP23017. I just checked and it works fine:

/// Author: Nick Gammon
// Date: 19 February 2011

// Demonstration of an interrupt service routine connected to the MCP23017

#include <Wire.h>

// MCP23017 registers (everything except direction defaults to 0)

#define IODIRA   0x00   // IO direction  (0 = output, 1 = input (Default))
#define IODIRB   0x01
#define IOPOLA   0x02   // IO polarity   (0 = normal, 1 = inverse)
#define IOPOLB   0x03
#define GPINTENA 0x04   // Interrupt on change (0 = disable, 1 = enable)
#define GPINTENB 0x05
#define DEFVALA  0x06   // Default comparison for interrupt on change (interrupts on opposite)
#define DEFVALB  0x07
#define INTCONA  0x08   // Interrupt control (0 = interrupt on change from previous, 1 = interrupt on change from DEFVAL)
#define INTCONB  0x09
#define IOCON    0x0A   // IO Configuration: bank/mirror/seqop/disslw/haen/odr/intpol/notimp
//#define IOCON 0x0B  // same as 0x0A
#define GPPUA    0x0C   // Pull-up resistor (0 = disabled, 1 = enabled)
#define GPPUB    0x0D
#define INFTFA   0x0E   // Interrupt flag (read only) : (0 = no interrupt, 1 = pin caused interrupt)
#define INFTFB   0x0F
#define INTCAPA  0x10   // Interrupt capture (read only) : value of GPIO at time of last interrupt
#define INTCAPB  0x11
#define GPIOA    0x12   // Port value. Write to change, read to obtain value
#define GPIOB    0x13
#define OLLATA   0x14   // Output latch. Write to latch output.
#define OLLATB   0x15


#define port 0x20  // MCP23017 is on I2C port 0x20

#define ISR_INDICATOR 12  // pin 12
#define ONBOARD_LED 13    // pin 13

volatile bool keyPressed;

// set register "reg" on expander to "data"
// for example, IO direction
void expanderWriteBoth (const byte reg, const byte data ) 
{
  Wire.beginTransmission (port);
  Wire.write (reg);
  Wire.write (data);  // port A
  Wire.write (data);  // port B
  Wire.endTransmission ();
} // end of expanderWrite

// read a byte from the expander
unsigned int expanderRead (const byte reg) 
{
  Wire.beginTransmission (port);
  Wire.write (reg);
  Wire.endTransmission ();
  Wire.requestFrom (port, 1);
  return Wire.read();
} // end of expanderRead

// interrupt service routine, called when pin D2 goes from 1 to 0
void keypress ()
{
  digitalWrite (ISR_INDICATOR, HIGH);  // debugging
  keyPressed = true;   // set flag so main loop knows
}  // end of keypress

void setup ()
{
  pinMode (ISR_INDICATOR, OUTPUT);  // for testing (ISR indicator)
  pinMode (ONBOARD_LED, OUTPUT);  // for onboard LED

  Wire.begin ();  
  Serial.begin (115200); 
  Serial.println ("Starting ..."); 

  // expander configuration register
  expanderWriteBoth (IOCON, 0b01100000); // mirror interrupts, disable sequential mode
 
  // enable pull-up on switches
  expanderWriteBoth (GPPUA, 0xFF);   // pull-up resistor for switch - both ports

  // invert polarity
  expanderWriteBoth (IOPOLA, 0xFF);  // invert polarity of signal - both ports
  
  // enable all interrupts
  expanderWriteBoth (GPINTENA, 0xFF); // enable interrupts - both ports
  
  // no interrupt yet
  keyPressed = false;

  // read from interrupt capture ports to clear them
  expanderRead (INTCAPA);
  expanderRead (INTCAPB);
  
  // pin 19 of MCP23017 is plugged into D2 of the Arduino which is interrupt 0
  attachInterrupt(0, keypress, FALLING);
  
}  // end of setup

// time we turned LED on
unsigned long time = 0;

// called from main loop when we know we had an interrupt
void handleKeypress ()
{
  unsigned int keyValue = 0;
  
  delay (100);  // de-bounce before we re-enable interrupts
  
  keyPressed = false;  // ready for next time through the interrupt service routine
  digitalWrite (ISR_INDICATOR, LOW);  // debugging
  
  // Read port values, as required. Note that this re-arms the interrupts.
  if (expanderRead (INFTFA))
    keyValue |= expanderRead (INTCAPA) << 8;    // read value at time of interrupt
  if (expanderRead (INFTFB))
    keyValue |= expanderRead (INTCAPB);        // port B is in low-order byte
  
  Serial.println ("Button states");
  Serial.println ("0                   1");
  Serial.println ("0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5");
  
  // display which buttons were down at the time of the interrupt
  for (byte button = 0; button < 16; button++)
    {
    // this key down?
    if (keyValue & (1 << button))
      Serial.print ("1 ");
    else
      Serial.print ("0 ");
    
    } // end of for each button

  Serial.println ();
  
  // if a switch is now pressed, turn LED on  (key down event)
  if (keyValue)
    {
    time = millis ();  // remember when
    digitalWrite (ONBOARD_LED, HIGH);  // on-board LED
    }  // end if
  
}  // end of handleKeypress

void loop ()
{
  // was there an interrupt?
  if (keyPressed)
    handleKeypress ();

  // turn LED off after 500 ms 
 if (millis () > (time + 500) && time != 0)
   {
    digitalWrite (ONBOARD_LED, LOW);
    time = 0;
   }  // end if time up
 
}  // end of loop

Wiring:

Put an LED on pin 12 (to see interrupts). Ground any of the data pins and the ISR LED should flash. Plus in the serial monitor you will see the pin states.

These MCP23017 interrupts just don’t make sense.

The idea of the code below is to have the MCP23017 trigger int0/dp2 and int1/dp3 on a nano separately, with 2 separate ISRs turning on dp11 and dp12 separately, each attached to a different color LED.

Each ISR has its own flag which is set in the ISR.

The loop function then resets those flags and turns off the appropriate dp and attached LED - causing a different color flash according to which ISR was triggered from the MCP.

However if I disconnect either one of the wires connecting the MCP int pins from the nano then touching both mcp portA 0 and mcp portA 1 with a wire connected to 5V both cause nano int0 to trigger or both cause nano int1 to trigger.

In other words the mcp int pins still seemed to be in mirror mode despite this: mcp.setupInterrupts(false, false, HIGH);

And with this code I can only trigger and interrupt once. In order to trigger it gain I have to reset the arduino. Digital pin 6 on the nano is connected to the rest pin of the mcp so that I can manually reset it in code so that I could continually trigger interrupts with the interrupt example that came with the library - it seemed to work, but not with my code below.

Is there a problem with the adafruit library?
Is there a problem with my MCP23017?
Is there a problem with my breadbaord setup?
Is there another library for this device that people have used and found to be reliable?

Adafruit-MCP23017 library files attached.

// Install the LowPower library for optional sleeping support.
// See loop() function comments for details on usage.
//#include <LowPower.h>

#include <Wire.h>
#include <Adafruit_MCP23017.h>

CMCP23017 mcp;
uint8_t nArdLEDPin1 = 12, nArdLEDPin2 = 11, nMCPResetPin = 6; 
volatile bool bISR1Flag = false, bISR2Flag = false;

void MCPInt1ISR()
{
  digitalWrite(nArdLEDPin1, HIGH);
  bISR1Flag = true;
}

void MCPInt2ISR()
{
  digitalWrite(nArdLEDPin2, HIGH);
  bISR2Flag = true;
}

void setupMCPInterrupts()
{
  // We mirror INTA and INTB, so that only one line is required between MCP and Arduino for int reporting
  // The INTA/B will not be Floating 
  // INTs will be signaled with a LOW
  mcp.setupInterrupts(false, false, HIGH);

  // configuration for a button on port A
  // interrupt will triger when the pin is taken to ground by a pushbutton
  mcp.pinMode(0, INPUT);
  //mcp.pullUp(0, HIGH);  // turn on a 100K pullup internally
  mcp.setupInterruptPin(0, RISING); 

  // similar, but on port B.
  mcp.pinMode(1, INPUT);
  //mcp.pullUp(1, HIGH);  // turn on a 100K pullup internall
  mcp.setupInterruptPin(1, RISING);
}

void setup()
{
  Serial.begin(115200);
  //Serial.println("MCP23007 Interrupt Test");

  mcp.begin();      // use default address 0
  

  pinMode(nMCPResetPin, OUTPUT);
  digitalWrite(nMCPResetPin, HIGH);
  pinMode(nArdLEDPin1, OUTPUT);
  pinMode(nArdLEDPin2, OUTPUT);
  digitalWrite(nArdLEDPin1, LOW);
  digitalWrite(nArdLEDPin2, LOW);
  setupMCPInterrupts();
  attachInterrupt(0, MCPInt1ISR, RISING);
  attachInterrupt(1, MCPInt2ISR, RISING);
}

/**
 * main routine: sleep the arduino, and wake up on Interrups.
 * the LowPower library, or similar is required for sleeping, but sleep is simulated here.
 * It is actually posible to get the MCP to draw only 1uA while in standby as the datasheet claims,
 * however there is no stadndby mode. Its all down to seting up each pin in a way that current does not flow.
 * and you can wait for interrupts while waiting.
 */
void loop()
{
  delay(200);
  
  if (bISR1Flag)
  {
    digitalWrite(nArdLEDPin1, LOW);
    Serial.println("XXXXXXXXX");
    bISR1Flag = false;
  }
  if (bISR2Flag)
  {
    digitalWrite(nArdLEDPin2, LOW);
    Serial.println("YYYYYYYYY");
    bISR2Flag = false;
  }
  if (bISR1Flag || bISR2Flag)
  {
    Serial.println("ZZZZZZZZZ");
    digitalWrite(nMCPResetPin, LOW);
    delay(10);
    digitalWrite(nMCPResetPin, HIGH);
    setupMCPInterrupts();
  }  
}

Adafruit_MCP23017.h (2.91 KB)

Adafruit_MCP23017.cpp (7.06 KB)

MCP23017 interrupts and arduino (sorry for re-posting - can't find my original)

Try a little harder next time. I put a bit of work into my reply. If you click on your profile you can see the posts you made. I found your previous thread in a couple of minutes.

Threads merged.

[quote author=Nick Gammon link=msg=2578760 date=1453528280] Try a little harder next time. I put a bit of work into my reply. If you click on your profile you can see the posts you made. I found your previous thread in a couple of minutes.

Threads merged. [/quote] I was looking for the word 'profile' (or something similar) - other forums have it some where near the top of the pqge.

But I just tried clicking on the icon thing and found my profile.

The forum is a bit quirky, but if you click on anyone's icon next to a post (including yours) you can see a link to recent posts (on the page that appears).

Nick I decided to write my own MCP23017 library to get a better understanding of how they work and in case there are problems in the adafruit library with the interrupt related functions.

There does not seem to be many arduino libraries for MCP23017 at all - I have only found two, one of which does not have interrupt related functions.

But I am having problems with my read function which seems to be returning 0b11111111 no matter what I do with the pins on the bread board.

Can you spot anything I am doing wrong in the code.

Here are the relevant functions but the files are attached if needed.

uint8_t CMCP23017::readReg(const uint8_t nRegAddr)
{
 const uint8_t nCtrlByte = 0b01000001;

 Wire.beginTransmission(nCtrlByte | m_nHardwareAddr);
 wireSend(nRegAddr);
 Wire.endTransmission();
 Wire.requestFrom(nCtrlByte & m_nHardwareAddr, 1);
 return wireRecv();

}

uint8_t CMCP23017::getPort(const uint8_t nPinNum, const uint8_t nPortA, const uint8_t nPortB)
{
 uint8_t nAddrPort = 0;

 if ((nPinNum >= 0) && (nPinNum <= 7))
 nAddrPort = PORT(nPortB);
 else if ((nPinNum >= 8) && (nPinNum <= 15))
 nAddrPort = PORT(nPortA);
 
 return nAddrPort;
}

uint8_t CMCP23017::getPinMask(uint8_t nPinNum)
{
 switch (nPinNum)
 {
 case 0:
 case 8: nPinNum |= 0b00000001; break;
 case 1:
 case 9: nPinNum |= 0b00000010; break;
 case 2:
 case 10: nPinNum |= 0b00000100; break;
 case 3:
 case 11: nPinNum |= 0b00001000; break;
 case 4:
 case 12: nPinNum |= 0b00010000; break;
 case 5:
 case 13: nPinNum |= 0b00100000; break;
 case 6:
 case 14: nPinNum |= 0b01000000; break;
 case 7:
 case 15: nPinNum |= 0b10000000; break;
 }
 return nPinNum;
}

uint8_t CMCP23017::read(const uint8_t nPinNum)
{
 // Get value from MCP23017 port A if nPinNum is between 8 and 15 or from port B if nPinNum is between 0 and 7
 uint8_t nPort = getPort(nPinNum, GPIOA, GPIOB),
 nPortVal = readReg(nPort);
debug("nPort", nPort, HEX);
debug("nPortVal", nPortVal, BIN);
debug("getPinMask(nPinNum)", getPinMask(nPinNum), BIN);
 // Reset all the pin values except for the one we are reading.
 nPortVal &= getPinMask(nPinNum);

 if (nPortVal == 0)
 nPortVal = LOW;
 else
 nPortVal = HIGH;

 return nPortVal;
}

MCP23017.cpp (5.07 KB)

MCP23017.h (4.2 KB)

I can see from your code from here: http://www.gammon.com.au/forum/?id=10940

That your control byte is 0x20:

Wire.beginTransmission (0x20);  // expander has I2C address 0x20
Wire.send (0x00);   // register 0 is the I/O direction register for Port A
Wire.send (0x00);   //  0x00 for all pins to output mode, 0xFF for all pins to input mode
Wire.endTransmission ();

So that would be 0010 0000 where 0000 corresponds to A2,A1,A0,R(1)/W(0)

According to the datasheet the control byte should be 0100 0000 so it should be Wire.beginTransmission (0x40)

What is the reason for this discrepancy?

Wire.beginTransmission (0x20);  // expander has I2C address 0x20

That is the address of the device, not the control byte.

Or are you referring to the fact that the I2C library automatically shifts the address left by one bit?

http://www.gammon.com.au/i2c

[quote author=Nick Gammon link=msg=2580573 date=1453624643]

Wire.beginTransmission (0x20);  // expander has I2C address 0x20

That is the address of the device, not the control byte. [/quote] It still doesn't make any sense to me Nick. Where is the datasheet does it say that the device address is 0x20?

The data sheet specifically says that the control byte you send to the MCP via I2C contains the device address as follows:

S 0 1 0 0 A2 A1 A0 R/W ACK

So where does this fit into the wire library? And is this format the same for all I2C chips?

OK, from the datasheet, the address is:

0  1  0  0  A2  A1  A0  R/W

S is the start bit (not part of the address) and ACK is the acknowledge bit (not part of the address).

Thus, if you jumper the address pins to Gnd you have:

0  1  0  0  0  0  0  R/W

Which is:

0x40 (for writing)
0x41 (for reading)

The Wire library saves you sending two addresses (0x40 and 0x41) depending on whether you are reading or writing so it wants you to shift the address right one:

0x20

Which is what I had:

Wire.beginTransmission (0x20);  // expander has I2C address 0x20

Then the library shifts it left again by one bit and ORs in either 0 or 1 depending on whether it is a write or read.

A lot of this is explained on my page: http://www.gammon.com.au/i2c

Not all that intuitive way of implementing it in the wire library from where I am sitting but OK, now it makes sense.

I have made my own library for this chip and implemented some of the MCP23017 functions that were not implemented in the adafruit MCP23017 library. E.G. Registers in two banks or one bank and auto-increment pointer or not.

And I have made my interrupt functions more closely reflect the way the interrupt functionality is implemented in the MCP23017.

void setInterruptBehavior(const bool bActiveHigh = true, const bool bMirror = false, const bool bOpenDrain = false);

void setChangeInterrupt(const uint8_t nPin, const bool bCompareVal, const bool bDoCompareToVal);

void setInterrupt(const uint8_t nPin);

The way they were implemented with the adafruit library was a little confusing - the interrupt behavior params were included in the function used to enable an interrupt thus giving me the impression that each interrupt you set was individually configurable in all respects.

I also finally realised why my interrupts were not behaving as I expected them to.

You can set an interrupt on all pins if you wish, but pins 0-7 will be mapped to int pin A and pins 8-15 will be mapped to int pin B.

So if I have two separate interrupts on the arduino, from mcp int A pin and mcp int b pin, then the former must be triggered from a GPIOA pin and the latter must be triggered from a GPIOB pin.

Probably should work out a way to make this more obvious in my own library interrupt related functions.

Once I finish perfecting and testing it are you guys interested in including it on the playground library page?

In the MCP23017 datasheet:

05h 0Ah IOCON 15h 0Bh IOCON

This is unclear to me.

Do 05h, 0Ah, 15h and 0Bh all refer to the same register, i.e. one register controls both banks.

Or is it a typo and should read as follows?

05h 0Ah IOCONA 15h 0Bh IOCONB

What does this mean?

INTPOL: This bit sets the polarity of the INT output pin. 1 = Active-high. 0 = Active-low.

Does it mean that the int pin has a standing state of high if the bit is set to 1 or does it mean the int pin goes HIGH when it triggers?

In which case there are separate control registers for each bank?