← All projects
Basics Intermediate

PCF8574: Eight Extra Pins Over I2C — Buttons, LEDs and an Interrupt

The chip on the back of every LCD 1602 I2C backpack also works as a pin expander. We look at its quasi-bidirectional ports, why an LED must be switched on with LOW, and how the INT pin saves you from polling the bus all the time.

Difficulty
Intermediate
Time
45 min
Category
Basics
Updated

About the project

The PCF8574 adds 8 digital pins, P0–P7, to any board with I2C. One bus can take up to 8 of these chips (addresses 0x20–0x27) plus 8 PCF8574As (0x38–0x3F) — up to 128 pins on two wires. In this project four buttons toggle four LEDs, and the microcontroller learns about presses through an interrupt.

What you need

  • ESP32 DevKit (or an Arduino)
  • A PCF8574 module (the blue board with A0–A2 jumpers)
  • 4 buttons, 4 LEDs, 4 × 330 Ω resistors

Wiring

PCF8574 ESP32
VCC 3V3
GND GND
SDA GPIO21
SCL GPIO22
INT GPIO4
P0–P3 LEDs: 3V3 → resistor → anode, cathode → P0…P3
P4–P7 buttons: P4…P7 → button → GND

The A0–A2 jumpers set the three low address bits: all to GND gives 0x20.

A quasi-bidirectional port

The PCF8574 has no pinMode(). Each pin either pulls to GND with a strong transistor (up to 25 mA) or, when you write 1, barely pulls up to VCC with about 100 µA. Two rules follow:

  1. An input is a pin you wrote 1 to. A button to GND easily beats the weak pull-up, and read() returns 0.
  2. An LED lights on LOW. Wire it from VCC through a resistor to the pin: at 0 the chip sinks the current. An LED from the pin to GND barely glows from those 100 µA. The simulator spots that wiring and tells you.

The chip handles up to 80 mA in total through GND, so eight LEDs at 10 mA each are fine.

Why INT

Polling I2C in every loop() is wasted traffic and delay. The INT output (open drain, active LOW) goes low as soon as any input changes and returns HIGH after the port is read. Connect it to a GPIO with a pull-up and read the PCF8574 only after an interrupt sets a flag.

Code

// 4 buttons and 4 LEDs on two I2C wires: a PCF8574 expander
#include <Wire.h>
#include <PCF8574.h>

PCF8574 pcf(0x20);
const int INT_PIN = 4;          // the module's INT: open drain, active LOW
volatile bool changed = true;   // true: read the state right after start-up

void IRAM_ATTR onChange() { changed = true; }

uint8_t leds = 0;          // bit = LED on
uint8_t lastButtons = 0;

void setup() {
  Serial.begin(115200);
  Wire.begin();
  pcf.begin(0xFF);   // all pins at 1: inputs with pull-ups, LEDs off
  if (!pcf.isConnected()) {
    Serial.println("PCF8574 not found");
    while (true) delay(1000);
  }
  pinMode(INT_PIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(INT_PIN), onChange, FALLING);
}

void loop() {
  if (!changed) return;   // nothing changed — leave the bus alone
  changed = false;
  uint8_t buttons = (~pcf.read8() >> 4) & 0x0F;   // P4..P7, 1 = pressed
  uint8_t pressedNow = buttons & ~lastButtons;      // just pressed
  lastButtons = buttons;
  if (!pressedNow) return;
  leds ^= pressedNow;                                // each button toggles its LED
  pcf.write8(0xF0 | (~leds & 0x0F));                 // P4..P7 stay inputs (1), an LED lights at 0
  Serial.printf("LEDs: %d %d %d %d\n", leds & 1, (leds >> 1) & 1, (leds >> 2) & 1, (leds >> 3) & 1);
}

Note the write8(0xF0 | …): the high bits are always written as ones, otherwise the buttons stop being inputs.

Another library, another syntax

There is also Renzo Mischianti's library with the same PCF8574.h name and an Arduino-style API: pcf8574.pinMode(P0, OUTPUT); pcf8574.begin(); pcf8574.digitalWrite(P0, LOW);. Underneath it is the same quasi-bidirectional port, so "an LED lights on LOW" applies there too. The simulator supports both.

What next

  • A 4×4 keypad on one PCF8574: rows as outputs, columns as inputs.
  • Active-LOW relay modules fit a PCF8574 perfectly.
  • An LCD 1602 I2C is the same PCF8574 soldered to a display. If an I2C scanner finds 0x27 or 0x3F, that is what it is.