VHDL HUB75 Row Scanner: Blanking, Latch & Address Timing

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.

SignalSafe controller ruleWhy it matters
RGB dataPresent the next six bits before the rising shift clock.The panel samples data on its clock edge.
CLKToggle only while OE is blanked.Some panels show shifted data immediately if OE is left enabled.
LATPulse after the final column, with OE still blank.The pulse copies the shift register into the active display register.
Row addressChange it after LAT and before OE is enabled.Address settling while blank prevents the old row from lighting with new data.
OEEnable 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.

StateOutputsTransition
blankOE high; column counter reset.Move to shift_low.
shift_lowOE high; drive the next RGB bits; CLK low.Move to shift_high.
shift_highOE high; CLK high; RGB cleared.Advance the column or move to latch.
latchOE high; LAT high for one FPGA clock.Move to set_row.
set_rowOE high; row address is now stable.Move to display.
displayOE 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_bits is 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_done belongs 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

  1. Simulate reset. Confirm the scanner starts blank, at column zero, row zero, and with LAT low.
  2. 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.
  3. Check the dangerous overlap. Assert that LAT and CLK never occur with OE enabled. This catches the most common ghosting bug directly.
  4. Check address settling. Change the row only while blank and give it a full controller cycle before enabling OE.
  5. 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.
  6. 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.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *