The other day I decided to level up my understanding of how these popular "HUB75" style RGB LED matrices work. So I sat down with the intent to write the most basic bit bang example I could come up with. Here I share the resulting code, written in CircuitPython.
There are plenty of Google discoverable write ups on these that go into lots more detail, discussing the shift registers, scan lines, etc. It's worth reading a few of those first. We found these to be useful:
- Everything You Didn't Want to Know About RGB Matrix Panels - A good write up with some nice animations by Nick Poole at Sparkfun who needed to also understand how these work to modify an Arduino library for an unsupported panel type.
- RGB LED Panel Driver Tutorial - read the "Theory of Operation" section.
- Driving a 64*64 RGB LED panel with an FPGA - read everything up to "Driving the matrix".
There's also a brief description in the Adafruit Learn System here:
These discussions can quickly get a bit complex, since there are a lot of pieces that work together in concert. However, there are a lot of things that are redundant. For example, how each red/green/blue LED is controlled is the same. Also, the matrices are typically split into upper and lower halves, but each is updated in the same way - R1/G1/B1 for the upper half, R2/G2/B2 for the lower half. So to keep things as simple as possible, this example does the following:
- Only use a single color
- Only work with the upper half of the matrix
This greatly reduces the total number of pins needed. It also really helps focus on the basic way in which data is moved into the matrices. By using just the upper half, the R2, G2, B2 pins can be left unused. And by focusing on just one color, we can use any one of R1, G1, or B1.
A Feather RP2040 was used with the following pin connections (Feather to Matrix):
- 13 to R1 (or G1 or B1 - your choice!)
- SCK to CLK
- MOSI to LAT
- MISO to OE
- A0 to A
- A1 to B
- A2 to C
- A3 to D
- GND to GND
There's nothing special about these pin choices - any digital pins could be used.
The matrix used was an Adafruit 32x32 4mm pitch panel (PID 607).
And here's the CircuitPython code example. It's very crude. It's not even PEP8'd.
import board
import digitalio
#--| PIN DEFINITIONS |-----------------------------------------------
R1 = digitalio.DigitalInOut(board.D13); R1.switch_to_output(False)
AA = digitalio.DigitalInOut(board.A0); AA.switch_to_output(False)
AB = digitalio.DigitalInOut(board.A1); AB.switch_to_output(False)
AC = digitalio.DigitalInOut(board.A2); AC.switch_to_output(False)
AD = digitalio.DigitalInOut(board.A3); AD.switch_to_output(False)
CLK = digitalio.DigitalInOut(board.SCK); CLK.switch_to_output(False)
LAT = digitalio.DigitalInOut(board.MOSI); LAT.switch_to_output(False)
OE = digitalio.DigitalInOut(board.MISO); OE.switch_to_output(False)
#--------------------------------------------------------------------
# define a bitmap
# 16 rows of 32 columns
# 0=OFF, 1=ON
bmp = (
(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0),
(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0),
(0,0,0,0,1,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0),
(0,0,0,1,0,0,1,0,1,0,0,1,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,1,0,0,0,0),
(0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0),
(0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0),
(0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0,1,1,0,0,0,1,1,0,1,0,0),
(0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,1,1,0,0,0,1,1,0,0,1,0),
(0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0),
(0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,1,0,0,1,0),
(0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,1,0,0,1,0,0),
(0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,1,1,1,0,0,0,1,0,0),
(0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0),
(0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,1,0,0,0,0),
(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0),
(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0),
)
COLS = len(bmp[0])
ROWS = len(bmp)
print(f"ROWS={ROWS}, COLS={COLS}")
while True:
for row in range(ROWS):
# clock row data out
for col in range(COLS):
# set color bit
R1.value = bmp[row][col]
# clock it out
CLK.value = True
CLK.value = False
# disable output
OE.value = True
# set row address bits
AA.value = (row >> 0) & 0x01
AB.value = (row >> 1) & 0x01
AC.value = (row >> 2) & 0x01
AD.value = (row >> 3) & 0x01
# latch it out
LAT.value = True
LAT.value = False
# enable output
OE.value = False
Running that code should produce the output shown in the banner image at the top of this Playground Note. There's a good chance the output will be glitchy. Nothing special is being done to control any of the timings. And bit banging this from CircuitPython is much slower than a real matrix driver. But this bare bones example shows the basic way in which the RGB matrices are controlled.
Try moving the matrix connection from R1 to G1 or B1 and running the code again. It should show the same thing, just in that different color. Could even connect to one of R2, G2, or B2 and the result should show up on the lower half of the matrix.
What About RGB Colors?
That would require more code and connecting to more pins. However, it would be the exact same logic used for each color channel as shown above for one channel. For example, if you connected to R1, G1, and B1 and then added code to clock out R1=1, G1=0, B1=1, that pixel would end up being red + blue = magenta.
What About Controlling The Full Matrix?
Again, just more code and wiring connections. Upper half is controlled by R1, G1, and B1. Lower half is controlled by R2, G2, and B2. The basic code logic would be the same though.
What About N-Bit Depth Colors?
That's where things get interesting and complex. It basically requires some form of PWM control of each and every LED. Doing that efficiently and quickly across the entire LED matrix is non-trivial. But regardless - all that work is done by the host controller. The RGB LED matrix itself is a very simple device.
This page (Super Basic RGB Matrix Driver Example) was last updated on August 14, 2024.