A HUB75 panel is forgiving about the shape of your pixel data and unforgiving about the order of its control signals. The safe transaction is simple: blank the outputs, shift one row’s pixels, pulse the latch, change the row address while the panel is still blank, then enable the new row for its dwell time. This tutorial turns that sequence into a small VHDL-2008 state machine and explains the timing decisions that prevent ghosting and wrong-row flashes.
This is the implementation layer of the FPGA LED controller tutorial series. The examples below are deliberately small enough to simulate first. They do not pretend to know your panel’s scan ratio, logic thresholds, or maximum ribbon-cable clock; those are board and panel constraints you still need to verify.
The transaction a HUB75 panel expects
For a typical 1/16-scan RGB panel, the controller drives two physical rows at once. The six data inputs are usually named R1/G1/B1 and R2/G2/B2, while CLK shifts one column’s worth of those six bits into the panel’s internal registers. LAT transfers the shifted register into the display register. OE is active low on many panels, so oe_n = '1' means blank and oe_n = '0' means display. The exact connector labels vary; the electrical sequence is the important part.
| Signal | Safe controller rule | Why it matters |
|---|---|---|
| RGB data | Present the next six bits before the rising shift clock. | The panel samples data on its clock edge. |
| CLK | Toggle only while OE is blanked. | Some panels show shifted data immediately if OE is left enabled. |
| LAT | Pulse after the final column, with OE still blank. | The pulse copies the shift register into the active display register. |
| Row address | Change it after LAT and before OE is enabled. | Address settling while blank prevents the old row from lighting with new data. |
| OE | Enable only during the controlled dwell interval. | It defines the visible brightness time for the selected row. |
A six-state VHDL sequence
The reference module in the example bundle uses six states. There are two states per shifted column so the RGB bus is stable for one FPGA clock before the clock-high phase; the remaining states make the blank, latch, address, and display boundaries visible in a waveform.
| State | Outputs | Transition |
|---|---|---|
blank | OE high; column counter reset. | Move to shift_low. |
shift_low | OE high; drive the next RGB bits; CLK low. | Move to shift_high. |
shift_high | OE high; CLK high; RGB cleared. | Advance the column or move to latch. |
latch | OE high; LAT high for one FPGA clock. | Move to set_row. |
set_row | OE high; row address is now stable. | Move to display. |
display | OE low for the current dwell. | On dwell_done, advance the row and blank again. |
That extra set_row cycle is not decorative. It gives the row-address outputs a complete clock period to settle before OE falls. A fast design can combine operations if its timing constraints and panel data sheet justify doing so; starting with explicit states makes the first waveform understandable and gives you a clean place to add a calibrated blanking interval later.
Reference implementation
This compact scanner shifts a six-bit RGB word for each column, then waits for a caller-provided dwell_done pulse. A real BCM controller would supply a different pixel_bits value for each bitplane and calculate the dwell from that plane’s weight; the scan transaction stays the same.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity hub75_row_scanner is
generic (COLUMNS : positive := 64; ROW_GROUPS : positive := 16);
port (
clk : in std_logic;
rst : in std_logic;
dwell_done : in std_logic;
pixel_bits : in std_logic_vector(5 downto 0);
hub_clk : out std_logic;
lat : out std_logic;
oe_n : out std_logic;
row_addr : out natural range 0 to ROW_GROUPS - 1;
rgb : out std_logic_vector(5 downto 0)
);
end entity;
architecture rtl of hub75_row_scanner is
type state_t is (blank, shift_low, shift_high, latch, set_row, display);
signal state : state_t := blank;
signal col : natural range 0 to COLUMNS - 1 := 0;
signal row : natural range 0 to ROW_GROUPS - 1 := 0;
begin
process (clk)
begin
if rising_edge(clk) then
if rst = '1' then
state <= blank;
col <= 0;
row <= 0;
else
case state is
when blank =>
col <= 0;
state <= shift_low;
when shift_low =>
state <= shift_high;
when shift_high =>
if col = COLUMNS - 1 then
state <= latch;
else
col <= col + 1;
state <= shift_low;
end if;
when latch =>
state <= set_row;
when set_row =>
state <= display;
when display =>
if dwell_done = '1' then
if row = ROW_GROUPS - 1 then
row <= 0;
else
row <= row + 1;
end if;
state <= blank;
end if;
end case;
end if;
end if;
end process;
hub_clk <= '1' when state = shift_high else '0';
lat <= '1' when state = latch else '0';
oe_n <= '0' when state = display else '1';
row_addr <= row;
rgb <= pixel_bits when state = shift_low else (others => '0');
end architecture;
What this module intentionally does not solve
- Pixel memory.
pixel_bitsis a single input. Add a framebuffer read port or a bitplane scheduler when the transaction itself is proven. - Panel geometry. Some panels reverse columns, swap RGB channels, or require an address mapping different from a simple binary row number.
- Clock constraints. Set the I/O standard, output drive, slew, and maximum clock from the FPGA board and panel combination, not from this generic module.
- Brightness timing.
dwell_donebelongs to the PWM or BCM layer. The scanner should only consume a clean boundary pulse.
Timing budget: where the refresh rate comes from
For one row group and one bitplane, the scanner spends two controller clocks per column plus the latch and address-settling cycles. If the FPGA clock is Fclk, the approximate shift transaction time is:
Tshift ≈ (2 × COLUMNS + Tblank + Tlat + Trow) ÷ Fclk
That is only the row transaction. A complete BCM cycle repeats it once per bitplane and once per row address, with each plane’s visible dwell weighted by its bit significance. The HUB75 timing calculator is useful for the system-level ceiling; measure the actual state-machine overhead here before promising a refresh rate.
A concrete 64-column example
With COLUMNS = 64, two clocks per column consume 128 FPGA clocks. Add one blank entry, one latch cycle, and one row-address cycle and the transaction is roughly 131 clocks before the visible dwell. At 20 MHz that is 6.55 microseconds of shift/control time. A 16-row group therefore needs about 104.8 microseconds before any BCM dwell is included. That estimate is intentionally approximate: a production controller may pipeline the fetch, add a multi-cycle blanking window, or use a different panel scan ratio.
Bring-up checklist: prove the waveform before the panel
- Simulate reset. Confirm the scanner starts blank, at column zero, row zero, and with LAT low.
- Count columns. For a four-column simulation, see exactly four clock-high phases before LAT. For a 64-column panel, do not rely on visual inspection.
- Check the dangerous overlap. Assert that LAT and CLK never occur with OE enabled. This catches the most common ghosting bug directly.
- Check address settling. Change the row only while blank and give it a full controller cycle before enabling OE.
- Probe the connector. Start with a low clock, a single lit pixel, and a short cable. Increase speed only after the signals have clean edges at the panel.
- Add the brightness layer last. Feed a fixed dwell first, then one BCM plane, then a complete bitplane schedule. This separates a scan-order fault from a brightness-budget fault.
The companion article, How to test a HUB75 row scanner in VHDL, turns the three most important waveform checks into a self-checking GHDL testbench. Once that passes, continue to FPGA LED controller verification for reusable invariants around PWM, BCM, and frame-boundary updates.
Continue the FPGA LED controller path
Start at the FPGA LED controller tutorial hub, test this scanner with the self-checking VHDL testbench, then connect it to the double-buffered framebuffer and the CDC frame-swap handshake.
Leave a Reply