Large LED sign with inexpensive modules and arduino: Difference between revisions

From Fvettore-WIKI
No edit summary
 
(22 intermediate revisions by the same user not shown)
Line 1: Line 1:
[[File:LedPanel.png]]
See in action https://www.youtube.com/embed/qrFwV6VtSZg <br>
https://www.youtube.com/embed/qrFwV6VtSZg
<br>
[[File:LedPanel small.png|600px]]


I have been asked by my company to create a system for production monitoring.
===General Overview===
A very important component of this system will be a large LED panel showing production status.
Way back in 2014 I have been asked by my company to create a system for production monitoring.<br>
Unfortunately I discoverd such components (for example the excellent Alphabrite panels) are very expensive: the mean quotation for a 2 line-21 chars ethernet controllable display is around 3000 USD!.
A very important component of this system was a large LED panel showing production status.<br>
So I tought i could save several thousands euros of my budget with a little DIY.
Unfortunately I discoverd such components (for example the excellent Alphabrite panels) are very expensive: the mean quotation for a 2 line-21 chars ethernet controllable display is around 3000 USD!.<br>
I discovered there are a lot of "nearly standard" and nearly-inexpensive LED panels that can be cascaded in order to assemble a large display.
So I tought i could save several thousands euros of my budget with a little DIY.<br>
I focused my attention on a 32x16 with pitch (pixel distance) of 10mm designed for outdoor (extrabright).
[[File:LED Panel upsidedown.jpg|thumb|none|upright 2.0|A funny issue during first installation :)]]
This kind of product is avaliable from several sellers on the Hong-Kong market for about 9 USD.
 
When the project started hardware core was an Arduino MEGA board.<br>
Please note the system si still in production and evolved. My team went on with development an now the core is ESP32 based and the system has WIFI with full MQTT support.</br>
For the ''visual part'' i discovered there are a lot of "nearly standard" and nearly-inexpensive LED panels that can be cascaded in order to assemble a large display.<br>
I focused my attention on a 32x16 with pitch (pixel distance) of 10mm designed for outdoor (extrabright).<br>
This kind of product is avaliable from several sellers on the Hong-Kong market for about 9 USD.<br>
(Search for ''Outdoor P10 RED Led panel 16x32'')


[[File:Panelfront-300x224.jpg]]
[[File:Panelfront-300x224.jpg]]
[[File:Panelrear-300x224.jpg]]
[[File:Panelrear-300x224.jpg]]


I thougt I could cascade 4 of them in order to get a panel 960mm X 320mm (really wide...) that should be visible up to 30m.
I tought I could cascade 4 of them in order to get a panel 960mm X 320mm (really wide...) that should be visible up to 30m.<br>
Unfortunately this module is based on a undocumented protocol (hub08 - hub12). But since it is a very stupid device, with a little of reverse-engineering it is possible to understand how to drive every single pixel.
Unfortunately this module is based on a undocumented protocol (hub08 - hub12). But since it is a very stupid device, with a little of reverse-engineering it is possible to understand how to drive every single pixel.<br>
The core of the device is composed by a certain amount of 74HC595 cascaded one each other.
The core of the device is composed by a certain amount of 74HC595 cascaded one each other.<br>
This component is a serial (SPI) 8 bit shift-register. Cascading them  the display can load info on pixel status (on/off) and send pin output to drive LEDs.
This component is a serial (SPI) 8 bit shift-register. Cascading them  the display can load info on pixel status (on/off) and send pin output to drive LEDs.<br>
The bad new is there is not 1 register (byte) every 8 LEDs but the system is "interlaced" with a certain scan ratio.
The bad new is there is not 1 register (byte) every 8 LEDs but the system is "interlaced" with a certain scan ratio.<br>
My panel, like the vaste majority of outdoor devices, has a 1/4 scan ratio. This means only 1 line over 4 is displayed at each scan time.
My panel, like the vaste majority of outdoor devices, has a 1/4 scan ratio. This means only 1 line over 4 is displayed at each scan time.<br>
A full display refresh needs 4 scans.
A full display refresh needs 4 scans.<br>
In a 1/16 scan display a single line is displayed and the device needs 16 scans for a full refresh
In a 1/16 scan display a single line is displayed and the device needs 16 scans for a full refresh<br>
If you are not sure about the scan ratio of your display you can simply count the amount of 74HC595 soldered on the back side and:
If you are not sure about the scan ratio of your display you can simply count the amount of 74HC595 soldered on the back side and:
<pre>
<pre>
scan-ratio = pixWidth x pixHeight / 8 / number_of_HC595.
scan-ratio = pixWidth x pixHeight / 8 / number_of_HC595.
</pre>
</pre>
The lines to be displayed at scan time are selected through a 74HC138 line decoder.  In this design, for 1/4 scan, only 2 input are used to select scan line and the third (pin 3) is grounded since not needed (2 bits -> 4 lines)
The lines to be displayed at scan time are selected through a 74HC138 line decoder.  In this design, for 1/4 scan, only 2 input are used to select scan line and the third (pin 3) is grounded since not needed (2 bits -> 4 lines)<br>
Lines are driven through 4 double mosfets, but pay attention: I cannot see any load resistor connected to the LEDs on the backside. So, I suggest to PWM the "enable output" pin to be sure not to damage your panel.  
Lines are driven through 4 double mosfets, but pay attention: I cannot see any load resistor connected to the LEDs on the backside. So, I suggest to PWM the "enable output" pin to be sure not to damage your panel. <br>
The overflow pin of the last serial register is connected to the output connector.
The overflow pin of the last serial register is connected to the output connector.<br>
Connecting it to the input of another panel you can cascade them and enlarge your display.
Connecting it to the input of another panel you can cascade them and enlarge your display.<br>


<strong>Connections</strong>
===Connections===


The main power supply connector is in the middle. You can power your panels with a standard PC PSU.
The main power supply connector is in the middle. You can power your panels with a standard PC PSU.<br>
Be careful if you don't connect power supply the panel will try to drain power from your Arduino (risk to damage it!) trough signal and enable pins.
Be careful if you don't connect power supply the panel will try to drain power from your Arduino (risk to damage it!) trough signal and enable pins.<br>


On the board there are 2 DMD connectors: 1 is for input 1 for output (in order to cascade other devices.)
On the board there are 2 DMD connectors: 1 is for input 1 for output (in order to cascade other devices.)<br>
The standard DMD pinout is:
The standard DMD pinout is:<br>


[[File:DMD.png]]


<a href="http://blog.vettore.org/building-a-large-led-sign-with-inexpensive-standard-modules-and-arduino/dmd/" rel="attachment wp-att-919"><img src="http://blog.vettore.org/wp-content/uploads/2014/09/DMD.png" alt="DMD" width="298" height="287" class="aligncenter size-full wp-image-919" /></a>
On a 1/4 SCAN only A and B pins are used for data.<br>
OE is output enable and you have to PWM it as explained above.<br>
CLK SCK and R are to be connected to your Arduino SPI pins (may vary with Arduino version)<br>


On a 1/4 SCAN only A and B pins are used for data.
===Software===
OE is output enable and you have to PWM it as explained above.
CLK SCK and R are to be connected to your Arduino SPI pins (may vary with Arduino version)


<strong>Software</strong>
After 1 hour testing I discovered 8 pixel blocks composing the full scan line are loaded in a non sequential order:


After 1 hour testing I discovered 8 pixel blocks composing the full scan line are loaded with a certain order:
[[File:Digits-300x224.jpg ]]


<a href="http://blog.vettore.org/building-a-large-led-sign-with-inexpensive-standard-modules-and-arduino/digits/" rel="attachment wp-att-923"><img src="http://blog.vettore.org/wp-content/uploads/2014/09/digits-300x224.jpg" alt="digits" width="300" height="224" class="aligncenter size-medium wp-image-923" /></a>
So the first scan line you will upload via SPI will be composed by total of 8 bytes.<br>
They will match the following sequence: first line of char 1  - first line of char 2.....<br>
So the first scan line you will upload via SPI will be composed by total of 8 bytes.
They will match the following sequence: first line of char 1  - first line of char 2.....


<strong>Code examples</strong>
===Code examples===


The first is a very simple sketch. It will show some chars on your single LED panel.
The first is a very simple sketch. It will show some chars on your single LED panel.
(code is ugly, but kept as simple as possible : ))  
(code is ugly, but kept as simple as possible : )) <br>


The second is more complex. Itcomposed by two files: main code and font definition (font.h).
The second is more complex. It is composed by two files: main code and font definition (font.h).<br>
It handles cascaded panels and allow to send the string to be displayed through the serial monitor.
It handles cascaded panels and allow to send the string to be displayed through the serial monitor.<br>
In order to handle continuous refresh even it implements a sort of multitasking. So the <a href="http://playground.arduino.cc/code/timer1" title="timerOne">TimerOne</a> library is required.
In order to handle continuous refresh even it implements a sort of multitasking. So the [https://docs.arduino.cc/libraries/timerone/ timer ONE libraryis required.


<code lang="c">
<pre>
/**************************************************************
/**************************************************************
*
*
*  Sample sketch for driving 32x16 LED PANEL (1/4 scan) with
*  Sample sketch for driving 32x16 LED PANEL (1/4 scan) with
*  HUB12 protocol
*  HUB12 protocol
*
*
*************************************************************/
*************************************************************/
#include <SPI.h>
//Pins specific for Mega .See Arduino SPI for a different board.
#define A 22
#define B 24
#define OE 26
#define R1 51
#define CLK 52
#define STB 53
 
//row to be shown (1-4 since it is 1/4 scan)
byte row=0;
//brightness: increase->more bright
int br=500;
//some digits from a 8x8 font (numeric 1-8)
byte digits[]={
0x04,  //1
0x0C,
0x04,
0x04,
0x04,
0x04,
0x0E,
0x00,
0x0E, //2
0x11,
0x01,
0x02,
0x04,
0x08,
0x1F,
0x00,
0x1F, //3
0x02,
0x04,
0x02,
0x01,
0x11,
0x0E,
0x00,
0x02, //4
0x06,
0x0A,
0x12,
0x1F,
0x02,
0x02,
0x00,
0x1F, //5
0x10,
0x1E,
0x01,
0x01,
0x11,
0x0E,
0x00,


#include <SPI.h>
0x06, //6
 
0x08,
//Pins specific for Mega .See Arduino SPI for a different board.
0x10,
#define A 22
0x1E,
#define B 24
0x11,
#define OE 26
0x11,
#define R1 51
0x0E,
#define CLK 52
0x00,
#define STB 53
0x1F,//7
0x01,
0x02,
0x04,
0x04,
0x04,
0x04,
0x00,
0x0E,//8
0x11,
0x11,
0x0E,
0x11,
0x11,
0x0E,
0x00,
};
    
    
void setup () {
    pinMode(A, OUTPUT);
    pinMode(B, OUTPUT);
    pinMode(OE, OUTPUT);
    pinMode(R1, OUTPUT);
    pinMode(CLK, OUTPUT);
    pinMode(STB, OUTPUT);
    SPI.begin();
    delay(300);
}   
//display alternatively scan lines
void loop(){
      showRow(0);
      showRow(1);
      showRow(2);
      showRow(3);
}


//row to be shown (1-4 since it is 1/4 scan)
//Load and show row (1-4) i.e. 1 and 5, 2 and 6.....
byte row=0;  
void showRow(int row){
 
//brightness: increase->more bright
      SPI.transfer(~(digits[row+36]));   //5
int br=500;
      SPI.transfer(~(digits[row+32]));             
 
//some digits from a 8x8 font (numeric 1-8)
      SPI.transfer(~(digits[row+4]));    //1
byte digits[]={
      SPI.transfer(~(digits[row]));          
      SPI.transfer(~(digits[row+44]));  //6
      SPI.transfer(~(digits[row+40]));             
      SPI.transfer(~(digits[row+12]));  //2
      SPI.transfer(~(digits[row+8]));             


0x04,  //1
      SPI.transfer(~(digits[row+52]));  //7
0x0C,
      SPI.transfer(~(digits[row+48]));             
0x04,
0x04,
0x04,
0x04,
0x0E,
0x00,


0x0E, //2
      SPI.transfer(~(digits[row+20]));  //3
0x11,
      SPI.transfer(~(digits[row+16]));             
0x01,
0x02,
      SPI.transfer(~(digits[row+60]));  //8
0x04,
      SPI.transfer(~(digits[row+56]));             
0x08,
 
0x1F,
      SPI.transfer(~(digits[row+28]));  //4
0x00,
      SPI.transfer(~(digits[row+24]));             
 
      digitalWrite(STB,LOW);
      digitalWrite(STB,HIGH);
      scanrow(row);//enable encoder for the line loaded


      //PWM like. Change br to adjust brightnes
      digitalWrite(OE,HIGH);
      delayMicroseconds(br);
      digitalWrite(OE,LOW);
      delayMicroseconds(900);
}
//enable encoder for this row in order to show it
void scanrow(int r){
    if(r==0){
      digitalWrite(A,0);
      digitalWrite(B,0);
    } 
    else if(r==1){
      digitalWrite(A,1);
      digitalWrite(B,0);
    }
    else if(r==2){
      digitalWrite(A,0);
      digitalWrite(B,1);
    }
    else if(r==3){
      digitalWrite(A,1);
      digitalWrite(B,1);
    }
}
</pre>


0x1F, //3
Second example: driving interactively an arbitrary number of cascaded panels
0x02,
0x04,
0x02,
0x01,
0x11,
0x0E,
0x00,
 
0x02, //4
0x06,
0x0A,
0x12,
0x1F,
0x02,
0x02,
0x00,


0x1F, //5
<pre>
0x10,
/**************************************************************
0x1E,
*
0x01,
*  Sample sketch for driving 24x16 LED PANEL (1/4 scan) with
0x01,
*  HUB12 protocol
0x11,
*  Support for multiple cascaded panels
0x0E,
*  Get text TO DISPLAY from the serial monitor
0x00,
*
*************************************************************/


#include <SPI.h>
#include <Timer.h>
#include "font.h"
Timer t;
Timer t1;


0x06, //6
//Pins specific for Mega.See Arduino SPI for a different board.
0x08,
#define A 2
0x10,
#define B 3
0x1E,
#define OE 4
0x11,
#define R1 51
0x11,
#define CLK 52
0x0E,
#define STB 53
0x00,
 
//number of cascaded panels 
#define N_PANELS 4 
 
//row to be shown (1-4 since it is 1/4 scan)
byte row=0;


0x1F,//7
//brightness: increase->more bright
0x01,
int br=400;
0x02,
0x04,
0x04,
0x04,
0x04,
0x00,


int incomingByte = 0;


0x0E,//8
//buffer 8 characters for N_PANELS
0x11,
0x11,
0x0E,
0x11,
0x11,
0x0E,
0x00,






byte stringDisp[8*N_PANELS]={
  'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','x','y','0','1','2','3','4','5','6','7',
};




//pixBuffer reserved space for N_PANELS
//contains status of every single pixel in our display!
byte pixBuffer[8*8*N_PANELS]={
};
};


Line 175: Line 308:


void setup () {
void setup () {
    Serial.begin(9600);
     pinMode(A, OUTPUT);
     pinMode(A, OUTPUT);
     pinMode(B, OUTPUT);
     pinMode(B, OUTPUT);
Line 181: Line 315:
     pinMode(CLK, OUTPUT);
     pinMode(CLK, OUTPUT);
     pinMode(STB, OUTPUT);
     pinMode(STB, OUTPUT);
     SPI.begin();
     SPI.begin();
    //set timers
    //first timer is the refresh rate for the 1/4 scan
    t.every(6,display);
    //second timer is for change display text IF INPUT AVAILABLE
    t1.every(1000,checkInput);
     delay(300);  
     delay(300);  
}     
}     




//display alternatively scan lines
void loop(){
void loop(){
      showRow(0);
      showRow(1);
      showRow(2);
      showRow(3);
}


  t.update();
  t1.update();
 
}


//Load and show row (1-4) i.e. 1 and 5, 2 and 6.....
void showRow(int row){


      SPI.transfer(~(digits[row+36]));   //5
//diplay alternatively the 4 scan lines
      SPI.transfer(~(digits[row+32]));            
void display(){
      showRow(0);
      showRow(1);
      showRow(2);
      showRow(3);  
}


      SPI.transfer(~(digits[row+4]));    //1
      SPI.transfer(~(digits[row]));           


      SPI.transfer(~(digits[row+44]));  //6
      SPI.transfer(~(digits[row+40]));             


      SPI.transfer(~(digits[row+12]));   //2
//Load and show row (1-4) i.e. 1-5-9-13, 2-6-10-14.....
      SPI.transfer(~(digits[row+8]));             
void showRow(int row)
 
      int col=0;//is a column of 2 chars


       SPI.transfer(~(digits[row+52]));  //7
       for(col=0;col<4*N_PANELS;col++){//show 2 characters every cicle
      SPI.transfer(~(digits[row+48]));            
        SPI.transfer(~(pixBuffer[row+(col*8)+4+(32*N_PANELS)]));  
 
        SPI.transfer(~(pixBuffer[row+(col*8)+(32*N_PANELS)]));
      SPI.transfer(~(digits[row+20]));  //3
        SPI.transfer(~(pixBuffer[row+(col*8)+4]));
      SPI.transfer(~(digits[row+16]));            
        SPI.transfer(~(pixBuffer[row+(col*8)]));
 
       }
      SPI.transfer(~(digits[row+60]));  //8
      SPI.transfer(~(digits[row+56]));            
 
      SPI.transfer(~(digits[row+28]));   //4
       SPI.transfer(~(digits[row+24]));             


       digitalWrite(STB,LOW);
       digitalWrite(STB,LOW);
Line 229: Line 365:
       //PWM like. Change br to adjust brightnes
       //PWM like. Change br to adjust brightnes
       digitalWrite(OE,HIGH);
       digitalWrite(OE,HIGH);
       delayMicroseconds(br);
       delayMicroseconds(br);//PWM per aggiustare luminosità
       digitalWrite(OE,LOW);
       digitalWrite(OE,LOW);
       delayMicroseconds(900);
       delayMicroseconds(900);
}
}


