Adding an MCP23017 16 port IO expander to Arduino or Esp8266 or Attiny85 or……..

Update 2023: After a year’s shortage of the MCP23017 it appears to be available again. However the new batch, coded as MCP23017D misses 2 I/O lines: PA7 & PB7
Update:
After I made this expander module, a ready made module
with this chip has become available. So I actually would advice anybody needing a 16 bit expander, to buy that one rather than build it. The module will cost you abt 1.50 euro, while the individual chip may set you back a euro or so.

I am not claiming that what I am describing here is earth shattering or trailblazing, because in fact it is very simple and no doubt has been done by many already. But sometimes what is simple for the one, is still a question mark for the other, so here is quick ‘how-to’ of adding 16 I/O ports to your microprocessor. This is especially handy when working with a chip like the ESP8266 that has only limited I/O
The MCP23017 is an I2C enabled 16 I/O port chip. That means that you only need 2 pins (yes with Vcc and ground it makes 4) to control the chip and the added advantage is that you can share I2C with various other devices as well.

The 16 I/O lines are divided into an 8 I/O PORT A and an 8 I/O PORT B. Both can be used as input as well as output. The chip also has 2 configurable interrupts (that I will not be using). The physical layout of the chip makes it quite easy to use it on a piece of strip board.

The circuit (at right) is rather simple. At a last moment I decided to leave out the pull up resistors so it would be more flexible to use together with other I/O devices. The 3 Address pins A0-A2 determine the I2C address that ranges from 0x20 (all pins on ground) to 0x27 (all pins on Vcc).
The chip  can take a Vcc from 2.7V to 5V and this is perfect for 3.3 Volt devices as  the modern arduino’s and the ESP8266 range of boards.

Using the chip in a program is fairly easy. There are good libraries available, but it might help if you know how to program the chip without a library.
In my case I have all  address lines tied to ground and therefore my I2C address is 0x20. Suppose I want to use all PORT A lines as outputs. I do that  as follows:

Wire.beginTransmission(0x20); Wire.write(0x00); // IODIRA register Wire.write(0x00); // set entire PORT A to output Wire.endTransmission();

For PORT B that  is rather similar:

Wire.beginTransmission(0x20); Wire.write(0x01); // IODIRB register Wire.write(0x00); // set entire PORT B to output Wire.endTransmission();

If we then want to send a specific value ‘X’ to that PORT A, we do that as follows

Wire.beginTransmission(0x20); Wire.write(0x12); // address port A Wire.write(X);  // value to send Wire.endTransmission();

‘X’ ofcourse is a byte value that determines whether we set a specific port HIGH or LOW.
If for instance ‘X’is ‘0’ that means we write a LOW to all PORT A outputs. If it is 255 that means we write a HIGH to all PORT A outputs.
To determine what value to send, consider the 8 I/O lines of PORT A as a byte in which the individual bits determine HIGH or LOW.
So if we only want to make PORTA.0 HIGH and the rest LOW, we write a binary value of 0b00000001 =1 to the A register. If we want to make PORTA.0 and PORTA.2 HIGH and the rest LOW we write a binary value of 0b00000101 = 5.
For PORT B it is similar:

Wire.beginTransmission(0x20); Wire.write(0x13); // address PORT B Wire.write(X);  // value to send Wire.endTransmission();

If we want to use PORT B (or PORT A for that matter) as input, we do that as follows:

Wire.beginTransmission(0x20); Wire.write(0x13); // address PORT B Wire.endTransmission(); Wire.requestFrom(0x20, 1); // request one byte of data byte input=Wire.read(); // store incoming byte into "input"

The byte “input” will vary between 0 and 255, in which the individual bits determine the input on the corresponding IO line. So if ‘input’  reads ‘3’  which in binary is 0b00000011, that means that both IO line 0 and 1  were HIGH and the rest LOW. (note: Wire.beginTransmission() and Wire.endTransmission() are only used for a Wire.write(), not or a Wire.read() or Wire.request().)

#include <Wire.h> // Wire.h byte input=0; void setup() {   Serial.begin(9600);   Wire.begin(); // wake up I2C bus   Wire.beginTransmission(0x20);   Wire.write(0x00); // IODIRA register   Wire.write(0x00); // set entire PORT A as output   Wire.endTransmission(); }   void loop() {   // read the inputs of bank B   Wire.beginTransmission(0x20);   Wire.write(0x13);   Wire.endTransmission();   Wire.requestFrom(0x20, 1);   input=Wire.read();     // now send the input data to bank A   Wire.beginTransmission(0x20);   Wire.write(0x12); // address PORT A   Wire.write(input);    // PORT A   Wire.endTransmission();   delay(100); // for debounce }

That’s basically it if you want to do the addressing yourself. Using a library, such as the one from Adafruit, makes it much easier though as it has commands to write and read from individual IO lines. One of the example programs to read a single button, looks  for instance like this:

#include <Wire.h> // Wire.h #include "Adafruit_MCP23017.h"  // Basic pin reading and pullup test for the MCP23017 I/O expander // public domain! // Connect pin #12 of the expander to Analog 5 (i2c clock) // Connect pin #13 of the expander to Analog 4 (i2c data) // Connect pins #15, 16 and 17 of the expander to ground (address selection) // Connect pin #9 of the expander to 5V (power) // Connect pin #10 of the expander to ground (common ground) // Connect pin #18 through a ~10kohm resistor to 5V (reset pin, active low) // Input #0 is on pin 21 so connect a button or switch from there to ground  Adafruit_MCP23017 mcp;  void setup()  { mcp.begin();      // use default address 0 mcp.pinMode(0, INPUT); mcp.pullUp(0, HIGH);  // turn on a 100K pullup internally pinMode(13, OUTPUT);  // use the p13 LED as debugging }  void loop() { // The LED will 'echo' the button digitalWrite(13, mcp.digitalRead(0)); }

If you want to use more than one MCP23017 do that as follows:

#define addr1 0 //addr1 =A2 low , A1 low , A0 low =000 #define addr2 1 //addr 2 = A2 low , A1 low , A0 high =001  setup(){     mcp1.begin(addr1);     mcp2.begin(addr2); }

Mind you that “0” is in fact 0x20 and ‘1’ is in fact 0x21

If you are using the Adafruit library with the ESP8266, you will encounter a compilation error signaling it cannot find <avr/pgmspace.h>. The solution for this is easy:

Open the cpp file of the library.

replace

#include <avr/pgmspace.h>

with

#if (defined(__AVR__)) #include <avr/pgmspace.h> #else #include <pgmspace.h> #endif

You will find extensive information on how to use the mcp23017 here.

The chip uses 1mA and in standby mode it uses 1uA. Sadly the datasheet is not clear on how to enter standbye mode but Micro-chip support gave the following explanation:

I believe that standby current is the VDD current when there is no active communication and no loads are being sourced from the MCP23008/230017. The datasheet is far from explicit on that point, but if you get a part and set it up so no outputs are being driven and no communication is happening I would expect to measure that current.

21 thoughts on “Adding an MCP23017 16 port IO expander to Arduino or Esp8266 or Attiny85 or……..”

  1. I am confused. How do I make HIGH or LOW separate pins on the expander? I understand it that every pin has it’s own address, where do I find it? By the way great article, it sums up basics and make it more understandable 🙂

    1. I explain that in the second half of my text starting at:
      “So if we only want to make PORTA.0 HIGH and the rest LOW, we write a binary value of 0b00000001 =1 to the A register. If we want to make PORTA.0 and PORTA.2 HIGH and the rest LOW we write a binary value of 0b00000101 = 5.
      For PORT B it is similar:”
      then i give some examples and conclude by saying it is easiest to use the library.
      The library allows you to use e.g.
      mcp.digitalWrite(0, HIGH); to write to pins and
      mcp.digitalRead(0); to read from pins

      The ‘own address’ the pins have is nothing more than it’s bit number in the port byte that is being read or written to
      If you choose to write to all pins at once, use mcp.writeGPIOAB(uint16_t)
      The ‘mcp’ prefix is the object name in the example. If you use a different name obviously, you need to use that when calling library procedures

  2. Nice! It’s good to finally find an article that explains a few things about the mcp23017 without oversimplifying things and without being too difficult either.

    1. Yes, just seen it. I think your idea is not going to work coz when you change the address, you need to reinitiize, but I will give it some more thought and get back to you

  3. You are one of a few that explains I2C & ESP8266, but I cannot get it to work. I have an ESP-01 and taking into account that GPIO0 and GPIO2 are the pins used, it does not discover (scanning) the MCP23017. I use a similar sketch for the Arduino UNO and it works fine. I am doing this on a breadboard and that may be the problem. Any suggestions?

    1. Other than a wiring problem there are not much reasons why that should happen. I presume you have chosen the right board on compilation and didnt swop sda and scl and have pull up resistors?
      Though it shouldn’t be necessary, it may help to state wire.begin(0.2) in your setup.
      Try some other wires and other holes on your breadboard.
      Are you using an esp-01 or esp-01S?

    1. You have to store the full byte that presents the status of port A or B, then you have to make the one bit high or low and write the byte back to the port.
      The easiest may be to use e.g. the adafruit library that has a command to individually address each pin. J described that in the article. If it is still not clear just ask again

  4. Hi all
    it is very interesting to rebuild this project.
    But I have a question it is possible to connect an ESP8266-01 and an the Port expander like MCP23017 with connection of an HX711 Scale sensor ?
    I am struggling at the moment, because the MCP23017 use also the same contact Pin’s of the ESP like SDA = GPIO-00 and the SCL = GPIO-o2 ..
    The HX711 uses also the same Pin’s like the mcp 23017 !!
    DataPin (HX711) = GPIO-00 (ESP) => SDA PIn of MCP)
    and the Clock-Pin (HX711) = GPIO-02(ESP) => SCL PIN of MCP)
    Is there any solution ?

    1. It is the intention to use the i2c pins of the esp8266. There is no problem connecting more than one i2c periferal as long as they have different addresses. The HX11 has a two-wire interface that i presume means i2c (but i have no experience with that chip)
      If you use the esp8266-01 and have used your i2c pins but stil would need a direct i/o pin, use gpio pins 1 and 3 (aka tx and rx)

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.