How Charlieplexing works: an interactive demo

Eleven pins, 104 LEDs, and a scan you can slow down until the trick falls apart. An interactive walkthrough of the Arduino UNO Q's charlieplexed matrix, built from the board's own devicetree.

Named after Charlie Allen, who wrote it up in a Maxim application note in the early 2000s. It was a pun on multiplexing, not a formal name. You will also see it called complementary drive or tri-state multiplexing.

The idea underneath it

  • An LED is a light source and a binary switch in the same package. Charlieplexing puts the second job to work: the diode does the address decoding, so the panel needs no decoder, no shift register and no driver IC. Just GPIOs.
  • That is also why the trick does not generalise. Buzzers, relays, heaters, motors – none of them are polarity-sensitive with a conduction threshold, so there is nothing to decode with. Button matrices have the same problem from the other side, which is why you end up adding a diode per key for computer keyboards.
  • The cost of the approach is that the display is never really finished. A latched row/column matrix can be set and forgotten but a charlieplexed one must be redrawn continuously, forever, in the background, or it goes dark.
0:00
/0:01

Video 1. A demo of Charlieplexing on the Arduino Uno Q. This demo is a companion to that blog article.

When it earns its place

  • Pins are the scarcest resource on a small microcontroller, and a matrix like this uses a lot of them – 104 if you drove each LED with its own GPIO. On a board whose LEDs are the main application, that is justified. On a board like the Uno Q with a set of jobs to do, it usually is an appropriate use of GPIOs.
  • The approach is suited to onboard status displays, badges and dev-board decoration – small LEDs, modest brightness, an MCU with spare timer and interrupt budget.
  • Poorly suited to anything large, bright, or CPU-hungry. Past a certain size a dedicated driver chip is more appropriate.
  • Single LED type only, in practice. Mixing colours on the same pins breaks the assumption the whole scheme rests on.

Why it is worth a demo

This is one of the few places in embedded work where firmware timing is directly visible to the naked eye. Flicker, banding, uneven greys and dim rows are all scheduling faults you can see, with no logic analyser required. This demo below shows some interesting electronics principles and even helps you appreciate the value of the persistence of vision human limitation.

💡
Start with the scan The walkthrough below builds up from two pins to all 104. If you only open one tab, open the scan . Then drag the time scale down and watch persistence of vision stop doing you any favours.
Charlieplexing the Arduino UNO Q 13×8 matrix — derekmolloy.ie
charlieplex-led-matrix · stm32u585 gpiof 0–10

104 LEDs from 11 pins

13 × 8 · 3-bit grey · 100 Hz refresh · vanilla Zephyr on the Arduino UNO Q

Charlieplexing exploits one thing an ordinary matrix throws away: a GPIO has three states, not two. Driven high, driven low, or high-impedance. With n pins you can address every ordered pair of pins, and each pair carries two LEDs wired back to back, so n(n−1) LEDs are individually reachable. Eleven pins gives 110; this board populates 104 of them.

Every number in the binding below is a design consequence of that. Walk through the tabs in order, or jump to the scan for the animation.

led_matrix: charlieplex-led-matrix {
  compatible = "charlieplex-led-matrix";
  gpios = <&gpiof 0 0>, ... <&gpiof 10 0>;   // 11 pins → 110 addressable LEDs
  counter = <&counter_matrix>;                // hardware timebase, not a thread
  refresh-frequency = <100>;                  // 10 ms per whole frame
  width = <13>;  height = <8>;                // 104 pixels, row-major
  grayscale-bits = <3>;                       // 8 levels via binary code modulation
  pixel-pairs = <0x0001 0x0100 ...>;         // pixel n → 0x<source><sink>
};
Derived timing · frame 10 ms → 11 scan phases of 909.09 µs → 7 grey ticks of 129.87 µs
  • pixel-pairs is the whole wiring map in one property: entry n is framebuffer pixel n, high byte = the pin that sources current, low byte = the pin that sinks it.
  • counter matters because the shortest interval the driver must hit is 130 µs. That is a timer compare channel's job, not a scheduler's.
  • 0x0001 and 0x0100 are the same two wires with the polarity swapped — the two anti-parallel LEDs on one pin pair.

Convention check: this page reads the high byte as the source. If your driver reverses it, every LED simply trades places with its anti-parallel twin and the image mirrors within each pair — the mechanism is unchanged.

Every value on this page is read from the board's own devicetree. derekmolloy.ie →