Line 256: Line 390:
}
}


</code>
//check if input from the serial monitor is available
 
//and update the display
 
void checkInput(void){
Second example: driving interactively an arbitrary number of cascaded panels
  int x=0;
 
  int y=0;
<code lang="c">
  if(Serial.available()){
/**************************************************************
        int h;
*
        //clean display
* Sample sketch for driving 24x16 LED PANEL (1/4 scan) with
        for(h=0;h<(8*N_PANELS);h++)stringDisp[h]=32;
*  HUB12 protocol
  }
*  Support for multiple cascaded panels
  while (Serial.available() > 0) {
*  Get text TO DISPLAY from the serial monitor
          // read the incoming byte:
*
          incomingByte = Serial.read();//only UP to the efective string length
*************************************************************/
          if(x<(8*N_PANELS)) stringDisp[x]=incomingByte;
          x++;
          }
  loadBuffer();
}
 
 


#include <SPI.h>
//load buffer with character pixels of the string
#include <Timer.h>
//performing a lookup on the FONT table
#include "font.h"
void loadBuffer(void){ 
Timer t;
      int x;
Timer t1;
      int y;
      for(y=0;y<(8*(N_PANELS));y++){           
          for(x=0;x<8;x++){
              pixBuffer[x+8*y]=font[8*stringDisp[y]+x-(31*8)];//char under <=31 not defined
            }
      }
}
</pre>


//Pins specific for Mega.See Arduino SPI for a different board.
FONT DEFINITION: font.h
#define A 2
#define B 3
#define OE 4
#define R1 51
#define CLK 52
#define STB 53
 
//number of cascaded panels 
#define N_PANELS 4 


//row to be shown (1-4 since it is 1/4 scan)
<pre>
byte row=0;
/**********************************************
*
*        USED FONTS DEFINITIONS
*
**********************************************/


