Skip to content

Instantly share code, notes, and snippets.

@battila
Forked from ChickenProp/gist:3183960
Created September 2, 2016 23:41
Show Gist options
  • Save battila/bb9b1a761a22ae8b9fcf05bd527489ae to your computer and use it in GitHub Desktop.
Save battila/bb9b1a761a22ae8b9fcf05bd527489ae to your computer and use it in GitHub Desktop.
Expanding the Raspberry Pi's GPIO capabilities with the MCP23017

Introduction

The MCP23017 is an I/O expander chip. It has 16 GPIO pins which you can control using an I2C interface using two pins from a Raspberry Pi, plus a power source and sink (which can also come from the Pi). It's not quite as simple as directly controlling the Pi's GPIO pins, but it's not complicated, either.

You need to install i2c-tools, which is probably in your distribution's package manager. You also need a kernel with I2C support; you might need to modprobe i2c-dev. It would presumably be possible to do without either of these things, and bitbang the I2C protocol over GPIO, but I don't understand the protocol well enough to try.

On pin numbering: if you like, you can refer to the datasheet for the MCP23017. There's a small dot in one corner of the chip, with a semi-circular cut-out at that end. The pin nearest the dot is pin 1, with pins 2, 3, ..., 14 along that long side of the chip. On the other side, pins 15 through 28 go in the other direction, so that pin 15 is opposite pin 14 and pin 28 is opposite pin 1.

On the Pi, we'll be using pins 3 (SDA) and 5 (SCL) to talk to the MCP, and pins 1 (3v3) and 6 (ground) as power source and sink.

Setup

We'll start by connecting the chip. Connect pins 9 and 18 to 3v3, and pin 10 to ground. For now, connect pins 15 through 17 to ground as well. (They configure the I2C address of the chip, which I'll talk about later.) Now to be able to talk to the chip, connect pin 12 to the Pi's pin 5 (SCL) and pin 13 to the Pi's pin 3 (SDA). Here's my first ever attempt at a circuit diagram showing it, although I haven't included the Pi:

Circuit diagram

At this point, run sudo i2cdetect -y 0 and you should get the following output:

     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: 20 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --                         

This says there's an I2C device with address 0x20 that you can talk to. (From now on I won't bother telling you to use sudo. -y says "don't ask me for confirmation", and 0 is the I2C bus to use. Bus 1 also exists, but it won't detect anything. I'm not sure what it corresponds to.)

(Update: in the comments, Tiersten informs me that bus 1 is on the camera CSI connector, which is the thing in between the ethernet and HDMI ports.)

To actually talk to it, use the programs i2cget and i2cdump for reading, and i2cset for writing. The chip has 21 registers at 22 addresses (one register has two addresses), and reading and writing these allows you to read and write to the 16 GPIO pins. (The register addresses have nothing to do with the device address.)

Writing

Let's start by activating an LED on pin 1. The 16 GPIO pins are divided into banks 'A' and 'B' of eight pins each, and somewhat counterintuitively, pin 1 is GPB0 (pin 0 in bank B). We need to set it up for output; the register we need is IODIRB at address 0x01, which controls the direction of each pin in bank B. (They can all be set individually, but they're all done from the same register.)

To get the current value of this register, run i2cget -y 0 0x20 0x01. Here 0 is the bus again, 0x20 is the device address and 0x01 is the register address. It will probably print 0xff. This should be read as eight bits rather than as a single byte; the least significant bit corresponds to GPB0, and so on. A 1 bit indicates that pin is configured for input, and a 0 indicates output, so we need to turn off bit 1. Run i2cset -y 0 0x20 0x01 0xfe, which means "assign value 0xfe to register 0x01 of device 0x20 on bus 0". Of course, you could use 0x00 or something else instead of 0xfe, as long as the final bit is 0.

