Skip to main content

Shift Registers

Expanding ESP32 output pins with 74HC595 shift registers to drive large annunciator panels and LED arrays from just three GPIO lines.

What is a Shift Register?

A shift register converts serial data (one bit at a time) into parallel outputs (all bits at once). The 74HC595 is a Serial-In Parallel-Out (SIPO) device: you clock 8 bits in over a single data wire, then latch them simultaneously to 8 output pins. Multiple chips can be daisy-chained to produce 16, 24, or more outputs while still using only 3 GPIO pins.

Property74HC595
TypeSerial-In, Parallel-Out (SIPO)
Outputs8 per chip (Q0–Q7), unlimited via daisy chain
Control pins3 (DATA, CLOCK, LATCH)
Supply voltage2–6 V (3.3 V compatible with ESP32)
Output current≤ 35 mA per pin, ≤ 70 mA total
Max clock speed~25 MHz (well above Arduino/ESP32 shiftOut speed)

Key pins

Pin nameNumberFunction
DS14Serial data input
SH_CP11Shift register clock (rising edge shifts data in)
ST_CP12Storage register clock — latch pulse copies to outputs
OE13Output Enable, active LOW — tie to GND
SRCLR10Shift register clear, active LOW — tie to VCC
Q0–Q715, 1–7Parallel outputs
Q7S9Serial output — connect to DS of the next chip in chain
Wiring — Single 74HC595
74HC595 GPIO 23 DS (data) GPIO 22 SH_CP (clock) GPIO 21 ST_CP (latch) 3.3 V VCC (pin 16) GND GND (pin 8) OE→GND · SRCLR→VCC Q0 → 220 Ω → LED Q1 → 220 Ω → LED Q2 → 220 Ω → LED Q3 → 220 Ω → LED Q4 → 220 Ω → LED Q5 → 220 Ω → LED Q6 → 220 Ω → LED Q7 → 220 Ω → LED Q7S → DS of next chip
74HC595 pinConnect toColour
DS (14)GPIO 23Orange
SH_CP (11)GPIO 22Yellow
ST_CP (12)GPIO 21Green
VCC (16)3.3 VRed
GND (8)GNDBlack
OE (13)GND (tie low — always enabled)
SRCLR (10)VCC (tie high — never clear)
Q0–Q7 (15, 1–7)220–470 Ω → LED anode
Arduino Code — Single Chip

Arduino's built-in shiftOut() function handles the bit-banging. Pull LATCH low before shifting, then high after to atomically update all outputs simultaneously — this prevents partial flicker during the shift.

const int DATA_PIN  = 23;   // DS  — serial data
const int CLOCK_PIN = 22;   // SH_CP — shift clock
const int LATCH_PIN = 21;   // ST_CP — storage (latch) clock

// Push 8 bits into the 595 and latch them to the outputs
void write595(uint8_t value) {
    digitalWrite(LATCH_PIN, LOW);
    shiftOut(DATA_PIN, CLOCK_PIN, MSBFIRST, value);
    digitalWrite(LATCH_PIN, HIGH);
}

void setup() {
    pinMode(DATA_PIN,  OUTPUT);
    pinMode(CLOCK_PIN, OUTPUT);
    pinMode(LATCH_PIN, OUTPUT);

    write595(0b00000000);   // all LEDs off on start
}

void loop() {
    write595(0b01010101);   // LEDs 0, 2, 4, 6 on
    delay(500);
    write595(0b10101010);   // LEDs 1, 3, 5, 7 on
    delay(500);
}
Daisy-Chaining Multiple Chips

Connect Q7S (pin 9) of the first chip to DS (pin 14) of the second. Share SH_CP and ST_CP across all chips. Shift data for the farthest chip first — it travels through each Q7S→DS link on its way to the final position.

// Two 74HC595 daisy-chained — 16 outputs, same 3 GPIO pins
// Data for the far chip is shifted out first (MSBFIRST order)

const int DATA_PIN  = 23;
const int CLOCK_PIN = 22;
const int LATCH_PIN = 21;

void write595x2(uint8_t chip2, uint8_t chip1) {
    digitalWrite(LATCH_PIN, LOW);
    shiftOut(DATA_PIN, CLOCK_PIN, MSBFIRST, chip2);   // far chip
    shiftOut(DATA_PIN, CLOCK_PIN, MSBFIRST, chip1);   // near chip
    digitalWrite(LATCH_PIN, HIGH);
}

void setup() {
    pinMode(DATA_PIN,  OUTPUT);
    pinMode(CLOCK_PIN, OUTPUT);
    pinMode(LATCH_PIN, OUTPUT);
    write595x2(0x00, 0x00);
}

void loop() {
    // Annunciator panel: overhead lights chip1, engine chip2
    write595x2(0b00000011, 0b11111100);
    delay(1000);
    write595x2(0b00000000, 0b00000000);
    delay(1000);
}
Cockpit Annunciator Pattern

Map each output bit to a named indicator. Keep a uint8_t state variable and toggle individual bits when the Bridge delivers simulator dataref updates.

// Cockpit annunciator driven by simulator datarefs
// Call this from your event handler whenever simulator state changes

const int DATA_PIN  = 23;
const int CLOCK_PIN = 22;
const int LATCH_PIN = 21;

// Annunciator bit map (one bit per indicator LED)
#define LED_GND_SPOILER   0
#define LED_DOOR_FWD      1
#define LED_DOOR_AFT      2
#define LED_SEATBELT      3
#define LED_NO_SMOKING    4
#define LED_MASTER_CAUTION 5
#define LED_FIRE_WARN     6
#define LED_OVHT_DET      7

uint8_t annunciatorState = 0;

void setIndicator(uint8_t led, bool on) {
    if (on) {
        annunciatorState |=  (1 << led);
    } else {
        annunciatorState &= ~(1 << led);
    }
    // Latch new state immediately
    digitalWrite(LATCH_PIN, LOW);
    shiftOut(DATA_PIN, CLOCK_PIN, MSBFIRST, annunciatorState);
    digitalWrite(LATCH_PIN, HIGH);
}

Call setIndicator() from your WebSocket message handler whenever the X-Plane dataref changes — no polling required.

Cockpit Applications
ApplicationChips neededNotes
Overhead annunciator panel (8 indicators)1 × 74HC595One bit per light, driven directly with current-limiting resistors
Main instrument panel (24–32 indicator LEDs)3–4 × 74HC595Daisy-chain on same 3 GPIO pins, one ESP32 per panel section
Seven-segment digit (common-cathode)1 × 74HC595Bits 0–6 → segments a–g, bit 7 → decimal point
High-current backlights / incandescent simulation1 × 74HC595 + ULN2003595 drives ULN2003 inputs; outputs switch up to 500 mA loads
MCP23017 alternative (I²C vs SPI)Use 595 when you need speed; MCP23017 when I²C bus space is tight