I/O Expanders
Adding inputs and outputs to an ESP32 with MCP23017 and PCF8574 I²C expanders and the CD4067 multiplexer, and wiring the I²C bus they share.
A full panel quickly needs more inputs and outputs than an ESP32 has free pins. An expander hosts buttons, switches, LEDs or analog inputs on its own pins, and talks to the ESP32 over a few shared lines. Each part wired to it is addressed by the expander plus a channel — which of the expander's pins it sits on.
| Chip | Pins | Connection | Best for |
|---|---|---|---|
| MCP23017 | 16 digital in/out | I²C, 0x20–0x27 | Mixed button and LED panels |
| PCF8574 / PCF8574A | 8 quasi-bidirectional | I²C, 0x20–0x27 / 0x38–0x3F | Cheap button banks, sinking LEDs |
| CD4067 | 16 analog or digital | 1 signal pin + 4 select lines | Many potentiometers on one ADC pin |
| 74HC165 / 74HC595 | 8 in / 8 out per chip, chainable | Data, clock, latch | Long input or LED chains — see Shift Registers |
I²C uses two wires — SDA (data) and SCL (clock) — shared by every device on it. A controller has one I²C bus : all MCP23017 and PCF8574 expanders and every LCD or OLED hang off the same two pins, and each device is told apart by its address . On a classic ESP32 the usual pins are GPIO 21 (SDA) and GPIO 22 (SCL), but any two output-capable pins will do.
| Device | Addresses | Set by |
|---|---|---|
| MCP23017 | 0x20–0x27 | A0–A2 pins |
| PCF8574 | 0x20–0x27 | A0–A2 pins or jumpers |
| PCF8574A | 0x38–0x3F | A0–A2 pins or jumpers |
| LCD backpack (PCF8574) | usually 0x27 or 0x3F | Solder jumpers on the backpack |
| SSD1306 / SH1106 OLED | 0x3C or 0x3D | Resistor jumper on the back |
| Bus speed | Use when |
|---|---|
| 100 kHz (standard) | Long wires, many devices, or an unreliable bus |
| 400 kHz (fast) | Short wires on one panel; faster input scanning and screen updates |
Character LCDs and OLEDs share this bus — see Text Displays . Run this scanner to see which addresses answer:
// I²C scanner — lists every device address on the bus
#include <Wire.h>
void setup() {
Serial.begin(115200);
Wire.begin(21, 22); // SDA, SCL
for (uint8_t addr = 0x08; addr < 0x78; addr++) {
Wire.beginTransmission(addr);
if (Wire.endTransmission() == 0) {
Serial.printf("Found device at 0x%02X\n", addr);
}
}
}
void loop() {}The MCP23017 gives you 16 digital pins in two ports of eight. Each pin can be an input with an internal pull-up (buttons, switches, encoders) or an output (LEDs, up to ~25 mA per pin). Tie A0–A2 to GND or 3.3 V to pick one of eight addresses, 0x20–0x27 — so up to eight chips, 128 pins, on one bus. Tie RESET high, or the chip stays in reset.
| Chip pins | Channel | Shown as |
|---|---|---|
| GPA0–GPA7 (port A) | 0–7 | MCP23017 0x20 · A0 … A7 |
| GPB0–GPB7 (port B) | 8–15 | MCP23017 0x20 · B0 … B7 |
// MCP23017 at 0x20: button on GPA0, LED on GPB0 (Adafruit_MCP23X17 library)
#include <Adafruit_MCP23X17.h>
Adafruit_MCP23X17 mcp;
void setup() {
Serial.begin(115200);
Wire.begin(21, 22);
mcp.begin_I2C(0x20);
mcp.pinMode(0, INPUT_PULLUP); // GPA0 = channel 0
mcp.pinMode(8, OUTPUT); // GPB0 = channel 8
}
void loop() {
bool pressed = mcp.digitalRead(0) == LOW;
mcp.digitalWrite(8, pressed ? HIGH : LOW);
delay(10);
}On a CockpitConnect controller you don't write this: add the MCP23017 with its address in the Flow Builder, then put a Button, Switch or LED on one of its channels. The Flow Builder shows it as, for example, "MCP23017 0x20 · B3", and the controller scans the chip and debounces each input for you.
The PCF8574 is the cheapest way to add 8 pins . Its pins are quasi-bidirectional : there is no direction register. A pin written HIGH is held up by a weak internal pull-up (about 100 µA) and can be read as an input; a pin written LOW sinks current hard. The PCF8574 answers on 0x20–0x27 and the PCF8574A on 0x38–0x3F, so you can fit up to sixteen on one bus.
| Use | Wiring | Notes |
|---|---|---|
| Button / switch | P0–P7 → contact → GND | Pin reads LOW while closed; no resistor needed |
| LED | 3.3 V → 330 Ω → LED → P0–P7 | Wire the LED to sink: it lights when the pin goes LOW |
| Bright or many LEDs | P0–P7 → transistor or ULN2003 | The weak pull-up can't source enough current to light an LED |
// PCF8574 at 0x21: read 8 buttons in one transfer
#include <Wire.h>
void setup() {
Serial.begin(115200);
Wire.begin(21, 22);
Wire.beginTransmission(0x21);
Wire.write(0xFF); // all pins HIGH = inputs with weak pull-up
Wire.endTransmission();
}
void loop() {
Wire.requestFrom(0x21, 1);
uint8_t pins = Wire.read(); // bit = 0 means that button is pressed
Serial.println(pins, BIN);
delay(50);
}The CD4067 is an analog switch: the four select lines S0–S3 connect one of its 16 channels C0–C15 to the single SIG pin. Because it passes analog voltages, it is the way to read many potentiometers or slide pots through one ADC1 pin — ADC2 can't be read while WiFi is on. It works for buttons and switches too. The signal pin is what identifies the mux; the select lines can be shared with a second CD4067 on a different signal pin.
| CD4067 pin | Connect to |
|---|---|
| SIG (1) | An ADC1 pin (GPIO 32–39) for analog, any input pin for digital |
| S0–S3 (10, 11, 14, 13) | Four output-capable GPIOs |
| EN (15) | GND (always on), or a GPIO to switch the mux off — optional |
| VDD (24) / VSS (12) | 3.3 V / GND — not 5 V, or the signal can exceed the ESP32's pin voltage |
| C0–C15 | Pot wipers (pot ends to 3.3 V and GND), or contacts to GND with pull-ups |
// CD4067: read 16 potentiometers through one ADC1 pin
const int SIG_PIN = 34; // ADC1 — ADC2 is unusable while WiFi is on
const int SEL_PINS[] = { 25, 26, 27, 14 };
void selectChannel(uint8_t channel) {
for (int i = 0; i < 4; i++) {
digitalWrite(SEL_PINS[i], (channel >> i) & 1);
}
}
void setup() {
Serial.begin(115200);
for (int i = 0; i < 4; i++) {
pinMode(SEL_PINS[i], OUTPUT);
}
}
void loop() {
for (uint8_t ch = 0; ch < 16; ch++) {
selectChannel(ch);
delayMicroseconds(10); // let the mux settle
Serial.printf("C%u = %d\n", ch, analogRead(SIG_PIN));
}
delay(100);
}On a CockpitConnect controller you don't write this: add the CD4067 in the Flow Builder and put Analog Inputs or Buttons on its channels. The controller steps through the channels, and calibrates and filters each analog channel like a pin of its own.
| Application | Expander | Notes |
|---|---|---|
| Overhead panel (30+ switches) | 2–3 × MCP23017 | One bus, addresses 0x20, 0x21, 0x22 |
| MCP / FCU buttons with LEDs | MCP23017 | Buttons on port A, their lights on port B |
| Simple button bank | PCF8574 | Eight momentary buttons for the price of a coffee |
| Throttle quadrant (throttles, prop, mixture, trim) | CD4067 | All levers through one ADC1 pin |
| Lighting knobs on a panel | CD4067 | Panel, flood and instrument dimmers as Analog Inputs |