Now to turn it on. (Connect the LED first, if you haven't already. Its + terminal connects to pin 1 on the MCP, its - terminal connects to ground via a suitable resistor.) The register we use for this is GPIOB at address 0x13. Write a 1 to bit 1 of this register and the LED should turn on: i2cset -y 0 0x20 0x13 0x01. Write a 0 again to turn it off: i2cset -y 0 0x20 0x13 0x00.

Circuit diagram

Reading

Now we'll read the state of a button, which we'll put on pin 28. This is GPA7, i.e. pin 7 on bank A. So connect a button with one terminal connected to pin 28 and the other connected to ground.

Circuit diagram

The direction register for bank A is IODIRA at address 0x00. Like IODIRB, it's probably already set up as 0xff, but if it doesn't already have the most significant bit set (which is true iff its value is less than 0x80), you'll need to write to it.

Now you can read the value of the button as the most significant bit from GPIOA, address 0x12. Except that there's no power being supplied to that pin or to the button, so you'll just read 0x00.

You can fix this by putting a pull-up resistor into your circuit; a 10 kΩ resistor between 3v3 and pin 28 will do the trick. But the MCP has pull-up resistors built-in, they just aren't enabled by default. To enable it for GPA7, we use register GPPUA at address 0x0c, and turn on the MSB: i2cset -y 0 0x20 0x0c 0x80. (Only two of the Pi's GPIO pins have pull-up resistors, so even if you don't need the extra GPIOs, the MCP might make your circuit simpler.)

(Update: it appears that in fact all of the Pi's GPIO pins have pull-up resistors, and most have pull-down resistors as well. The gpio program from WiringPi can enable and disable these.)

After this, i2cget -y 0 0x20 0x12 should return 0x00 if the putton is pressed, and 0x80 if the button is released. I found that for a short time after releasing the button, I would read 0xc0, indicating that GPA6 was also returning a 1 bit. I assume this is just due to electrical interference or something; pin 27 isn't connected to either power or ground, so its value is unreliable. (When I enabled its pull-up resistor, or connected it to ground, it read the expected value every time.)

Miscellaneous extras

You can read the value of every register using i2cdump -y 0 0x20. This actually returns 256 registers; I don't know what happens if you try to write to a register that doesn't exist, but I wouldn't be surprised if it's possible to destroy the chip like that, so I'm not going to try.

You can set the address of the device to any value from 0x20 to 0x28 by connecting pins 15, 16 and 17 to a combination of power and ground. These pins are called A0, A1 and A2 respectively; the device address is 0b10cba where a is 1 if A0 is connected to power and 0 if it's connected to ground; b and c are the same for A1 and A2. If you don't connect them, I think their values depend on things like electrical interference and can't be relied upon.

The device address seems to be important if you have multiple I2C devices on one bus. I assume the protocol here is to connect SCL and SDA to both devices in parallel, and use the device address to only talk to one of them. But until I get a second I2C device, I can't test that.

If you haven't looked at the datasheet yet, the 16 GPIO pins are pins 1 through 8 (GPB0 through GPB7) and pins 21 through 28 (GPA0 through GPA7).

Most of the registers have an A version and a B version; the address of the A version has its least significant bit 0, and the address of the B version has its LSB 1. So IODIRA and IODIRB are at 0x00 and 0x01; GPPUA and GPPUB are at 0x0c and 0x0d; etc. The exception is the register IOCON, which can be considered shared between the two pin banks; it has addresses 0x0a and 0x0b.

The registers that I understand are (register address given for bank A):

  • IODIRx (0x00): set pins in the given bank to be either input or output pins.
  • IOPOLx (0x02): invert polarity of input pins. In the example above, i2cset -y 0 0x20 0x02 0x80 would cause reads of GPA7 to return 1 when the button is pressed and 0 when it's not. Has no effect when reading output pins.
  • GPPUx (0x0c): enable or disable pull-up resistors.
  • GPIOx (0x12): read and write the values of pins. If not all pins in a bank have the same direction, you'll sometimes be reading an output pin or writing an input pin. If you read an output pin, you'll be told its current value. If you write an input pin, you'll set the value it takes when it next becomes an output pin.
  • OLATx (0x14): for an output pin, reading and writing this will have the same effect as GPIOx. For an input pin, its value is the value that the pin will take if it becomes set to output. Writing to GPIOx actually modifies this register. ("OLAT" means "output latch".)

The other registers are GPINTENx (0x04), DEFVALx (0x06), INTCONx (0x08), IOCON, INTFx (0x0e) and INTCAPx (0x10). Mostly they seem related to interrupt pins 19 (INTB) and 20 (INTA), but I haven't looked closely into that.

IOCON does expose one interesting feature: its most significant bit is called BANK. If you set BANK to 1 (i.e. i2cset -y 0 0x20 0x0a 0x80) it gives almost every register a different address. (Only IODIRA stays the same.) The new address is the old one, but with the last five bits rotated one to the right; so a register's bank is now given by its 0x10 bit instead of its 0x01 bit. I'm not sure why this is considered useful; maybe for compatibility with other chips? I don't think you can always tell just by looking which mode the chip is in, because IOCON also moves; but I doubt that's a significant problem in the real world.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment