25. PWM with 4-bit Resolution

Generate a pulse-width–modulated output with 4-bit resolution. The synchronous active-high reset realigns the waveform. The duty value selects how many clocks per 16 the output stays high.

Requirements

  • Module name: pwm4_basic
  • Inputs:
    • clk
    • rst
    • duty[3:0]
  • Outputs:
    • pwm_out — high for duty clocks in every 16-clock frame; low otherwise
  • Clocking & edge behavior:
    • All registers sample on the rising edge of clk.
    • rst is sampled on the rising edge; when rst=1 at a rising edge, the internal counter becomes 0 and pwm_out=0.
    • duty is sampled on the rising edge and may change at any time; changes take effect on the next rising edge.
    • pwm_out updates only on rising edges and equals 1 when the sampled counter < duty, else 0.

Testbench Code

`timescale 1ns/1ps
module tb_pwm4_basic;
  reg        clk;
  reg        rst;
  reg  [3:0] duty;

  wire       pwm_out;
  wire       expected_pwm;
  wire       mismatch;

  pwm4_basic dut(.clk(clk), .rst(rst), .duty(duty), .pwm_out(pwm_out));

  reg [3:0] exp_cnt;
  reg       exp_pwm_q;
  assign expected_pwm = exp_pwm_q;

  always @(posedge clk) begin
    exp_pwm_q <= rst ? 1'b0 : (exp_cnt < duty);
    exp_cnt   <= rst ? 4'd0 : (exp_cnt + 4'd1);
  end

  assign mismatch = (pwm_out !== expected_pwm);

  initial begin
    clk = 1'b0;
    forever #5 clk = ~clk;
  end

  initial begin
    $dumpfile("tb_pwm4_basic.vcd");
    $dumpvars(0, clk, rst, duty, pwm_out, expected_pwm, mismatch);
  end

  integer TOTAL_TEST_CASES, TOTAL_PASSED_TEST_CASES, TOTAL_FAILED_TEST_CASES;

  task apply_cycles;
    input integer n; integer i;
    begin for (i=0;i<n;i=i+1) @(posedge clk); end
  endtask

  task run_frame_now;
    input [3:0] val;
    input [8*40-1:0] name;
    integer i, highs; reg case_failed;
    begin
      duty = val;
      highs = 0; case_failed = 0;
      for (i=0;i<16;i=i+1) begin
        @(posedge clk);
        if (expected_pwm === 1'b1) highs = highs + 1;
        if (mismatch) case_failed = 1;
      end
      $display("CASE=%s : duty=%0d highs=%0d expected_highs=%0d %s",
               name, val, highs, val, case_failed ? "MISMATCH" : "OK");
      TOTAL_TEST_CASES = TOTAL_TEST_CASES + 1;
      if (!case_failed && highs==val) TOTAL_PASSED_TEST_CASES = TOTAL_PASSED_TEST_CASES + 1;
      else TOTAL_FAILED_TEST_CASES = TOTAL_FAILED_TEST_CASES + 1;
    end
  endtask

  task run_frame_next;
    input [3:0] val;
    input [8*40-1:0] name;
    integer i, highs; reg case_failed;
    begin
      @(negedge clk); duty = val;
      highs = 0; case_failed = 0;
      for (i=0;i<16;i=i+1) begin
        @(posedge clk);
        if (expected_pwm === 1'b1) highs = highs + 1;
        if (mismatch) case_failed = 1;
      end
      $display("CASE=%s : duty=%0d highs=%0d expected_highs=%0d %s",
               name, val, highs, val, case_failed ? "MISMATCH" : "OK");
      TOTAL_TEST_CASES = TOTAL_TEST_CASES + 1;
      if (!case_failed && highs==val) TOTAL_PASSED_TEST_CASES = TOTAL_PASSED_TEST_CASES + 1;
      else TOTAL_FAILED_TEST_CASES = TOTAL_FAILED_TEST_CASES + 1;
    end
  endtask

  initial begin
    TOTAL_TEST_CASES = 0; TOTAL_PASSED_TEST_CASES = 0; TOTAL_FAILED_TEST_CASES = 0;

    rst = 1'b1; duty = 4'd0; apply_cycles(4);
    @(negedge clk) rst = 1'b0;

    run_frame_now (4'd0 , "duty_0_percent");
    run_frame_next(4'd1 , "duty_min_nonzero");
    run_frame_next(4'd4 , "duty_25_percent");
    run_frame_next(4'd8 , "duty_50_percent");
    run_frame_next(4'd12, "duty_75_percent_minus");
    run_frame_next(4'd15, "duty_max");

    $display("TOTAL_TEST_CASES=%0d",        TOTAL_TEST_CASES);
    $display("TOTAL_PASSED_TEST_CASES=%0d", TOTAL_PASSED_TEST_CASES);
    $display("TOTAL_FAILED_TEST_CASES=%0d", TOTAL_FAILED_TEST_CASES);
    $display("ALL_TEST_CASES_PASSED=%s",    (TOTAL_FAILED_TEST_CASES==0) ? "true" : "false");
    $finish;
  end
endmodule