Counters and the wrap

Build a modulo-N counter whose terminal count matches its brief, count its period on a printed trace, and find the off-by-one and the width mistake before they reach a netlist.

Lesson
R2.4
Time
20 min
Runs on
Your browser
Check
5 questions

A digit counter was meant to show 0 to 9 and wrap. Its terminal-count compare read count == 10, so the register held 10 for one cycle before it wrapped: eleven values per turn. A copy reused for a 0-to-19 range kept its 4-bit register, which can never equal 19, and it ran 0 to 15.

The modulo-10 counter that compares with 10Edges 8 to 14 of the lab bench, one value per edge, printed after the edge settles.
0123456clkcount78910012tc
  1. Cycle 2

    Edge 10: count is 9, the last value the brief allows. tc is still 0, because it compares with 10.

  2. Cycle 3

    Edge 11: the adder has made 10. The compare is true, tc is 1, and a display would show 10.

  3. Cycle 4

    Edge 12: tc was 1 when this edge came, so the register loads 0. The turn had eleven values, 0 to 10.

Measured on the lab bench. tc is 1 at edges 11 and 22: eleven edges per turn.

Three decisions in every counter

A counter is a register, an adder and a decision about the next value. Three things must be written down before the RTL. The width: a W-bit register holds 0 to 2^W - 1 and wraps to 0 on its own. The terminal count: the value after which the counter returns to 0, when the range is not a power of two. And which value is the last one held.

A modulo-N counterArrows carry data. The register feeds the adder and the compare; the compare picks 0 or count + 1 for the next edge.
A modulo-N counterArrows carry data. The register feeds the adder and the compare; the compare picks 0 or count + 1 for the next edge. register count register W flops logic + 1 adder logic == LAST terminal count logic next value 0 or count + 1 output tc output, one cycle perturn countcountcount + 1select 0next edge
  1. Counting

    While the compare is false the next value is count + 1: 8 becomes 9.

  2. The last value

    With LAST = 9 the compare is true while the register holds 9. tc is 1 and the next value is 0.

    Follow the calls, values and waits
    1. data 9: count register → == LAST
Read the diagram as text
  • count register: W flops
  • + 1: adder
  • == LAST: terminal count
  • next value: 0 or count + 1
  • tc: output, one cycle per turn
  • count register → + 1: count
  • count register → == LAST: count
  • + 1 → next value: count + 1
  • == LAST → next value: select 0
  • == LAST → tc
  • next value → count register: next edge

Measured on this flow: 4 flops, 16 cells and 4 levels of logic for N = 10.

ShapeNext valueWhere the off-by-one waits
Free-runningcount + 1, wrapping at 2^Wthe width: 3 bits wrap after 7
Modulo-N0 when count == N-1, else count + 1comparing with N gives N + 1 values
Down counter with reloadthe reload value at the terminal count, else count - 1reload value and terminal count together set the period (R2.7)

Each shape is a few lines, and each has one place where it is easy to be one off.

The terminal count is the last value held

Read the hook trace again. The compare uses 10, the first value outside the range. So the counter must first reach 10 before the compare can see it, and 10 is held for a cycle. Compare with N - 1, the last value inside the range, and the register goes from 9 straight to 0.

Compare with N

localparam logic [W-1:0] LAST = W'(N);
assign tc = (count == LAST);

Eleven values, 0 to 10: tc at edges 11 and 22.

Compare with N - 1

localparam logic [W-1:0] LAST = W'(N - 1);
assign tc = (count == LAST);

Ten values, 0 to 9: tc at edges 10 and 20.

tc marks the cycle that holds the last value. A next stage, such as the tens digit of a clock, adds one when tc is 1. It acts on the same edge that wraps this counter, so both change together.

Never assume the period: count it. The bench prints every edge across two wraps. Subtract the edge numbers of two tc pulses and you have the period.

When the compare can never be true

The 0-to-19 copy compared a 4-bit count with 5'd19. For ==, both operands are first widened to the larger width. So count gets a 0 on top, and its largest value is 15. The compare is false forever, and the counter wraps at 16 on its own.

The synthesizer sees the same thing. A compare that is always false is a constant 0, so it removes the compare and the mux. Measured on this flow, that design is 10 cells and 3 levels of logic, the same as a plain 4-bit counter. Lint is more helpful: Verilator says %Warning-WIDTHEXPAND: Operator EQ expects 5 bits on the LHS, but LHS's VARREF 'count' generates 4 bits.

Design brief
PortDirectionWidthMeaning
clkinput1clock
rst_ninput1reset, asynchronous, active-low
countoutput[$clog2(N)-1:0]count runs 0, 1, ..., N-1, then 0
tcoutput11 while count is N-1
Reset
count is 0; the first edge after release makes it 1
Parameters
N, 2 or more; the lab bench reads 4 bits, so N up to 16
Corner cases
N a power of two (the compare and the natural wrap agree); the period across two wraps

The lab ships the counter that compares with N. Run it and find tc at edges 11 and 22. Change W'(N) to W'(N - 1) and run again: tc at edges 10 and 20. Then set the parameter to 12 and count the period again. Synthesize each: 16 cells for N = 10, 14 cells for N = 12, 4 flops in both.

Asked in interviews
  1. How do you build a modulo-N counter?

    A register of $clog2(N) bits, an adder, and a compare with N-1. When the compare is true the next value is 0, otherwise count + 1. A tc output marks the cycle that holds N-1.

  2. What is a terminal count?

    The value at which the counter turns around: N-1 for an up counter that wraps to 0, or the stop value of a down counter. The tc output is 1 in that cycle, so the next stage can act on the same edge.

  3. Where do off-by-one errors hide in counters?

    In the terminal count (N instead of N-1), in the reload value of a down counter, and in the width: a register too narrow for the constant never matches it. Print the value at every edge across a wrap and count the period.

Try it

Edges 0 and 1 are in reset. From edge 2 the counter climbs, reaches 10 at edge 11 with tc = 1, and loads 0 at edge 12. The next tc is at edge 22: eleven edges per turn.

Fix the terminal count to W'(N - 1) and the lines read 9 with tc = 1 at edges 10 and 20, then 0. Set N = 12 and tc comes at edges 12 and 24.

RTLIcarus Verilog · in your browser · Lint: Verilator service · Synthesize: Yosys service
Edit the design, then Run, Lint or Synthesize
Expected: e0 count=0 tc=0 e1 count=0 tc=0 e2 count=1 tc=0 e3 count=2 tc=0 e4 count=3 tc=0 e5 count=4 tc=0 e6 count=5 tc=0 e7 count=6 tc=0 e8 count=7 tc=0 e9 count=8 tc=0 e10 count=9 tc=0 e11 count=10 tc=1 e12 count=0 tc=0 e13 count=1 tc=0 e14 count=2 tc=0 e15 count=3 tc=0 e16 count=4 tc=0 e17 count=5 tc=0 e18 count=6 tc=0 e19 count=7 tc=0 e20 count=8 tc=0 e21 count=9 tc=0 e22 count=10 tc=1 e23 count=0 tc=0 e24 count=1 tc=0 e25 count=2 tc=0

Original design · 16 cells · 4 flip-flops · 0 latches · 4 logic levels. Synthesis uses Yosys; counts depend on the toolchain.

Read included file: tb.sv
`timescale 1ns/1ps
module tb;
  logic clk = 0;
  always #5 clk = ~clk;                   // rising edges at 5, 15, 25, ...
  logic rst_n = 1;                        // falls at 1 ns: a real edge, so an asynchronous reset acts before any clock
  int edge_n = -1;
  always @(posedge clk) edge_n = edge_n + 1;
  logic [3:0] count;                      // wide enough for N up to 16
  logic       tc;
  modn dut (.clk(clk), .rst_n(rst_n), .count(count), .tc(tc));
  // Advance to the next rising edge, wait for the design to settle, then print one line.
  task automatic tick();
    @(posedge clk); #1;
    $display("e%0d count=%0d tc=%0b", edge_n, count, tc);
  endtask
  initial begin
    #1 rst_n = 0; tick(); tick(); rst_n = 1;   // asserted at 1 ns, edges 0 and 1 in reset, released 1 ns after edge 1
    repeat (24) tick();
    $finish(0);                             // quiet: no "$finish called" line
  end
endmodule

Check yourself

Predict the outputA 3-bit free-running counter starts at 0 after reset. The program applies ten clock edges and prints. What is the output line?
module tb;
  logic clk = 0, rst_n = 1;
  logic [2:0] count;                 // 3 bits: 0 .. 7, then wraps
  always_ff @(posedge clk or negedge rst_n)
    if (!rst_n) count <= '0;
    else        count <= count + 1'b1;
  initial begin
    #1 rst_n = 0;                    // reset: count = 0
    #1 rst_n = 1;
    repeat (10) begin #1 clk = 1; #1 clk = 0; end
    $display("count=%0d", count);
    $finish(0);
  end
endmodule
Spot the bugEach option is the body of a counter meant to run 0 to 5 and wrap. The bench applies 13 edges and checks every value. Which body fails?
ConceptA modulo-10 counter feeds the tens digit of a clock. Which cycle should its tc output mark?
ConceptA 4-bit count is compared with 5'd16. What does count == 5'd16 do?
Read the waveformThis is the lab bench run on the counter that compares with 10, edges 10 to 23. How long is one turn of the counter?
Two wraps of the counter
012345678910111213count9100123456789100tc

One value per edge, from edge 10 on the left to edge 23 on the right.

Remember

  • A counter is a register, an adder and a decision about the next value; write down its width, its terminal count and which value is held last.
  • A modulo-N counter compares with N-1, the last value it holds; comparing with N adds a value to every turn.
  • tc marks the cycle that holds the last value, so the next stage acts on the same edge that wraps the counter.
  • A compare against a constant the register cannot hold is never true: the counter wraps at 2^W, the synthesizer removes the compare, and Lint warns WIDTHEXPAND.