Verilog · 2026-06-16 · FPGA · RTL · PWM · timing

PWM generator (Verilog)

A PWM generator is just a counter and a comparator: count 0 .. period-1, drive the output high while the count is below duty. The frequency is f_clk / period and the duty cycle is duty / period, both set at runtime through input ports. The one detail that separates a toy from a usable peripheral is what happens when you change those values mid-cycle — do it naively and the output glitches. This module shadows period and duty and only adopts the new values at the start of the next period, exactly like a hardware timer with ARR/CCR preload.

module pwm_gen #(
  parameter integer WIDTH = 16          // counter width; sets the max period/duty
)(
  input  wire             clk,
  input  wire             rst_n,

  input  wire [WIDTH-1:0] period,       // clocks per PWM period; f_pwm = f_clk / period
  input  wire [WIDTH-1:0] duty,         // clocks high per period (0 = off, >= period = full)

  output reg              pwm
);
  reg [WIDTH-1:0] cnt;
  reg [WIDTH-1:0] periodReg, dutyReg;   // shadow regs: reloaded at each period start

  // End of the current period -> wrap and adopt the latest period/duty, so a
  // mid-cycle change can never glitch the output (ARR/CCR preload, in hardware).
  wire             wrap     = (cnt + 1'b1) >= periodReg;
  wire [WIDTH-1:0] cntNext  = wrap ? {WIDTH{1'b0}} : (cnt + 1'b1);
  wire [WIDTH-1:0] dutyNext = wrap ? duty : dutyReg;

  always @(posedge clk or negedge rst_n)
    if (!rst_n) begin
      cnt       <= {WIDTH{1'b0}};
      periodReg <= period;
      dutyReg   <= duty;
      pwm       <= 1'b0;
    end
    else begin
      cnt       <= cntNext;
      periodReg <= wrap ? period : periodReg;
      dutyReg   <= dutyNext;
      pwm       <= (cntNext < dutyNext);    // high for the first `duty` clocks
    end
endmodule

Frequency and duty

  • Frequency. One period is period clocks, so f_pwm = f_clk / period. With a 100 MHz clock, period = 1000 gives 100 kHz; period = 8 gives 12.5 MHz. The resolution is one clock, so smaller periods trade duty resolution for frequency.
  • Duty cycle. pwm is high while cnt < duty, i.e. for duty clocks out of period. duty = 0 is always low; duty >= period is always high; anything between is duty / period.
  • Glitch-free updates. Because wrap reloads periodReg/dutyReg only at the period boundary, writing new period/duty at any time just takes effect on the next cycle — the current pulse always completes cleanly. Drop the shadow registers and a duty change can cut a pulse short or stretch it, which shows up as audible noise on a motor or a flicker on an LED.

Testbench (self-checking)

Align to a period boundary, sample exactly period clocks and count the high ones — for a range of duties (including 0% and 100%) and two different frequencies. Build and run with iverilog -g2012 -o sim design.v tb.v && vvp sim.

`timescale 1ns/1ps
module tb;
  localparam integer W = 16;

  reg          clk = 0, rst_n = 0;
  reg  [W-1:0] period = 0, duty = 0;
  wire         pwm;

  pwm_gen #(
    .WIDTH (W)
  ) dut (
    .clk    (clk),
    .rst_n  (rst_n),
    .period (period),
    .duty   (duty),
    .pwm    (pwm)
  );

  always #5 clk = ~clk;       // 100 MHz -> 10 ns period

  integer pass = 0, fail = 0;

  // Align to a period boundary, sample exactly `p` clocks, count the high ones.
  task measure (input [W-1:0] p, input [W-1:0] d);
    integer i, high;
    reg [W-1:0] expHigh;
    begin
      period = p; duty = d;
      repeat (2 * p + 4) @(posedge clk);        // let the shadow regs reload
      @(posedge clk); #1;
      while (dut.cnt !== 0) begin @(posedge clk); #1; end

      high = 0;
      for (i = 0; i < p; i = i + 1) begin
        if (pwm) high = high + 1;
        @(posedge clk); #1;
      end

      expHigh = (d > p) ? p : d;                // duty clamps at period (full on)
      if ((high === expHigh) && (dut.cnt === 0)) begin
        pass = pass + 1;
        $display("  PASS  period=%0d duty=%0d -> high=%0d  (f = 100MHz/%0d)", p, d, high, p);
      end
      else begin
        fail = fail + 1;
        $display("  FAIL  period=%0d duty=%0d -> high=%0d exp=%0d cnt=%0d", p, d, high, expHigh, dut.cnt);
      end
    end
  endtask

  initial begin
    repeat (4) @(posedge clk);
    rst_n = 1;

    measure(10, 5);     // 50% at 10 MHz
    measure(10, 0);     // 0%  (always low)
    measure(10, 10);    // 100% (always high)
    measure(20, 6);     // 30% at 5 MHz
    measure(8,  4);     // 50% at 12.5 MHz (frequency changed)
    measure(100, 25);   // 25% at 1 MHz

    $display("  ==== %0d passed, %0d failed ====", pass, fail);
    $finish;
  end
endmodule
  PASS  period=10 duty=5 -> high=5  (f = 100MHz/10)
  PASS  period=10 duty=0 -> high=0  (f = 100MHz/10)
  PASS  period=10 duty=10 -> high=10  (f = 100MHz/10)
  PASS  period=20 duty=6 -> high=6  (f = 100MHz/20)
  PASS  period=8 duty=4 -> high=4  (f = 100MHz/8)
  PASS  period=100 duty=25 -> high=25  (f = 100MHz/100)
  ==== 6 passed, 0 failed ====

Usage

  • Pick WIDTH for the largest period you need (2**WIDTH - 1 clocks); duty resolution is the same WIDTH bits. For a fixed-frequency PWM, tie period to a constant and only drive duty. Size it against the PWM frequency & resolution tool.
  • Centre-aligned PWM (count up then down) halves the harmonic content for motor drives — swap the up-counter for an up/down counter and compare against duty on both ramps.
  • For analog output, follow it with an RC low-pass and check the ripple with the PWM → voltage (RC DAC) tool.