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
periodclocks, sof_pwm = f_clk / period. With a 100 MHz clock,period = 1000gives 100 kHz;period = 8gives 12.5 MHz. The resolution is one clock, so smaller periods trade duty resolution for frequency. - Duty cycle.
pwmis high whilecnt < duty, i.e. fordutyclocks out ofperiod.duty = 0is always low;duty >= periodis always high; anything between isduty / period. - Glitch-free updates. Because
wrapreloadsperiodReg/dutyRegonly at the period boundary, writing newperiod/dutyat 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
WIDTHfor the largest period you need (2**WIDTH - 1clocks); duty resolution is the sameWIDTHbits. For a fixed-frequency PWM, tieperiodto a constant and only driveduty. 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
dutyon both ramps. - For analog output, follow it with an RC low-pass and check the ripple with the PWM → voltage (RC DAC) tool.