Tutorial: PWM Signal Generation#


PWM Signal#

To implement a PWM (Pulse Width Modulation) signal using FPGA logic, an up-counter is used, and its current count value is compared against a specified threshold. When the count is less than the threshold, the PWM output is set high; otherwise, it is set low. The counter’s bit width determines the PWM duty cycle resolution. For example, an 8-bit counter provides up to 256 discrete duty cycle steps (from 0 to 255).

Figure: PWM signals with different duty cycles, ranging from 0% to 100%.

 


VHDL Implementation#

To implement a PWM signal using FPGA logic, an up-counter is used, and its current count value is compared against a specified threshold. When the count is less than the threshold, the PWM output is set high; otherwise, it is set low.

The following is a VHDL model of a PWM signal generator. The duty cycle is updated once per PWM period based on the DC input. The duty cycle value (DC) is sampled and loaded into an internal register (dc_reg) at the start of each new PWM period.

This VHDL entity defines three generics:

  • CLK_HZ : The frequency of the clock input (in Hz)
  • CLK_DIV : The clock division factor used to scale down the counter clock
  • DC_WIDTH : The bit width (resolution) of the PWM signal

For example, if the clock input is 50MHz, the clock divider is 50, and the PWM resolution is 10 bits, the PWM frequency can be calculated as:

 

-- File: pwm_gen.vhd
LIBRARY IEEE;
USE IEEE.STD_LOGIC_1164.ALL;
USE IEEE.NUMERIC_STD.ALL;

ENTITY pwm_gen IS
    GENERIC (
        CLK_HZ   : INTEGER := 50000000; -- 50MHz input clock
        CLK_DIV  : INTEGER := 50;       -- Clock divider (set for PWM freq.)
        DC_WIDTH : INTEGER := 10        -- Duty cycle resolution
    );
    PORT (
        CLK   : IN STD_LOGIC;           -- Clock input
        RST_N : IN STD_LOGIC;           -- Asynch. active-low reset
        DC    : IN STD_LOGIC_VECTOR(DC_WIDTH - 1 DOWNTO 0); -- Duty cycle (0–255)
        OE    : IN STD_LOGIC := '1';    -- Output enable
        PWM   : OUT STD_LOGIC           -- PWM output
    );
END pwm_gen;

ARCHITECTURE behavioral OF pwm_gen IS
    CONSTANT PWM_STEPS : INTEGER := 2 ** DC_WIDTH;

    SIGNAL clk_div_cnt : INTEGER RANGE 0 TO CLK_DIV - 1 := 0;
    SIGNAL clk_tick    : STD_LOGIC := '0';

    SIGNAL pwm_step_cnt : unsigned(DC_WIDTH - 1 DOWNTO 0) := (OTHERS => '0');
    SIGNAL dc_reg  : unsigned(DC_WIDTH - 1 DOWNTO 0) := (OTHERS => '0');
    SIGNAL pwm_out : STD_LOGIC := '0';
BEGIN

    clk_div_proc : PROCESS (CLK, RST_N)
    BEGIN
        IF RST_N = '0' THEN
            clk_div_cnt <= 0;
            clk_tick <= '0';
        ELSIF rising_edge(CLK) THEN
            IF clk_div_cnt = CLK_DIV - 1 THEN
                clk_div_cnt <= 0;
                clk_tick <= '1';
            ELSE
                clk_div_cnt <= clk_div_cnt + 1;
                clk_tick <= '0';
            END IF;
        END IF;
    END PROCESS;

    pwm_counter_proc : PROCESS (CLK, RST_N)
    BEGIN
        IF RST_N = '0' THEN
            pwm_step_cnt <= (OTHERS => '0');
            dc_reg <= (OTHERS => '0');
        ELSIF rising_edge(CLK) THEN
            IF clk_tick = '1' THEN
                IF pwm_step_cnt = PWM_STEPS - 1 THEN
                    pwm_step_cnt <= (OTHERS => '0');
                    dc_reg <= unsigned(DC); -- load DC at start of PWM period
                ELSE
                    pwm_step_cnt <= pwm_step_cnt + 1;
                END IF;
            END IF;
        END IF;
    END PROCESS;

    pwm_output_proc : PROCESS (CLK, RST_N)
    BEGIN
        IF RST_N = '0' THEN
            pwm_out <= '0';
        ELSIF rising_edge(CLK) THEN
            IF dc_reg = to_unsigned(PWM_STEPS - 1, DC_WIDTH) THEN
                pwm_out <= '1'; -- 100% duty cycle
            ELSIF dc_reg = to_unsigned(0, DC_WIDTH) THEN
                pwm_out <= '0'; -- 0% duty cycle
            ELSIF pwm_step_cnt < dc_reg THEN
                pwm_out <= '1';
            ELSE
                pwm_out <= '0';
            END IF;
        END IF;
    END PROCESS;

    -- Output assignment
    PWM <= pwm_out WHEN OE = '1' ELSE '0';

END behavioral;

 


VHDL Testbench#

A VHDL testbench is also provided below.

-- File: tb_pwm_gen.vhd
LIBRARY IEEE;
USE IEEE.STD_LOGIC_1164.ALL;
USE IEEE.NUMERIC_STD.ALL;

ENTITY tb_pwm_gen IS
END tb_pwm_gen;

