Building a sequential counter with clock, reset, and enable. Understanding clock-driven hardware design through a practical example.
A counter is the simplest sequential circuit -- it holds state across clock cycles. Unlike the combinational ALU, a counter inherits from SequentialComponent, which gives it a clock-driven register primitive. The counter increments on each rising clock edge when enabled, and resets to zero when the reset signal is asserted.
class Counter < RHDL::Sim::SequentialComponent
# Clock and control signals
input :clk # Clock input - drives all state updates
input :rst # Synchronous reset - clears count to 0
input :enable # Count enable - freezes count when low
# Counter output: 8 bits = counts 0 to 255
output :count, width: 8
output :carry # Overflow flag: pulses when count wraps
# Internal register holding the current count
register :count_reg, width: 8,
clock: :clk,
reset: :rst,
reset_value: 0
behavior do
# Next-state logic: what count_reg becomes on next clock edge
if rst
count_reg.next <= 0
elsif enable
count_reg.next <= count_reg + 1
else
count_reg.next <= count_reg # Hold current value
end
# Output assignments (active every cycle)
count <= count_reg
carry <= enable & count_reg.eq?(0xFF)
end
end
Key concepts in this design: The register declaration creates a flip-flop with specified width, clock, and reset behavior. The .next assignment describes what the register will hold after the next rising clock edge -- this is the fundamental distinction between combinational and sequential logic. The carry output is combinational: it goes high during the cycle where the count is 255 and enable is asserted, signaling that the counter will wrap to 0 on the next edge.
The generated Verilog uses a standard always @(posedge clk) block for the register, exactly as you would write by hand. The reset is synchronous, matching the RHDL specification. CIRCT produces clean, idiomatic Verilog that any synthesis tool will recognize as a simple counter.
module Counter (
input wire clk,
input wire rst,
input wire enable,
output wire [7:0] count,
output wire carry
);
reg [7:0] count_reg;
always @(posedge clk) begin
if (rst)
count_reg <= 8'd0;
else if (enable)
count_reg <= count_reg + 8'd1;
end
assign count = count_reg;
assign carry = enable & (count_reg == 8'hFF);
endmodule
The Verilog output demonstrates how RHDL's sequential abstractions map to standard HDL constructs.
| RHDL Construct | Verilog Output |
|---|---|
SequentialComponent | always @(posedge clk) |
register :count_reg | reg [7:0] count_reg |
count_reg.next <= | count_reg <= (non-blocking) |
reset_value: 0 | if (rst) count_reg <= 8'd0 |
count_reg + 1 | count_reg + 8'd1 |
count_reg.eq?(0xFF) | count_reg == 8'hFF |
Note how the implicit hold behavior (the else branch in RHDL that assigns count_reg to itself) is omitted in Verilog. This is correct: Verilog registers retain their value when not assigned in an always block, so the hold is implicit. CIRCT recognizes this pattern and produces minimal code.
Understanding hardware behavior requires thinking in terms of signal waveforms over time. Below is a cycle-by-cycle trace of the counter's signals during a typical reset-then-count sequence. Each column represents one clock cycle, and the values show signal states at the rising edge of the clock.
| Cycle | clk | rst | enable | count_reg | count | carry | Event |
|---|---|---|---|---|---|---|---|
| 0 | ↑ | 1 | 0 | 0x00 | 0x00 | 0 | Reset asserted |
| 1 | ↑ | 1 | 0 | 0x00 | 0x00 | 0 | Reset held |
| 2 | ↑ | 0 | 0 | 0x00 | 0x00 | 0 | Reset released, enable low |
| 3 | ↑ | 0 | 1 | 0x01 | 0x01 | 0 | First count |
| 4 | ↑ | 0 | 1 | 0x02 | 0x02 | 0 | Counting |
| 5 | ↑ | 0 | 0 | 0x02 | 0x02 | 0 | Enable deasserted -- hold |
| 6 | ↑ | 0 | 0 | 0x02 | 0x02 | 0 | Still holding |
| 7 | ↑ | 0 | 1 | 0x03 | 0x03 | 0 | Resume counting |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 257 | ↑ | 0 | 1 | 0xFE | 0xFE | 0 | Approaching overflow |
| 258 | ↑ | 0 | 1 | 0xFF | 0xFF | 1 | Carry flag goes high |
| 259 | ↑ | 0 | 1 | 0x00 | 0x00 | 0 | Wrap to zero |
You can generate this waveform trace programmatically using the RHDL simulator and dump it to a VCD file for viewing in GTKWave or any waveform viewer.
require 'rhdl/sim'
sim = RHDL::Sim::Simulator.new(Counter)
sim.vcd_dump("counter_waves.vcd")
# Reset sequence: hold rst high for 2 cycles
sim.drive(:rst => 1, :enable => 0)
sim.tick(2)
# Release reset, keep enable low for 1 cycle
sim.drive(:rst => 0)
sim.tick(1)
# Enable counting for 260 cycles (full wrap + 4)
sim.drive(:enable => 1)
sim.tick(260)
# Check the carry pulse occurred at cycle 258
puts "Final count: #{sim.peek(:count)}" # => 4
sim.finish
Key timing observations: The counter updates one cycle after the enable is asserted, because the register captures its next value on the rising clock edge. The carry flag is combinational and appears in the same cycle as count=0xFF, providing a one-cycle advance warning that the counter will wrap. This is critical for cascading counters: the carry output of one counter drives the enable of the next, creating multi-byte counters with correct timing.
The basic counter pattern can be extended in several ways. Below are four common variations, each building on the same sequential design principles.
Up/Down Counter -- adds a direction control input. When up is high the counter increments; when low it decrements. The carry and borrow flags indicate overflow in each direction.
class UpDownCounter < RHDL::Sim::SequentialComponent
input :clk, :rst, :enable
input :up # 1 = count up, 0 = count down
output :count, width: 8
output :carry, :borrow
register :count_reg, width: 8,
clock: :clk, reset: :rst,
reset_value: 0
behavior do
if rst
count_reg.next <= 0
elsif enable
count_reg.next <= mux(up,
count_reg + 1,
count_reg - 1)
end
count <= count_reg
carry <= enable & up &
count_reg.eq?(0xFF)
borrow <= enable & ~up &
count_reg.eq?(0x00)
end
end
Loadable Counter -- adds a parallel load capability. When load is asserted, the counter is preset to the value on the data input rather than incrementing. This is essential for timer applications where you need to count from an arbitrary starting point.
class LoadableCounter < RHDL::Sim::SequentialComponent
input :clk, :rst, :enable
input :load # Parallel load strobe
input :data, width: 8 # Load value
output :count, width: 8
output :carry
register :count_reg, width: 8,
clock: :clk, reset: :rst,
reset_value: 0
behavior do
if rst
count_reg.next <= 0
elsif load
count_reg.next <= data # Priority: load over enable
elsif enable
count_reg.next <= count_reg + 1
end
count <= count_reg
carry <= enable & count_reg.eq?(0xFF)
end
end
Saturating Counter -- stops at the maximum value instead of wrapping around. Useful in branch predictors, signal processing, and any context where overflow would produce incorrect results. The saturated output flag indicates the counter has reached its maximum.
class SaturatingCounter < RHDL::Sim::SequentialComponent
input :clk, :rst, :enable
output :count, width: 8
output :saturated
register :count_reg, width: 8,
clock: :clk, reset: :rst,
reset_value: 0
behavior do
at_max = count_reg.eq?(0xFF)
if rst
count_reg.next <= 0
elsif enable & ~at_max
count_reg.next <= count_reg + 1
end
count <= count_reg
saturated <= at_max
end
end
Cascaded Counter -- chains two 8-bit counters to form a 16-bit counter. The carry output of the low byte drives the enable of the high byte, demonstrating hierarchical design composition in RHDL.
class Counter16 < RHDL::Sim::SequentialComponent
input :clk, :rst, :enable
output :count, width: 16
output :carry
# Instantiate two 8-bit counters
component :low_byte, Counter
component :high_byte, Counter
wire_map do
# Low byte: enabled by external enable
low_byte.clk <= clk
low_byte.rst <= rst
low_byte.enable <= enable
# High byte: enabled by low byte carry
high_byte.clk <= clk
high_byte.rst <= rst
high_byte.enable <= low_byte.carry
# Concatenate outputs for 16-bit count
count <= concat(high_byte.count,
low_byte.count)
carry <= high_byte.carry
end
end