RTL Design Patterns · Chapter 3 · Counters, Timers & Pulse Generators
Pulse & Strobe Generators
Control logic thinks in events: start now, one sample is ready, advance the pointer once. But the signals feeding it usually arrive as levels, like a go bit firmware holds high for hundreds of cycles or a status flag that stays asserted. A held-high level is not an event, so wiring it into logic that means do it once fires that action every single cycle the level stays high. The fix is the pulse or strobe generator, which turns a level or a slow edge into a clean single-cycle pulse, high for exactly one cycle per event. The core idea is that a strobe is this cycle's value and not last cycle's: register the signal and gate it. From that atom come rising, falling, and any-edge detectors, the one-shot that re-arms on deassert, the pulse stretcher, and the async-input hazard. Everything is built and self-checked in SystemVerilog, Verilog, and VHDL.
Intermediate16 min readRTL Design PatternsEdge DetectorSingle-Cycle StrobeOne-ShotPulse StretcherClock Domain Crossing
Chapter 3 · Section 3.6 · Counters, Timers & Pulse Generators
1. The Engineering Problem
You are building a small transaction engine. It exposes a control register with a start bit: firmware writes 1 to start, and the engine is supposed to launch one transaction. Simple enough — until you look at what start actually is on the wire. Firmware sets the bit and moves on; the register keeps driving start high for as long as the bit is set, which is hundreds of clock cycles. From the hardware's point of view, start is not the event "launch now" — it is the level "the start bit is still 1," held high cycle after cycle.
Now wire that level into the launch logic the obvious way: if (start) begin launch_transaction; end. Every cycle start is high, the condition is true, so the engine launches a transaction — and then another, and another, one per clock, a flood of hundreds of back-to-back transactions from a single firmware write. The bug is not in the launch logic; it is in the mismatch between what the signal is (a level, true for many cycles) and what the logic needs (an event, true for exactly one cycle).
This mismatch is everywhere in real design. Control logic is written in the language of events — "sample ready," "advance the pointer," "kick off the DMA," "toggle the LED" — each of which must happen once per occurrence. But the signals that carry those occurrences arrive as levels (a held-high enable, a status flag that stays asserted, a go bit) or as the edges of slow inputs (a debounced button, a slow handshake line). Something has to stand between the level and the event-driven logic and convert one into the other: take a signal that is high for many cycles and emit a pulse that is high for exactly one cycle, exactly once, at the moment the level became true. That converter is the pulse/strobe generator, and it is why a well-designed start fires once instead of every cycle.
// Firmware writes the start bit; hardware sees a LEVEL held high for many cycles:
// start = 1'b1; // stays 1 for hundreds of cycles until firmware clears it
// WRONG — treat the LEVEL as the event: launches EVERY cycle start is high.
// always @(posedge clk) if (start) launch <= 1'b1; // a FLOOD of launches
// What the engine needs is a single-cycle STROBE at the moment start went high:
// start_pulse high for EXACTLY ONE cycle, then low even while start stays high.
// That pulse is: start_pulse = start & ~start_d; // this cycle AND-NOT last
// always @(posedge clk) if (start_pulse) launch <= 1'b1; // launches ONCE2. Mental Model
3. Pattern Anatomy
The whole family is built from one atom — a register plus a gate — arranged three ways, plus one hazard that governs when the atom is even valid.
The edge detector (the atom). One register (sig_d <= sig_in) delays the signal by a cycle; one gate compares the two versions. The three useful gates give the three edge polarities:
- Rising-edge strobe:
rise_pulse = sig_in & ~sig_d— high for one cycle when the signal goes 0→1. - Falling-edge strobe:
fall_pulse = ~sig_in & sig_d— high for one cycle when the signal goes 1→0. - Any-edge strobe:
any_pulse = sig_in ^ sig_d— high for one cycle on either transition.
Every one of these is high for exactly one cycle because sig_d catches up to sig_in on the very next edge, collapsing the disagreement. That single-cycle guarantee is the whole point of a strobe.
The one-shot (level → single pulse, re-arm on deassert). A one-shot is the rising-edge detector applied to a level. A go level held high for many cycles produces exactly one go_pulse, and — crucially — it will not produce a second pulse until go returns to 0 and rises again. The falling edge is the re-arm: no re-arm, no second pulse. This is the pattern behind "one action per button press" or "one transaction per firmware write," and it is why a correctly-built start fires once.
The pulse stretcher (1-cycle → N-cycle level). The inverse problem: you have a clean single-cycle strobe, but a slower consumer needs the signal held high for N cycles (a slow-clocked peripheral, an LED you want visible, a downstream block that samples less often). A stretcher catches the 1-cycle pulse and holds its output high while a down-counter (loaded to N on the pulse) counts down — output high for exactly N cycles, then low. It is a register (the counter, 3.1) plus a comparator, the mirror image of the edge detector: one narrows, the other widens.
The pulse family — one register-plus-gate atom, three arrangements, one hazard
data flowThe async-input hazard (why the atom can lie). The edge detector's correctness rests on one assumption: that sig_in is a clean, synchronous signal, stable around each clock edge, so that "its value this cycle vs last cycle" is a meaningful comparison. If sig_in comes from another clock domain (or an asynchronous input like a raw button) and was not synchronized first, it can change right at the sampling edge — the register may go metastable, and worse, the two flops that form sig_in and sig_d can end up disagreeing about a single transition, producing a false pulse or a double pulse from one real edge. The rule is absolute and is previewed in Chapter 11: synchronize an async signal through a two-flop synchronizer first, then edge-detect the clean, synchronous output. Never edge-detect a raw cross-domain signal. The atom on this page assumes a synchronous input; §6 and the synchronizer chapter cover the crossing.
4. Real RTL Implementation
This is the core of the page. We build three patterns — (a) the edge detector / single-cycle strobe (register + pulse = sig_in & ~sig_d, with rising/falling/any variants), (b) the N-generic pulse stretcher (1-cycle → N-cycle via a down-counter), and (c) the level-as-pulse bug (buggy level-driven start vs edge-detected fix) — and each is shown in SystemVerilog, Verilog, and VHDL, with both an RTL block and a self-checking, clocked testbench. The idea is identical in all three; the syntax differs, and seeing them side by side is what makes the pattern language-independent in your head.
One discipline runs through every clocked RTL block: the delay register is written with non-blocking assignment (<=) so sig_d really is last cycle's value, and every design has an explicit synchronous reset that clears the history to a known state — without it, sig_d is X at power-up and the first "edge" is spurious.
4a. The edge detector — single-cycle strobe, three ways
Start with the atom. One flop delays the signal; three gates give the three edges. Because sig_d catches up to sig_in on the next edge, each pulse is high for exactly one cycle. The reset clears sig_d to 0 so the first real rising edge is detected honestly (and no phantom pulse fires at power-up).
module edge_detect (
input logic clk,
input logic rst, // synchronous, active-high; clears the history
input logic sig_in, // MUST be synchronous to clk (see §6 for async)
output logic rise_pulse, // 1 cycle when sig_in goes 0 -> 1
output logic fall_pulse, // 1 cycle when sig_in goes 1 -> 0
output logic any_pulse // 1 cycle on EITHER edge
);
logic sig_d; // sig_in delayed by one cycle = "the value LAST cycle"
// The ONLY state: a one-cycle delay of sig_in. Non-blocking so sig_d is
// genuinely the pre-edge value; reset to 0 so power-up cannot fake an edge.
always_ff @(posedge clk) begin
if (rst) sig_d <= 1'b0;
else sig_d <= sig_in;
end
// Pure combinational compare of "now" vs "then" — one gate per edge polarity.
assign rise_pulse = sig_in & ~sig_d; // high AND was-low -> rose this cycle
assign fall_pulse = ~sig_in & sig_d; // low AND was-high -> fell this cycle
assign any_pulse = sig_in ^ sig_d; // disagree -> either edge
endmoduleThe self-checking testbench does the thing the whole pattern promises: it holds sig_in high for many cycles and asserts rise_pulse is high for exactly one of them, then drops it and checks fall_pulse fires exactly once. A reference model computes the expected pulses from a locally-registered copy of the stimulus, so the check is automatic every cycle.
module edge_detect_tb;
logic clk = 0, rst = 1, sig_in = 0;
logic rise_pulse, fall_pulse, any_pulse;
logic sig_ref; // reference: sig_in delayed one cycle
int rise_count = 0, fall_count = 0, errors = 0;
edge_detect dut (.clk(clk), .rst(rst), .sig_in(sig_in),
.rise_pulse(rise_pulse), .fall_pulse(fall_pulse), .any_pulse(any_pulse));
always #5 clk = ~clk; // rising edge every 10 time units
// Independent reference model of the one-cycle delay, updated every edge.
always_ff @(posedge clk) sig_ref <= rst ? 1'b0 : sig_in;
initial begin
@(negedge clk); rst = 1; @(negedge clk); rst = 0; // release reset
// Drive sig_in HIGH and hold it for 6 cycles: exactly ONE rise_pulse expected.
sig_in = 1;
for (int c = 0; c < 6; c++) begin
@(posedge clk); #1;
if (rise_pulse) rise_count++;
// Invariant: the strobe equals the reference AND-NOT every cycle.
assert (rise_pulse === (sig_in & ~sig_ref))
else begin $error("rise_pulse mismatch @cyc %0d", c); errors++; end
end
// Now drop it and hold LOW for 6 cycles: exactly ONE fall_pulse expected.
sig_in = 0;
for (int c = 0; c < 6; c++) begin
@(posedge clk); #1;
if (fall_pulse) fall_count++;
end
// A level held high for 6 cycles must yield EXACTLY ONE rising strobe.
assert (rise_count == 1)
else begin $error("expected 1 rise_pulse, got %0d", rise_count); errors++; end
assert (fall_count == 1)
else begin $error("expected 1 fall_pulse, got %0d", fall_count); errors++; end
if (errors == 0) $display("PASS: one strobe per edge, exactly one cycle wide");
else $display("FAIL: %0d errors", errors);
$finish;
end
endmoduleThe Verilog form is identical in intent — reg/wire typing and always @(posedge clk) instead of always_ff. The compare gates are the same three continuous assignments.
module edge_detect (
input wire clk,
input wire rst, // synchronous, active-high
input wire sig_in, // must be synchronous to clk
output wire rise_pulse,
output wire fall_pulse,
output wire any_pulse
);
reg sig_d; // one-cycle delay of sig_in
always @(posedge clk) begin
if (rst) sig_d <= 1'b0; // clear history on reset (no phantom edge)
else sig_d <= sig_in; // non-blocking: sig_d is last cycle's value
end
assign rise_pulse = sig_in & ~sig_d; // 0 -> 1
assign fall_pulse = ~sig_in & sig_d; // 1 -> 0
assign any_pulse = sig_in ^ sig_d; // either edge
endmodule module edge_detect_tb;
reg clk, rst, sig_in;
wire rise_pulse, fall_pulse, any_pulse;
reg sig_ref;
integer c, rise_count, fall_count, errors;
edge_detect dut (.clk(clk), .rst(rst), .sig_in(sig_in),
.rise_pulse(rise_pulse), .fall_pulse(fall_pulse), .any_pulse(any_pulse));
initial clk = 1'b0;
always #5 clk = ~clk;
always @(posedge clk) sig_ref <= rst ? 1'b0 : sig_in; // reference delay
initial begin
rise_count = 0; fall_count = 0; errors = 0;
rst = 1'b1; sig_in = 1'b0;
@(negedge clk); @(negedge clk); rst = 1'b0; // release reset
sig_in = 1'b1; // hold HIGH for 6 cycles
for (c = 0; c < 6; c = c + 1) begin
@(posedge clk); #1;
if (rise_pulse) rise_count = rise_count + 1;
if (rise_pulse !== (sig_in & ~sig_ref)) begin
$display("FAIL: rise_pulse mismatch @cyc %0d", c);
errors = errors + 1;
end
end
sig_in = 1'b0; // hold LOW for 6 cycles
for (c = 0; c < 6; c = c + 1) begin
@(posedge clk); #1;
if (fall_pulse) fall_count = fall_count + 1;
end
if (rise_count !== 1) begin
$display("FAIL: expected 1 rise_pulse, got %0d", rise_count);
errors = errors + 1;
end
if (fall_count !== 1) begin
$display("FAIL: expected 1 fall_pulse, got %0d", fall_count);
errors = errors + 1;
end
if (errors == 0) $display("PASS: one strobe per edge, exactly one cycle wide");
else $display("FAIL: %0d errors", errors);
$finish;
end
endmoduleIn VHDL the delay register is a process(clk) guarded by rising_edge(clk); the compare gates are concurrent assignments using and, not, and xor. The signal assignment <= is inherently non-blocking, so sig_d is last cycle's value exactly as in SV/Verilog.
library ieee;
use ieee.std_logic_1164.all;
entity edge_detect is
port (
clk : in std_logic;
rst : in std_logic; -- synchronous, active-high
sig_in : in std_logic; -- must be synchronous to clk
rise_pulse : out std_logic;
fall_pulse : out std_logic;
any_pulse : out std_logic
);
end entity;
architecture rtl of edge_detect is
signal sig_d : std_logic := '0'; -- one-cycle delay of sig_in
begin
-- The only state: a one-cycle delay. Cleared on reset so no phantom edge.
process (clk)
begin
if rising_edge(clk) then
if rst = '1' then sig_d <= '0';
else sig_d <= sig_in;
end if;
end if;
end process;
-- Combinational compare of now vs then — one gate per edge polarity.
rise_pulse <= sig_in and (not sig_d); -- 0 -> 1
fall_pulse <= (not sig_in) and sig_d; -- 1 -> 0
any_pulse <= sig_in xor sig_d; -- either edge
end architecture; library ieee;
use ieee.std_logic_1164.all;
entity edge_detect_tb is
end entity;
architecture sim of edge_detect_tb is
signal clk, rst, sig_in : std_logic := '0';
signal rise_pulse, fall_pulse, any_pulse : std_logic;
begin
dut : entity work.edge_detect
port map (clk => clk, rst => rst, sig_in => sig_in,
rise_pulse => rise_pulse, fall_pulse => fall_pulse, any_pulse => any_pulse);
clk <= not clk after 5 ns; -- rising edge every 10 ns
stim : process
variable rise_count : integer := 0;
variable fall_count : integer := 0;
begin
rst <= '1'; sig_in <= '0';
wait until falling_edge(clk); wait until falling_edge(clk);
rst <= '0'; -- release reset
sig_in <= '1'; -- hold HIGH for 6 cycles
for c in 0 to 5 loop
wait until rising_edge(clk); wait for 1 ns;
if rise_pulse = '1' then rise_count := rise_count + 1; end if;
end loop;
sig_in <= '0'; -- hold LOW for 6 cycles
for c in 0 to 5 loop
wait until rising_edge(clk); wait for 1 ns;
if fall_pulse = '1' then fall_count := fall_count + 1; end if;
end loop;
-- A level held high many cycles must produce EXACTLY ONE rising strobe.
assert rise_count = 1
report "expected exactly one rise_pulse" severity error;
assert fall_count = 1
report "expected exactly one fall_pulse" severity error;
report "edge_detect self-check complete" severity note;
wait;
end process;
end architecture;4b. The pulse stretcher — 1-cycle strobe → N-cycle level (N-generic)
Now the inverse. A single-cycle strobe arrives, but a slower consumer needs the signal held high for N cycles. Load a down-counter to N-1 on the pulse and hold the output high while it counts to zero — high for exactly N cycles. N is a parameter (generic in VHDL), and one design serves any stretch width. One decision is load-bearing: a strobe that arrives mid-stretch must re-trigger (reload the counter), not be ignored and not shorten the current stretch — so a new event always gets its full N cycles.
module pulse_stretch #(
parameter int N = 4 // output stays high for exactly N cycles
)(
input logic clk,
input logic rst, // synchronous, active-high
input logic pulse_in, // single-cycle strobe (from 4a)
output logic stretch_out // high for N cycles after each pulse_in
);
localparam int CW = (N > 1) ? $clog2(N) : 1;
logic [CW-1:0] cnt; // cycles remaining in the current stretch
always_ff @(posedge clk) begin
if (rst) begin
cnt <= '0;
stretch_out <= 1'b0;
end else if (pulse_in) begin
// (Re)trigger: a new event ALWAYS gets a full N-cycle stretch, even
// if one is already in progress. Load N-1 and drive the output high.
cnt <= CW'(N - 1);
stretch_out <= 1'b1;
end else if (cnt != 0) begin
cnt <= cnt - 1'b1; // count down the remaining cycles
stretch_out <= 1'b1;
end else begin
stretch_out <= 1'b0; // counter drained -> stretch done
end
end
endmoduleThe testbench fires a single-cycle pulse_in, then holds pulse_in low and asserts stretch_out stays high for exactly N cycles and then drops — the direct proof that a 1-cycle event became an N-cycle level.
module pulse_stretch_tb;
localparam int N = 4;
logic clk = 0, rst = 1, pulse_in = 0, stretch_out;
int high_count = 0, errors = 0;
pulse_stretch #(.N(N)) dut (.clk(clk), .rst(rst), .pulse_in(pulse_in), .stretch_out(stretch_out));
always #5 clk = ~clk;
initial begin
@(negedge clk); rst = 1; @(negedge clk); rst = 0;
// Fire ONE single-cycle pulse:
pulse_in = 1; @(negedge clk); pulse_in = 0;
// Count how many consecutive cycles stretch_out stays high afterwards.
for (int c = 0; c < N + 3; c++) begin
@(posedge clk); #1;
if (stretch_out) high_count++;
end
// A 1-cycle pulse must become an EXACTLY-N-cycle level.
assert (high_count == N)
else begin $error("stretch width %0d, expected %0d", high_count, N); errors++; end
if (errors == 0) $display("PASS: 1-cycle pulse stretched to exactly N=%0d cycles", N);
else $display("FAIL: %0d errors", errors);
$finish;
end
endmoduleThe Verilog stretcher is the same counter with reg typing; SELW-style width handling is done with a fixed parameter the instantiator sets.
module pulse_stretch #(
parameter N = 4,
parameter CW = 2 // = clog2(N); set by the instantiator
)(
input wire clk,
input wire rst,
input wire pulse_in,
output reg stretch_out
);
reg [CW-1:0] cnt;
always @(posedge clk) begin
if (rst) begin
cnt <= {CW{1'b0}};
stretch_out <= 1'b0;
end else if (pulse_in) begin
cnt <= N - 1; // (re)trigger: full N-cycle stretch
stretch_out <= 1'b1;
end else if (cnt != {CW{1'b0}}) begin
cnt <= cnt - 1'b1; // count the remaining cycles down
stretch_out <= 1'b1;
end else begin
stretch_out <= 1'b0; // stretch complete
end
end
endmodule module pulse_stretch_tb;
parameter N = 4;
parameter CW = 2;
reg clk, rst, pulse_in;
wire stretch_out;
integer c, high_count, errors;
pulse_stretch #(.N(N), .CW(CW)) dut
(.clk(clk), .rst(rst), .pulse_in(pulse_in), .stretch_out(stretch_out));
initial clk = 1'b0;
always #5 clk = ~clk;
initial begin
high_count = 0; errors = 0;
rst = 1'b1; pulse_in = 1'b0;
@(negedge clk); @(negedge clk); rst = 1'b0;
pulse_in = 1'b1; @(negedge clk); pulse_in = 1'b0; // ONE single-cycle pulse
for (c = 0; c < N + 3; c = c + 1) begin
@(posedge clk); #1;
if (stretch_out) high_count = high_count + 1;
end
if (high_count !== N) begin
$display("FAIL: stretch width %0d, expected %0d", high_count, N);
errors = errors + 1;
end
if (errors == 0) $display("PASS: 1-cycle pulse stretched to exactly N=%0d cycles", N);
else $display("FAIL: %0d errors", errors);
$finish;
end
endmoduleIn VHDL the counter is an integer range (a clean, self-documenting width) reloaded on the pulse; the generic sets N. The output is high while the counter is nonzero or the pulse arrives.
library ieee;
use ieee.std_logic_1164.all;
entity pulse_stretch is
generic ( N : positive := 4 ); -- output high for exactly N cycles
port (
clk : in std_logic;
rst : in std_logic; -- synchronous, active-high
pulse_in : in std_logic; -- single-cycle strobe
stretch_out : out std_logic
);
end entity;
architecture rtl of pulse_stretch is
signal cnt : integer range 0 to N := 0; -- cycles remaining in the stretch
begin
process (clk)
begin
if rising_edge(clk) then
if rst = '1' then
cnt <= 0;
stretch_out <= '0';
elsif pulse_in = '1' then
cnt <= N - 1; -- (re)trigger: full N-cycle stretch
stretch_out <= '1';
elsif cnt /= 0 then
cnt <= cnt - 1; -- count down the remaining cycles
stretch_out <= '1';
else
stretch_out <= '0'; -- stretch complete
end if;
end if;
end process;
end architecture; library ieee;
use ieee.std_logic_1164.all;
entity pulse_stretch_tb is
end entity;
architecture sim of pulse_stretch_tb is
constant N : positive := 4;
signal clk, rst, pulse_in : std_logic := '0';
signal stretch_out : std_logic;
begin
dut : entity work.pulse_stretch generic map (N => N)
port map (clk => clk, rst => rst, pulse_in => pulse_in, stretch_out => stretch_out);
clk <= not clk after 5 ns;
stim : process
variable high_count : integer := 0;
begin
rst <= '1'; pulse_in <= '0';
wait until falling_edge(clk); wait until falling_edge(clk);
rst <= '0';
pulse_in <= '1'; wait until falling_edge(clk); pulse_in <= '0'; -- one pulse
for c in 0 to N + 2 loop
wait until rising_edge(clk); wait for 1 ns;
if stretch_out = '1' then high_count := high_count + 1; end if;
end loop;
-- A 1-cycle pulse must become an EXACTLY-N-cycle level.
assert high_count = N
report "stretch width /= N" severity error;
report "pulse_stretch self-check complete" severity note;
wait;
end process;
end architecture;4c. The level-as-pulse bug — buggy (level drives start) vs fixed (edge-detected strobe)
Pattern (c) is the signature failure, shown here as buggy vs fixed RTL in each language, then dramatized narratively in §7. The bug: firmware sets a start bit and leaves it high, and the hardware wires that level straight into the launch condition — so a transaction launches on every cycle start is high, a flood of hundreds of repeats. The fix is one register and one gate: edge-detect start into a single-cycle strobe and launch on the strobe, which fires once and re-arms only when start drops.
// BUGGY: `start` is a LEVEL held high for many cycles. Using it directly as the
// launch condition fires a NEW transaction every cycle it is high.
module launcher_bad (
input logic clk,
input logic rst,
input logic start, // firmware sets this and LEAVES IT HIGH
output logic launch // pulses to kick off ONE transaction
);
always_ff @(posedge clk) begin
if (rst) launch <= 1'b0;
else launch <= start; // launch high for EVERY cycle start is high -> FLOOD
end
endmodule
// FIXED: edge-detect `start` into a single-cycle strobe. `launch` fires for ONE
// cycle when start RISES, and cannot fire again until start drops and rises.
module launcher_good (
input logic clk,
input logic rst,
input logic start,
output logic launch
);
logic start_d; // start delayed one cycle
always_ff @(posedge clk) begin
if (rst) start_d <= 1'b0;
else start_d <= start;
end
assign launch = start & ~start_d; // ONE-shot: one launch per 0->1 of start
endmodule module start_bug_tb;
logic clk = 0, rst = 1, start = 0, launch;
int launch_count = 0, errors = 0;
// Point at launcher_good to PASS; at launcher_bad to see the flood.
launcher_good dut (.clk(clk), .rst(rst), .start(start), .launch(launch));
always #5 clk = ~clk;
initial begin
@(negedge clk); rst = 1; @(negedge clk); rst = 0;
// Firmware behaviour: set start and HOLD it high for 8 cycles.
start = 1;
for (int c = 0; c < 8; c++) begin
@(posedge clk); #1;
if (launch) launch_count++;
end
start = 0; @(posedge clk); #1;
// A single held-high start must launch EXACTLY ONE transaction.
assert (launch_count == 1)
else begin $error("expected 1 launch, got %0d (level treated as pulse?)", launch_count); errors++; end
if (errors == 0) $display("PASS: one held-high start -> exactly one launch");
else $display("FAIL: %0d launches from one start (a flood)", launch_count);
$finish;
end
endmoduleThe Verilog pair is the same story with reg typing. Holding start high and counting launch pulses is what exposes the flood: the buggy module counts one per cycle, the fixed one counts exactly one.
// BUGGY: level drives launch -> a new transaction every cycle start is high.
module launcher_bad (
input wire clk,
input wire rst,
input wire start,
output reg launch
);
always @(posedge clk) begin
if (rst) launch <= 1'b0;
else launch <= start; // FLOOD: launch high every cycle start is high
end
endmodule
// FIXED: edge-detect start into a single-cycle strobe -> one launch per rise.
module launcher_good (
input wire clk,
input wire rst,
input wire start,
output wire launch
);
reg start_d;
always @(posedge clk) begin
if (rst) start_d <= 1'b0;
else start_d <= start;
end
assign launch = start & ~start_d; // one-shot: one launch per 0->1 of start
endmodule module start_bug_tb;
reg clk, rst, start;
wire launch;
integer c, launch_count, errors;
// Bind to launcher_good to PASS; to launcher_bad to observe the flood.
launcher_good dut (.clk(clk), .rst(rst), .start(start), .launch(launch));
initial clk = 1'b0;
always #5 clk = ~clk;
initial begin
launch_count = 0; errors = 0;
rst = 1'b1; start = 1'b0;
@(negedge clk); @(negedge clk); rst = 1'b0;
start = 1'b1; // set start and HOLD it high
for (c = 0; c < 8; c = c + 1) begin
@(posedge clk); #1;
if (launch) launch_count = launch_count + 1;
end
start = 1'b0; @(posedge clk); #1;
if (launch_count !== 1) begin
$display("FAIL: expected 1 launch, got %0d (level treated as pulse?)", launch_count);
errors = errors + 1;
end
if (errors == 0) $display("PASS: one held-high start -> exactly one launch");
else $display("FAIL: %0d launches from one start (a flood)", launch_count);
$finish;
end
endmoduleIn VHDL the bug is identical: the buggy architecture registers the start level onto launch; the fixed one registers a delayed copy and drives launch from the AND-NOT. A raw async start would additionally need a two-flop synchronizer before this edge detector (§6).
library ieee;
use ieee.std_logic_1164.all;
-- BUGGY: registering the LEVEL makes launch high every cycle start is high.
entity launcher_bad is
port ( clk, rst, start : in std_logic; launch : out std_logic );
end entity;
architecture rtl of launcher_bad is
begin
process (clk)
begin
if rising_edge(clk) then
if rst = '1' then launch <= '0';
else launch <= start; -- FLOOD: one launch per high cycle
end if;
end if;
end process;
end architecture;
-- FIXED: delay start one cycle and AND-NOT -> single-cycle strobe (one-shot).
library ieee;
use ieee.std_logic_1164.all;
entity launcher_good is
port ( clk, rst, start : in std_logic; launch : out std_logic );
end entity;
architecture rtl of launcher_good is
signal start_d : std_logic := '0';
begin
process (clk)
begin
if rising_edge(clk) then
if rst = '1' then start_d <= '0';
else start_d <= start;
end if;
end if;
end process;
launch <= start and (not start_d); -- one launch per 0 -> 1 of start
end architecture; library ieee;
use ieee.std_logic_1164.all;
entity start_bug_tb is
end entity;
architecture sim of start_bug_tb is
signal clk, rst, start : std_logic := '0';
signal launch : std_logic;
begin
-- Bind to launcher_good to PASS; to launcher_bad to observe the flood.
dut : entity work.launcher_good port map (clk => clk, rst => rst, start => start, launch => launch);
clk <= not clk after 5 ns;
stim : process
variable launch_count : integer := 0;
begin
rst <= '1'; start <= '0';
wait until falling_edge(clk); wait until falling_edge(clk);
rst <= '0';
start <= '1'; -- set start and HOLD it high
for c in 0 to 7 loop
wait until rising_edge(clk); wait for 1 ns;
if launch = '1' then launch_count := launch_count + 1; end if;
end loop;
start <= '0'; wait until rising_edge(clk); wait for 1 ns;
-- One held-high start must launch EXACTLY ONE transaction.
assert launch_count = 1
report "level treated as pulse: launch fired more than once" severity error;
report "start_bug self-check complete" severity note;
wait;
end process;
end architecture;Across all three languages the lesson is one sentence: an event is an edge, not a level — to fire logic once per occurrence, edge-detect the signal into a single-cycle strobe (sig & ~sig_d) and drive the action from the strobe, never from the held-high level.
5. Verification Strategy
A pulse generator's correctness is temporal — it is a claim about how many cycles a signal is high and how many times it fires — so its testbench is clocked and its self-check counts cycles and pulses. The single invariant that captures a correct strobe is:
A signal held high for many cycles produces exactly ONE rising strobe, high for exactly ONE cycle; the generator does not fire again until the signal drops and rises again.
Everything below is that invariant, made checkable, and it maps directly onto the clocked testbenches in §4.
- Hold the input high, assert exactly one strobe. The core self-check: drive
sig_inhigh and keep it high for several cycles, countrise_pulseassertions, and assert the count is exactly1and the pulse was high for exactly one cycle. This is the property the whole page exists for — a level yields one strobe — and it is what the §4a testbenches do (SVassert (rise_count == 1), Verilogif (rise_count !== 1) $display("FAIL"), VHDLassert rise_count = 1 ... severity error). A generator that fires every cycle (the §7 bug) fails this on the second cycle. - Test both polarities. Rising and falling are different gates and must be tested separately: hold high (expect one
rise_pulse), then drop and hold low (expect onefall_pulse). A design that got the polarity backwards passes a rising-only test and fails here — which is exactly why §4a exercises both. - The one-shot re-arms only after deassert. Prove the re-arm: after the first strobe, keep the level high and assert no further strobe; then drop the level, raise it again, and assert a second strobe now appears. This distinguishes a true one-shot from a generator that leaks extra pulses, and it is the property the launcher of §4c relies on.
- The stretcher holds exactly N. For the stretcher, fire a single-cycle
pulse_inand assertstretch_outis high for exactlyNcycles and then low — a direct measurement of the stretch width against the parameter. Re-run for severalN(a stretcher is parameter-generic and must be verified generic, not assumed generic), and add a re-trigger corner: fire a secondpulse_inmid-stretch and assert the output extends to a fullNcycles from the new pulse, not the old one. - The async corner — false/double pulses. The dangerous case is an input that was not synchronized. Conceptually, if
sig_incan change at the sampling edge, one real edge can produce a false pulse or a double pulse. The verification response is architectural, not a single assertion: drive the edge detector only with a synchronous stimulus and, at the system level, require that any async input passes through a two-flop synchronizer first (Chapter 11). The invariant to state is: "the edge detector's input is always synchronous toclk" — a design constraint the testbench honours and a review must confirm. - Expected waveform. A correct rising strobe is a single narrow spike one cycle wide, appearing on the cycle
sig_intransitions 0→1, then flat-low even whilesig_instays high — never a level that tracks the input. A correct stretcher shows a one-cycle input spike widening into an N-cycle output plateau. The visual signature of the §7 bug is the opposite: alaunchthat tracks the held-high level, a plateau where there should be a spike.
6. Common Mistakes
Treating a level as a pulse. The signature failure of this pattern. A held-high go/start/enable is still true every cycle, so logic that reads it directly ("if the level is high, do the thing") does the thing every cycle the level is high — a start that launches hundreds of back-to-back transactions from one firmware write, a counter that increments continuously instead of by one. The fix is to edge-detect the level into a single-cycle strobe (sig & ~sig_d) and drive the action from the strobe. The tell is a burst of repeats where you expected one action. (Full post-mortem in §7; buggy-vs-fixed RTL for all three HDLs in §4c.)
Edge-detecting an unsynchronized async input. An edge detector assumes its input is synchronous — stable around each clock edge — so that "value this cycle vs last cycle" is meaningful. Feed it a raw asynchronous signal (another clock domain, a bare button) and two failures appear: the delay flop can go metastable, and the two sampled versions (sig_in and sig_d) can disagree about a single transition, producing a false pulse or a double pulse from one real edge. Never edge-detect a raw cross-domain signal; synchronize it through a two-flop synchronizer first, then edge-detect the clean, synchronous output. This is the single most common way a "working" pulse generator produces intermittent, unreproducible false triggers on hardware.
Wrong edge polarity. The gates look alike and are easy to swap: sig & ~sig_d fires on the rising edge, ~sig & sig_d on the falling edge, sig ^ sig_d on either. Wire the falling-edge gate where you meant rising and the strobe fires one cycle after the signal drops instead of when it rises — the logic still "works" for a steady input but triggers on the wrong transition. Always name the strobe for its edge (rise_pulse, fall_pulse) and verify the polarity with a directed hold-high / hold-low test, not just a single toggle.
A stretcher that re-triggers wrong (or not at all). If a new pulse_in arrives mid-stretch, the intended behaviour is usually to reload the counter so the new event gets a full N cycles. A stretcher that instead ignores the new pulse (or, worse, only extends by the remaining count) silently drops or shortens events — the output looks plausible but a second event within N cycles is lost or truncated. Decide the re-trigger policy deliberately (reload vs ignore), implement it explicitly (the §4b design reloads on every pulse_in), and test it with a mid-stretch second pulse.
Missing reset on the delay flop. If sig_d powers up as X (no reset), the first "comparison" is sig_in & ~X — undefined, and in a gate-level or FPGA run it can produce a spurious first pulse. Always reset the delay register to a known value (0) so the first real edge is the first strobe, and power-up cannot fake an event.
7. DebugLab
The transaction engine that fired hundreds of times — a level treated as a pulse
The engineering lesson: an event is an edge, not a level — control logic that must act once per occurrence has to be driven by a single-cycle strobe, never by a signal that is held high across many cycles. A held-high level is "the condition is still true," and reading it directly makes the action repeat every cycle the condition holds — the flood in the DebugLab. The tell is a burst of repeated actions where you expected one, and a bug that hides behind a one-cycle test poke but floods under real held-high firmware: if your stimulus pulsed the signal but production holds it, you never tested the actual failure. Two habits make it impossible, and they are identical across SystemVerilog, Verilog, and VHDL: edge-detect any held-high control level into a single-cycle strobe (sig & ~sig_d) and drive the action from the strobe, and verify by holding the input high for many cycles and asserting the action fires exactly once — and if the level is asynchronous, synchronize it before you edge-detect it.
8. Interview Q&A
9. Exercises
Design and reasoning exercises — no syntax drills. Each rehearses the "an event is an edge, not a level" habit.
Exercise 1 — Trace the strobe against a held-high level
A rising-edge detector (sig_d <= sig_in; rise_pulse = sig_in & ~sig_d) starts with sig_d = 0. The input sig_in is 0 at cycle 0, then goes 1 at cycle 1 and stays high through cycle 6, then drops to 0 at cycle 7. Tabulate sig_in, sig_d, and rise_pulse for cycles 0–8. State exactly which cycle(s) rise_pulse is high, and explain in one line why it is high for only one cycle even though sig_in is high for six.
Exercise 2 — Pick the gate for the edge
For each requirement, state which gate on sig_in/sig_d you'd use — sig & ~sig_d, ~sig & sig_d, or sig ^ sig_d — and why in one line: (a) start a DMA when a go bit is set; (b) latch a result when a busy flag clears (goes 1→0); (c) toggle an LED on any change of a debounced switch; (d) count the number of times a status line has been asserted. (Hint: name the transition each requirement cares about.)
Exercise 3 — Why did the strobe fire twice?
An engineer edge-detects a signal that comes directly from a slower clock domain (no synchronizer). On the bench it mostly works, but occasionally a single transition of the source produces two rise_pulse cycles, launching the downstream action twice. Explain precisely how one real edge can become two pulses through the two-flop path (sig_in and sig_d), name the property of the input that was violated, and state the one architectural change that fixes it. (Hint: what is the detector assuming about when the input can change?)
Exercise 4 — Size and re-trigger the stretcher
You have a single-cycle sample_ready strobe on a 100 MHz clock, and a downstream block on a 25 MHz clock that must see the signal held high long enough to sample it reliably. (a) Roughly how many fast-clock cycles must the stretcher hold the output high so the slow block is guaranteed at least one of its own edges within the window — and why "one" is not enough? (b) If two sample_ready strobes can arrive 3 fast cycles apart while N = 8, what does your re-trigger policy do to the second event, and what is the consequence of choosing "ignore during stretch" instead of "reload"?
10. Related Tutorials
Continue in RTL Design Patterns (this-batch siblings go live together; forward links unlock as they ship):
- Ring & Johnson Counters — Chapter 3.4; shift-register counters whose one-hot output is a natural, glitch-free source of periodic single-cycle strobes.
- Clock Dividers & Timers — Chapter 3.5; where periodic strobes come from — a timer's terminal-count is a single-cycle tick, generated exactly like the strobes on this page.
- valid/ready Handshake — Chapter 9.1;
validandreadyare levels, and a transfer is the single-cycle eventvalid & ready— the level-vs-event distinction of this page, formalized into a protocol. - Pulse Handshake Synchronizer — Chapter 11; how to move a single-cycle pulse safely across clock domains when a two-flop synchronizer alone would miss it.
- Two-Flop Synchronizer — Chapter 11.2; the mandatory front-end for any async signal before you edge-detect it — the fix for this page's async hazard.
- FSM Fundamentals — Chapter 4.1; the control logic that consumes these strobes as single-cycle "go" events driving state transitions.
Backward / method:
- The Register Pattern (D-FF) — Chapter 2.1; the one-cycle delay register (
sig_d <= sig_in) that this whole pattern is built on — an edge detector is a register plus one gate. - Synchronous Reset — Chapter 2.3; why the delay flop needs a defined power-up value so the first real edge is the first strobe.
- Up/Down Counters — Chapter 3.1; the down-counter inside the pulse stretcher that re-widens a 1-cycle tick into an N-cycle level.
- Mod-N Counters — Chapter 3.2; the terminal-count strobe a mod-N counter emits is exactly the single-cycle tick this page generates.
- Gray Counters — Chapter 3.3; single-bit-change encoding, the counterpart discipline for pointers that must cross clock domains cleanly.
- Multiplexers — Chapter 1.1; the selection primitive a strobe often gates (load-enable = a 2:1 mux driven by a single-cycle strobe).
- The RTL Design Mindset — Chapter 0.2; the Interface → State → Datapath → Control → Verification lenses this page applies to the level-vs-event boundary.
- What Is an RTL Design Pattern? — Chapter 0.1; why the strobe generator earns the name "pattern" — a recurring need, a one-gate form, and a signature failure.
Verilog v1 prerequisites (the grammar this pattern relies on):
- Blocking and Non-Blocking Assignments — why the delay register uses
<=sosig_dis genuinely last cycle's value. - Bitwise Operators — the
&,~, and^that form the rising, falling, and any-edge strobes. - Case Statements — the construct behind the stretcher's state selection.
- if-else Statements — the reset/trigger/count-down priority in the stretcher's clocked block.
11. Summary
- A strobe is this cycle's value AND-NOT last cycle's. Register the signal (
sig_d <= sig_in) and computepulse = sig_in & ~sig_dfor a rising-edge strobe — high for exactly one cycle, on the single cycle where "now" and "then" disagree. Swap the gate for the other edges:~sig_in & sig_d(falling),sig_in ^ sig_d(any). The whole thing is the register of Chapter 2.1 plus one gate. - It turns levels into events. Control logic runs on events ("start now," "sample ready"), but signals arrive as levels held high for many cycles. The edge detector converts a held-high level into a single one-shot pulse that fires once and re-arms only when the level drops — which is why a correctly-built
startlaunches one transaction instead of a flood. - The stretcher is the inverse. When a slower consumer needs a signal held high, a down-counter loaded to
N-1on the strobe re-widens a 1-cycle pulse into an exactly-N-cycle level.Nis a parameter, and a second pulse mid-stretch must re-trigger (reload) so no event is shortened. - Never edge-detect a raw async signal. The atom assumes a synchronous input; feed it an unsynchronized cross-domain signal and it produces false or double pulses (metastability plus a disagreement between
sig_inandsig_d). Synchronize through a two-flop synchronizer first, then edge-detect — the async hazard fixed in Chapter 11. - Verify temporally, in every HDL. A clocked, self-checking testbench holds the input high for many cycles and asserts exactly one strobe, one cycle wide; tests both polarities; proves the one-shot re-arms only on deassert; and measures the stretcher at exactly
Ncycles. The signature bug — a level treated as a pulse (§7) — hides behind a one-cycle test poke but floods under real held-high firmware; the self-checking testbenches, shown here in SystemVerilog, Verilog, and VHDL, catch it by holding the input high.