ARCHITECTURE sim OF tb_pwm_gen IS
    -- Constants for simulation
    CONSTANT CLK_HZ     : INTEGER := 50000000;
    CONSTANT CLK_PERIOD : TIME := 20 ns; -- 50 MHz clock
    CONSTANT CLK_DIV    : INTEGER := 10;
    CONSTANT DC_WIDTH   : INTEGER := 8;

    SIGNAL value   : INTEGER := 0;

    -- DUT Signals
    SIGNAL t_CLK   : STD_LOGIC := '0';
    SIGNAL t_RST_N : STD_LOGIC := '0';
    SIGNAL t_DC    : STD_LOGIC_VECTOR(DC_WIDTH - 1 DOWNTO 0) := (OTHERS => '0');
    SIGNAL t_OE    : STD_LOGIC := '1';
    SIGNAL t_PWM   : STD_LOGIC;

    -- Component Declaration
    COMPONENT pwm_gen
        GENERIC (
            CLK_HZ   : INTEGER := 50000000;
            CLK_DIV  : INTEGER := 100;
            DC_WIDTH : INTEGER := 8
        );
        PORT (
            CLK   : IN STD_LOGIC;
            RST_N : IN STD_LOGIC;
            DC    : IN STD_LOGIC_VECTOR(DC_WIDTH - 1 DOWNTO 0);
            OE    : IN STD_LOGIC := '1';
            PWM   : OUT STD_LOGIC
        );
    END COMPONENT;

BEGIN
    -- Instantiate the Unit Under Test (UUT)
    uut : pwm_gen
    GENERIC MAP(
        CLK_HZ   => CLK_HZ,
        CLK_DIV  => CLK_DIV,
        DC_WIDTH => DC_WIDTH
    )
    PORT MAP(
        CLK   => t_CLK,
        RST_N => t_RST_N,
        DC    => t_DC,
        OE    => t_OE,
        PWM   => t_PWM
    );

    -- Clock generation
    clk_process : PROCESS
    BEGIN
        t_CLK <= '0';
        WAIT FOR CLK_PERIOD / 2;
        t_CLK <= '1';
        WAIT FOR CLK_PERIOD / 2;
    END PROCESS;

    -- Stimulus process
    stim_proc : PROCESS
    BEGIN
        t_RST_N <= '0';
        t_OE  <= '0';
        value <= 0;
        WAIT FOR 100 ns;
        t_RST_N <= '1';
        WAIT FOR 200 ns;
        t_OE  <= '1';
        value <= 15;
        WAIT FOR 200 us;
        value <= 63;
        WAIT FOR 0.2 ms;
        WAIT UNTIL t_PWM = '0';
        t_OE  <= '0';
        WAIT FOR 0.2 ms;
        t_OE  <= '1';
        value <= 127;
        WAIT FOR 0.2 ms;
        value <= 191;
        WAIT FOR 0.5 ms;
        value <= 255;
        WAIT;
    END PROCESS;

    t_DC <= STD_LOGIC_VECTOR( to_unsigned(value, DC_WIDTH) );

END sim;

-- ghdl -a pwm_gen.vhd tb_pwm_gen.vhd && ghdl -e tb_pwm_gen  
-- ghdl -r tb_pwm_gen --vcd=waveform.vcd --stop-time=2s
-- gtkwave waveform.vcd &

When simulating the design using a VHDL testbench, the GHDL simulator produced the following waveforms:

Figure: Simulation waveforms generated by GHDL for the PWM signal.

 


FPGA Implementation#

The following Tcl script is used to specify the FPGA pin assignments (for the MAX10 Lite FPGA board).

#============================================================
# FPGA assignments
#============================================================

#set_global_assignment -name FAMILY "MAX 10 FPGA"
#set_global_assignment -name DEVICE 10M50DAF484C7G

#============================================================
# CLOCK
#============================================================
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to CLK
set_location_assignment PIN_P11 -to CLK

#============================================================
# Slide Switches
#============================================================
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to DC[0]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to DC[1]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to DC[2]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to DC[3]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to DC[4]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to DC[5]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to DC[6]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to DC[7]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to DC[8]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to DC[9]
set_location_assignment PIN_C10 -to DC[0]
set_location_assignment PIN_C11 -to DC[1]
set_location_assignment PIN_D12 -to DC[2]
set_location_assignment PIN_C12 -to DC[3]
set_location_assignment PIN_A12 -to DC[4]
set_location_assignment PIN_B12 -to DC[5]
set_location_assignment PIN_A13 -to DC[6]
set_location_assignment PIN_A14 -to DC[7]
set_location_assignment PIN_B14 -to DC[8]
set_location_assignment PIN_F15 -to DC[9]

#============================================================
# PUSH BUTTONS
#============================================================

set_instance_assignment -name IO_STANDARD "3.3 V SCHMITT TRIGGER" -to RST_N
set_instance_assignment -name IO_STANDARD "3.3 V SCHMITT TRIGGER" -to OE
set_location_assignment PIN_B8 -to RST_N
set_location_assignment PIN_A7 -to OE

set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to PWM
set_location_assignment PIN_AB20 -to PWM

Figure: PWM output signals measured by a digital oscilloscope

 


Coding Exercise#

  • Generate 3 PWM signals (frequency approx. 500Hz) to drive 3 control pins of the RGB LED.
  • Use 3 slide switches per color to set the duty cycle of the corresponding PWM signal.

 


This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Created: 2025-06-15 | Last Updated: 2025-06-15