MCP23017 16-Bit I/O Expander – Complete Guide

The MCP23017 16-bit I/O expander module is one of the easiest ways to add extra GPIO pins to your ESP32 project. The module is based on the MCP23017 from Microchip and gives you 16 additional, fully configurable I/O pins over a simple two-wire I²C bus. Running out of pins on a compact board like the ESP32-C6 SUPER MINI? This is the solution.

In this complete guide we cover:

  • What the MCP23017 is
  • Technical specifications
  • Pinout (MCU-2317 / CJMCU-2317 breakout)
  • I²C address selection
  • GPIO overview – all 16 pins and their functions
  • ESP32-C6 SUPER MINI wiring
  • Register map explained
  • ESP-IDF example code
  • Interrupts
  • MCP23017 vs PCF8574 comparison
  • Practical engineering tips

What is the MCP23017?

The MCP23017 is a 16-bit I/O expander controlled over I²C. It adds two 8-bit GPIO ports — PORT A (GPA0–GPA7)and PORT B (GPB0–GPB7) — to your microcontroller, each pin individually configurable as input or output.

Unlike simple shift registers, the MCP23017 includes:

  • 16 bidirectional I/O pins
  • Individually configurable internal 100 kΩ pull-ups
  • Two interrupt outputs (INTA / INTB), mirrorable to one pin
  • Interrupt-on-change with programmable compare values
  • Configurable input polarity inversion
  • Up to 8 devices on one I²C bus (128 extra GPIOs!)

There is also an SPI version of this chip, the MCP23S17. The MCU-2317 breakout board supports both, which is why several pins carry double labels such as SDA/SI and SCL/SCK. In this guide we use the I²C version.

Technical Specifications

ParameterValue
I/O pins16 (2 × 8-bit ports)
InterfaceI²C (MCP23017) / SPI (MCP23S17)
I²C speed100 kHz, 400 kHz, 1.7 MHz
I²C address range0x20 – 0x27 (3 address pins)
Supply voltage1.8V – 5.5V
Max current per pin25 mA (source/sink)
Max total current125 mA per package
Internal pull-ups100 kΩ, per pin selectable
Interrupt outputsINTA, INTB (mirrorable)
Operating temperature-40°C to +125°C

⚠️ Power the module from 3.3V when using it with an ESP32. The chip itself tolerates 5V, but the ESP32 I²C pins are not 5V tolerant — running everything at 3.3V keeps the logic levels perfectly matched.

Pinout

The MCU-2317 (also sold as CJMCU-2317) breakout exposes the following pins:

PinDescription
VCC3.3V supply
GNDGround
SDA/SII²C data (SPI MOSI on MCP23S17)
SCL/SCKI²C clock (SPI clock on MCP23S17)
RESETActive-low reset — must be tied HIGH
A0, A1, A2I²C address selection
NC/S0Not connected (SPI MISO on MCP23S17)
NC/CSNot connected (SPI chip select on MCP23S17)
ITA (INTA)Interrupt output PORT A
ITB (INTB)Interrupt output PORT B
B0/A0 – B7/A7The 16 GPIO pins (GPB0–GPB7 / GPA0–GPA7)

The small 8-pin component marked 103 on the board is a 10 kΩ resistor array providing the I²C pull-ups — so no external pull-up resistors are needed for short wire runs.

⚠️ The most common mistake with this module: leaving RESET floating. The pin is active-low, and without a connection the chip randomly stays in reset and is never found on the bus. Always tie RESET to 3.3V (directly or through 10 kΩ).

I²C Address Selection

The three address pins A0, A1 and A2 set the 7-bit I²C address. This allows up to 8 modules on one bus:

A2A1A0I²C Address
GNDGNDGND0x20
GNDGNDVCC0x21
GNDVCCGND0x22
GNDVCCVCC0x23
VCCGNDGND0x24
VCCGNDVCC0x25
VCCVCCGND0x26
VCCVCCVCC0x27

⚠️ The address pins must never float — always tie them to GND or VCC. For a single module, connect all three to GND for address 0x20.

GPIO Overview – Pins, Capabilities and Functions

Every one of the 16 GPIO pins offers the same capabilities. This table shows what each pin can do:

Module PinGPIOChip PinInputOutputInternal Pull-upInterrupt-on-ChangePolarity Inversion
B0/A0 (right)GPA021✅ 100 kΩ✅ → INTA
B1/A1 (right)GPA122✅ 100 kΩ✅ → INTA
B2/A2 (right)GPA223✅ 100 kΩ✅ → INTA
B3/A3 (right)GPA324✅ 100 kΩ✅ → INTA
B4/A4 (right)GPA425✅ 100 kΩ✅ → INTA
B5/A5 (right)GPA526✅ 100 kΩ✅ → INTA
B6/A6 (right)GPA627✅ 100 kΩ✅ → INTA
B7/A7 (right)GPA728✅ 100 kΩ✅ → INTA
B0/A0 (left)GPB01✅ 100 kΩ✅ → INTB
B1/A1 (left)GPB12✅ 100 kΩ✅ → INTB
B2/A2 (left)GPB23✅ 100 kΩ✅ → INTB
B3/A3 (left)GPB34✅ 100 kΩ✅ → INTB
B4/A4 (left)GPB45✅ 100 kΩ✅ → INTB
B5/A5 (left)GPB56✅ 100 kΩ✅ → INTB
B6/A6 (left)GPB67✅ 100 kΩ✅ → INTB
B7/A7 (left)GPB78✅ 100 kΩ✅ → INTB

