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. It is not really an invention. Every ingredient was already on the first page of the LED's datasheet; the contribution was noticing what they added up to.

The idea underneath it

  • An LED is a light source and a 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.
  • The cost is that the display is never really finished. A latched row/column matrix can be set and forgotten; 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. On a board whose LEDs are the main application, that is justified. On a board with a job to do, it usually is not.
  • 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 cheaper in every currency except BOM lines.
  • 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.

💡
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 →