Skip to content

Theory Of Operation

bitslip6 edited this page Oct 4, 2025 · 2 revisions

Hub75 Operation:

All documentation reefers to 64x64 panels. Your Mileage May Vary. Pins: r1,r2,g1,g2,b1,b2, this is the color data. each LED is actually 3 leds (red, green and blue). The LED can be on or off. We will be pulsing them on/off very quickly to achieve the illusion of different color values. Color data is sent 2 pixels at a time beginning on column 0. r1,g1,b1 is the pixel in the upper 32 rows, r2,g2,b2 is the pixel in the lower 32 rows.

A,B,C,D,E are the address lines. These 5 pins represent the row address. 2^5 = 32 so depending on which bitmask is set on the address lines, the 2 corresponding led rows will be addressed for shifting in data. Data is shifted in on the falling edge of CLK. so after setting the address line, we set pixel value for column 0 along with clock, and then we pull the clock low. That is pixel 0. We now shift in the next pixel and so on 64 times. If we have multiple panels we simply continue shifting in data (in 64-column chunks) for as many panels as we have.

To actually update the panel, we must bring OE (output enable) line high (to disable to display) and toggle the latch pin. data for one row is now latched. we advance the address row lines drop the enable pin low (turn the display on) and begin the process again.

RGM to BCM Mapping:

Data is indexed as follows where bcm is the current bit plane index (0 - bit_depth):

int offset = ((y * scene->width + (x)) * scene->bit_depth) + bcm;

using a linear mapping for RGB (255, 128, 0), the bcm data for a single pixel would map to: r: 1,1,1,1,1,1,1,1,1,1,1... g: 1,0,1,0,1,0,1,0,1,0,1... b: 0,0,0,0,0,0,0,0,0,0,0...

these values would be precomputed after every frame and toggled for each display update (9600-2400Hz)

each bit plane (that is a uint32_t with all of the pin toggles for all 3 output ports for a particular pixel on a single bit plane, there are bit_depth number of bit planes per image) is updated atomically in a single write. This means there is no need for double buffering to achieve a flicker-free display. Simply call map_byte_image_to_bcm with your new image buffer as often as you like. The data will be overwritten and the new PWM data will be updated immediately. This allows you to draw to the display at up to 9600Hz (depending on the number of chained displays) however frame rates of about 120fps seem to produce excellent results and higher frame rates have diminishing returns after that.

Because we have a 9600-2400Hz refresh rate we can use up to 64bit PWM cycles. That means that each RGB value can have 64 levels of brightness or 646464 = 262144 colors. In practice though, values at the lower end (more bits off than on) have a much more perceptible effect on brightness than values at the higher end (more on than off). This is because the human eye is much more sensitive to brightness changes at darker levels than at brighter levels. To correct this I have added gamma correction. See color calibration further in this document for details.

brightness is controlled via a 9K "jitter mask". 9K of random bytes are generated and if each random value is > brightness level (uint8_t) the OE pin is toggled for the mask. when OE is toggled high, the display is toggled off. By applying this mask for every pixel, we are able to output our normal BCM color data and toggle the brightness value on or off randomly averaging out to the current brightness level. This provides fine-tuned brightness control (255 levels) while maintaining excellent color balance.

Alternatively, you can encode brightness data directly into the PWM data, however, this yields poor results for low brightness levels even when using 64 bits of BCM data. This feature is primarly for Pi3 & 4 models.

The mapping from 24bpp (or 32bpp) RGB data to BCM data is very optimized. It uses 128-bit SIMD vectors for the innermost loop. All 3 output ports are mapped in a single line of code outputing a single byte containing 6 bits to output (2 outputs per port (upper and lower display halfs) * 3 output ports)

for (int j=0; j<bit_depth; j++) {
        // mask off just this bit plane's data
        uint64_t mask = 1L << j;
        bcm_signal[bcm_offset++] =
            (BIT1(r0,  k) << ADDRESS_P0_R1) | (BIT1(g0,  k) << ADDRESS_P0_G1) | (BIT1(b0,  k) << ADDRESS_P0_B1) |
            (BIT1(r0b, k) << ADDRESS_P0_R2) | (BIT1(g0b, k) << ADDRESS_P0_G2) | (BIT1(b0b, k) << ADDRESS_P0_B2) |

            (BIT1(r1t, k) << ADDRESS_P1_R1) | (BIT1(g1t, k) << ADDRESS_P1_G1) | (BIT1(b1t, k) << ADDRESS_P1_B1) |
            (BIT1(r1b, k) << ADDRESS_P1_R2) | (BIT1(g1b, k) << ADDRESS_P1_G2) | (BIT1(b1b, k) << ADDRESS_P1_B2) |

            (BIT1(r2t, k) << ADDRESS_P2_R1) | (BIT1(g2t, k) << ADDRESS_P2_G1) | (BIT1(b2t, k) << ADDRESS_P2_B1) |
            (BIT1(r2b, k) << ADDRESS_P2_R2) | (BIT1(g2b, k) << ADDRESS_P2_G2) | (BIT1(b2b, k) << ADDRESS_P2_B2);

    }

Clone this wiki locally