What the GPIO pins cannot do — important to know:

FunctionAvailable?Alternative
Digital input
Digital output
PWMUse ESP32 LEDC pins or a PCA9685
ADC (analog input)Use ESP32 ADC pins or an ADS1115
DACUse an MCP4725
High-speed switching❌ (limited by I²C)Native ESP32 GPIO

The MCP23017 is perfect for buttons, switches, relays, LEDs, rotary encoders and keypad matrices — anything that doesn’t require analog or high-frequency signals.

Connecting to the ESP32-C6 SUPER MINI

Only four wires are needed, plus the fixed connections on the module itself:

MCP23017 ModuleESP32-C6 SUPER MINIWire
VCC3V3Red
GNDGNDBlack
SDA/SIGPIO6Green
SCL/SCKGPIO7Yellow
RESET3V3
A0, A1, A2GND

Notes:

  • GPIO6 and GPIO7 are free, safe pins on the ESP32-C6 SUPER MINI — the I²C peripheral can be routed to any GPIO thanks to the ESP32 GPIO matrix.
  • Avoid GPIO8: on most SUPER MINI boards the onboard RGB LED is connected there.
  • NC/S0 and NC/CS remain unconnected (they are only used by the SPI variant).
  • INTA/INTB are optional — connect them to a free GPIO only if you want interrupt-driven inputs (see below).

Register Map

The MCP23017 is controlled through 22 registers. In the default mode (IOCON.BANK = 0) the A and B registers are interleaved:

RegisterAddress (A/B)Function
IODIRA / IODIRB0x00 / 0x01Direction: 1 = input, 0 = output
IPOLA / IPOLB0x02 / 0x03Input polarity inversion
GPINTENA / GPINTENB0x04 / 0x05Interrupt-on-change enable
DEFVALA / DEFVALB0x06 / 0x07Default compare value for interrupts
INTCONA / INTCONB0x08 / 0x09Interrupt control (compare vs. change)
IOCON0x0A / 0x0BConfiguration (mirror, bank, polarity)
GPPUA / GPPUB0x0C / 0x0D100 kΩ pull-up enable
INTFA / INTFB0x0E / 0x0FInterrupt flag (which pin fired)
INTCAPA / INTCAPB0x10 / 0x11Pin state captured at interrupt
GPIOA / GPIOB0x12 / 0x13Read/write the port
OLATA / OLATB0x14 / 0x15Output latch

⚠️ After power-up all pins default to input. Always configure IODIRA/IODIRB first.

ESP-IDF Example Code

The example below configures GPA0 as output (blinking LED) and GPB0 as input with internal pull-up (button to GND), using the new ESP-IDF I²C master driver:

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/i2c_master.h"
#include "esp_log.h"

#define I2C_SDA_GPIO        6
#define I2C_SCL_GPIO        7
#define MCP23017_ADDR       0x20

/* MCP23017 registers (BANK = 0) */
#define REG_IODIRA          0x00
#define REG_IODIRB          0x01
#define REG_GPPUB           0x0D
#define REG_GPIOA           0x12
#define REG_GPIOB           0x13

static const char *TAG = "MCP23017";
static i2c_master_dev_handle_t mcp_handle;

static esp_err_t mcp_write_reg(uint8_t reg, uint8_t value)
{
    uint8_t buf[2] = { reg, value };
    return i2c_master_transmit(mcp_handle, buf, sizeof(buf), -1);
}

static esp_err_t mcp_read_reg(uint8_t reg, uint8_t *value)
{
    return i2c_master_transmit_receive(mcp_handle, &reg, 1, value, 1, -1);
}

