Jump to content

LED Panel 30x7: Difference between revisions

From base48
Buger (talk | contribs)
first usable version
Buger (talk | contribs)
mNo edit summary
Line 638: Line 638:


== Flash the FW into the panel ==
== Flash the FW into the panel ==
You need programmer able to program atmega16 MCUs<code> in DIP40 packages, luckily there is ISP header on the control borad, so I used cheap USBasp to do that.
You need programmer able to program atmega16 MCUs in DIP40 packages, luckily there is ISP header on the control borad, so I used cheap USBasp to do that.


!!! THE ISP HEADER USE NON-STANDARD PINOUT - DO NOT PLUG STANDARD USBASP CABLE, YOU NEED TO PIN THESE ACCORDING TO SCHEMATIC OTHERWISE USBASP POWER WILL BE SHORTED !!!
!!! THE ISP HEADER USE NON-STANDARD PINOUT - DO NOT PLUG STANDARD USBASP CABLE, YOU NEED TO PIN THESE ACCORDING TO SCHEMATIC OTHERWISE USBASP POWER WILL BE SHORTED !!!

Revision as of 00:37, 29 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();
}

You need MightyCore in your arduino https://github.com/MCUdude/Mightycore and here is the arduino configuration

Also here is the compiled firmware in .hex (sorry wiki does not let me to upload is as file...

:100000000C9465000C948D000C948D000C948D0064
:100010000C948D000C948D000C948D000C948D002C
:100020000C948D000C94A9020C948D000C94770212
:100030000C9451020C948D000C948D000C948D0046
:100040000C948D000C948D000C948D000C948D00FC
:100050000C948D0000000002000000000000000071
:100060000504000700000000000000000000000080
:100070000000000000003B003800350032000000A6
:100080003A0037003400310002020202020202028A
:100090000404040404040404030303030303030328
:1000A0000101010101010101010204081020408049
:1000B0000102040810204080010204081020408042
:1000C0000102040810204080FB0411241FBECFE56C
:1000D000D4E0DEBFCDBF10E0A0E6B0E0E6E3F0E1A3
:1000E00002C005900D92AC3AB107D9F722E0ACEA14
:1000F000B0E001C01D92AF32B207E1F710E0C5E6F3
:10010000D0E004C02197FE010E94E307C436D10766
:10011000C9F70E94F3020C9419080C940000CF93C5
:10012000DF9323E0829FC001112430E020E070E0E3
:10013000635A7E4FF901E80FF91FEC58FF4FC08159
:10014000FB01E20FF31F50E040E0D0E0DE01042E9F
:1001500002C0B595A7950A94E2F7A170A68F4F5FEC
:100160005F4F7E964530510589F72F5F3F4F233013
:10017000310501F7DF91CF91089590E0FC01E8553A
:10018000FF4F249188579F4FFC018491882399F059
:1001900090E0880F991FFC01E258FF4FA591B491A0
:1001A000FC01EC58FF4F859194918FB7F894EC9136
:1001B000E22BEC938FBF0895AF92BF92CF92DF9264
:1001C000EF92FF920F931F93CF93DF936C017B010C
:1001D0008B01040F151FEB015E01AE18BF08C0179D
:1001E000D10759F06991D601ED91FC910190F08110
:1001F000E02DC6010995892B79F7C501DF91CF91D3
:100200001F910F91FF90EF90DF90CF90BF90AF9034
:100210000895FC01538D448D252F30E0842F90E00C
:10022000821B930B541710F0CF96089501970895F1
:10023000FC01918D828D981761F0A28DAE0FBF2FBA
:10024000B11D5D968C91928D9F5F9F73928F90E010
:1002500008958FEF9FEF0895FC01918D828D98177F
:1002600031F0828DE80FF11D858D90E008958FEFBC
:100270009FEF0895FC01918D228D892F90E0805C85
:100280009F4F821B91098F739927089583EB90E00C
:100290000E943A0121E0892B09F420E0822F089581
:1002A00080E090E0892B29F00E94460181110C9496
:1002B00000000895FC01A48DA80FB92FB11DA35A09
:1002C000BF4F2C91848D90E001968F739927848F76
:1002D000A689B7892C93A089B1898C9182748C935B
:1002E000938D848D981306C00288F389E02D808158
:1002F0008F7D80830895EF92FF920F931F93CF938A
:10030000DF93EC0181E0888F9B8D8C8D981319C051
:10031000E889F989808185FF14C09FB7F894EE8938
:10032000FF896083E889F9898081827480839FBF17
:1003300081E090E0DF91CF911F910F91FF90EF90BE
:100340000895F62E0B8D10E00F5F1F4F0F731127CE
:10035000E02E8C8D8E110CC00FB607FCFACFE88909
:10036000F989808185FFF5CFCE010E945A01F1CF36
:10037000EB8DEC0FFD2FF11DE35AFF4FF0829FB77D
:10038000F8940B8FEA89FB8980818062CFCFCF936D
:10039000DF93EC01888D8823B9F0AA89BB89E889AD
:1003A000F9898C9185FD03C0808186FD0DC00FB653
:1003B00007FCF7CF8C9185FFF2CF808185FFEDCFD1
:1003C000CE010E945A01E9CFDF91CF91089590E0CC
:1003D000FC01EC5AFF4F2491FC01E855FF4F34918A
:1003E000FC01E857FF4FE491EE23C9F0222339F0D6
:1003F0002430F1F0A8F4213011F1223001F1F0E0C5
:10040000EE0FFF1FEC58FF4FA591B4918FB7F894F2
:10041000EC91611118C030953E233C938FBF089535
:10042000253051F0273059F785B58F7D85BDE7CF51
:100430008FB58F778FBDE3CF8FB58F7DFBCF83B720
:100440008F7D83BFDCCF3E2BE8CF3FB7F894809100
:10045000AD009091AE00A091AF00B091B00022B776
:1004600008B600FE05C02F3F19F00196A11DB11D71
:100470003FBFBA2FA92F982F8827BC01CD01620F4B
:10048000711D811D911D42E0660F771F881F991F06
:100490004A95D1F708958FB90000779BFECF8FB1B1
:1004A00008951F920F920FB60F9211242F933F932E
:1004B0004F935F936F937F938F939F93AF93BF936C
:1004C000EF93FF9383EB90E00E945A01FF91EF912D
:1004D000BF91AF919F918F917F916F915F914F915C
:1004E0003F912F910F900FBE0F901F9018951F9264
:1004F0000F920FB60F9211242F938F939F93EF9328
:10050000FF93E091C300F091C4008081E091C900A5
:10051000F091CA0082FD1BC090818091CC008F5F5A
:100520008F732091CD00821741F0E091CC00F0E074
:10053000ED54FF4F958F8093CC00FF91EF919F91E9
:100540008F912F910F900FBE0F901F901895808163
:10055000F4CF1F920F920FB60F9211242F933F9357
:100560008F939F93AF93BF938091500190915101CE
:10057000A0915201B09153013091AC0023E0230FC0
:100580002D3768F126E8230F0296A11DB11D209397
:10059000AC008093500190935101A0935201B0930D
:1005A00053018091AD009091AE00A091AF00B09149
:1005B000B0000196A11DB11D8093AD009093AE00D7
:1005C000A093AF00B093B000BF91AF919F918F9176
:1005D0003F912F910F900FBE0F901F90189501968D
:1005E000A11DB11DD4CFCF93DF93CDB7DEB729972F
:1005F0000FB6F894DEBF0FBECDBF789483B783608B
:1006000083BF83B7886483BF89B7816089BF92E065
:100610009EBD8EB581608EBD8FB581608FBD85B565
:10062000846085BD85B5806485BD87E886B91AB8C4
:10063000E091C300F091C4009083E091BF00F0917D
:10064000C0001082E091C100F091C20080E180837F
:100650001092CB00E091C700F091C80086E880833B
:10066000E091C500F091C600808188698083E091A7
:10067000C500F091C60080818F7D80831FB7F894FC
:100680008091B100811120C0ECE8F0E08491ECEAA7
:10069000F0E09491E82FF0E0EE0FFF1FE258FF4FDB
:1006A000A591B491EC91E92321F461E084E00E94EA
:1006B000E70184E00E94BD006C9A6E9A87E00E9478
:1006C000BD0085E00E94BD008091B1008F5F8093E6
:1006D000B1001FBF80E58DB981E08EB985E10E9430
:1006E000BD0084E10E94BD0080E10E94BD0081E167
:1006F0000E94BD0082E10E94BD0083E10E94BD0016
:1007000061E084E10E94E70161E083E10E94E7018A
:1007100061E085E10E94E70160E080E10E94E7017D
:1007200060E081E10E94E70160E082E10E94E70170
:100730008EE18093B200109254010E942502685EFF
:10074000734081099109609355017093560180931C
:1007500057019093580190E080E00E94A4078068C0
:1007600080936000EDE5F1E092EDDF011D929A9536
:10077000E9F71F928F9387EA90E09F938F931F92E0
:1007800085E08F93CE0101969F938F930E94E90796
:1007900067E080E00E948F006BE080E10E948F00A4
:1007A000998180ED890F0FB6F894DEBF0FBECDBFE3
:1007B0008A3010F089EC890F6FE00E948F009A81D7
:1007C00080ED890F8A3010F089EC890F63E10E9477
:1007D0008F002FB7F8948091500190915101A09112
:1007E0005201B09153012FBF8093590190935A0148
:1007F000A0935B01B0935C0181E0809361002DE5E3
:10080000822E21E0922E3EE1232E412C512C3201EA
:10081000439450E0A52E50E0B52E809161008111E7
:1008200033C083EB90E00E943A01181619060CF0D1
:100830004EC083EB90E00E94180187FF07C09091A3
:1008400060009813EECF1092B200EBCF6091B2002F
:100850006E3138F7E62FF0E0E35AFE4F30E020E04B
:100860009927AC01022E02C0559547950A94E2F7EC
:10087000417040832F5F3F4F7E962730310589F7C7
:100880006F5F6093B200CDCF2FB7F8948091500185
:1008900090915101A0915201B09153012FBF40910D
:1008A000590150915A0160915B0170915C01841B68
:1008B000950BA60BB70B883B9B40A105B10538F003
:1008C00010926100D401E2ED1D92EA95E9F70E94D1
:1008D0002502009155011091560120915701309148
:1008E00058016B017C01C01AD10AE20AF30A970190
:1008F0008601083E13402105310508F46DC0609360
:10090000550170935601809357019093580161E00F
:1009100084E10E94E70161E083E10E94E7013090F9
:100920005401329C9001112490E080E0C12CD12C24
:100930007601032D10E0F901E80FF91FE35AFE4F8D
:100940005081552371F0B301A201082E04C0440F59
:10095000551F661F771F0A94D2F7C42AD52AE62AA4
:10096000F72A01968E31910531F760E085E10E940A
:10097000E7018F2D0E944B028E2D0E944B028D2D80
:100980000E944B028C2D0E944B0261E085E10E9487
:10099000E701632D617080E10E94E701B801759560
:1009A0006795617081E10E94E7011595079515959E
:1009B0000795602F617082E10E94E70160E083E1AA
:1009C0000E94E70160E084E10E94E70181E0830D7D
:1009D000873070F480935401A114B10409F41DCF41
:1009E0000E944601882309F418CF0E94000015CF09
:1009F00010925401F1CFE3EBF0E01382128288EE03
:100A000093E0A0E0B0E084839583A683B78386E675
:100A100090E09183808380E490E09587848729E249
:100A200030E0378726872BE230E0318B208B2AE2BB
:100A300030E0338B228B958B848B8CE290E0978B0C
:100A4000868B118E128E138E148E0895ABE0B0E05B
:100A5000ECE2F5E00C94AC077C013B018A01FC015F
:100A600017821682838181FFC8C1CE0101965C0185
:100A7000F7019381F30193FD859193FF81913F01EC
:100A8000882309F453C1853239F493FD859193FF8E
:100A900081913F01853229F4B70190E00E94160749
:100AA000E7CF912C212C312CFFE1F31538F08B325C
:100AB00011F190F4803209F1833229F137FC3CC006
:100AC00020ED280F2A3050F536FE20C08AE0989E8F
:100AD000200D1124922E06C08D3291F0803371F7D3
:100AE000689430F8F30193FD859193FF81913F0164
:100AF0008111DACF21C0689431F8689432F8F2CFCE
:100B0000689433F8EFCF689434F8ECCFEAE02E9E87
:100B1000200D1124222E689435F8E4CF8E3229F46A
:100B200036FC04C1689436F8DDCF8C3619F468942D
:100B300037F8D8CF8836B1F2982F9F7D95549330EF
:100B4000E0F08336A1F18337C1F1833509F063C04A
:100B50002801F2E04F0E511CF801C080D180692DB0
:100B600070E036FC02C06FEF7FEFC6010E94000705
:100B70004C01689437F882010AC00C5F1F4FFFE3F5
:100B8000F98388248394912C6501E89437F833FE27
:100B90002DC0522C8114910471F5552009F468CFB1
:100BA000B70180E290E00E9416075A94F6CFF80150
:100BB000808189830E5F1F4FE4CF2801F2E04F0E42
:100BC000511CF801C080D180692D70E036FC02C054
:100BD0006FEF7FEFC6010E940B074C018201D5CF5A
:100BE000B70180E290E00E9416072A9428141904A5
:100BF00009F0B0F7CECFF60137FC859137FE819131
:100C00006F01B70190E00E94160751105A94F1E06D
:100C10008F1A9108BFCF843619F0893609F077C052
:100C2000F80137FE6BC060817181828193810C5F16
:100C30001F4FF32DFF763F2E97FF09C090958095AB
:100C4000709561957F4F8F4F9F4F689437F82AE0DA
:100C500030E0A5010E944607C82ECA188C2C432CF0
:100C600036FE0CC0E89440F8C91440F434FE05C0C8
:100C700032FC03C0F32DFE7E4F2E892C44FE95C01E
:100C8000FE01EC0DF11D8081803309F087C0242D19
:100C9000297E422E842D8870582E43FC95C040FE3C
:100CA0008FC09C2C821418F42C0C922C981844FEA3
:100CB00091C0B70180E390E00E94160742FE09C090
:100CC00088E790E041FE02C088E590E0B7010E940D
:100CD0001607C91408F48AC0CA94D12C9FEFC91A08
:100CE000D90ACA0CDB1CF60182916F01B70190E0B2
:100CF0000E941607AC14BD04B1F74FCF608171811B
:100D0000072E000C880B990B0E5F1F4F92CFD32C30
:100D1000E894D4F82AE030E0853739F1232D297F93
:100D2000D22E8F3601F148F48835A9F0F70186817B
:100D300097812B96E2E10C94C807803749F08837F9
:100D4000A9F7D4FE02C06894D2F820E130E00DC0CB
:100D50006894D4F8F6CF34FE03C0822F8660D82E74
:100D600020E132E002C028E030E0F801D7FE0FC0F9
:100D700060817181828193810C5F1F4FA5010E9468
:100D80004607C82ECA183D2CE89437F867CF608113
:100D9000718190E080E00E5F1F4FF0CF42FC02C0F7
:100DA000839478CF8394839475CF842D867809F4C7
:100DB00071CFF6CFB70180E290E00E9416078394CE
:100DC0008214C0F3512C73CF522C5818821408F49B
:100DD0006ECFF8CF842D867809F47BCF8BE241FE6D
:100DE00080E247FC8DE2B70190E071CFB70180E36C
:100DF00090E00E9416079A946CCF8FEF9FEF99CFE7
:100E0000FC010590615070400110D8F780959095D5
:100E10008E0F9F1F0895FC016150704001900110DA
:100E2000D8F7809590958E0F9F1F08950F931F936D
:100E3000CF93DF93182F092FEB018B8181FD09C020
:100E40001FEF0FEF812F902FDF91CF911F910F9107
:100E5000089582FF14C02E813F818C819D812817C7
:100E600039073CF4E881F981CF01019699838883A1
:100E700010838E819F8101969F838E83E3CFE885C7
:100E8000F985812F0995892BA1F3DACFFA01AA27D9
:100E9000283051F1203181F1E8946F936E7F6E5FBD
:100EA0007F4F8F4F9F4FAF4FB1E03ED0B4E03CD06B
:100EB000670F781F891F9A1FA11D680F791F8A1F4E
:100EC000911DA11D6A0F711D811D911DA11D20D0B5
:100ED00009F468943F912AE0269F11243019305D6F
:100EE0003193DEF6CF010895462F4770405D419360
:100EF000B3E00FD0C9F7F6CF462F4F70405D4A33AD
:100F000018F0495D31FD4052419302D0A9F7EACF74
:100F1000B4E0A6959795879577956795BA95C9F7A3
:100F200000976105710508959B01AC010A2E069496
:100F30005795479537952795BA95C9F7620F731F4F
:100F4000841F951FA01D0895E199FECF9FBB8EBB06
:100F5000E09A99278DB308952F923F924F925F9216
:100F60006F927F928F929F92AF92BF92CF92DF92B9
:100F7000EF92FF920F931F93CF93DF93CDB7DEB71E
:100F8000CA1BDB0B0FB6F894DEBF0FBECDBF0994B2
:100F90002A88398848885F846E847D848C849B8409
:100FA000AA84B984C884DF80EE80FD800C811B8117
:100FB000AA81B981CE0FD11D0FB6F894DEBF0FBE46
:100FC000CDBFED010895EE0FFF1F0590F491E02DC8
:100FD0000994AEE0B0E0EFEEF7E00C94BA070D89AB
:100FE0001E898F89988D26E02C831A83098397FFA9
:100FF00002C080E090E801979E838D83AE01455E3C
:101000005F4F698D7A8DCE0101960E9426054D8134
:101010005E8157FD0AC02F813885421753070CF4B3
:101020009A01020F131FF80110822E96E4E00C942F
:06103000D607F894FFCF83
:101036005001000000007B01DC000901C7013A01F4
:1010460018012C011F111F001F001D151715151F54
:1010560007041F17151D1F151D01011F1F151F173B
:10106600151F1E051E1F150A0E11111F110E1F1525
:0C107600111F0501140814253032580029
:00000001FF

Flash the FW into the panel

You need programmer able to program atmega16 MCUs in DIP40 packages, luckily there is ISP header on the control borad, so I used cheap USBasp to do that.

!!! THE ISP HEADER USE NON-STANDARD PINOUT - DO NOT PLUG STANDARD USBASP CABLE, YOU NEED TO PIN THESE ACCORDING TO SCHEMATIC OTHERWISE USBASP POWER WILL BE SHORTED !!!

  • install avrdude
  • connect USBasp and panel power supply
  • verify that you can communicate with the atmega over USBasp avrdude -c usbasp -p m16 that should say "device signature = 0x1e9403 (probably m16)"
  • erase the locked atmega16 using avrdude -c usbasp -p m16 -e
  • set the fuses (16MHz xtal, 4V BOD, jtag off, preserved EEPROM on chip erase, ISP allowed) using avrdude -c usbasp -p m16 -U lfuse:w:0x3f:m -U hfuse:w:0xc7:m
  • set the device address (needs to be in range 0x80-0xFF) using avrdude -c usbasp -p m16 -U eeprom:w:0x80:m replace 0x80 with desired address
  • write and verify the firmware using avrdude -c usbasp -p m16 -U flash:w:firmware.hex:i
  • done

Show something on the screen

Here are some bash examples (to be easy to understand) how to display something on the screen.

This example shows the "base48" text on screen with address 0x80:

#!/bin/bash

PORT="/dev/ttyUSB0"

# Nastavení sériového portu: 115200 baud, 8N1
stty -F "$PORT" 115200 cs8 -cstopb -parenb raw -echo

# Odeslat příkaz 0x80 a grafická data.
# Každý znak má 4 sloupce, mezi znaky je jeden prázdný sloupec.
# Bit 0 = horní LED, bit 6 = spodní LED.

printf '\x80\x7F\x48\x48\x30\x00\x20\x54\x54\x78\x00\x48\x54\x54\x20\x00\x38\x54\x54\x18\x00\x18\x14\x12\x7F\x00\x36\x49\x49\x36\x00' > "$PORT"

This example shows simple clock on screen with address 0x80

#!/bin/bash

PORT="/dev/ttyUSB0"

# Nastavení sériového portu
stty -F "$PORT" 115200 cs8 -cstopb -parenb raw -echo

# Font číslic 3x5
declare -a FONT=(
    "111 101 101 101 111"  # 0
    "010 110 010 010 111"  # 1
    "111 001 111 100 111"  # 2
    "111 001 111 001 111"  # 3
    "101 101 111 001 001"  # 4
    "111 100 111 001 111"  # 5
    "111 100 111 101 111"  # 6
    "111 001 010 010 010"  # 7
    "111 101 111 101 111"  # 8
    "111 101 111 001 111"  # 9
)

# Vykreslení číslice do sloupcového bufferu.
# Číslice má šířku 3 a výšku 5 LED.
# Začíná na řádku 1, takže je na displeji vystředěná.

draw_digit() {
    local digit=$1
    local x=$2
    local row col bits
    local -a rows

    read -ra rows <<< "${FONT[$digit]}"

    for ((row = 0; row < 5; row++)); do
        for ((col = 0; col < 3; col++)); do
            if [[ ${rows[$row]:$col:1} == 1 ]]; then
                ((bits = 1 << (row + 1)))
                ((columns[x + col] |= bits))
            fi
        done
    done
}

# Nekonečná smyčka hodin
while true; do

    # Získat aktuální čas počítače
    time=$(date '+%H%M%S')

    # Vymazat obrazový buffer
    for ((i = 0; i < 30; i++)); do
        columns[i]=0
    done

    # Rozložení HH:MM:SS

    draw_digit "${time:0:1}" 2
    draw_digit "${time:1:1}" 6

    # Dvojtečka
    ((columns[10] |= 0x14))

    draw_digit "${time:2:1}" 12
    draw_digit "${time:3:1}" 16

    # Dvojtečka
    ((columns[20] |= 0x14))

    draw_digit "${time:4:1}" 22
    draw_digit "${time:5:1}" 26
    # Sestavit příkaz 0x83 a všech 30 sloupců.
    # Data jsou zatím reprezentována jako escape sekvence,
    # aby fungovaly i bajty s hodnotou 0x00.

    payload='\x80'

    for ((i = 0; i < 30; i++)); do
        printf -v byte '\\x%02X' "${columns[i]}"
        payload+="$byte"
    done

    # Odeslat celý obraz jedním printf
    printf '%b' "$payload" > "$PORT"

    # Aktualizace přibližně jednou za sekundu
    sleep 1

done

Here is a simple pong game for two players controlled with keys Q, A, P, L...

#!/bin/bash

PORT="/dev/ttyUSB0"
WIDTH=30
HEIGHT=7
DELAY=0.05

# Nastavení sériového portu
stty -F "$PORT" 115200 cs8 -cstopb -parenb raw -echo

# Uložení nastavení terminálu a povolení čtení kláves bez Enteru
OLD_STTY=$(stty -g) || exit 1
restore_terminal() {
    stty "$OLD_STTY"
}
trap restore_terminal EXIT INT TERM

stty -echo -icanon min 0 time 0

# Počáteční pozice pálek
left_paddle=3
right_paddle=3

# Inicializace hry
new_game() {
    ball_x=15
    ball_y=3

    # Náhodný směr míčku
    if ((RANDOM % 2)); then
        vx=1
    else
        vx=-1
    fi

    # Náhodný vertikální směr
    if ((RANDOM % 2)); then
        vy=1
    else
        vy=-1
    fi
}

new_game

while true; do

    # ---------------------------------------
    # Čtení kláves
    # ---------------------------------------

    while IFS= read -r -s -n 1 -t 0.001 key; do
        case "$key" in
            q|Q)
                ((left_paddle > 1)) && ((left_paddle--))
                ;;
            a|A)
                ((left_paddle < HEIGHT - 2)) && ((left_paddle++))
                ;;
            p|P)
                ((right_paddle > 1)) && ((right_paddle--))
                ;;
            l|L)
                ((right_paddle < HEIGHT - 2)) && ((right_paddle++))
                ;;
            $'\003')
                exit 0
                ;;
        esac
    done

    # ---------------------------------------
    # Výpočet nové pozice míčku
    # ---------------------------------------

    next_x=$((ball_x + vx))
    next_y=$((ball_y + vy))

    # Odraz od horní a dolní hrany
    if ((next_y < 0)); then
        next_y=1
        vy=1
    elif ((next_y >= HEIGHT)); then
        next_y=$((HEIGHT - 2))
        vy=-1
    fi

    # ---------------------------------------
    # Levá pálka
    # ---------------------------------------

    if ((vx < 0 && next_x <= 0)); then
        if ((next_y >= left_paddle - 1 &&
             next_y <= left_paddle + 1)); then

            # Míček zasáhl levou pálku
            next_x=1
            vx=1

            if ((next_y < left_paddle)); then
                vy=-1
            elif ((next_y > left_paddle)); then
                vy=1
            else
                # Zásah doprostřed: náhodný vertikální směr
                if ((RANDOM % 2)); then
                    vy=1
                else
                    vy=-1
                fi
            fi
        else
            # Minutí levé pálky: okamžitý restart
            new_game
            continue
        fi
    fi

    # ---------------------------------------
    # Pravá pálka
    # ---------------------------------------

    if ((vx > 0 && next_x >= WIDTH - 1)); then
        if ((next_y >= right_paddle - 1 &&
             next_y <= right_paddle + 1)); then

            # Míček zasáhl pravou pálku
            next_x=$((WIDTH - 2))
            vx=-1

            if ((next_y < right_paddle)); then
                vy=-1
            elif ((next_y > right_paddle)); then
                vy=1
            else
                if ((RANDOM % 2)); then
                    vy=1
                else
                    vy=-1
                fi
            fi
        else
            # Minutí pravé pálky: okamžitý restart
            new_game
            continue
        fi
    fi

    # Uložení pozice míčku
    ball_x=$next_x
    ball_y=$next_y

    # ---------------------------------------
    # Vykreslení snímku
    # ---------------------------------------

    for ((x = 0; x < WIDTH; x++)); do
        columns[x]=0
    done

    # Levá pálka
    for ((y = left_paddle - 1; y <= left_paddle + 1; y++)); do
        ((columns[0] |= 1 << y))
    done

    # Pravá pálka
    for ((y = right_paddle - 1; y <= right_paddle + 1; y++)); do
        ((columns[WIDTH - 1] |= 1 << y))
    done

    # Míček
    ((columns[ball_x] |= 1 << ball_y))

    # ---------------------------------------
    # Odeslání snímku displeji
    # ---------------------------------------

    payload='\x80'

    for ((x = 0; x < WIDTH; x++)); do
        printf -v byte '\\x%02X' "${columns[x]}"
        payload+="$byte"
    done

    printf '%b' "$payload" > "$PORT"

    sleep "$DELAY"
done