Close
0%
0%

TWI or UART to HUB75 matrix driver - Koakuma V3

Easy to use, cheap, and adafruit ecosystem compatible. Drive up to 64x64 rgbled matrices via TWI commands

Similar projects worth following
So one day i bought an RGB-LED matrix for a project and appearently, these things are not that trivial to control. The datasheet said i need an RPie, Adafruit said i need an ESP32.

That broke my AVR loving heart and i took it as a challenge.

It got its limitations, but i made it work.
Outcome is a driver that lets you write single pixels via UART or TWI with easy "arduino style" commands. You just send the pixels you want to draw and let the driver handle the rest. So your master controller is free for other tasks.

Also, the alphabet and numbers are already preloaded as sprites and can be drawn with a single command.

Features:
- 64 colors
- Easy TWI/UART commands
- Configurable address for up to 16 daisychainable displays
- A onboard DCDC converter up to 26V
- Alphabet and numbers already pre-loaded
- 3V3 and 5V TWI compatible

Goal was to make a standalone HUB75 display driver using an AVR controller.

While anything with more power like an ESP32 would be way better for the job, i just love AVR so much i really wanted to make it work.

Software

The basic idea is to have two buffers. One screenbuffer which constantly gets rendered (=the stuff on screen) and one working buffer or shadow buffer how i call it which we can slowly fill with stuff that we want on screen without flickering. So the basic working principle is:

1. Clear shadow buffer
2. write pixels we want to shadow buffer
3 command to copy shadow buffer to screen

Color codes are 1 byte / 2 bit per color

Color byte = 0b00bbggrr

so yellow would be 0b00001111

Examples:

Control with an Arduino via TWI commands:

Control with python script via USB-UART dongle:

-> I just used the first pretty GIF i found on Google for testing. Original GIF by MetaruPX on Newgrounds

TWI commands

Commands are just a command byte, followed by 2-4 data bytes (except clear buffer) sent to the address

Clear buffer:
1 byte: 0x01

Draw pixel:
4 byte: 0x02, x_pos, y_pos, color

Draw letter:
5 byte: 0x03, x_pos, y_pos, ASCII, color

Copy buffer:
1 byte: 0x04

So to write a red "A" to position 10,10 the arduino command would be:

  Wire.beginTransmission(0x41);
  Wire.write(0x03);
  Wire.write(10);
  Wire.write(10);
  Wire.write('A');
  Wire.write(0b00000011);
  Wire.endTransmission();

Then just send another 0x04 to the address and done. Cant get any easier than that.

The AVR already runs on its very limit and it was not possible to add another bigger buffer. So every command needs to be its own Transmission.

UART control

Uart control is meant for "streaming" so the commands dont exist.

You start a transmission by writing 0xF0 (clear shadow buffer and start counter)

Then you write the color-bytes for every pixel 

And end the transmission with 0xF1 (copy shadow buffer to screen)

So an example python script to write one screen would be:

pack = bytearray()
pack.append(0xF0)
    for y in range(64):
        for x in range(64):
                pack.append(Buffer[x][y])
    pack.append(0xF1)
ser.write(pack)

Hardware

The design is kept in a handy business-card size. Input and supply in the bottom, output on top.

Controller

For this project i use an AVR128DA32.

The clock speed is a bit higher than the other AVR with 24MHz.
Also the DA family has the most RAM which is needed for the screen buffers.

Power supply

The need for beefy 5V supplies is kinda annoying. So i added an onboard DCDC.
Maximum Vin = 26V.
Iout max ~2A (enough to drive a 64x64 matrix at full tilt)

Connectivity

Two TWI connectors with a Adafruit-STEMMA pinout. Compatible with 3v3 and 5V (configurable via solder jumpers) and optional 4k7 pullup.
The slave address is 0x40 with an configurable offset via solder jumpers (0x40 - 0x4F)

A JST-PH connector for UART control (fixed BAUD rate of 230400)

MatrixDriver.kicad_pcb

kicad_pcb - 551.31 kB - 04/02/2025 at 16:19

Download

MatrixDriver_V3.hex

hex - 14.24 kB - 04/02/2025 at 16:19

