Gamma-Corrected LED Dimming on FPGA With a Lookup Table

A mathematically linear PWM duty cycle does not look like a linear brightness ramp. The lower codes appear crowded together and the upper codes consume much of the electrical range. Gamma correction fixes the control curve before values reach the PWM or BCM engine. This tutorial generates a lookup table, infers it as ROM, and explains how to validate the result.

Why the ramp looks wrong

PWM controls average optical power approximately in proportion to duty cycle, but perceived brightness is nonlinear. A practical starting curve is output = inputγ, with γ around 2.0 to 2.4. It is a design starting point, not a universal constant: LED efficiency, optics, ambient light, and the desired creative response all matter.

Generate the ROM contents

# gamma_lut.py
gamma = 2.2
input_bits = 8
output_bits = 12
top = (1 << output_bits) - 1

with open("gamma_8_to_12.mem", "w") as f:
    for code in range(1 << input_bits):
        x = code / ((1 << input_bits) - 1)
        corrected = round((x ** gamma) * top)
        f.write(f"{corrected:03x}
")

The generated file has 256 hexadecimal words. Code 0 maps to 0 and code 255 maps to 4095. Keeping 12 output bits preserves small changes near black even when the external interface is only 8 bits.

Infer the table in SystemVerilog

module gamma_lut (
    input  logic         clk,
    input  logic [7:0]   linear_level,
    output logic [11:0]  pwm_level
);
    logic [11:0] rom [0:255];

    initial $readmemh("gamma_8_to_12.mem", rom);

    always_ff @(posedge clk)
        pwm_level <= rom[linear_level];
endmodule

The registered read gives the table a one-clock latency and usually makes block-memory inference easier. Pipeline the associated pixel address and color-channel metadata by the same amount.

Validate more than the endpoints

  • Assert that the table is monotonic.
  • Assert exact endpoints at zero and full scale.
  • Plot input code against corrected output before synthesis.
  • Display a slow ramp and look for plateaus, jumps, or low-level flicker.
  • Measure each RGB channel separately; identical digital curves do not guarantee a neutral white.

Calibration and dithering

Gamma correction shapes the response; dot correction fixes channel-to-channel differences; white-balance gains align red, green, and blue. Apply those stages deliberately and keep enough intermediate precision to avoid banding. If the hardware has fewer PWM bits than the corrected value, temporal dithering can alternate adjacent codes across frames, but only when the refresh rate is high and the frame sequence is deterministic.

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.

Comments

Leave a Reply

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