Shift Registers
Expanding ESP32 pins with shift registers: 74HC595 chains drive large annunciator panels and LED arrays, 74HC165 chains read banks of buttons and switches, each 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 12 dataref changes — no polling required.
The 74HC165 is the 595's mirror image: a Parallel-In Serial-Out (PISO) register. A pulse on PL takes a snapshot of all 8 inputs at once, then each clock pulse shifts one bit out of Q7 into the ESP32. Chain chips by connecting the next chip's Q7 to this chip's DS (pin 10): 16, 24 or 64 inputs still use the same three GPIO lines.
| 74HC165 pin | Connect to | Colour |
|---|---|---|
| Q7 (9) | GPIO 19 — the chain's data line into the ESP32 | Orange |
| CP (2) | GPIO 18 — clock, shared by every chip in the chain | Yellow |
| PL (1) | GPIO 5 — parallel load (latch), shared by every chip | Green |
| CE (15) | GND (clock always enabled) | — |
| DS (10) | Q7 of the next chip in the chain, or GND on the last chip | — |
| D0–D7 (11–14, 3–6) | 10 kΩ to VCC, and the button or switch to GND | — |
| VCC (16) / GND (8) | 3.3 V / GND, with a 100 nF capacitor across them at the chip | Red / Black |
// Read 8 buttons through one 74HC165 — LOW = pressed (pull-up + button to GND)
const int DATA_PIN = 19; // Q7 — serial data out of the chip
const int CLOCK_PIN = 18; // CP — clock
const int LOAD_PIN = 5; // PL — parallel load, active LOW
void setup() {
Serial.begin(115200);
pinMode(DATA_PIN, INPUT);
pinMode(CLOCK_PIN, OUTPUT);
pinMode(LOAD_PIN, OUTPUT);
digitalWrite(LOAD_PIN, HIGH);
}
// Not shiftIn(): it pulses the clock before it reads, which shifts D7 away unread.
// Q7 already shows D7 after the load, so read first, then clock in the next bit.
uint8_t readInputs() {
digitalWrite(LOAD_PIN, LOW); // snapshot D0–D7 into the register
delayMicroseconds(5);
digitalWrite(LOAD_PIN, HIGH); // back to shift mode
uint8_t value = 0;
for (int bit = 7; bit >= 0; bit--) { // D7 arrives first
value |= digitalRead(DATA_PIN) << bit;
digitalWrite(CLOCK_PIN, HIGH);
delayMicroseconds(1);
digitalWrite(CLOCK_PIN, LOW);
}
return value;
}
uint8_t last = 0xFF;
void loop() {
uint8_t now = readInputs();
uint8_t changed = now ^ last;
for (int bit = 0; bit < 8; bit++) {
if (changed & (1 << bit)) {
Serial.printf("D%d %s\n", bit, (now & (1 << bit)) ? "released" : "pressed");
}
}
last = now;
delay(5);
}On a CockpitConnect controller you don't write this loop. Add a 74HC165 chain to the controller in the Flow Builder with its data, clock and latch pins and the number of chips, then put Buttons, Switches or Rotary Switch contacts on its inputs. The controller scans the chain, debounces every input and reports changes like a native pin. The 595 works the same way for outputs: LEDs on its Q pins are driven from the Flow like any other LED.
| 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 |
| Switch panel (16 toggles and buttons) | 2 × 74HC165 | One 10 kΩ pull-up per input, chained on the same 3 GPIO pins |
| MCP23017 alternative (I²C vs SPI) | — | Use 595 when you need speed; MCP23017 when I²C bus space is tight |