//brightness: increase->more bright
byte font[]={
int br=400;
//SPACE
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // !


int incomingByte = 0;
  //SPACE
 
0x00,
//buffer 8 characters for N_PANELS
0x00,
 
0x00,
 
0x00,
 
0x00,
byte stringDisp[8*N_PANELS]={
0x00,
  'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','x','y','0','1','2','3','4','5','6','7',
0x00,
};
0x00,
 
  // !
 
0x04,
 
0x04,
//pixBuffer reserved space for N_PANELS
0x04,
//contains status of every single pixel in our display!
0x04,
byte pixBuffer[8*8*N_PANELS]={
0x00,
};
0x00,
 
0x04,
 
0x00,
 
  // "
void setup () {
0x0A,
    Serial.begin(9600);
0x0A,
    pinMode(A, OUTPUT);
0x0A,
    pinMode(B, OUTPUT);
0x00,
    pinMode(OE, OUTPUT);
0x00,
    pinMode(R1, OUTPUT);
0x00,
    pinMode(CLK, OUTPUT);
0x00,
    pinMode(STB, OUTPUT);
0x00,
 
  // #
    SPI.begin();
0x0A,
 
0x0A,
    //set timers
0x1F,
    //first timer is the refresh rate for the 1/4 scan
0x0A,
    t.every(6,display);
0x1F,
    //second timer is for change display text IF INPUT AVAILABLE
0x0A,
    t1.every(1000,checkInput);
0x0A,
 
0x00,
    delay(300);
  // $
}   
0x04,
 
0x0F,
 
0x14,
void loop(){
0x0E,
 
0x05,
  t.update();
0x1E,
  t1.update();
0x04,
 
0x00,
}
  // %
 
0x18,
 
0x19,
//diplay alternatively the 4 scan lines
0x02,
void display(){
0x04,
      showRow(0);
0x08,
      showRow(1);
0x13,
      showRow(2);
0x03,
      showRow(3);
0x00,
}
   // &
 
0x0C,
 
0x12,
 
0x14,
//Load and show row (1-4) i.e. 1-5-9-13, 2-6-10-14.....
0x08,
void showRow(int row){ 
0x15,
    
0x12,
      int col=0;//is a column of 2 chars
0x0D,
 
0x00,
      for(col=0;col<4*N_PANELS;col++){//show 2 characters every cicle
  // '
        SPI.transfer(~(pixBuffer[row+(col*8)+4+(32*N_PANELS)]));
0x0C,
        SPI.transfer(~(pixBuffer[row+(col*8)+(32*N_PANELS)]));
0x04,
        SPI.transfer(~(pixBuffer[row+(col*8)+4]));
0x08,
        SPI.transfer(~(pixBuffer[row+(col*8)]));
0x00,
      }
0x00,
 
0x00,
      digitalWrite(STB,LOW);
0x00,
      digitalWrite(STB,HIGH);
0x00,
 
  // (
      scanrow(row);//enable encoder for the line loaded
0x02,
 
0x04,
      //PWM like. Change br to adjust brightnes
0x08,
      digitalWrite(OE,HIGH);
0x08,
      delayMicroseconds(br);//PWM per aggiustare luminosità
0x08,
      digitalWrite(OE,LOW);
0x04,
      delayMicroseconds(900);
0x02,
}
0x00,
 
  // )
//enable encoder for this row in order to show it
0x08,
void scanrow(int r){
0x04,
    if(r==0){
0x02,
      digitalWrite(A,0);
0x02,
      digitalWrite(B,0);
0x02,
    } 
0x04,
    else if(r==1){
0x08,
      digitalWrite(A,1);
0x00,
      digitalWrite(B,0);
  // *
    }
0x00,
    else if(r==2){
0x04,
      digitalWrite(A,0);
0x15,
      digitalWrite(B,1);
0x0E,
    }
0x15,
    else if(r==3){
0x04,
      digitalWrite(A,1);
0x00,
      digitalWrite(B,1);
0x00,
    }
   // +
}
0x00,
 
0x04,
//check if input from the serial monitor is available
0x04,
//and update the display
0x1F,
void checkInput(void){
0x04,
  int x=0;
0x04,
  int y=0;
0x00,
  if(Serial.available()){
0x00,
        int h;
  // ,
        //clean display
0x00,
        for(h=0;h<(8*N_PANELS);h++)stringDisp[h]=32;
0x00,
  }
0x00,
   while (Serial.available() > 0) {
0x00,
          // read the incoming byte:
0x0C,
          incomingByte = Serial.read();//only UP to the efective string length
0x04,
          if(x<(8*N_PANELS)) stringDisp[x]=incomingByte;
0x08,
          x++;
0x00,
          }
  // -
  loadBuffer();
0x00,
}
0x00,
 
 
 
//load buffer with character pixels of the string
//performing a lookup on the FONT table
void loadBuffer(void){ 
      int x;
      int y;
      for(y=0;y<(8*(N_PANELS));y++){           
          for(x=0;x<8;x++){
              pixBuffer[x+8*y]=font[8*stringDisp[y]+x-(31*8)];//char under <=31 not defined
            }
      }
}
</code>
 
FONT DEFINITION: font.h
 
<code lang="c">
/**********************************************
*
*        USED FONTS DEFINITIONS
*
**********************************************/
 
byte font[]={
//SPACE
0x00,
0x00,
0x1F,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // .
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // !
  //SPACE
0x00,
0x00,
0x00,
0x00,
0x0C,
0x0C,
0x00,
0x00,
  // /
0x00,
0x00,
0x01,
0x02,
0x04,
0x08,
0x10,
0x00,
0x00,
0x00,
0x00,
  // 0
0x0E,
0x11,
0x13,
0x15,
0x19,
0x11,
0x0E,
0x00,
0x00,
0x00,
   // 1
   // !
0x04,
0x0C,
0x04,
0x04,
0x04,
0x04,
0x04,
0x04,
0x04,
0x04,
0x0E,
0x00,
0x00,
  // 2
0x0E,
0x11,
0x01,
0x02,
0x04,
0x08,
0x1F,
0x00,
0x00,
  // 3
0x1F,
0x02,
0x04,
0x04,
0x02,
0x01,
0x11,
0x0E,
0x00,
0x00,
   // "
   // 4
0x0A,
0x02,
0x0A,
0x06,
0x0A,
0x0A,
0x12,
0x1F,
0x02,
0x02,
0x00,
0x00,
  // 5
0x1F,
0x10,
0x1E,
0x01,
0x01,
0x11,
0x0E,
0x00,
0x00,
  // 6
0x06,
0x08,
0x10,
0x1E,
0x11,
0x11,
0x0E,
0x00,
0x00,
0x00,
   // 7
0x00,
   // #
0x0A,
0x0A,
0x1F,
0x1F,
0x0A,
0x01,
0x1F,
0x02,
0x0A,
0x04,
0x0A,
0x00,
  // $
0x04,
0x04,
0x0F,
0x14,
0x0E,
0x05,
0x1E,
0x04,
0x04,
0x00,
  // %
0x18,
0x19,
0x02,
0x04,
0x04,
0x08,
0x13,
0x03,
0x00,
0x00,
   // &
   // 8
0x0C,
0x1E,
0x12,
0x11,
0x14,
0x11,
0x08,
0x0E,
0x15,
0x11,
0x12,
0x11,
0x0D,
0x0E,
0x00,
0x00,
   // '
   // 9
0x0E,
0x11,
0x11,
0x0F,
0x01,
0x02,
0x0C,
0x0C,
0x04,
0x08,
0x00,
0x00,
  // :
0x00,
0x00,
0x0C,
0x0C,
0x00,
0x0C,
0x0C,
0x00,
0x00,
0x00,
0x00,
  // ;
0x00,
0x00,
   // (
0x0C,
0x0C,
0x00,
0x0C,
0x04,
0x08,
0x00,
   // <
0x02,
0x02,
0x04,
0x04,
0x08,
0x08,
0x08,
0x10,
0x08,
0x08,
0x04,
0x04,
0x02,
0x02,
0x00,
0x00,
   // )
   // =
0x00,
0x00,
0x1F,
0x00,
0x1F,
0x00,
0x00,
0x00,
  // >
0x08,
0x08,
0x04,
0x04,
0x02,
0x02,
0x02,
0x01,
0x02,
0x02,
0x04,
0x04,
0x08,
0x08,
0x00,
0x00,
   // *
   // ?
0x0E,
0x11,
0x01,
0x02,
0x04,
0x00,
0x00,
0x04,
0x04,
0x00,
  // @
0x0E,
0x11,
0x01,
0x0D,
0x15,
0x15,
0x15,
0x0E,
0x0E,
0x15,
0x04,
0x00,
0x00,
  // A
0x0E,
0x11,
0x11,
0x11,
0x1F,
0x11,
0x11,
0x00,
0x00,
   // +
   // B
0x00,
0x1E,
0x04,
0x09,
0x04,
0x09,
0x1F,
0x0E,
0x04,
0x09,
0x04,
0x09,
0x00,
0x1E,
0x00,
  // ,
0x00,
0x00,
0x00,
  // C
0x0E,
0x11,
0x10,
0x10,
0x10,
0x11,
0x0E,
0x00,
0x00,
  // D
0x1E,
0x09,
0x09,
0x09,
0x09,
0x09,
0x1E,
0x00,
0x00,
0x0C,
  // E
0x04,
0x1F,
0x08,
0x10,
0x00,
0x10,
  // -
0x1F,
0x00,
0x10,
0x00,
0x10,
0x1F,
0x00,
0x00,
  // F
0x1F,
0x1F,
0x10,
0x10,
0x1E,
0x10,
0x10,
0x10,
0x00,
0x00,
0x00,
   // G
0x00,
0x00,
   // .
0x00,
0x00,
0x00,
0x00,
0x00,
0x0C,
0x0C,
0x00,
  // /
0x00,
0x01,
0x02,
0x04,
0x08,
0x10,
0x00,
0x00,
  // 0
0x0E,
0x0E,
0x11,
0x11,
0x10,
0x13,
0x13,
0x15,
0x19,
0x11,
0x11,
0x11,
0x0F,
0x00,
  // H
0x11,
0x11,
0x11,
0x1F,
0x11,
0x11,
0x11,
0x00,
  // I
0x0E,
0x0E,
0x00,
  // 1
0x04,
0x04,
0x0C,
0x04,
0x04,
0x04,
0x04,
Line 613: Line 823:
0x0E,
0x0E,
0x00,
0x00,
   // 2
   // J
0x0E,
0x07,
0x11,
0x02,
0x01,
0x02,
0x02,
0x02,
0x04,
0x02,
0x08,
0x12,
0x1F,
0x0C,
0x00,
0x00,
   // 3
   // K
0x1F,
0x02,
0x04,
0x02,
0x01,
0x11,
0x11,
0x0E,
0x00,
  // 4
0x02,
0x06,
0x0A,
0x12,
0x12,
0x1F,
0x14,
0x02,
0x18,
0x02,
0x14,
0x12,
0x11,
0x00,
0x00,
   // 5
   // L
0x10,
0x10,
0x10,
0x10,
0x10,
0x10,
0x1F,
0x1F,
0x10,
0x00,
0x1E,
  // M
0x01,
0x11,
0x01,
0x1B,
0x15,
0x15,
0x11,
0x11,
0x0E,
0x00,
  // 6
0x06,
0x08,
0x10,
0x1E,
0x11,
0x11,
0x11,
0x11,
0x0E,
0x00,
0x00,
   // 7
   // N
0x1F,
0x11,
0x01,
0x19,
0x02,
0x19,
0x04,
0x15,
0x04,
0x13,
0x04,
0x13,
0x04,
0x11,
0x00,
0x00,
   // 8
   // O
0x1E,
0x0E,
0x11,
0x11,
0x11,
0x11,
0x11,
0x0E,
0x11,
0x11,
0x11,
0x11,
0x0E,
0x0E,
0x00,
0x00,
   // 9
   // P
0x0E,
0x1E,
0x11,
0x11,
0x11,
0x11,
0x0F,
0x1E,
0x01,
0x10,
0x02,
0x10,
0x0C,
0x10,
0x00,
0x00,
   // :
   // Q
0x0E,
0x11,
0x11,
0x11,
0x15,
0x12,
0x1D,
0x00,
0x00,
0x0C,
  // R
0x0C,
0x1E,
0x11,
0x11,
0x1E,
0x14,
0x12,
0x11,
0x00,
0x00,
0x0C,
  // S
0x0C,
0x0E,
0x11,
0x10,
0x0E,
0x01,
0x11,
0x0E,
0x00,
0x00,
0x00,
   // T
   // ;
0x1F,
0x00,
0x04,
0x0C,
0x0C,
0x00,
0x0C,
0x04,
0x04,
0x08,
0x00,
  // <
0x02,
0x04,
0x04,
0x08,
0x10,
0x08,
0x04,
0x04,
0x02,
0x00,
  // =
0x00,
0x00,
0x1F,
0x00,
0x1F,
0x00,
0x00,
0x00,
  // >
0x08,
0x04,
0x04,
0x02,
0x01,
0x02,
0x04,
0x04,
0x08,
0x00,
0x00,
   // ?
   // U
0x0E,
0x11,
0x11,
0x11,
0x11,
0x11,
0x11,
0x01,
0x02,
0x04,
0x00,
0x04,
0x00,
  // @
0x0E,
0x11,
0x11,
0x01,
0x0D,
0x15,
0x15,
0x0E,
0x0E,
0x00,
0x00,
   // A
   // V
0x0E,
0x11,
0x11,
0x11,
0x11,
0x11,
0x11,
0x1F,
0x11,
0x11,
0x11,
0x11,
0x0A,
0x04,
0x00,
0x00,
   // B
   // W
0x1E,
0x11,
0x09,
0x11,
0x09,
0x11,
0x0E,
0x15,
0x09,
0x15,
0x09,
0x1B,
0x1E,
0x11,
0x00,
0x00,
   // C
   // X
0x0E,
0x11,
0x11,
0x0A,
0x04,
0x0A,
0x11,
0x11,
0x10,
0x10,
0x10,
0x11,
0x11,
0x0E,
0x00,
0x00,
   // D
   // Y
0x1E,
0x11,
0x09,
0x11,
0x09,
0x11,
0x09,
0x0A,
0x09,
0x04,
0x09,
0x04,
0x1E,
0x04,
0x00,
0x00,
   // E
   // Z
0x1F,
0x1F,
0x10,
0x01,
0x10,
0x02,
0x1F,
0x04,
0x10,
0x08,
0x10,
0x10,
0x1F,
0x1F,
0x00,
0x00,
   // F
   // [
0x1F,
0x0E,
0x10,
0x08,
0x10,
0x08,
0x1E,
0x08,
0x10,
0x08,
0x10,
0x08,
0x0E,
0x00,
 
////////////
//0x00,
 
  // \
0x00,
0x10,
0x10,
0x08,
0x04,
0x02,
0x01,
0x00,
0x00,
0x00,
   // G
   // ]
0x0E,
0x0E,
0x02,
0x02,
0x02,
0x02,
0x02,
0x0E,
0x00,
  // ^
0x04,
0x0A,
0x11,
0x11,
0x10,
0x13,
0x11,
0x11,
0x0F,
0x00,
0x00,
  // H
0x11,
0x11,
0x11,
0x1F,
0x11,
0x11,
0x11,
0x00,
0x00,
  // I
0x0E,
0x04,
0x04,
0x04,
0x04,
0x04,
0x0E,
0x00,
0x00,
  // J
0x07,
0x02,
0x02,
0x02,
0x02,
0x12,
0x0C,
0x00,
0x00,
  // K
0x11,
0x12,
0x14,
0x18,
0x14,
0x12,
0x11,
0x00,
0x00,
   // L
   // _
0x10,
0x00,
0x10,
0x00,
0x10,
0x00,
0x10,
0x00,
0x10,
0x00,
0x10,
0x00,
0x1F,
0x1F,
0x00,
0x00,
   // M
   // `
0x11,
0x10,
0x1B,
0x08,
0x15,
0x04,
0x15,
0x00,
0x11,
0x00,
0x11,
0x00,
0x11,
0x00,
0x00,
  // a
0x00,
0x00,
  // N
0x11,
0x19,
0x19,
0x15,
0x13,
0x13,
0x11,
0x00,
0x00,
  // O
0x0E,
0x0E,
0x01,
0x0F,
0x11,
0x11,
0x0F,
0x00,
  // b
0x10,
0x10,
0x16,
0x19,
0x11,
0x11,
0x11,
0x11,
0x1E,
0x00,
  // c
0x00,
0x00,
0x0E,
0x11,
0x11,
0x10,
0x11,
0x11,
0x0E,
0x0E,
0x00,
0x00,
   // P
   // d
0x1E,
0x01,
0x01,
0x0D,
0x13,
0x11,
0x11,
0x11,
0x11,
0x1E,
0x0F,
0x10,
0x00,
0x10,
  // e
0x10,
0x00,
0x00,
0x00,
  // Q
0x0E,
0x0E,
0x11,
0x11,
0x11,
0x1F,
0x11,
0x10,
0x15,
0x0E,
0x12,
0x1D,
0x00,
0x00,
   // R
   // f
0x1E,
0x02,
0x11,
0x05,
0x11,
0x04,
0x1E,
0x14,
0x12,
0x11,
0x00,
  // S
0x0E,
0x11,
0x10,
0x0E,
0x01,
0x11,
0x0E,
0x0E,
0x00,
  // T
0x1F,
0x04,
0x04,
0x04,
0x04,
0x04,
0x04,
0x04,
0x04,
0x04,
0x00,
0x00,
   // U
   // g
0x11,
0x00,
0x11,
0x00,
0x11,
0x0D,
0x13,
0x13,
0x0D,
0x01,
0x0E,
  // h
0x10,
0x10,
0x16,
0x19,
0x11,
0x11,
0x11,
0x11,
0x11,
0x11,
0x0E,
0x00,
0x00,
   // V
   // i
0x11,
0x11,
0x11,
0x11,
0x11,
0x0A,
0x04,
0x04,
0x00,
0x00,
  // W
0x0C,
0x11,
0x04,
0x11,
0x11,
0x15,
0x15,
0x1B,
0x11,
0x00,
  // X
0x11,
0x11,
0x0A,
0x04,
0x0A,
0x11,
0x11,
0x00,
  // Y
0x11,
0x11,
0x11,
0x0A,
0x04,
0x04,
0x04,
0x04,
0x04,
0x0E,
0x00,
0x00,
   // Z
   // j
0x1F,
0x02,
0x01,
0x00,
0x06,
0x02,
0x02,
0x02,
0x04,
0x12,
0x08,
0x0C,
0x10,
0x1F,
0x00,
0x00,
   // [
   // k
0x0E,
0x08,
0x08,
0x08,
0x08,
0x08,
0x09,
0x08,
0x0A,
0x08,
0x0C,
0x0E,
0x0A,
0x09,
0x00,
0x00,
 
  // l
////////////
0x0C,
//0x00,
0x04,
 
0x04,
  // \
0x04,
0x04,
0x00,
0x10,
0x08,
0x04,
0x04,
0x02,
0x0E,
0x01,
0x00,
0x00,
  // m
0x00,
0x00,
  // ]
0x0E,
0x02,
0x02,
0x02,
0x02,
0x02,
0x0E,
0x00,
0x00,
  // ^
0x1A,
0x04,
0x15,
0x0A,
0x15,
0x11,
0x15,
0x15,
0x00,
0x00,
  // n
0x00,
0x00,
0x00,
0x00,
0x16,
0x19,
0x11,
0x11,
0x11,
0x00,
0x00,
  // o
0x00,
0x00,
  // _
0x00,
0x00,
0x0E,
0x11,
0x11,
0x11,
0x0E,
0x00,
0x00,
  // p
0x00,
0x00,
0x00,
0x00,
0x00,
0x16,
0x00,
0x19,
0x1F,
0x19,
0x00,
0x16,
  // `
0x10,
0x10,
0x10,
0x08,
  // q
0x04,
0x00,
0x00,
0x00,
0x00,
0x0D,
0x13,
0x13,
0x0D,
0x01,
0x01,
  // r
0x00,
0x00,
0x00,
0x00,
0x16,
0x19,
0x10,
0x10,
0x10,
0x00,
0x00,
   // a
   // s
0x00,
0x00,
0x00,
0x00,
0x0E,
0x01,
0x0F,
0x11,
0x0F,
0x0F,
0x10,
0x1E,
0x01,
0x1F,
0x00,
0x00,
   // b
   // t
0x10,
0x08,
0x10,
0x08,
0x16,
0x1C,
0x19,
0x08,
0x11,
0x08,
0x11,
0x09,
0x1E,
0x06,
0x00,
0x00,
   // c
   // u
0x00,
0x00,
0x12,
0x12,
0x12,
0x12,
0x0D,
0x00,
  // v
0x00,
0x00,
0x00,
0x00,
0x0E,
0x11,
0x10,
0x11,
0x11,
0x0E,
0x00,
  // d
0x01,
0x01,
0x0D,
0x13,
0x11,
0x11,
0x11,
0x11,
0x0F,
0x0A,
0x04,
0x00,
0x00,
   // e
   // w
0x00,
0x00,
0x00,
0x00,
0x0E,
0x11,
0x11,
0x1F,
0x11,
0x10,
0x15,
0x0E,
0x15,
0x0A,
0x00,
  // x
0x00,
0x00,
0x00,
  // f
0x11,
0x02,
0x0A,
0x05,
0x04,
0x0E,
0x04,
0x04,
0x04,
0x04,
0x0A,
0x11,
0x00,
0x00,
   // g
   // y
0x00,
0x00,
0x00,
0x00,
0x0D,
0x11,
0x13,
0x11,
0x13,
0x13,
0x0D,
0x0D,
0x01,
0x01,
0x0E,
0x0E,
   // h
   // z
0x10,
0x00,
0x10,
0x16,
0x19,
0x11,
0x11,
0x11,
0x00,
0x00,
  // i
0x1F,
0x02,
0x04,
0x04,
0x08,
0x1F,
0x00,
0x00,
0x0C,
  //
0x02,
0x04,
0x04,
0x04,
0x08,
0x04,
0x04,
0x04,
0x04,
0x0E,
0x00,
  // j
0x02,
0x02,
0x00,
0x00,
0x06,
  // |
0x02,
0x04,
0x02,
0x04,
0x12,
0x04,
0x0C,
0x00,
0x04,
0x04,
0x04,
0x00,
0x00,
   // k
   //  
0x08,
0x08,
0x08,
0x09,
0x0A,
0x0C,
0x0A,
0x09,
0x00,
  // l
0x0C,
0x04,
0x04,
0x04,
0x04,
0x02,
0x04,
0x04,
0x04,
0x04,
0x04,
0x08,
0x0E,
0x00,
0x00,
   // m
   // ~
0x00,
0x00,
0x00,
0x00,
0x1A,
0x08,
0x15,
0x15,
0x15,
0x15,
0x15,
0x02,
0x00,
0x00,
  // n
0x00,
0x00,
0x00,
0x00,
0x16,
  // 5F
0x19,
0x11,
0x11,
0x11,
0x00,
0x00,
  // o
0x00,
0x00,
0x00,
0x00,
0x0E,
0x11,
0x11,
0x11,
0x0E,
0x00,
0x00,
  // p
0x00,
0x00,
0x00,
0x00,
0x16,
0x19,
0x19,
0x16,
0x10,
0x10,
  // q
0x00,
0x00,
0x00,
0x00,
0x0D,
   // 60
0x13,
0x13,
0x0D,
0x01,
0x01,
   // r
0x00,
0x00,
0x00,
0x00,
0x16,
0x19,
0x10,
0x10,
0x10,
0x00,
0x00,
  // s
0x00,
0x00,
0x00,
0x00,
0x0F,
0x10,
0x1E,
0x01,
0x1F,
0x00,
0x00,
  // t
0x08,
0x08,
0x1C,
0x08,
0x08,
0x09,
0x06,
0x00,
0x00,
  // u
0x00,
0x00,
  // 61
0x00,
0x00,
0x12,
0x12,
0x12,
0x12,
0x0D,
0x00,
0x00,
  // v
0x00,
0x00,
0x00,
0x00,
0x11,
0x11,
0x11,
0x0A,
0x04,
0x00,
0x00,
  // w
0x00,
0x00,
0x00,
0x00,
0x11,
0x11,
0x15,
0x15,
0x0A,
0x00,
0x00,
   // x
   // 62
0x00,
0x00,
0x00,
0x00,
0x00,
0x11,
0x0A,
0x04,
0x0A,
0x11,
0x00,
0x00,
  // y
0x00,
0x00,
0x00,
0x00,
0x11,
0x11,
0x13,
0x0D,
0x01,
0x0E,
  // z
0x00,
0x00,
0x00,
0x00,
0x1F,
  // 63
0x02,
0x04,
0x08,
0x1F,
0x00,
0x00,
  //
0x02,
0x04,
0x04,
0x08,
0x04,
0x04,
0x02,
0x00,
0x00,
  // |
0x04,
0x04,
0x04,
0x00,
0x00,
0x04,
0x04,
0x04,
0x00,
0x00,
  //
0x08,
0x04,
0x04,
0x02,
0x04,
0x04,
0x08,
0x00,
0x00,
  // ~
0x00,
0x00,
0x00,
0x00,
0x08,
0x15,
0x02,
0x00,
0x00,
  // 64
0x00,
0x00,
0x00,
0x00,
  // 5F
0x00,
0x00,
0x00,
0x00,
Line 1,318: Line 1,359:
0x00,
0x00,
0x00,
0x00,
  // 65
0x00,
0x00,
0x00,
0x00,
  // 60
0x00,
0x00,
0x00,
0x00,
Line 1,327: Line 1,368:
0x00,
0x00,
0x00,
0x00,
  // 66
0x00,
0x00,
0x00,
0x00,
  // 61
0x00,
0x00,
0x00,
0x00,
Line 1,336: Line 1,377:
0x00,
0x00,
0x00,
0x00,
  // 67
0x00,
0x00,
0x00,
0x00,
  // 62
0x00,
0x00,
0x00,
0x00,
Line 1,345: Line 1,386:
0x00,
0x00,
0x00,
0x00,
  // 68
0x00,
0x00,
0x00,
0x00,
  // 63
0x00,
0x00,
0x00,
0x00,
Line 1,354: Line 1,395:
0x00,
0x00,
0x00,
0x00,
0x00,
   // 69
0x00,
  // 64
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 65
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 66
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 67
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 68
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
   // 69
0x00,
0x00,
0x00,
0x00,
Line 1,611: Line 1,605:




</code>
</pre>

Latest revision as of 13:32, 22 July 2025

See in action https://www.youtube.com/embed/qrFwV6VtSZg

General Overview

Way back in 2014 I have been asked by my company to create a system for production monitoring.
A very important component of this system was a large LED panel showing production status.
Unfortunately I discoverd such components (for example the excellent Alphabrite panels) are very expensive: the mean quotation for a 2 line-21 chars ethernet controllable display is around 3000 USD!.
So I tought i could save several thousands euros of my budget with a little DIY.

A funny issue during first installation :)

When the project started hardware core was an Arduino MEGA board.
Please note the system si still in production and evolved. My team went on with development an now the core is ESP32 based and the system has WIFI with full MQTT support.
For the visual part i discovered there are a lot of "nearly standard" and nearly-inexpensive LED panels that can be cascaded in order to assemble a large display.
I focused my attention on a 32x16 with pitch (pixel distance) of 10mm designed for outdoor (extrabright).
This kind of product is avaliable from several sellers on the Hong-Kong market for about 9 USD.
(Search for Outdoor P10 RED Led panel 16x32)

I tought I could cascade 4 of them in order to get a panel 960mm X 320mm (really wide...) that should be visible up to 30m.
Unfortunately this module is based on a undocumented protocol (hub08 - hub12). But since it is a very stupid device, with a little of reverse-engineering it is possible to understand how to drive every single pixel.
The core of the device is composed by a certain amount of 74HC595 cascaded one each other.
This component is a serial (SPI) 8 bit shift-register. Cascading them the display can load info on pixel status (on/off) and send pin output to drive LEDs.
The bad new is there is not 1 register (byte) every 8 LEDs but the system is "interlaced" with a certain scan ratio.
My panel, like the vaste majority of outdoor devices, has a 1/4 scan ratio. This means only 1 line over 4 is displayed at each scan time.
A full display refresh needs 4 scans.
In a 1/16 scan display a single line is displayed and the device needs 16 scans for a full refresh
If you are not sure about the scan ratio of your display you can simply count the amount of 74HC595 soldered on the back side and:

scan-ratio = pixWidth x pixHeight / 8 / number_of_HC595.

The lines to be displayed at scan time are selected through a 74HC138 line decoder. In this design, for 1/4 scan, only 2 input are used to select scan line and the third (pin 3) is grounded since not needed (2 bits -> 4 lines)
Lines are driven through 4 double mosfets, but pay attention: I cannot see any load resistor connected to the LEDs on the backside. So, I suggest to PWM the "enable output" pin to be sure not to damage your panel.
The overflow pin of the last serial register is connected to the output connector.
Connecting it to the input of another panel you can cascade them and enlarge your display.

Connections

The main power supply connector is in the middle. You can power your panels with a standard PC PSU.
Be careful if you don't connect power supply the panel will try to drain power from your Arduino (risk to damage it!) trough signal and enable pins.

On the board there are 2 DMD connectors: 1 is for input 1 for output (in order to cascade other devices.)
The standard DMD pinout is:

On a 1/4 SCAN only A and B pins are used for data.
OE is output enable and you have to PWM it as explained above.
CLK SCK and R are to be connected to your Arduino SPI pins (may vary with Arduino version)

Software

After 1 hour testing I discovered 8 pixel blocks composing the full scan line are loaded in a non sequential order:

So the first scan line you will upload via SPI will be composed by total of 8 bytes.
They will match the following sequence: first line of char 1 - first line of char 2.....

Code examples

The first is a very simple sketch. It will show some chars on your single LED panel. (code is ugly, but kept as simple as possible : ))

