You can use the MCP23017 to increase the number of I/O pins for any microcontroller that can communicate using an I2C interface.
This page shows you how to control the device
for driving LEDs and reading button presses but you can use it anywhere
that you need more general purpose inputs or outputs
It also shows you exactly
how to use
interrupts which is very tricky as there are some problems (solved here)
using existing Arduino code.
The MCP23017 is a port expander that gives you virtually identical PORTS
compared to standard microcontrollers e.g. Arduino or PIC devices and it even
includes interrupts. It gives you an extra 16 I/O pins using an I2C interface as
well as comprehensive interrupt control. This is a very versatile and
multi-configurable I/O expander. By adding more devices you can increase the
total I/O to 128 pins still using only two I2C pins!
The rest of this page shows you how to use the mcp23017 with Arduino in
Note: For a multiple 23017 interrupt tutorial for this chip see
For some applications it could be a bit over the top; For example if you
only want to control outputs a 74HC595 would be cheaper.
The IC2 version operates at 100kHz, 400kHz and 1.7MHz speeds. It also
operates from 1.8V to 5.5V.
Having interrupt outputs is one of the most important features of the
MCP23017, since the microcontroller does not have to continuously poll the
device to detect an input change. Instead an interrupt service routine can be
used to react quickly to an input change on any pin e.g. a key press etc.
The MCP23017 has internal pullups for each input pin (configurable) so there is
no need to add external pull up resistors for inputs such as push buttons
The device also has provision for 3 address inputs so you can add a total of
8 devices onto the same I2C bus which gives you 128 new I/o pins only requiring
two microcontroller I2C pins for control.
An alternative device is the MCP23S17 which is uses the SPI
interface that can operate at 10MHz (a lot faster than the I2C version). This
SPI device has the same number and arrangement of pins, but uses two unused
(I2C) pins to implement the SPI interface. In all other respects it operates
the same as the MCP23017.
To make life even easier each GPIO input pin can be configured with an
internal pullup (~100k) and that means you won't have to wire up external pull
up resistors for keyboard input. You can also mix and match inputs and outputs
the same as any standard microcontroller 8 bit port.
* This is an
unusual parameter specification. and relates to the fact that there must be a
asymmetrical architecture in the chip that can accept only the specified
current. The overall sink current is 150mA Vss and overall source current is
Sink current is the current flowing (from an external circuit) when a pin is
taken low. Source current is the flowing (to an external circuit) when a pin is taken
The "Current Max out of Vss" is the sum of all sinking currents.
The "Current Max into Vdd" is the sum of all sourcing currents.
The 23017 has three input pins to allow you to set a different address for
each attached MCP23017. Addresses available are specified in the I2C control
byte and each device is selected by the I2C sequence below:
The above corresponds to a hardware address for the three lines A0, A1, A2
corresponding to the input pin values at the IC. You must set the value of
these hardware inputs as 0V or (high) volts and not leave them floating
otherwise they will get random values from electrical noise and the chip will
The four left most bits are fixed a 0100 (specified by a consortium who
doles out address ranges to manufacturers).
So the MCP23017 I2C address range is 32 decimal to 37 decimal or 0x20 to
0x27 for the MCP23017.
Note: The address range allows 3 bits and
this means a maximum of eight MCP23017 devices can be attached
to any single I2C bus. If you want more you would need to
either use a second I2C bus or bit bang some pins to simulate one, or do
something clever with more hardware i.e. another microcontroller intercepting
unused I2C addresses and converting an them to control the extra
MCP23017 LED Driver
The above specification shows that the device is quite capable of driving
current to LEDs however there are 16 outputs so the maximum output current for
the whole device has to be shared by all the LEDs.
Warning: Maximum total current chip is 150mA,
and the maximum for an individual pin is 25mA.
If you drive 16 LEDs then it will be 150mA/16 =9.38mA per GPIO pin (MAX).
This also assumes that you are turning the LED on when it is driven low. Note:
Choose a lower current to keep within limits.
Remember that different LED colours have a different forward voltage drop
when driven (this is constant due to how a diode behaves) so to control the
forward current you have to specify the correct current limiting resistor for
the voltage source used.
If you are turning it on when driven high then the equation is
125/16mA=7.8mA (MAX). Note: Choose a lower current to keep within limits.
TIP: If your MCP23017 is getting hot, then
look at the overall output current from the chip by calculating the current
from each pin and then adding up these results. If it is more than 150mA then
reduce the current by increasing the current limiting resistors for each
Obviously reducing the number of outputs in use will allow increasing
current up to a maximum of 25mA per pin but then you could drive only 6 pins at
full current! (150mA/25mA=6). You must always to keep the maximum overall
output current for the chip within the 150mA limit otherwise the chip gets hot
and will probably fail.
The fact that the MCP23017 gets hot is not a problem if you keep the maximum
overall current consumption to 150mA. You should look at the data sheet for the
maximum temperature allowed, and measure the chip temperature at its surface.
The data sheet, usually, I did not look in this case, also specifies a
de-rating parameter (or graph) so that if used within a hot enclosure (or any
environment) you have to reduce the maximum current consumed!
MCP23017 Non interrupt
IODIR I/O direction register
For controlling I/O direction of each pin, register IODIR (A/B) lets you set
the pin to an output when a zero is written and to an input when a '1' is
written to the register bit. This is the same scheme for most microcontrollers
- the key is to remember that zero ('0') equates to the 'O' in Output.
GPPU Pullup register
Setting a bit high sets the pullup active for the corresponding I/O pin.
OLAT Output Latch register
This is exactly the same as the I/O port in 18F series PIC chips where you
can read back the "desired" output of a port pin whether or not the actual
state of that pin is reached. i.e. consider a strong current LED attached to
the pin - it is easily possible to pull down the output voltage at the pin to
below the logic threshold i.e. you would read back a zero if reading from the
pin itself when in fact it should be a one. Reading the OLAT register bit
returns a 'one' as you would expect from a software engineering point of
IPOL pin inversion register
The IPOL(A/B) register allows you to selectively invert any input pin. This
reduces the glue logic needed to interface other devices to the MCP23017 since
you won't need to add inverter logic chips to get the correct signal polarity
into the MCP23017.
It is also very handy for getting the signals the right way up e.g. it is
common to use a pull up resistor for an input so when a user presses an input
key the voltage input is zero, so in software you have to remember to test for
Using the MCP23017 you could invert that input and test for a 1 (in my mind
a key press is more equivalent to an on state i.e. a '1') however I use pullups
all the time (and uCs in general use internal pullups when enabled) so have to
put up with a zero as 'pressed'. Using this device would allow you to correct
Note: The reason that active low signals are used everywhere
is a historical one: TTL (Transistor Transistor Logic) devices draw more power
in the active low state due to the internal circuitry, and it was important to
reduce unnecessary power consumption - therefore signals that are inactive most
of the time e.g. a chip select signal - were defined to be high. With CMOS
devices either state causes the same power usage so it now does not matter -
however active low is used because everyone uses it now and used it in the
SEQOP polling mode : register bit : (Within
If you have a design that has critical interrupt code e.g. for performing a
timing critical measurement you may not want non critical inputs to generate an
interrupt i.e. you reserve the interrupt for the most important input data.
In this case, it may make more sense to allow polling of some of the device
inputs. To facilitate this "Byte mode" is provided. In this mode you can read
the same set of GPIOs using clocks but not needling to provide other control
information. i.e. it stays on the same set of GPIO bits, and you can
continuously read it without the register address updating itself. In non byte
mode you either have to set the address you read from (A or B bank) as control
Note: Interrupt registers are discussed later on here.
Software Library and versions
Arduino IDE Version
Version : 1.8.3
MCP23017 library for aruduino
Adafruit MCP23017 Library 1.0.3
This is easily installed from the Arduino IDE.
If you do not see the library as an entry when you click the menus then
install the library as follows:
Then select manage libraries :
Sketch-->Include Library -->Manage
Search for and install <lib name> using the "Filter Your Search"
Pin definition for MCP23017
Note: In the library pins are labelled from 0 to 15 where:
pin 0 is bit 0 of port A
pin 7 is bit 7 of port A
pin 8 is bit 0 of port B
pin 15 is bit 7 of port B
MCP23017 I/O control
Single Bit I/O
Similar member functions to the pin controls on the Arduino are used to
control the MCP23017 pins:
Simple Netlist connections
The following netlist and diagram show you how connecting the MCP23017 to
the Arduino is very simple.
Connect pin #12 of the expander to Arduino Analog 5 (i2c clock)
Connect pin #13 of the expander to Arduino Analog 4 (i2c data)
Connect pin #19 of the expander to Arduino pin 3 (interrupt input).
Connect pins #15, 16 and 17 of the expander to Arduino ground (address
Connect pin #9 of the expander to Arduino 5V (power)
Connect pin #10 of the expander to Arduino ground (common ground)
Connect pin #18 of the expander through a ~10kohm resistor to 5V (reset pin,
Connect pin #28 of the expander to +ve end of an LED then to a~1kohm
resistor to GND (MCP_LED1).
Connect pin #26 of the expander to +ve end of an LED then to a~1kohm
resistor to GND (MCP_LEDTOG1).
Connect pin #4 of the expander to +ve end of an LED then to a~1kohm resistor
to GND (MCP_LEDTOG2).
Connect pin #1 of the expander to a normally open push button that then
connects to GND (MCP_INPUTPIN).
Note: pullups are enabled for I2C pins in the "Wire" library
so are not shown in the above circuit connection or layout (below). They are
high value (probably 50k~100k), so for a faster rising edge on I2C signals use
lower value physical pullup resistors that will override the high value..
Fritzing Layout : MCP23017 Circuit
Arduino MCP23017 Examples Code
Example 1 Basic operation
This example shows three LEDs on different ports of the MCP23017, with two
on port A (Green and Red). Two LEDs are alternately flashed (Red ones) while
the third shows the state of the input on GPB0 i.e GPB0 is read by the Arduino
and then the Green LED is updated. This shows independent control of individual
port bits i.e. while flashing the red LEDs the button is read and the green one
It serves to demonstrate how interrupts will be useful (see next example for
interrupt code). Try pushing the button and releasing it quickly - usually the
output green LED does not change state immediately - you may find the key press
is not even detected!.
The following example shows using arduino and the mcp23017 library.
Reason for lagging key
If you push the button to change the state of the LED (LED1) there is
sometimes a lag from when you hit the button to when it goes off and the same
is true when you release it. You could say the code has a rather relaxed
attitude to displaying the key update - in some cases this would not matter
e.g. non critical turn on of an actuator for a window opener in a greenhouse
(which would be slow anyway) but in others this would be critical e.g. an
emergency motor stop in a machine.
The reason it reacts slowly is because of the long delays within the code
that are "do-nothing" delays and if you hit the button during one of these
periods, the processor is ignoring your button press.
Lagging key detect solution
Of course this a slightly contrived example as you can easily change the
code for faster polling or use dynamic delays (using the millis function) but
it demonstrates real world operation since in a "real" program you will want
the processor to do quite a lot of work and this work will produce unavoidable
delays (since it can only do one thing at a time).
This is where interrupts become essential. Interrupts make it appear that
the processor is capable of performing more than one process at a time i.e
performing a delay action and detecting a keypress. In this case you need an
interrupt to detect a keypress on the MCP23017. Example 2 uses interrupts to
overcome the delays within the loop function.
Warning on Wire Library
It took a little while to figure out but this is one of the pitfalls of
working with the arduino library you sometimes have to have a deeper
understanding of what is going on under the hood before you can see why
something won't work.
Example 2 MCP23017
Interrupts Don't Work
In the first example when you push the button to zero the input port (GPB0)
it takes a while for the output LED (LED1) to go low. The idea here was to use
a simple interrupt routine to service the external interrupt, re-start them and
continue so that the output LED is toggled immediately that the button is
pressed i.e. using an interrupt for instant LED update.
However there is a major non-obvious gotcha in that idea, that stops this
simple interrupt code from working.
The following code shows typical use for attaching a callback function to an
interrupt [Source https://www.arduino.cc/en/Reference/AttachInterrupt]
The following code does work ;It is a normal external
interrupt operating only on the Arduino Uno R3 - just attach a button to pin 2
and the LED will toggle. An external interrupt caused when you press a button
that pulls pin 2 low fires the interrupt routine 'blink' that then toggles the
LED state (written in the main loop).
Here is the code (below) that I wanted to use and when you look at it, it
appears perfectly reasonable.
Note: The example code below is show for
educational purposes and does not work
correctly (See below for why this is the case).
The following code has three standard functions:
The setup function initialises the ports while loop function toggles some
leds and the isr reacts to a button press (it should do but the code fails).
Note: This code hangs waiting for interrupts see next code
example for the solution.
Example 3 Interrupt Example
This Arduino MCP23017 Interrupt Example code shows you exactly how to use
and connect an external interrupt pin and make interrupts work correctly. As
you saw in the previous example - you can not just use the Arduino template
code because there is a subtle problem involved. This example explains the
problem and solves it.
The requirement for operating interrupts is that to clear an interrupt state
you have to read from either INTCAP (interrupt data captured) or GPIO.
Important: To clear interrupts you must read back data from
either INTCAP(A/B) or GPIO(A/B).
In the previous code (Example 2), GPIO is read inside the isr() function in
order to reset the MCP23017 interrupts. However the code hangs at that
If you look in the library code you won't see any reason for it at all.
Looking a little deeper reveals the culprit - which is the "Wire" library.
In this library a hardware interrupt from the I2C module is in use. Since
interrupts are disabled when the callback function is executed the code hangs
waiting for the I2C interrupt that is never actioned.
That means you can't use the mcp23017 library code to clear the interrupts
using the "read GPIO" function (unless you use a special technique - see below)
because the Wire library never completes!
The key is to allow other interrupts in the current isr() routine but not
those that would restart the current isr i.e. stop the MCP23017 interrupts
temporarily. Arduino functions detatchInterrupt(), attachInterrupt(),
interrupts(), noInterrupts() are used to achieve this.
Note: Accessing the MCP23017 via I2C takes a while so debounce
is probably accounted for by the time that it takes to refresh the I/O
It is worth studying the code, as the way this works is fairly complex i.e.
allowing interrupts from within an already operating interrupt triggered
Warning: The arduino library cannot cope with more than one
interrupt source due to the way the library code is written (which fits to the
Arduino philosophy of "abstracted pin operation"). Since code is pin-centric
within Arduino the first interrupt flag value that is found active (in the INTF
register) returns that pin value - any more are ignored. For multiple interrupt
sources you will need to write your own functions that are not pin-based.
MCP23017 Interrupt registers
The interrupt are comprehensive in this
device, and consequently require a large amount of control registers. The
following information allows you to easily understand and control the MCP23017
when using interrupt operation.
The device is split into two sets of 8 GPIO registers and each set can have
a separate interrupt associated with it (INTA and INTB outputs).
These are the registers associated with interrupts in the MCP23017:
IOCON.MIRROR (a bit in the IOCON register)
IOCON.INTPOL (a bit in the IOCON register)
IOCON.ODR (a bit in the IOCON register)
Add an A or B to the name for control of a specific register set.
Note: Register names may sound similar to PIC device names since the chip
was also designed by MicroChip.
MIRROR register bit : (Within IOCON
The control bit MIRROR wire ORs INTA and INTB together if set - that means
any interrupt condition on any port will cause both INTA and INTB to be
activated. When not set you have two separate interrupt outputs: INTA and INTB
each associated with the A or B register.
INTPOL register bit : (Within IOCON
If set means the interrupt output is active high.
ODR register bit : (Within IOCON
If set means the interrupt pin is open drain (overrides INTPOL).
INTCON controls how interrupt conditions are detected as below:
There are two ways of detecting interrupt states for a specific register
Input Pin change, (A specific INTCON bit associated with a pin is 0) -
This is the default.
Input pin change compared to defined value in DEFVAL, (A specific INTCON
bit associated with a pin is 1)
In the first case an interrupt is generated if the new pin value is
different to the old pin value.
In the second case an interrupt is generated if the new pin value is
different to the stored default value (DEFVAL).
Sets the default value of a pin which is the inactive input interrupt
Say you have two interrupt sources e.g. an active low alarm (ALRM) and an
active high sensor (SENS) and you want either of these to cause an interrupt.
Using the DEFVAL register you would set:
the DEFVAL value for ALRM to high,
the DEFVAL value for SENS to low.
Any signal opposite to the DEFVAL value would trigger an interrupt. So if
the ALRM signal goes to 0V an interrupt would be triggered, and if the SENS
signal goes to 5V an interrupt would be triggered.
This saves having to put inverter chips all over the place to get the right
signal polarity. You would also enable the corresponding INTCON and GPINTEN
bits corresponding to the bit positions in the register for the pins used as
You may have some pins as LED outputs so you won't want them causing an
interrupt or you may have some inputs that you only need to poll occasionally.
GPINTEN is the register that enables each bit of the register to act as an
interrupt on change pin (IOC). Using GPINTEN, you enable specific input pins as
interrupts by setting the corresponding bit high.
INTA and INTB registers
The chip also allows ultimate configuration of the output states of INTA and
INTB. This is quite unusual as chips usually have a fixed output state that you
have to add glue logic around to get the right signal polarity.
For each INTA and INTB you can set the output as:
These can be controlled from within the IOCON register.
Note: Only the INTA and INTB pins on the MCP23017 are open
drain (if you set them to be open drain).
INTF Interrupt Flag register
The INTF register is the interrupt flag register and tells you which
interrupt input cause the current interrupt trigger. When a bit is set in this
register (that is enabled for interrupts) the associated pin caused the
INTCAP Interrupt Data
A further feature of the chip is that the value of the register is captured
when an interrupt is triggered. INTCAP stores the input state of the input pins
when the interrupt occurred. Any further interrupt conditions will not cause an
interrupt until either INTCAP or GPIO is read.
During the process of reacting to the INTF flag your code will likely take
too long to read the interrupt data registers (remember this is a serial
interface device so it takes time to get the serial data back from the
Even though it is on a fast serial interface you will have to set
some command register addresses which are themselves serial output data bursts.
You could have missed any changes from when the interrupt fired to the time
that you get around to reading back exactly which interrupt caused INTF change!
To avoid this situation, INTCAP preserves that interrupt state when the interrupt fires so that you
only see the correct state data matching the interrupt condition flags
No other interrupts can fire until you either read a GPIO register or read
the INTCAP register. Using INTCAP means that you can find out exactly what the
pin value was at the time of the interrupt.
It is therefore up to you to figure out in your overall design whether your
software can keep up with the expected interrupt input rate. If you are only
detecting key presses from a user the input rate would be low so there would be
no problem but if detecting a high frequency signal there could likely be a
problem in keeping up with it but it all depends on the exact speed and how
fast the I2C interface is operating and if the interrupt routine is
If you want to have several MCP23017 devices on the I2C bus and you want to
also allow any of them to interrupt a processor then you will need to use the
open-drain capability of the interrupt output pin. Using a single resistor to
pull the open drain high, tie all the interrupt outputs from each MCP23017
together and set the INTA and INTB outputs to wire 'OR' mode.
If any one of the inputs causes an interrupt then this open drain "wire or"
connection will be pulled low. It is then up to you to go and find out which
device caused the interrupt.
Warning: This does mean that you have to have an active low
interrupt input on the microcontroller or use falling edge interrupt mode.
To clear interrupt conditions so that more interrupts can be received
read either INTCAP or GPIO.
You must read either of these during initialisation otherwise the interrupts
may not start at all.
Warning: Interrupts appear to fail if you do not read either
INTCAP or GPIO.
One thing that can catch you out is while debugging - if you read the INTCAP
register then the MCP23017 clears the INTF register and INTCAP register so
reading it again will result in zeros in the response.
Important Interrupt Operation
A very important part of using interrupts with the MCP23017 is the following
extract from the datasheet:
Note the 2nd condition which indicates that the interrupt will
not be cleared after reading from INTCAP or GPIO. In this case you would have
to remove the source of the interrupt (e.g. release a key) or re-program the
interrupt condition. You should only use the 2nd case where you know
that the interrupt source is going to be short.
For instance if you use a keypress as the interrupt source and the 2nd case
setting, the user can force the processor to halt by holding down the button!
It is therefore best to employ the 1st method for detecting
interrupt states simply because it is easier to turn them off!
Warning: The data sheet (Section 3.6.4) talks about the
interrupt-on-change register then it talks about the IOINTEN register (does not
exist), then it talks about the IOC register (this signal DOES not exist except as bits
within INTCON). It is actually the INTCONA/B register that is the
interrupt-on-change register containing IOC bits.
The following are the linear address ranges when BANK is set to zero