Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • RTL Foundations36
  • Combinational Design25
  • Sequential Design27
    • Flip-Flops and Registers
    • Counters and Timers
      • Counting as Controlled State Evolution
      • Up/Down Counter with Reload
      • A Prescaler Feeding a Timer Tick
      • Watchdog Timer Example
      • A Gray-Code Counter
      • Mini Project: a Programmable Timer
    • Shift Registers and LFSRs
    • Pipelining
    • Stalls, Bubbles and Hazards
    • Retiming
  • FSM & Control27
  • Timing & Clocking39
  • Reset & CDC34
  • Memory & Buses57
  • Advanced RTL28
  • Senior & Interview76
  • Glossary and Notation
Loading lesson

‹Counters and Timers
  1. Topics
  2. RTL Design
  3. Sequential Design
  4. Counters and Timers
  5. Counting as Controlled State Evolution

Part 3 · Sequential · Intermediate

Counting as Controlled State Evolution

Binary and modulo-M counters, the terminal-count flag, and the off-by-one errors that cause most counter bugs.

On this page

    Why this matters

    A counter that is off by one fires a cycle early or late, and a modulo counter with the wrong width breaks when M is 1. Count and terminal-count timing is where the bugs live.

    After this lesson you can: write a free-running and a modulo-M counter, place the terminal-count pulse at M-1, and name the usual off-by-one mistakes.

    A free-running binary counter

    counter_bin.v
    1
    2
    3
    4
    5
    6
    7
    8
    module counter_bin #(parameter W = 4) (
      input  wire         clk, rst_n,
      output reg  [W-1:0] count
    );
      always @(posedge clk or negedge rst_n)
        if (!rst_n) count <= {W{1'b0}};
        else        count <= count + 1'b1;     // wraps naturally at 2^W
    endmodule

    An N-bit counter overflows from 2ᴺ−1 to 0, so its period is 2ᴺ cycles. That counter suits power-of-two period lengths.

    Modulo-M: count to M−1, then wrap

    Other periods reset at the terminal count M-1, the last value before wrapping. A one-cycle pulse on that value marks each period boundary.

    M may be 1 (a counter that holds 0 with tc always high): the count width is guarded as localparam int CW = (M > 1) ? $clog2(M) : 1; because $clog2(1) is 0, which would give a [-1:0] count.

    The bottom row, tc, is high when count holds M-1 (the module decodes count >= M-1, which is the same thing for every legal count).

    • Slots 1–5: count runs 0 through 4 and tc stays low.

    • Slot 6: count is 5 and tc is high.

    • Slot 7: count wraps to 0 and tc falls.

    • Slots 8–11: count runs 1 through 4 and tc stays low.

    • Slot 12: count is 5 and tc is high.

    • Slots 13–14: count is 0 then 1 and tc stays low.

    Off-by-one: the classic mistakes

    Mistake

    Effect

    Fix

    Compare to M instead of M-1

    Period is M+1

    Terminal count is M-1

    Decode tc from the next value

    tc pulse is one cycle early or late

    Decide whether tc is combinational on count, or registered, and document it

    Use $clog2(M) bits but M is a power of two with count == M

    M does not fit in the register

    Compare to M-1

    Counter free-runs past a limit it should stop at

    Needs saturation, not wrap

    See the up/down lesson

    Common pitfall

    tc here is combinational from count: it is high while count = M-1, and the wrap follows that cycle. A pulse aligned with the first count of the next lap needs a registered version; the interface defines which one applies.

    Tip

    Enabled counters advance only when en is high (else if (en) in the update). Holding en low mid-lap and checking that the count freezes tests the gating.

    Check yourself A mod-10 counter uses 4 bits. What fraction of the 4-bit range is unused, and what happens if noise forces it into an unused state?

    6 of 16 states (37.5%) are unused. The module below does this: it decodes tc = (count >= M-1) (>=, not ==), so a forced out-of-range value such as 12 in a mod-10 instance is treated as terminal and returns to 0 on the next clock, instead of counting through 13, 14 and 15 first. With == the counter would only recover once it had walked round to 0 by itself.

    Testbench

    The modulo-M counter plus a testbench that checks four laps and the tc pulse.

    counter_mod.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    module counter_mod #(parameter int M = 6) (   // M must be at least 1
      input  logic                  clk, rst_n,
      output logic [(M > 1 ? $clog2(M) : 1)-1:0] count,
      output logic                  tc            // high while count >= M-1 (equals M-1 in normal operation)
    );
      // M = 1 guard ($clog2(1) is 0, so [$clog2(M)-1:0] would be [-1:0]).
      localparam int CW = (M > 1) ? $clog2(M) : 1;
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n)          count <= '0;
        else if (tc)         count <= '0;          // wrap after the terminal count
        else                 count <= count + 1'b1;
    
      assign tc = (count >= M - 1);              // >= (not ==): a forced out-of-range count still wraps
    endmodule
    

    Remember

    • An N-bit free-running counter has a period of 2^N cycles.

    • A modulo-M counter wraps after M-1, and tc is high while count holds M-1.

    • Decode tc with >=, not ==, so an out-of-range value cannot lock the counter. Guard the width for M = 1.

    Keep going

    • Code lab: Enable counter

    • Interview: run a mock interview on this topic

    Related topics

    • Up/down counter with reload

    Previous

    67 / 350

    Next: Up/Down Counter with Reload

    Up next

    Up nextUp/Down Counter with Reload

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • A Prescaler Feeding a Timer Tick
    • Watchdog Timer Example
    • A Gray-Code Counter
    Modulo-6 counter: terminal count (tc) is high while count = 5clkcount01234501234501tc
    Simulated from the module below. tc is high for exactly one cycle per period, in the cycle where count equals M−1.