The second is more complex. It is composed by two files: main code and font definition (font.h).
It handles cascaded panels and allow to send the string to be displayed through the serial monitor.
In order to handle continuous refresh even it implements a sort of multitasking. So the timer ONE library is required.

 /**************************************************************
 *
 *  Sample sketch for driving 32x16 LED PANEL (1/4 scan) with
 *  HUB12 protocol
 *
 *************************************************************/
 #include <SPI.h>
 
 //Pins specific for Mega .See Arduino SPI for a different board.
 #define A 22 
 #define B 24 
 #define OE 26
 #define R1 51
 #define CLK 52
 #define STB 53
   
 //row to be shown (1-4 since it is 1/4 scan)
 byte row=0; 
 
 //brightness: increase->more bright
 int br=500;
 
 //some digits from a 8x8 font (numeric 1-8)
 byte digits[]={
 
 0x04,  //1
 0x0C,
 0x04,
 0x04,
 0x04,
 0x04,
 0x0E,
 0x00, 
 
 0x0E, //2
 0x11,
 0x01,
 0x02,
 0x04,
 0x08,
 0x1F,
 0x00, 
 
 
 0x1F, //3
 0x02,
 0x04,
 0x02,
 0x01,
 0x11,
 0x0E,
 0x00,
 
 0x02, //4
 0x06,
 0x0A,
 0x12,
 0x1F,
 0x02,
 0x02,
 0x00,
 
 0x1F, //5
 0x10,
 0x1E,
 0x01,
 0x01,
 0x11,
 0x0E,
 0x00,

 0x06, //6
 0x08,
 0x10,
 0x1E,
 0x11,
 0x11,
 0x0E,
 0x00,
 
 0x1F,//7
 0x01,
 0x02,
 0x04,
 0x04,
 0x04,
 0x04,
 0x00,
 
 
 0x0E,//8
 0x11,
 0x11,
 0x0E,
 0x11,
 0x11,
 0x0E,
 0x00,
 };
  
 
 
 void setup () {
     pinMode(A, OUTPUT);
     pinMode(B, OUTPUT);
     pinMode(OE, OUTPUT);
     pinMode(R1, OUTPUT);
     pinMode(CLK, OUTPUT);
     pinMode(STB, OUTPUT);
     SPI.begin();
     delay(300); 
 }    
 
 
 //display alternatively scan lines
 void loop(){
       showRow(0);
       showRow(1);
       showRow(2);
       showRow(3);
 }

 //Load and show row (1-4) i.e. 1 and 5, 2 and 6.....
 void showRow(int row){
 
      SPI.transfer(~(digits[row+36]));   //5
      SPI.transfer(~(digits[row+32]));              
 
      SPI.transfer(~(digits[row+4]));    //1
      SPI.transfer(~(digits[row]));            
 
      SPI.transfer(~(digits[row+44]));   //6 
      SPI.transfer(~(digits[row+40]));              
 
      SPI.transfer(~(digits[row+12]));   //2
      SPI.transfer(~(digits[row+8]));              

      SPI.transfer(~(digits[row+52]));   //7
      SPI.transfer(~(digits[row+48]));              

      SPI.transfer(~(digits[row+20]));   //3
      SPI.transfer(~(digits[row+16]));              
 
      SPI.transfer(~(digits[row+60]));   //8
      SPI.transfer(~(digits[row+56]));              
  
      SPI.transfer(~(digits[row+28]));   //4
      SPI.transfer(~(digits[row+24]));              
  
      digitalWrite(STB,LOW);
      digitalWrite(STB,HIGH);
 
      scanrow(row);//enable encoder for the line loaded

      //PWM like. Change br to adjust brightnes
      digitalWrite(OE,HIGH);
      delayMicroseconds(br);
      digitalWrite(OE,LOW);
      delayMicroseconds(900);
 }
 
 //enable encoder for this row in order to show it
 void scanrow(int r){
    if(r==0){
      digitalWrite(A,0);
      digitalWrite(B,0);
    }  
    else if(r==1){
      digitalWrite(A,1);
      digitalWrite(B,0);
    }
    else if(r==2){
      digitalWrite(A,0);
      digitalWrite(B,1);
    }
    else if(r==3){
      digitalWrite(A,1);
      digitalWrite(B,1);
    }
 } 
 

