Skip to content

RTL Design Patterns · Chapter 3 · Counters, Timers & Pulse Generators

Up/Down & Loadable Counters

A counter is the simplest stateful datapath there is: a register whose next value is itself plus or minus one. Feed a register's output back through an adder and it stops merely remembering and starts sequencing, stepping through values on its own to address memory, time a delay, divide a clock, or track FIFO occupancy. A real counter does more than tick upward, though. It must also count down, hold when told to, load an arbitrary starting value, and flag when it hits its terminal count, all under a strict reset-beats-load-beats-enable priority. This lesson builds the whole family from a register, an adder, some muxes, and a comparator, covers wrap versus saturate, and exposes the signature off-by-one bug that hides in the terminal value. Everything is coded in SystemVerilog, Verilog, and VHDL.

Foundation12 min readRTL Design PatternsCounterUp/Down CounterLoadable CounterTerminal CountOff-By-One

Chapter 3 · Section 3.1 · Counters, Timers & Pulse Generators

1. The Engineering Problem

You are building a block that must do something every N cycles — issue a refresh, sample a slow input, advance a memory address, emit a strobe that a downstream divider turns into a slower clock. Nothing in Chapter 1 can do this: a mux, a decoder, a comparator all recompute from this instant's inputs and remember nothing, so none of them can know "how many cycles have passed." And a bare register from 2.1, on its own, only holds a value you feed it — it does not advance. What you need is a register that updates itself: one that, each cycle, takes its current value and produces the next one in a sequence. That structure is the counter, and it is the first thing you build once you have both a register and an adder, because it is the simplest possible stateful datapath — the first place the storage of Chapter 2 meets the arithmetic of a datapath.

This is not a made-up need. A counter is the beating heart of an enormous fraction of real hardware: an address generator sequences 0, 1, 2, … to walk a memory or a lookup table; a timer counts cycles to measure a delay or generate a periodic event; a clock divider counts to N and toggles, producing a slower clock; a FIFO tracks occupancy by counting writes up and reads down; a baud generator counts to a fixed value and pulses. In every case the shape is identical: a stored value that advances by a fixed step each cycle, under some control, and signals when it hits a boundary.

But "just increment a register" is where the subtlety hides. A useful counter is never only an up-counter. It must be able to count down as well as up (occupancy, backwards address walks). It must hold — freeze at its current value — when a control says "not this cycle." It must load an arbitrary value in parallel (start a timer at a preset, seed an address). And it must produce a terminal-count signal at exactly the right value — off by a single count and a clock divider divides by the wrong number, a timer fires a cycle early, a FIFO reports full one entry too soon. The moment you add those controls, you must also decide their priority: if a reset, a load, and a count all want to happen on the same edge, which wins? Get that ordering wrong and a load is silently overwritten by a count, or a reset fails to win — and the counter is quietly, dangerously incorrect.

the-need.v — a register that advances itself, with controls and a boundary flag
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // A bare register (2.1) only HOLDS what you feed it — it never advances:
   //   always @(posedge clk) count <= d;        // stores d; does not sequence
 
   // A COUNTER feeds the register's own output back through an adder:
   //   always @(posedge clk) count <= count + 1; // now it SEQUENCES 0,1,2,3,...
 
   // But a real counter must also: count DOWN, HOLD, LOAD a value, and signal
   // TERMINAL COUNT at exactly the right value, under a strict priority:
   //   reset  > load > enable   (reset wins; then load; then a count; else hold)
   //   tc = (count == terminal)  // the boundary flag — a comparator (1.5)
   // Off-by-one in that terminal value is the signature counter bug (this page).

2. Mental Model

3. Pattern Anatomy

The counter is built from one clocked register and a small next-value datapath wrapped around it.

The count register + the adder. At the core is the register of 2.1 — a WIDTH-bit value, updated on the rising edge with non-blocking <=, given a defined power-up value by its reset (2.3/2.4). What makes it a counter rather than a plain register is the combinational next-value logic feeding its D: an adder that computes count + 1 (and, for a down-counter, count − 1). The register stores; the adder advances. That feedback loop — output into adder, adder into input — is the entire distinction between "remember" and "count."

The count-enable (hold vs advance). A useful counter must be able to not count this cycle — to freeze while some upstream condition is false. That is a 2:1 mux (a register-with-enable, from 2.2) on the next-value path: when en is high, the register loads the adder's output (advance); when en is low, the register loads its own current value (count <= count, i.e. hold). The hold path is easy to forget — a counter that always increments cannot pause, and a great many "why did my timer run while it was supposed to be stalled?" bugs are a missing enable-hold path.

The up/down direction mux. To count in both directions, the adder's addend is selected by direction: up_down high selects +1, low selects −1 (equivalently, add +1 or add −1 / subtract 1). This is a 2:1 mux (1.1) choosing the step. Its width and signedness are deliberate: on an unsigned WIDTH-bit counter, counting down past 0 wraps to 2^WIDTH − 1, and counting up past 2^WIDTH − 1 wraps to 0 — the same modulo-2^WIDTH wrap in both directions.

The parallel-load mux. To start the counter at an arbitrary value — a timer preset, a seeded address — a load control selects load_val onto the register's D instead of the counted value. This is another 2:1 mux (1.1) on the next-value path: when load is high, count <= load_val; otherwise the count/hold logic applies. Load is a synchronous parallel load here (it takes effect on the next edge, like every other update), which keeps the whole counter in one clean clocked process.

