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
VHDL-2008 reference implementation
This article uses synthesizable VHDL-2008. The complete tested VHDL bundle is linked below; adapt clock constraints, I/O standards, and timing parameters to the target board and panel.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity led_pwm is
generic (BITS : positive := 8);
port (
clk : in std_logic;
rst : in std_logic;
level : in unsigned(BITS - 1 downto 0);
pwm_out : out std_logic
);
end entity;
architecture rtl of led_pwm is
signal counter : unsigned(BITS - 1 downto 0) := (others => '0');
signal active_level : unsigned(BITS - 1 downto 0) := (others => '0');
begin
process (clk)
begin
if rising_edge(clk) then
if rst = '1' then
counter <= (others => '0');
active_level <= (others => '0');
else
counter <= counter + 1;
if counter = 0 then
active_level <= level;
end if;
end if;
end if;
end process;
-- Maximum code intentionally means continuously on.
pwm_out <= '1' when active_level = (active_level'range => '1') else
'1' when counter < active_level else '0';
end architecture;
Download the VHDL-2008 FPGA LED controller examples
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
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 VHDL-2008
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 VHDL-2008 verification.
Leave a Reply