FPGA LED Controller Verification With SystemVerilog Assertions

LED controllers are ideal candidates for self-checking simulation because their visible behavior reduces to countable timing invariants. Instead of approving a screenshot by eye, make the testbench fail when PWM duty cycle, BCM weight, HUB75 blanking, or frame-boundary ownership is wrong.

Four properties worth automating

  1. A PWM code produces the expected number of high clocks in one complete period.
  2. A BCM code produces the same number of illuminated base slots as its numeric value.
  3. HUB75 output enable remains inactive during shifting, latching, and row-address changes.
  4. Requested framebuffer or brightness data becomes active only at the defined boundary.

PWM: count the duty cycle

For an 8-bit counter, sample exactly 256 clock positions after the new brightness becomes active. The saturation convention matters: this design maps 255 to continuously on, so its expected high count is 256 rather than 255.

task automatic check_level(input logic [7:0] requested);
    int high_count = 0;
    int expected;

    level = requested;
    while (dut.active_level !== requested)
        @(negedge clk);

    repeat (256) begin
        @(negedge clk);
        high_count += pwm_out;
    end

    expected = (requested == 8'hff) ? 256 : requested;
    assert (high_count == expected)
        else $fatal("expected %0d, got %0d", expected, high_count);
endtask

Run boundary values before random values: 0, 1, midpoint, maximum minus one, and maximum. Off-by-one errors hide most effectively at rollover and full scale.

BCM: sum illuminated slots

An eight-bit BCM cycle contains 255 base slots. If every plane receives the correct binary weight, the total number of illuminated slots must equal the unsigned input value.

on_slots = 0;
repeat (255) begin
    on_slots += led_out;
    @(negedge clk);
end

assert (on_slots == requested)
    else $fatal("BCM %0d produced %0d on-slots",
                requested, on_slots);

This one invariant catches incorrect plane dwell, skipped planes, double-counted boundaries, and many update-timing errors. Add a second monitor that records per-plane durations when debugging which weight failed.

HUB75: assert blanking around dangerous edges

The most important safety property is simple: do not pulse the output latch while LEDs are enabled. A concurrent assertion can express it directly:

property latch_only_while_blanked;
    @(posedge clk) disable iff (rst)
        lat |-> oe_n;
endproperty

assert property (latch_only_while_blanked);

Also count shift-clock rising edges between latch pulses. For a 64-column chain, each completed transaction should contain exactly 64 edges unless the interface intentionally sends several chained panels. Assert that the row address changes only while output enable is inactive.

Check atomic updates

A multi-bit value cannot be treated like one asynchronous control bit. Synchronizing each bit separately allows the destination to observe a combination that never existed in the source domain. Use a handshake, asynchronous FIFO, or dual-port memory plus an ownership toggle. Then assert that active data remains stable throughout one PWM period, BCM cycle, or displayed frame.

property active_level_stable_inside_period;
    @(posedge clk) disable iff (rst)
        !period_start |=> $stable(active_level);
endproperty

assert property (active_level_stable_inside_period);

Avoid sampling races

Testbench code that samples on the same edge as nonblocking assignments can accidentally observe the previous state. A simple educational testbench can drive and sample at the opposite edge. A larger verification environment should use clocking blocks or clearly defined sampling regions. Whatever convention you choose, document it and keep it consistent.

Run it in continuous integration

The downloadable project includes a Makefile and runs with Icarus Verilog. The same test intent can be adapted to Verilator or a vendor simulator. A CI job only needs to install the simulator, run make test, and retain waveforms when a test fails.

unzip fpga-led-controller-examples.zip
cd fpga-led-controller-examples
make test

What simulation cannot prove

RTL simulation does not prove I/O voltage compatibility, signal integrity, power distribution, optical linearity, or that a panel meets its undocumented settling time. After the digital invariants pass, measure shift clock, latch, row address, output enable, and optical output on the real system. Verification narrows the hardware search space; it does not replace electrical validation.

Continue with the PWM implementation, BCM engine, and HUB75 row scanner, or browse the complete tutorial series.


FPGA LED controller tutorial series

This article is part of the FPGA LED Controller Tutorials learning path. Continue with PWM vs. BCM, the HUB75 timing calculator, or SystemVerilog verification.