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 clockDC_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