Jump to content

LED Panel 30x7: Difference between revisions

From base48
Buger (talk | contribs)
m more info...
Buger (talk | contribs)
m incl code
Line 28: Line 28:


After some polishing here is the firmware that:
After some polishing here is the firmware that:
- on powerup shows the device address for 3 seconds
* on power up it reads device address from EEPROM address 0
- then screen is cleared and it waits for serial data (115200, 8N1)
* shows the device address for 3 seconds on the screen
- it ignores all data until device address is received
* then screen is cleared and it waits for serial data (115200, 8N1)
- after it receives its address, it starts to receive graphics data
* it ignores all data until device address is received
- because each colum of display consists of 7 LEDs, I decided that all bytes with 0 in the most significant bit (0x0-0x7F) will represent graphics data (simply each bit corresponds to LED in the column) and all with 1 in this bit (0x80-0xFF) will represent device address
* after it receives its address, it starts to receive graphics data and resets the column counter
- therefore the device address needs to be in 0x80-0xFF range, this way we can handle up to 128 devices on the same bus
* because each colum of display consists of 7 LEDs, I decided that all bytes with 0 in the most significant bit (0x0-0x7F) will represent graphics data (simply each bit corresponds to LED in the column) and all with 1 in this bit (0x80-0xFF) will represent device address
- after each received graphics byte column counter is incremented
* therefore the device address needs to be in 0x80-0xFF range, this way we can handle up to 128 devices on the same bus
- therefore if the device address its for example 0x80, you need to send 0x80, 0x7F, 0x7F, ... and 28 more 0x7Fs to make all LEDs on the panel lid up
* after each received graphics byte column counter is incremented
* therefore if the device address its for example 0x80, you need to send 0x80, 0x7F, 0x7F, ... and 28 more 0x7Fs to make all LEDs on the panel lid up
* the device ignores all bytes >=0x80 if they are not its device address
* the device ignores all graphics data if end of display was reached (means all 30 columns received)
<syntaxhighlight lang="arduino">
#include <SPI.h>
#include <EEPROM.h>
 
// device address is stored in eeprom on address 0 and should be >=0x80
// to change it run "avrdude -c usbasp -p m16 -U eeprom:w:0x80:m" and replace 0x80 with your address
 
// graphics data = all bytes from serial < 0x80 (as we have only 7 LEDs)
// when device address is received on serial = start receiving graphics data to display buffer
 
// =====================================================
// Konfigurace pinů – ATmega16, MightyCore std pipnout
// =====================================================
 
const uint8_t PIN_LATCH = 21;  // PC5
const uint8_t PIN_OE    = 20;  // PC4
const uint8_t PIN_S0    = 16;  // PC0
const uint8_t PIN_S1    = 17;  // PC1
const uint8_t PIN_S2    = 18;  // PC2
const uint8_t PIN_EN    = 19;  // PC3
 
const uint32_t SPI_SPEED = 16000000;  //drivery by mely fungovat do 20MHz
 
// =====================================================
// Parametry displeje
// =====================================================
 
const uint8_t WIDTH  = 30;
const uint8_t HEIGHT = 7;
 
const uint32_t ROW_TIME_US = 1000;
 
const uint8_t DECODER_ENABLED  = LOW;
const uint8_t DECODER_DISABLED = HIGH;
const uint8_t OUTPUTS_ENABLED  = LOW;
const uint8_t OUTPUTS_DISABLED = HIGH;
 
// =====================================================
// Sériová komunikace
// =====================================================
 
const uint32_t SERIAL_BAUD = 115200;
 
// =====================================================
// Obrazová paměť
//
// buffer[row][column]
// =====================================================
 
bool buffer[HEIGHT][WIDTH];
 
// Index následujícího zapisovaného sloupce.
// WIDTH znamená, že je buffer plný.
 
uint8_t writeColumn = 0;
 
uint8_t devAddr = 80;
uint32_t startupDisplayTime = 0;
bool startupDisplayActive = true;
 
