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
- Analog Devices: Using Lookup Tables to Perform Gamma Correction on LEDs
- AMD UG901: RAM HDL Coding Techniques
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.
Leave a Reply