RTL Design Patterns · Chapter 2 · Registers & Sequential Building Blocks
Enables & Loads
A plain D-register updates on every clock edge, but that is almost never what a real register does. A configuration register must latch a value once and hold it for thousands of cycles, an accumulator must pause while the pipeline stalls, and a capture register must sample on the cycle a strobe arrives and then freeze. Each needs a register that updates only when commanded, and often one that chooses which source to load. Those two controls are the clock-enable and the load mux, and they hide the most damaging beginner reflex in sequential design: implementing enable by gating the clock. This lesson shows why an enable is not a gated clock but a feedback mux on the D input, why a load is a mux that selects the data source, and why you always gate the data and never the clock. Everything is built and self-checked in SystemVerilog, Verilog, and VHDL.
Foundation12 min readRTL Design PatternsClock EnableLoad RegisterFeedback MuxClock GatingSequential Logic
Chapter 2 · Section 2.2 · Registers & Sequential Building Blocks
1. The Engineering Problem
You are designing a peripheral's control block, and three of its registers refuse to fit the "update every cycle" mould of the plain D-flop.
The first is a configuration register. Software writes a mode word once, at boot, and the hardware must hold it for the rest of time — millions of cycles during which the write-data bus is carrying completely unrelated traffic. If this register swallowed the bus every cycle like a plain flop, the mode word would be overwritten instantly. It must update on exactly the one cycle the write strobe is asserted, and hold otherwise.
The second is an accumulator that sums a stream of samples. When the upstream stage stalls — no new sample this cycle — the accumulator must not add garbage; it must pause, keeping its running total exactly as it was, and resume when data returns. Again: update when told, hold when not.
The third is a capture register on a status input. It must snapshot the input on the cycle an event fires and then freeze that snapshot so software can read it, undisturbed, whenever it gets around to it.
All three need the same missing feature: a control input that says "take a new value this cycle" versus "keep what you have." That control is the clock-enable. And often a register needs a second decision on top of it — which source to load. A program counter loads either pc + 4 (sequential), a branch target (taken branch), or an exception vector; a shift register loads either a parallel word or its own shifted self. Choosing among those sources on the update cycle is the load mux. Enable answers "do I update?"; load answers "update from what?"
// The plain register from 2.1 — updates UNCONDITIONALLY every edge:
always @(posedge clk) q <= d; // no way to HOLD; no way to CHOOSE d
// What the config register / accumulator / PC actually need:
// * HOLD q until an ENABLE says "update this cycle"
// * on that update, CHOOSE which SOURCE loads (data vs feedback vs vector)
// Both are answered by MUXES in front of the flop — never by touching clk.2. Mental Model
3. Pattern Anatomy
An enabled, loadable register is a plain D-flop with one or two muxes bolted onto its D input. Nothing else changes — same clock, same reset, same Q.
The enable — a hold-vs-update feedback mux. Start with the plain flop q <= d. To make it hold, put a 2:1 mux in front of D: one input is the new data d, the other is q fed back, and the enable en is the select. When en is high the flop captures d; when en is low it captures its own q and appears frozen. The register still clocks every cycle — it just re-loads the same value on disabled cycles. This is the q <= en ? d : q form, and the feedback path (Q back to the mux) is the part beginners forget, which is exactly what turns "hold" into an accidental latch or a wrong value (§6).
The load mux — choose the source. Layer a second mux ahead of the enable mux to select which source becomes the update value. A program counter's load mux picks among pc+4, a branch target, and an exception vector; a shift register's load mux picks between a parallel word and its own shifted self. The load select (load_sel) steers that choice; the result feeds the enable mux, which decides whether to commit it this cycle.
Priority when both exist. With both an enable and a load, order matters and must be deliberate. The natural, almost universal priority is enable outermost: if the register is disabled, it holds regardless of load_sel — no source loads, period. load_sel only matters on cycles the enable permits an update. Written out: q <= en ? source[load_sel] : q. The enable gates; the load chooses within the gate. (Some designs also carry a synchronous reset, whose priority sits above both: reset wins, then enable, then load — but reset is 2.3/2.5's topic; here we keep reset simple and focus on enable-over-load.)
Why you gate the DATA and not the CLOCK. You could imagine implementing hold by stopping the clock — always @(posedge (clk & en)) — and it even simulates. It is wrong hardware for four concrete reasons, all covered in §6 and §7: (1) ANDing a gate into the clock creates glitches — if the enable changes while the clock is high, the AND output produces a spurious edge that clocks the flop at the wrong instant; (2) it adds an extra gate delay into the clock path, wrecking clock skew and timing closure; (3) it fractures the clock tree the physical tools spent effort balancing, and static timing can no longer reason about the domain cleanly; (4) it is hostile to CDC analysis and to the standard-cell libraries and tools, which expect enables on the data path. The synchronous-enable feedback mux has none of these problems: the clock stays pristine, and the control is ordinary combinational logic in front of D. (There is a legitimate, disciplined way to gate a clock for power — an integrated clock-gating cell — but that is a different mechanism from an RTL enable, and a later low-power topic; §8 draws the line.)
Ties to 1.1 Multiplexers. The enable and the load are muxes — the exact 2:1 and N:1 selectors from 1.1, now feeding a flop instead of a bus. Everything true of a mux is true here: the output is one of the inputs verbatim, the select steers, and an incompletely-specified selection can infer the wrong hardware. The only new element is that the mux's output is registered.
Enable and load — muxes stacked in front of a D-flop (clock untouched)
data flowThe anatomy closes with the discipline that keeps it correct: because the enable and load are combinational muxes feeding a clocked assignment, every path must produce a defined D — a total conditional (en ? d : q) or an exhaustive case/when for the load — so no cycle leaves D undriven. Miss the hold path (the : q) and you either infer a latch in a combinational mistake or, in the clocked block, silently drop the enable's meaning. Miss a load-select arm and the source choice becomes a latch or an X.
4. Real RTL Implementation
This is the core of the page. We build three patterns — (a) a register with a synchronous clock-enable (the feedback-mux form, WIDTH-generic); (b) a loadable register (a load mux selecting a source, plus the enable, with explicit priority); and (c) the gated-clock anti-pattern (buggy posedge (clk & en) vs the fixed synchronous enable) — and each is shown in SystemVerilog, Verilog, and VHDL, with an RTL block and a self-checking clocked testbench. The idea is identical in all three; only the syntax differs.
One discipline runs through every clocked block: the enable lives on the D side, the clock is never gated, and every path produces a defined next value. In SystemVerilog and Verilog that means a clocked if (en) q <= d; (whose implicit else is "hold Q" — exactly the feedback mux); in VHDL it means if rising_edge(clk) then if en = '1' then ... end if; end if;, whose omitted else likewise holds. Never @(posedge (clk & en)).
4a. Register with a synchronous clock-enable — the feedback-mux form
The atom of the page: a WIDTH-generic register that updates on an edge only when en is high, and holds otherwise. The clocked if (en) with no explicit else is the feedback mux q <= en ? d : q — on a disabled cycle no assignment runs, so the flop keeps its value, which is precisely "feed Q back to D." This is a flop enable, not a clock gate: the always block is still sensitive to posedge clk alone.
module reg_en #(
parameter int WIDTH = 8 // any datapath width
)(
input logic clk, // clean, ungated clock — runs every cycle
input logic rst_n, // synchronous active-low reset (simple here)
input logic en, // ENABLE: high = update, low = HOLD
input logic [WIDTH-1:0] d, // new data (loaded only when en)
output logic [WIDTH-1:0] q // registered output
);
// Synchronous enable == a MUX on D: q <= en ? d : q. The absence of an
// explicit "else" is the HOLD path (Q feeds back to itself). The clock is
// NOT gated — the block is sensitive to posedge clk only, every cycle.
always_ff @(posedge clk) begin
if (!rst_n) q <= '0; // sync reset (kept minimal; see 2.3)
else if (en) q <= d; // update; implicit else => HOLD (feedback)
end
endmoduleThe self-checking testbench generates a clock, then does the two things a plain-flop TB never has to: it drives en low across several edges and asserts Q holds, and it drives en high and asserts Q takes D on the next edge. The reference model is a one-liner: after each edge, expected = en ? d : expected.
module reg_en_tb;
localparam int WIDTH = 8;
logic clk = 0, rst_n, en;
logic [WIDTH-1:0] d, q, exp;
int errors = 0;
reg_en #(.WIDTH(WIDTH)) dut (.clk(clk), .rst_n(rst_n), .en(en), .d(d), .q(q));
always #5 clk = ~clk; // 100 MHz-ish clock
// Reference model: sampled in the SAME clocked region as the DUT.
task automatic step(input logic en_i, input logic [WIDTH-1:0] d_i);
en = en_i; d = d_i;
@(posedge clk); // drive, then take the edge
exp = en_i ? d_i : exp; // golden: update or HOLD
#1; // settle past the NBA region
assert (q === exp)
else begin $error("en=%b d=%h q=%h exp=%h", en_i, d_i, q, exp); errors++; end
endtask
initial begin
rst_n = 0; en = 0; d = '0; @(posedge clk); #1; exp = '0; rst_n = 1;
step(1'b1, 8'hA5); // en=1 -> Q takes A5
step(1'b0, 8'h3C); // en=0 -> Q HOLDS A5 (d ignored)
step(1'b0, 8'hFF); // en=0 -> still A5 across edges
step(1'b1, 8'h77); // en=1 -> Q takes 77
step(1'b0, 8'h00); // en=0 -> HOLDS 77
if (errors == 0) $display("PASS: reg_en holds on en=0, updates on en=1");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleThe Verilog form is identical in intent — reg/wire typing and $display instead of $error. The clocked if (en) is the same feedback mux; the block is sensitive to posedge clk only.
module reg_en #(
parameter WIDTH = 8
)(
input wire clk, // ungated clock
input wire rst_n, // synchronous active-low reset
input wire en, // ENABLE: update vs HOLD
input wire [WIDTH-1:0] d,
output reg [WIDTH-1:0] q
);
// if(en) with no else is the feedback mux q <= en ? d : q. Sensitivity is
// posedge clk ONLY — never posedge (clk & en).
always @(posedge clk) begin
if (!rst_n) q <= {WIDTH{1'b0}};
else if (en) q <= d; // implicit else => hold
end
endmodule module reg_en_tb;
parameter WIDTH = 8;
reg clk, rst_n, en;
reg [WIDTH-1:0] d, exp;
wire [WIDTH-1:0] q;
integer errors;
reg_en #(.WIDTH(WIDTH)) dut (.clk(clk), .rst_n(rst_n), .en(en), .d(d), .q(q));
initial clk = 0;
always #5 clk = ~clk;
task do_step;
input en_i;
input [WIDTH-1:0] d_i;
begin
en = en_i; d = d_i;
@(posedge clk);
exp = en_i ? d_i : exp; // reference: update or HOLD
#1;
if (q !== exp) begin
$display("FAIL: en=%b d=%h q=%h exp=%h", en_i, d_i, q, exp);
errors = errors + 1;
end
end
endtask
initial begin
errors = 0; rst_n = 0; en = 0; d = 0;
@(posedge clk); #1; exp = 0; rst_n = 1;
do_step(1'b1, 8'hA5); // update -> A5
do_step(1'b0, 8'h3C); // HOLD -> A5
do_step(1'b0, 8'hFF); // HOLD -> A5
do_step(1'b1, 8'h77); // update -> 77
do_step(1'b0, 8'h00); // HOLD -> 77
if (errors == 0) $display("PASS: reg_en holds on en=0, updates on en=1");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleIn VHDL the same enable is an inner if en = '1' nested inside if rising_edge(clk). The omitted else on the enable is the hold path — VHDL's feedback mux. The clock stays on the sensitivity of rising_edge(clk) alone; the enable is pure data-side logic.
library ieee;
use ieee.std_logic_1164.all;
entity reg_en is
generic ( WIDTH : positive := 8 ); -- generic == parameter
port (
clk : in std_logic; -- ungated clock
rst_n : in std_logic; -- synchronous active-low reset
en : in std_logic; -- ENABLE: update vs HOLD
d : in std_logic_vector(WIDTH-1 downto 0);
q : out std_logic_vector(WIDTH-1 downto 0)
);
end entity;
architecture rtl of reg_en is
begin
-- rising_edge(clk) drives the flop EVERY cycle; the nested "if en = '1'"
-- with no else is the HOLD path (q keeps its value) — the feedback mux.
process (clk)
begin
if rising_edge(clk) then
if rst_n = '0' then
q <= (others => '0');
elsif en = '1' then
q <= d; -- else (implicit) => hold
end if;
end if;
end process;
end architecture;The VHDL testbench clocks the DUT, drives en/d, and self-checks with assert q = exp report … severity error after each edge — HOLD when en = '0', update when en = '1'.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity reg_en_tb is
end entity;
architecture sim of reg_en_tb is
constant WIDTH : positive := 8;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal en : std_logic := '0';
signal d : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
signal q : std_logic_vector(WIDTH-1 downto 0);
begin
dut : entity work.reg_en
generic map (WIDTH => WIDTH)
port map (clk => clk, rst_n => rst_n, en => en, d => d, q => q);
clk <= not clk after 5 ns; -- free-running clock
stim : process
variable exp : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
procedure step(en_i : std_logic; d_i : std_logic_vector(WIDTH-1 downto 0)) is
begin
en <= en_i; d <= d_i;
wait until rising_edge(clk);
if en_i = '1' then exp := d_i; end if; -- update or HOLD
wait for 1 ns;
assert q = exp
report "reg_en mismatch: q /= (en ? d : held)" severity error;
end procedure;
begin
rst_n <= '0'; wait until rising_edge(clk); wait for 1 ns; rst_n <= '1';
step('1', x"A5"); -- update -> A5
step('0', x"3C"); -- HOLD -> A5
step('0', x"FF"); -- HOLD -> A5
step('1', x"77"); -- update -> 77
step('0', x"00"); -- HOLD -> 77
report "reg_en self-check complete" severity note;
wait;
end process;
end architecture;4b. Loadable register — a load mux selecting a source, with enable priority
Now add the second mux. A loadable register chooses which source becomes the update value, then the enable decides whether to commit it. The priority is enable outermost: on a disabled cycle the register holds regardless of load_sel; load_sel matters only when en permits an update. The example is a small program-counter-style register: source 0 is the sequential value d_seq, source 1 is a branch/parallel value d_alt.
module reg_load #(
parameter int WIDTH = 8
)(
input logic clk,
input logic rst_n,
input logic en, // ENABLE: gate on the update (outermost)
input logic load_sel, // LOAD: 0 = d_seq, 1 = d_alt
input logic [WIDTH-1:0] d_seq, // source 0 (e.g. pc + 4)
input logic [WIDTH-1:0] d_alt, // source 1 (e.g. branch target)
output logic [WIDTH-1:0] q
);
// Two muxes in front of the flop: the LOAD mux picks the source, the ENABLE
// mux picks new-vs-held. Priority is EXPLICIT: en gates first, so a disabled
// cycle holds no matter what load_sel is.
logic [WIDTH-1:0] src;
always_comb src = load_sel ? d_alt : d_seq; // load mux (combinational)
always_ff @(posedge clk) begin
if (!rst_n) q <= '0;
else if (en) q <= src; // en outermost; else => HOLD (feedback)
end
endmoduleThe testbench must check three things, not two: with en=0, Q holds across edges regardless of load_sel; with en=1 and load_sel=0, Q takes d_seq; with en=1 and load_sel=1, Q takes d_alt. The reference model encodes the priority exactly: expected = en ? (load_sel ? d_alt : d_seq) : expected.
module reg_load_tb;
localparam int WIDTH = 8;
logic clk = 0, rst_n, en, load_sel;
logic [WIDTH-1:0] d_seq, d_alt, q, exp;
int errors = 0;
reg_load #(.WIDTH(WIDTH)) dut (.clk(clk), .rst_n(rst_n), .en(en),
.load_sel(load_sel), .d_seq(d_seq), .d_alt(d_alt), .q(q));
always #5 clk = ~clk;
task automatic step(input logic en_i, input logic ls_i,
input logic [WIDTH-1:0] s0, input logic [WIDTH-1:0] s1);
en = en_i; load_sel = ls_i; d_seq = s0; d_alt = s1;
@(posedge clk);
if (en_i) exp = ls_i ? s1 : s0; // golden: en gates, load chooses
#1;
assert (q === exp)
else begin $error("en=%b ls=%b q=%h exp=%h", en_i, ls_i, q, exp); errors++; end
endtask
initial begin
rst_n = 0; en = 0; load_sel = 0; d_seq = '0; d_alt = '0;
@(posedge clk); #1; exp = '0; rst_n = 1;
step(1'b1, 1'b0, 8'h11, 8'h22); // en, load_sel=0 -> Q takes d_seq (11)
step(1'b1, 1'b1, 8'h33, 8'h44); // en, load_sel=1 -> Q takes d_alt (44)
step(1'b0, 1'b0, 8'h55, 8'h66); // en=0 -> HOLD 44 even though load_sel=0
step(1'b0, 1'b1, 8'h77, 8'h88); // en=0 -> still 44 (priority: en outermost)
step(1'b1, 1'b0, 8'h99, 8'hAA); // en, load_sel=0 -> Q takes d_seq (99)
if (errors == 0) $display("PASS: reg_load respects enable-over-load priority");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleThe Verilog loadable register mirrors this with reg outputs and an always @(*) load mux feeding a clocked always @(posedge clk). Same priority, same feedback-mux hold.
module reg_load #(
parameter WIDTH = 8
)(
input wire clk,
input wire rst_n,
input wire en, // outermost gate
input wire load_sel, // 0 = d_seq, 1 = d_alt
input wire [WIDTH-1:0] d_seq,
input wire [WIDTH-1:0] d_alt,
output reg [WIDTH-1:0] q
);
reg [WIDTH-1:0] src;
always @(*) src = load_sel ? d_alt : d_seq; // LOAD mux
always @(posedge clk) begin
if (!rst_n) q <= {WIDTH{1'b0}};
else if (en) q <= src; // ENABLE outermost; else => hold
end
endmodule module reg_load_tb;
parameter WIDTH = 8;
reg clk, rst_n, en, load_sel;
reg [WIDTH-1:0] d_seq, d_alt, exp;
wire [WIDTH-1:0] q;
integer errors;
reg_load #(.WIDTH(WIDTH)) dut (.clk(clk), .rst_n(rst_n), .en(en),
.load_sel(load_sel), .d_seq(d_seq), .d_alt(d_alt), .q(q));
initial clk = 0;
always #5 clk = ~clk;
task do_step;
input en_i, ls_i;
input [WIDTH-1:0] s0, s1;
begin
en = en_i; load_sel = ls_i; d_seq = s0; d_alt = s1;
@(posedge clk);
if (en_i) exp = ls_i ? s1 : s0; // en gates, load chooses
#1;
if (q !== exp) begin
$display("FAIL: en=%b ls=%b q=%h exp=%h", en_i, ls_i, q, exp);
errors = errors + 1;
end
end
endtask
initial begin
errors = 0; rst_n = 0; en = 0; load_sel = 0; d_seq = 0; d_alt = 0;
@(posedge clk); #1; exp = 0; rst_n = 1;
do_step(1'b1, 1'b0, 8'h11, 8'h22); // load d_seq -> 11
do_step(1'b1, 1'b1, 8'h33, 8'h44); // load d_alt -> 44
do_step(1'b0, 1'b0, 8'h55, 8'h66); // HOLD 44 (en=0 wins over load_sel)
do_step(1'b0, 1'b1, 8'h77, 8'h88); // HOLD 44
do_step(1'b1, 1'b0, 8'h99, 8'hAA); // load d_seq -> 99
if (errors == 0) $display("PASS: reg_load enable-over-load priority holds");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleIn VHDL the load mux is a when/else concurrent assignment (or an inner if), and the enable is the nested if en = '1' inside rising_edge(clk). The priority reads straight off the nesting: reset, then enable, then the pre-selected source.
library ieee;
use ieee.std_logic_1164.all;
entity reg_load is
generic ( WIDTH : positive := 8 );
port (
clk : in std_logic;
rst_n : in std_logic;
en : in std_logic; -- outermost gate
load_sel : in std_logic; -- '0' = d_seq, '1' = d_alt
d_seq : in std_logic_vector(WIDTH-1 downto 0);
d_alt : in std_logic_vector(WIDTH-1 downto 0);
q : out std_logic_vector(WIDTH-1 downto 0)
);
end entity;
architecture rtl of reg_load is
signal src : std_logic_vector(WIDTH-1 downto 0);
begin
src <= d_alt when load_sel = '1' else d_seq; -- LOAD mux (concurrent)
process (clk)
begin
if rising_edge(clk) then
if rst_n = '0' then
q <= (others => '0');
elsif en = '1' then
q <= src; -- ENABLE outermost; else => hold
end if;
end if;
end process;
end architecture; library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity reg_load_tb is
end entity;
architecture sim of reg_load_tb is
constant WIDTH : positive := 8;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal en : std_logic := '0';
signal load_sel : std_logic := '0';
signal d_seq : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
signal d_alt : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
signal q : std_logic_vector(WIDTH-1 downto 0);
begin
dut : entity work.reg_load
generic map (WIDTH => WIDTH)
port map (clk => clk, rst_n => rst_n, en => en, load_sel => load_sel,
d_seq => d_seq, d_alt => d_alt, q => q);
clk <= not clk after 5 ns;
stim : process
variable exp : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
procedure step(en_i, ls_i : std_logic;
s0, s1 : std_logic_vector(WIDTH-1 downto 0)) is
begin
en <= en_i; load_sel <= ls_i; d_seq <= s0; d_alt <= s1;
wait until rising_edge(clk);
if en_i = '1' then -- en gates, load chooses
if ls_i = '1' then exp := s1; else exp := s0; end if;
end if;
wait for 1 ns;
assert q = exp
report "reg_load mismatch: priority or source wrong" severity error;
end procedure;
begin
rst_n <= '0'; wait until rising_edge(clk); wait for 1 ns; rst_n <= '1';
step('1', '0', x"11", x"22"); -- load d_seq -> 11
step('1', '1', x"33", x"44"); -- load d_alt -> 44
step('0', '0', x"55", x"66"); -- HOLD 44 (en=0 over load_sel)
step('0', '1', x"77", x"88"); -- HOLD 44
step('1', '0', x"99", x"AA"); -- load d_seq -> 99
report "reg_load self-check complete" severity note;
wait;
end process;
end architecture;4c. The gated-clock anti-pattern — buggy vs fixed, in all three HDLs
Here is the signature failure of this page, shown as buggy vs fixed RTL in each language and dramatized narratively in §7. The buggy engineer implements "enable" by ANDing the enable into the clock — @(posedge (clk & en)). It simulates plausibly at RTL, but it is not a synchronous enable at all: it is a gated clock that manufactures glitches, breaks timing, and behaves differently on a real clock tree. The fix is the §4a synchronous enable — the feedback mux on D, with the clock left alone.
// BUGGY: "enable" implemented by ANDing en into the clock. The always block now
// triggers on posedge of (clk & en). If EN changes while CLK is high, the
// AND output produces a SPURIOUS edge -> the flop clocks at the wrong time
// (glitch). It also inserts a gate into the clock path (skew), and on the
// real clock tree it does not behave like the RTL sim. Not synthesizable
// as intended sequential logic.
module reg_en_bad #(parameter int WIDTH = 8)(
input logic clk,
input logic en,
input logic [WIDTH-1:0] d,
output logic [WIDTH-1:0] q
);
logic gclk;
assign gclk = clk & en; // <-- GATED CLOCK: the bug
always_ff @(posedge gclk) // clocks on a glitchy, gated edge
q <= d; // no clean hold; timing- & CDC-hostile
endmodule
// FIXED: gate the DATA, not the clock. The clock is pristine and runs every
// cycle; the enable is a mux on D (feedback hold). Glitch-free, timing-
// clean, tool-friendly.
module reg_en_good #(parameter int WIDTH = 8)(
input logic clk,
input logic en,
input logic [WIDTH-1:0] d,
output logic [WIDTH-1:0] q
);
always_ff @(posedge clk) // ungated clock, every cycle
if (en) q <= d; // else => HOLD: q <= en ? d : q
endmoduleThe self-checking testbench drives the same stimulus into both flavours and shows the divergence: the fixed register holds cleanly on en=0 and updates on en=1; the gated-clock version misbehaves precisely when en toggles while the clock is high — the glitch window. Bind the DUT to reg_en_good to PASS.
module reg_gate_bug_tb;
localparam int WIDTH = 8;
logic clk = 0, en;
logic [WIDTH-1:0] d, q, exp;
int errors = 0;
// Bind to reg_en_good to PASS; to reg_en_bad to observe the gated-clock glitch.
reg_en_good #(.WIDTH(WIDTH)) dut (.clk(clk), .en(en), .d(d), .q(q));
always #5 clk = ~clk;
initial begin
en = 1; d = 8'h10; exp = 8'h10;
@(posedge clk); #1;
// Golden model: a SYNCHRONOUS enable samples en/d at the clk edge only.
repeat (6) begin
d = $urandom; en = $urandom & 1'b1;
// Toggle en mid-high-phase — legal for a real design, lethal to a
// gated clock (spurious edge), harmless to the synchronous enable.
#2 en = ~en; #1 en = ~en;
@(posedge clk);
exp = en ? d : exp; // sampled at the edge
#1;
assert (q === exp)
else begin $error("d=%h en=%b q=%h exp=%h", d, en, q, exp); errors++; end
end
if (errors == 0) $display("PASS: synchronous enable samples cleanly at the edge");
else $display("FAIL: %0d mismatches (gated-clock glitch)", errors);
$finish;
end
endmoduleThe Verilog pair tells the same story with reg/wire typing. assign gclk = clk & en; then always @(posedge gclk) is the bug; always @(posedge clk) if (en) q <= d; is the fix.
// BUGGY: gated clock. posedge (clk & en) glitches when en changes mid-high-phase,
// inserts a gate into the clock path (skew), and diverges on the real tree.
module reg_en_bad #(parameter WIDTH = 8)(
input wire clk,
input wire en,
input wire [WIDTH-1:0] d,
output reg [WIDTH-1:0] q
);
wire gclk = clk & en; // <-- GATED CLOCK: the bug
always @(posedge gclk)
q <= d;
endmodule
// FIXED: synchronous enable — gate the DATA. Clean clock, feedback-mux hold.
module reg_en_good #(parameter WIDTH = 8)(
input wire clk,
input wire en,
input wire [WIDTH-1:0] d,
output reg [WIDTH-1:0] q
);
always @(posedge clk)
if (en) q <= d; // else => hold
endmodule module reg_gate_bug_tb;
parameter WIDTH = 8;
reg clk, en;
reg [WIDTH-1:0] d, exp;
wire [WIDTH-1:0] q;
integer i, errors;
// Bind to reg_en_good to PASS; to reg_en_bad to see the gated-clock glitch.
reg_en_good #(.WIDTH(WIDTH)) dut (.clk(clk), .en(en), .d(d), .q(q));
initial clk = 0;
always #5 clk = ~clk;
initial begin
errors = 0; en = 1'b1; d = 8'h10; exp = 8'h10;
@(posedge clk); #1;
for (i = 0; i < 6; i = i + 1) begin
d = $random; en = $random & 1'b1;
#2 en = ~en; #1 en = ~en; // toggle mid-high-phase
@(posedge clk);
exp = en ? d : exp; // sampled at the edge only
#1;
if (q !== exp) begin
$display("FAIL: d=%h en=%b q=%h exp=%h", d, en, q, exp);
errors = errors + 1;
end
end
if (errors == 0) $display("PASS: synchronous enable samples at the edge");
else $display("FAIL: %0d mismatches (gated-clock glitch)", errors);
$finish;
end
endmoduleIn VHDL the anti-pattern is gclk <= clk and en; with if rising_edge(gclk) — the same manufactured, glitchy edge. The fix keeps rising_edge(clk) and moves the enable to an inner if en = '1' on the data side.
library ieee;
use ieee.std_logic_1164.all;
-- BUGGY: gated clock. rising_edge(clk and en) fabricates a glitchy edge when en
-- changes while clk is high, adds a gate into the clock path (skew), and
-- behaves differently on the physical clock tree.
entity reg_en_bad is
generic ( WIDTH : positive := 8 );
port (
clk : in std_logic;
en : in std_logic;
d : in std_logic_vector(WIDTH-1 downto 0);
q : out std_logic_vector(WIDTH-1 downto 0)
);
end entity;
architecture rtl of reg_en_bad is
signal gclk : std_logic;
begin
gclk <= clk and en; -- <-- GATED CLOCK: the bug
process (gclk)
begin
if rising_edge(gclk) then
q <= d;
end if;
end process;
end architecture;
-- FIXED: synchronous enable — gate the DATA. Clock stays clean; the inner
-- "if en = '1'" (no else) is the feedback-mux hold.
entity reg_en_good is
generic ( WIDTH : positive := 8 );
port (
clk : in std_logic;
en : in std_logic;
d : in std_logic_vector(WIDTH-1 downto 0);
q : out std_logic_vector(WIDTH-1 downto 0)
);
end entity;
architecture rtl of reg_en_good is
begin
process (clk)
begin
if rising_edge(clk) then
if en = '1' then
q <= d; -- else (implicit) => hold
end if;
end if;
end process;
end architecture; library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity reg_gate_bug_tb is
end entity;
architecture sim of reg_gate_bug_tb is
constant WIDTH : positive := 8;
signal clk : std_logic := '0';
signal en : std_logic := '1';
signal d : std_logic_vector(WIDTH-1 downto 0) := x"10";
signal q : std_logic_vector(WIDTH-1 downto 0);
begin
-- Bind to reg_en_good to PASS; to reg_en_bad to observe the gated-clock glitch.
dut : entity work.reg_en_good
generic map (WIDTH => WIDTH)
port map (clk => clk, en => en, d => d, q => q);
clk <= not clk after 5 ns;
stim : process
variable exp : std_logic_vector(WIDTH-1 downto 0) := x"10";
variable seed1, seed2 : positive := 7;
variable r : real;
begin
wait until rising_edge(clk); wait for 1 ns;
for i in 0 to 5 loop
uniform(seed1, seed2, r);
d <= std_logic_vector(to_unsigned(integer(r * 255.0), WIDTH));
en <= '1' when (i mod 2 = 0) else '0';
wait for 2 ns; en <= not en; wait for 1 ns; en <= not en; -- toggle mid-phase
wait until rising_edge(clk);
if en = '1' then exp := d; end if; -- sampled at the edge only
wait for 1 ns;
assert q = exp
report "gated-clock glitch: q /= (en ? d : held)" severity error;
end loop;
report "reg_en synchronous-enable self-check complete" severity note;
wait;
end process;
end architecture;Across all three languages the lesson is one sentence: gate the DATA, not the CLOCK — a synchronous enable is a feedback mux on D, never posedge (clk & en).
5. Verification Strategy
An enabled, loadable register is sequential, so its correctness is not a truth table but a behaviour over clock edges. The single invariant that captures it is:
At every clock edge, Q becomes
en ? source[load_sel] : Q— it takes the selected source when enabled, and holds its previous value when disabled — and the clock is never gated.
Everything below is that invariant made checkable, and it maps directly onto the clocked testbenches in §4.
- Clocked self-checking with a golden model (hold vs update). Generate a clock, and after each edge compare Q against a one-line reference:
expected = en ? source : expected. The: expectedterm is what proves the hold — withen=0, drive several distinct values on D and assert Q is unchanged across multiple edges (the §4a testbenches do exactly this: SVassert (q === exp), Verilogif (q !== exp) $display("FAIL"), VHDLassert q = exp severity error). Sampling both DUT and model in the same clocked region avoids the race between blocking drive and non-blocking update. - Load-source coverage. For the loadable register, the sweep must hit every
load_selvalue withen=1and assert the right source loads (en=1, load_sel=0 → d_seq;en=1, load_sel=1 → d_alt), then hold withen=0and assert Q ignoresload_selentirely — the §4b testbenches encodeexpected = en ? (load_sel ? d_alt : d_seq) : expected. - Priority as a directed check. The enable-over-load priority is a property, so test it directly: set
en=0while togglingload_sel, and assert Q does not move. If the RTL got the priority backwards (load able to sneak a value through while disabled), this is the check that catches it. - Corner cases. Reset while
en=1(reset must win);enasserted for exactly one cycle (single-cycle capture — the config/strobe case); back-to-back updates then a long hold;entoggling within the clock-high phase (the stimulus that distinguishes a synchronous enable from a gated clock — §4c drives this deliberately). - Failure modes the TB must catch. The gated-clock anti-pattern (§7): if "enable" is
posedge (clk & en), the register updates on spurious edges whenenmoves mid-phase and its behaviour diverges from the goldenen ? d : qmodel — the mismatch the §4c testbench surfaces. A missing hold path (no feedback / noelse) shows up as Q changing whenen=0. A load-mux priority error shows up as Q loading while disabled. - Expected waveform. A correct enabled register is easy to read: on cycles where
enis high at the rising edge, Q steps to the selected source on that edge; on cycles whereenis low, Q is a flat, unbroken line across as many edges asenstays low — no matter what D orload_seldo. A Q that changes whileenis low, or that updates on a phantom edge whenentoggles mid-cycle, is the visual signature of the §7 gated-clock bug.
6. Common Mistakes
Gating the clock instead of using a synchronous enable. The signature error: writing always @(posedge (clk & en)) (or rising_edge(clk and en)) to implement "enable." It reads like it should work and even simulates plausibly, but it is a gated clock, not an enable. ANDing a control into the clock creates a glitch whenever EN changes while CLK is high (the AND output produces a spurious edge that clocks the flop at the wrong instant); it inserts a gate delay into the clock path, wrecking skew and timing closure; it fractures the balanced clock tree so static timing can no longer reason about the domain; and it is hostile to CDC analysis and to the standard-cell/tool flow, which expect enables on the data path. The fix is always the same: gate the DATA — if (en) q <= d; with the clock left clean (full post-mortem in §7; buggy-vs-fixed RTL in §4c).
Forgetting the hold path. The enable's meaning lives in what happens when EN is low. In a clocked block, if (en) q <= d; implicitly holds (no assignment runs, the flop keeps its value) — that is the feedback mux and it is correct. But if you (mistakenly) express the enable in a combinational block as if (en) q = d; with no else, you infer a latch on Q, because a combinational block that skips an assignment must remember the old value. And if you write an explicit but wrong hold — else q <= 0; when you meant hold — you clear the register instead of freezing it. Know that the hold path is the whole point, and that in a clocked block the omitted else is the hold; in a combinational block an omitted else is a bug.
Reading the enable combinationally when it must be sampled. The enable is a synchronous control: it takes effect only at the clock edge, sampled together with D. Treating it as if it acts the instant it changes — the mental model behind the gated clock — is what leads to expecting the register to update or freeze mid-cycle. In a correct synchronous enable, EN and D are both sampled at the rising edge and nothing the enable does between edges matters.
Load-mux priority errors. With both an enable and a load, the priority must be explicit and, almost always, enable outermost: a disabled register holds regardless of LOAD_SEL. Getting this backwards — letting the load mux drive a value through while the register is supposed to be disabled — produces a register that updates when it should be frozen. Nest the conditions so the intended priority is unmistakable (if (en) q <= source;, with the source pre-selected by the load mux), and verify it with a directed "disabled while LOAD_SEL toggles" check (§5).
Incomplete load-select assignment. The load mux is a mux, and it obeys 1.1's discipline: every LOAD_SEL value must produce a defined source. A load case with a missing arm and no default re-opens the latch (in a combinational mux) or drops into an undefined source. Use a total conditional or an exhaustive case/when with a default, exactly as for any mux.
7. DebugLab
The register that clocked itself at the wrong time — an enable built by gating the clock
The engineering lesson: an enable gates DATA, not the CLOCK. A synchronous enable is a mux on the D input that feeds Q back to itself when disabled (q <= en ? d : q); the clock stays clean and runs every cycle, and EN is a value sampled at the edge, not a switch that acts the instant it moves. Gating the clock with clk & en manufactures glitches, destroys skew and clock-tree balance, and makes RTL sim lie about real hardware — the tell is a register that works in an isolated block sim but goes intermittently, non-repeatably wrong at full-chip and in silicon, with STA flagging a gated clock. The habit that makes it impossible is universal across SystemVerilog, Verilog, and VHDL: write the enable as if (en) q <= d; on a clean posedge clk, and verify it by toggling EN mid-cycle against a golden edge-sampled model.
8. Interview Q&A
9. Exercises
Design and reasoning exercises — no syntax drills. Each rehearses the "enables gate data, not the clock" habit.
Exercise 1 — Rewrite the gated clock as a synchronous enable
An engineer hands you wire gclk = clk & en; always @(posedge gclk) count <= count + 1; for a counter that should "count only when enabled." (i) Explain the two distinct ways this fails on real hardware (name the glitch mechanism and the timing/clock-tree mechanism). (ii) Rewrite it as a correct synchronous-enable counter and identify, in your rewrite, exactly which line is the feedback mux. (iii) State the one stimulus you would add to a testbench that would have exposed the original bug but that the engineer's clean-EN block sim never hit.
Exercise 2 — Priority you can defend
You are building a program-counter register with a synchronous reset (to the boot vector), an enable (stall when low), and a load mux choosing between pc+4 and a branch target. (i) State the full priority order and justify each level in one line (what real event forces reset above enable? enable above load?). (ii) Write the nested clocked conditional that encodes it. (iii) Describe a directed test that proves the enable-over-load priority specifically — what do you drive, and what must Q do?
Exercise 3 — Hold path forensics
Three engineers write a "register that holds when disabled." A writes a clocked if (en) q <= d;. B writes a combinational always @(*) if (en) q = d; and a separate flop. C writes a clocked if (en) q <= d; else q <= 0;. For each, state precisely what hardware results and whether it correctly holds — name the latch, the accidental clear, and the correct feedback mux, and say which is which. Then explain why the omitted else is correct in A but a bug in B.
Exercise 4 — When would you accept a gated clock?
Your power lead says a large block is burning dynamic power clocking flops whose outputs are unused for long stretches, and asks whether you can "gate the clock." (i) Explain why hand-writing clk & en is still the wrong answer here, and (ii) what mechanism is the right answer and why it is glitch-safe. (iii) Contrast the purpose of that mechanism with the purpose of the synchronous enable you have been writing all along — what problem does each solve?
10. Related Tutorials
Continue in RTL Design Patterns (forward links unlock as they ship):
- Asynchronous Reset — Chapter 2.3; the reset half of a register's control, and where it sits in the reset-enable-load priority stack.
- Shift Registers — Chapter 3.1; a register whose load mux chooses between a parallel word and its own shifted self — the load pattern of this page, specialized.
- Up/Down Counters — Chapter 3.2; an enabled register whose "source" is its own value plus or minus one — enable-to-count and load-to-preset in action.
In-track dependencies (this batch → live):
- The Register Pattern (D-FF) — Chapter 2.1; the plain clocked flop this page adds control to — the flop the enable and load muxes feed.
- Synchronous Reset — Chapter 2.4; the clocked reset that sits at the top of the reset-enable-load priority order sketched here.
Backward / combinational foundations:
- Multiplexers (2:1, N:1, one-hot) — Chapter 1.1; the enable and load are these muxes — a 2:1 for the enable's hold-vs-update, an N:1 for the load source — now feeding a flop.
- Decoders · Encoders · Priority Encoders — Chapter 1; the combinational blocks that often produce an enable or a load-select (a decoded write strobe, a priority-selected source).
- Comparators · Parity & ECC Blocks · Encoding Conversions — Chapter 1; more combinational primitives whose outputs commonly gate or steer a register.
Method:
- The RTL Design Mindset — Chapter 0.2; the Interface → State → Datapath → Control → Verification lenses this page applies (an enabled register is state controlled by a datapath mux).
- What Is an RTL Design Pattern? — Chapter 0.1; why the enabled/loadable register earns "pattern" status — recurring need, reusable form, synthesis reality, signature failure.
Verilog v1 prerequisites (the grammar this pattern transcribes into):
- Blocking and Non-Blocking Assignments — why the enabled flop uses non-blocking (
<=) in its clocked block, and how that differs from the blocking (=) load mux feeding it. - Case Statements — the construct behind a multi-source load mux, and where
defaultand latch inference live. - Race Conditions & Determinism — why the self-checking testbenches sample the golden model in the same clocked region as the DUT, avoiding the drive-vs-update race.
11. Summary
- An enable is a feedback mux on D, not a gated clock. The flop clocks every cycle on a clean, ungated clock; the enable is a 2:1 mux that feeds Q back to itself when disabled —
q <= en ? d : q. In a clocked block,if (en) q <= d;with its implicitelseis that hold. - A load is one more mux, out front. The load mux selects which source becomes the update value (
pc+4vs branch target, parallel vs shifted); the enable mux then decides whether to commit it. Enables and loads are 1.1's muxes composed with 2.1's flop — no new primitive. - Priority is deliberate: enable outermost.
q <= en ? source[load_sel] : q— a disabled register holds regardless ofload_sel; with a synchronous reset added, the order is reset, then enable, then load, written as a nested clocked conditional that reads like the priority. - You gate the DATA, never the CLOCK.
posedge (clk & en)manufactures glitches, wrecks skew and clock-tree balance, and makes RTL sim diverge from silicon — the §7 bug that works in a block sim and fails intermittently at full-chip. Legitimate clock gating for power uses glitch-safe ICG cells inserted by the flow, not hand-writtenclk & en. - Verify it as sequential behaviour. Clock the DUT and, after each edge, assert
Q === en ? source : Qagainst a golden model — hold onen=0across multiple edges, the right source on eachload_sel, and enable-over-load priority — including the mid-cycle EN toggle that distinguishes a synchronous enable from a gated clock. Self-checking clocked testbenches shown here in SystemVerilog, Verilog, and VHDL.