// =====================================================
// Font 3x5 pro úvodní zobrazení adresy
// =====================================================
 
// Každý znak má 3 sloupce, každý sloupec obsahuje 5 bitů.
// Bit 0 = horní pixel.
 
const uint8_t font3x5[][3] = {
  {0x1F, 0x11, 0x1F}, // 0
  {0x00, 0x1F, 0x00}, // 1
  {0x1D, 0x15, 0x17}, // 2
  {0x15, 0x15, 0x1F}, // 3
  {0x07, 0x04, 0x1F}, // 4
  {0x17, 0x15, 0x1D}, // 5
  {0x1F, 0x15, 0x1D}, // 6
  {0x01, 0x01, 0x1F}, // 7
  {0x1F, 0x15, 0x1F}, // 8
  {0x17, 0x15, 0x1F}, // 9
  {0x1E, 0x05, 0x1E}, // A
  {0x1F, 0x15, 0x0A}, // B
  {0x0E, 0x11, 0x11}, // C
  {0x1F, 0x11, 0x0E}, // D
  {0x1F, 0x15, 0x11}, // E
  {0x1F, 0x05, 0x01}, // F
  {0x14, 0x08, 0x14}  // x
};
 
// Vymazání obrazového bufferu.
void clearBuffer() {
  memset(buffer, 0, sizeof(buffer));
}
 
// Vykreslení jednoho znaku 3x5.
void drawChar(uint8_t index, uint8_t x, uint8_t y) {
  for (uint8_t col = 0; col < 3; col++) {
    uint8_t bits = font3x5[index][col];
 
    for (uint8_t row = 0; row < 5; row++) {
      if (x + col < WIDTH && y + row < HEIGHT) {
        buffer[y + row][x + col] = (bits >> row) & 1;
      }
    }
  }
}
 
// Zobrazení adresy, například 0x83.
void showDeviceAddress() {
  clearBuffer();
 
  char text[5];
  snprintf(text, sizeof(text), "%02X", devAddr);
 
  // Text tvoří 0, x a dvě hexadecimální číslice.
  // Šířka: 4 znaky * 3 pixely + 3 mezery = 15 pixelů.
  // Začátek je vypočítán pro vodorovné vycentrování.
  uint8_t x = (WIDTH - 15) / 2;
  uint8_t y = (HEIGHT - 5) / 2;
 
  drawChar(0, x, y);  // 0
  x += 4;
 
  drawChar(16, x, y);  // x
  x += 4;
 
  for (uint8_t i = 0; i < 2; i++) {
    char c = text[i];
 
    uint8_t index;
    if (c >= '0' && c <= '9') {
      index = c - '0';
    } else {
      index = c - 'A' + 10;
    }
 
    drawChar(index, x, y);
    x += 4;
  }
}
 
// =====================================================
// Příjem dat ze sériového portu
// =====================================================
 
void processSerial() {
 
  while (Serial.available() > 0) {
 
    uint8_t data = Serial.read();
 
    // Bajty s nejvyšším bitem 1 jsou příkazy.
    if (data & 0x80) {
 
      // Jediný podporovaný příkaz: reset adresy zápisu pokud je prijat devAddr.
      if (data == devAddr) {
        writeColumn = 0;
      }
 
      // Ostatní ignorovat
      continue;
    }
 
    // Pokud je buffer plný, další data ignorovat.
    if (writeColumn >= WIDTH) {
      continue;
    }
 
    // Zapsat všech 7 bitů do aktuálního sloupce.
    for (uint8_t row = 0; row < HEIGHT; row++) {
 
      buffer[row][writeColumn] = (data >> row) & 1;
    }
 
    // Připravit následující sloupec.
    writeColumn++;
  }
}
 
// =====================================================
// Výběr řádku přes 74LS138
// =====================================================
 
void selectRow(uint8_t row) {
 
  digitalWrite(PIN_S0, (row & 0x01) ? HIGH : LOW);
  digitalWrite(PIN_S1, (row & 0x02) ? HIGH : LOW);
  digitalWrite(PIN_S2, (row & 0x04) ? HIGH : LOW);
}
 
