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
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
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
5. Simulation interpretation — count, hold, load, wrap
An 8-bit counter: enable pauses it, load jumps it, and it wraps at the top
10 cycles6. 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.
-- 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-1explicitly. - Off-by-one in the wrap/terminal compare. Decide
count = N-1(last value) vscount = 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/elsifso reset (and load) take precedence over the increment as intended. - Reading the count for a tick a cycle late. A registered
tickappears 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.