Become a subscriber (Free)

Join 29,000 other subscribers to receive subscriber sale discounts and other free resources.
Don't worry -- youre-mail address is totally secure. I promise to use it only to send you MicroZine.

The MCP23017 I/O Expander

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 pin.

It also shows you exactly how to use interrupts which is very tricky as there are some problems (solved here) in 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 detail.

Note: For a multiple 23017 interrupt tutorial for this chip see here.

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.

mcp23017 text circuit breadboard layout

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 etc.

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.

MCP23017 Pinout

mcp23017 pinout

MCP23017 Specification

  Voltage Supply (Vs)
1V8 ~ 5V5
  Abs Max Voltage
-0V3 ~ 5V5
  I2C Address (depends on ALT ADDR)
0x20 ~ 0x27
  Interface Speed (kHz)
100, 400, 1700
  Supply Current (idle): 1mA
  Operational Current Max out of Vss [*] 150mA
  Operational Current Max into Vdd [*] 125mA
  Output current per pin 25mA
  Standby Current 01uA
  Operating temperature
-40°C ~ 85°C
* 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 125mA.

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 high.

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.

MCP23017 Datasheet

Download the datasheet here.

MCP23017 Addressing

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:

mcp23017 hardware address

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 do nothing!

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 MCP23017s.

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 pin.

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 registers

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 view.

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 zero.

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 this easily.

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 past.

SEQOP polling mode : register bit : (Within IOCON register)

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 input data.

Note: Interrupt registers are discussed later on here.

Software Library and versions

Arduino IDE Version

Version : 1.8.3

Adafruit library

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:

Sketch-->Include Library

Then select manage libraries :

Sketch-->Include Library -->Manage Libraries...

Search for and install <lib name> using the "Filter Your Search" form.

Library Operation

Pin definition for MCP23017 library

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 functions

Single Bit I/O

Similar member functions to the pin controls on the Arduino are used to control the MCP23017 pins:

mcp.pinMode(0, OUTPUT);




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 selection)

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, active low).

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

mcp23017 text circuit breadboard layout

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 is updated.

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 detection

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 and Interrupts

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]

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).

const byte ledPin = 13;
const byte interruptPin = 2;
volatile byte state = LOW;

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(interruptPin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(interruptPin), blink, CHANGE);

void loop() {
  digitalWrite(ledPin, state);

void blink() {
  state = !state;

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:

  • setup()
  • isr()
  • loop()

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 point!

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 expander.

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 routine.


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)
  • INTF

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 register)

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 register)

If set means the interrupt output is active high.

ODR register bit : (Within IOCON register)

If set means the interrupt pin is open drain (overrides INTPOL).

INTCON register

INTCON controls how interrupt conditions are detected as below:

There are two ways of detecting interrupt states for a specific register bit:

  1. Input Pin change, (A specific INTCON bit associated with a pin is 0) - This is the default.
  2. 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).

DEFVAL register

Sets the default value of a pin which is the inactive input interrupt state.

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 interrupts.

GPINTEN register

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:

  • Active low
  • Active high
  • Open Drain

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 interrupt.

INTCAP Interrupt Data Capture register

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.

Interrupt Operation

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 MCP23017).

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 (INTF).

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 efficient!

Multiple Interrupt Connections

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.

Note: Open this link for a detailed multiple interrupt tutorial.

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.

Clearing interrupts

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:

Causes of interrupts for the MCP23017

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.

Register Reference

The following are the linear address ranges when BANK is set to zero


Access to:























Going further: To learn how to use multiple MCP23017 devices and connect their interrupt outputs together; Follow this tutorial: Click Here.

New! Comments

Have your say about what you just read! Leave me a comment in the box below.

Privacy Policy | Contact | About Me

Site Map | Terms of Use

Visit our Facebook Page:

   Click Here

  1. How to use the MCP23017 I/O Expander on the Arduino

    How to use the MCP23017 to increase your I/O by 16 pins (or more) and use its interrupt system.

    Read more

  2. Arduino Battery Charger: How to save the planet one battery at a time; Recycle and re-use Alkaline battries

    Arduino Battery Charger: A very useful project that lets you charge 'un-rechargeable' alkaline batteries!

    Read more

  3. Digispark Attiny85 Easy IDE install and setup

    Digispark ATtiny85: Essential information on setting up and using this tiny, but powerful, chip

    Read more

Readers Comments

"I wanted to thank
you so so so much
for all the information
you have provided in
your site it's


- Ranish Pottath

"This site really is
the best and my favorite.
I find here many useful
projects and tips."

- Milan


"Awesome site,
very, very easy and nice
to navigate!"

- Matt

Learn Microcontrollers

"Interested in

Sign up for The
Free 7 day guide:


"I am a newbie to PIC
and I wanted to say
 how great your
site has been for me."

- Dave


"Your site is a great
and perfect work.

- Suresh


"I couldn't find the correct
words to define
yourweb site.

Very useful, uncovered,
honest and clear.

Thanks so much for
your time and works.

- Anon

Back to Top