hi all!
tl;dr I had some fun with PIO and LEDs and thought it was cool enough to share so here it is: https://github.com/nmattia/pio-charlie
I've been playing with the rp2040 for a while and used it with basic compiled programs or simple MicroPython scripts for the longest time and recently decided to dig a bit deeper. I never quite understood PIO (it seemed very scary) though I think I did use it indirectly with some Pimoroni boards for instance.
It really felt magical (borderline occult) at first so I started trying to drive one LED with it, then two, then fell down a pretty big rabbit hole. It seemed like it would be possible to drive an absurdly large number of LEDs without any intermediary drivers or silicon via charlieplexing, so I started diligently reading the datasheet and played around with some MicroPython.
The first success came from using two state machines, where one would read the actual pixel data from memory (DMA), and the other one the pin positions, for instance with 4 pins: LED1 = [1, 0, X, X], LED2 = [ 0, 1, X, X ], etc. The two state machine synced with IRQ instructions and it worked ok, but it felt almost too simple and I started wondering if I could do it with a single state machine and that's where it all got a bit crazy... but it worked eventually!
The idea is to generate all charlieplexing pin positions via PIO, i.e. [0, 1, X, X], [1, 0, X, X], etc and for each position use PINS and PINDIRS to drive the correct pins either high, low, or X, while at the same time still streaming the data via DMA. The final solution fits in 31 PIO instructions, and requires one state machine + 2 DMA channels per "display". If the SMs are in the same PIO bank of course they can share the same memory, so on the rp2040 I think (?) you can have up to 6 displays since you're limited by the 12 DMA channels (I've used one channel for data and one to restart the data channel, though I guess the later could be replaced with an interrupt with some CPU tradeoff). By changing the data channel's read address it's also straightforward to implement something like double buffering. I've ordering some PCBs as proof of concepts and am happy to report it actually works!
![Image]()
I've tried to explain the PIO hacks here but not gonna lie it's pretty hairy and it felt like a long series of code golfing sessions: https://github.com/nmattia/pio-charlie/ ... ain.py#L30
In practice I've actually been limited by the number of available (contiguous) pins on the Pico which I didn't think was a problem I'd ever have... I have two rp2350Bs lying around with all 48 pins broken out and I can't wait to try them out![😬]()
anyway hope this isn't too off topic but it felt like a good showcase of how flexible PIO can actually be!
tl;dr I had some fun with PIO and LEDs and thought it was cool enough to share so here it is: https://github.com/nmattia/pio-charlie
I've been playing with the rp2040 for a while and used it with basic compiled programs or simple MicroPython scripts for the longest time and recently decided to dig a bit deeper. I never quite understood PIO (it seemed very scary) though I think I did use it indirectly with some Pimoroni boards for instance.
It really felt magical (borderline occult) at first so I started trying to drive one LED with it, then two, then fell down a pretty big rabbit hole. It seemed like it would be possible to drive an absurdly large number of LEDs without any intermediary drivers or silicon via charlieplexing, so I started diligently reading the datasheet and played around with some MicroPython.
The first success came from using two state machines, where one would read the actual pixel data from memory (DMA), and the other one the pin positions, for instance with 4 pins: LED1 = [1, 0, X, X], LED2 = [ 0, 1, X, X ], etc. The two state machine synced with IRQ instructions and it worked ok, but it felt almost too simple and I started wondering if I could do it with a single state machine and that's where it all got a bit crazy... but it worked eventually!
The idea is to generate all charlieplexing pin positions via PIO, i.e. [0, 1, X, X], [1, 0, X, X], etc and for each position use PINS and PINDIRS to drive the correct pins either high, low, or X, while at the same time still streaming the data via DMA. The final solution fits in 31 PIO instructions, and requires one state machine + 2 DMA channels per "display". If the SMs are in the same PIO bank of course they can share the same memory, so on the rp2040 I think (?) you can have up to 6 displays since you're limited by the 12 DMA channels (I've used one channel for data and one to restart the data channel, though I guess the later could be replaced with an interrupt with some CPU tradeoff). By changing the data channel's read address it's also straightforward to implement something like double buffering. I've ordering some PCBs as proof of concepts and am happy to report it actually works!

I've tried to explain the PIO hacks here but not gonna lie it's pretty hairy and it felt like a long series of code golfing sessions: https://github.com/nmattia/pio-charlie/ ... ain.py#L30
In practice I've actually been limited by the number of available (contiguous) pins on the Pico which I didn't think was a problem I'd ever have... I have two rp2350Bs lying around with all 48 pins broken out and I can't wait to try them out
anyway hope this isn't too off topic but it felt like a good showcase of how flexible PIO can actually be!
Statistics: Posted by nmattia — Tue Feb 24, 2026 10:22 pm