Back to examples

Counter Deep Dive

Building a sequential counter with clock, reset, and enable. Understanding clock-driven hardware design through a practical example.

Sequential Counter Clock Reset

Ruby Implementation

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.

counter.rb
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.

Generated Verilog

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.

counter.v (generated)
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 ConstructVerilog Output
SequentialComponentalways @(posedge clk)
register :count_regreg [7:0] count_reg
count_reg.next <=count_reg <= (non-blocking)
reset_value: 0if (rst) count_reg <= 8'd0
count_reg + 1count_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.

Waveform Analysis

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
0100x000x000Reset asserted
1100x000x000Reset held
2000x000x000Reset released, enable low
3010x010x010First count
4010x020x020Counting
5000x020x020Enable deasserted -- hold
6000x020x020Still holding
7010x030x030Resume counting
........................
257010xFE0xFE0Approaching overflow
258010xFF0xFF1Carry flag goes high
259010x000x000Wrap 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.

counter_waveform.rb
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.

Variations

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.

up_down_counter.rb
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.

loadable_counter.rb
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.

saturating_counter.rb
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.

counter_16bit.rb
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