Why 74HC595 Seven-Segment Displays Flicker and How QUAD7SHIFT Handles the Latch Boundary
Contents
When building thermometers or counters with an Arduino, the go-to display is a seven-segment LED—seven bar-shaped LEDs arranged to draw digits 0 through 9, the same kind found in alarm clocks and microwave timers.
The Arduino Uno only has 14 digital output pins.
Driving a 4-digit seven-segment display directly needs 7 pins for segments plus 4 for digit selection, and that’s before the decimal point or any control lines. You run out fast.
The standard fix is the 74HC595 shift register IC: you feed data in serially on a single line, and the IC fans it out to 8 output pins.
Daisy-chain two of them for 16 outputs, and you can drive all 4 digits. This is the classic Arduino setup.
Tutorials covering the first digit are everywhere, but plenty of people hit the next wall—a faint, persistent flicker.
A post on DEV Community, Why Most 74HC595 Display Drivers Flicker, digs into exactly this.
The interesting part is that the QUAD7SHIFT author says the flicker-free behavior wasn’t a design goal.
By implementing around the latch’s actual role in the 74HC595, the conditions for ghosting simply never arose.
The Problem Isn’t Two Transfers—It’s the State in Between
The 74HC595 has, roughly speaking, two registers.
| Component | Role |
|---|---|
| Shift register | Accepts serial input one bit per clock |
| Storage register | Reflects to output pins on latch rising edge |
The Nexperia 74HC595 datasheet separates the shift register’s input clock from the storage register’s latch clock.
This is the IC’s key feature: data being shifted in doesn’t appear on the output pins until you explicitly latch it.
A typical seven-segment setup uses one 74HC595 for segment lines and a second for digit-select lines.
You shift in “next segment pattern” and “next digit select,” then raise the latch.
Sounds correct, but when the software calls shiftOut() twice, interrupts or jitter can slip in between those two calls.
Whether that intermediate state leaks to the output pins depends on how the latch and digit selection are managed.
At minimum, treating segment update and digit-select update as separate operations widens the window for boundary mistakes.
Typical implementation
LATCH LOW -------------------------------- HIGH
DATA [new segments] [new digit select]
↑
An interrupt here can put
new segments on the old digit
Strictly speaking, the storage register hasn’t updated yet, so the output doesn’t change instantly.
But in the display driver as a whole, any slip in the ordering of digit selection, blanking, next-digit update, and latch shows up as ghosting or brightness unevenness.
It’s the kind of failure most visible through a camera or under fluorescent lighting.
QUAD7SHIFT Treats 16 Bits as a Single Display State
QUAD7SHIFT is a 4-digit seven-segment display library targeting Arduino Uno/Nano and ATtiny85.
Its GitHub README lists support for 74HC595-based 4-digit displays, common anode and common cathode, numeric and string output, and configurable refresh rate.
The core idea from the original article is not treating segments and digit select as separate display operations.
The 16 bits going to two 74HC595s are assembled as a single display state, shifted out continuously with the latch held low, and latched exactly once at the end.
QUAD7SHIFT approach
LATCH LOW -------------------------- HIGH
DATA [segments | digit select]
↑
All 16 bits are in place
before output reflects
On AVR, it uses hardware SPI for 16-bit transfers. On ATtiny85, it sends two bytes consecutively via USI.
Different mechanisms, same output boundary.
Not “sent one byte, sent another byte,” but “assembled one digit’s display state, then latched.”
The difference looks small but matters.
Even if the software makes two function calls, the output pins see a single update boundary, so intermediate states don’t leak to the display.
Multiplexed Display Needs More Than Correct Latching
When driving 4 digits of seven-segment LEDs, you’re almost always multiplexing—switching one digit on at a time fast enough that the human eye sees all digits lit simultaneously.
Three kinds of instability mix together here.
| Symptom | How it looks | Typical cause |
|---|---|---|
| Ghosting | Faint image of adjacent digits | Sloppy latch, digit-select, or blanking order |
| Brightness unevenness | Some digits dimmer than others | Unequal on-time per digit |
| Flicker | Entire display pulsing | Refresh rate dragged down by loop() processing time |
The QUAD7SHIFT article also mentions a design where display refresh runs in its own timed loop rather than depending on loop().
When sensor reads, serial output, or other work slows loop() down, the refresh rate drops with it—and the display flickers even if the latch is correct.
This is an easy trap with Arduino projects.
It looks fine when all the sketch does is output numbers.
Add a temperature sensor, button handling, serial logging, and network waits, and the display suddenly dims or the brightness wobbles per digit.
Check the Signal Boundary Before the Wiring
When you see this kind of flicker, the instinct is to check power supply capacity, resistor values, and LED variance.
Those can be factors, but with a daisy-chained 74HC595 pair, there’s a signal to check first.
flowchart TD
A["Display flickers"] --> B["Count RCLK/STCP pulses"]
B --> C["Exactly one per full digit data?"]
C -->|No| D["Restructure latch boundary"]
C -->|Yes| E["Check blanking between digit switches"]
E --> F["Check per-digit on-time"]
F --> G["Check heavy processing or ISRs in loop"]
If you have a logic analyzer, capture DATA, SHCP, STCP, and digit-select lines simultaneously.
At minimum, verify that STCP isn’t firing extra pulses on every segment or digit-select update.
With cascaded 74HC595s, latching before all bits are shifted in produces combinations of old digit-select and new segment data.
Another consideration is blanking—briefly turning all digits off before switching.
Even at high switching speeds, LED and transistor response times, parasitic capacitance, and software ordering can produce afterimages.
Accepting a slight brightness reduction and inserting a blanking interval can make the display more stable.
QUAD7SHIFT Doesn’t Cover Everything
QUAD7SHIFT targets Arduino Uno/Nano and ATtiny, and it’s registered in the Arduino Library Manager.
The Arduino Libraries page lists it under the Display category, GPL 3.0 license, AVR architecture.
So it’s not a universal driver for ESP32 or RP2040—it’s a focused implementation for 74HC595-based 4-digit displays on AVR.
When porting the same approach to a different microcontroller, the thing to carry over isn’t the library name but the update unit.
Assemble segments and digit select as a single state, don’t reflect to output until all bits are shifted in.
One latch per display state.
Separate loop() processing from display refresh timing, and add blanking before digit switches if needed.
The 74HC595 is a decades-old staple IC, so sample code is abundant.
But code that displays something and code with clean signal boundaries are different things.
The QUAD7SHIFT article reads less as a library introduction and more as an example of what happens when you abstract according to the datasheet—flicker resistance falls out naturally.
The 74HC595 in Software Terms
This article has been throwing around “shift register” and “latch” without much explanation, so here’s a software-developer translation of the key 74HC595 pins.
| 74HC595 Pin | Signal | Software analogy |
|---|---|---|
| SER (pin 14) | Serial data input | SPI MOSI. One bit at a time |
| SRCLK (pin 11) | Shift clock | SPI SCK. Captures one bit on rising edge |
| RCLK (pin 12) | Latch clock | DB COMMIT. Flushes internal buffer to output |
| OE (pin 13) | Output enable | Master switch. LOW = output active |
A shift register is a circuit that pushes data one cell over on each clock.
Think of it as a fixed-length-8 FIFO queue.
After 8 clock pulses, 8 bits are lined up inside.
The latch is the operation that reflects those lined-up bits to the output pins all at once.
It’s close to a database transaction: shifting is INSERT, latching is COMMIT.
Nothing changes on the pins until COMMIT.
Arduino’s shiftOut() doesn’t use hardware SPI.
It’s a function that toggles GPIO pins one by one in a for loop, manually driving SER and SRCLK.
Since it’s a C for loop, an interrupt in the middle halts the transfer.
This is why QUAD7SHIFT uses AVR’s hardware SPI—the SPI peripheral (a hardware block separate from the CPU) handles the transfer, so transmission continues even while the CPU services an interrupt.
ATtiny85’s USI (Universal Serial Interface) is a stripped-down serial communication block found in lower-end AVR chips.
It’s not as capable as full SPI, but the hardware assist makes it more stable than pure software bit-banging.
Does This Happen on Raspberry Pi and ESP32 Too?
The flicker is an IC-interface issue, so the conditions don’t change regardless of which microcontroller you connect.
How easily the flicker manifests, though, varies significantly by platform.
Raspberry Pi runs GPIO through Linux.
When you write shiftOut()-equivalent bit-bang code in Python or C, the kernel’s task scheduler can preempt the CPU at any point.
Arduino runs bare-metal (no OS), so nothing steals the CPU except interrupts.
Software bit-bang on a Raspberry Pi makes things worse, not better, compared to Arduino.
That said, the Raspberry Pi does have hardware SPI.
A 16-bit transfer via /dev/spidev goes through the kernel driver and the SPI peripheral, so the transfer proceeds even if the user process gets scheduled out.
You can achieve the same “shift all bits, then latch” pattern as QUAD7SHIFT.
ESP32 runs on FreeRTOS, and the WiFi and BLE stacks fire interrupts periodically in the background.
Using shiftOut() in the ESP32 Arduino framework means WiFi processing can interrupt the bit-bang mid-transfer.
ESP32 has two hardware SPI controllers, so switching to SPI transfer eliminates the flicker.
RP2040 (Raspberry Pi Pico) is a special case—it has PIO (Programmable I/O), dedicated state machines for GPIO operations.
PIO runs completely independently of the CPU, so even bit-bang-style code isn’t affected by CPU-side interrupts.
| Platform | Software bit-bang | Hardware transfer |
|---|---|---|
| Arduino Uno/Nano (AVR) | shiftOut() stable except for interrupts | QUAD7SHIFT uses SPI 16-bit bulk |
| ATtiny85 | QUAD7SHIFT supports via USI | No full SPI peripheral |
| Raspberry Pi | Preempted by Linux scheduler. Least stable | /dev/spidev 16-bit transfer is stable |
| ESP32 | Interrupted by WiFi | SPI peripheral is stable |
| RP2040 (Pico) | PIO is stable; regular bit-bang is not | Hardware SPI also available |
The dividing line is whether you can send all 16 bits to the 74HC595 without interruption.
With software bit-bang like shiftOut(), the CPU getting pulled away mid-transfer breaks the stream.
Hardware SPI, DMA, or RP2040’s PIO—any mechanism that completes the transfer independently of the CPU—eliminates the latch boundary problem structurally.
The 74HC595 is an IC from over 40 years ago, and seven-segment displays on Arduino are a beginner staple, yet a library that properly handles latch boundaries didn’t show up until recently. That’s a bit surprising.
I’d assumed mature technology meant mature solutions, but it seems the standard tutorials got copied so many times that the problems got copied right along with them.
The feeling of working within tight constraints—fitting 16 bits into one atomic transfer, decoupling refresh timing from loop()—is familiar from older software work.
Packing data to fit a packet size, juggling buffers in limited memory.
Knowing people in the embedded world are still doing the same thing today is oddly nostalgic.