Second example: driving interactively an arbitrary number of cascaded panels

/**************************************************************
*
*  Sample sketch for driving 24x16 LED PANEL (1/4 scan) with
*  HUB12 protocol
*  Support for multiple cascaded panels
*  Get text TO DISPLAY from the serial monitor
*
*************************************************************/

#include <SPI.h>
#include <Timer.h>
#include "font.h"
Timer t;
Timer t1;

//Pins specific for Mega.See Arduino SPI for a different board.
#define A 2 
#define B 3 
#define OE 4
#define R1 51
#define CLK 52
#define STB 53
  
//number of cascaded panels  
#define N_PANELS 4  

//row to be shown (1-4 since it is 1/4 scan)
byte row=0; 

//brightness: increase->more bright
int br=400;

int incomingByte = 0;

//buffer 8 characters for N_PANELS



byte stringDisp[8*N_PANELS]={
  'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','x','y','0','1','2','3','4','5','6','7',
};



//pixBuffer reserved space for N_PANELS
//contains status of every single pixel in our display!
byte pixBuffer[8*8*N_PANELS]={
};



void setup () {
     Serial.begin(9600);
     pinMode(A, OUTPUT);
     pinMode(B, OUTPUT);
     pinMode(OE, OUTPUT);
     pinMode(R1, OUTPUT);
     pinMode(CLK, OUTPUT);
     pinMode(STB, OUTPUT);

     SPI.begin();

     //set timers
     //first timer is the refresh rate for the 1/4 scan
     t.every(6,display);
     //second timer is for change display text IF INPUT AVAILABLE
     t1.every(1000,checkInput);

     delay(300); 
}    


void loop(){

  t.update();
  t1.update();
   
}


//diplay alternatively the 4 scan lines
void display(){
       showRow(0);
       showRow(1);
       showRow(2);
       showRow(3); 
}



//Load and show row (1-4) i.e. 1-5-9-13, 2-6-10-14.....
void showRow(int row){  
  
      int col=0;//is a column of 2 chars

      for(col=0;col<4*N_PANELS;col++){//show 2 characters every cicle
        SPI.transfer(~(pixBuffer[row+(col*8)+4+(32*N_PANELS)])); 
        SPI.transfer(~(pixBuffer[row+(col*8)+(32*N_PANELS)]));
        SPI.transfer(~(pixBuffer[row+(col*8)+4]));
        SPI.transfer(~(pixBuffer[row+(col*8)]));
      }

      digitalWrite(STB,LOW);
      digitalWrite(STB,HIGH);

      scanrow(row);//enable encoder for the line loaded

      //PWM like. Change br to adjust brightnes
      digitalWrite(OE,HIGH);
      delayMicroseconds(br);//PWM per aggiustare luminosità
      digitalWrite(OE,LOW);
      delayMicroseconds(900);
}

//enable encoder for this row in order to show it
void scanrow(int r){
    if(r==0){
      digitalWrite(A,0);
      digitalWrite(B,0);
    }  
    else if(r==1){
      digitalWrite(A,1);
      digitalWrite(B,0);
    }
    else if(r==2){
      digitalWrite(A,0);
      digitalWrite(B,1);
    }
    else if(r==3){
      digitalWrite(A,1);
      digitalWrite(B,1);
    }
}

//check if input from the serial monitor is available 
//and update the display
void checkInput(void){
  int x=0;
  int y=0;
  if(Serial.available()){
        int h;
        //clean display
        for(h=0;h<(8*N_PANELS);h++)stringDisp[h]=32;
  }
  while (Serial.available() > 0) {
           // read the incoming byte:
           incomingByte = Serial.read();//only UP to the efective string length
           if(x<(8*N_PANELS)) stringDisp[x]=incomingByte;
           x++; 
          }
  loadBuffer();
}



//load buffer with character pixels of the string
//performing a lookup on the FONT table
void loadBuffer(void){  
      int x;
      int y;
      for(y=0;y<(8*(N_PANELS));y++){            
           for(x=0;x<8;x++){
               pixBuffer[x+8*y]=font[8*stringDisp[y]+x-(31*8)];//char under <=31 not defined
            }
      }
}

FONT DEFINITION: font.h

/**********************************************
*
*        USED FONTS DEFINITIONS
*
**********************************************/

byte font[]={
//SPACE
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // !

  //SPACE
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // !
0x04,
0x04,
0x04,
0x04,
0x00,
0x00,
0x04,
0x00,
  // "
0x0A,
0x0A,
0x0A,
0x00,
0x00,
0x00,
0x00,
0x00,
  // #
0x0A,
0x0A,
0x1F,
0x0A,
0x1F,
0x0A,
0x0A,
0x00,
  // $
0x04,
0x0F,
0x14,
0x0E,
0x05,
0x1E,
0x04,
0x00,
  // %
0x18,
0x19,
0x02,
0x04,
0x08,
0x13,
0x03,
0x00,
  // &
0x0C,
0x12,
0x14,
0x08,
0x15,
0x12,
0x0D,
0x00,
  // '
0x0C,
0x04,
0x08,
0x00,
0x00,
0x00,
0x00,
0x00,
  // (
0x02,
0x04,
0x08,
0x08,
0x08,
0x04,
0x02,
0x00,
  // )
0x08,
0x04,
0x02,
0x02,
0x02,
0x04,
0x08,
0x00,
  // *
0x00,
0x04,
0x15,
0x0E,
0x15,
0x04,
0x00,
0x00,
  // +
0x00,
0x04,
0x04,
0x1F,
0x04,
0x04,
0x00,
0x00,
  // ,
0x00,
0x00,
0x00,
0x00,
0x0C,
0x04,
0x08,
0x00,
  // -
0x00,
0x00,
0x00,
0x1F,
0x00,
0x00,
0x00,
0x00,
  // .
0x00,
0x00,
0x00,
0x00,
0x00,
0x0C,
0x0C,
0x00,
  // /
0x00,
0x01,
0x02,
0x04,
0x08,
0x10,
0x00,
0x00,
  // 0
0x0E,
0x11,
0x13,
0x15,
0x19,
0x11,
0x0E,
0x00,
  // 1
0x04,
0x0C,
0x04,
0x04,
0x04,
0x04,
0x0E,
0x00,
  // 2
0x0E,
0x11,
0x01,
0x02,
0x04,
0x08,
0x1F,
0x00,
  // 3
0x1F,
0x02,
0x04,
0x02,
0x01,
0x11,
0x0E,
0x00,
  // 4
0x02,
0x06,
0x0A,
0x12,
0x1F,
0x02,
0x02,
0x00,
  // 5
0x1F,
0x10,
0x1E,
0x01,
0x01,
0x11,
0x0E,
0x00,
  // 6
0x06,
0x08,
0x10,
0x1E,
0x11,
0x11,
0x0E,
0x00,
  // 7
0x1F,
0x01,
0x02,
0x04,
0x04,
0x04,
0x04,
0x00,
  // 8
0x1E,
0x11,
0x11,
0x0E,
0x11,
0x11,
0x0E,
0x00,
  // 9
0x0E,
0x11,
0x11,
0x0F,
0x01,
0x02,
0x0C,
0x00,
  // :
0x00,
0x0C,
0x0C,
0x00,
0x0C,
0x0C,
0x00,
0x00,
  // ;
0x00,
0x0C,
0x0C,
0x00,
0x0C,
0x04,
0x08,
0x00,
  // <
0x02,
0x04,
0x08,
0x10,
0x08,
0x04,
0x02,
0x00,
  // =
0x00,
0x00,
0x1F,
0x00,
0x1F,
0x00,
0x00,
0x00,
  // >
0x08,
0x04,
0x02,
0x01,
0x02,
0x04,
0x08,
0x00,
  // ?
0x0E,
0x11,
0x01,
0x02,
0x04,
0x00,
0x04,
0x00,
  // @
0x0E,
0x11,
0x01,
0x0D,
0x15,
0x15,
0x0E,
0x00,
  // A
0x0E,
0x11,
0x11,
0x11,
0x1F,
0x11,
0x11,
0x00,
  // B
0x1E,
0x09,
0x09,
0x0E,
0x09,
0x09,
0x1E,
0x00,
  // C
0x0E,
0x11,
0x10,
0x10,
0x10,
0x11,
0x0E,
0x00,
  // D
0x1E,
0x09,
0x09,
0x09,
0x09,
0x09,
0x1E,
0x00,
  // E
0x1F,
0x10,
0x10,
0x1F,
0x10,
0x10,
0x1F,
0x00,
  // F
0x1F,
0x10,
0x10,
0x1E,
0x10,
0x10,
0x10,
0x00,
  // G
0x0E,
0x11,
0x10,
0x13,
0x11,
0x11,
0x0F,
0x00,
  // H
0x11,
0x11,
0x11,
0x1F,
0x11,
0x11,
0x11,
0x00,
  // I
0x0E,
0x04,
0x04,
0x04,
0x04,
0x04,
0x0E,
0x00,
  // J
0x07,
0x02,
0x02,
0x02,
0x02,
0x12,
0x0C,
0x00,
  // K
0x11,
0x12,
0x14,
0x18,
0x14,
0x12,
0x11,
0x00,
  // L
0x10,
0x10,
0x10,
0x10,
0x10,
0x10,
0x1F,
0x00,
  // M
0x11,
0x1B,
0x15,
0x15,
0x11,
0x11,
0x11,
0x00,
  // N
0x11,
0x19,
0x19,
0x15,
0x13,
0x13,
0x11,
0x00,
  // O
0x0E,
0x11,
0x11,
0x11,
0x11,
0x11,
0x0E,
0x00,
  // P
0x1E,
0x11,
0x11,
0x1E,
0x10,
0x10,
0x10,
0x00,
  // Q
0x0E,
0x11,
0x11,
0x11,
0x15,
0x12,
0x1D,
0x00,
  // R
0x1E,
0x11,
0x11,
0x1E,
0x14,
0x12,
0x11,
0x00,
  // S
0x0E,
0x11,
0x10,
0x0E,
0x01,
0x11,
0x0E,
0x00,
  // T
0x1F,
0x04,
0x04,
0x04,
0x04,
0x04,
0x04,
0x00,
  // U
0x11,
0x11,
0x11,
0x11,
0x11,
0x11,
0x0E,
0x00,
  // V
0x11,
0x11,
0x11,
0x11,
0x11,
0x0A,
0x04,
0x00,
  // W
0x11,
0x11,
0x11,
0x15,
0x15,
0x1B,
0x11,
0x00,
  // X
0x11,
0x11,
0x0A,
0x04,
0x0A,
0x11,
0x11,
0x00,
  // Y
0x11,
0x11,
0x11,
0x0A,
0x04,
0x04,
0x04,
0x00,
  // Z
0x1F,
0x01,
0x02,
0x04,
0x08,
0x10,
0x1F,
0x00,
  // [
0x0E,
0x08,
0x08,
0x08,
0x08,
0x08,
0x0E,
0x00,

////////////
//0x00,

  // \
 
0x00,
0x10,
0x08,
0x04,
0x02,
0x01,
0x00,
0x00,
  // ]
0x0E,
0x02,
0x02,
0x02,
0x02,
0x02,
0x0E,
0x00,
  // ^
0x04,
0x0A,
0x11,
0x00,
0x00,
0x00,
0x00,
0x00,
  // _
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x1F,
0x00,
  // `
0x10,
0x08,
0x04,
0x00,
0x00,
0x00,
0x00,
0x00,
  // a
0x00,
0x00,
0x0E,
0x01,
0x0F,
0x11,
0x0F,
0x00,
  // b
0x10,
0x10,
0x16,
0x19,
0x11,
0x11,
0x1E,
0x00,
  // c
0x00,
0x00,
0x0E,
0x11,
0x10,
0x11,
0x0E,
0x00,
  // d
0x01,
0x01,
0x0D,
0x13,
0x11,
0x11,
0x0F,
0x00,
  // e
0x00,
0x00,
0x0E,
0x11,
0x1F,
0x10,
0x0E,
0x00,
  // f
0x02,
0x05,
0x04,
0x0E,
0x04,
0x04,
0x04,
0x00,
  // g
0x00,
0x00,
0x0D,
0x13,
0x13,
0x0D,
0x01,
0x0E,
  // h
0x10,
0x10,
0x16,
0x19,
0x11,
0x11,
0x11,
0x00,
  // i
0x04,
0x00,
0x0C,
0x04,
0x04,
0x04,
0x0E,
0x00,
  // j
0x02,
0x00,
0x06,
0x02,
0x02,
0x12,
0x0C,
0x00,
  // k
0x08,
0x08,
0x09,
0x0A,
0x0C,
0x0A,
0x09,
0x00,
  // l
0x0C,
0x04,
0x04,
0x04,
0x04,
0x04,
0x0E,
0x00,
  // m
0x00,
0x00,
0x1A,
0x15,
0x15,
0x15,
0x15,
0x00,
  // n
0x00,
0x00,
0x16,
0x19,
0x11,
0x11,
0x11,
0x00,
  // o
0x00,
0x00,
0x0E,
0x11,
0x11,
0x11,
0x0E,
0x00,
  // p
0x00,
0x00,
0x16,
0x19,
0x19,
0x16,
0x10,
0x10,
  // q
0x00,
0x00,
0x0D,
0x13,
0x13,
0x0D,
0x01,
0x01,
  // r
0x00,
0x00,
0x16,
0x19,
0x10,
0x10,
0x10,
0x00,
  // s
0x00,
0x00,
0x0F,
0x10,
0x1E,
0x01,
0x1F,
0x00,
  // t
0x08,
0x08,
0x1C,
0x08,
0x08,
0x09,
0x06,
0x00,
  // u
0x00,
0x00,
0x12,
0x12,
0x12,
0x12,
0x0D,
0x00,
  // v
0x00,
0x00,
0x11,
0x11,
0x11,
0x0A,
0x04,
0x00,
  // w
0x00,
0x00,
0x11,
0x11,
0x15,
0x15,
0x0A,
0x00,
  // x
0x00,
0x00,
0x11,
0x0A,
0x04,
0x0A,
0x11,
0x00,
  // y
0x00,
0x00,
0x11,
0x11,
0x13,
0x0D,
0x01,
0x0E,
  // z
0x00,
0x00,
0x1F,
0x02,
0x04,
0x08,
0x1F,
0x00,
  // 
0x02,
0x04,
0x04,
0x08,
0x04,
0x04,
0x02,
0x00,
  // |
0x04,
0x04,
0x04,
0x00,
0x04,
0x04,
0x04,
0x00,
  // 
0x08,
0x04,
0x04,
0x02,
0x04,
0x04,
0x08,
0x00,
  // ~
0x00,
0x00,
0x08,
0x15,
0x02,
0x00,
0x00,
0x00,
  // 5F
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 60
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 61
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 62
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 63
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 64
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 65
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 66
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 67
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 68
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 69
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 6A
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 6B
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 6C
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 6D
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 6E
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 6F
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 70
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 71
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 72
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 73
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 74
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 75
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 76
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 77
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 78
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 79
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 7A
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 7B
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 7C
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 7D
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 7E
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
  // 7F
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00
};