// =====================================================
// Zápis sloupců do dvou A6276E
// =====================================================
 
void writeColumns(uint8_t row) {
 
  uint32_t word = 0;
 
  for (uint8_t col = 0; col < WIDTH; col++) {
 
    if (buffer[row][col]) {
      word |= (1UL << col);
    }
  }
 
  digitalWrite(PIN_LATCH, LOW);
 
  SPI.transfer((word >> 24) & 0xFF);
  SPI.transfer((word >> 16) & 0xFF);
  SPI.transfer((word >> 8)  & 0xFF);
  SPI.transfer(word & 0xFF);
 
  digitalWrite(PIN_LATCH, HIGH);
}
 
// =====================================================
// Multiplexní obsluha displeje
// =====================================================
 
uint8_t currentRow = 0;
uint32_t lastRowChange = 0;
 
void refreshDisplay() {
 
  uint32_t now = micros();
 
  if ((uint32_t)(now - lastRowChange) < ROW_TIME_US) {
    return;
  }
 
  lastRowChange = now;
 
  // 1. Zhasnout výstupy během změny řádku.
  digitalWrite(PIN_OE, OUTPUTS_DISABLED);
 
  // 2. Zakázat dekodér.
  digitalWrite(PIN_EN, DECODER_DISABLED);
 
  // 3. Zapsat data pro aktuální řádek.
  writeColumns(currentRow);
 
  // 4. Vybrat řádek.
  selectRow(currentRow);
 
  // 5. Povolit řádek.
  digitalWrite(PIN_EN, DECODER_ENABLED);
 
  // 6. Povolit výstupy LED driverů.
  digitalWrite(PIN_OE, OUTPUTS_ENABLED);
 
  // 7. Připravit následující řádek.
  currentRow++;
 
  if (currentRow >= HEIGHT) {
    currentRow = 0;
  }
}
 
// =====================================================
// Inicializace
// =====================================================
 
void setup() {
  // Sériová komunikace 115200 baud.
  Serial.begin(SERIAL_BAUD);
 
  // SPI pro LED drivery.
  SPI.begin();
 
  SPI.beginTransaction(
    SPISettings(SPI_SPEED, MSBFIRST, SPI_MODE0)
  );
 
  // Konfigurace pinů.
  pinMode(PIN_LATCH, OUTPUT);
  pinMode(PIN_OE, OUTPUT);
  pinMode(PIN_S0, OUTPUT);
  pinMode(PIN_S1, OUTPUT);
  pinMode(PIN_S2, OUTPUT);
  pinMode(PIN_EN, OUTPUT);
 
  // Displej při inicializaci zhasnout.
  digitalWrite(PIN_OE, OUTPUTS_DISABLED);
  digitalWrite(PIN_EN, DECODER_DISABLED);
  digitalWrite(PIN_LATCH, HIGH);
  digitalWrite(PIN_S0, LOW);
  digitalWrite(PIN_S1, LOW);
  digitalWrite(PIN_S2, LOW);
 
  // buffer pointer je po resetu nastaven na WIDTH aby se ignorovala data dokud nedojde odpovidajici devaddr
  writeColumn = WIDTH;
 
  // Okamžitá první obnova displeje.
  currentRow = 0;
  lastRowChange = micros() - ROW_TIME_US;
 
  // Adresa zarizeni z EEPROM z adresy 0
  devAddr = EEPROM.read(0) | 0x80; //nacist adresu z eeprom a ujistit se ze bude >=0x80
 
  showDeviceAddress();
 
  startupDisplayTime = millis();
  startupDisplayActive = true;
}
 
// =====================================================
// Hlavní smyčka
// =====================================================
 
void loop() {
 
  if (!startupDisplayActive) {
    processSerial();
  } else if ((uint32_t)(millis() - startupDisplayTime) >= 3000) {
    // Po 3 sekundách odstranit uvidni screen s adresou a zacit prijimat data
    startupDisplayActive = false;
    clearBuffer();   
  }
 
  // obnovovat displej
  refreshDisplay();
}
 
