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

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


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.

Comments

Leave a Reply

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