4-bit decade counter
A counter drawn as one box, and a count that leaves it as a bus. What a block hides, and how four bits travel on one wire and one waveform row.
- Level
- rtl
- Code
- counter-4bit
- Checked
- Verilator 5.052, Icarus Verilog 13.0, Yosys 0.66
- Published
- Authorship
- Drafted with AI assistance
- Status
- In progress — published so it can be reviewed, not finished.
A counter adds one to itself on every rising edge it is allowed to take. This one is four bits
wide, counts up, wraps to 0 after 9, and holds its count whenever en is low. tc — the
terminal count — is high on the last count before the wrap, which is what lets a second counter
use it as an enable and carry the next digit.
Run plays this scenario from the start; driving an input continues from here.
A circuit that runs: a schematic and its waveform on one time axis. Inputs: en, rst. Outputs: q (4 bits), tc. clk runs at 8 ticks a period. It is built from 1 counter. The waveform shows clk, rst, en, q, tc.
Figure 1. Press Run. One edge is one step while en is high. The other two scenarios run the counter to the wrap and hold it with en.
How it works, edge by edge
Before the first rising edge q is x. Nothing has given the counter a value, and the reset
cannot: it is synchronous, so it sits in front of the flip-flops as data and waits for an edge
like everything else. That first edge is where q leaves x and becomes 0 — and it is the reset
that put it there, not the counting.
After it, one edge is one step: 1, 2, 3, with en high and rst low. The new count is the
counter's own count plus one, so nothing outside the box has to remember where it was.
"Wrapping at ten" runs far enough to show the end of the range. While q sits on 9, tc is
high — it is a comparison on the count, not a pulse the counter generates, so it appears with
the 9 and leaves with it. The next edge wraps q to 0 and tc falls on the same edge.
"Held by en" drops en at an edge, and the edge takes the value en had just before it: the
count still steps to 2, then stops. The clock keeps running underneath — the edges arrive and
are ignored. en comes back high at another edge, and again that edge takes the old value, so
the first step after the hold is one edge later than the rise that allowed it.
The box the figure does not draw
The counter is one rectangle with its name written over it and its ports named inside: clk at
the wedge, en under it, rst entering from below, q and tc leaving on the right. There is
no adder in the drawing, no chain of flip-flops and no comparator — but the figure runs all
three. A block is a behavioural model the engine simulates in place of the gates, and this one
is set to four bits, counting up, wrapping at ten, with a synchronous active-high reset and an
enable.
Drawing those gates is worth doing once. On a page about something else — a FIFO, a handshake, a state machine that counts cycles — thirty more symbols would be noise around the one thing being explained. The box keeps the behaviour and drops the noise.
One wire, four bits
q leaves the box on a single line drawn as a hollow double line, with /4 under a short slash
near the pin it comes from: four bits travelling together, and a line that cannot be mistaken
for a 0 or a 1. It ends on a value pad rather than a lamp. A lamp has one bit and can be a face
— empty, filled, hatched; a bus has a number, so it prints one.
In the waveform that same wire is one row, and the row carries the count between two rails
instead of riding a high or a low rail. This figure prints it in decimal, because on a counter
that wraps at ten a 9 is easier to find than a 1001; four-bit values are binary here by default
and wider ones hexadecimal. tc on the row below is one bit and reads as a level, which is the
whole difference between the two kinds of row.
The code
// A 4-bit decade counter: with en high it counts 0 to 9 and wraps to 0, and tc
// marks the last count before the wrap. This is the module the block on the
// page stands for, written out.
//
// Reset style for this module: synchronous, active high. One style per page,
// stated up front, so nothing here has to be read twice.
module counter4 #(
// The count wraps to zero after MODULO-1: 10 counts a decade, 16 counts
// everything four bits hold.
parameter int MODULO = 10
) (
input logic clk,
input logic rst,
input logic en,
output logic [3:0] q,
output logic tc
);
// The last count before the wrap, in the width of q.
localparam logic [3:0] LastCount = 4'(MODULO - 1);
localparam logic [3:0] OneStep = 4'd1;
always_ff @(posedge clk) begin
if (rst) begin
q <= '0;
end else if (en) begin
q <= (q == LastCount) ? '0 : q + OneStep;
end
end
// High for one clock period per wrap, read off the count alone: a reader can
// point at q on the waveform and read tc off it.
assign tc = (q == LastCount);
endmoduleThe reset style is stated at the top of the file and used once: synchronous, active high. The
wrap is the parameter — 10 counts a decade, 16 counts everything four bits hold — and it is
compared against, which is why tc can be read off the count with an assign instead of being
remembered in a flip-flop of its own.
There is no else on the enable and none is wanted: an always_ff holds what it has when
nothing assigns it, because that is what a flip-flop does. The same code in an always_comb
would be a latch.
Synthesis result
Yosys's generic synthesis turns counter4.sv into 16 cells, 4 of them flip-flops. The cells are Yosys's own gate library, not an FPGA's or a standard-cell library's: the count measures the logic, not the area of any chip.
| Cell | What it is | Count |
|---|---|---|
$_SDFFE_PP0P_ | D flip-flop on the rising edge, active-high synchronous reset to 0, active-high enable | 4 |
$_AND_ | 2-input AND | 4 |
$_NAND_ | 2-input NAND | 2 |
$_NOR_ | 2-input NOR | 1 |
$_NOT_ | inverter | 2 |
$_XNOR_ | 2-input XNOR | 1 |
$_XOR_ | 2-input XOR | 2 |
| Total | 16 | |
Lint by Verilator 5.052, the testbench by Icarus Verilog 13.0, synthesis by Yosys 0.66.
Icarus replayed this page's three scenarios on this code, and all 950 values the waveform draws match.
The netlist before synthesis
What Yosys's prep reads in counter4.sv, before any gate is chosen, drawn by netlistsvg 1.0.2.
Pitfalls
tc is a comparison, not a flag. It is high while q sits on 9 and falls when the count
leaves it, with no edge of its own. Register it to tidy it up and you have moved it: the
registered version is high during the count 0, one clock period after the one it announced.
Six values this counter should never hold. Four bits hold sixteen values and this counter uses ten. The wrap is a comparison with 9, not a range check, so a count that somehow sat on 12 would keep going — 13, 14, 15 — and reach 0 through the four-bit overflow rather than through the wrap. Reset is what keeps the count in range; nothing else in the module does.
An enable is an input, not a switch. On the waveform en falls at the same instant as an
edge and the counter still steps, because the edge takes the value en had before it — exactly
as the count it loads is the count that was there before it. Reading the fall as "it stops here"
is off by one step, and so is reading the rise as "it starts here".
Check yourself
- Why is tc high on the count 9 rather than on the 0 the counter wraps to?
- Because tc is a comparison on the count that is there now, not a record of the wrap that has happened. High on 9, it is already valid at the edge that wraps this counter — which is the edge a second counter enabled by tc has to use to advance.
- What is the difference between holding the count with en low and stopping the clock?
- With en low every flip-flop is still clocked on every edge; the enable only chooses between the new count and the old one, and the timing of the circuit does not change. Stopping the clock is clock gating — a different circuit, where the enable is only safe to change while the clock is low.
- The waveform shows q as x for the first clock period even though rst is high. Why?
- Because the reset here is synchronous: it is data in front of the flip-flops, not a wire into them. Nothing reaches the count until a rising edge arrives, and until then the counter holds no value at all.