MCP23017 tutorial: 16 more GPIOs over i2c
You've run out of pins. The MCP23017 spends two of the ones you have left and hands back sixteen — with real direction control, built-in pull-ups and interrupts, which is exactly what the cheaper expander everyone reaches for doesn't give you.
The trade
Two pins in, sixteen GPIOs out
An expander is the cleanest deal in embedded: SDA and SCL are a shared bus, so those two pins were already spoken for by every other sensor you own. Adding an MCP23017 costs nothing extra in host pins and returns 16 fully-featured GPIOs.
They're not second-class pins either. Each one is push-pull — it can actively drive high or low — with a direction register, an optional internal pull-up, polarity inversion, and interrupt-on-change. The only thing they can't do is speed: every read or write is an i2c transaction, so you're looking at microseconds, not the nanoseconds of a native pin. For buttons, LEDs, relays and switches — the things you actually run out of pins for — that's irrelevant.
And it stacks. Three address pins give eight addresses (0x20–0x27), so eight chips is 128 GPIOs on the same two wires.
Pick the right chip
MCP23017 vs PCF8574: not a close call
Both are "i2c GPIO expanders" at similar prices, and they're genuinely different chips. The PCF8574 is the one most people meet first — usually without realising, because it's the chip on every i2c LCD backpack.
| Spec | MCP23017 | PCF8574 |
|---|---|---|
| Pins | 16 | 8 |
| Output type | true push-pull | quasi-bidirectional |
| Direction register | yes (IODIR) | none — write 1 and read back |
| Internal pull-ups | yes, per pin | no |
| Interrupt on change | yes (INTA/INTB) | yes (single INT) |
| Addresses | 0x20–0x27 | 0x20–0x27 (and 0x38–0x3F on the 'A') |
"Quasi-bidirectional" is the PCF8574's real limitation. It has no direction register: you make a pin an input by writing a 1 and then reading it back. That 1 isn't a strong drive, it's a weak internal pull-up — so a PCF8574 output can sink current properly but barely source any. Drive an LED from it high-side and you get a sad glow. The MCP23017 just drives the pin.
Take the PCF8574 when you want eight cheap outputs, or when you're talking to an LCD backpack that already has one. Take the MCP23017 for everything else. Both share 0x20–0x27, so they collide on one bus — see the collision roundup, and note the XL9535 relay boards sit in the same block.
Step 1
MCP23017 wiring and the address pins
- VCC / GND — 3.3–5 V, so it suits a Pi or an Uno.
- SDA / SCL — to your host, same as everything else on the bus.
- RESET — tie it high. It's active-low, and floating it means random resets. This is the number one reason a fresh MCP23017 "works intermittently".
- A0 / A1 / A2 — each to GND or VCC. All three to GND gives
0x20; the pins read as a 3-bit number added to that base, so A0 high alone is0x21, all three high is0x27. - GPA0–GPA7 / GPB0–GPB7 — your sixteen pins, in two 8-bit banks. INTA / INTB are optional interrupt outputs.
Then scan and confirm 0x20 before writing code. If the address is wrong, it's the A-pins; if it's flaky, it's RESET.
Step 2
MCP23017 Arduino code: a button and an LED
Install Adafruit's MCP23017 library, then:
#include <Adafruit_MCP23X17.h>
Adafruit_MCP23X17 mcp;
void setup() {
Serial.begin(115200);
if (!mcp.begin_I2C(0x20)) { // A0-A2 all tied to GND
Serial.println("No MCP23017 at 0x20 — check A0-A2 and RESET");
while (1);
}
mcp.pinMode(0, OUTPUT); // GPA0 -> LED (with a resistor!)
mcp.pinMode(8, INPUT_PULLUP); // GPB0 -> button to GND
}
void loop() {
// pressed == LOW, because the internal pull-up holds it high when open
bool pressed = (mcp.digitalRead(8) == LOW);
mcp.digitalWrite(0, pressed ? HIGH : LOW);
delay(20); // crude debounce
} That's deliberately familiar: pinMode, digitalRead, digitalWrite — the same names as native pins, just with an object in front. Pins 0–7 are bank A, 8–15 are bank B.
INPUT_PULLUP is doing real work here. It enables the chip's own ~100 kΩ pull-up on that pin, so a button wired straight to ground needs no external resistor. Wire sixteen buttons and you need sixteen buttons — nothing else. (These are the GPIO pull-ups, not the bus pull-ups on SDA/SCL, which are a separate matter your breakout probably already handles.)
Step 3
MCP23017 registers: what the library is hiding
Worth seeing once, because it makes the datasheet readable. The chip is three registers per bank:
| Register | Bank A / B | What it does |
|---|---|---|
| IODIR | 0x00 / 0x01 | direction — 1 = input, 0 = output (all inputs at reset) |
| GPPU | 0x0C / 0x0D | internal pull-up — 1 = enabled |
| GPIO | 0x12 / 0x13 | read pin states, or write output states |
Each is one byte, one bit per pin. So the whole of the sketch above is: write IODIRA to make GPA0 an output, write IODIRB to make GPB0 an input, set the matching GPPUB bit for its pull-up, then read GPIOB and write GPIOA in a loop.
Note everything is an input at reset — which is the safe default, and why a freshly-powered expander drives nothing until you tell it to.
You can poke these by hand. On a Pi, no code at all:
# make all of bank A outputs (IODIRA = 0x00)
i2cset -y 1 0x20 0x00 0x00
# drive GPA0 high (GPIOA = 0x01)
i2cset -y 1 0x20 0x12 0x01
# read bank B (GPIOB)
i2cget -y 1 0x20 0x13 Going further
Interrupts, and 128 pins on two wires
Stop polling. Reading sixteen buttons in a loop means hammering the bus forever to discover nothing changed. The MCP23017's interrupt-on-change flips it: tell it which pins to watch, wire INTA or INTB to a spare host GPIO, and it tells you. That's the standard answer to i2c having no "data ready" signal of its own — the same reason a laptop touchpad has a dedicated interrupt line.
The two banks have separate INT pins, so you can watch buttons on bank B while bank A drives outputs, and only get interrupted by the half you care about.
Stacking: eight addresses × 16 pins = 128 GPIOs on the same SDA/SCL. The limits are the usual ones — each board adds its pull-ups in parallel and more bus capacitance. And watch the current: about 25 mA per pin but only ~150 mA for the whole package, so sixteen LEDs at 20 mA is already over budget. For lots of LEDs, a PCA9685 is the right chip; for relays, drive them from their own supply.
Get one
Questions
MCP23017 FAQ
What is the MCP23017's i2c address?
0x20 by default, up to 0x27 using its three address pins A0, A1 and A2 — tie each to GND or VCC and the binary value lands on the base address. Eight addresses means eight chips, so 128 GPIOs on one pair of wires. Unlike some parts, these pins must be tied deliberately; the datasheet doesn't guarantee behaviour if you leave them floating.
What's the difference between the MCP23017 and the PCF8574?
The MCP23017 is the better chip and it isn't close. The PCF8574 gives you 8 quasi-bidirectional pins with no direction register — you fake an input by writing a 1 and reading back, and its 'high' is a weak pull-up that can't source real current. The MCP23017 gives 16 true push-pull pins with a real direction register, configurable internal pull-ups, and interrupt-on-change. The PCF8574 survives because it's cheap and it's what LCD backpacks use.
Can I drive LEDs directly from an MCP23017?
Small ones, carefully. Each pin sources or sinks around 25 mA, but the whole package is limited to about 150 mA total — so sixteen LEDs at 20 mA each would be roughly double what the chip can take. Use current-limiting resistors, keep the total in budget, and for anything brighter drive a transistor or use a dedicated LED driver like the PCA9685.
Does the MCP23017 have internal pull-up resistors?
Yes — around 100 kΩ per pin, enabled individually through the GPPU register. That's the feature that makes it excellent for buttons: enable the pull-up, wire the switch to ground, and read a 0 when pressed. No external resistors at all. Note these are the chip's GPIO pull-ups and have nothing to do with the i2c bus pull-ups on SDA and SCL, which are a separate concern.
How does the MCP23017 interrupt pin work?
It flags a pin change without you polling. Configure which pins to watch, and INTA or INTB goes active when one changes — wire that to a spare GPIO on your host and you get an interrupt instead of a loop hammering the bus. It's the standard fix for i2c's lack of any 'data ready' signal, and it's what makes a 16-button keypad practical.
Is the MCP23017 the same as the MCP23S17?
Same chip, different bus. The MCP23017 is i2c, the MCP23S17 is SPI. Identical registers and features, so code ports almost directly. The S17 is faster; the 017 costs you two pins instead of four and can share the bus you already have.