Setting up multiple MCP23017 Interrupts on the Arduino:
There is a trick to getting them going...
...An extra function is needed to report the interrupt flag state.
This page is all about how to use multiple MCP23017 interrupts. If you are
looking for more information on the MCP23017 then goto this page where there is more general information on all
aspects of the device including other example code and register
descriptions.
In the page link above there is an example of interrupt usage but that is
for one device. What prompted me to write this page was a question from Victor,
which is shown below:
I have read your excellent work on MCP23017, the best yet on the web! I do need your expertise
with my RFID-chessboard project. My prototype uses mcp23017 controlling a
matrix of antennae (underneath a chess square) connected to only one RFID
reader module. The proof of concept works well with the antennae matrix. Now I
need to progress from this stage of the project. To speed up the system I need
to put hall-effect switches on each of the chess square. I would need to use 8
mcp23017s, 4 to control the antennae and 4 to monitor the switches.That means I
have to use interrupts for the switches.
The help I need is how to I go about monitoring 4 MCPs Interrupts/ BTW I
plan on using the Centipede from Macetech.com.
Thanks
Victor Feria
Reprinted with permission.
I had a look at the centipede product and it looks like a useful little
board. It has four MCP23017 devices providing I/O capability of 64 pins and you
can attach another one onto the I2C bus for a maximum of 128 I/O pins. On the
board is a jumper to set the address range of each board.
You can then use the software library provided by macetech to access pins on
each board (2 boards maximum for maximum 128 pins) in a similar way to using
Arduino functions such as digitalWrite etc.
MCP23017 GPA7, GPB7 problem
Warning: The latest
datasheet - Revision D (June 2022) - says you must not use GPA7
& GPB7 as inputs pins - even though you can set them as inputs!
The data sheet does not say why but the microchip site has more details; summarised here:
If the pin voltagechanges while these pins are being read during transmission
of a bit, the SDA signal can be corrupted. This can cause bus host
malfunction. The relevant pins in the library used below are P7 and P15.
So you may get reasonable operation with these pins as inputs but there is a chance it could go wrong!
See the highlights section (top of pdf) Rev.c 2022 datasheet. The only other place this is mentioned in the datasheet is in 3.5.1 I/O DIRECTION REGISTER.
Note that it specifically says MCP23017, and it looks like an I2C specific problem.
This data sheet is an MCP23017/MCP23S17 combined information - the SPI version does not have this problem.
Assumptions about the board
I don't have one myself so have made some, hopefully reasonable, assumptions
about the pcb since the website does not show the actual pcb traces.
Assumptions are:
INTA outputs from each MCP23017 interrupt pin are not connected anywhere
else on the board.
There is no connection to Arduino pin 3 on the centipede board.
The board is set to address 0 ( jumper on left side) - meaning that
MCP23017 pins 0 - 64 are at address 0 - 64 i.e. low addresses in
software.
Make the board start at address 0 : jumper to left side of board.
Connect all INTA pads together and the connect them to pin 3 (an
interrupt pin).
Connect a 10k resistor from one of the INTA pads and to 5V.
TIP: I noticed that each board has a set of I2C pullup
resistors - If they come pre-fitted then when using 2 boards you only need one
set. The lower the resistor value the faster the edge but also the more power
is used (higher current). If you leave them on both boards the value of
resistor will be in parallel i.e. half the measured R4 or R5. If they are 1k
then 500 Ohms is getting a little low - pull down current will be 5/500 = 10mA
- not too much but unnecessary.
Pin Mapping to numbers
You can get a bit confused (or at least I did) - just not paying enough
attention. Port B is occupying pins 1 to 8 of the chip (see below) and you tend
to assume that these are the 1st ports. However port A occupies pins 21 to 28.
In the Centipede layout PORT A is used first - this makes sense to use the
lowest port identifier first. It is a shame that the 23017 creators did not
label 1-8 as Port A.
Therefore when thinking of Arduino 'like' pinouts, pin zero is chip pin 21
of the MCP23017 (and not conveniently pin 1 of the chip!).
When using more than one MCP23017, if you wanted to
access pin 20 (this will be the 2nd port (or chip) and it will be GPA4 - four pins
down on the right hand side of the chip.
(GPA0 = P16, GPA1 = P17, GPA2
= P18, GPA3 = P19, GPA4 = P20 - where P,<n> stands means an Arduno like
pin).
Software Library and versions
Arduino IDE Version
Version : 1.8.5
Library
Macetech provides its own library for the boards (maximum of two connected
boards) and this provides arduino-like command functions. Download the library
(Centipede.zip) and unzip it in your arduino library folder; Something like:
c:/user/Documents/Arduino - create a folder here called Centipede and unzip
into that!
Download Centipede.zip from here
from http://macetech.com
The library functions for interrupt control in the Centipede library are:
portInterrupts
portIntPinConfig
The member function portInterrupts, has a hidden operation that sets the
MIRROR bit for sub-ports A and B so any interrupt on A or B sub-ports (GPIOA
and GPIOB) is also signalled at both INTA and INTB pins.
Incorrect writing of
Centipede.cpp function:
Warning: The following Member Function (of Centipede.cpp) is
wrong
The library has not been developed far enough for multiple open drain
operation and is missing a member function to detect the state of INTFA and
INTFB registers 0x0e and 0x0f. These return an interrupt flag state that allows
you to determine which port (device) produced an interrupt (and it could be
more than one device).
Add the member function into centipede.h within the public definitions:
uint16_tgetIntF(intport);
Example MCP23017 Interrupt
Code
The following example shows how to use multiple interrupts from MCP23017
chips and feed them into one external interrupt on the Arduino. To do this a
modified centipede library is used (see above code snippet).
The following code also uses the same principle described in using
interrupts on this
page. That is, allowing wire-library interrupts while within the MCP23017
interrupt routine.
Note: The routine findPorts() was used because I only used one MCP23017 to
test the code. The chip's address was set to '1' meaning it is the 2nd chip out
of device addresses 0-3.
Detecting Presence of
MCP23017 chips
When retrieving INTF data from each device, if a device is not present then,
in my case, the returned data result was 0xffff. This meant that all 16
interrupt bits were high (actually not there at all), so the interrupt flag was
detected in error for non-existent chips.
The solution to that was to create the function findPorts() which retrieves
the IOCON.MIRROR bit from the device, inverts it and writes it back. Then reads
it again. if the returned bit is inverted then the device is present and the
relevant index in array portExists[] is set.
Note: you should change portExists[] to a 16 bit integer and
use bit manipulation to set and retrieve flag bits if you are worried about
saving RAM space. However the array indexing method is fast.
You could find the routine useful for checking :
That all chips are operational.
if a second centipede board is attached.
Multiple MCP23017 Interrupts
Setup()
in the setup() routine a serial instance is instantiated (initialised). This
is only used for showing the presence of chips as described above - if chips
are not present then you could indicate that via the on board LED and remove
the serial output. Instead in this code the LED is used to indicate that any
key has been pressed. Indicating an error condition is usually made through a
serial interface to show more detail information about the error.
For each of 4 ports The following actions are taken:
1. All port pins are set as inputs and all have the pullups enabled.
2. Interrupts are setup by calling portInterrupts() 2 times so that
spurious
interrupts are not generated (by changing the interrupt register values
which then react with existing conditions of the pins). In between these
calls, the
capture register is invoked to clear interrupts.
3. Finally the open drain output interrupt is enabled.
At the end of setup the external interrupt is enabled.
Multiple MCP23017
Interrupts Toggle_LED()
As it sounds the on board LED (Apin 13) is toggled.
Multiple MCP23017 Interrupts isr()
This is the Interrupt Service Routine (ISR) the is activated by an external
interrupt. The external interrupt is selected at the top of the file and uses a
macro to insert the code: controlArduinoInt.
In the routine itself interrupts are turned off, the external interrupt is
detached (ignored) then interrupts are turned on to enable the wire-library to
operate.
Since the interrupt has fired we now need find out which chip gave the
interrupt so we loop through 4 ports and read the INTF register (also ignoring
chips that are not present using portExists data). When found the LED is
toggled.
This means that the only way that the LED is turned on and off is if an
interrupt is generated by one of the MCP23017s.
Note: since it is assumed that only one pin is pressed at a time the LED is
toggled for which ever INTF register is non-zero - you can use a break
statement after toggling - a bit quicker and good practice to show code
operation. In practice you will want to use bit maniplulation macros to extract
which pin caused the interrupt.
Arduino has the following bit macros:
bitSet(x, nthbit)
bitClear(x, nthbit)
bitRead(x, nthbit)
Multiple MCP23017 Interrupts
Loop()
This is a do-nothing loop as we are only interested in the ISR operation.
All we do is have a 300ms delay (could be left blank).
// MCP23017 Example: MCP23017 Interrupt operation.
//
// This code sends an interrupt from multiple MCP23017s
//
// www.best-microcntroller-projects.com/mcp23017.html
//
// Copyright : John Main
// Free for non commercial use.
//
// V1.01
// Descripted interrupt controls in setup.
// Removed text in intf loop saying stops at 1st int found.
// V1.02
// Changed to using I2C NACK for chip detecction.
//
#include<Wire.h>
#include<Centipede.h>
// Arduino pins
#defineINTPIN3// Interrupt on this Arduino Uno pin.
#defineLED13
#defineLED_HIGHdigitalWrite(LED,HIGH)
#defineLED_LOWdigitalWrite(LED,LOW)
#defineCSAddress0b0100000// base address of MCP23017
#definecontrolArduioIntattachInterrupt(digitalPinToInterrupt(INTPIN),isr,FALLING)
staticuint8_tportExists[8]={0,0,0,0,0,0,0,0};// For single chip debug detect chips.
CentipedeCS;// Create Centipede object.
//////////////////////////////////////////////
// Read and change a register bit to see if device is present
uint8_tfindPorts(uint8_tport){
uint8_terr;
Wire.beginTransmission(CSAddress+port);
Wire.write((byte)0x0a);// IOCON
err=Wire.endTransmission();
// Since only sending the address can get
// OK(0), or NACK on address(2), or other error(4).
// Assume any error = not present.
if(err==0)return1;
return0;
}
//////////////////////////////////////////////
voidsetup(void){
uint8_ti;
Serial.begin(115200);
Serial.println("MCP23017 MULTI");
pinMode(LED,OUTPUT);
LED_LOW;
Wire.begin();// start I2C
CS.initialize();// set all registers to default
// Check if ports exist.
for(i=0;i<8;i++){
Serial.print("Port: ");Serial.print(i);
if(findPorts(i)){
Serial.println(" Exists: ");portExists[i]=1;// Flag that it exists.
}
elseSerial.println(" Not found");}
for(i=0;i<4;i++){CS.portMode(i,0xffff);// Chip pins on port to inputs.
CS.portPullup(i,0xffff);// All pins on port to pullup.
// Disable interrupt but set conditions for interrupt to occur
// This stops an intial interrupt firing in error.
// portInterrupts(int port, int gpinten, int defval, int intconval)
CS.portInterrupts(i,0x0000,0xFFFF,0X0000);//
CS.portCaptureRead(i);// Read capture reg. to clear ints.
// GPINTEN = 0xffff enabled.
// DEFVAL = 0xffff all default to pulled high.
// INTCON = 0x0000 all interrupt on change.
CS.portInterrupts(1,0xffff,0xFFFF,0X0000);// Set open drain ( ODR high overrides IPOL)
// portIntPinConfig(int port, int drain, int polarity) {
CS.portIntPinConfig(i,1,0);
}controlArduioInt;// Enable Arduino interrupt control.
}
//////////////////////////////////////////////
voidtoggle_LED(void){
staticuint8_ttog=1;
tog=!tog;
if(tog){
LED_HIGH;
}else{
LED_LOW;
}}
//////////////////////////////////////////////
// The interrupt routine handles LED1
// This is the button press since this is the only active interrupt.
voidisr(void){
uint8_ti,val;
staticuint16_tledState=0;
noInterrupts();
// Stop interrupts from external pin.
detachInterrupt(digitalPinToInterrupt(INTPIN));
interrupts();// re-start interrupts for mcp
// Find out which port produced the interrupt reg INTFAB 0x0E
// Assumption is that only one event happens at a time.
for(i=0;i<4;i++){
if(portExists[i]&&
CS.getIntF(i)!=0){
CS.portCaptureRead(i);// Read capture reg. to clear ints.
toggle_LED();}
}
controlArduioInt;// Reinstate interrupts from external pin.
}
//////////////////////////////////////////////
voidloop(){
delay(300);
}
Detecting
chess piece positions using MCP23017 Interrupts.
A more refined program that identifies piece positions as you would in a
chess board 1-8 and a-h. As a switch changes state so a piece is identified as
having been placed or removed. Additionally the chess board location is
reported.
To complete the chess piece detection you would need an piece identifier;s
Victor suggests one way As Victor suggests one way is to use RFID.
The code below is similar to the above code, but adds 'place' and 'remove'
detection simulated by a push button input to a pin of the MCP23017.
Debouncing is essential as rapid push button movements cause multiple
interrupts so in the isr() routine a 30ms delay is used (new interrupts are
ignored for 30ms) while the input settles.
The isr communicates to main code via the variable intFired and only in main
code are MCP interrupts reinstated.
A unique pin number is identified by getMultiPin and this is converted to
row and column chess notation using divide (/) and mod (%) operators. Note the
use of ascii table method to convert to a-h.
// MCP23017 Example: MCP23017 Interrupt operation.
//
// This code sends an interrupt from multiple MCP23017s
// 4 mcp23017S which are used detect placing and removal
// of chess pieces from a board thus requiring 64 (8x8)
// positions These are provided by 4x16 digital inputs
// probably also using reed relays and a magnet in each
// piece.
// Additional h/w is required to identify the pieces
// e.g. RFID h/w
//
// www.best-microcntroller-projects.com/mcp23017-interrupt.html
//
// Copyright : John Main
// Free for non commercial use.
//
// V1.00
//
// Inspired by Victor Feria's Chessboard Project.
//
// CHESSBOARD NOTATION
// Row
// 8 [ ][ ][ ][ ][ ][ ][ ][ ] GPIOB b0-b7
// 7 [ ][ ][ ][ ][ ][ ][ ][ ] chip4 GPIOA b0-b7
// 6 [ ][ ][ ][ ][ ][ ][ ][ ] GPIOB b0-b7
// 5 [ ][ ][ ][ ][ ][ ][ ][ ] chip5 GPIOA b0-b7
// 4 [ ][ ][ ][ ][ ][ ][ ][ ] GPIOB b0-b7
// 3 [ ][ ][ ][ ][ ][ ][ ][ ] chip2 GPIOA b0-b7
// 2 [ ][ ][ ][ ][ ][ ][ ][ ] GPIOB b0-b7
// 1 [ ][ ][ ][ ][ ][ ][ ][ ] chip1 GPIOA b0-b7
// . a b c d e f g h ----> Columns
//
#include<Wire.h>
#include<Centipede.h>
// Arduino pins
#defineINTPIN3// Interrupt on this Arduino Uno pin.
#defineLED13
#defineCSAddress0b0100000// I2C Base address of chip.
#definecontrolArduioIntattachInterrupt(digitalPinToInterrupt(INTPIN),isr,FALLING)
staticuint8_tportExists[8]={0,0,0,0,0,0,0,0};// For single chip debug detect chips.
staticuint8_tintFired=0;
staticuint16_tstoreINTF;// For later processing.
staticuint16_tstoreCAP;// For later processing.
staticuint8_tstorePort;// For later processing.
CentipedeCS;// Create Centipede object.
//////////////////////////////////////////////
// Read and change a register bit to see if device is present(1).
uint8_tfindPorts(uint8_tport){
uint8_terr;
// Use I2c NACK to detect chip
Wire.beginTransmission(CSAddress+port);
Wire.write((byte)0x0a);// IOCON
err=Wire.endTransmission();
if(err==0)return1;
return0;
}
//////////////////////////////////////////////
voidsetup(void){
uint8_ti;
intFired=0;
pinMode(LED,OUTPUT);
digitalWrite(LED,LOW);
Wire.begin();// start I2C
CS.initialize();// set all registers to default
Serial.begin(115200);
// Check if ports exist.
for(i=0;i<8;i++){
Serial.print("Port: ");Serial.print(i);
if(findPorts(i)){
Serial.println(" Exists: ");portExists[i]=1;// Flag that it exists.
}
elseSerial.println(" Not found");}
for(i=0;i<4;i++){CS.portMode(i,0xffff);// Chip pins on port to inputs.
CS.portPullup(i,0xffff);// All pins on port to pullup.
// port, int on pin, default, int !=default
// Disable interrupt but set conditions for interrupt to occur
// This stops an intial interrupt firing in error.
// portInterrupts(int port, int gpinten, int defval, int intconval)
CS.portInterrupts(i,0x0000,0xFFFF,0X0000);//
CS.portCaptureRead(i);// Read capture reg. to clear ints.
// GPINTEN = 0xffff enabled.
// DEFVAL = 0xffff all default to pulled high.
// INTCON = 0x0000 all interrupt on change.
CS.portInterrupts(i,0xffff,0xFFFF,0X0000);// Set open drain ( ODR high overrides IPOL)
// portIntPinConfig(int port, int drain, int polarity) {
CS.portIntPinConfig(i,1,0);
}controlArduioInt;// Enable Arduino interrupt control.
}
//////////////////////////////////////////////
voidtoggle_LED(void){
staticuint8_ttog=0;
tog=!tog;
if(tog){
digitalWrite(LED,HIGH);
}else{
digitalWrite(LED,LOW);
}}
//////////////////////////////////////////////
// The interrupt routine handles LED1
// This is the button press since this is the only active interrupt.
voidisr(void){
uint8_ti;
uint16_tval;
noInterrupts();
// Debounce. This debunce time is essential as you must wait for the
// input to the MCP23017 to settle - if not multiple interrupts will
// hang the code.
// Note: Can not use delay() in interrupt code (not interrupt safe).
delayMicroseconds(30000);// Stop interrupts from external pin.
detachInterrupt(digitalPinToInterrupt(INTPIN));
interrupts();// re-start interrupts for wire library.
// Find out which port produced the interrupt reg INTFAB 0x0E
// This stops at 1st interrupt detected
// Assumption is that only one event happens at a time.
for(i=0;i<4;i++){
if(portExists[i]&&(val=CS.getIntF(i))!=0){
storePort=i;storeINTF=val;storeCAP=CS.portCaptureRead(i);// Read capture reg. to clear ints.
intFired=1;// Indicate low and high changes.
break;
}}
toggle_LED();
}
//////////////////////////////////////////////
// Return a unique pin number from multiple MCP23017s
// or return -1 on fail.
//
// Return value can be 0-127 as can return 1 of 128
// pins if 8 chips are used.
//
// valINTF - The MCP interrupt register value.
//
// Note: Only returns 1st found interrupt bit that is high
// for each port not multiple interrupts.
//
int8_tgetMultiPin(uint8_tport,uint16_tvalINTF){
uint16_tmask=1;
uint8_ti;
for(i=0;i<16;i++){
if((valINTF&mask)!=0)break;// i contains index
mask=mask<<1;
}if(i==16)return-1;// Failed
returnport*16+i;
}
//////////////////////////////////////////////
voidloop(){
staticuint8_tpin;
uint8_tval,row,col,colChar,i;
uint16_tval_16;
charstr[2];
if(intFired){// Find which pin caused an interrupt.
pin=getMultiPin(storePort,storeINTF);
val=CS.digitalRead(pin);if(pin!=-1){// No error so use the pin.
row=pin/8+1;// +1 : index from 1 not zero.
col=pin%8;
colChar=col+'a';// Convert 0-8 to a-h
str[0]=colChar;str[1]='\0';// Convert to string for printing.
Serial.print("Pin ");Serial.print(pin);
Serial.print(" row ");Serial.print(row);
Serial.print(" col ");Serial.print(str);
if(val==0)Serial.println(" Placed");elseSerial.println(" Removed");
}
intFired=0;// Restart.
controlArduioInt;// Reinstate MCP interrupts from external pin.
}
}
Typical output from above
program:
In this case the address was set to 1 so rows 3 and 4 were available
More
questions on code operation for the MCP23017 interrupt
#1. How
do I determine which chip hosted the MPC23017 interrupt?
That is the reason that I added the INTF register read function for the port
(chip) getIntF().
You cycle through all the expected ports (chips) until you find an interrupt
bit set high (i.e. a non zero 16 bit value is returned as the INTF register
state).
You need the the INTF read function, as one of the bits will be high (depending on the
pin that triggered the interrupt). The capture register tells you the actual
state of the pin that caused the interrupt.
Since you can set pins to be
interrupt active on low or high reading the capture register alone does not
show you which interrupt fired. INTF register always sets a bit high regardless of the polarity of the input
trigger. Combining these two pieces of information gives you the actual pin that caused the interrupt and its current state.
The following code snippet (from 1st example in the interrupt routine) shows
looping through 4 ports of value 0 to 3, and toggling an LED if an interrupt
flag is found high. The function CS.getIntF(i) returns the 16 bit interrupt
flag register for each chip:
Note: if you don't care about chips being present remove portExists[i] check
as I needed it (not having 4 chips available). When you have got 4 connected
the default state is all working!
The MCP code is set to interrupt on change (as discussed in the 1st
mcp23017.html page). Therefore to see if a pin was pressed detect an interrupt
using intf reg. and check capture reg. to see if low = press, if high =
release.
#2 What port
bank caused the MCP23017 interrupt
See above code . Since the routine getMultiPin() returns a pin number from
0-127 and you can decode using % and / you don't directly need the port/bank
information
#3
Detection of which pin caused an MCP23017 interrupt.
See above code and getMultiPin()
#4.
Which method is faster four MCP23017 interrupts or open-drain method?
The open drain method is simply a wire or function and each MCP23017 pulls
the open drain down by activating a transistor that has its collector open (or
FET where drain is open) emitter ( or FET source) is connected to Ground. So
for a falling edge i.e. open drain active the edge speed is fast. On the rising
edge the 10k pulls up and is therefore slow (depends on capacitance at that
pin) - to speed this up reduce that 10k value. However we are looking for a
falling edge so all is good.
Your question is probably about how fast you can react to a switch being
made. So I measured the access function for getIntF and capture at 820us 520us
for getIntF(). So to scan all 4 chips would take 2ms scanning only with
getIntF(). That seems fast enough. If you used 4 interrupts then you can react
to each chip in 520us so yes 4 interrupts are faster.
But looking at the system there is no need for speed. Picking up and placing
chess pieces takes 100s of milliseconds or more and even in your code there is
a 100ms debounce time (do nothing time). For this system there is no need for
faster speed. I used 30ms delay within the isr (using microsecond() as you
can't use delay() in an interrupt) which seems to be enough time for bouncing
to settle (but could be different for your setup) for a push button input.
You would want speed if it was critical to do something in under a
millisecond. Even then the I2C bus speed can be increased to 1.7MHz or use a
20MHz SPI version. That does not automatically increase speed to that rate as
ultimately the processor needs to do something between I2C or SPI writes but it
would help.
For this design it is all about the input rate which is slow. All switches
can be read in about 4ms and the interrupt flag tells you which one has
changed. You only have to deal with switch bounce.
One other thing when you have 4 interrupts you have to manage code for 4 -
that's a lot of code maintenance and each piece of code will be identical to
the other! i.e. a pain when it needs changing.
Also note the method of pin number detection and the way to output a digit,
which uses a standard ASCII table output trick: i + 'a';
Get started with an Arduino humidity sensor using the DHT11, which reports both humidity and temperature. Complete guide with full code for using this sensor
The Arduino delay function is an Extremely Useful function which you can use to get small delays. However, sometimes it's not the right function to use � there is another!
Control the colors of an RGB LED through Arduino code. This beginners guide covers connections and provides example sketches for interesting LED control...
Tired of wasting time trying to get the notoriously bad KY-032 IR sensor module to work reliably? How to use a 3 component substitute that actually works!
Comments
Have your say about what you just read! Leave me a comment in the box below.
Don’t see the comments box? Log in to your Facebook account, give Facebook consent, then return to this page and refresh it.