A combinational assign clk = sel ? clk_a : clk_b is unsafe. If select changes while source levels differ, the output can create a runt pulse or an extra active edge.
Read the picture from the sel fall at 27 ns, drawn in slot 5. clk_a has a 10 ns period and clk_b a 20 ns period; each slot is 2.5 ns, and sel picks clk_a while high and clk_b while low.
clk_bad follows clk_b the moment sel falls. clk_b is low, so the clk_a pulse that began at 25 ns is cut off after about 2 ns: a runt, one slot wide where a full pulse is two.
clk_good lets the clk_a pulse finish (it is two slots wide), then stays low for 5 ns while neither clock is selected.
clk_good starts the first clk_b pulse only at the clk_b rising edge, so every pulse it delivers is full width.
A zero-delay simulation like this one only shows the logic; real cells add delay, so the approved cell and its timing checks stay the evidence.
// bad-example: never mux clocks as ordinary combinational data
assign clk_bad = select_i ? clk_a_i : clk_b_i;clk_good in the waveform comes from the circuit below. Each source has its own enable, and each enable is held by a latch that is open only while its own clock is low.
Read each row from left to right: the select request, the latch, then the AND with that row's own clock.
A latch can only change while its clock is low, so an enable never cuts a high pulse in half. That is why clk_good finishes the clk_a pulse in the waveform.
Each row also needs the other enable off. B cannot start until en_a has fallen, so both clocks are never passed together.
Between the two, clk_o simply stays low: the 5 ns gap in the waveform.
If the old clock stops, its latch never opens and the switch never completes. That is why the controller below needs a timeout.
In the worked tree, firmware cannot drive the mux directly. A clock controller drains core traffic, requests PLL-to-debug switching, waits for the approved mux wrapper to acknowledge the active source, and only then releases the core.
Block new work and drain the domain.
Request the new source through the clock controller.
Wait for glitch-free mux acknowledgement and source stability.
Re-establish generated-clock/PLL status as required.
Release reset or resume traffic under the new clock.
Requirement | Reason |
|---|---|
Dedicated mux cell | Implements mutually exclusive, low-phase source enables |
Stable select protocol | Select does not chatter during transfer |
Both sources running or documented stop behavior | Cell can observe safe phases |
Domain quiescent | No transaction depends on exact switching latency |
Switch acknowledgement | Controller knows new clock is active |
Reset/CDC review | Control crosses safely into source domains |
The mux protects pulse shape, not transaction latency or timers. A clock pause or stretched interval during switching can violate active protocol assumptions.
This section is an integration draft for the clock controller and the approved mux wrapper. Read it when you own that controller; the rule above is enough for most RTL work.
// snippet: interface draft, not generic mux RTL. Status is synchronous to aon_clk_i.
// The wrapper implements the approved cell's CDC and completion protocol.
clock_mux_wrapper u_clk_mux (
.ctrl_clk_i(aon_clk_i),
.ctrl_rst_ni(aon_rst_ni),
.clk_a_i,
.clk_b_i,
.req_i(switch_req),
.target_b_i(target_b),
.busy_o(switch_busy),
.done_o(switch_done),
.active_valid_o(active_valid),
.active_b_o(active_b),
.fault_o(switch_fault),
.test_enable_i,
.clk_o(domain_clk)
);AON controller/wrapper contract | Required behavior |
|---|---|
Accept | req && !busy at an always-on (AON) edge; controller holds target and request until done or fault; only one outstanding request; wrapper rearms only after observing req low |
Status | busy from acceptance through completion; done is one AON-cycle pulse; active_b meaningful only with active_valid; controller drops req on completion before another acceptance |
Completion | done implies active_valid, active_b == requested target, old source disabled, and target edges observed by the approved status mechanism; echoed select is insufficient |
Failure | fault sticky until explicit recovery; no done on failure; active_valid clears if active source is no longer known healthy |
Reset/test | Initial source/status and test override behavior belong to the cell contract; no functional requests during test override |
The AON controller owns separate bounded drain and switch timers. Illustrative limits are 32 and 64 AON cycles, chosen here only to specify a finite contract; hardware limits must cover approved cell latency, source frequencies and synchronizer delays. On either timeout or wrapper fault, latch an error and keep work blocked and the domain isolated/reset. A stopped source may prevent completion indefinitely, including when stopped high. Timeout does not force a mux select or make a stopped-high clock safe; fallback requires a documented cell recovery protocol.
Cycle | Event | Signals |
|---|---|---|
C0 | Block new work; start 32-cycle drain watchdog | - |
C3 | Quiescent; assert req, target_b=1; start switch watchdog | req=1, target_b=1 |
C4 | Wrapper accepts; hold target_b=1 | busy=1 |
C9 | Completion; stop switch watchdog | done=1, active_valid=1, active_b=1 |
C10+ | Qualify clock/reset release, then resume (never on done alone) | - |
Stopped-source branch | No done through 64 switch cycles: sticky timeout; keep isolated/reset; a late done cannot authorize resume until explicit recovery | no done |
// Controller-side checker draft. Bind to AON-synchronous status only.
// pending/age are controller state; age=0 at acceptance, then saturates.
assert property (@(posedge aon_clk_i) disable iff (!aon_rst_ni)
switch_pending && !switch_done && !switch_fault |=> $stable(target_b))
else $fatal(1, "target changed during switch");
assert property (@(posedge aon_clk_i) disable iff (!aon_rst_ni)
switch_done |-> switch_pending && active_valid && (active_b == target_b))
else $fatal(1, "completion lacks matching active source");
assert property (@(posedge aon_clk_i) disable iff (!aon_rst_ni)
switch_pending && (switch_age == 63) && !switch_done |=> timeout_q)
else $fatal(1, "switch watchdog failed");
assert property (@(posedge aon_clk_i) disable iff (!aon_rst_ni)
timeout_q || switch_fault |-> block_work && isolate_domain && !resume)
else $fatal(1, "clock fault escaped containment");The waveform is a zero-delay simulation: it shows the logic, not real cell delay.
The interface contract and assertion fragment are an unverified integration draft, not an implementation or a pulse-shape proof. Verification still has to cover both switching directions, no-op requests, reset and test, stopped-low and stopped-high sources, timeout and late acknowledgement, and recovery. Cell timing, minimum pulse widths, CDC and RDC, and physical clock signoff are separate.
The 32 and 64 AON-cycle limits are illustrative values chosen to specify a finite contract.