• Part 4: Working software

    Smidge20402/26/2023 at 02:22 0 comments

    More progress writing the driver software. TL;DR: Arduino IDE is pants, rewrote it in AVR Assembly.

    First let's talk about the strategy to run four panels from a single atMega2560 MCU. I decided that to minimize processing, and thus maximize speed, I should do some pre-processing of the image data and pack the bits in a way that's efficient to output. To whit, I gathered all of the IO lines from four panels and packed them into the available outputs on the Arduino Mega 2560:

    This scheme was alluded to in the previous log with the sample source code. a standard 24-bit bitmap has one byte for R,G and B values in groups for each pixel. Preprocessing the image data separates the RGB values, and then repacks the bits such that the first byte of the output contains the first bit of the R, G and B streams. Each byte of output is equated to one port per the above image.

    Further, the order of the pixels is accounted for: The pixels are sampled starting at row 12, column 16, and across the row to column 1. On the LED panel this maps to (assuming Bank 1):

    L400, L399 [...] L385. L208, L207 [...] L193, L16, L17 [...] L1

    The data stream for the entire 48x48 display is therefore 5 bytes wide (using 5 ports on the MCU) by 48*8 long; a total of  1920 bytes. Per row. When we account for there being four rows, that's 7680 bytes to fully define the 48x48 pixel image. Luckily the atMega2560 has 8192 bytes of SRAM, meaning we can stuff one full frame of image data into the faster SRAM space with a few hundred bytes left over for things like stack space. Yay!


    Now for those paying attention, you might recall that the drivers use 36 bits per pixel - 12 each per color - but we're only sending 24? We do this by padding extra bits, as zeros, to the beginning of the data. This reduces the maximum brightness without messing with the original image data, which is perfectly fine because these things are CRAZY bright at full power and run quite hot to boot, so this reduction makes it much more appropriate for use as, say, a wall display.

    The preprocessor is written in VisualBasic6 because that's how I roll. The output is a *.asm file that simply gets included at compile time, and one of the first things the CPU does is copy it from program memory into SRAM.

    Enough rambling, let's get to some code! Everything is written for AtmelStudio 7, which is now known as Microchip Studio. I've not used the new IDE but I'm sure it's fine.

    The main.asm and imgData.asm files are available for download through this project. There is a copious amount of comments explaining what each and every instruction is doing, which is essentially identical to the sample code in the Part 3 project log.

    While I wait for the bits I need to clean up this nightmare of wiring (which causes TONS of problems with signal noise, BTW...) the next step is to find a way to mount these panels to a frame along with all the gubbins.

  • Part 3: Some hardware and software

    Smidge20402/19/2023 at 03:45 0 comments

    Been plugging away at it, and while the Arduino Mega I decided to use is barely up to the task, it can be hacked to be good enough. Here's some preliminary driver code and the hardware hacks required to get it running.

    First the software. 

    #define CTRLPINS   PORTA            // Port A used for all the control signals
    #define CTRLPOUTS  PINA             // Easier and faster to read port state than keep a local variable
    #define Data1      PORTC            // Port C is for the RGB data
    
    #define DDCTRLPINS DDRA             // Data direction settings. Change these
    #define DDData1    DDRC             // to patch ports above
    
    uint8_t UPDATE;                     // Flag to check if data is ready for update
    uint8_t ROW;                        // Keeps track of the row we're updating
    
    void setup() {
      noInterrupts();                   // Disable interrupts while we set things up
    
      DDCTRLPINS = 0xFF;                // Set data direction to "output" by setting these registers
      DDData1   = 0xFF;
    
      ROW = 0b00001110;                 // Rows are active LOW so 00001110 = Row 0 active
      UPDATE = 0;                       // Clear UPDATE flag
      
      // Set up timer/counter. Refer to ATMega datasheet section 17.11.1
      TCCR1A = 0;                       // Reset Timer1 control Register A
      bitClear(TCCR1B, WGM13);          // Set CTC (Clear Timer on Compare) mode
      bitSet(TCCR1B, WGM12);
      bitSet(TCCR1B, CS12);             // Set clock source to T1 (pin 31 of the MCU, NOT Arduino board!)
      bitSet(TCCR1B, CS11);             // This pin is not connected to anything on the Arduino Mega 2560
      bitSet(TCCR1B, CS10);             // So to use it you'll have to solder your own bodge wire
    
      TCNT1 = 0;                        // Reset Timer1 to known state
      OCR1A = 4096;                     // Set compare value. Interrupt will trigger when counter reaches this value
    
      bitSet(TIMSK1, OCIE1A);           // Enable Timer1 compare interrupt
    
      interrupts();                     // Enable interrupts again
    }

    The above clip is the setup, written in the Arduino IDE so I use some Arduino wrapper functions because speed isn't critical here.

    The strategy is to use the 16MHz system clock, divide it by 4 (down to 4MHz) externally and use that for our PWM clock source. We will then feed that 4MHz clock back into the MCU to make a counter go up.

    When that counter hits 4096 cycles (the full PWM register value of the TLC5941) it will trigger an interrupt service routine that will reset the PWM counters (setting BLANK to high, then low) and, if all the data for the next update has been clocked in, cycle XLAT and enable the next row of LEDs:

    ISR(TIMER1_COMPA_vect)
    {
      noInterrupts();                   // Disable interrupts so our interrupt handler isn't interrupted...
      TCNT1 = 0;                        // Reset Timer1 to known state
     
      CTRLPINS = CTRLPOUTS | BLANK_ON;  // Set BLANK high to disable TLC5941 output and reset PWM counters
      
      if (UPDATE > 0)                   // If the data has all been shifted in...
      {
       CTRLPINS = CTRLPOUTS | XLAT_ON;  // Pulse XLAT to move TLC5941 input buffers to output registers
       CTRLPINS = CTRLPOUTS & XLAT_OFF;
    
       UPDATE = 0;                      // Clear update flag
    
       r = CTRLPOUTS | 0xF0;            // Some jiggery-pokery to quickly update the ROW drivers
       r = r ^ (1 << (ROW+4));          // Probably not the best way but it works
       CTRLPINS = r;
    
       ROW++;                           // Set the next row
       if(ROW >= 4) ROW=0;              // Don't forget to wrap around
      }
     
      CTRLPINS = CTRLPOUTS & BLANK_OFF; // Set BLANK low and this enables the output on the TLC5941s
     
      interrupts();                     // Re-enable interrupts
    }

    In the roughly  1 millisecond we have between interrupt calls, we can do whatever work we need to do to prepare and shift the new data in. It's perfectly fine that many of the interrupts will only toggle the BLANK line and reset the PWM counters - that's what keeps the lights on. Actual data updates will only occur if all the data has been clocked in as per the UPDATE flag.

    void loop()
    {
     if (UPDATE > 0) return;            // Skip all this if we're up to date
     
     /* Whatever image processing needs to be done goes here */
     
     for (x=0; x<575; x++)
     {
      CData1 = gsData[ROW][x];          // Put serial data on pin.
      CTRLPINS = CTRLPOUTS | SCLK_ON;   // Pulse the SCLK line to clock the bit into the TLC5941s
      CTRLPINS = CTRLPOUTS & SCLK_OFF;
     }
     
     /*
     In this case, gsData[4][576] is an array of four sets of 576 bytes. Each...
    Read more »

  • Part 2: Data format

    Smidge20402/08/2023 at 01:56 0 comments

    With a better understanding of the physical layout, we can start to figure out how to load bits into this thing to get some blinkenlights happening.

    The module is divided into three identical banks, and each bank has separate, identical connections, so we will just focus on a single bank and know we'll have to do everything X3 for get the whole panel working...

    The TLC5941 is a 16-channel, 12-bit-per-channel, PWM capable LED driver with dot correction. They can be chained together by linking the serial out (SOUT) of one chip to the serial in (SIN) of the next chip in the chain, and driving the serial clock (SCLK) of all chips in the chain with the same source.

    Each of the 16 channels takes 12 bits of data, so fully loading one driver requires clocking in 192 bits. This 12-bit value is used by the PWM control to specify its brightness (0x000 = 0%,  0xFFF = 100%).

    Each bank is three groups of three chained-together drivers. Each of these groups is a color channel: Red, Green, and Blue. All three groups share the common control signals but have separate SIN lines. Since there are three chips chained together per group, you will need to clock in 576 bits per color channel to fully define that bank's colors.

    Data is shifted into (and out of, via SOUT) the input register. When all of the data is loaded, a signal is given to transfer the input register to the output register, and the output is updated. This lets you clock in new data while the previous data is still being displayed, which reduces glitchyness. Data is clocked in one bit at a time, most significant bit (MSB) first.

    The common control lines are:

    BLANK: When BLANK is logic high, all outputs are disabled (but the data in the output registers is retained) and the PWM counter is reset. BLANK is pulled high just before the XLAT line is used to transfer data from the input register to the output registers. Resetting the PWM counter is also critical to the operation, and I'll describe that more under GSCLK. Pulling BLANK low will re-enable the output causing whatever data is in the output register to be displayed.

    XLAT: Transfer latch. Pulling XLAT high (ideally, while BLANK is also high) will transfer the contents of the input register to the output register. It will also fill the input register with status data, which you can then clock out into your controller, however these modules do not have any connection from the SOUT of the last chip in each chain so getting that signal will require some mod wires.

    SCLK: Serial clock. Each pulse of SCLK will shift the input register left by 1 bit, putting the MSB into SOUT and filling the LSB with the state of SIN.

    MODE: Programming mode. When MODE is low, SIN and SOUT are connected to the 192-bit input register. When MODE is high, SIN and SOUT are instead connected to a different, 96-bit register (6 bits per channel) that controls the dot correction. Dot correction allows you to set a constant offset for each channel to correct differences in brightness from one LED to the next.

    GSCLK: Greyscale clock, aka PWM clock. While the driver has an internal PWM counter, it relies on an externally supplied clock to increment this counter. When BLANK goes high, the PWM counter register is reset. When BLANK goes low, the PWM counter will be incremented by 1 on each rising edge of GSCLK. When the PWM counter matches the input value for each individual channel, that channel output is disabled. The PWM counter must be manually reset by toggling BLANK high then low. Failure to cycle the BLANK signal means the output will get disabled as soon as the PWM counter is up and it wills stay off. In practice, since each channel is 12-bits, you should cycle the BLANK line every 4096 pulses of GSCLK.

    Okay, so now that we know what the data lines do, let's discuss how to actually load data and get this thing working...

    Let's say we want to light up LED1 (top left corner) solid RED at 100% brightness. LED 1 is bank 1, row 1A, and we're just...

    Read more »

  • Part 1: Physical overview

    Smidge20402/07/2023 at 23:41 0 comments

    Let's start with a broad overview of what we're dealing with.

    Each board has one one side a 48x12 array of RGB LEDs covered by a multi-part injection molded lens assembly composed of a frosted lens panel and six black grid panels that help visually separate the individual pixel squares. The fact that the black trim bits are 8x12 suggests they made modules of various sizes...

    On the back of the board is two power connections, a 60-position main data connector, a step-down converter (as a separate assembly), a bunch of ICs and misc. passives. My boards are stamped  "SACO TECHNOLOGIES V9 REV. B." with a field for a hand-written serial number that is blank.

    Power connectors are 8-pin, 2.54mm (0.100") pitch, with 4 pins each for +5V and GND. Not able to identify the part number (yet?). All the +5VDC and all the GND pins are connected to each other to form common rails. +5VDC feeds the DC-DC converter, all of the ICs (except the P-channel mosfets), and also connects to the +5V pins of the main data connector. Let this be a warning: It is possible to accidentally power the entire board through the pins on the main connector, which I really doubt are up to the task. In my testing I only connected the GND for common voltage reference and used only the bulk power connectors for the +5VDC supply.

    The main data connector looks to be a 3M "Pak 50" series, P50L-060P-AS-DA. The mating connector is P50L-060S-AS-DA. At time of writing, the plug that matches the socket on these boards goes for $10.52 each on Digikey, with a minimum order of 200 and 28 weeks lead time, so we ain't doin' that.

    An old floppy drive cable fits the 1.27mm pitch perfectly. This soldering job might not look like much, but it's worth at least $2,104. (and 7 months...)

    The power supply module, which steps the 5VDC supply to 3.3VDC, is a PTH05060. This module is rated for an output of 10 amps at a configurable 0.8 to 3.6VDC. As far as I can tell, this 3.3VDC rail is used exclusively for the red LEDs, and all the other LEDs and ICs are supplied by the 5V rail.

    The rest of this chips include: LED drivers, octal buffers, mosfet drivers, and dual P-channel mosfets. There are a total of 27 LED drivers, which of course are the key to the whole thing, but let's cover the other ICs briefly.

    There are three VHCT541 octal buffer chips, all with their output enable pins tied to ground. 24 of the 28 data pins from the main connector go to these buffers, and since the output is always enabled they immediately pass the input pin state to the output pins. I presume this is done to save the controller from having to source current to drive so many LED driver lines.

    Four of the data pins from the main connector go to two dual-channel MOSFET drivers, TC1427C, which of course drive the P-Channel MOSFETs at the other end of the board. Each control input controls 4 of the 16 MOSFETS.

    There are eight dual P-Channel MOSFETs. Mine are labeled "D6P02." These are divided into four ICs for the 5VDC rail and and four ICs 3.3VDC rail, but are connected in parallel on their respective rails. That's two MOSFETs per rail per input, and they are in different chips presumably to spread the current out and reduce heat load.

    And now the 27 LED drivers; Texas Instruments TLC5941 (Datasheet). These are arranged in three groups that I'll refer to as "banks" in keeping with Huffine's post. Each bank is divided into three "rows," and each row is powered by four power rails each switched separately by the MOSFETs (A,B,C,D)

    9 drivers per bank, 3 per row. Each driver in a row's group of 3 does R,G, and B channels. It's important to note that they are electrically grouped by color, meaning you will need to separate the pixel data by color channel and clock in three rows worth of data for each color, rather than clocking in one row of RGB data.

    A modified and annotated pinout from Huffine's post. They had originally had Banks 1 and 3 reversed, so I changed them around...

    Read more »