</syntaxhighlight>

Revision as of 23:51, 28 October 2026


What is this about

This project is about to make these LED panels working. We got them for free including power supplies and some cables, however the communication protocol is unknown.

To be able to use them we need to find out the protocol, or make our own firmware.

Hack the protocol!

The device is equipped with pair of XLR connectors connected in parallel suggesting use of DMX or RS485 protocol with daisy-chaining in mind, also the vendor (https://www.srs-group.com) is making a lot of DMX stuff, however I tried to send it DMX data on all 512 channels but no luck. I also tried to send it RS485 data in various formats, baudrates, etc - nothing worked.

I also tried to read the firmware of the main controller (atmega16) but its locked (as usual).

So I deciced to go "my firmware" way...

Make our own firmware

To be able to make our own firmware we need to know how this works - so I spent couple of hours reverse engineering the control board so we have the schematic

LED & driver layout
LED & driver layout

Then I used money of the US tax payers (free chatgpt AI) to make the firmware for atmega16 in arduino with the MightyCore atmega16 support. I told him where and how are the shift registers and row decoders connected, his first task was just to display "base48" text... He did it... Then I asked him to add very simple serial protocol that whill wait for device address and then starts to receive graphics data column by column... And that worked too.

After some polishing here is the firmware that:

  • on power up it reads device address from EEPROM address 0
  • shows the device address for 3 seconds on the screen
  • then screen is cleared and it waits for serial data (115200, 8N1)
  • it ignores all data until device address is received
  • after it receives its address, it starts to receive graphics data and resets the column counter
  • because each colum of display consists of 7 LEDs, I decided that all bytes with 0 in the most significant bit (0x0-0x7F) will represent graphics data (simply each bit corresponds to LED in the column) and all with 1 in this bit (0x80-0xFF) will represent device address
  • therefore the device address needs to be in 0x80-0xFF range, this way we can handle up to 128 devices on the same bus
  • after each received graphics byte column counter is incremented
  • therefore if the device address its for example 0x80, you need to send 0x80, 0x7F, 0x7F, ... and 28 more 0x7Fs to make all LEDs on the panel lid up
  • the device ignores all bytes >=0x80 if they are not its device address
  • the device ignores all graphics data if end of display was reached (means all 30 columns received)
#include <SPI.h>
#include <EEPROM.h>

// device address is stored in eeprom on address 0 and should be >=0x80
// to change it run "avrdude -c usbasp -p m16 -U eeprom:w:0x80:m" and replace 0x80 with your address

// graphics data = all bytes from serial < 0x80 (as we have only 7 LEDs)
// when device address is received on serial = start receiving graphics data to display buffer

// =====================================================
// Konfigurace pinů – ATmega16, MightyCore std pipnout
// =====================================================

const uint8_t PIN_LATCH = 21;  // PC5
const uint8_t PIN_OE    = 20;  // PC4
const uint8_t PIN_S0    = 16;  // PC0
const uint8_t PIN_S1    = 17;  // PC1
const uint8_t PIN_S2    = 18;  // PC2
const uint8_t PIN_EN    = 19;  // PC3

const uint32_t SPI_SPEED = 16000000;  //drivery by mely fungovat do 20MHz

// =====================================================
// Parametry displeje
// =====================================================

const uint8_t WIDTH  = 30;
const uint8_t HEIGHT = 7;

const uint32_t ROW_TIME_US = 1000;

const uint8_t DECODER_ENABLED  = LOW;
const uint8_t DECODER_DISABLED = HIGH;
const uint8_t OUTPUTS_ENABLED  = LOW;
const uint8_t OUTPUTS_DISABLED = HIGH;

// =====================================================
// Sériová komunikace
// =====================================================

const uint32_t SERIAL_BAUD = 115200;

// =====================================================
// Obrazová paměť
//
// buffer[row][column]
// =====================================================

bool buffer[HEIGHT][WIDTH];

// Index následujícího zapisovaného sloupce.
// WIDTH znamená, že je buffer plný.

uint8_t writeColumn = 0;

uint8_t devAddr = 80;
uint32_t startupDisplayTime = 0;
bool startupDisplayActive = true;

// =====================================================
// Font 3x5 pro úvodní zobrazení adresy
// =====================================================

// Každý znak má 3 sloupce, každý sloupec obsahuje 5 bitů.
// Bit 0 = horní pixel.

const uint8_t font3x5[][3] = {
  {0x1F, 0x11, 0x1F}, // 0
  {0x00, 0x1F, 0x00}, // 1
  {0x1D, 0x15, 0x17}, // 2
  {0x15, 0x15, 0x1F}, // 3
  {0x07, 0x04, 0x1F}, // 4
  {0x17, 0x15, 0x1D}, // 5
  {0x1F, 0x15, 0x1D}, // 6
  {0x01, 0x01, 0x1F}, // 7
  {0x1F, 0x15, 0x1F}, // 8
  {0x17, 0x15, 0x1F}, // 9
  {0x1E, 0x05, 0x1E}, // A
  {0x1F, 0x15, 0x0A}, // B
  {0x0E, 0x11, 0x11}, // C
  {0x1F, 0x11, 0x0E}, // D
  {0x1F, 0x15, 0x11}, // E
  {0x1F, 0x05, 0x01}, // F
  {0x14, 0x08, 0x14}  // x
};

// Vymazání obrazového bufferu.
void clearBuffer() {
  memset(buffer, 0, sizeof(buffer));
}

// Vykreslení jednoho znaku 3x5.
void drawChar(uint8_t index, uint8_t x, uint8_t y) {
  for (uint8_t col = 0; col < 3; col++) {
    uint8_t bits = font3x5[index][col];

    for (uint8_t row = 0; row < 5; row++) {
      if (x + col < WIDTH && y + row < HEIGHT) {
        buffer[y + row][x + col] = (bits >> row) & 1;
      }
    }
  }
}

// Zobrazení adresy, například 0x83.
void showDeviceAddress() {
  clearBuffer();

  char text[5];
  snprintf(text, sizeof(text), "%02X", devAddr);

  // Text tvoří 0, x a dvě hexadecimální číslice.
  // Šířka: 4 znaky * 3 pixely + 3 mezery = 15 pixelů.
  // Začátek je vypočítán pro vodorovné vycentrování.
  uint8_t x = (WIDTH - 15) / 2;
  uint8_t y = (HEIGHT - 5) / 2;

  drawChar(0, x, y);  // 0
  x += 4;

  drawChar(16, x, y);  // x
  x += 4;

  for (uint8_t i = 0; i < 2; i++) {
    char c = text[i];

    uint8_t index;
    if (c >= '0' && c <= '9') {
      index = c - '0';
    } else {
      index = c - 'A' + 10;
    }

    drawChar(index, x, y);
    x += 4;
  }
}

// =====================================================
// Příjem dat ze sériového portu
// =====================================================

void processSerial() {

  while (Serial.available() > 0) {

    uint8_t data = Serial.read();

    // Bajty s nejvyšším bitem 1 jsou příkazy.
    if (data & 0x80) {

      // Jediný podporovaný příkaz: reset adresy zápisu pokud je prijat devAddr.
      if (data == devAddr) {
        writeColumn = 0;
      }

      // Ostatní ignorovat
      continue;
    }

    // Pokud je buffer plný, další data ignorovat.
    if (writeColumn >= WIDTH) {
      continue;
    }

    // Zapsat všech 7 bitů do aktuálního sloupce.
    for (uint8_t row = 0; row < HEIGHT; row++) {

      buffer[row][writeColumn] = (data >> row) & 1;
    }

    // Připravit následující sloupec.
    writeColumn++;
  }
}

// =====================================================
// Výběr řádku přes 74LS138
// =====================================================

void selectRow(uint8_t row) {

  digitalWrite(PIN_S0, (row & 0x01) ? HIGH : LOW);
  digitalWrite(PIN_S1, (row & 0x02) ? HIGH : LOW);
  digitalWrite(PIN_S2, (row & 0x04) ? HIGH : LOW);
}

// =====================================================
// Zápis sloupců do dvou A6276E
// =====================================================

void writeColumns(uint8_t row) {

  uint32_t word = 0;

  for (uint8_t col = 0; col < WIDTH; col++) {

    if (buffer[row][col]) {
      word |= (1UL << col);
    }
  }

  digitalWrite(PIN_LATCH, LOW);

  SPI.transfer((word >> 24) & 0xFF);
  SPI.transfer((word >> 16) & 0xFF);
  SPI.transfer((word >> 8)  & 0xFF);
  SPI.transfer(word & 0xFF);

  digitalWrite(PIN_LATCH, HIGH);
}

// =====================================================
// Multiplexní obsluha displeje
// =====================================================

uint8_t currentRow = 0;
uint32_t lastRowChange = 0;

void refreshDisplay() {

  uint32_t now = micros();

  if ((uint32_t)(now - lastRowChange) < ROW_TIME_US) {
    return;
  }

  lastRowChange = now;

  // 1. Zhasnout výstupy během změny řádku.
  digitalWrite(PIN_OE, OUTPUTS_DISABLED);

  // 2. Zakázat dekodér.
  digitalWrite(PIN_EN, DECODER_DISABLED);

  // 3. Zapsat data pro aktuální řádek.
  writeColumns(currentRow);

  // 4. Vybrat řádek.
  selectRow(currentRow);

  // 5. Povolit řádek.
  digitalWrite(PIN_EN, DECODER_ENABLED);

  // 6. Povolit výstupy LED driverů.
  digitalWrite(PIN_OE, OUTPUTS_ENABLED);

  // 7. Připravit následující řádek.
  currentRow++;

  if (currentRow >= HEIGHT) {
    currentRow = 0;
  }
}

// =====================================================
// Inicializace
// =====================================================

void setup() {
  // Sériová komunikace 115200 baud.
  Serial.begin(SERIAL_BAUD);

  // SPI pro LED drivery.
  SPI.begin();

  SPI.beginTransaction(
    SPISettings(SPI_SPEED, MSBFIRST, SPI_MODE0)
  );

  // Konfigurace pinů.
  pinMode(PIN_LATCH, OUTPUT);
  pinMode(PIN_OE, OUTPUT);
  pinMode(PIN_S0, OUTPUT);
  pinMode(PIN_S1, OUTPUT);
  pinMode(PIN_S2, OUTPUT);
  pinMode(PIN_EN, OUTPUT);

  // Displej při inicializaci zhasnout.
  digitalWrite(PIN_OE, OUTPUTS_DISABLED);
  digitalWrite(PIN_EN, DECODER_DISABLED);
  digitalWrite(PIN_LATCH, HIGH);
  digitalWrite(PIN_S0, LOW);
  digitalWrite(PIN_S1, LOW);
  digitalWrite(PIN_S2, LOW);

  // buffer pointer je po resetu nastaven na WIDTH aby se ignorovala data dokud nedojde odpovidajici devaddr
  writeColumn = WIDTH;

  // Okamžitá první obnova displeje.
  currentRow = 0;
  lastRowChange = micros() - ROW_TIME_US;

  // Adresa zarizeni z EEPROM z adresy 0
  devAddr = EEPROM.read(0) | 0x80; //nacist adresu z eeprom a ujistit se ze bude >=0x80

  showDeviceAddress();

  startupDisplayTime = millis();
  startupDisplayActive = true;
}

// =====================================================
// Hlavní smyčka
// =====================================================

void loop() {

  if (!startupDisplayActive) {
    processSerial();
  } else if ((uint32_t)(millis() - startupDisplayTime) >= 3000) {
    // Po 3 sekundách odstranit uvidni screen s adresou a zacit prijimat data
    startupDisplayActive = false;
    clearBuffer();    
  }

  // obnovovat displej
  refreshDisplay();
}