23. Divide-by-4 Tick Generator

module tick_div4(
  input  wire clk,
  input  wire rst,
  output reg  tick
);
  reg [1:0] cnt;
  always @(posedge clk) begin
    tick <= rst ? 1'b0 : (cnt == 2'd3);
    cnt  <= rst ? 2'd0 : (cnt + 2'd1);
  end
endmodule

💡 Remember

  • A 2-bit register can count 0→1→2→3→0…; that’s all you need to make a /4 divider.
  • With non-blocking updates, the right-hand sides use the previous value of cnt for the whole clock edge.
    • tick <= (cnt == 3); and cnt <= cnt + 1; both see the old cnt, so tick is high only when the old value was 3, while cnt wraps to 0. That guarantees a one-cycle pulse.
  • Synchronous reset realigns the sequence: after deasserting rst on a rising edge, the first tick appears on the fourth subsequent rising edge (three low cycles between pulses).
  • Mental model you can reuse: “Divide-by-N” = “counter that rolls over after N-1; assert a one-cycle tick when the old count equals N-1.”

Testbench Code

`timescale 1ns/1ps
module tb_tick_div4;
  reg clk;
  reg rst;

  wire tick;
  wire expected_tick;
  wire mismatch;

  tick_div4 dut(.clk(clk), .rst(rst), .tick(tick));

  reg [1:0] exp_cnt;
  reg       exp_tick_q;
  assign expected_tick = exp_tick_q;

  always @(posedge clk) begin
    exp_tick_q <= rst ? 1'b0 : (exp_cnt == 2'd3);
    exp_cnt    <= rst ? 2'd0 : (exp_cnt + 2'd1);
  end

  assign mismatch = (tick !== expected_tick);

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

  initial begin
    $dumpfile("tb_tick_div4.vcd");
    $dumpvars(0,
      clk, rst,
      tick,
      expected_tick,
      mismatch
    );
  end

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

  integer TOTAL_TEST_CASES, TOTAL_PASSED_TEST_CASES, TOTAL_FAILED_TEST_CASES;
  reg [8*48-1:0] TC_NAME;

  task check_case;
    input [8*48-1:0] name;
    begin
      TC_NAME = name;
      #1;
      $display("CASE=%s : tick=%0b expected_tick=%0b %s",
               TC_NAME, tick, expected_tick, mismatch ? "MISMATCH" : "OK");
      TOTAL_TEST_CASES = TOTAL_TEST_CASES + 1;
      if (!mismatch) TOTAL_PASSED_TEST_CASES = TOTAL_PASSED_TEST_CASES + 1;
      else begin
        TOTAL_FAILED_TEST_CASES = TOTAL_FAILED_TEST_CASES + 1;
        $display("  FAILED: expected_tick=%0b", expected_tick);
      end
      apply_cycles(1);
    end
  endtask

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

    rst = 1'b1; apply_cycles(1);
    rst = 1'b0; apply_cycles(1);
    check_case("after_reset_no_tick");

    apply_cycles(2);
    check_case("no_tick_until_4th_edge");

    apply_cycles(1);
    check_case("first_tick_at_4th_edge");

    apply_cycles(3);
    check_case("no_tick_edges_5_to_7");

    apply_cycles(1);
    check_case("second_tick_at_8th_edge");

    rst = 1'b1; apply_cycles(1);
    rst = 1'b0; apply_cycles(1);
    check_case("realign_after_reset");

    apply_cycles(3);
    check_case("tick_again_4_edges_after_realign");

    $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