Gating a clock with a plain AND gate works until the enable changes while the clock is high, and then it slices a pulse in half. A runt clock corrupts state exactly like a runt mux glitch, but it strikes the block that was trying to save power, so the failure arrives disguised as a functional bug. The integrated clock-gating cell exists to make that slice impossible. This lesson teaches the cell and its timing contract. Its one job is the cell; who drives the enables is next.
The cell is a level-sensitive latch followed by an AND gate; the circuit treatment lives in the RTL course and is recalled here only as far as the integrator uses it. The latch is transparent while the clock is low and closes at the rising edge, so the AND gate only ever sees its enable change while the clock is low. The gated output therefore produces only full-width pulses and clean stretches of low, provided the enable meets its setup and hold to the latch-closing edge: a setup before and a hold after the rising edge, checked by the clock-gating checks in timing analysis (see Clock Gating Checks). An enable that comes from another clock is not an ICG input yet: the latch is not a synchroniser, so it first goes through a two-flop synchroniser in the gated clock's domain. On NanoSoC the CLK_EN bits are written in clk_per and the gates they drive sit in clk_sys, so each enable crosses exactly that way, one branch of crossing X7. Synthesis infers these cells from coded enable style (see Clock Gating and Low Power); the integrator's job is the enables and their synchronisers, not the transistors. NanoSoC has three block-level gates (clk_cpu, clk_dma, clk_sbr) and the two fast-clock root gates opened at step R4; the two CLK_EN bits need one two-flop synchroniser each.
// NanoSoC teaching values: latch-based ICG wrapper. The latch passes en
// only while clk is low, so gclk shows full pulses and clean low stretches.
module nano_icg (
input logic clk,
input logic en,
input logic test_en, // test mode forces the clock on; owned by DFT
output logic gclk
);
logic en_lat;
always_latch begin
if (!clk) en_lat = en | test_en;
end
assign gclk = clk & en_lat;
endmoduleThe module compiles, which proves the syntax, but the no-runt property needs its own proof: drive the enable so it changes inside a high phase, which is the case a plain AND gate fails, and confirm the gated output still never shows a pulse narrower than a full half-period. The model below mirrors the latch and the AND gate sample by sample and runs the plain AND gate beside it, so the test can fail:
# Models of the ICG (latch + AND) and of the forbidden plain AND gate.
# Sample grid: 0,0,1,1 per period, so a full half-period is 2 samples.
def icg_trace(clk, en):
gclk, held = [], 0
for c, e in zip(clk, en):
if c == 0:
held = e # latch transparent while the clock is low
gclk.append(c & held)
return gclk
def and_trace(clk, en):
return [c & e for c, e in zip(clk, en)]
def high_runs(trace):
runs, n = [], 0
for v in trace + [0]:
if v:
n += 1
elif n:
runs.append(n)
n = 0
return runs
clk = [0, 0, 1, 1] * 6
en = [1] * 7 + [0] * 12 + [1] * 5 # changes at index 7 and 19: mid-high
# The enable really does change inside high phases (the hard case).
assert clk[7] == 1 and clk[19] == 1
gclk = icg_trace(clk, en)
bad = and_trace(clk, en)
# ICG: the pulse containing the change is still full width (indices 6,7),
# and the high phase containing the rise (18,19) stays low.
assert gclk[6:8] == [1, 1], gclk[6:8]
assert gclk[18:20] == [0, 0], gclk[18:20]
assert all(r >= 2 for r in high_runs(gclk)), high_runs(gclk)
# Plain AND: the same stimulus gives a runt, so the test discriminates.
assert any(r < 2 for r in high_runs(bad)), high_runs(bad)
print("PASS: enable changes mid-high; ICG full width, plain AND runs short")Every gated clock in the tree passes through a library ICG cell, never a coded AND gate; the lint report proves it.
The enable timing contract is checked per gate, with the test_en override included in the check.
The model above, or its equivalent, runs for enables changing at every phase of the clock.
Toggle each enable at random phases in gate-level simulation and watch the gated clock for runt pulses; a pulse shorter than a full half-period convicts the cell or its wiring. If the failure appears only with test_en asserted, the DFT override path is guilty, not the functional enable. For the gating review discipline see Clock gating done right.
The enable path must meet setup and hold to the rising edge of that clock, where the latch closes. Same-domain generation is the normal case; it still needs the check, because a late enable misses the latch window and the gate opens or closes one cycle later.