Simulation finds bugs that were triggered. Formal CDC verification proves that no bug can exist — for every possible input sequence, every synchronizer correctly handles the crossing. This lesson covers the complete flow: static structural analysis with SpyGlass CDC, mathematical proof with JasperGold, SVA properties for CDC correctness, and the sign-off process that closes a design for tape-out.
The two verification methods answer different questions and are used in sequence, not as alternatives.
Static CDC analysis is structural. It parses the RTL, identifies which clock drives each register, builds a clock domain graph, and then checks every signal that crosses a domain boundary. It answers: "Is there a synchronizer here? Is it the right type? Does the fanout create reconvergence?" Static tools are fast — they scale to hundreds of millions of gates — and they flag missing synchronizers within minutes of running.
Formal CDC verification is mathematical. It takes the synchronizer structure that static analysis found and proves — for all possible input sequences — that the synchronizer correctly isolates the two domains. It uses model checking: exploring the full state space of the synchronizer circuit and proving that no input sequence can cause a CDC violation. If it finds a sequence that violates the property, it produces a counterexample (CEX) — a concrete waveform showing exactly what must happen to trigger the bug.
Static analysis tells you where the crossing points are and whether synchronizers are structurally present. Formal analysis tells you whether those synchronizers actually work correctly for every possible sequence of events. You need both. Static first to clean up structural issues cheaply; formal second to get mathematical proof.
A static CDC tool performs the following steps automatically:
| Check Type | SpyGlass Rule | What it catches |
|---|---|---|
| Unsynchronized crossing | Ac_unsync01 | Signal crosses domain boundary with no synchronizer at all |
| Reconvergence fanout | Ac_conv01 | Synchronized output fans out and reconverges with source-domain logic |
| Clock glitch | Ac_glitch01 | Combinational logic in the clock path creates glitch hazards |
| Reset domain crossing | Ac_reset01 | Reset signal crosses domain without reset synchronizer |
| Multi-bit crossing | Ac_unsync04 | Multi-bit bus crosses domain — must use FIFO or gray code, not simple 2-FF |
| Incomplete handshake | Ac_handshake01 | Handshake protocol missing ack return path or acknowledge timing is wrong |
Reconvergence fanout is the most commonly missed CDC issue in static analysis. It is easy to construct a design where every crossing has a synchronizer, all synchronizers are correct, and yet the design still has a CDC bug.
Here is what happens: Signal sig_a originates in domain A. It crosses to domain B through a 2-FF synchronizer producing sig_b_sync. Good so far. But the designer then uses sig_a — the original, unsynchronized version — in combinational logic inside domain B, and the output of that combinational logic converges with sig_b_sync at a register in domain B.
At the convergence point, there are now two versions of the same signal: one delayed by one or two destination clock cycles (the synchronized copy) and one that may have already changed again (the source domain copy). The logic at the convergence point sees inconsistent values and produces corrupted output — even though every individual crossing had a synchronizer.
SpyGlass is Synopsys's industry-standard static CDC tool. The typical flow has four phases: read, compile, run goals, review results. The Tcl script below shows a complete production-ready setup.
## SpyGlass CDC Setup Script
## Phase 1: Read design files
read_file -type verilog {
rtl/top.sv
rtl/sync_2ff.sv
rtl/cdc_fifo.sv
rtl/clk_a_domain.sv
rtl/clk_b_domain.sv
}
## Phase 2: Specify clocks and frequencies
set_option clock_domain {clk_a 200} ;# 200 MHz
set_option clock_domain {clk_b 125} ;# 125 MHz
set_option clock_domain {clk_ref 50} ;# 50 MHz
## Specify asynchronous clock relationships
set_option async_clk_pairs {{clk_a clk_b} {clk_a clk_ref}}
## Phase 3: Set synchronizer recognition
## Tell SpyGlass what your 2-FF synchronizer cell is called
set_option sync_cell {sync_2ff}
set_option sync_depth 2
## Phase 4: Compile design
compile_design -top top_module
## Phase 5: Run CDC goal
current_goal {CDC/cdc_setup}
run_goal
## Run detailed analysis goals
current_goal {CDC/cdc_verify}
run_goal
## Generate reports
report_cdc -file cdc_violations.rpt -type violations
report_cdc -file cdc_waivers.rpt -type waivers
report_cdc -file cdc_summary.rpt -type summary
After running, SpyGlass produces a violation list. Each entry shows the crossing signal, the source domain, the destination domain, the rule that fired, and a severity level (error, warning, info). Engineers must disposition each violation — either fix it (add or correct the synchronizer) or waive it with documented justification.
## SpyGlass CDC Waiver File
## Format: waive -rule RULE_NAME -signal SIGNAL_PATH -comment "justification"
## Waiver 1: False path -- signal is stable when sampled
## SDC contains set_false_path from clk_a domain
## to cfg_mode_b register; only changes during reset
waive -rule Ac_unsync01 \
-signal {top.u_ctrl.cfg_mode} \
-comment "False path per SDC: cfg_mode only changes \
during global reset; guaranteed stable \
before clk_b domain samples it"
## Waiver 2: Gray-code counter -- tool cannot prove 1-bit
## change statically; our encoding guarantees it
waive -rule Ac_unsync04 \
-signal {top.u_fifo.wr_ptr[3:0]} \
-comment "Gray-code encoded write pointer; \
only 1 bit changes per cycle by design; \
verified by formal proof in jasper run 2026-06-23"
## Waiver 3: Reset synchronizer topology not recognized
waive -rule Ac_reset01 \
-signal {top.u_rstsync.rst_b_sync} \
-comment "Reset synchronizer at u_rstsync uses \
async assert / sync deassert topology; \
SpyGlass does not recognize this cell; \
cell reviewed and approved by CDC lead"
JasperGold's CDC App automates the formal verification of synchronizer correctness. It does not require you to write properties manually for straightforward synchronizers — the tool auto-extracts prove targets. You only write manual SVA for protocol-level properties (handshake ordering, gray-code monotonicity, FIFO pointer ordering).
The CDC App works in three steps:
## JasperGold CDC App Tcl Script
## Step 1: Elaborate design
analyze -sv09 {
rtl/top.sv
rtl/sync_2ff.sv
rtl/cdc_fifo.sv
rtl/clk_a_domain.sv
rtl/clk_b_domain.sv
}
elaborate -top top_module
## Step 2: Define clocks
clock clk_a -period 5 ;# 200 MHz (5 ns period)
clock clk_b -period 8 ;# 125 MHz (8 ns period)
clock clk_ref -period 20 ;# 50 MHz (20 ns period)
## Step 3: Define resets
reset -expression {!rst_n}
## Step 4: Load CDC App -- auto-generates prove targets
cdc run
## Step 5: Review CDC app results
cdc report -severity {error warning} \
-file cdc_formal_report.rpt
## Step 6: Add manual assume/guarantee for protocol properties
## Constrain the source domain input behavior
assume -name A_REQ_STABLE {
@(posedge clk_a) $stable(req) |-> ##[1:2] $stable(req)
}
## Prove handshake property on destination side
prove -name P_ACK_AFTER_REQ {
@(posedge clk_b) req_sync |-> ##[1:4] ack
}
## Step 7: Run formal engine on all targets
prove -all
## Step 8: Generate closure report
cdc report -type closure -file cdc_closure.rpt
Formal CDC at the full-chip level can be computationally intractable because the state space of two fully independent clock domains is enormous. The industry solution is assume-guarantee (AG) decomposition — splitting the proof into sub-problems that each run on a manageable piece of the design.
The key idea: when proving domain B's synchronizer, you do not need to model the full behavior of domain A. Instead, you write an assumption that constrains what domain A inputs look like at the domain boundary. Then you prove that if those assumptions hold, domain B's synchronizer output is always valid.
To compose proofs hierarchically:
// Assume-Guarantee CDC Properties
// File: cdc_ag_properties.sv
module cdc_ag_props (
input logic clk_a, clk_b, rst_n,
input logic sig_a, // source domain signal
input logic sig_b_sync, // synchronized output in domain B
input logic sig_b_sync_q // one cycle delayed in domain B
);
// -------------------------------------------------------
// ASSUMPTIONS -- constrain domain A behavior at boundary
// -------------------------------------------------------
// A1: sig_a must remain stable for at least 3 clk_a cycles
// before any transition (meets 2-FF synchronizer setup)
property A_SIG_STABILITY;
@(posedge clk_a) disable iff (!rst_n)
$changed(sig_a) |-> $past($stable(sig_a), 3);
endproperty
assume property (A_SIG_STABILITY);
// A2: No glitches -- sig_a cannot pulse for less than 1 cycle
property A_NO_GLITCH;
@(posedge clk_a) disable iff (!rst_n)
$rose(sig_a) |-> sig_a ##1 sig_a;
endproperty
assume property (A_NO_GLITCH);
// -------------------------------------------------------
// GUARANTEES -- prove domain B synchronizer correctness
// -------------------------------------------------------
// G1: Synchronizer output must never be X
property G_SYNC_STABILITY;
@(posedge clk_b) disable iff (!rst_n)
!$isunknown(sig_b_sync);
endproperty
prove_property: assert property (G_SYNC_STABILITY);
// G2: Synchronizer output must eventually track source
// (liveness -- no permanent de-sync)
property G_SYNC_LIVENESS;
@(posedge clk_b) disable iff (!rst_n)
$rose(sig_a) |-> ##[2:6] sig_b_sync;
endproperty
prove_liveness: assert property (G_SYNC_LIVENESS);
// G3: Safety -- synchronized output never contradicts
// source while source is in steady state
property G_NO_CONTRADICTION;
@(posedge clk_b) disable iff (!rst_n)
($stable(sig_a) && !sig_a) |-> ##[0:4] !sig_b_sync;
endproperty
prove_safety: assert property (G_NO_CONTRADICTION);
endmodule
Beyond the auto-generated synchronizer proofs, CDC sign-off requires manually written SVA properties for protocol-level guarantees. The four most critical categories are:
| Property Category | What it proves | Where it applies |
|---|---|---|
| Synchronizer stability | Output is never X; eventually tracks input; latency bounded | Every 2-FF, pulse synchronizer |
| No simultaneous req+ack | req and ack cannot assert in the same cycle (handshake ordering) | Req-ack handshake pairs |
| Gray-code monotonicity | Only 1 bit changes per source clock cycle in the gray-coded counter | Async FIFO pointer crossings |
| FIFO pointer ordering | Write pointer never equals read pointer when FIFO is not empty; no overflow | All async FIFOs |
// Async FIFO CDC SVA Properties
module fifo_cdc_props #(parameter DEPTH = 8, AW = 3) (
input logic clk_wr, clk_rd, rst_n,
input logic [AW:0] wr_ptr_gray, rd_ptr_gray,
input logic [AW:0] wr_ptr_bin, rd_ptr_bin,
input logic wr_en, rd_en,
input logic full, empty
);
// Gray-code monotonicity -- exactly 1 bit changes per write clock
property P_GRAY_MONOTONE;
@(posedge clk_wr) disable iff (!rst_n)
$changed(wr_ptr_gray) |->
$onehot(wr_ptr_gray ^ $past(wr_ptr_gray));
endproperty
gray_monotone: assert property (P_GRAY_MONOTONE);
// Full flag: write pointer has lapped read pointer by DEPTH
property P_FULL_CORRECT;
@(posedge clk_wr) disable iff (!rst_n)
full == (wr_ptr_bin - rd_ptr_bin == DEPTH);
endproperty
full_correct: assert property (P_FULL_CORRECT);
// Empty flag: pointers are equal
property P_EMPTY_CORRECT;
@(posedge clk_rd) disable iff (!rst_n)
empty == (wr_ptr_bin == rd_ptr_bin);
endproperty
empty_correct: assert property (P_EMPTY_CORRECT);
// No write when full (no overflow)
property P_NO_OVERFLOW;
@(posedge clk_wr) disable iff (!rst_n)
full |-> !wr_en;
endproperty
no_overflow: assert property (P_NO_OVERFLOW);
// No read when empty (no underflow)
property P_NO_UNDERFLOW;
@(posedge clk_rd) disable iff (!rst_n)
empty |-> !rd_en;
endproperty
no_underflow: assert property (P_NO_UNDERFLOW);
endmodule
Static CDC tools produce false positives — violations that are structurally flagged but are not real bugs. Understanding when a waiver is legitimate versus when it is hiding a real problem is a critical CDC engineering skill.
Legitimate waiver categories:
set_false_path, and guaranteed stable when any destination clock samples it. The tool cannot read SDC semantics so it flags the crossing even though it is safe by design intent.Every waiver is a risk. If you waive a real bug as a false positive, that bug ships in silicon. Each waiver must have: (1) a written justification referencing design intent or SDC constraints; (2) approval by a second engineer; (3) a formal proof or simulation coverage trace confirming the path is safe. Never batch-waive violations without reviewing each one individually.
CDC sign-off is a formal gate in the tape-out checklist. The closure process has six steps that must be completed in order:
Total crossings found · Errors fixed (with commit ID) · Waivers approved (with justification and approver name) · Formal proofs run (with tool version, date, and result) · Open items with owner and closure date. Any open item at tape-out freeze is a risk accepted by the chip architect in writing.
Siemens Questa CDC (formerly part of ModelSim) provides a similar structural analysis capability to SpyGlass with different rule naming and a GUI-focused waiver manager. Both tools share the same fundamental approach; knowing one transfers directly to the other.
| Feature | SpyGlass CDC | Questa CDC |
|---|---|---|
| Vendor | Synopsys | Siemens EDA |
| Unsync crossing rule | Ac_unsync01 | cdc_violation_unsynchronized |
| Reconvergence rule | Ac_conv01 | cdc_violation_reconvergence |
| Waiver format | .awl file (Tcl-based) | .do file / GUI waiver manager |
| Report format | HTML + ASCII .rpt | HTML dashboard + CSV export |
| Formal integration | Links to VC Formal / JasperGold | Links to Questa Formal |
| Common in | ARM, Qualcomm, Intel flows | Automotive and safety-critical flows |
## Questa CDC Run Script
## Read design
vlog -sv rtl/top.sv rtl/sync_2ff.sv rtl/cdc_fifo.sv
## Define CDC intent
cdc run -d top_module \
-clock clk_a -period 5 \
-clock clk_b -period 8 \
-async_pairs {{clk_a clk_b}} \
-output_dir questa_cdc_results
## Load synchronizer recognition library
cdc prefer_sync_cell -cell sync_2ff -depth 2
## Run full CDC analysis
cdc analyze
## Generate reports
cdc report violations -file questa_violations.rpt
cdc report waivers -file questa_waivers.rpt
cdc report summary -file questa_summary.rpt
## Launch interactive GUI for waiver management
## questa_cdc_gui questa_cdc_results/cdc_db
Ac_unsync01 (missing sync), Ac_conv01 (reconvergence), Ac_glitch01 (clock glitch), Ac_unsync04 (multi-bit bus)Static CDC analysis parses RTL structurally to find crossing points and check synchronizer presence — fast but pattern-matching only. Formal CDC uses model checking to mathematically prove synchronizers handle every possible input sequence, generating a counterexample waveform if any sequence causes a failure. Static runs first to clean up structural issues; formal provides the mathematical guarantee needed for tape-out sign-off.
Reconvergence occurs when a synchronized signal fans out to logic in the destination domain AND also reconverges with unsynchronized logic still derived from the source domain. At the convergence point two versions of the signal exist simultaneously — one delayed by destination clock cycles, one that may have already changed again. This causes corrupted output even though each individual crossing had a synchronizer. SpyGlass flags it as Ac_conv01.
A waiver is appropriate when the tool flags a structural pattern that cannot actually cause metastability in context: paths constrained as set_false_path in SDC with guaranteed signal stability; unrecognized custom synchronizer topologies that are correct by design; gray-code counters that formal proof confirms only change 1 bit per cycle; or constant signals. Every waiver requires a written justification, approval by a second engineer, and either a formal proof or simulation coverage trace confirming safety. Never batch-waive.