Clock gating saves power. Clock domain crossing saves signal integrity. Combine them and you face a trap that catches experienced designers: the enable signal that controls your ICG cell comes from a different clock domain. Feed it raw and you get a glitched gated clock — the most destructive clock defect in digital design, capable of corrupting every register in the affected domain simultaneously.
An ICG (Integrated Clock Gate) cell is a latch-and-AND circuit that produces a glitch-free gated clock from a raw clock and an enable. The latch captures the enable on the clock-low phase, holding it stable so the AND gate cannot glitch during a clock cycle. This works perfectly when the enable comes from the same clock domain.
The problem: in a multi-clock design, the enable signal often originates in domain A while the ICG lives in domain B. These clocks are independent — no fixed phase relationship. The enable change can arrive at the ICG latch at any time relative to the latch's capture window.
If a metastable or glitching enable reaches the ICG's AND gate during the clock-high phase, the gated clock output spikes — a narrow pulse or a missing pulse. This is a clock glitch: not a setup/hold violation on one register, but a timing corruption event that simultaneously affects every register clocked by that gated clock. The result is system-wide silent data corruption. STA cannot catch it because STA assumes clean clocks.
| Root cause | Effect on gated clock | Severity |
|---|---|---|
| Enable glitch during clock HIGH | Extra narrow pulse on gated clock output | All downstream FFs corrupted |
| Enable metastable at ICG capture | ICG latch output indeterminate | Random corruption pattern |
| Enable de-asserted during clock HIGH | Truncated clock pulse | Hold violations in all FFs |
| Enable stable but async to ICG domain | Subtle phase drift; glitch on PVT corner | Field failures, hard to reproduce |
Before fixing the problem, understand the cell. A standard ICG cell is implemented as a transparent-low latch (captures enable when the clock is LOW, holds it when the clock goes HIGH) plus an AND gate whose inputs are the latched enable and the raw clock.
The latch-and-AND structure is the key to glitch-free operation within a domain: because the latch captured and held the enable during clock-low, the AND gate input is stable by the time the clock goes high. No enable transition occurs during clock-high — so no glitch appears on the gated clock output.
This is the ICG's built-in protection against combinational glitches that originate in the same domain. It does not protect against a cross-domain enable. An enable arriving asynchronously can arrive at any phase, including exactly during the latch's capture-to-hold boundary, creating metastability inside the ICG latch itself.
The ICG latch has its own setup and hold requirements: the enable must be stable before the rising edge of the clock (when the latch closes) and after the falling edge (when the latch opens). An enable coming from another clock domain cannot guarantee either constraint without prior synchronization.
The fundamental conflict: the enable signal lives in domain A, clocked by clk_A. The ICG cell lives in domain B, clocked by clk_B. Domain A decides to enable or disable a clock in domain B — a legitimate power management need — but without synchronization it is a clock integrity violation.
The enable transition in domain A is a CDC signal. Like every CDC signal, it needs a synchronizer before it is used in domain B. The difference here is that it is not used as data — it is used as a clock control input. The consequences of getting it wrong are therefore much worse than a single-bit data error.
Two failure modes to understand clearly:
The solution is straightforward once the problem is understood: place a 2-FF synchronizer in domain B between the source enable and the ICG enable input. The synchronized output is fully domain-B-synchronous by the time it reaches the ICG latch. The ICG latch then operates exactly as designed — with a synchronous, stable input.
The 2-FF synchronizer absorbs the metastability. After two clk_B cycles, the enable signal is domain-B-synchronous. The ICG latch now receives a signal that respects its setup and hold windows, and the gated clock output is guaranteed clean.
One might think the ICG's own latch provides protection — it only captures during clock-low. But the ICG latch is not a synchronizer. It has a narrow capture window and no MTBF guarantee against cross-domain inputs. A metastable input to the latch can propagate as a slowly-resolving metastable voltage on the AND gate input, corrupting the gated clock output timing in ways that STA cannot predict. The 2-FF synchronizer is mandatory; the ICG latch is a second line of defense for same-domain glitches only.
Most designers remember to synchronize enable assertion (0 to 1). Fewer remember to synchronize de-assertion (1 to 0), which is equally dangerous.
Consider: domain A de-asserts enable. Without synchronization, this 1-to-0 transition arrives asynchronously in domain B. If it arrives while clk_B is HIGH, the ICG's AND gate sees enable go low mid-cycle, producing a truncated clock pulse. This is a hold violation on every register that uses the gated clock.
A truncated clock pulse is a hold-time violation for all downstream registers simultaneously. Unlike setup violations, hold violations cannot be fixed by reducing clock frequency — they require physical timing margin that is already committed. The fix is the same: route de-assertion through the 2-FF synchronizer before the ICG. Both edges of the enable must be synchronous to domain B.
The 2-FF synchronizer handles both directions automatically: both the 0-to-1 and 1-to-0 transitions pass through two domain-B flip-flops before reaching the ICG. The ICG latch then sees a clean, synchronous enable for both assertion and de-assertion.
One important subtlety: the 2-FF synchronizer introduces 2 to 3 cycles of latency on enable changes. This is intentional. The power controller in domain A must account for this latency when scheduling enable and disable events. Never attempt to bypass or shorten the synchronizer to reduce latency.
A common design requirement is switching between two independent clocks at runtime — for example, switching from a high-frequency PLL clock to a slow RC oscillator during low-power mode, or switching to a test clock. A naive clock mux feeding two async clocks produces a glitch on every select transition.
The safe approach uses a double-synchronized, glitch-free clock mux:
sel signal in both clock domains independentlyclk0 with the inverse of sel synchronized in domain 0clk1 with sel synchronized in domain 1clk_outThe key property: the switching-off of one clock and switching-on of the other only happens during the low phase of each respective clock, because each gating enable was synchronized in its own domain. The OR output sees no overlap and no glitch.
A practical and critical use of the glitch-free mux is PLL bypass mode. During PLL lock acquisition at chip startup, the SoC cannot use the PLL output. It runs on a slow reference clock. Once the PLL locks, the design must switch to the PLL output clock. An unsafe switch creates a clock glitch that corrupts every register in the affected domain.
The PLL bypass mux uses the same glitch-free architecture, with one additional precaution: the switch must only happen after pll_locked has been stable for several cycles, not immediately on the first lock assertion. PLL lock outputs can pulse briefly before stabilizing. A recommended implementation:
pll_locked in the reference clock domainsel input to the glitch-free muxA clock divider (divide-by-2 implemented as a toggle flip-flop) in domain A produces a clock at half the frequency of clk_A. This divided clock may appear useful as a clock in domain B. But it carries a hidden problem: phase ambiguity.
A divide-by-2 flip-flop toggles on every rising edge of clk_A. Its output has two possible phases depending on when the divider was reset or started. An asynchronous or domain-crossing reset can put the divider into either phase. Domain B, trying to synchronize signals relative to this divided clock, cannot determine which phase is active without additional handshaking.
Two strategies exist:
clk_B. This is strongly preferred and eliminates the problem entirely.Treat a divided clock as a data signal when it needs to cross domains. Do not use it as a clock in the destination domain. Register data on the source divided clock, then synchronize the data signals into the destination domain using the destination's own native clock. This eliminates phase ambiguity entirely and keeps the clock network clean.
Correct SDC constraints are mandatory. Without them, STA may analyze across gated clock paths incorrectly, miss clock gating check violations, or generate false timing paths that confuse sign-off.
Every ICG output that drives registers must have a create_generated_clock constraint. Without it, STA treats the ICG output pin as a combinational signal, not a clock network node.
# Define the master clock in domain B create_clock -name clk_B -period 5.0 [get_ports clk_B] # Define the gated clock derived from the ICG output pin (Q) create_generated_clock \ -name clk_B_gated \ -source [get_ports clk_B] \ -divide_by 1 \ [get_pins u_icg/Q] # Clock gating check: ICG enable must meet setup (0.2 ns) # and hold (0.1 ns) margins relative to clk_B rising edge # (the edge that closes the ICG latch) set_clock_gating_check -setup 0.2 -hold 0.1 [get_cells u_icg]
The raw path from the clk_A source flip-flop to the first stage of the synchronizer has no common clock between source and destination. STA will either report it as unconstrained or error. Mark it with set_false_path or set_max_delay -datapath_only:
# False path from clk_A enable FF to synchronizer FF1 input
set_false_path \
-from [get_clocks clk_A] \
-to [get_pins u_enable_sync/sync_ff1/D]
# Alternative: bound the path with max_delay (tighter control)
# Use a value slightly less than one clk_B period
set_max_delay 4.5 -datapath_only \
-from [get_cells enable_src_ff] \
-to [get_pins u_enable_sync/sync_ff1/D]
# Ensure synthesis does not optimize across synchronizer FFs
set_dont_touch [get_cells {u_enable_sync/sync_ff1 u_enable_sync/sync_ff2}]
The following modules implement the full safe pattern: a reusable 2-FF enable synchronizer feeding an ICG cell wrapper, and a standalone glitch-free clock mux. Both include a testbench that verifies glitch-free behavior with a waveform-based glitch detector.
// ============================================================
// Module: enable_sync_icg
// Purpose: Safely gate a clock when the enable comes from a
// different clock domain.
//
// Architecture:
// en_src (clk_A domain)
// --> 2-FF synchronizer in clk_B domain
// --> synchronized enable (sync_ff2, clk_B synchronous)
// --> ICG cell (transparent-low latch + AND gate)
// --> clk_b_gated (glitch-free gated clock)
//
// Synthesis note: replace the RTL latch+AND model with the
// foundry ICG cell instantiation for actual implementation.
// ============================================================
module enable_sync_icg (
// en_src comes from domain A — it is asynchronous to clk_b.
// Do NOT register it with clk_b upstream of this module.
input wire clk_b, // Destination domain clock
input wire rst_b_n, // Active-low synchronous reset (clk_B domain)
input wire en_src, // Enable from clk_A domain (async to clk_B)
output wire clk_b_gated // Glitch-free gated version of clk_B
);
// ----------------------------------------------------------
// 2-FF Synchronizer: absorbs metastability from en_src.
// Both FFs are clocked by clk_B.
// After sync_ff2, the enable is fully clk_B-synchronous.
//
// ASYNC_REG attribute: informs tools this is a synchronizer;
// prevents optimization across the two FFs; improves MTBF.
// ----------------------------------------------------------
(* ASYNC_REG = "TRUE" *) reg sync_ff1;
(* ASYNC_REG = "TRUE" *) reg sync_ff2;
always @(posedge clk_b or negedge rst_b_n) begin
if (!rst_b_n) begin
sync_ff1 <= 1'b0;
sync_ff2 <= 1'b0;
end else begin
sync_ff1 <= en_src; // May carry metastability for 1 cycle
sync_ff2 <= sync_ff1; // Resolved; safe to use downstream
end
end
// ----------------------------------------------------------
// ICG: RTL model of transparent-low latch + AND gate.
//
// Behavior:
// When clk_B = 0: latch is transparent, latched_en = sync_ff2
// When clk_B = 1: latch holds latched_en (no change)
// Output: clk_b_gated = clk_B & latched_en
//
// The enable can only change while clk_B = 0 (latch open).
// When clk_B rises and the AND gate output goes high, the
// enable input is already stable (latch closed). No glitch.
//
// For synthesis, replace with foundry cell, e.g.:
// CKLNQD1 u_icg (.CP(clk_b), .E(sync_ff2), .Q(clk_b_gated));
// ----------------------------------------------------------
reg latched_en;
// Transparent-low latch (combinational model for simulation)
always @(*) begin
if (!clk_b)
latched_en = sync_ff2;
// else: latched_en holds (inferred latch in RTL simulation)
end
assign clk_b_gated = clk_b & latched_en;
endmodule
// ============================================================
// Module: glitch_free_clk_mux
// Purpose: Switch between two unrelated clocks without glitches.
//
// sel = 0 => output is clk0
// sel = 1 => output is clk1
//
// Algorithm:
// 1. Synchronize sel in clk0 domain (2-FF, inverted feedback).
// 2. Synchronize sel in clk1 domain (2-FF, inverted feedback).
// 3. Gate clk0 with NOT(sel_sync_clk0) using ICG-style latch.
// 4. Gate clk1 with sel_sync_clk1 using ICG-style latch.
// 5. OR the gated outputs.
//
// The cross-feedback (each chain sees the other chain's output)
// ensures mutual exclusion: both chains cannot be high at once,
// preventing both clocks from driving the OR simultaneously.
// ============================================================
module glitch_free_clk_mux (
input wire clk0, // Clock source 0 (e.g., reference / bypass)
input wire clk1, // Clock source 1 (e.g., PLL output)
input wire rst_n, // Async active-low reset
input wire sel, // 0 = select clk0, 1 = select clk1
output wire clk_out // Glitch-free muxed output clock
);
// ----------------------------------------------------------
// Synchronized select in clk0 domain.
// When sel = 1 (switch to clk1), we want to DISABLE clk0.
// Feedback from sel_sync1_b prevents race during transition.
// ----------------------------------------------------------
(* ASYNC_REG = "TRUE" *) reg sel_sync0_a, sel_sync0_b;
always @(posedge clk0 or negedge rst_n) begin
if (!rst_n) begin
sel_sync0_a <= 1'b0;
sel_sync0_b <= 1'b0;
end else begin
// Only allow assertion of clk0 disable if clk1 is
// not yet providing a clock (mutual exclusion guard)
sel_sync0_a <= sel & (~sel_sync1_b);
sel_sync0_b <= sel_sync0_a;
end
end
// ----------------------------------------------------------
// Synchronized select in clk1 domain.
// When sel = 1 (switch to clk1), we want to ENABLE clk1.
// Feedback from sel_sync0_b prevents race during transition.
// ----------------------------------------------------------
(* ASYNC_REG = "TRUE" *) reg sel_sync1_a, sel_sync1_b;
always @(posedge clk1 or negedge rst_n) begin
if (!rst_n) begin
sel_sync1_a <= 1'b0;
sel_sync1_b <= 1'b0;
end else begin
// Only allow assertion of clk1 enable once clk0
// gate is confirmed disabled (mutual exclusion guard)
sel_sync1_a <= sel & (~sel_sync0_b);
sel_sync1_b <= sel_sync1_a;
end
end
// ----------------------------------------------------------
// ICG-style latch gating for each clock.
// Latches capture on the LOW phase so gating changes only
// take effect on the next rising edge — no mid-cycle glitch.
// ----------------------------------------------------------
reg latch_en0, latch_en1;
// clk0 gate: active when sel_sync0_b = 0 (clk0 is selected)
always @(*) begin
if (!clk0) latch_en0 = ~sel_sync0_b;
end
// clk1 gate: active when sel_sync1_b = 1 (clk1 is selected)
always @(*) begin
if (!clk1) latch_en1 = sel_sync1_b;
end
wire clk0_gated = clk0 & latch_en0;
wire clk1_gated = clk1 & latch_en1;
// OR: at most one is active at any time; no glitch possible
assign clk_out = clk0_gated | clk1_gated;
endmodule
// ============================================================
// Testbench: tb_enable_sync_icg
// Verifies: glitch-free enable synchronization into ICG.
// Method: event-driven glitch detector on clk_b_gated.
// Any pulse narrower than 1 ns = glitch = FAIL.
// ============================================================
`timescale 1ns/1ps
module tb_enable_sync_icg;
reg clk_a, clk_b, rst_b_n, en_src;
wire clk_b_gated;
enable_sync_icg dut (
.clk_b (clk_b),
.rst_b_n (rst_b_n),
.en_src (en_src),
.clk_b_gated (clk_b_gated)
);
// clk_A: 7 ns period (143 MHz) — intentionally unrelated to clk_B
initial clk_a = 0;
always #3.5 clk_a = ~clk_a;
// clk_B: 5 ns period (200 MHz)
initial clk_b = 0;
always #2.5 clk_b = ~clk_b;
// ----------------------------------------------------------
// Glitch detector: monitors clk_b_gated for narrow pulses.
// A transition pair closer than 1 ns apart is a glitch.
// ----------------------------------------------------------
real last_edge_time;
real edge_gap;
integer glitch_count;
initial begin
glitch_count = 0;
last_edge_time = 0.0;
end
always @(clk_b_gated) begin
edge_gap = $realtime - last_edge_time;
if (last_edge_time > 0.0 && edge_gap < 1.0) begin
$display("ERROR: GLITCH at %0t ns (pulse width = %.3f ns)",
$realtime, edge_gap);
glitch_count = glitch_count + 1;
end
last_edge_time = $realtime;
end
// ----------------------------------------------------------
// Stimulus: assert and de-assert enable asynchronously
// (offset from any clk_B edge to stress the synchronizer)
// ----------------------------------------------------------
initial begin
rst_b_n = 0;
en_src = 0;
#20;
rst_b_n = 1;
#10;
// Assert enable: 3.7 ns offset — lands mid-clk_B cycle
#3.7;
en_src = 1;
$display("[%0t ns] en_src asserted (async to clk_B)", $realtime);
// Wait long enough for 2-FF latency + ICG propagation
#35;
$display("[%0t ns] clk_b_gated observed as %b (expect toggling)",
$realtime, clk_b_gated);
// De-assert enable: 7.3 ns offset — lands mid-clk_B cycle
#7.3;
en_src = 0;
$display("[%0t ns] en_src de-asserted (async to clk_B)", $realtime);
#35;
$display("[%0t ns] clk_b_gated = %b (expect 0)", $realtime, clk_b_gated);
// Rapid toggle stress test: toggle en_src every 1.1 ns
// (sub-cycle period, maximally stresses the synchronizer)
repeat (10) begin
#1.1 en_src = ~en_src;
end
en_src = 0;
#30;
// Report
if (glitch_count == 0)
$display("PASS: No glitches detected on clk_b_gated.");
else
$display("FAIL: %0d glitch(es) detected on clk_b_gated!", glitch_count);
$finish;
end
initial begin
$dumpfile("tb_enable_sync_icg.vcd");
$dumpvars(0, tb_enable_sync_icg);
end
endmodule
pll_locked assertion; filter for several stable cycles before changing sel to prevent switching on a false lock pulsecreate_generated_clock on the ICG output pin, set_clock_gating_check for setup/hold margins, set_false_path or set_max_delay -datapath_only on the raw synchronizer inputWhen the enable signal controlling an ICG cell comes from a different clock domain, it arrives asynchronously relative to the ICG latch's capture window. A metastable or glitching enable causes the ICG to produce a glitched gated clock output, corrupting every register that uses that clock simultaneously. The fix is to synchronize the enable in the destination domain using a 2-FF synchronizer before the ICG.
Place a 2-flip-flop synchronizer in the destination clock domain between the source enable and the ICG enable input. After two destination-domain clock cycles, the enable is fully synchronous. The ICG latch then receives a stable, synchronous enable and produces a guaranteed glitch-free gated clock. This applies equally to enable assertion and de-assertion.
Use a glitch-free clock mux: double-synchronize the select signal in each clock domain separately using 2-FF chains with cross-feedback for mutual exclusion, AND-gate each clock with its domain-local synchronized select (inverted for the clock-0 side), then OR the two gated outputs. Switching only occurs during the low phase of each clock, preventing any glitch on the mux output.