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.
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:
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.)
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
.
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 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.)
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 ofGPA7
to return1
when the button is pressed and0
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 asGPIOx
. For an input pin, its value is the value that the pin will take if it becomes set to output. Writing toGPIOx
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.