void app_main(void)
{
    /* I²C master bus */
    i2c_master_bus_config_t bus_config = {
        .i2c_port = I2C_NUM_0,
        .sda_io_num = I2C_SDA_GPIO,
        .scl_io_num = I2C_SCL_GPIO,
        .clk_source = I2C_CLK_SRC_DEFAULT,
        .glitch_ignore_cnt = 7,
        .flags.enable_internal_pullup = true,
    };
    i2c_master_bus_handle_t bus_handle;
    ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, &bus_handle));

    /* MCP23017 device @ 0x20, 400 kHz */
    i2c_device_config_t dev_config = {
        .dev_addr_length = I2C_ADDR_BIT_LEN_7,
        .device_address = MCP23017_ADDR,
        .scl_speed_hz = 400000,
    };
    ESP_ERROR_CHECK(i2c_master_bus_add_device(bus_handle, &dev_config, &mcp_handle));

    /* GPA0 = output, rest of PORT A = input */
    ESP_ERROR_CHECK(mcp_write_reg(REG_IODIRA, 0xFE));
    /* PORT B = all inputs */
    ESP_ERROR_CHECK(mcp_write_reg(REG_IODIRB, 0xFF));
    /* Enable pull-up on GPB0 */
    ESP_ERROR_CHECK(mcp_write_reg(REG_GPPUB, 0x01));

    ESP_LOGI(TAG, "MCP23017 initialized at 0x%02X", MCP23017_ADDR);

    bool led_state = false;

    while (1) {
        /* Blink LED on GPA0 */
        led_state = !led_state;
        mcp_write_reg(REG_GPIOA, led_state ? 0x01 : 0x00);

        /* Read button on GPB0 (LOW = pressed) */
        uint8_t port_b;
        if (mcp_read_reg(REG_GPIOB, &port_b) == ESP_OK) {
            ESP_LOGI(TAG, "Button GPB0: %s",
                     (port_b & 0x01) ? "released" : "pressed");
        }

        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

Because the MCP23017 works with plain register reads and writes, no external library is required — the ESP-IDF I²C driver is all you need.

Prefer Arduino?

With the Adafruit MCP23017 library it’s even shorter:

#include <Adafruit_MCP23X17.h>

Adafruit_MCP23X17 mcp;

void setup() {
  Wire.begin(6, 7);              // SDA = GPIO6, SCL = GPIO7
  mcp.begin_I2C(0x20);
  mcp.pinMode(0, OUTPUT);        // GPA0 → LED
  mcp.pinMode(8, INPUT_PULLUP);  // GPB0 → button
}

void loop() {
  mcp.digitalWrite(0, !mcp.digitalRead(8));
  delay(50);
}

Interrupts

Polling 16 inputs over I²C wastes bus bandwidth. The MCP23017 solves this elegantly with interrupt-on-change:

  1. Enable the interrupt per pin via GPINTENA/GPINTENB
  2. Choose the trigger: any change (INTCON = 0) or compare against DEFVAL (INTCON = 1)
  3. Connect INTA (PORT A) and/or INTB (PORT B) to a free ESP32 GPIO
  4. Set the MIRROR bit in IOCON to combine both interrupts on a single pin
  5. On interrupt: read INTF (which pin) and INTCAP (captured state) — this also clears the interrupt

This way the ESP32 only touches the bus when something actually changes — ideal for battery-powered and event-driven designs.

MCP23017 vs PCF8574

A common alternative is the PCF8574 from NXP/Texas Instruments:

FeatureMCP23017PCF8574
I/O pins168
Max I²C speed1.7 MHz100 kHz
Direction registerYesNo (quasi-bidirectional)
Internal pull-upsConfigurable 100 kΩFixed weak current source
Interrupt outputs2 (INTA/INTB)1
Interrupt capture registersYesNo
Max devices per bus88
Current per pin25 mA~25 mA sink only

When to Choose the MCP23017

  • You need more than 8 extra pins
  • True push-pull outputs required
  • Advanced interrupt handling
  • Faster bus speeds

When to Choose the PCF8574

  • Only a few extra pins needed
  • Absolute minimum cost
  • Classic LCD backpack applications

Practical Engineering Tips

1. Never Leave RESET Floating

The number one reason an MCP23017 is “not found” on the I²C bus. Tie it to 3.3V, done.

2. Run an I²C Scanner First

Before writing any application code, scan the bus. If 0x20 shows up, your wiring is correct. If not, check RESET, the address pins and your SDA/SCL wiring.

3. Watch the Current Limits

25 mA per pin sounds like plenty, but the total package limit is 125 mA. Driving 16 LEDs at 20 mA each = 320 mA = a dead chip. Use transistors or a driver IC for higher loads.

4. Keep I²C Wires Short

The onboard 10 kΩ pull-ups are fine for a breadboard. For longer runs (> 30 cm) or multiple devices, add external 4.7 kΩ pull-ups to 3.3V.

5. Use OLAT for Read-Modify-Write

When toggling individual output bits, read OLATA/OLATB (the output latch) instead of GPIOA/GPIOB. Reading GPIO returns the actual pin state, which can differ under load.

Conclusion

The MCP23017 16-bit I/O expander is a small module that solves a big problem: running out of GPIO pins. Combined with the ESP32-C6 SUPER MINI it turns a tiny board into an I/O powerhouse.

It offers:

  • 16 extra GPIO pins over just 2 wires
  • Internal pull-ups — fewer external components
  • Powerful interrupt system
  • Up to 8 modules per bus: 128 extra pins
  • Simple register-based control, no library required

For ESP32 + ESP-IDF developers building button matrices, relay boards or LED panels, the MCP23017 is still one of the best-value building blocks you can add to your parts drawer.

error: Content is protected !!
Scroll to Top