Overview
Run out of pins mid-project? The MCP23017 hands you sixteen more over two I2C wires. Microchip's 16-bit port expander presents two full 8-bit GPIO ports (GPA0-7 and GPB0-7), each pin individually configurable as input or output, with optional internal pull-ups and a pair of interrupt outputs (INTA/INTB — silked ITA/ITB on this breakout) that can flag any input change without polling. This pre-soldered CJMCU-style breakout brings every pin to breadboard-friendly headers.
Address pins A0-A2 set three bits of the I2C address (0x20-0x27), so up to eight expanders share one bus — 128 extra GPIO from two microcontroller pins. The chip runs from 1.8-5.5V, matching 3.3V and 5V systems alike, and each pin sources/sinks a healthy 25 mA (chip total ~125 mA), enough to drive LEDs directly and switch transistors for anything bigger.
It's the classic fix for keypads and button matrices, LED banks, relay boards, rotary encoders farms, front panels with many switches, and retro-computing peripherals. One nuance worth knowing before you wire: this dual-marked board silks both I2C (MCP23017) and SPI (MCP23S17) pin names — with the I2C part fitted you use SDA/SI as SDA and SCL/SCK as SCL, and the NC/CS + NC/SO pads stay unconnected.
At a Glance
Specifications
| Parameter | Value |
| Chip | Microchip MCP23017 (I2C variant of the 23x17 family) |
| GPIO | 16 bidirectional pins in two ports (A and B) |
| I2C Address | 0x20 + A2:A1:A0 (tie each pin high or low — don't float) |
| Bus Speed | 100 kHz / 400 kHz / 1.7 MHz |
| Supply Voltage | 1.8 - 5.5V (logic levels follow VCC) |
| Drive | 25 mA per pin, ~125 mA total chip budget |
| Inputs | Optional per-pin 100k pull-ups; interrupt-on-change per pin |
| Interrupt Outputs | INTA (port A), INTB (port B), mirrorable into one line |
| RESET | Active-low — tie to VCC for normal use |
| Key Registers | IODIRA/B 0x00/0x01 · GPPUA/B 0x0C/0x0D · GPIOA/B 0x12/0x13 · OLATA/B 0x14/0x15 |
| Breakout Silk | Dual-marked for MCP23017/23S17 — use SDA/SI, SCL/SCK; leave NC/CS, NC/SO empty |
Pinout Diagram
Left column: address pins A2/A1/A0, RESET, the SPI-only pads (NC/SO, NC/CS — unused here), SDA/SI, SCL/SCK, GND, VCC. Right side: VCC/GND duplicates, the interrupt pair ITB/ITA, and the sixteen port pins B0/A0 through B7/A7 in paired columns. Note the right-side pairs are labeled "Bx/Ax" — the inner column is port B, the outer column port A.
Wiring Guide
Baseline for every platform: VCC and GND, SDA/SCL to the I2C bus, RESET tied to VCC, and A0-A2 tied to GND for address 0x20. Then treat GPA/GPB pins like any GPIO.
Arduino Wiring
| Breakout Pin | Arduino Pin | Details |
|---|---|---|
| VCC / GND | 5V / GND | |
| SDA/SI | A4 (SDA) | |
| SCL/SCK | A5 (SCL) | |
| RESET | 5V | Must be high |
| A0, A1, A2 | GND | Address 0x20 |
| Demo: A0 pin (GPA0) | LED + 330 ohm to GND | |
| Demo: B0 pin (GPB0) | Button to GND | Internal pull-up in code |
ESP32 Wiring
| Breakout Pin | ESP32 Pin | Details |
|---|---|---|
| VCC / GND | 3V3 / GND | 3.3V keeps GPIO levels ESP32-safe |
| SDA/SI | GPIO 21 | |
| SCL/SCK | GPIO 22 | |
| RESET | 3V3 | |
| A0-A2 | GND | 0x20 |
Raspberry Pi Wiring
| Breakout Pin | Pi Pin | Details |
|---|---|---|
| VCC / GND | Pin 1 (3.3V) / Pin 6 | 3.3V only on a Pi |
| SDA/SI | Pin 3 (GPIO 2) | |
| SCL/SCK | Pin 5 (GPIO 3) | |
| RESET | 3.3V | |
| A0-A2 | GND | i2cdetect shows 0x20 |
Raspberry Pi Pico Wiring
| Breakout Pin | Pico Pin | Details |
|---|---|---|
| VCC / GND | 3V3(OUT) / GND | |
| SDA/SI | GP4 (I2C0 SDA) | |
| SCL/SCK | GP5 (I2C0 SCL) | |
| RESET | 3V3 | |
| A0-A2 | GND | 0x20 |
Code Examples
Same demo everywhere: an LED on GPA0 blinks while a button on GPB0 (internal pull-up) is read — output port and input port exercised together. Arduino/ESP32 use the Adafruit library; Pi and Pico talk registers directly so you see exactly how the chip works.
Arduino
// MCP23017 - Arduino Example
// SDA->A4, SCL->A5, addr 0x20 | Library: "Adafruit MCP23017"
#include <Adafruit_MCP23X17.h>
Adafruit_MCP23X17 mcp;
void setup() {
Serial.begin(115200);
if (!mcp.begin_I2C(0x20)) {
Serial.println("MCP23017 not found - check wiring/address");
while (1);
}
mcp.pinMode(0, OUTPUT); // GPA0 = LED
mcp.pinMode(8, INPUT_PULLUP); // GPB0 = button (pins 8-15 = port B)
Serial.println("16 extra GPIO online at 0x20");
}
void loop() {
mcp.digitalWrite(0, HIGH);
delay(250);
mcp.digitalWrite(0, LOW);
delay(250);
if (mcp.digitalRead(8) == LOW) {
Serial.println("Button on GPB0 pressed!");
}
}
ESP32 (Arduino IDE) — 16-LED chaser
// MCP23017 - ESP32 Example: chase across all 16 pins
// SDA->21, SCL->22 | Library: "Adafruit MCP23017"
#include <Adafruit_MCP23X17.h>
Adafruit_MCP23X17 mcp;
void setup() {
Serial.begin(115200);
Wire.begin(21, 22);
if (!mcp.begin_I2C(0x20)) {
Serial.println("MCP23017 not found");
while (1) delay(10);
}
for (int p = 0; p < 16; p++) mcp.pinMode(p, OUTPUT);
}
void loop() {
for (int p = 0; p < 16; p++) { // GPA0..7 then GPB0..7
mcp.digitalWrite(p, HIGH);
delay(60);
mcp.digitalWrite(p, LOW);
}
}
Raspberry Pi (Python, direct registers)
#!/usr/bin/env python3
# MCP23017 - Raspberry Pi Example (raw registers via smbus2)
# SDA->GPIO2, SCL->GPIO3 | Install: pip3 install smbus2
from smbus2 import SMBus
import time
ADDR = 0x20
IODIRA = 0x00 # 1 = input, 0 = output
IODIRB = 0x01
GPPUB = 0x0D # port B pull-ups
GPIOA = 0x12
GPIOB = 0x13
bus = SMBus(1)
bus.write_byte_data(ADDR, IODIRA, 0x00) # port A all outputs
bus.write_byte_data(ADDR, IODIRB, 0xFF) # port B all inputs
bus.write_byte_data(ADDR, GPPUB, 0xFF) # pull-ups on port B
print("GPA0 blinking, watching GPB0...")
try:
led = False
while True:
led = not led
bus.write_byte_data(ADDR, GPIOA, 0x01 if led else 0x00)
b = bus.read_byte_data(ADDR, GPIOB)
if not (b & 0x01): # pulled low = pressed
print("Button on GPB0 pressed!")
time.sleep(0.25)
except KeyboardInterrupt:
bus.write_byte_data(ADDR, GPIOA, 0x00)
print("Stopped by user")
Raspberry Pi Pico (MicroPython, direct registers)
# MCP23017 - Pico MicroPython Example (raw registers)
# SDA->GP4, SCL->GP5
from machine import I2C, Pin
import time
i2c = I2C(0, sda=Pin(4), scl=Pin(5), freq=400000)
print("I2C scan:", [hex(a) for a in i2c.scan()]) # expect 0x20
ADDR = 0x20
def reg_write(reg, val): i2c.writeto_mem(ADDR, reg, bytes([val]))
def reg_read(reg): return i2c.readfrom_mem(ADDR, reg, 1)[0]
reg_write(0x00, 0x00) # IODIRA: port A outputs
reg_write(0x01, 0xFF) # IODIRB: port B inputs
reg_write(0x0D, 0xFF) # GPPUB: pull-ups on B
print("GPA0 blinking, watching GPB0...")
led = False
while True:
led = not led
reg_write(0x12, 0x01 if led else 0x00) # GPIOA
if not (reg_read(0x13) & 0x01): # GPIOB bit 0 low = pressed
print("Button on GPB0 pressed!")
time.sleep(0.25)