CDC and RDC with Formal
Separate what a structural crossing check proves from what a property proves, model a racing synchroniser bit as a free choice, and see why a two-clock formal model needs that model to find anything.
- Lesson
- F5.6
- Time
- 18 min
- Runs on
- SymbiYosys
- Check
- 4 questions
A FIFO's write pointer crossed into the read domain through two flops, and a refactor replaced its gray converter with the binary count. The structural CDC check still saw a two-flop synchroniser and passed it. In silicon a pointer change caught mid-flight produced a value the writer never held, and the reader ran past the data.
Measured with smtbmc yices at depth 20. The shortest failure is the first count with two changing bits, 1 to 2, not the famous 7 to 8. Sampled at step 3, reported at 4.
Structure Finds the Synchroniser; a Property Checks the Value
Read the diagram as text
- bin counter (wclk): counts writes
- gray register (wclk): bin ^ (bin >> 1), from a flop
- sync1 (rclk): may go metastable when ptr moves near the edge
- sync2 (rclk): settled copy used by the read side
- Structural check: is there a recognised synchroniser? yes
- Formal property: does at most one bit change per source edge?
- bin counter (wclk) → gray register (wclk)
- gray register (wclk) → sync1 (rclk): crosses
- sync1 (rclk) → sync2 (rclk)
- Structural check → sync1 (rclk)
- Formal property → gray register (wclk)
Replace the gray register with the binary count and the structure is unchanged: same two flops, same pass. Only the property sees the difference.
A structural CDC check reads the netlist: every flop in one domain whose input comes from another must pass through a recognised synchroniser. That catches a missing synchroniser. It cannot know whether the value sent through a correct one is safe to send.
That is a property. On the source clock: $countones(ptr ^ $past(ptr)) <= 1. The gray pointer gives BMC PASS to depth 20 and Prove PASS (k = 4); the binary count gives FAIL at reported step 4.
Why One Bit Is the Rule
| Step | Binary | Bits moving | Mixes a racing sample can produce | Gray | Mixes |
|---|---|---|---|---|---|
| 1 to 2 | 0001 to 0010 | 2 | 0000, 0011: neither old nor new | 0001 to 0011 | 0001 or 0011 only |
| 7 to 8 | 0111 to 1000 | 4 | any of 16 values | 0100 to 1100 | 0100 or 1100 only |
With one bit moving, every mix of old and new bits is the old or the new value. That is the whole reason for gray code.
A flop that samples a bit as it changes may settle either way. If several bits move at once, each may settle differently. On this engine a flop never goes metastable, so the lab adds a free input, meta, that chooses old or new for every moving bit (F1.6).
Measured with smtbmc yices at depth 20, binary pointer. Old 1, new 2, caught 3: one bit from each. Reported at 5.
The binary pointer fails, FAIL at reported step 5: the reader sees 3. The gray pointer gives BMC PASS to depth 20 and Prove PASS (k = 4).
Two Real Clocks
Two-clock run (SBY multiclock on) | Result | Time |
|---|---|---|
| Binary pointer, plain flops | BMC PASS to depth 40 | 293 ms |
| Binary pointer, racing bits modelled | FAIL at reported step 5 | 189 ms |
| Gray pointer, racing bits modelled | BMC PASS to depth 40 | 331 ms |
| Gray pointer, racing bits, cover sync2 == 9 | Cover reached, reported step 31 | 323 ms |
Both clocks come from phase accumulators with free, fixed steps; the read clock is at least as fast as the write clock. Run straight from the pinned suite: the site's runner accepts one clock only.
With two clocks and plain flops, the binary pointer passes: in the model every capture is clean. Only with racing bits modelled does it fail, and the gray pointer still passes. The cover shows that run moved data across, so its pass is not empty.
The lesson for any CDC proof: the model decides what can go wrong. Commercial CDC tools add such metastability models or ask for them; check which your flow uses before trusting a pass.
Reset Domain Crossings
The hazard
rst_a asserted (async to clk_b)
-> flop in domain A changes now
-> domain-B flop samples it mid-change
A reset edge is one more asynchronous data change.
The check
structural: which crossings see rst_a?
property: while rst_a is asserted,
the domain-B consumer is held
Same split as CDC: structure first, then a protocol property.
An asynchronous reset in one domain changes the crossing flop's output at a time the other domain did not choose. This engine refuses asynchronous resets, so the page draws the check and leaves the run to a full two-clock flow.
Asked in Interviews
| Question | Answer |
|---|---|
| What does formal CDC check that structural CDC does not? | The protocol on the crossing: one bit per edge for a gray pointer, pulse width for a pulse synchroniser, the request/acknowledge rule for a handshake. |
| Why gray code? | One bit changes per step, so any mix of old and new bits is still the old or the new value. |
| Does a two-clock formal model find metastability bugs? | Not with plain flops: every capture is clean. You add a model in which a changing bit settles either way. |
| What is RDC? | A reset in one domain changing a value that crosses into a running domain: the same hazard, with reset as the cause. |
Try It
The rules in SVA
assert property (@(posedge clk) disable iff (rst) $countones(ptr ^ $past(ptr)) <= 1);
assert property (@(posedge clk) disable iff (rst) sync1 == $past(ptr) || sync1 == $past(ptr, 2));
One rule on the source pointer, one on the first synchroniser flop.
gray_sync as this engine runs it
always @(posedge clk) begin
if ($initstate) begin assume(rst); end else begin assume(!rst); end
end
reg [3:0] ptr_q2=0; // ptr two edges ago
reg live=0;
always @(posedge clk) begin ptr_q2 <= ptr_q; live <= !rst; end
always @(posedge clk) if (!rst && live) assert(sync1 == ptr_q || sync1 == ptr_q2);
$past(ptr, 2) becomes the copy ptr_q2.
prep -top gray_sync.[options]
mode bmc
depth 20
[engines]
smtbmc yices
[script]
read_verilog -formal -sv design.sv
prep -top gray4
[files]
design.svThe two-clock design and its .sby file
module cdc2(input winc, input [3:0] meta);
(* anyconst *) reg [3:0] wstep, rstep; // clock model: two phase accumulators
reg [3:0] wph=0, rph=0;
always @(*) assume(wstep != 0 && wstep <= 4'd8 && rstep >= wstep && rstep <= 4'd8);
always @($global_clock) begin wph <= wph + wstep; rph <= rph + rstep; end
wire wclk = wph[3], rclk = rph[3];
reg [3:0] bin=0, ptr=0; // write domain
wire [3:0] bin_next = bin + winc;
always @(posedge wclk) begin bin <= bin_next; ptr <= bin_next; end
reg [3:0] ptr_g=0, ptr_prev=0; // ptr one global tick ago, and before its last change
always @($global_clock) begin ptr_g <= ptr; if (ptr != ptr_g) ptr_prev <= ptr_g; end
wire [3:0] moving = ptr ^ ptr_g;
reg [3:0] sync1=0, sync2=0; // read domain: two-flop synchroniser
always @(posedge rclk) begin
sync1 <= (ptr & ~moving) | (moving & ((meta & ptr) | (~meta & ptr_g)));
sync2 <= sync1;
end
always @($global_clock) assert(sync1 == ptr || sync1 == ptr_g || sync1 == ptr_prev);
endmodule
sby -f cdc2.sby.[options]
mode bmc
depth 40
multiclock on
[engines]
smtbmc yices
[script]
read_verilog -formal -sv cdc2.sv
prep -top cdc2
[files]
cdc2.sv
Break the converter
Run as it is: FAIL at reported step 4. Which count does the trace stop at, and why not 7 to 8?
Restore gray
Put the converter back: BMC PASS to depth 20. Switch to
mode prove, depth 4: Prove PASS (k = 4).Race the bits
Run gray_sync with the binary pointer: FAIL at reported step 5. Read
metaat the failing step.Race gray bits
Restore the gray pointer in gray_sync: BMC PASS to depth 20.
design.sv
props.sv
Use assert property (@(posedge clk) ...);. Leave blank for immediate assertions in the design.
Inputs are free unless constrained. Run to find a counterexample or check a proof.
Model: one positive-edge clock, binary data. Sampled history requires declaration-initialized variables; primary-input history is unsupported.
SBY summary and log
Check Yourself
Remember
- Structural CDC finds a missing synchroniser; a property checks that the value sent through it is one a synchroniser can carry.
- A multi-bit value crosses safely only if at most one bit changes per source edge: gray code.
- Formal flops never go metastable; to find these bugs, model a changing bit as settling either way.
- RDC is the same hazard with an asynchronous reset as the cause.