Skip to content

Counter

counter is a small complex component (CCP): a clocked register plus a chain of conditional adds that is committed into it once per cycle. It is the shape you reach for whenever several independent conditions each want to bump the same count — a free list, an occupancy counter, a credit counter — and you would otherwise hand-build a chain of adders and a mux per contributor.

from kathryn import counter
class occupancy(Module):
@init
def com_declare(self):
self.en1 = wire(1, "en1")
self.en2 = wire(1, "en2")
self.cnt = counter(3, "cnt") # 3-bit, wraps mod 8
@flow
def my_flow(self):
self.cnt.reset(0)
self.cnt.add(2, self.en1) # stage 0: en1 ? cnt + 2 : cnt
self.cnt.add(3, self.en2) # stage 1: en2 ? st0 + 3 : st0
self.cnt.update() # cnt <= chain head

Each add(amount, enable) chains one combinational stage — enable ? prev + amount : prev — onto the counter. update() commits the head of the chain into the backing register as one clocked statement in the current flow scope; the chain then restarts from the register. So the adds accumulate: with both enables high, cnt' = cnt + 5.

flowchart LR
    REG["cnt register<br/>(committed value)"] --> S0["stage 0<br/>en1 ? +2 : hold"]
    S0 --> S1["stage 1<br/>en2 ? +3 : hold"]
    S1 --> HEAD["chain head<br/>(.now)"]
    HEAD -->|"update() — one clocked statement"| REG
MemberMeaning
counter(bit_width, name=None)Construct. The value wraps mod 2**bit_width.
.add(amount, enable=None)Chain one stage. amount may be a signal or an int (wrapped to counter width). Without enable the add is unconditional. Returns the new chain head as a SignalRef.
.update()Commit the chain head into the register — one clocked statement in the current scope.
.valueThe committed register: what the counter reads after the clock edge.
.nowThe chain head: the uncommitted combinational value (the register itself when nothing is pending).
.reset(value)Reset value of the backing register — mirrors reg.reset.
.widthThe declared bit width.
.identThe underlying CcpIdent.

.value is an ordinary signal handle, so it takes mark_output(...) and can be read anywhere:

self.cnt.value.mark_output("cnt_out")

.now is what you read when you need this cycle’s pending total — for example to decide, in the same cycle, whether the count will overflow:

self.now_probe *= self.cnt.now # sampled BEFORE the commit

Omit the enable for a plain free-running count:

self.free = counter(4, "free")
self.free.reset(0)
self.free.add(1)
self.free.update() # +1 every cycle, mod 16

The register is a plain reg named REG_<name>_CNT_<id>; each chained stage is a combinational wire WIRE_<name>_ST<n>_<id> driven by a zif/zelse pair over the stage’s adder expression:

reg [2:0] REG_cnt_CNT_9520;
always @(*) begin
WIRE_cnt_ST0_9536[2:0] <= VAL_WIRE_cnt_ST0_9536_DEFAULT_ZERO_9587[2:0];
if (WIRE_en1_9518) begin
WIRE_cnt_ST0_9536[2:0] <= EXPR_cnt_ADD0_9535[2:0]; // cnt + 2
end else begin
WIRE_cnt_ST0_9536[2:0] <= REG_cnt_CNT_9520[2:0]; // hold
end
end
always @(posedge WIRE_clk_9559) begin
REG_cnt_CNT_9520[2:0] <= WIRE_cnt_ST1_9543[2:0]; // update()
if (WIRE_mrst_9560) begin
REG_cnt_CNT_9520[2:0] <= VAL_val0_9530[2:0]; // reset, wins
end
end
assign EXPR_cnt_ADD0_9535 = REG_cnt_CNT_9520[2:0] + VAL_val2_9533[2:0];

An unconditional add skips the mux entirely — the commit reads the adder expression directly (REG_free_CNT <= EXPR_free_ADD0).

Note the reset write is emitted last in the clocked block, at DEFAULT_UE_PRI_RST, so a held master reset pins the counter at its reset value regardless of what the chain says — the usual write-priority rule.

tc39_dyn_counter is exactly the two counters above — a 3-bit counter with two enabled adds plus a now probe, and a 4-bit enable-less counter — with a cocotb testbench driving every enable combination and checking the modular wrap. See the Examples Gallery.