The terminal count / carry-out — a comparator (1.5). The counter must announce when it reaches its boundary. Terminal count (tc) is an equality comparator: tc = (count == TERMINAL), where TERMINAL is the last value in the intended sequence (for an N-state up-counter starting at 0, that is N − 1, not N). Carry-out is the related but distinct signal that marks the wrap — the transition across the modulus (an up-counter's count == 2^WIDTH − 1 about to roll to 0). The two are not the same: tc marks your chosen terminal value; carry-out marks the natural wrap of the adder. Confusing them, or comparing against N instead of N − 1, is the off-by-one this whole page is built around (§6, §7).

Wrap vs saturate. When the count reaches its boundary and is told to advance again, one of two things must happen, and it is a design decision, not a default: wrap (roll over — …, N−1, 0, 1, … for a free-running or mod-N counter) or saturate (stick at the boundary — …, MAX, MAX, MAX for a saturating counter, e.g. a occupancy count that must not roll past full). The natural behaviour of a plain adder is wrap; saturation must be added (hold at the terminal when tc), and forgetting to add it — or adding it where you wanted wrap — is a distinct bug.

The priority: reset > load > enable. The controls are ranked, and the ranking is encoded as the order of decisions in the clocked process: reset wins (force the reset value), else load wins (force load_val), else if enabled, count (up or down), else hold. Writing this as an if (rst) … else if (load) … else if (en) … else (hold) chain in the clocked block is exactly a priority mux (1.1) on the register's D, with the priority you intend. Any other order changes the semantics: put en before load and a count can overwrite a load; put load before rst and a reset can fail to win.

The counter — a register (2.1) whose next value is an adder output, steered by muxes; tc is a comparator (1.5)

data flow
The counter — a register (2.1) whose next value is an adder output, steered by muxes; tc is a comparator (1.5)count register(2.1)WIDTH-bit, clocked <=, reset valueadder (+1 / -1)next = count ± 1 (direction mux)enable muxen ? advance : hold (count<=count)load muxload ? load_val : counted valuepriority:rst>load>enreset wins, then load, then countterminal count(1.5)tc = (count == TERMINAL = N-1)
The register's output feeds an adder that computes count ± 1 (the direction mux picks the step). The enable mux chooses advance vs hold; the load mux chooses load_val vs the counted value; the priority chain (reset > load > enable) ranks them and drives the register's next value. Separately, an equality comparator produces terminal count at the EXACT terminal value (N-1 for an N-state up-counter, not N). Everything here is a Chapter-1 mux/comparator or a Chapter-2 register — the counter composes them. This is language-independent: the same loop exists in SystemVerilog, Verilog, and VHDL.

One boundary is worth stating plainly: this page's counters use a synchronous load and a synchronous reset (updates take effect on the edge), and they are unsigned (numeric_std.unsigned in VHDL), so "down past zero" and "up past max" both wrap modulo 2^WIDTH unless saturation is explicitly added. Those are deliberate choices, stated so the widths and the wrap behaviour are unambiguous — not accidents.

4. Real RTL Implementation

This is the core of the page. We build three patterns — (a) a parameterized up counter (WIDTH-generic, count-enable, sync reset, terminal-count flag); (b) an up/down + loadable counter (direction mux + load mux with a clear reset > load > enable priority); and (c) the terminal-count / wrap off-by-one bug (buggy vs fixed) — and each is shown in SystemVerilog, Verilog, and VHDL, with both an RTL block and a self-checking, clocked testbench. The idea is identical in all three; the syntax differs, and seeing them side by side is what makes the pattern language-independent in your head.

Two disciplines run through every RTL block on this page: the update is non-blocking (<=), gated to the rising edge (it is a register, from 2.1), and the terminal comparator uses the exact terminal valueN − 1 for an N-state up-counter that starts at 0, never N. That single arithmetic fact is what pattern (c) gets wrong.

4a. The parameterized up counter — WIDTH-generic, count-enable, sync reset, terminal count

Start with the everyday counter: it counts up while enabled, holds while disabled, resets synchronously to zero, and raises tc on the last value of its sequence. The terminal value is a parameter (MAX), and tc is a pure comparator (1.5). The SystemVerilog form uses always_ff to document the sequential intent.

up_counter.sv — parameterized up counter (count-enable, sync reset, terminal count)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module up_counter #(
       parameter int WIDTH = 8,                 // counter width: 1..N bits
       parameter logic [WIDTH-1:0] MAX = '1     // TERMINAL value (default 2^WIDTH-1)
   )(
       input  logic             clk,
       input  logic             rst,            // synchronous, active-high: force count to 0
       input  logic             en,             // count-enable: advance only when high, else HOLD
       output logic [WIDTH-1:0] count,          // the stored count value
       output logic             tc              // terminal count: high WHEN count == MAX
   );
       // Sequential: the register of 2.1 with an adder on its feedback path.
       // Priority here is reset > enable (no load in this variant): reset wins,
       // else if enabled advance, else HOLD (the count<=count path — do NOT forget it).
       always_ff @(posedge clk) begin
           if (rst)      count <= '0;           // sync reset: defined value 0 on the edge
           else if (en)  count <= count + 1'b1; // ADVANCE: register + adder = a counter
           // else: count holds (no assignment on this path keeps its value — it is a
           //       clocked reg, so "no update" means HOLD, which is exactly what we want)
       end
 
       // Terminal count is a pure COMBINATIONAL comparator (1.5): it fires WHEN the
       // count EQUALS MAX — i.e. on the last value, not one past it. For an N-state
       // 0..N-1 sequence, MAX must be N-1. This exact value is the whole ballgame.
       assign tc = (count == MAX);
   endmodule

The SystemVerilog testbench is clocked: it generates a clock, drives en, and checks the count sequence against a reference model each cycle — including that tc fires on MAX and not on MAX-1 or one cycle late, and that a de-asserted en holds the count.

up_counter_tb.sv — clocked self-check: sequence, HOLD, and tc fires exactly on MAX
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module up_counter_tb;
       localparam int WIDTH = 4;
       localparam logic [WIDTH-1:0] MAX = 4'd9;      // terminal at 9 => a 0..9 (mod-10) sweep
       logic             clk = 0, rst, en;
       logic [WIDTH-1:0] count, tc_dummy;
       logic             tc;
       logic [WIDTH-1:0] expected;                    // reference model of the count
       int               errors = 0;
 
       up_counter #(.WIDTH(WIDTH), .MAX(MAX)) dut
           (.clk(clk), .rst(rst), .en(en), .count(count), .tc(tc));
 
       always #5 clk = ~clk;
 
       initial begin
           rst = 1; en = 0; expected = '0;
           @(negedge clk); @(negedge clk);            // hold reset a couple of edges
           @(posedge clk); #1;
           assert (count === '0) else begin $error("reset: count=%0d exp=0", count); errors++; end
           rst = 0;
 
           // COUNT UP while enabled; check the sequence AND terminal count exactly.
           en = 1;
           for (int cyc = 0; cyc < 25; cyc++) begin
               @(negedge clk);                        // drive/sample stimulus off the edge
               @(posedge clk); #1;                    // register advances here
               expected = (expected == MAX) ? '0 : expected + 1'b1;   // reference wraps at MAX
               assert (count === expected)
                   else begin $error("cyc=%0d count=%0d exp=%0d", cyc, count, expected); errors++; end
               // tc must be high IFF count == MAX — not at MAX-1, not one cycle late:
               assert (tc === (count == MAX))
                   else begin $error("cyc=%0d tc=%b count=%0d MAX=%0d", cyc, tc, count, MAX); errors++; end
           end
 
           // HOLD: drop enable and confirm the count does NOT advance.
           en = 0; expected = count;
           repeat (3) begin @(negedge clk); @(posedge clk); #1;
               assert (count === expected)
                   else begin $error("HOLD broke: count=%0d exp=%0d", count, expected); errors++; end
           end
 
           if (errors == 0) $display("PASS: up_counter sequences, holds, and tc fires exactly on MAX");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

The Verilog form is identical in intent; the only differences are reg/wire typing and $display-based checking. The comparator and the reset-then-enable priority are the same.

up_counter.v — parameterized up counter in Verilog (count-enable, sync reset, tc)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module up_counter #(
       parameter WIDTH = 8,
       parameter [WIDTH-1:0] MAX = {WIDTH{1'b1}}     // terminal value (default 2^WIDTH-1)
   )(
       input  wire             clk,
       input  wire             rst,                  // sync, active-high
       input  wire             en,                   // count-enable
       output reg  [WIDTH-1:0] count,
       output wire             tc                    // terminal count
   );
       always @(posedge clk) begin
           if (rst)      count <= {WIDTH{1'b0}};     // sync reset to 0
           else if (en)  count <= count + 1'b1;      // advance (register + adder)
           // else HOLD (no assignment on the clocked reg keeps the value)
       end
       assign tc = (count == MAX);                   // comparator: high WHEN count == MAX
   endmodule
up_counter_tb.v — clocked self-check; sequence, HOLD, tc exact (print PASS/FAIL)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module up_counter_tb;
       parameter WIDTH = 4;
       parameter [WIDTH-1:0] MAX = 4'd9;             // 0..9 mod-10 sweep
       reg              clk, rst, en;
       wire [WIDTH-1:0] count;
       wire             tc;
       reg  [WIDTH-1:0] expected;
       integer          cyc, errors;
 
       up_counter #(.WIDTH(WIDTH), .MAX(MAX)) dut
           (.clk(clk), .rst(rst), .en(en), .count(count), .tc(tc));
 
       initial clk = 1'b0;
       always #5 clk = ~clk;
 
       initial begin
           errors = 0; rst = 1'b1; en = 1'b0; expected = {WIDTH{1'b0}};
           @(negedge clk); @(negedge clk);
           @(posedge clk); #1;
           if (count !== {WIDTH{1'b0}}) begin
               $display("FAIL: reset count=%0d exp=0", count); errors = errors + 1;
           end
           rst = 1'b0; en = 1'b1;
 
           for (cyc = 0; cyc < 25; cyc = cyc + 1) begin
               @(negedge clk);
               @(posedge clk); #1;
               expected = (expected == MAX) ? {WIDTH{1'b0}} : expected + 1'b1;   // reference
               if (count !== expected) begin
                   $display("FAIL: cyc=%0d count=%0d exp=%0d", cyc, count, expected);
                   errors = errors + 1;
               end
               if (tc !== (count == MAX)) begin       // tc fires exactly on MAX
                   $display("FAIL: cyc=%0d tc=%b count=%0d MAX=%0d", cyc, tc, count, MAX);
                   errors = errors + 1;
               end
           end
 
           en = 1'b0; expected = count;               // HOLD check
           for (cyc = 0; cyc < 3; cyc = cyc + 1) begin
               @(negedge clk); @(posedge clk); #1;
               if (count !== expected) begin
                   $display("FAIL: HOLD broke count=%0d exp=%0d", count, expected);
                   errors = errors + 1;
               end
           end
 
           if (errors == 0) $display("PASS: up_counter sequences, holds, tc exact");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

In VHDL the same counter is a process(clk) guarded by rising_edge(clk), with the count held as an unsigned from numeric_std so + 1 and the = MAX comparison are unambiguous. The generic plays the role of the Verilog parameter.

up_counter.vhd — parameterized up counter in VHDL (numeric_std unsigned, rising_edge)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity up_counter is
       generic (
           WIDTH : positive := 8;
           MAXV  : natural  := 255                    -- terminal value (2^WIDTH-1 by default)
       );
       port (
           clk   : in  std_logic;
           rst   : in  std_logic;                     -- sync, active-high
           en    : in  std_logic;                     -- count-enable
           count : out std_logic_vector(WIDTH-1 downto 0);
           tc    : out std_logic                      -- terminal count
       );
   end entity;
 
   architecture rtl of up_counter is
       signal cnt : unsigned(WIDTH-1 downto 0);       -- unsigned so + 1 and = MAX are exact
   begin
       -- Sequential: the register of 2.1 with an adder. Priority reset > enable.
       process (clk)
       begin
           if rising_edge(clk) then
               if rst = '1' then
                   cnt <= (others => '0');            -- sync reset to 0
               elsif en = '1' then
                   cnt <= cnt + 1;                     -- advance
               end if;                                 -- else: HOLD (no assignment)
           end if;
       end process;
 
       count <= std_logic_vector(cnt);
       -- Terminal count: combinational comparator (1.5), fires WHEN cnt = MAXV.
       tc <= '1' when cnt = to_unsigned(MAXV, WIDTH) else '0';
   end architecture;

The VHDL testbench generates a clock, drives en, and self-checks with assert ... report ... severity error after each rising edge — the count sequence, the terminal-count timing, and the hold behaviour.

up_counter_tb.vhd — clocked self-check (assert report severity); sequence, tc, HOLD
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity up_counter_tb is
   end entity;
 
   architecture sim of up_counter_tb is
       constant WIDTH : positive := 4;
       constant MAXV  : natural  := 9;                -- 0..9 mod-10 sweep
       signal clk   : std_logic := '0';
       signal rst   : std_logic := '1';
       signal en    : std_logic := '0';
       signal count : std_logic_vector(WIDTH-1 downto 0);
       signal tc    : std_logic;
   begin
       dut : entity work.up_counter
           generic map (WIDTH => WIDTH, MAXV => MAXV)
           port map (clk => clk, rst => rst, en => en, count => count, tc => tc);
 
       clk <= not clk after 5 ns;
 
       stim : process
           variable expected : natural := 0;
       begin
           wait until falling_edge(clk);
           wait until falling_edge(clk);
           wait until rising_edge(clk); wait for 1 ns;
           assert count = std_logic_vector(to_unsigned(0, WIDTH))
               report "reset: count /= 0" severity error;
           rst <= '0'; en <= '1';
 
           for cyc in 0 to 24 loop
               wait until falling_edge(clk);
               wait until rising_edge(clk); wait for 1 ns;
               if expected = MAXV then expected := 0; else expected := expected + 1; end if;
               assert count = std_logic_vector(to_unsigned(expected, WIDTH))
                   report "count sequence mismatch" severity error;
               -- tc must be '1' exactly when the count equals MAXV:
               if expected = MAXV then
                   assert tc = '1' report "tc did not fire on MAX" severity error;
               else
                   assert tc = '0' report "tc fired off the terminal value" severity error;
               end if;
           end loop;
 
           en <= '0';                                  -- HOLD check
           for k in 0 to 2 loop
               wait until falling_edge(clk);
               wait until rising_edge(clk); wait for 1 ns;
               assert expected = to_integer(unsigned(count))
                   report "HOLD broke: count advanced while disabled" severity error;
           end loop;
 
           report "up_counter self-check complete" severity note;
           wait;
       end process;
   end architecture;

4b. The up/down + loadable counter — direction mux + load mux, priority reset > load > enable

Now the full family member: it counts up or down, loads a parallel value, holds when disabled, and resets — all under the strict reset > load > enable priority, written as an if / else if / else if chain in the clocked process (which is the priority mux on the register's D). Down past zero and up past MAX both wrap modulo 2^WIDTH, and tc fires on the up terminal MAX and the down terminal 0 so a caller can detect either boundary.

updown_counter.sv — up/down + loadable, priority reset > load > enable
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module updown_counter #(
       parameter int WIDTH = 8,
       parameter logic [WIDTH-1:0] MAX = '1          // up-terminal value (default 2^WIDTH-1)
   )(
       input  logic             clk,
       input  logic             rst,                 // sync, active-high  (HIGHEST priority)
       input  logic             load,                // sync parallel load (2nd priority)
       input  logic [WIDTH-1:0] load_val,            //   value loaded when load is high
       input  logic             en,                  // count-enable       (3rd priority)
       input  logic             up_down,             // 1 = count up, 0 = count down (direction mux)
       output logic [WIDTH-1:0] count,
       output logic             tc                   // terminal count at the ACTIVE direction's end
   );
       // The priority chain IS a mux on the register's D:
       //   reset wins -> else load wins -> else if enabled count (up/down) -> else HOLD.
       always_ff @(posedge clk) begin
           if (rst)          count <= '0;                     // 1. reset always wins
           else if (load)    count <= load_val;               // 2. parallel load beats a count
           else if (en)      count <= up_down ? count + 1'b1  // 3a. direction mux: +1 up ...
                                              : count - 1'b1;  // 3b.               ... -1 down
           // else HOLD (disabled: keep the current count)
       end
 
       // Terminal count depends on DIRECTION: the up-terminal is MAX, the down-terminal
       // is 0. Fire tc on whichever end the counter is heading toward — exact values,
       // never MAX+1 or "one below 0".
       assign tc = up_down ? (count == MAX) : (count == '0);
   endmodule

The SystemVerilog testbench exercises every control: it loads a value and verifies it lands, counts up and checks the sequence, counts down and checks the wrap, holds when disabled, and confirms tc at both terminals — and it checks the priority by asserting load and en on the same edge and proving load wins.

updown_counter_tb.sv — clocked self-check: load, up, down, HOLD, priority, tc
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module updown_counter_tb;
       localparam int WIDTH = 4;
       localparam logic [WIDTH-1:0] MAX = 4'd15;         // full 4-bit range, wraps at 16
       logic             clk = 0, rst, load, en, up_down;
       logic [WIDTH-1:0] load_val, count;
       logic             tc;
       logic [WIDTH-1:0] expected;
       int               errors = 0;
 
       updown_counter #(.WIDTH(WIDTH), .MAX(MAX)) dut
           (.clk(clk), .rst(rst), .load(load), .load_val(load_val),
            .en(en), .up_down(up_down), .count(count), .tc(tc));
 
       always #5 clk = ~clk;
 
       task automatic step; @(negedge clk); @(posedge clk); #1; endtask
 
       initial begin
           rst = 1; load = 0; en = 0; up_down = 1; load_val = '0;
           step;
           assert (count === '0) else begin $error("reset failed"); errors++; end
           rst = 0;
 
           // LOAD a value and verify it lands next edge.
           load = 1; load_val = 4'd5; step; load = 0;
           expected = 4'd5;
           assert (count === expected) else begin $error("load: count=%0d exp=5", count); errors++; end
 
           // COUNT UP from 5; check the sequence and the up-terminal tc at MAX.
           en = 1; up_down = 1;
           repeat (12) begin
               step;
               expected = (expected == MAX) ? '0 : expected + 1'b1;   // wraps at MAX
               assert (count === expected) else begin $error("up: count=%0d exp=%0d", count, expected); errors++; end
           end
           assert (tc === (count == MAX)) else begin $error("up-tc wrong at count=%0d", count); errors++; end
 
           // COUNT DOWN; check the wrap through 0 -> MAX and the down-terminal tc at 0.
           up_down = 0;
           repeat (10) begin
               step;
               expected = (expected == '0) ? MAX : expected - 1'b1;   // wraps at 0
               assert (count === expected) else begin $error("down: count=%0d exp=%0d", count, expected); errors++; end
           end
           assert (tc === (count == '0)) else begin $error("down-tc wrong at count=%0d", count); errors++; end
 
           // HOLD: disable and confirm no movement.
           en = 0; expected = count;
           repeat (3) begin step;
               assert (count === expected) else begin $error("HOLD broke"); errors++; end
           end
 
           // PRIORITY: assert load AND en together — LOAD must win over the count.
           load = 1; load_val = 4'd12; en = 1; up_down = 1; step; load = 0;
           expected = 4'd12;                             // loaded, NOT 12±1 from a count
           assert (count === expected) else begin $error("priority: load lost to en, count=%0d exp=12", count); errors++; end
 
           if (errors == 0) $display("PASS: updown_counter load/up/down/HOLD/priority/tc all correct");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

The Verilog form flattens the same logic into reg outputs and a $display-based self-check. The if / else if / else if priority chain and the direction ternary are identical.

updown_counter.v — up/down + loadable in Verilog (priority reset > load > enable)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module updown_counter #(
       parameter WIDTH = 8,
       parameter [WIDTH-1:0] MAX = {WIDTH{1'b1}}
   )(
       input  wire             clk,
       input  wire             rst,                 // sync (highest priority)
       input  wire             load,                // sync parallel load (2nd)
       input  wire [WIDTH-1:0] load_val,
       input  wire             en,                  // count-enable (3rd)
       input  wire             up_down,             // 1 = up, 0 = down
       output reg  [WIDTH-1:0] count,
       output wire             tc
   );
       always @(posedge clk) begin
           if (rst)          count <= {WIDTH{1'b0}};        // 1. reset wins
           else if (load)    count <= load_val;             // 2. load beats a count
           else if (en)      count <= up_down ? count + 1'b1
                                              : count - 1'b1; // 3. direction mux; else HOLD
       end
       assign tc = up_down ? (count == MAX) : (count == {WIDTH{1'b0}});
   endmodule
updown_counter_tb.v — clocked self-check: load, up, down, HOLD, priority, tc
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module updown_counter_tb;
       parameter WIDTH = 4;
       parameter [WIDTH-1:0] MAX = 4'd15;
       reg              clk, rst, load, en, up_down;
       reg  [WIDTH-1:0] load_val, count_exp;
       wire [WIDTH-1:0] count;
       wire             tc;
       integer          k, errors;
 
       updown_counter #(.WIDTH(WIDTH), .MAX(MAX)) dut
           (.clk(clk), .rst(rst), .load(load), .load_val(load_val),
            .en(en), .up_down(up_down), .count(count), .tc(tc));
 
       initial clk = 1'b0;
       always #5 clk = ~clk;
 
       task step; begin @(negedge clk); @(posedge clk); #1; end endtask
 
       initial begin
           errors = 0;
           rst = 1'b1; load = 1'b0; en = 1'b0; up_down = 1'b1; load_val = {WIDTH{1'b0}};
           step;
           if (count !== {WIDTH{1'b0}}) begin $display("FAIL: reset"); errors = errors + 1; end
           rst = 1'b0;
 
           // LOAD
           load = 1'b1; load_val = 4'd5; step; load = 1'b0;
           count_exp = 4'd5;
           if (count !== count_exp) begin $display("FAIL: load count=%0d exp=5", count); errors = errors + 1; end
 
           // UP
           en = 1'b1; up_down = 1'b1;
           for (k = 0; k < 12; k = k + 1) begin
               step;
               count_exp = (count_exp == MAX) ? {WIDTH{1'b0}} : count_exp + 1'b1;
               if (count !== count_exp) begin $display("FAIL: up count=%0d exp=%0d", count, count_exp); errors = errors + 1; end
           end
           if (tc !== (count == MAX)) begin $display("FAIL: up-tc at count=%0d", count); errors = errors + 1; end
 
           // DOWN (wrap through 0 -> MAX)
           up_down = 1'b0;
           for (k = 0; k < 10; k = k + 1) begin
               step;
               count_exp = (count_exp == {WIDTH{1'b0}}) ? MAX : count_exp - 1'b1;
               if (count !== count_exp) begin $display("FAIL: down count=%0d exp=%0d", count, count_exp); errors = errors + 1; end
           end
           if (tc !== (count == {WIDTH{1'b0}})) begin $display("FAIL: down-tc at count=%0d", count); errors = errors + 1; end
 
           // HOLD
           en = 1'b0; count_exp = count;
           for (k = 0; k < 3; k = k + 1) begin
               step;
               if (count !== count_exp) begin $display("FAIL: HOLD broke"); errors = errors + 1; end
           end
 
           // PRIORITY: load + en together => load wins
           load = 1'b1; load_val = 4'd12; en = 1'b1; up_down = 1'b1; step; load = 1'b0;
           if (count !== 4'd12) begin $display("FAIL: priority load lost, count=%0d exp=12", count); errors = errors + 1; end
 
           if (errors == 0) $display("PASS: updown_counter load/up/down/HOLD/priority/tc correct");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

In VHDL the priority chain is an if / elsif / elsif inside rising_edge(clk), the direction is a conditional on up_down, and the count is unsigned so + 1 / - 1 wrap modulo 2^WIDTH cleanly. load beats a count because it is tested before en in the chain.

updown_counter.vhd — up/down + loadable in VHDL (priority reset > load > enable)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity updown_counter is
       generic (
           WIDTH : positive := 8;
           MAXV  : natural  := 255
       );
       port (
           clk      : in  std_logic;
           rst      : in  std_logic;                    -- sync (highest priority)
           load     : in  std_logic;                    -- sync parallel load (2nd)
           load_val : in  std_logic_vector(WIDTH-1 downto 0);
           en       : in  std_logic;                    -- count-enable (3rd)
           up_down  : in  std_logic;                    -- '1' up, '0' down
           count    : out std_logic_vector(WIDTH-1 downto 0);
           tc       : out std_logic
       );
   end entity;
 
   architecture rtl of updown_counter is
       signal cnt : unsigned(WIDTH-1 downto 0);
   begin
       process (clk)
       begin
           if rising_edge(clk) then
               if rst = '1' then
                   cnt <= (others => '0');              -- 1. reset wins
               elsif load = '1' then
                   cnt <= unsigned(load_val);           -- 2. load beats a count
               elsif en = '1' then
                   if up_down = '1' then
                       cnt <= cnt + 1;                  -- 3a. up (wraps at 2^WIDTH-1)
                   else
                       cnt <= cnt - 1;                  -- 3b. down (wraps at 0)
                   end if;
               end if;                                  -- else HOLD
           end if;
       end process;
 
       count <= std_logic_vector(cnt);
       -- tc at the ACTIVE direction's terminal: MAXV going up, 0 going down.
       tc <= '1' when (up_down = '1' and cnt = to_unsigned(MAXV, WIDTH))
                   or (up_down = '0' and cnt = to_unsigned(0, WIDTH))
             else '0';
   end architecture;
updown_counter_tb.vhd — clocked self-check (assert report); load, up, down, HOLD, priority
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity updown_counter_tb is
   end entity;
 
   architecture sim of updown_counter_tb is
       constant WIDTH : positive := 4;
       constant MAXV  : natural  := 15;
       signal clk      : std_logic := '0';
       signal rst      : std_logic := '1';
       signal load     : std_logic := '0';
       signal load_val : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
       signal en       : std_logic := '0';
       signal up_down  : std_logic := '1';
       signal count    : std_logic_vector(WIDTH-1 downto 0);
       signal tc       : std_logic;
 
       procedure step(signal clk_s : in std_logic) is
       begin
           wait until falling_edge(clk_s);
           wait until rising_edge(clk_s);
           wait for 1 ns;
       end procedure;
   begin
       dut : entity work.updown_counter
           generic map (WIDTH => WIDTH, MAXV => MAXV)
           port map (clk => clk, rst => rst, load => load, load_val => load_val,
                     en => en, up_down => up_down, count => count, tc => tc);
 
       clk <= not clk after 5 ns;
 
       stim : process
           variable exp : natural := 0;
       begin
           step(clk);
           assert to_integer(unsigned(count)) = 0 report "reset failed" severity error;
           rst <= '0';
 
           -- LOAD
           load <= '1'; load_val <= std_logic_vector(to_unsigned(5, WIDTH));
           step(clk); load <= '0'; exp := 5;
           assert to_integer(unsigned(count)) = exp report "load did not land" severity error;
 
           -- UP
           en <= '1'; up_down <= '1';
           for k in 0 to 11 loop
               step(clk);
               if exp = MAXV then exp := 0; else exp := exp + 1; end if;   -- wraps at MAX
               assert to_integer(unsigned(count)) = exp report "up sequence mismatch" severity error;
           end loop;
 
           -- DOWN (wrap through 0 -> MAX)
           up_down <= '0';
           for k in 0 to 9 loop
               step(clk);
               if exp = 0 then exp := MAXV; else exp := exp - 1; end if;   -- wraps at 0
               assert to_integer(unsigned(count)) = exp report "down sequence mismatch" severity error;
           end loop;
 
           -- HOLD
           en <= '0';
           for k in 0 to 2 loop
               step(clk);
               assert to_integer(unsigned(count)) = exp report "HOLD broke" severity error;
           end loop;
 
           -- PRIORITY: load + en together => load wins
           load <= '1'; load_val <= std_logic_vector(to_unsigned(12, WIDTH));
           en <= '1'; up_down <= '1'; step(clk); load <= '0';
           assert to_integer(unsigned(count)) = 12
               report "priority: load lost to enable" severity error;
 
           report "updown_counter self-check complete" severity note;
           wait;
       end process;
   end architecture;

4c. The terminal-count / wrap off-by-one bug — buggy vs fixed, in all three HDLs

Pattern (c) is the signature failure, shown here as buggy vs fixed RTL in each language, then dramatized narratively in §7. The intent: a mod-N counter that produces exactly N distinct states 0, 1, …, N−1 and wraps to 0 — the building block of a clock divider that divides by N. The bug: the wrap comparison uses N instead of N−1, so the counter reaches N before it rolls over and visits N+1 distinct values (0 … N) — one too many. A "divide-by-10" becomes a divide-by-11; a period is off by one; tc fires a cycle late. Same structure, one wrong constant.

modn_bug.sv — BUGGY (wraps at N) vs FIXED (wraps at N-1)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: the counter is meant to produce N states 0..N-1 (mod-N). But it compares
   //        against N, so it only wraps AFTER reaching N — visiting 0..N, which is
   //        N+1 states. A divide-by-N built on this divides by N+1.
   module modn_bad #(
       parameter int WIDTH = 8,
       parameter int N     = 10                     // intended: 10 states, 0..9
   )(
       input  logic             clk,
       input  logic             rst,
       output logic [WIDTH-1:0] count
   );
       always_ff @(posedge clk) begin
           if (rst)             count <= '0;
           else if (count == N) count <= '0;        // WRONG: wraps at N (off by one)...
           else                 count <= count + 1'b1; // ...so it counts 0..N = N+1 values
       end
   endmodule
 
   // FIXED: wrap at N-1 -> the sequence is 0..N-1, exactly N states, a true mod-N.
   module modn_good #(
       parameter int WIDTH = 8,
       parameter int N     = 10
   )(
       input  logic             clk,
       input  logic             rst,
       output logic [WIDTH-1:0] count
   );
       always_ff @(posedge clk) begin
           if (rst)                     count <= '0;
           else if (count == N - 1)     count <= '0;    // RIGHT: wrap at N-1 => 0..N-1
           else                         count <= count + 1'b1;
       end
   endmodule
modn_bug_tb.sv — clocked self-check: the sequence must have EXACTLY N states
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module modn_bug_tb;
       localparam int WIDTH = 8;
       localparam int N     = 10;
       logic             clk = 0, rst;
       logic [WIDTH-1:0] count;
       int               seen_max = 0, wrap_cyc = -1, errors = 0;
       logic [WIDTH-1:0] prev;
 
       // Point the DUT at modn_good to PASS; at modn_bad to see the off-by-one.
       modn_good #(.WIDTH(WIDTH), .N(N)) dut (.clk(clk), .rst(rst), .count(count));
 
       always #5 clk = ~clk;
 
       initial begin
           rst = 1; @(negedge clk); @(posedge clk); #1; rst = 0; prev = 0;
           // Run a couple of full periods; record the max value reached and the wrap.
           for (int cyc = 0; cyc < 3*N; cyc++) begin
               @(negedge clk); @(posedge clk); #1;
               if (count > seen_max) seen_max = count;
               if (prev != 0 && count == 0 && wrap_cyc < 0) wrap_cyc = cyc;  // first wrap
               prev = count;
           end
           // A true mod-N visits 0..N-1: the MAX value seen must be N-1, never N.
           assert (seen_max == N - 1)
               else begin $error("off-by-one: max count seen = %0d, expected N-1 = %0d", seen_max, N-1); errors++; end
           if (errors == 0) $display("PASS: mod-%0d counter visits exactly %0d states (0..%0d)", N, N, N-1);
           else             $display("FAIL: counter reaches %0d -> it is a mod-%0d, not mod-%0d", seen_max, seen_max+1, N);
           $finish;
       end
   endmodule

The Verilog buggy/fixed pair is the same story with reg outputs. The testbench records the maximum value the count ever reaches; a correct mod-N tops out at N−1, the buggy one reaches N — the extra state is the whole bug.

modn_bug.v — BUGGY (wraps at N) vs FIXED (wraps at N-1)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: compares against N, so the counter visits 0..N = N+1 states (mod-(N+1)).
   module modn_bad #(
       parameter WIDTH = 8,
       parameter N     = 10
   )(
       input  wire             clk,
       input  wire             rst,
       output reg  [WIDTH-1:0] count
   );
       always @(posedge clk) begin
           if (rst)             count <= {WIDTH{1'b0}};
           else if (count == N) count <= {WIDTH{1'b0}};    // WRONG: wrap at N
           else                 count <= count + 1'b1;
       end
   endmodule
 
   // FIXED: wrap at N-1 -> exactly N states 0..N-1.
   module modn_good #(
       parameter WIDTH = 8,
       parameter N     = 10
   )(
       input  wire             clk,
       input  wire             rst,
       output reg  [WIDTH-1:0] count
   );
       always @(posedge clk) begin
           if (rst)                 count <= {WIDTH{1'b0}};
           else if (count == N - 1) count <= {WIDTH{1'b0}}; // RIGHT: wrap at N-1
           else                     count <= count + 1'b1;
       end
   endmodule
modn_bug_tb.v — clocked self-check; max value seen must be N-1 (not N)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module modn_bug_tb;
       parameter WIDTH = 8;
       parameter N     = 10;
       reg              clk, rst;
       wire [WIDTH-1:0] count;
       integer          cyc, seen_max, errors;
 
       // Bind to modn_good to PASS; to modn_bad to observe the extra state.
       modn_good #(.WIDTH(WIDTH), .N(N)) dut (.clk(clk), .rst(rst), .count(count));
 
       initial clk = 1'b0;
       always #5 clk = ~clk;
 
       initial begin
           errors = 0; seen_max = 0;
           rst = 1'b1; @(negedge clk); @(posedge clk); #1; rst = 1'b0;
           for (cyc = 0; cyc < 3*N; cyc = cyc + 1) begin
               @(negedge clk); @(posedge clk); #1;
               if (count > seen_max) seen_max = count;
           end
           if (seen_max !== (N - 1)) begin
               $display("FAIL: off-by-one, max count = %0d, expected N-1 = %0d", seen_max, N-1);
               errors = errors + 1;
           end
           if (errors == 0) $display("PASS: mod-%0d counter visits exactly %0d states (0..%0d)", N, N, N-1);
           else             $display("FAIL: reaches %0d -> divides by %0d, not %0d", seen_max, seen_max+1, N);
           $finish;
       end
   endmodule

In VHDL the same off-by-one appears when the wrap compares cnt = N instead of cnt = N-1. The unsigned count and the to_unsigned comparison make the boundary explicit — and wrong by exactly one in the buggy form.

modn_bug.vhd — BUGGY (wraps at N) vs FIXED (wraps at N-1)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   -- BUGGY: wraps at N, so the sequence is 0..N = N+1 states (a mod-(N+1)).
   entity modn_bad is
       generic ( WIDTH : positive := 8; N : positive := 10 );
       port ( clk : in std_logic; rst : in std_logic;
              count : out std_logic_vector(WIDTH-1 downto 0) );
   end entity;
   architecture rtl of modn_bad is
       signal cnt : unsigned(WIDTH-1 downto 0);
   begin
       process (clk)
       begin
           if rising_edge(clk) then
               if rst = '1' then
                   cnt <= (others => '0');
               elsif cnt = to_unsigned(N, WIDTH) then      -- WRONG: wrap at N
                   cnt <= (others => '0');
               else
                   cnt <= cnt + 1;
               end if;
           end if;
       end process;
       count <= std_logic_vector(cnt);
   end architecture;
 
   -- FIXED: wrap at N-1 -> exactly N states 0..N-1 (a true mod-N).
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
   entity modn_good is
       generic ( WIDTH : positive := 8; N : positive := 10 );
       port ( clk : in std_logic; rst : in std_logic;
              count : out std_logic_vector(WIDTH-1 downto 0) );
   end entity;
   architecture rtl of modn_good is
       signal cnt : unsigned(WIDTH-1 downto 0);
   begin
       process (clk)
       begin
           if rising_edge(clk) then
               if rst = '1' then
                   cnt <= (others => '0');
               elsif cnt = to_unsigned(N-1, WIDTH) then     -- RIGHT: wrap at N-1
                   cnt <= (others => '0');
               else
                   cnt <= cnt + 1;
               end if;
           end if;
       end process;
       count <= std_logic_vector(cnt);
   end architecture;
modn_bug_tb.vhd — clocked self-check (assert report); max value seen must be N-1
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity modn_bug_tb is
   end entity;
 
   architecture sim of modn_bug_tb is
       constant WIDTH : positive := 8;
       constant N     : positive := 10;
       signal clk   : std_logic := '0';
       signal rst   : std_logic := '1';
       signal count : std_logic_vector(WIDTH-1 downto 0);
   begin
       -- Bind to modn_good to PASS; to modn_bad to observe the extra state.
       dut : entity work.modn_good
           generic map (WIDTH => WIDTH, N => N)
           port map (clk => clk, rst => rst, count => count);
 
       clk <= not clk after 5 ns;
 
       stim : process
           variable seen_max : natural := 0;
           variable v        : natural;
       begin
           wait until falling_edge(clk);
           wait until rising_edge(clk); wait for 1 ns;
           rst <= '0';
           for cyc in 0 to 3*N-1 loop
               wait until falling_edge(clk);
               wait until rising_edge(clk); wait for 1 ns;
               v := to_integer(unsigned(count));
               if v > seen_max then seen_max := v; end if;
           end loop;
           -- A true mod-N tops out at N-1; reaching N is the off-by-one.
           assert seen_max = N-1
               report "off-by-one: max count seen is not N-1 (counter is mod-(N+1))" severity error;
           report "modn self-check complete" severity note;
           wait;
       end process;
   end architecture;

Across all three languages the lesson is one sentence: a mod-N counter that must produce N distinct states wraps at N−1, and terminal count fires on the last value — comparing against N (or firing at MAX+1) adds exactly one state, one cycle, or one division, and that off-by-one is the signature counter bug.

5. Verification Strategy

A counter is sequential, so its correctness is a behaviour over time, and the single invariant that captures a correct counter is:

On every edge, the count takes exactly the value the controls demand — reset value, or loaded value, or count ± 1, or held — under the reset > load > enable priority; and terminal count fires on the exact terminal value, never one early or one late.

Everything below is that one invariant, made checkable, and it maps directly onto the clocked testbenches in §4.

  • A clocked, self-checking testbench with a reference model. Generate a free-running clock (always #5 clk = ~clk; in SV/Verilog, clk <= not clk after 5 ns; in VHDL). Drive the controls on the falling edge so they are stable before each rising edge, maintain a software expected count that mirrors the intended behaviour (expected = (expected==MAX) ? 0 : expected+1, and its down/load/hold variants), and after each rising edge assert count === expected. This is the core self-check — a one-line reference model of the counter's next-value function, compared automatically every cycle. All three §4a/§4b testbenches do exactly this.
  • Count up from reset and check the sequence. After a defined reset to 0, enable and step the counter, asserting the count is 0, 1, 2, … and wraps at MAX. This proves both the adder path and the reset value. A counter that starts at 1 (reset didn't win), or skips a value (an enable glitch), fails here.
  • Count down and check the wrap. Reverse direction and step, asserting the count decrements and that 0 wraps to MAX (unsigned modulo). The down path is where a signedness or wrap mistake hides — an unsigned counter must roll 0 → MAX, not go negative.
  • Load a value and verify it. Assert load with a known load_val, step one edge, and assert the count equals load_val exactly — then that it counts from there. This proves the load mux and that a synchronous load lands on the next edge.
  • Hold when disabled. Drop en (with load and rst low) and assert the count does not move for several cycles. This is the check most learners skip, and it catches the missing enable-hold path — a counter that always increments passes every other test but fails this.
  • Assert terminal count fires on the right value — not off-by-one. The load-bearing corner. Assert tc is high exactly when count == TERMINAL (MAX up, 0 down) and low otherwise — checked every cycle, not just once. A directed check should also confirm tc is not high at TERMINAL−1 and does not stay high one cycle too long. For a mod-N counter, assert the maximum value ever reached is N−1 (the §4c testbenches record seen_max and check it): if it ever reaches N, the counter is a mod-(N+1) and the off-by-one is caught.
  • Verify the priority. Drive load and en on the same edge and assert load wins (count becomes load_val, not load_val±1); drive rst with load and assert rst wins (count becomes 0). The priority is invisible until two controls collide — a directed collision test is the only way to prove reset > load > enable.
  • Parameter-generic verification (WIDTH = 1, 4, 8; several N). A parameterized counter must be verified generic. Re-run the sequence/hold/tc checks at WIDTH = 4 and WIDTH = 8, and re-run the mod-N check for several N (including one that is not a power of two, where the N−1 wrap earns its keep). The reference model does not change; only the width and the terminal value do.
  • Expected waveform. On a waveform, a correct counter shows count changing only at rising edges, stepping by exactly one in the active direction while enabled, flat while disabled, jumping to load_val on a load, and 0 on a reset; tc is a single-cycle-wide pulse aligned exactly with the cycle in which count == TERMINAL. The visual signature of the off-by-one is tc pulsing one cycle late, or the count reaching one value beyond its intended terminal before it wraps — the delay line's worth of extra period a divider inherits.

6. Common Mistakes

Terminal-count / wrap off-by-one. The signature counter bug. A mod-N counter that must produce N distinct states wraps at N−1; comparing against N makes it visit 0 … N — N+1 states — so a divide-by-N divides by N+1 and a period is one cycle too long. The mirror image: raising tc at MAX+1 (or after the wrap) instead of on MAX, so terminal count fires a cycle late, or holding the maximum value an extra cycle before rolling over. The fix is to nail the exact terminal value — tc = (count == N−1) for a 0-based N-state sequence — and to verify it by asserting the maximum value reached is N−1 and that tc aligns with the terminal cycle, not the one after. (Full post-mortem in §7; buggy-vs-fixed RTL for all three HDLs in §4c.)

Load-vs-enable-vs-reset priority wrong. The controls are ranked reset > load > enable, and the ranking lives in the order of the if / else if / else if chain. Put en before load and a count silently overwrites a load — you load 5, but because en was also high the same edge, the counter stores 6 and the preset is lost. Put load before rst and a reset fails to win — the counter loads instead of resetting when both fire. The RTL must test rst first, then load, then en, and verification must collide two controls on one edge to prove the winner, because the priority is invisible until they conflict.

Forgetting the enable-hold path. A counter must be able to not count. If the clocked process has no "else hold" — for example a bare count <= count + 1 with no enable guard — the counter free-runs and cannot be stalled, so any upstream "pause" signal is ignored and the count drifts ahead of the data it is supposed to track. The hold is the else if (en) guard (with the implicit else keeping the value on a clocked reg): drop that guard and the enable does nothing. A timer that "runs while stalled" is almost always a missing hold path.

Unintended wrap when saturation was intended (or vice-versa). The natural behaviour of a plain adder is to wrap modulo 2^WIDTH; a saturating counter (an occupancy count that must stop at full, a signal-strength meter that must peg at max) needs saturation added — hold at the terminal when tc instead of rolling to 0. Relying on the default wrap where you needed saturation lets an occupancy count roll from full back to empty (catastrophic for a FIFO), and adding saturation where you needed a free-running mod-N counter freezes it at the top. Decide wrap vs saturate deliberately and encode it — do not inherit the adder's default and hope.

Comparing terminal count against N "because there are N states." A subtle framing error: "there are N states, so I compare to N" feels right and is off by one. The N states are 0 … N−1; the last one is N−1. The count only equals N if you let it advance one step past the intended sequence — which is exactly the bug. Anchor on "the last value of the sequence," not "the number of values."

Assuming a power-of-two width hides the wrap. For a full-range counter (MAX = 2^WIDTH − 1) the natural adder wrap and your terminal coincide, so the off-by-one hides. The moment N is not a power of two — a mod-10 counter in a 4-bit register (range 0–15) — the terminal N−1 = 9 is nowhere near the natural wrap at 15, and a sloppy comparison is exposed. Test with a non-power-of-two N so the terminal and the natural wrap are different values; that is where the off-by-one surfaces.

7. DebugLab

The clock divider that divided by 11 — a terminal-count off-by-one

The engineering lesson: a counter's terminal value is an exact contract, not an approximate one — an N-state sequence starting at 0 ends at N−1, and comparing against N (or firing terminal count one value late) adds exactly one state, one cycle, or one division. The tell is a divider that divides by one more than intended, a timer that times out a cycle long, or a period that reads one tick high — quantities that are almost right, off by exactly one, which is the fingerprint of a boundary comparison anchored on the count of states instead of the last state. Two habits make it impossible, and they are identical across SystemVerilog, Verilog, and VHDL: anchor every terminal comparison on the last value of the sequence (N−1), never the number of values (N), and verify by asserting the maximum value the count reaches is exactly the intended terminal (and measuring the period), never by eyeballing that "it wraps."

8. Interview Q&A

9. Exercises

Design and reasoning exercises — no syntax drills. Each rehearses the "a counter is a register plus an adder plus muxes, with an exact terminal and a strict priority" habit.

Exercise 1 — Decompose the counter into primitives

Take the up/down + loadable counter of §4b and draw it as a composition of the patterns you already know. Name (a) which part is the register of 2.1, (b) which mux (1.1) implements the direction select, (c) which mux implements the parallel load, (d) which mux (and which control) implements hold-vs-advance, and (e) which comparator (1.5) produces terminal count. Then state, in one line each, what the reset > load > enable priority corresponds to structurally, and why the terminal comparator is combinational while the count itself is sequential.

Exercise 2 — Find and fix the off-by-one

An engineer builds a "divide-by-16" clock divider from a mod-16 counter in a 4-bit register and writes the wrap as if (count == 16) count <= 0;. It appears to work in simulation. Explain (i) exactly what the 4-bit counter actually does when the comparison is count == 16 (hint: what is the widest value a 4-bit register can hold?), (ii) whether this particular width hides or exposes the off-by-one and why, and (iii) how the same mistake would behave differently if the counter were a mod-10 counter in the same 4-bit register. Then give the corrected wrap constant for the mod-16 and the mod-10 cases.

Exercise 3 — Prove the control priority

Two engineers implement the same full counter. Engineer A writes if (rst) … else if (load) … else if (en) …. Engineer B writes if (rst) … else if (en) … else if (load) …. Both pass a testbench that drives only one control at a time. Construct the single stimulus (which controls high on which edge, and the expected result) that distinguishes them, state what B's counter does wrong in that case, and explain why a testbench that never asserts two controls on the same edge can never catch the difference.

Exercise 4 — Wrap or saturate?

For each of the following counters, decide whether it should wrap or saturate at its boundary, and say in one line what goes wrong if you pick the other behaviour: (a) an address generator sweeping a 256-entry ring buffer; (b) a FIFO occupancy counter tracking how many of DEPTH slots are full; (c) a mod-N clock divider; (d) a saturating "retry count" that must stop incrementing at a maximum of 7. Then, for the two that must saturate, sketch in words the one extra guard you add to the plain up-counter to make it saturate instead of wrap.

Continue in RTL Design Patterns (sibling counters go live together; forward links unlock as they ship):

  • Mod-N & Programmable Counters — Chapter 3.2; the mod-N counter this page's DebugLab is built around, generalized to a runtime-programmable modulus — where the N−1 wrap becomes a loaded value.
  • Gray-Code Counters — Chapter 3.3; a counter whose successive values differ by one bit — the safe way to pass a count across a clock domain, built on the same register-plus-next-value skeleton.
  • Clock Dividers & Timers — Chapter 3; the direct application of a counter with an exact terminal — divide a clock by N, time out a delay — where the off-by-one of §7 becomes a wrong frequency.
  • Pulse & Strobe Generators — Chapter 3; a counter whose terminal count emits a single-cycle strobe every N cycles — terminal count put to work.
  • FIFO Full/Empty Logic — Chapter 7; read/write pointers are counters, and the full/empty off-by-one is this page's terminal-count off-by-one at the pointer level.

Backward / built-from (the primitives this pattern composes):

  • The Register Pattern (D-FF) — Chapter 2.1; the storage atom — a counter is this register with an adder feeding its D. Its non-blocking <= discipline is exactly the one this page relies on.
  • Register with Enable / Load — Chapter 2.2; the count-enable (hold vs advance) and the parallel load are the enable and load of this pattern, applied to a counting register.
  • Synchronous Reset — Chapter 2.3; the sync reset that gives every counter on this page a defined power-up value of 0.
  • Asynchronous Reset — Chapter 2.4; the alternative reset style and its recovery trade-offs, applicable to a counter's reset.
  • Comparators — Chapter 1.5; the equality comparator that produces terminal count (count == TERMINAL) — the flag half of this pattern.
  • Multiplexers — Chapter 1.1; the direction, load, and enable muxes on the counter's next-value path, and the priority chain that ranks them.
  • Decoders · Encoders · Priority Encoders · Shift Registers — the neighbouring datapath and sequential building blocks a counter is wired alongside.

Verilog v1 prerequisites (the grammar this pattern transcribes into):

  • Arithmetic Operators — the + / on which the counter's adder is built, and where unsigned wrap-around is defined.
  • Blocking and Non-Blocking Assignments — the <= discipline the counter's clocked update relies on, inherited from the register pattern.
  • Case Statements — an alternative encoding of the direction/load selection and the mux family behind the counter's controls.
  • If-Else Statements — the if / else if / else chain that encodes the reset > load > enable priority.

11. Summary

  • A counter is a register whose next value is itself plus or minus one. It composes the register of 2.1 (storage, clocked <=, reset value), an adder (the ±1 step), a stack of muxes from 1.1 (direction, load, hold-vs-advance), and a comparator from 1.5 (terminal count). There is nothing new in count + 1 — it is the simplest stateful datapath, the first place a register meets an adder.
  • Direction, load, enable, and hold are all muxes on the next-value path. Up/down is a 2:1 mux on the adder's step; parallel load is a 2:1 mux choosing load_val vs the count; count-enable is the hold-vs-advance mux (drop it and the counter cannot be stalled). Unsigned counters wrap modulo 2^WIDTH in both directions unless saturation is deliberately added.
  • The controls have a strict priority: reset > load > enable. Encoded as the order of the if / else if / else if chain in the clocked process, it ranks the controls so a reset always wins, then a load, then a count. Get the order wrong and a load is overwritten by a count, or a reset fails to win — a bug invisible until two controls collide on one edge, which is why verification must collide them.
  • Terminal count is an exact contract, and off-by-one is the signature bug. For an N-state sequence starting at 0, the terminal is N−1, not N; tc = (count == N−1). Comparing against N (or firing tc a cycle late) adds exactly one state — a divide-by-N becomes divide-by-(N+1), a timer times out a cycle long — and it hides when the width is a power of two but surfaces with a non-power-of-two N. This is the DebugLab of §7.
  • Verify over time, with a reference model and exact boundaries. A clocked, self-checking testbench counts up from reset and checks the sequence, counts down and checks the wrap, loads and verifies, holds when disabled, collides controls to prove the priority, and asserts tc fires on the exact terminal (and that the max value reached is N−1) — parameter-generic at several WIDTH and N. The self-checking testbenches are shown here in SystemVerilog, Verilog, and VHDL.