Download

  • Added video playback and colors

    Phil Weasel04/02/2025 at 14:38 0 comments

    So having 7 colors is nice but incredibly limited .... i wanted more.

    But more colors = more memory and processing power. So i sacrifieced a huge part of the sprite system (which nobody would use anyways) and added additional buffers to enable 6 bit color -> 64 possible colors.

    Also a USB-UART adapter is used to send framedata with a 230400 Baudrate.

    The result is up to 5 FPS and with a small python script i can even play some GIF´s

    Obviously, it looks much better in reality .... damn phone camera ; )

  • It cant get any easier than that

    Phil Weasel03/17/2025 at 16:37 0 comments

    So we know sending every frame pixel by pixel is awfully slow. But that was never the goal. Goal was to send "draw this sprite here" commands.

    And it does.

    The Letters are already preloaded, so i just send "draw sprite 'W' on X/Y.

    So making an animation like this is just a matter of. 

    Drawing the letter sprite.
    Delay a bit.
    Draw a blank sprite over the old position.
    Draw the letter on new position.
    Copy Shadowbuffer

    Easypeasy.

    The Arduino function looks like:

    void Scr_DrawSprite(char x, char y, char spr){
      Wire.beginTransmission(0x41);
      Wire.write(3);
      Wire.write(x);
      Wire.write(y);
      Wire.write(spr);
      Wire.endTransmission();
    }

    So i can just write

    Scr_DrawSprite(30, 20, 'W');

    In my Arduino code.

    It seriously cant get any easier than that.

  • Harder better faster stronger

    Phil Weasel03/17/2025 at 16:30 0 comments

    So after a complete rewrite of the render function i split the display buffer in two.

    One "Displaybuffer" which buffers everything on screen. A interrupt task which shifts out this buffer on the screen every x-milliseconds.

    Via TWI we can write into an, i call it, "shadowbuffer". Only after we give a command to copy the shadowbuffer into the displaybuffer we can see what we send over. This reduces flickering by A LOT.

    To write a pixel we simply send the X, Y and color value via TWI. While this is comparably awfully slow its incredibly easy to use.

    So a write pixel function on an Arduino looks like this:

    void Scr_DrawPixel(char x, char y, char col){
      Wire.beginTransmission(0x41);
      Wire.write(2);
      Wire.write(x);
      Wire.write(y);
      Wire.write(col);
      Wire.endTransmission();
    }

    So lets test the possible refresh rate with a simple arduino program:

    for(int c=0; c<8; c++){
        for(int x=0; x<64; x++){
          for(int y=0; y<64; y++){
            Scr_DrawPixel(y,x,c);  // Write pixel
          }
        }
        Scr_CopyBuf();   //copy shadow buffer into screenbuffer
      }

    Increasing the TWI speed of the Arduino up to 700Khz we can reach up to 365ms/frame in 64x64 mode.

    Decreasing the resolution down to a 32x32 screen we can get 90ms (~10FPS)

    While this is not very much and pretty slow/unusable for video playback...... IT IS NOT THE INTENDED PURPOSE.

    Goal is to make it as easy as possible. This way the TWI master is free to do whatever and does not have to render every frame.

  • Go big or go home

    Phil Weasel03/17/2025 at 16:14 0 comments

    So i thougt, screw it. I got a 64x64 matrix to test the full capabilities of the driver.

    First of all, yes! The DCDC can drive it on full tilt.

    However only for about a minute until it overheats again. Lets improve in this again in version 4.

    Also: rendering 32 lines causes quite some flickering. Also the sprite feature tends to break down under quite some flickering.

    So i had to rewrite the whole thing.....

  • Adding sprite buffer and preload letters

    Phil Weasel02/26/2025 at 08:59 0 comments

    So the way this screen works is by buffering 8x8 sprites and displaying them on demand.
    The sprite buffer is just a multidimensional array of 100x8x8 bytes.
    At startup all 100 bytes are blank, but i preloead bytes 64-90 with letters and 48-57 with numbers. Yes according to the ascii table.

    My plan is to make the commands be like "DrawSprite(number, Xpos, YPos);". So drawing an "A" could be like "DrawSprite('A', 15, 6);"

    First test: I just display the Name, Version and TWI-Address on startup.

    Great success!

  • DCDC converter and full tilt test

    Phil Weasel02/26/2025 at 08:43 0 comments

    One Problem if the older versions was that i used an LDO to power the screen. However i underestimated the power consumption and the LDO constantly overheated. So this version got a buck converter.

    Using a simple TPS4020 we can generate our deeded 5V with up to 3A.

    Also you can power the board also with 24V eliminating the need for beefy 5V supplies and makes power distribution easier.


    Running the screen in full tilt (all pixels white, maximum brightness) is now possible.
    The DCDC doesnt even get warm.
    And before you ask. The brighter stripe in the middle is due to the slow phone camera this is shot with. in reality you wont see any flickering with your eyes.

View all 6 project logs

Enjoy this project?

Share

Discussions

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates