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.
- Cycle 2
Edge 10:
countis 9, the last value the brief allows.tcis still 0, because it compares with 10. - Cycle 3
Edge 11: the adder has made 10. The compare is true,
tcis 1, and a display would show 10. - Cycle 4
Edge 12:
tcwas 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.
- Counting
While the compare is false the next value is
count + 1: 8 becomes 9. - The last value
With LAST = 9 the compare is true while the register holds 9.
tcis 1 and the next value is 0.Follow the calls, values and waits
- 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.
| Shape | Next value | Where the off-by-one waits |
|---|---|---|
| Free-running | count + 1, wrapping at 2^W | the width: 3 bits wrap after 7 |
| Modulo-N | 0 when count == N-1, else count + 1 | comparing with N gives N + 1 values |
| Down counter with reload | the reload value at the terminal count, else count - 1 | reload 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.
| Port | Direction | Width | Meaning |
|---|---|---|---|
| clk | input | 1 | clock |
| rst_n | input | 1 | reset, asynchronous, active-low |
| count | output | [$clog2(N)-1:0] | count runs 0, 1, ..., N-1, then 0 |
| tc | output | 1 | 1 while count is N-1 |
- Reset
countis 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
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
tcoutput marks the cycle that holds N-1.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.
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.
Synthesize the design to measure its cells and inspect the mapped gates.
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=0Original 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
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.
tcmarks 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.