t r o n i x s t u f f

fun and learning with electronics

Tutorial: Maximising your Arduino’s I/O ports

This is chapter forty-one of a series originally titled “Getting Started/Moving Forward with Arduino!” by John Boxall – a series of articles on the Arduino universe. The first chapter is here, the complete series is detailed here. Any files from tutorials will be found here.

Welcome back fellow arduidans!

In this article we discuss how to use the Microchip MCP23017 16-bit serial expander with I2C serial interface. This 28-pin IC offers sixteen inputs or outputs – and up to eight of the ICs can be used on one I2C bus… offering a maximum of 128 extra I/O ports. A few people may be thinking “Why not just get an Arduino Mega2560?” – a good question. However you may have a distance between the Arduino and the end-point of the I/O pins – so with these ICs you can run just four wires instead of a lot more; save board space with custom designs, and preserve precious digital I/O pins for other uses. Plus I think the I2C bus is underappreciated! So let’s get started…

Here is our subject of the article in DIP form:

At this point you should also download yourself a copy of data sheet – it will be referred to several times, and very useful for reference and further reading. Furthermore if you are not familiar with Arduino and the I2C bus, please familiarise yourself with the I2C tutorials parts one and two. The MCP23017 can be quite simple or complex to understand, so the goal of this article is to try and make it as simple as possible. After reading this you should have the knowledge and confidence to move forward with using a MCP23017.

First, let’s look at the hardware basics of this IC. Consider the pinouts:

The sixteen I/O ports are separated into two ‘banks’ – A (on the right) and B (on the left. Pin 9 connects to 5V, 10 to GND, 11 isn’t used, 12 is the I2C bus clock line (Arduino Uno/Duemilanove analogue pin 5, Mega pin  21), and 13 is the I2C bus data line (Arduino Uno/Duemailnove analogue pin 4, Mega pin 20). External pull-up resistors should be used on the I2C bus – in our examples we use 4.7k ohm values. Pin 14 is unused, and we won’t be looking at interrupts, so ignore pins 19 and 20. Pin 18 is the reset pin, which is normally high – therefore you ground it to reset the IC. So connect it to 5V!

Finally we have the three hardware address pins 15~17. These are used to determine the I2C bus address for the chip. If you connect them all to GND, the address is 0×20. If you have other devices with that address or need to use multiple MCP23017s, see figure 1-2 on page eight of the data sheet. You can alter the address by connecting a combination of pins 15~17 to 5V (1) or GND (0). For example, if you connect 15~17 all to 5V, the control byte becomes 0100111 in binary, or 0×27 in hexadecimal.

Next, here is a basic schematic illustrating how to connect an MCP23017 to a typical Arduino board. It contains the minimum to use the IC, without any sensors or components on the I/O pins:

Now to examine how to use the IC in our sketches.

Remember – if you are using Arduino v1.0 or greater – you will need to change all the Wire.send commands in our examples to Wire.write, and use Wire.read() instead of Wire.receive(). 

As you should know by now most I2C devices have several registers that can be addressed. Each address holds one byte of data that determines various options. With the MCP23017 the registers can be ordered in one of two ways – see tables 1.3 and 1.4 on page nine of the data sheet. In our examples we will use the addresses listed on table 1.4. So the first command to use in void setup() is:

  Wire.beginTransmission(0x20);
  Wire.send(0x12);
  Wire.send(0x20); // use table 1.4 addressing
  Wire.endTransmission();

The next is to set the I/O ports as inputs or outputs. First we will work with outputs. When the MCP23017 is turned on or reset, it defaults to inputs so we need to change them. So we use:

  Wire.beginTransmission(0x20);
  Wire.send(0x00); // IODIRA register
  Wire.send(0x00); // set all of bank A to outputs
  Wire.send(0x00); // set all of bank B to outputs
  Wire.endTransmission();

Go back to the data sheet and see table 1.4. Notice how we started with the IODIRA (“I/O direction, bank A”) register at 0×00 and sent two bytes? You can do this without having to separate address the second register. This only works when the registers have sequential addresses, as in this example we wanted a byte to go to 0×00 then 0×01. We sent zero which in binary is 00000000 – each bit refers to one output of the bank and refers to I/O pins 7~0.

So now we are in void loop()  or a function of your own creation and want to control some output pins. To control bank A, we use:

  Wire.beginTransmission(0x20);
  Wire.send(0x12); // address bank A
  Wire.send(??);  // value to send
  Wire.endTransmission();

… replacing ?? with the binary or equivalent hexadecimal or decimal value to send to the register. To calculate the required number, consider each I/O pin from 7 to 0 matches one bit of a binary number – 1 for on, 0 for off. So you can insert a binary number representing the output levels. Or if binary does your head in, convert it to hexadecimal. So for example, you want pins 7 and 1 on. In binary that would be 10000010, in hexadecimal that is 0×82, or 130 decimal. (Using decimals is convenient if you want to display values from an incrementing value or function result).

If you had some LEDs via resistors connected to the outputs, you would have this as a result of sending 0×82:

Now if you want to address all the outputs at once, just send the byte for bank B after bank A. For example, we want bank A to be 11001100 and bank B to be 10001000 – so we send the following:

  Wire.beginTransmission(0x20);
  Wire.send(0xCC); // address bank A
  Wire.send(0x88); // address bank B
  Wire.endTransmission();

… with the results as such (bank B on the left, bank A on the right):

You can also just address bank B, if so bank A does not change. Now let’s put all of this output knowledge into a more detailed example. From a hardware perspective we are using a circuit as described above, with the addition of a 560 ohm resistor followed by an LED thence to ground from on each of the sixteen outputs. Here is the sketch (download):

Example 41.1

/*
 Example 41.1 - Microchip MCP23017 with Arduino
 http://tronixstuff.wordpress.com/tutorials > chapter 41
 John Boxall | CC by-sa-nc
*/

// pins 15~17 to GND, I2C bus address is 0x20

#include "Wire.h"

void setup()
{
  Wire.begin(); // wake up I2C bus

  // setup addressing style
  Wire.beginTransmission(0x20);
  Wire.send(0x12);
  Wire.send(0x20); // use table 1.4 addressing
  Wire.endTransmission();

  // set I/O pins to outputs
  Wire.beginTransmission(0x20);
  Wire.send(0x00); // IODIRA register
  Wire.send(0x00); // set all of bank A to outputs
  Wire.send(0x00); // set all of bank B to outputs
  Wire.endTransmission();
}

void binaryCount()
{
  for (byte a=0; a<256; a++)
  {
    Wire.beginTransmission(0x20);
    Wire.send(0x12); // GPIOA
    Wire.send(a);    // bank A
    Wire.send(a);    // bank B
    Wire.endTransmission();
    delay(100);
  }
}

void loop()
{
  binaryCount();
  delay(500);
}

And here is the example blinking away:

Although that may have seemed like a simple demonstration, it was created show how the outputs can be used. So now you know how to control the I/O pins set as outputs. Note that you can’t source more than 25 mA of current from each pin, so if switching higher current loads use a transistor and an external power supply and so on.

Now let’s turn the tables and work on using the I/O pins as digital inputs. The MCP23017 I/O pins default to input mode, so all we need to do is set the addressing method as such in void setup()

  // setup addressing style
  Wire.beginTransmission(0x20);
  Wire.send(0x12);
  Wire.send(0x20); // use table 1.4 addressing
  Wire.endTransmission();

Then in the void loop() or other function all we do is set the address of the register to read and receive one byte of data. For our next example, we have our basic sketch as described at the start of this article using four normally-open buttons (once again using the ‘button board‘) which are connected to bank B inputs 0~3. Consider the first five lines of void loop() in the following example (download);

Example 41.2

/*
 Example 41.2 - Microchip MCP23017 with Arduino
 http://tronixstuff.wordpress.com/tutorials > chapter 41
 John Boxall | CC by-sa-nc
 */

// pins 15~17 to GND, I2C bus address is 0x20
#include "Wire.h"
byte inputs=0;

void setup()
{
  Serial.begin(9600);
  Wire.begin(); // wake up I2C bus

  // setup addressing style
  Wire.beginTransmission(0x20);
  Wire.send(0x12);
  Wire.send(0x20); // use table 1.4 addressing
  Wire.endTransmission();
}

void loop()
{
  Wire.beginTransmission(0x20);
  Wire.send(0x13); // set MCP23017 memory pointer to GPIOB address
  Wire.endTransmission();
  Wire.requestFrom(0x20, 1); // request one byte of data from MCP20317
  inputs=Wire.receive(); // store the incoming byte into "inputs"
  if (inputs>0) // if a button was pressed
  {
    Serial.println(inputs, BIN); // display the contents of the GPIOB register in binary
    delay(200); // for debounce
  }
}

In this example void loop() sends the GPIOB address (0×13) to the IC. Then using Wire.requestFrom() it asks for one byte of data from the IC – the contents of the register at 0×13. This byte is stored in the variable inputs. Finally if inputs is greater than zero (i.e. a button has been pressed) the result is sent to the serial monitor window and displayed in binary. We display it in binary as this represents the state of the inputs 0~7. Here is an example of pressing the buttons 1, 2, 3 then 4 – three times:

And as we are reading eight inputs at once – you can detect multiple keypresses. The following is an example of doing just that:

As you can see pressing all four buttons returned 1111, or the first and third returned 101. Each combination of highs and lows on the inputs is a unique 8-bit number that can also be interpreted in decimal or hexadecimal. And if you wanted to read all sixteen inputs at once, just request and store two bytes of data instead of one.

For our last example – a demonstration of using bank A as outputs and bank B as inputs. Four LEDs with matching resistors are connected to bank A outputs 0~3, with the buttons connected as per example 41.2. Here is the sketch (download):

Example 41.3

/*
 Example 41.3 - Microchip MCP23017 with Arduino
 http://tronixstuff.wordpress.com/tutorials > chapter 41
 John Boxall | CC by-sa-nc
*/

// pins 15~17 to GND, I2C bus address is 0x20
#include "Wire.h"
byte inputs=0;

void setup()
{
  Serial.begin(9600);
  Wire.begin(); // wake up I2C bus

  // setup addressing style
  Wire.beginTransmission(0x20);
  Wire.send(0x12);
  Wire.send(0x20); // use table 1.4 addressing
  Wire.endTransmission();

  Wire.beginTransmission(0x20);
  Wire.send(0x00); // IODIRA register
  Wire.send(0x00); // set all of bank A to outputs
  Wire.endTransmission();
}

void loop()
{
  // read the inputs of bank B
  Wire.beginTransmission(0x20);
  Wire.send(0x13);
  Wire.endTransmission();
  Wire.requestFrom(0x20, 1);
  inputs=Wire.receive();

  // now send the input data to bank A
  Wire.beginTransmission(0x20);
  Wire.send(0x12); // GPIOA
  Wire.send(inputs);    // bank A
  Wire.endTransmission();
  delay(200); // for debounce
}

By now there shouldn’t be any surprises in the last example – it receives a byte that represents bank B, and sends that byte out to bank A to turn on the matching outputs and LEDs. For the curious, here it is in action:

So there you have it… another way to massively increase the quantity of digital I/O pins on any Arduino system by using the I2C bus.

Have fun and keep checking into tronixstuff.com. Why not follow things on twitterGoogle+, subscribe  for email updates or RSS using the links on the right-hand column, or join our Google Group – dedicated to the projects and related items on this website. Sign up – it’s free, helpful to each other –  and we can all learn something.

August 26, 2011 - Posted by | arduino, education, I2C, microcontrollers | , , , , , , , , , , , , , , , ,

23 Comments »

  1. Just finished you latest tutorial, which was great as usual, and noticed, what I think is, a conflict in op-code usage.

    void setup()
    {
    Wire.begin(); // wake up I2C bus

    // setup addressing style
    Wire.beginTransmission(0×20);

    ======================================
    Shouldn’t this op-code be 0x0A for IOCON
    Wire.send(0×12);
    ======================================

    Wire.send(0×20); // use table 1.4 addressing
    Wire.endTransmission();

    void binaryCount()
    {
    for (byte a=0; a<256; a++)
    {
    Wire.beginTransmission(0×20);

    ====================================
    Otherwise how do you resolve this conflict
    Wire.send(0×12); // GPIOA
    ====================================

    Wire.send(a); // bank A
    Wire.send(a); // bank B
    Wire.endTransmission();
    delay(100);
    }
    }

    Keep up the tutorials.
    Hugh

    Comment by smithh | August 27, 2011 | Reply

    • Possibly, however the hardware worked perfectly with the published sketch. I’ve disassembled the hardware so will try your code later when I update the article.
      cheers
      john

      Comment by John Boxall | August 28, 2011 | Reply

    • I agree. My reading of the data sheet shows that it’s the BANK bit in the IOCON register that changes the way the registers are mapped.

      The BANK bit is set low at Power On Reset, which is why the sketch still works. You can comment these lines out and the sketch still works fine.

      If you want to use the alternate address register mapping then the BANK bit needs to be set high. The BANK bit is bit 7, so to change you write 0×80 into address 0x0A. (You need to write 0×00 into 0×05 to revert back, as the mapping has changed.)

      Enjoying and learning lots as I work though these. Thanks for putting them out there.

      Comment by Mal | September 25, 2011 | Reply

  2. Hi John
    Great tutorial , as I know I have some expander chips I thought I would give it a go ,
    but as luck would have it, ( bad luck mainly) my chips are all MCP23S17 which I believe are
    the SPI version not I2C.
    Any chance of you doing a tutorial with this chip ?

    Don

    Comment by Don | September 3, 2011 | Reply

  3. waiting for your next tutorial.

    Comment by hayankata | September 16, 2011 | Reply

    • I’m in holiday mode for a few weeks, next tutorial in October.

      Comment by John Boxall | September 17, 2011 | Reply

  4. You get holidays as well !!!
    next thing you will be wanting tea breaks :-)

    Don

    Comment by Don | September 17, 2011 | Reply

    • Well that’s the joy of working for oneself :)

      Comment by John Boxall | September 17, 2011 | Reply

  5. I have a question – It seems like you can only change the bus address on the last 3 digits, meaning there’s a maximum of 7 unique addresses you can set it to. With only 7 Addresses, and 16 I/O per Chip, that’s a maximum of 112 I/O. For my project, I need about 150 – is there any way to change the 1st-4th bits of the address?

    Comment by Snipeye | September 25, 2011 | Reply

    • You can use eight MCP23017s at once :) which gives you 128 I/O. There is also the MCP23S17 which is identical but has an SPI interface – so you could use a couple of those as well to solve your problem.

      Comment by John Boxall | September 25, 2011 | Reply

      • Hi, when I google about I2C limited address solution, I found this PCA9545. Can you make tutorial about I2C multiplexer?

        Comment by Fairul Izham | March 27, 2012

      • Yes, I will. It is on my to-do list – probably in about one month.
        John

        Comment by John Boxall | March 28, 2012

  6. Hi Snipye, you wrote

    “I have a question – It seems like you can only change the bus address on the last 3 digits, meaning there’s a maximum of 7 unique addresses”

    Surely its 8 addresses , remember “0″ is also a unique address

    000, 001, 010, 011, 100, 101, 110, 111

    Don

    Comment by Don | September 25, 2011 | Reply

  7. Thanks for a very interesting tutorial
    This is great for home automation…
    Keep the good working

    Comment by Antonio | September 25, 2011 | Reply

  8. I’ve been playing around with this chip but have found it to be intermittently dropping out of communication with with Arduino. I had to remove power from the chip, and then reset the Arduino to recover. Adding a 10k pull up resistor to the Reset pin seems to have fixed it for me.

    Cheers.

    Comment by Mal | September 29, 2011 | Reply

  9. you’re tutorials are simply great
    it’s my first stop when I need to educate myself on a subject to implement it with Arduino
    keep it up!

    Comment by Steve | October 3, 2011 | Reply

  10. “your tutorials”
    in the old days I used to laugh whenever I would see people confusing “your” with “you’re” but I guess after all these years the internet has influenced me as well lol

    Comment by Steve | October 3, 2011 | Reply

    • Thanks for your feedback, I’m happy you can make use of the articles.
      cheers
      john

      Comment by John Boxall | October 3, 2011 | Reply

  11. example 41.1Question: “From a hardware perspective we are using a circuit as described above, with the addition of a 560 ohm resistor followed by an LED thence to ground from on each of the sixteen outputs” Is this to say the i/o pins are wired PIN > RESISTOR > LED ANODE then LED CATHODE >GND ?

    Comment by Ubuntu Guru | May 1, 2012 | Reply

    • Yes.

      Comment by John Boxall | May 1, 2012 | Reply

      • Thanks John, I’ve wired up example41p1 using 10K resistors on the 12 and 13 (arduino 4 and 5) and 330ohm resistors on each of the sixteen output pins but I get nothing from the LEDs. I followed the schematic exactly as you have it with the exception of the resistor values. What might I be doing wrong? Any ideas? Thank you again.
        Caesar

        Comment by Ubuntu Guru | May 2, 2012

      • First use 4.7k resistors as described for the pull-ups on the I2C bus. Then double-check your wiring. The code compiles fine on a fresh v23 installation.

        Comment by John Boxall | May 2, 2012


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 2,588 other followers