Shift Registers
Expanding ESP32 output pins with 74HC595 shift registers to drive large annunciator panels and LED arrays from just three GPIO lines.
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.
| Property | 74HC595 |
|---|---|
| Type | Serial-In, Parallel-Out (SIPO) |
| Outputs | 8 per chip (Q0–Q7), unlimited via daisy chain |
| Control pins | 3 (DATA, CLOCK, LATCH) |
| Supply voltage | 2–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 name | Number | Function |
|---|---|---|
| DS | 14 | Serial data input |
| SH_CP | 11 | Shift register clock (rising edge shifts data in) |
| ST_CP | 12 | Storage register clock — latch pulse copies to outputs |
| OE | 13 | Output Enable, active LOW — tie to GND |
| SRCLR | 10 | Shift register clear, active LOW — tie to VCC |
| Q0–Q7 | 15, 1–7 | Parallel outputs |
| Q7S | 9 | Serial output — connect to DS of the next chip in chain |
| 74HC595 pin | Connect to | Colour |
|---|---|---|
| DS (14) | GPIO 23 | Orange |
| SH_CP (11) | GPIO 22 | Yellow |
| ST_CP (12) | GPIO 21 | Green |
| VCC (16) | 3.3 V | Red |
| GND (8) | GND | Black |
| OE (13) | GND (tie low — always enabled) | — |
| SRCLR (10) | VCC (tie high — never clear) | — |
| Q0–Q7 (15, 1–7) | 220–470 Ω → LED anode | — |
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);
}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);
} 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.
| Application | Chips needed | Notes |
|---|---|---|
| Overhead annunciator panel (8 indicators) | 1 × 74HC595 | One bit per light, driven directly with current-limiting resistors |
| Main instrument panel (24–32 indicator LEDs) | 3–4 × 74HC595 | Daisy-chain on same 3 GPIO pins, one ESP32 per panel section |
| Seven-segment digit (common-cathode) | 1 × 74HC595 | Bits 0–6 → segments a–g, bit 7 → decimal point |
| High-current backlights / incandescent simulation | 1 × 74HC595 + ULN2003 | 595 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 |