Skip to content

VHDL · Chapter 7.6 · Sequential Logic Design

Counters

A counter is the natural next step from a register: a register whose next value is its current value plus one, or minus one, or a value you load in. Counters are everywhere in real designs, from address generators and timers to dividers and baud-rate generators. Built on the standard numeric library, a counter is just a register fed by an incrementer and wrapped with a little control logic: an enable to pause it, a synchronous load to jump to a value, a clear to reset it, and a terminal-count output that fires when it reaches its maximum. This lesson builds the standard counter variants, shows why the bit width sets the wrap point, and maps each one to its datapath of register plus adder plus control.

Foundation14 min readVHDLCounternumeric_stdRegisterTerminal CountRTL

1. Engineering intuition — a register that adds to itself

A counter is just a register with a rule for its next value: take the current count and add one. Because the next value depends on the current value (controlled feedback through a register — exactly the safe version of the loop from lesson 6.8), it advances one step per clock. Add a few controls — pause it (enable), jump it to a value (load), zero it (clear), and flag when it tops out (terminal count) — and you have the building block behind timers, address generators, and rate dividers.

2. Formal explanation — the counter variants

counter_forms.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all;
signal count : unsigned(7 downto 0);
 
cnt : process (clk)
begin
  if rising_edge(clk) then
    if rst = '1' then        count <= (others => '0');     -- synchronous clear
    elsif load = '1' then    count <= unsigned(load_val);  -- jump to a value
    elsif en = '1' then      count <= count + 1;           -- count up only when enabled
    end if;                                                 -- else hold
  end if;
end process;
 
tc <= '1' when count = 255 else '0';                        -- terminal count (max for 8 bits)

A counter is a register (lesson 7.4) whose next value is count + 1 (up) or count - 1 (down), gated by an enable (lesson 7.5). A synchronous load overrides the increment to set a value; a clear resets it; a terminal-count output compares against the maximum. Priority among these is set by the if/elsif order (reset highest, then load, then count).

3. Production RTL — modulo-N counter and a tick generator

modulo_and_tick.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all;
-- Modulo-N counter (0 .. N-1), with a one-cycle 'tick' when it wraps.
modn : process (clk)
begin
  if rising_edge(clk) then
    tick <= '0';                                  -- default (latch-free style)
    if rst = '1' then        count <= (others => '0');
    elsif en = '1' then
      if count = N-1 then     count <= (others => '0'); tick <= '1';  -- wrap → emit tick
      else                    count <= count + 1;
      end if;
    end if;
  end if;
end process;

What hardware does this become? A register holding count, an incrementer, a comparator against N-1, and a mux choosing between increment and wrap-to-zero — plus a registered tick pulse on wrap. This modulo-N + tick pattern is exactly how baud-rate generators and timed strobes are built: count N clocks, emit one tick. The width of count must be large enough to hold N-1.

4. Hardware interpretation — register, adder, control

counter datapath: register feeding an incrementer and control muxcurrent+1nextcount registerclocked stateincrementercount + 1 (or - 1)next-value muxincr / load / clear (en,load, rst)comparator= max / N-1 → tc / wrap12
A counter is a register fed by an incrementer and a small control network. The current count feeds an adder (+1 for up, -1 for down); a mux selects the next value among increment, a loaded value, and zero (clear), under the enable and load/clear controls; the result is captured by the register on the clock edge. A comparator produces terminal-count or wrap. This register-plus-adder-plus-control datapath is the template for every counter variant.

5. Simulation interpretation — count, hold, load, wrap

An 8-bit counter: enable pauses it, load jumps it, and it wraps at the top

10 cycles
An 8-bit counter: enable pauses it, load jumps it, and it wraps at the topen=0 → counter HOLDS at 2en=0 → counter HOLDS a…load=1 → count jumps to load_val (here 0xFE)load=1 → count jumps t…0xFF + 1 wraps to 0x00 (8-bit width sets the wrap point)0xFF + 1 wraps to 0x00…clken1100111111load0000001100count0122234FEFF00t0t1t2t3t4t5t6t7t8t9
The counter advances one per enabled edge, holds when en=0, jumps to the loaded value when load=1, and wraps from 0xFF to 0x00 because it is 8 bits. The wrap point is fixed by the width; a modulo-N counter would instead wrap at N-1. All behaviour is register-plus-adder-plus-control.

6. Debugging example — the counter that never wraps, or wraps wrong

Expected: a modulo-N counter that wraps at N-1. Observed: it wraps at a power of two instead of N, or never reaches N (off by one), or overflows unexpectedly. Root cause: for the wrong wrap, the counter relied on natural width wrap (2^width) instead of an explicit count = N-1 compare; for off-by- one, the compare used N instead of N-1 (or < vs <=); for overflow, the count width was too small to hold N-1. Fix: compare explicitly against N-1 to wrap, and size the register for the largest value it must hold. Engineering takeaway: a modulo-N counter needs an explicit = N-1 wrap and a width that fits N-1; do not rely on natural binary wrap unless N is exactly a power of two.

modulo_wrap_fix.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- BUG: relies on natural 2^width wrap → only correct if N is a power of two.
-- if en='1' then count <= count + 1; end if;          -- wraps at 2^width, not N
-- FIX: explicit modulo-N wrap.
if en = '1' then
  if count = N-1 then count <= (others => '0');         -- wrap at N-1
  else                count <= count + 1; end if;
end if;

7. Common mistakes & what to watch for

  • Relying on natural wrap for modulo-N. Only works when N is a power of two; otherwise compare to N-1 explicitly.
  • Off-by-one in the wrap/terminal compare. Decide count = N-1 (last value) vs count = N; size the range carefully.
  • Undersized count width. The register must hold N-1 (or the max value); size it accordingly.
  • Missing enable/reset priority. Order if/elsif so reset (and load) take precedence over the increment as intended.
  • Reading the count for a tick a cycle late. A registered tick appears one cycle after the wrap condition; account for that latency.

8. Engineering insight & continuity

A counter is the clearest example of controlled feedback through a register: next = current + 1, made safe by the clock. Layering enable, load, clear, and terminal-count onto that core gives every timer, divider, and address generator you will build, and the modulo-N + tick pattern is the basis of rate generation. Keep the width sized for the range and the wrap explicit, and counters are routine. The next lesson covers the other canonical sequential structure — the Shift Register — where state moves sideways through a chain of flip-flops each clock, the basis of serialisation, delay lines, and LFSRs.