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.

clk 0en 1rst 0counterclkenqtcrstq 3line 16tc 0/4

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

counter4.sv
// 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);

endmodule

The 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.

Cells after synth -nofsm, by type
CellWhat it isCount
$_SDFFE_PP0P_D flip-flop on the rising edge, active-high synchronous reset to 0, active-high enable4
$_AND_2-input AND4
$_NAND_2-input NAND2
$_NOR_2-input NOR1
$_NOT_inverter2
$_XNOR_2-input XNOR1
$_XOR_2-input XOR2
Total16

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.

clkrstenqtc10x90x0

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.

Tags rtl counter bus

Found an error? Send a correction