Category: FPGA & LED Controllers

  • Double-Buffered FPGA LED Framebuffer With Block RAM

    An LED scanner reads pixels continuously while a CPU, DMA engine, or animation core writes the next image. If both sides touch the displayed image, the panel can show half of one frame and half of another. A double buffer fixes tearing: one memory bank is scanned while the other is updated, and ownership changes only at a frame boundary.

    The architecture

    • Front bank: read by the real-time display scanner.
    • Back bank: written by the producer.
    • Swap request: says the back bank contains a complete frame.
    • Frame boundary: the only place the scanner changes banks.

    The producer must not write the bank currently being scanned. The scanner must not switch until the last row and last bitplane have completed.

    Infer two synchronous memories

    localparam int PIXELS = WIDTH * HEIGHT;
    
    logic [23:0] bank0 [0:PIXELS-1];
    logic [23:0] bank1 [0:PIXELS-1];
    logic        front_bank;
    logic [23:0] scan_pixel;
    
    always_ff @(posedge write_clk) begin
        if (write_en) begin
            if (front_bank) bank0[write_addr] <= write_data;
            else            bank1[write_addr] <= write_data;
        end
    end
    
    always_ff @(posedge scan_clk) begin
        if (front_bank) scan_pixel <= bank1[scan_addr];
        else            scan_pixel <= bank0[scan_addr];
    end

    This example writes the bank opposite front_bank. If the clocks are unrelated, do not pass front_bank directly into write logic without a synchronization and ownership protocol. A safer system acknowledges each swap back to the producer before accepting more writes.

    Swap only on a complete frame

    always_ff @(posedge scan_clk) begin
        if (rst) begin
            front_bank <= 1'b0;
            swap_ack   <= 1'b0;
        end else if (frame_done && swap_pending) begin
            front_bank <= ~front_bank;
            swap_ack   <= ~swap_ack;
        end
    end

    A toggle acknowledgement is easier to cross between clock domains than a one-cycle pulse. The producer holds the back bank stable after requesting a swap and waits until it sees the acknowledgement toggle.

    Account for memory latency

    Block RAM normally has a synchronous read. Request pixel N one clock before its bits are needed, and pipeline the column counter, plane index, and row metadata alongside the data. An off-by-one memory pipeline often appears as a one-column horizontal shift.

    Capacity example

    A 64×32 RGB framebuffer at 24 bits per pixel contains 49,152 bits. Double buffering requires 98,304 bits before padding and parity. Compare that requirement with the block-RAM geometry of the target FPGA, not only its headline memory total.

    Official reference

    AMD UG901 RAM HDL Coding Techniques documents portable coding patterns for distributed and block-memory inference.


    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.

  • Driving TLC5940 and TLC5955 LED Drivers From an FPGA

    Constant-current LED drivers move current regulation and grayscale timing closer to the LEDs. The FPGA sends brightness words, controls shift and latch signals, and supplies any required grayscale clock. This tutorial uses TI’s TLC5940 and TLC5955 as concrete examples while keeping the controller architecture reusable.

    Know what the driver provides

    DeviceChannelsGrayscaleSerial rate
    TLC59401612 bitUp to 30 MHz
    TLC59554816 bitUp to 25 MHz

    The TLC5940 uses an external grayscale clock and exposes BLANK, XLAT, error, and programming-mode behavior. The TLC5955 adds 16-bit grayscale, per-color brightness controls, dot correction, error detection, and display-repeat features. The exact bit packing and edge timing come from the selected datasheet—not from a generic SPI assumption.

    A reusable shift-and-latch engine

    typedef enum logic [2:0] {IDLE, LOAD, CLK_HI, CLK_LO, LATCH, DONE} state_t;
    
    always_ff @(posedge clk) begin
        case (state)
            IDLE: if (start) begin
                shift_reg <= frame_data;
                bit_count <= FRAME_BITS-1;
                state <= LOAD;
            end
            LOAD: begin
                sin   <= shift_reg[bit_count];
                sclk  <= 1'b0;
                state <= CLK_HI;
            end
            CLK_HI: begin
                sclk  <= 1'b1;
                state <= CLK_LO;
            end
            CLK_LO: begin
                sclk <= 1'b0;
                if (bit_count == 0) state <= LATCH;
                else begin bit_count <= bit_count - 1'b1; state <= LOAD; end
            end
            LATCH: begin
                lat   <= 1'b1;
                state <= DONE;
            end
            DONE: begin
                lat   <= 1'b0;
                done  <= 1'b1;
                state <= IDLE;
            end
        endcase
    end

    Parameterize frame width and bit order, then wrap the engine with a device-specific formatter. Register all outputs and constrain the interface as real external timing paths.

    Initialization sequence matters

    • Hold outputs blank while clocks and configuration stabilize.
    • Program current range, brightness control, and dot correction before grayscale data.
    • Load a known all-zero frame and latch it.
    • Start the grayscale clock or display-repeat function.
    • Release blanking only after a complete valid frame is active.

    Electrical and thermal checks

    The current-setting resistor, LED supply, output compliance voltage, simultaneous-channel current, package power dissipation, and PCB copper determine safe operation. Do not choose LED current from RTL. Calculate it from the device equations and verify worst-case thermal conditions.

    Official datasheets


    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.

  • Clock Domain Crossing for Tear-Free FPGA LED Updates

    An LED scanner often runs from a deterministic display clock while pixels arrive from a CPU bus, DMA engine, USB bridge, or network clock. Passing a “new frame” pulse or a multibit control word directly between those domains can cause lost events, metastability, and torn frames. This tutorial builds a safe frame-swap handshake.

    Synchronizers solve only single-bit sampling

    A two-register chain in the destination domain reduces the probability that metastability escapes into user logic. It does not make a changing multibit bus coherent, and a narrow source pulse can disappear entirely between destination clock edges.

    (* ASYNC_REG = "TRUE" *) logic req_meta, req_sync;
    
    always_ff @(posedge scan_clk) begin
        req_meta <= swap_request_toggle;
        req_sync <= req_meta;
    end

    Use a toggle for an event

    The producer flips a bit after finishing the back buffer. The scanner synchronizes that bit and compares it with the last value it serviced. The event persists until observed, regardless of the relative clock rates.

    logic serviced_toggle;
    logic swap_pending;
    
    always_ff @(posedge scan_clk) begin
        if (rst) begin
            serviced_toggle <= 1'b0;
            swap_pending     <= 1'b0;
        end else begin
            if (req_sync != serviced_toggle)
                swap_pending <= 1'b1;
    
            if (frame_done && swap_pending) begin
                front_bank      <= ~front_bank;
                serviced_toggle <= req_sync;
                swap_pending    <= 1'b0;
                ack_toggle      <= ~ack_toggle;
            end
        end
    end

    Synchronize ack_toggle back into the producer domain. The producer must not overwrite the completed back buffer until it observes the acknowledgement.

    Move multibit data safely

    • Use a dual-clock FIFO for a stream of pixels or commands.
    • Use dual-port memory plus ownership handshakes for framebuffers.
    • For a stable configuration bus, hold the bus unchanged from request through acknowledgement and capture it only after the synchronized request.
    • Never synchronize each bit of a bus independently; different bits can settle on different cycles.

    Constraints and structural checks

    Mark recognized synchronizer registers with the vendor-supported attribute, declare asynchronous clock relationships correctly, and run the tool’s CDC report. A false-path constraint hides timing analysis; it does not create safe hardware. Review every reported crossing and confirm the RTL structure matches the protocol.

    Verification strategy

    • Run source and destination clocks at non-integer ratios.
    • Randomize request timing around destination edges.
    • Assert every accepted request receives exactly one acknowledgement.
    • Assert bank ownership never overlaps.
    • Assert front_bank changes only on frame_done.

    Official references


    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.

  • Debugging FPGA LED Flicker, Ghosting, Tearing, and Wrong Colors

    LED display bugs are unusually visual: a single missed clock becomes a shifted column, an unsafe latch becomes a flash, and a clock-domain error becomes a torn frame. The fastest path to a fix is to translate the visible symptom into a timing hypothesis, then prove or reject it with a test pattern, assertion, oscilloscope, or logic analyzer.

    Symptom-to-cause map

    SymptomLikely causes
    Whole display flickersLow refresh rate, irregular frame timing, power droop
    Faint previous rowOE active during latch or address change, insufficient blanking
    Image shifted one columnWrong clock count, block-RAM latency, data changed on wrong edge
    Red and blue exchangedConnector or bit-order mismatch
    Horizontal tearFramebuffer changed before frame boundary
    Low-level shimmerLSB dwell too short, jitter, unsynchronized brightness update
    Camera bandsRefresh interaction with exposure or rolling shutter

    Use diagnostic patterns

    • Solid red, green, and blue reveal channel mapping.
    • Alternating columns reveal shift direction and off-by-one clocks.
    • A single moving pixel reveals row/column addressing.
    • One bitplane at a time reveals dwell-weight errors.
    • A gray ramp reveals gamma, missing low bits, and framebuffer format mistakes.

    Probe the timing contract

    Capture CLK, LAT, OE, row address, and at least one color data line. Trigger on LAT. Count column clocks between latches, measure how long OE remains active for each plane, and confirm the row address is stable before light is enabled. A passthrough adapter or spare test points make this much easier than probing a ribbon cable.

    Turn expectations into assertions

    // Outputs must be blank while latching.
    assert property (@(posedge clk) lat |-> oe_n);
    
    // A bank swap is legal only at the frame boundary.
    assert property (@(posedge clk) $changed(front_bank) |-> $past(frame_done));
    
    // The serializer emits exactly COLUMNS rising edges per row/plane.
    assert property (@(posedge clk) latch_event |-> shift_count == COLUMNS);

    Separate logic faults from electrical faults

    If the FPGA waveform is correct but the panel is not, inspect voltage levels, ground bounce, cable length, edge rate, current-supply wiring, and connector orientation. Reduce brightness and display one channel; if the failure changes with total current, power integrity is a stronger suspect than bitplane math.

    A disciplined debug order

    1. Verify the clock and reset.
    2. Verify active polarities with a static output.
    3. Count shifts and check data setup around the external clock.
    4. Check blank-latch-address-enable ordering.
    5. Measure row and frame rates.
    6. Enable one bitplane, then add planes.
    7. Add framebuffer swaps only after static frames are clean.
    8. Finally test maximum brightness, cable length, and camera behavior.

    Change one variable at a time and save known-good captures. LED controllers become manageable when every visible artifact is tied back to a small timing contract that can be measured.


    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.

  • External PWM Bitframes vs. 16-Bit Grayscale

    External PWM Bitframes vs. 16-Bit Grayscale

    An FPGA can drive an LED display in two very different ways: send every PWM bitplane on a strict schedule, or send complete intensity values to a driver that generates PWM internally. The pixels may look identical, but the timing, bandwidth, and failure modes are not.

    FPGA development board driving an RGB LED matrix beside an oscilloscope showing PWM waveforms
    External PWM makes the controller responsible for both pixel data and the timing of the light output.

    The difference in one sentence

    External PWM sends a timeline; internal grayscale sends a value. In the first architecture, the controller decides exactly when each bitplane is visible. In the second, the controller writes an intensity word and the driver turns that value into pulses.

    QuestionExternal PWM bitframes16-bit grayscale driver
    What crosses the interface?One bit per channel, once per bitplaneOne complete intensity word per channel
    Who schedules PWM?FPGA or microcontrollerLED driver
    Traffic for a static imageContinuousUsually only the initial load
    Timing freedomHighDefined by the driver
    Main design riskMissed real-time deadlinesUpdate and latch behavior

    External PWM bitframes

    With external PWM, the FPGA or microcontroller decomposes every channel value into bitplanes. For an N-bit value, plane 0 contains every channel’s least-significant bit, plane 1 contains the next bit, and so on. Each plane is displayed for a time proportional to its binary weight: 1, 2, 4, 8 … up to 2N-1.

    This technique is also called binary coded modulation, bit-angle modulation, or bitplane PWM. The display driver can be simple—it shifts, latches, and enables outputs—but the controller must continuously meet the shift-clock, latch, row-select, blanking, and output-enable deadlines.

    Small Verilog example: select one bitplane

    // 8-bit RGB value for one pixel; plane runs from 0 to 7.
    wire [7:0] red   = pixel_rgb[23:16];
    wire [7:0] green = pixel_rgb[15:8];
    wire [7:0] blue  = pixel_rgb[7:0];
    
    assign serial_rgb = { red[plane], green[plane], blue[plane] };

    The extraction is easy. The harder logic is the scheduler around it: load a plane, blank the outputs, latch it, select the row, enable it for the weighted interval, and repeat without jitter.

    Driver-managed 16-bit grayscale

    A grayscale-capable driver accepts a complete intensity word for each channel and stores it internally. A local timing engine compares those stored values against a grayscale counter and produces the output pulses. The controller updates the image; the driver keeps refreshing it.

    Close-up of an RGB LED driver board with logic probes and LEDs at stepped brightness levels
    A grayscale driver accepts intensity words and generates the high-rate PWM locally.

    This turns a continuous waveform-generation job into a frame-update job. Once a frame is loaded, the driver can display it while the FPGA calculates the next image, services another interface, or simply waits.

    Small C example: write one 16-bit channel

    static void write_grayscale(uint16_t level)
    {
        spi_write((uint8_t)(level >> 8));
        spi_write((uint8_t)(level));
    }
    
    write_grayscale(0x6000);  // about 37.5% of full scale

    A real driver may pack channels differently and may require a latch or frame-boundary signal, so its datasheet still controls the exact transaction.

    Bandwidth: count payload bits first

    For C color channels, B bitplanes, and a PWM refresh rate of F, the minimum external-bitplane payload is:

    external payload = C × B × F bits/s

    A 64 × 32 RGB panel has 6,144 channels. At 12-bit color and 200 complete PWM cycles per second, the raw payload is about 14.7 Mbit/s before latch, blanking, addressing, or protocol overhead. That traffic continues even when the picture is static.

    For a 16-bit grayscale driver updated at U frames per second:

    grayscale update payload = C × 16 × U bits/s

    The same panel updated at 60 frames per second needs about 5.9 Mbit/s of raw intensity data. Hold the image still and the update traffic can fall to zero while the driver continues its PWM. Update all 16 bits at 200 frames per second, however, and the raw payload rises to about 19.7 Mbit/s. Internal PWM is not automatically lower bandwidth; its advantage is that display refresh is decoupled from image updates.

    Timing pressure grows faster than bit depth

    The bitplane count grows linearly with resolution, but the weighted time range grows exponentially. A 12-bit PWM cycle contains 4,095 least-significant-bit time units; a 16-bit cycle contains 65,535. Ignoring scan multiplexing and overhead, the shortest slot is:

    TLSB = 1 ÷ [F × (2B − 1)]

    At 200 Hz and 12 bits, that is about 1.22 µs before row scanning, blanking, latching, and settling time take their share. This is why the least-significant planes are often where a design first shows brightness errors, flicker, or lost effective bits.

    What each architecture buys you

    External PWM

    • Full timing control: choose plane order, blanking, refresh strategy, and effective depth.
    • Room to experiment: temporal dithering, camera synchronization, and custom compensation live in FPGA logic.
    • Simple output devices: shift-register-style drivers can be inexpensive and transparent.
    • The cost: continuous traffic, more memory and scheduling logic, and hard real-time deadlines.

    Internal grayscale PWM

    • Lower controller workload: send intensity data and let dedicated hardware perform modulation.
    • Stable local timing: PWM is isolated from unrelated controller stalls and arbitration.
    • Efficient static display: stored values can be reused indefinitely.
    • The cost: less control, a more capable driver, and device-specific latch and buffering rules.

    Which should you choose?

    Choose external PWM when modulation itself is part of the design: you need deterministic custom timing, unusual plane ordering, camera-aware refresh, or the freedom to trade color depth against refresh dynamically. It is a natural fit when the FPGA already has enough I/O bandwidth, RAM, and timing margin.

    Choose a grayscale driver when you want a clean frame-oriented boundary, stable high-resolution brightness, and less continuous work in the controller. Verify whether the device double-buffers its grayscale memory, when new data takes effect, whether outputs must be blanked during latching, and how its PWM clock relates to your required refresh rate.

    The deciding question is not simply “How many bits?” It is where should the timing contract live? External bitframes keep it in programmable logic. A grayscale driver moves it next to the LEDs.


    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.

  • VHDL and FPGA

    I’ve recently been learning more and more about FPGA development and VHDL. It’s quite different from what many software engineers think “programming” is. FPGA’s require a different mindset for everything. There are no data structures, there are no bytes, there isn’t even any numbers. The only thing you have is bits. You have physical metal pins on the FPGA that handle inputs and outputs and they can send or receive physical bits (0 or 1).

    The way you write the design is also different. Internally, everything is basically just flip-flops. We also have this new found concept of a “clock” which is alien to programmers who don’t ever touch hardware.

    What is a clock? A clock is generally something like a crystal osc which is on the board. Heres an image of a 25 Mhz crystal osc.

    This thing has a piece of quartz crystal inside of it which vibrates at 25 million times per second, thus, 25 Mhz. There are varying speeds for the crystal oscillators, from 25, 50, 125, and even higher. They also have varying prices for the cost of these physical components. The 25 Mhz clock is quite useful because it can be used to do 1G ethernet.

    This brings me to another topic I’ve recently learned about, “PLL” https://en.wikipedia.org/wiki/Phase-locked_loop

    A PLL is a phased lock loop. It’s basically a way to use a clock to make more clocks. You can create “synthetic” clocks from a real clock. The way it works is simple. Let’s say you have a 25 Mhz clock. 1G ethernet needs a 125 Mhz clock. So, you can create a PLL clock with the source being the 25 Mhz clock and multiply it by 5 which will give you a 125 Mhz clock.

    Anyways, I might expand more upon this at a later point, but, this is quite interesting stuff.


    Build an FPGA LED controller next

    Continue from FPGA fundamentals into a complete practical series covering PWM, binary-coded modulation, HUB75 row scanning, framebuffers, clock-domain crossing, and self-checking SystemVerilog simulation.