If an ESP32 has run out of GPIOs, an I²C GPIO expander is often cleaner than moving to a larger board. The three parts you are most likely to encounter are the MCP23017, PCF8574 and PCF8575. All can add digital inputs and outputs using only SDA and SCL, but they are not interchangeable in capability.
The short version is simple: MCP23017 is the best general-purpose choice when you want predictable GPIO behaviour, configurable pull-ups and proper interrupt control. PCF8574 is excellent when you only need eight cheap, simple I/O lines. PCF8575 extends the same PCF approach to 16 pins and is often the cheapest way to build a large button, LED or relay-control panel.
The key difference is architectural. MCP23017 behaves much more like ordinary microcontroller GPIO, with direction registers, output registers, pull-ups and configurable interrupt logic. PCF8574 and PCF8575 use quasi-bidirectional pins: there is no normal input/output direction register, and a pin is effectively used as an input by writing it HIGH and then reading it.
MCP23017 vs PCF8574 vs PCF8575 at a Glance
| Feature | MCP23017 | PCF8574 | PCF8575 |
|---|---|---|---|
| GPIO count | 16 | 8 | 16 |
| Interface | I²C | I²C / SMBus | I²C / SMBus |
| Maximum documented I²C speed | 1.7 MHz | 100 kHz | 400 kHz |
| Hardware address pins | 3 | 3 | 3 |
| Devices per address family | Up to 8 | Up to 8 | Up to 8 |
| GPIO direction register | Yes | No – quasi-bidirectional | No – quasi-bidirectional |
| Internal pull-up control | Yes | No conventional pull-up register | No conventional pull-up register |
| Interrupt output | 2 configurable outputs | 1 open-drain INT | 1 open-drain INT |
| Per-pin interrupt configuration | Yes | No – port-change style | No – port-change style |
| External reset pin | Yes | No dedicated reset | No dedicated reset |
| ESPHome support | Native | Native | Native through PCF8574 component |
| Best fit | Mixed inputs/outputs, buttons, control panels | Simple 8-pin expansion | Low-cost 16-pin expansion |
Do not choose only by pin count. PCF8575 and MCP23017 both provide 16 lines, but the MCP gives much more control over how each line behaves.
How I²C GPIO Expansion Works
An I²C expander sits on the same two-wire bus as sensors, displays and other peripherals:
ESP32 3.3V
│
├── SDA ─────────────┬── MCP23017 / PCF857x SDA
│ └── other I²C devices
│
├── SCL ─────────────┬── MCP23017 / PCF857x SCL
│ └── other I²C devices
│
└── GND ───────────────── common ground
The expander receives commands over I²C and changes its remote GPIOs. Reading a button means the ESP32 sends an I²C transaction and receives the expander’s port state. Driving an output means transmitting the new output state over the bus.
This makes expanders excellent for slow digital I/O such as switches, contact sensors, relay-module inputs, indicator LEDs and keypads. They are not replacements for native ESP32 pins when you need microsecond timing, high-frequency PWM, UART, I²S, WS2812 timing or similar specialised functions.
MCP23017: the Most Flexible Option
The MCP23017 provides two eight-bit GPIO banks, normally referred to as GPA0–GPA7 and GPB0–GPB7. Each pin can be individually configured as input or output. Inputs can use the chip’s internal pull-ups, and the device has a significantly more capable interrupt system than the PCF parts.
Microchip specifies I²C operation at 100 kHz, 400 kHz and up to 1.7 MHz. That does not mean your entire ESP32 bus should automatically run at 1.7 MHz: every other device on the same bus must also support the selected speed.
The MCP23017 also has polarity inversion, output-latch and interrupt-capture registers. For a large control panel, these details are useful because software can distinguish what changed instead of repeatedly treating the expander as one simple 16-bit port.
MCP23017 interrupt advantages
There are two interrupt outputs, INTA and INTB. They can operate independently or be combined, and individual GPIOs can be configured to interrupt on a change or when their state differs from a programmed default. An interrupt-capture register preserves the port state associated with the interrupt.
This is a major advantage for button panels or many contact sensors. The ESP32 can sleep or perform other work and react only when the MCP23017 reports a real input event.
PCF8574: Simple and Very Common
The PCF8574 adds eight I/O lines and is found on countless low-cost breakout boards, including many I²C backpacks for HD44780 character LCDs. It uses a much simpler internal architecture than the MCP23017.
Its eight pins are quasi-bidirectional. There is no separate direction register. Writing a zero actively drives a pin LOW. Writing a one releases it into its HIGH/input state using the device’s internal current-source behaviour. You then read the port to determine whether an external circuit is pulling that line LOW.
That approach is elegant for switches and simple outputs, but it can surprise users expecting ordinary push-pull GPIO behaviour. A PCF8574 pin driven HIGH is not equivalent to a normal ESP32 push-pull HIGH output.
TI specifies the PCF8574 for up to 100 kHz I²C. That is adequate for buttons, relays and character displays, but it is noticeably slower than the MCP23017 and PCF8575 if you need to update many lines frequently.
PCF8575: 16 Pins with the Same PCF Behaviour
The PCF8575 is essentially the larger option in this comparison: 16 quasi-bidirectional lines arranged as two eight-bit ports. It retains the simple PCF programming model but supports a faster 400 kHz I²C bus.
For a project that needs 16 low-speed digital lines and does not require MCP23017-style pull-up registers or sophisticated interrupt configuration, the PCF8575 can be excellent value. Typical examples include large relay boards, status indicators and panels of dry-contact inputs.
In ESPHome, PCF8575 is configured through the pcf8574 component with pcf8575: true. ESPHome numbers its pins from 0 to 15, even though the datasheet labels the two groups differently.
GPIO Direction: the Biggest Practical Difference
With MCP23017, software explicitly configures a pin as input or output. This matches the programming model developers already know from ESP32:
Pin 0 → INPUT_PULLUP
Pin 1 → INPUT
Pin 8 → OUTPUT
Pin 9 → OUTPUT
With PCF8574/PCF8575, the concept is different. A bit written LOW is an active LOW output. A bit written HIGH is released and can be read as an input. Libraries and ESPHome hide much of this behaviour, but it remains important electrically.
This is why MCP23017 is easier when the same PCB mixes several input and output types or when you want predictable pull-up behaviour. PCF parts are ideal when the application is simple enough that their quasi-bidirectional model is an advantage rather than a constraint.
Internal Pull-Ups: MCP23017 Wins
MCP23017 has configurable internal pull-ups on its GPIO inputs. In ESPHome you can therefore define an input with a pull-up much like a native ESP32 pin.
PCF8574 and PCF8575 do not have an equivalent programmable pull-up register. Their HIGH/input state includes the quasi-bidirectional current-source behaviour, but this should not be confused with a conventional selectable pull-up. For long button wires, noisy environments or defined logic thresholds, an external resistor is often the better design.
A typical dry-contact input is easy to wire: pull the input up to 3.3 V and let the switch connect it to ground. This also keeps the logic entirely within the ESP32’s 3.3 V domain.
Interrupts: All Three Can Avoid Constant Polling
All three devices have interrupt capability, but the sophistication differs.
| Feature | MCP23017 | PCF8574 / PCF8575 |
|---|---|---|
| Interrupt outputs | INTA + INTB | One INT |
| Output type | Configurable; open-drain available | Open-drain |
| Per-pin interrupt enable | Yes | Simple port change behaviour |
| Compare to stored/default state | Yes | No equivalent register system |
| Captured state register | Yes | No equivalent capture register |
| ESPHome interrupt-driven support | Yes | Yes |
Current ESPHome can use an interrupt_pin for both families. When configured, ESPHome reads the expander when its INT line changes instead of continuously polling it. The INT connection must go to a real ESP32 GPIO, not to another expander pin.
Because PCF8574/PCF8575 INT is active-low and open-drain, it requires a pull-up. MCP23017 can also be configured for open-drain interrupts, which is particularly useful if the expander is powered above 3.3 V while the ESP32 interrupt pin remains in the 3.3 V domain.
I²C Addresses and Using Multiple Expanders
Each part has three hardware address pins, normally labelled A0, A1 and A2. That gives eight address combinations.
| Device | Common address range | Notes |
|---|---|---|
| MCP23017 | 0x20–0x27 | A0–A2 select address |
| PCF8574 | 0x20–0x27 | A0–A2 select address |
| PCF8574A | 0x38–0x3F | Different base range from PCF8574 |
| PCF8575 | 0x20–0x27 | A0–A2 select address |
Notice the collision: MCP23017, PCF8574 and PCF8575 can all occupy the 0x20–0x27 range. If you mix them on one bus, choose address-pin combinations carefully. Two I²C devices cannot share the same address unless additional bus switching or multiplexing is used.
PCF8574A is useful when you need additional PCF8574 devices because its address family is different. Always run an I²C scan on the finished hardware rather than relying only on breakout-board labels.
3.3 V Wiring with ESP32
For a straightforward ESP32 design, power the expander from 3.3 V and keep SDA/SCL pull-ups at 3.3 V. All three families support operation in that range.
Breakout modules sometimes include their own I²C pull-up resistors. If you connect several modules, those pull-ups end up in parallel. Too many parallel pull-ups can make the effective resistance unnecessarily low, so inspect the modules instead of automatically adding another pair.
If an expander is powered from 5 V, do not automatically pull SDA, SCL or an interrupt line up to 5 V and connect it directly to the ESP32. Keep the ESP32 side within its 3.3 V logic domain and use a proper level-shifting arrangement where required.
Can These Expanders Drive Relays?
They can control relay modules or driver transistors, but do not connect a bare relay coil directly to an expander GPIO. A relay coil is an inductive load and normally requires a transistor or MOSFET driver plus flyback protection.
PCF8574 and PCF8575 are often used with active-low relay boards because pulling an output LOW fits their electrical behaviour well. However, check the relay module’s input current and voltage requirements. A board designed for 5 V logic is not automatically a clean match for a 3.3 V expander.
MCP23017 gives more conventional output behaviour and is the easier choice when you are designing a custom relay interface. For mains switching, the expander is only the logic side; isolation, relay/contact ratings, creepage and the enclosure still determine whether the finished product is safe.
For the complete output side, see our ESP32 smart relay with ESPHome guide.
Which Expander Is Best for Buttons and Contact Sensors?
For a few simple buttons, any of the three works well. For a large input panel, MCP23017 is normally the most comfortable because it provides pull-ups and richer interrupt handling.
| Input project | Best choice | Why |
|---|---|---|
| 4–8 simple buttons | PCF8574 | Cheap and sufficient |
| 8–16 dry contacts | MCP23017 or PCF8575 | Both provide enough pins |
| Long wired contact panel | MCP23017 | Clear input configuration and pull-up control |
| Interrupt-heavy button panel | MCP23017 | Per-pin interrupt configuration and capture |
| Lowest-cost 16-input panel | PCF8575 | Simple 16-bit device |
For door and window contacts, debounce in software and use sensible external wiring. Our ESP32 reed-switch Home Assistant guide covers the sensor-side logic and filtering.
Which Expander Is Best for a Keypad?
A matrix keypad repeatedly changes some lines between driven outputs and sensed inputs. MCP23017 is therefore the cleaner choice because direction and output state are explicitly controlled. Microchip even publishes application guidance for matrix keypads using its GPIO expanders.
PCF8574 can be used for keypads and is found in many hobby designs, but its quasi-bidirectional behaviour needs to be understood. If you are writing your own scanning code, do not assume each PCF pin behaves exactly like a native MCU GPIO.
ESPHome Example: MCP23017
Current ESPHome has native MCP23017 support. This example adds one relay-module output and one button input with an internal pull-up:
i2c:
sda: GPIO21
scl: GPIO22
scan: true
mcp23017:
- id: mcp_hub
address: 0x20
interrupt_pin: GPIO27
open_drain_interrupt: true
switch:
- platform: gpio
name: "Expander Relay"
pin:
mcp23xxx: mcp_hub
number: 8
mode:
output: true
inverted: false
binary_sensor:
- platform: gpio
name: "Expander Button"
pin:
mcp23xxx: mcp_hub
number: 0
mode:
input: true
pullup: true
interrupt: FALLING
inverted: true
filters:
- delayed_on: 20ms
- delayed_off: 20ms
ESPHome numbers MCP23017 GPA0–GPA7 as 0–7 and GPB0–GPB7 as 8–15. The interrupt pin shown above must be wired from the MCP23017’s interrupt output to ESP32 GPIO27.
ESPHome Example: PCF8574
For an eight-pin PCF8574:
i2c:
sda: GPIO21
scl: GPIO22
scan: true
pcf8574:
- id: pcf8_hub
address: 0x20
pcf8575: false
interrupt_pin: GPIO27
switch:
- platform: gpio
name: "PCF8574 Output 0"
pin:
pcf8574: pcf8_hub
number: 0
mode:
output: true
inverted: false
binary_sensor:
- platform: gpio
name: "PCF8574 Input 1"
pin:
pcf8574: pcf8_hub
number: 1
mode:
input: true
inverted: true
filters:
- delayed_on: 20ms
- delayed_off: 20ms
For a physical button, use a defined external bias suitable for your wiring. The PCF input model is different from MCP23017, so do not add an ESPHome pullup: true and expect a separate internal pull-up register to appear.
ESPHome Example: PCF8575
PCF8575 uses the same ESPHome component. Set pcf8575: true and use pins 0–15:
pcf8574:
- id: pcf16_hub
address: 0x21
pcf8575: true
interrupt_pin: GPIO27
switch:
- platform: gpio
name: "PCF8575 Output 12"
pin:
pcf8574: pcf16_hub
number: 12
mode:
output: true
inverted: false
The pcf8574: key is not a mistake: ESPHome’s PCF8574 component handles both the eight-bit and 16-bit devices.
Arduino Example: MCP23017
With the Adafruit MCP23X17 library, MCP23017 programming closely resembles normal Arduino GPIO:
#include <Wire.h>
#include <Adafruit_MCP23X17.h>
Adafruit_MCP23X17 mcp;
void setup() {
Serial.begin(115200);
Wire.begin();
if (!mcp.begin_I2C(0x20)) {
Serial.println("MCP23017 not found");
while (true) delay(100);
}
mcp.pinMode(0, INPUT_PULLUP);
mcp.pinMode(8, OUTPUT);
mcp.digitalWrite(8, LOW);
}
void loop() {
bool pressed = !mcp.digitalRead(0);
mcp.digitalWrite(8, pressed ? HIGH : LOW);
delay(20);
}
This example deliberately uses a low update rate. I²C GPIO should be treated as remote I/O, not as a substitute for timing-critical native GPIO.
Arduino Example: PCF8574 without a Library
The PCF8574 protocol is simple enough to demonstrate with Wire. Writing 0xFF leaves all eight lines in their HIGH/input state; reading returns the current port value:
#include <Wire.h>
constexpr uint8_t PCF_ADDR = 0x20;
void setup() {
Serial.begin(115200);
Wire.begin();
// Release all pins HIGH so they can be read as inputs.
Wire.beginTransmission(PCF_ADDR);
Wire.write(0xFF);
Wire.endTransmission();
}
void loop() {
Wire.requestFrom(PCF_ADDR, (uint8_t) 1);
if (Wire.available()) {
uint8_t state = Wire.read();
Serial.printf("Port = 0x%02X\n", state);
}
delay(250);
}
If you later drive some bits LOW, maintain a software copy of the output byte so you do not accidentally change unrelated pins when writing a new port value.
Performance and Bus Traffic
For human-speed controls, all three devices are fast enough. The differences matter when software repeatedly updates or scans many I/O lines.
- PCF8574: limited to 100 kHz, so use it for simple low-rate I/O.
- PCF8575: 400 kHz makes the 16-bit device more comfortable for larger panels.
- MCP23017: supports faster I²C and richer register access, but the practical bus speed is still limited by the slowest device on that I²C segment.
Using the interrupt pin is often more important than increasing bus speed. A 100 kHz expander that generates an interrupt only when a button changes can create less traffic than a faster expander polled hundreds of times per second.
What Not to Put on an I²C GPIO Expander
Use native ESP32 GPIO for functions that depend on tight timing or dedicated peripherals. Avoid putting these on MCP23017/PCF857x unless the specific software component explicitly supports it:
- WS2812 / NeoPixel data.
- High-frequency PWM.
- UART RX/TX.
- I²S signals.
- OneWire buses such as DS18B20.
- Fast pulse counting.
- Stepper-motor step pulses.
ESPHome makes the same distinction: expander pins can emulate GPIO for many components, but components that depend on more complicated communication or timing do not automatically work through an expander.
Common Problems
| Symptom | First checks |
|---|---|
| I²C scan finds no expander | Check 3.3 V power, common ground, SDA/SCL pins, pull-ups and address straps. |
| Wrong device responds at 0x20 | Check for address collision; MCP23017 and PCF857x share common address ranges. |
| PCF input always reads HIGH | Confirm the pin was released HIGH before reading and that the external switch actually pulls it LOW. |
| MCP button floats | Enable its pull-up or add an external bias resistor. |
| INT never changes | Check open-drain pull-up, interrupt configuration and wiring to a real ESP32 GPIO. |
| Relay works inverted | Many relay modules are active LOW; configure inverted: true if appropriate. |
| Random I²C errors after adding boards | Inspect total pull-up resistance, wiring length, bus speed and supply noise. |
| Fast pulse input misses events | Move the signal to a native ESP32 GPIO or dedicated counter peripheral. |
Which GPIO Expander Should You Buy?
Buy MCP23017 when the expander is an important part of the design rather than just a cheap way to gain pins. It is the best option for mixed input/output panels, many switches, interrupt-driven inputs and projects where predictable GPIO configuration matters.
Buy PCF8574 when you need only eight slow lines, especially for a few buttons, simple outputs or an LCD backpack. It is cheap, widely available and easy to understand once you remember the quasi-bidirectional behaviour.
Buy PCF8575 when you need 16 low-speed lines at minimal cost and the simpler PCF architecture is acceptable. It is particularly attractive for large relay/input boards where each line changes relatively slowly.
If the price difference is small, MCP23017 is the safest default. PCF8574 and PCF8575 remain excellent parts when their simpler electrical model matches the application.
Related ESP32 Guides
- ESP32 Smart Relay for Home Assistant — safe relay control and ESPHome configuration.
- ESP32 Reed Switch Door/Window Sensor — digital inputs, debounce and Home Assistant.
- Home Assistant Garage Door Controller with Feedback — relay outputs plus contact inputs.
- ESP32 Roller Shutter Control — an example where output count and interlocking matter.
Datasheets & External Resources
Specifications and ESPHome behaviour were checked against the current manufacturer and ESPHome documentation.
- Microchip MCP23017 product page and datasheet — I/O count, voltage range, interrupt features and I²C speeds.
- Texas Instruments PCF8574 documentation — quasi-bidirectional I/O, INT and 100 kHz I²C.
- Texas Instruments PCF8575 documentation — 16-bit expansion, INT and 400 kHz I²C.
- ESPHome MCP230xx component — MCP23017 pin mapping, pull-ups and interrupt configuration.
- ESPHome PCF8574/PCF8575 component — 8/16-pin setup and interrupt-driven operation.