The actual first time I worked with such a panel was on the 36C3 in the end of 2019, when a friend of mine tried to get them running with an esp32. I wanted to also do some fun things with such a panel, so I got myself a 32x64 P.3 RGB LED panel.
January 22nd, I got this panel in the mail, and having no other controller on my hand back then I just wanted to get it working with the NANO. I've seen many libraries and projects with the UNO and a 16x32 panel, but everything this size was always run by a teensy or a pi. I looked for Libraries, but couldn't find any.
Sparkfun even states that:
At least an Arduino Uno (or comparable ATmega328-based Arduino). These panels really stretch the Arduino to its limits. If you have an Arduino Mega 2560 you may want to whip that out instead. Any size higher than a 32x32 panel requires an Arduino Mega 2560 or faster microcontroller.
So I went ahead and made my own. The first problem was the pinout of the panel, but that was quickly solved by some searching on the internet.
I just connected the Panel and the Arduino with a wiring table I found in some UNO library, and it seemed to also work.
- A A0
- B A1
- C A2
- D A4
- R1 2
- R2 5
- B1 4
- B2 7
- G1 3
- G2 6
- LAT A3
- CLK 8
- OE 9
- GND GND
For Power just use a powerful enough 5v supply, these panels draw about 30W. I use a PC power supply.
After connecting everything, which was really easy with just jumper wires, it was about making the LEDs go all blinky blinky. This part was not that easy, especially given the general lack of information on how exactly the protocol works, but in hindsight it is really easy.
The Coding begins...I found some comment in a library which kind of explained the protocol, but some key parts like in which order to set the pins wasn't given. So here it is:
At first, you select a line to draw to via the 4 line select pins. They are in binary, ordered A B C D where a High means 1 and a low means 0. So for example for row 11 we would have A=1 B=1 C=0 D=1 (1+2+0+8 = 11). But now you might say: How is it possible to drive 32 rows with just 4 pins, which give only 2^4=16 options??
The answer lies in the fact, that there are actually two RED channel pins, two GREEN channel pins, and two BLUE channel pins, one for the upper half of the display, and one for the lower half.
Afterwards you have to set your RGB pins either on or off, then make the CLK pin go HIGH and LOW, and after a row full of led data you make the latch go HIGH LOW. You should also turn the OE(Output Enable) HIGH at some point in the line, it doesn't really matter where, I prefer the beginning.
I tried to implement a frame buffer many times, and failed even more times:
I've since changed all the digitalWrite()
calls to direct pin register manipulation, which is worse to read and write, but way, way faster. The logic behind the driver stays the same, so i wont change any of the explanations. Just be wary that the actual code may/will differ.
Here i will try to explain the most basic inner workings of the code on the color-example i provide in the repo.
The following code is from the .ino sketch file.
#include <Panel.h>
//which cable goes where
#define RA 14 //register selector a
#define RB 15 //register selector b
#define RC 16 //register selector c
#define RD 18 //register selector d
#define RF 2 //red first byte
#define RS 5 //red second byte
#define BF 4 //blue first byte
#define BS 7 //blue second byte
#define GF 3 //green first byte
#define GS 6 //green second byte
#define LAT 17 //data latch
#define CLK 8 //clock signal
#define OE 9 //output enable
This part just includes the Header for the library, and tells you how to connect the nano and the matrix.
We create an instance of the panel class with the following line
Panel panel(32,64, false);
where the first value is our height, second our width, and the third specifies whether to use a frame buffer or not. In this case we do not need a frame buffer, so we pass false as an argument.
void setup() {}
void loop() {
//this example iterates through all available colors in the panel
for (long i = 0; i < 26000; i++) {
panel.fillScreenColor(i/1000);//changes after some time
}
/*
|||ALL COLORS|||
RED,
GREEN,
BLUE,
WHITE,
BLACK,
PURPLE,
YELLOW,
CYAN,
LIGHTRED,
LIGHTGREEN,
LIGHTBLUE,
LIGHTWHITE,
LIGHTCYAN,
DARKYELLOW,
LIGHTPURPLE,
LIGHTYELLOW,
TURQUOISE,
PINK,
DARKPURPLE,
BRIGHTGREEN,
BRIGHTCYAN,
MEDIUMGREEN,
DEEPPURPLE,
OCEANBLUE,
FLESH,
LIGHTPINK,
*/
}
The setup is empty, we don't have to do anything there.
In the main loop we iterate for 26000 times, and set the color of the whole panel every 1000 iterations. There are 26 colors in total, and when we divide by 1000, the rest gets chopped off. so for example if i
is 13487, then the color we will see is number 13. This way we get a new color after some time.
using the library is easy, but how does it work inside the library itself?
#include "Panel.h"
#define RA 14 //register selector a
#define RB 15 //register selector b
#define RC 16 //register selector c
#define RD 18 //register selector d
#define RF 2 //red first byte
#define RS 5 //red second byte
#define BF 4 //blue first byte
#define BS 7 //blue second byte
#define GF 3 //green first byte
#define GS 6 //green second byte
#define LAT 17 //data latch
#define CLK 8 //clock signal
#define OE 9 //output enable
uint8_t rows;
uint8_t cols;
uint16_t bsize;
//color values
uint8_t rc1;
uint8_t gc1;
uint8_t bc1;
uint8_t rc2;
uint8_t gc2;
uint8_t bc2;
//register for string pin status
bool r1;
bool g1;
bool b1;
bool r2;
bool g2;
bool b2;
Again, we include the Header which just defines the functions and tells the compiler how much memory we will need. After the header, we define a few commands, which will be replaced by the corresponding pin number at compilation time, this just makes it easier to read.
the last few lines create variables, in this case properties such as height and width, together with some bools and the color values for later
I will cut everything we don't need.
Panel::Panel(uint8_t height,uint8_t width, bool usebuffer){
rows = height;
cols = width;
/*
Pin mapping:
A A0,
B A1,
C A2,
D A4,
R1 2,
R2 5,
B1 4,
B2 7,
G1 3,
G2 6,
LAT A3,
CLK 8,
OE 9,
GND GND
*/
pinMode(RA, OUTPUT);
pinMode(RB, OUTPUT);
pinMode(RC, OUTPUT);
pinMode(RD, OUTPUT);
pinMode(CLK, OUTPUT);
pinMode(RF, OUTPUT);
pinMode(RS, OUTPUT);
pinMode(GF, OUTPUT);
pinMode(GS, OUTPUT);
pinMode(BF, OUTPUT);
pinMode(BS, OUTPUT);
pinMode(LAT, OUTPUT);
pinMode(OE, OUTPUT);
if (usebuffer) {}
}
In the class constructor we set the height and width accordingly, together with all the pinModes. We don't need the buffer, that's why i cut the code from the buffer creation. Now our nano knows that all pins are output, and we can write to them.
void Panel::fillScreenColor(uint8_t c){//fills the screen with the set color
//switches all the colrs and sets the values depending on colors
switch (c)
{
case RED:
for (uint8_t r = 0; r < rows / 2; r++) {
//switch through all rows
selectLine(r);
rc1 = 1;
gc1 = 0;
bc1 = 0;
sendWholeRow(rc1,gc1,bc1,rc1,gc1,bc1);
}
break;
}
}
next we check for the color to display, I only put the case for red here for sake of readability. But it is basically the same for all colors
We iterate through all rows, and for each row we send two lines of pixel color data to be displayed. This is due to the fact, that we have 32 rows in total, but only 16 rows to address.
selectLine() turns the address select pins on according to the binary representation of the wanted row
void Panel::selectLine(uint8_t c) {//selects one of the 16 lines, 0 based
switch (c) {
case B0000:
digitalWrite(RA, HIGH);
digitalWrite(RB, HIGH);
digitalWrite(RC, HIGH);
digitalWrite(RD, HIGH);
digitalWrite(RA, LOW);
digitalWrite(RB, LOW);
digitalWrite(RC, LOW);
digitalWrite(RD, LOW);
break;
case B0001:
digitalWrite(RA, HIGH);
break;
case B0010:
digitalWrite(RB, HIGH);
break;
case B0011:
digitalWrite(RA, HIGH);
digitalWrite(RB, HIGH);
break;
case B0100:
digitalWrite(RC, HIGH);
break;
case B0101:
digitalWrite(RA, HIGH);
digitalWrite(RC, HIGH);
break;
case B0110:
digitalWrite(RB, HIGH);
digitalWrite(RC, HIGH);
break;
case B0111:
digitalWrite(RA, HIGH);
digitalWrite(RB, HIGH);
digitalWrite(RC, HIGH);
break;
case B1000:
digitalWrite(RD, HIGH);
break;
case B1001:
digitalWrite(RA, HIGH);
digitalWrite(RD, HIGH);
break;
case B1010:
digitalWrite(RB, HIGH);
digitalWrite(RD, HIGH);
break;
case B1011:
digitalWrite(RA, HIGH);
digitalWrite(RB, HIGH);
digitalWrite(RD, HIGH);
break;
case B1100:
digitalWrite(RC, HIGH);
digitalWrite(RD, HIGH);
break;
case B1101:
digitalWrite(RA, HIGH);
digitalWrite(RC, HIGH);
digitalWrite(RD, HIGH);
break;
case B1110:
digitalWrite(RB, HIGH);
digitalWrite(RC, HIGH);
digitalWrite(RD, HIGH);
break;
case B1111:
digitalWrite(RA, HIGH);
digitalWrite(RB, HIGH);
digitalWrite(RC, HIGH);
digitalWrite(RD, HIGH);
break;
}
}
The next function to be called is sendWholeRow()
void Panel::sendWholeRow(uint8_t ru, uint8_t gu, uint8_t bu, uint8_t rl, uint8_t gl, uint8_t bl) { //sends two rows of pixels to display | first upper half values, the lower half
if (ru > 0 && r1 == false) //turns upper half red
{
digitalWrite(RF, HIGH);
r1 = true;
}
else if (ru == 0 && r1 == true) {
digitalWrite(RF, LOW);
r1 = false;
}
if (gu > 0 && g1 == false) //turns upper half green
{
digitalWrite(GF, HIGH);
g1 = true;
}
else if (gu == 0 && g1 == true) {
digitalWrite(GF, LOW);
g1 = false;
}
if (bu > 0 && b1 == false) //turns upper half blue
{
digitalWrite(BF, HIGH);
b1 = true;
}
else if (bu == 0 && b1 == true) {
digitalWrite(BF, LOW);
b1 = false;
}
if (rl > 0 && r2 == false) //turns lower half red
{
digitalWrite(RS, HIGH);
r2 = true;
}
else if (rl == 0 && r2 == true) {
digitalWrite(RS, LOW);
r2 = false;
}
if (gl > 0 && g2 == false) //turns lower half green
{
digitalWrite(GS, HIGH);
g2 = true;
}
else if (gl == 0 && g2 == true) {
digitalWrite(GS, LOW);
g2 = false;
}
if (bl > 0 && b2 == false) //turns lower half blue
{
digitalWrite(BS, HIGH);
b2 = true;
}
else if (bl == 0 && b2 == true) {
digitalWrite(BS, LOW);
b2 = false;
}
for (uint8_t i = 0; i < cols; i++) {
clock();
}
latch();
}
Depending on whether the rows should be red, the red pin is set to HIGH or LOW. the same is the case for the other row and the other colors. After the color pins have been set, we clock in the data for the whole row, so 64 times.
void Panel::clock() {//clock function for data entry
digitalWrite(CLK, HIGH);
digitalWrite(CLK, LOW);
}
For clocking we just turn the CLK pin HIGH and then LOW again.
And as a last part for one row, we latch data, which means that the LEDs are turned on with the new data sent to them.
void Panel::latch() {//this function cleans up and latches the data, so diplays it
//latch output
digitalWrite(LAT, HIGH);
//delatch
digitalWrite(LAT, LOW);
//enable output
digitalWrite(OE, HIGH);
emptyLine();
}
We make the LAT pin HIGH and then LOW again in order to latch the data, and then we enable the output with digitalWrite(OE, HIGH);
.
The emptyLine() function sets all address pins to LOW again in order to clear everything. It also turns the output off, so the panel can receive new data.
void Panel::emptyLine() {//outputs an empty row on the selected line
//output off
digitalWrite(OE, LOW);
//address zero
digitalWrite(RA, LOW);
digitalWrite(RB, LOW);
digitalWrite(RC, LOW);
digitalWrite(RD, LOW);
}
This is how it works in its easiest form. The complete code is on the github repo.
The library formsAfter a lot of trial and error, probably 250+ hours of work, I finally reached a point at which I can dump this thing onto the internet.
The library comes with a ReadMe on GitHub, and many examples with commented code. I also made a small program to convert images to a format which allows them to be displayed. It comes precompiled as an.exe and with a makefile.
Feel free to add any valuable functions / ideas / suggestions on Github or here.
Right now it can change its whole color to one of 26 colors, display an image with up to 8 colors, draw shapes(rectangles, lines and circles) and output text either via a sketch or via Serial input.
Links
Comments