Documentation

MCP23017 Pre-Soldered I2C 16-Bit I/O Port Expander for Arduino, Raspberry Pi & ESP32 | ShillehTek Product Manual
Documentation / MCP23017 Pre-Soldered I2C 16-Bit I/O Port Expander for Arduino, Raspberry Pi & ESP32 | ShillehTek Product Manual

MCP23017 Pre-Soldered I2C 16-Bit I/O Port Expander for Arduino, Raspberry Pi & ESP32 | ShillehTek Product Manual

mcp23017-i2c-16bit-io-port-expander-presolderedshillehtek

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

GPIO Added
16 (GPA0-7, GPB0-7)
Interface
I2C @ 0x20 - 0x27
Per-Pin Current
25 mA source/sink
Pull-ups
Internal 100k, per pin
Interrupts
INTA / INTB outputs
Supply Voltage
1.8 - 5.5V

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.

MCP23017 I2C 16-bit IO expander breakout pinout diagram showing address pins, SDA SCL, interrupts and GPA GPB ports

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.ino
// 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.ino
// 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)

mcp23017_rpi.py
#!/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.py
# 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)

Frequently Asked Questions

Nothing at 0x20 on an I2C scan. What now?
Confirm RESET is tied HIGH — a floating/low RESET keeps the chip silent, and it's the most-missed wire on this breakout. Then make sure every address pin is tied (all to GND for 0x20); floating address pins produce ghost/shifting addresses. Finally check you're on the SDA/SI and SCL/SCK pads — not the NC/CS / NC/SO pair, which belong to the SPI variant only.
How do the library pin numbers map to GPA/GPB?
Adafruit's library numbers pins 0-15: 0-7 are GPA0-GPA7 and 8-15 are GPB0-GPB7 (so "pin 8" in code is the GPB0/B0 pad). Working with raw registers, each port is its own byte: bit 0 of GPIOA is GPA0, and so on. The board's Bx/Ax labels list port B inner, port A outer on the paired columns.
Can it drive relays, or LED strips?
Directly: LEDs, optocouplers, and small loads inside 25 mA/pin (mind the ~125 mA whole-chip budget — sixteen LEDs at 20 mA exceeds it, so use 8-10 mA per LED or bank them). Relays and anything inductive go through a transistor/ULN2803 driver stage. And it's plain GPIO — no PWM and no WS2812-style precise timing, so it dims nothing and can't drive NeoPixels.
How fast can I toggle pins through it?
Every operation is an I2C transaction: at 400 kHz that's tens of microseconds per byte, so realistic toggle rates are tens of kHz at best — thousands of times slower than native GPIO. It's built for human-speed I/O: buttons, LEDs, relays, panels. Keep bit-banged protocols and precise timing on the microcontroller's own pins.
How do the interrupts (ITA/ITB) work?
Enable interrupt-on-change per input pin (GPINTEN register or the library's setupInterrupts/setupInterruptPin) and the chip drops INTA (port A) or INTB (port B) when any watched pin changes; reading INTCAP or GPIO clears it. Wire those to one MCU interrupt pin (mirror mode joins them) and 16 buttons cost you a single interrupt line instead of polling.
Can I mix 3.3V and 5V — chip at one, devices at the other?
The chip's GPIO levels follow its VCC, so power it at the voltage of the things it talks to — but its I2C side must then match the master's levels too. Same-voltage everywhere is clean; mixed systems need a level shifter on SDA/SCL (or run everything at 3.3V — the MCP23017 is perfectly happy there). Never power it at 5V on a Raspberry Pi's bus.
Eight expanders on one bus — anything to watch?
Give each a unique A2:A1:A0 combination (0x20-0x27), keep the shared bus wiring short with one set of pull-ups (most breakouts and host boards already provide them), and open one library object per address. 128 GPIO later, the only real constraints are total bus capacitance — stay under ~30 cm of daisy-chain — and remembering which address drives which panel.

Related Tutorials