HomeCDC GuideDay 9
DAY 9 · ADVANCED CDC PATTERNS

Clock Gating Across Domains

By EcrioniX · Updated Jun 23, 2026

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.

1. Why CDC + Clock Gating Is Dangerous

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.

The clock glitch scenario

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 causeEffect on gated clockSeverity
Enable glitch during clock HIGHExtra narrow pulse on gated clock outputAll downstream FFs corrupted
Enable metastable at ICG captureICG latch output indeterminateRandom corruption pattern
Enable de-asserted during clock HIGHTruncated clock pulseHold violations in all FFs
Enable stable but async to ICG domainSubtle phase drift; glitch on PVT cornerField failures, hard to reproduce
Unsafe: Enable From Domain A Fed Directly To ICG in Domain B DOMAIN A (clk_A) enable_src FF ASYNC! DANGER ICG CELL (latch + AND) clk_B GLITCHED CLK DOMAIN B (clk_B) All FFs in domain B CORRUPTED
Figure 1 — Feeding an enable from domain A directly to an ICG in domain B creates an async path into the clock network. Clock glitches corrupt every register in the gated domain.

2. ICG Cell Review — Latch + AND

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.

ICG cell timing requirements

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.

3. The Enable Synchronization Problem

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:

4. Safe Pattern: Synchronize Enable BEFORE the ICG

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.

Safe Pattern: 2-FF Synchronizer Before ICG DOMAIN A (clk_A) enable_src CDC FF1 sync stage 1 FF2 sync stage 2 stable enable ICG CELL latch + AND GLITCH-FREE clk_B clk_gated DOMAIN B (clk_B)
Figure 2 — Correct CDC + clock gating: 2-FF synchronizer in domain B stabilizes the enable before reaching the ICG cell. Gated clock output is always glitch-free.

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.

Why the ICG latch alone is not enough

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.

5. Enable De-assertion Synchronization

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.

De-assertion is as dangerous as assertion

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.

6. Synchronous Clock Switching

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:

  1. Double-synchronize the sel signal in both clock domains independently
  2. In domain 0: gate clk0 with the inverse of sel synchronized in domain 0
  3. In domain 1: gate clk1 with sel synchronized in domain 1
  4. OR the two gated clocks to form clk_out

The 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.

Glitch-Free Clock Mux Architecture sel clk0 FF1,2 (clk0) 2-stage sync AND (clk0) clk1 FF1,2 (clk1) 2-stage sync AND (clk1) OR clk_out (glitch-free) Each sync chain uses the clock of its own domain. OR combines them when exactly one is active.
Figure 3 — Glitch-free clock mux: 2-FF sync in each domain, AND-gate each clock with its domain-local synchronized select (inverted for clk0 side), OR the results.

7. Clock Mux for PLL Bypass

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:

  1. Double-synchronize pll_locked in the reference clock domain
  2. Run the synchronized lock signal through a 4-cycle filter counter — only assert the switch enable after 4 consecutive high values
  3. Use this filtered signal as the sel input to the glitch-free mux
  4. Monitor: after switching to the PLL clock, verify the PLL clock is toggling before releasing the reference clock gate entirely

8. Generated Clocks in CDC — The Phase Ambiguity Problem

A 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:

Best practice for divided clocks in CDC

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.

9. UPF/SDC Constraints for Gated Clocks

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.

create_generated_clock on ICG output

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.

sdc — gated clock definition and gating check
# 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]

False path on the cross-domain enable input

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:

sdc — synchronizer path constraints
# 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}]

10. Complete Verilog Implementation

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.

enable_sync_icg.sv — safe enable synchronizer + ICG wrapper
// ============================================================
// 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
glitch_free_clk_mux.sv — safe runtime clock switching
// ============================================================
// 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
tb_enable_sync_icg.sv — testbench with glitch detector
// ============================================================
// 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

11. Key Takeaways

Day 9 — Clock Gating Across Domains

FAQ

Why is clock gating dangerous across clock domain boundaries?

When 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.

What is the safe pattern for enable synchronization before an 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.

How do you safely switch between two unrelated clocks?

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.

Previous
← Day 8: Arbiters & Control Data

← Full course roadmap