Skip to content

Instantly share code, notes, and snippets.

@ChickenProp
Created July 26, 2012 19:21
Show Gist options
  • Select an option

  • Save ChickenProp/3183960 to your computer and use it in GitHub Desktop.

Select an option

Save ChickenProp/3183960 to your computer and use it in GitHub Desktop.
Expanding the Raspberry Pi's GPIO capabilities with the MCP23017
### Introduction
The [MCP23017](http://www.skpang.co.uk/catalog/mcp23017-16bit-i2c-io-expander-for-raspberry-pi-p-1104.html) 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. It's not quite as simple as [directly controlling the Pi's GPIO pins](https://gist.github.com/3050085), 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](http://www.adafruit.com/datasheets/mcp23017.pdf) 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](http://elinux.org/File:GPIOs.png) 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).
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.)
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`.
### 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.
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](http://en.wikipedia.org/wiki/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`.
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, I didn't have issues.)
### 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`.
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.
* `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 `IPOLx` (`0x02`), `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.
@Tiersten
Copy link

The second I2C bus (Bus 1) is on the MIPI CSI-2 camera connector (S5) so if you don't have a camera plugged in then nothing will show up on the bus. It is 3.3V like the expansion pins so connecting anything to it with a higher voltage is a bad idea and will damage the SoC.

Pin 1 = Ground
Pin 13 = SCL
Pin 14 = SDA
Pin 15 = 3.3V

@ChickenProp
Copy link
Author

Thanks. I've put a note in the post.

@k3v1np
Copy link

k3v1np commented Oct 5, 2012

Very good stuff. Anyone know of any simple c++ code that demonstrates something similar to this?

Copy link

ghost commented Jan 27, 2013

I think you mixed up the sda and scl pins in your wiring diagram. Otherwise, nice post, thanks

@halherta
Copy link

halherta commented Apr 1, 2013

k3v1np check by blog entry . http://hertaville.com/2013/04/01/interfacing-an-i2c-gpio-expander-mcp23017-to-the-raspberry-pi-using-c/

It contains a C++ class that can be used for controlling the MCP23017. Its experimental but well explained

@chigs
Copy link

chigs commented Jun 8, 2013

Great intro. helped me massively to understand how to use the MCP23017 on the R Pi's I2C Bus. Thanks allot.

@RymeIntrinseca
Copy link

A very helpful tutorial. Thank you.

@attilagyorffy
Copy link

FYI I think in the intro diagram SCL and SDA may be reversed on the graphics.

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