Hello! Welcome to your seventh lesson.
In our last session, we mastered writing safety properties to catch spurious events and illegal state transitions within a single clock domain. We learned to use !, $stable, and $isunknown to ensure our design doesn't do something wrong.
Today, we address one of the most critical and error-prone areas in hardware design: Clock Domain Crossing (CDC). Verifying logic that spans asynchronous clock domains is a non-trivial challenge where formal verification excels. Your experience in microelectronics means you're well aware of the physical-layer realities of metastability. In this lesson, we'll focus on the logical layer: how to write correct and robust SystemVerilog Assertions to prove data integrity and protocol correctness across these clock boundaries. Mastering this is a must for any senior FV role.
Our goal is to implement multi-clock domain assertions with correct clock resolution semantics, understanding both the syntax and the subtle "gotchas" that can lead to flawed checks.
1. The Challenge of Multi-Clock Verification
When a signal generated by clk_A is sampled by logic running on an asynchronous clk_B, we can't use a simple single-clock assertion. The assertion's clock must "jump" from clk_A to clk_B to follow the data flow.
SVA provides specific mechanisms for this, but they must be used with a precise understanding of their timing semantics. A seemingly correct property can pass in simulation while hiding a real CDC bug, simply because the assertion's sampling is misaligned with the design's actual behavior.
2. Core SVA Constructs for Multi-Clock Properties
To build multi-clock assertions, we need to understand how SVA handles clock changes. The key lies in the interaction between an antecedent on one clock and a consequent on another.
Multiclocked Sequences and Properties
The 'SystemVerilog Assertions Handbook' provides the definitive rules for this. Let's study the foundational principles of how multiclocked sequences and properties are constructed.
Read the section titled 'Multiclocked Sequences and Properties', focusing on the first two rules about concatenation and implication operators. Pay close attention to the descriptions of ##1, ##0, |=>, and |-> in a multi-clock context.
Based on that reading, here are the fundamental takeaways:
- Clock Transition: You switch clock domains by explicitly specifying a new clocking event, like
... ##1 @(posedge clk_B) .... - Concatenation Operators: To connect sequences on different clocks, you can only use
##1or##0.s1 ##1 @(posedge clk_B) s2: The start ofs2is evaluated at the first strictly subsequent rising edge ofclk_Bafters1completes.s1 ##0 @(posedge clk_B) s2: The start ofs2is evaluated at the nearest, possibly overlapping rising edge ofclk_Bafters1completes.
- Implication Operators: The implication operators provide the synchronization glue.
antecedent |=> @(posedge clk_B) consequent: The consequent starts on the next tick ofclk_B(like##1).antecedent |-> @(posedge clk_B) consequent: The consequent starts on the same tick ofclk_Bif the clocks align, or the next tick if they don't (like##0).
The distinction between |-> (overlapping) and |=> (non-overlapping) is subtle but critical when dealing with clocks that might have coincidental edges.
3. Essential Rules for Writing Valid Multi-Clock Assertions
The LRM (Language Reference Manual) and best practices impose strict rules on multi-clock properties. Violating them leads to compile errors or, worse, unsound verification.
Multiclocked Sequences and Properties
Let's continue with the same guide to understand the key structural rules you must follow.
Please read the following sections: 'Unique leading clocking event': Understand why an assertion needs a single starting point. 'No empty matches in the clock boundary': Note the restriction on repetition operators like [*0]. 'Clock Flow': Focus on how clocks propagate (or don't) across implications and out of named sequences or parenthesized expressions.
Here's a summary of those critical rules:
-
Unique Leading Clock: Every concurrent assertion must have one, and only one, top-level clocking event. This event is the anchor for starting all evaluation threads.
// ILLEGAL: Two leading clocks assert property ( (@(posedge clk_A) a) and (@(posedge clk_B) b) ); // LEGAL: One leading clock, with clock changes inside assert property ( @(posedge clk_A) a |-> @(posedge clk_B) b ); -
No
and/orAcross Clocks: You cannot use operators likeand,or, orintersectto combine sequences that have different clocking events.// ILLEGAL (@(posedge clk_A) a) and (@(posedge clk_B) b) -
Clock Scoping is "Trapped": A clock defined inside a named sequence or a parenthesized expression does not "flow out" to subsequent parts of the larger property. The external clock flows in, but the internal clock stays in. This prevents ambiguity.
sequence s1; @(posedge clk_B) b ##1 c; endsequence // ILLEGAL: The clock from 's1' does not flow out to 'd'. 'd' has no clock. assert property (s1 ##1 d); // LEGAL: The top-level clock flows to 'd'. assert property (@(posedge clk_A) a ##1 (@(posedge clk_B) b) ##1 d); // Here, a and d are sampled on clk_A. b is on clk_B.
4. A Robust Pattern for CDC Data Verification
Now let's apply this theory to a real-world problem: verifying a simple data signal crossing from clk_A to clk_B through a two-flop synchronizer.
There is a very common pitfall here related to the difference between event-driven RTL simulation and the cycle-based sampling of SVA.
SystemVerilog Assertions for Clock-Domain-Crossing Data Paths
This excellent paper by Don Mills walks through a common but flawed approach to CDC assertion writing, explains why it fails, and then provides a robust solution. This is exactly the kind of debug-oriented thinking prized in interviews.
Your focus should be on these key parts: Read Section 4.0, 'SVASSERTIONS TO MONITOR CDC', including the code in Example 3. This is the 'naive' property. Skim Section 4.2 to see the refinement using $changed and a local variable (Example 4). Read Section 4.2 starting from 'The following code is an extended model...' and look at Figure 9. Understand why the property fails. The text explains the crucial race condition. Read the text accompanying Examples 8 & 9. This shows the corrected property structure. Finally, read Section 4.4 ('MODELING FOR BOTH RTL AND GLS'). This contains a key insight for creating truly robust, professional-grade assertions.
Let's break down the journey from a flawed property to a robust one.
The Flawed Approach
A common first attempt might look something like this:
// FLAWED APPROACH - DO NOT USE
property cdc_check_flawed;
logic temp_data;
@(posedge clk_A)
($changed(data_in), temp_data = data_in) |=> // Antecedent on clk_A
($stable(data_in)) ##1 // Still on clk_A!
@(posedge clk_B) // Finally switch to clk_B
##[1:4] (sync_data_out === temp_data);
endproperty
The Problem: As the paper explains (Figure 9), the property gets "stuck" in the source clock domain (clk_A) waiting for its antecedent sequence to complete. By the time the assertion's context switches to clk_B, the actual event on the data signal may have already been sampled and propagated in the destination domain. The assertion is looking for the event too late and may fail spuriously or miss bugs.
The Robust Pattern
The solution is to switch the clock context immediately after capturing the source event.
// ROBUST RTL/GLS-FRIENDLY APPROACH
property cdc_data_integrity_check;
logic temp_data;
@(posedge clk_A)
($changed(data_in), temp_data = data_in) |-> // 1. Capture event on clk_A
@(posedge clk_B) // 2. IMMEDIATELY switch to clk_B
##[0:1] // 3. Add 0-1 cycle delay for GLS robustness
(synchronized_data_out === temp_data); // 4. Check the result on clk_B after some latency
endproperty
Let's dissect this superior pattern:
@(posedge clk_A) ($changed(data_in), temp_data = data_in): The antecedent detects a change ondata_inon aclk_Aedge and saves its new value into a local variabletemp_data. This is the trigger.|-> @(posedge clk_B): This is the crucial step. We use the overlapping implication|->to immediately change the property's clocking context toclk_Bin the very same evaluation that the antecedent passed.##[0:1]: This is the subtle but vital addition from Section 4.4 of the paper. Gate-level simulations (GLS) have clock-to-q delays. This small, variable delay makes the assertion robust enough to handle the timing differences between RTL and GLS, preventing false failures in GLS. For an experienced FV role, explaining this detail shows deep practical knowledge.##[latency] (synchronized_data_out === temp_data): After some number ofclk_Bcycles (the synchronizer latency), we assert that the output data matches the value we saved intemp_data.
This pattern ensures the assertion "follows the data" across the clock domain boundary correctly.

Conclusion
Verifying CDC logic is a high-stakes activity, and doing it correctly with SVA requires discipline. Today you learned the formal rules and, more importantly, a practical, robust pattern that avoids common timing pitfalls.
Key Takeaways:
- Unique Leading Clock: Every assertion needs a single, unambiguous starting clock.
- Clock Transition Syntax: Use
@(posedge clk)within a property to switch contexts, and use only##0or##1to connect sequences across clock boundaries. - The Pitfall of Delayed Switching: Do not add logic on the source clock domain in the consequent of an implication. This can cause your assertion to miss the event in the destination domain.
- The Robust CDC Pattern:
- Detect change and store value on
clk_A. - Immediately switch to
clk_Busing|-> @(posedge clk_B). - Add
##[0:1]for RTL vs. GLS robustness. - After the known latency, check for the stored value at the output.
- Detect change and store value on
In our next lesson, we will make our assertions even more powerful and reusable by learning how to write parameterized SVA properties. This will allow us to create scalable and generic checkers for things like arbiters, FIFOs, and the CDC logic we discussed today.
🧠Interview & Practice Drill
Time to test your understanding of these multi-clock semantics.
🎤 Interview Drill Section
-
Rapid Fire Theory:
- What is the semantic difference between using
##0and##1to connect sequences on different clocks? - An engineer writes
assert property ((@(posedge clk_a) req) and (@(posedge clk_b) ack));. Why will this fail to compile? How would you express the intent "ifreqhappens onclk_a, thenackshould eventually happen onclk_b"? - You are reviewing a colleague's CDC assertion. You see they check a condition on the source clock after the implication operator, before switching to the destination clock. What risk does this introduce?
- Why is adding
##[0:1]to a CDC assertion a good practice for ensuring robustness between RTL and gate-level simulations?
- What is the semantic difference between using
-
Resume Deep Dive Question:
- "On your Read Return Unit FPV project, data was likely returned from a memory subsystem (on
mem_clk) to a processing core (oncore_clk). Describe the SVA property you would write to verify that the data payload that left the memory controller onmem_clkis the same data that arrived at the core's input buffer oncore_clka few cycles later. Walk me through the antecedent, the clock switch, and the consequent."
- "On your Read Return Unit FPV project, data was likely returned from a memory subsystem (on
-
Whiteboard Problem:
- A design has a
sendsignal onclk_Aand arecvsignal onclk_B. The protocol is that for every rising edge ofsend, there must be a rising edge ofrecvwithin 5 cycles ofclk_B. Write a robust SVA property to verify this.
- A design has a
🧩 Coding Exercises
-
CDC Data Bus Integrity: A 32-bit data bus
data_acrosses fromclk_atoclk_b, becomingdata_b. The synchronizer and path have a fixed latency of 3clk_bcycles. Write a property nameda_data_bus_integritythat verifies that wheneverdata_achanges,data_breflects that same value 3clk_bcycles later. Use the robust pattern discussed in the lesson (including a local variable). -
Request-Acknowledge Protocol: A request
req_ais asserted onclk_a. This should cause an acknowledge signalack_bto be asserted onclk_b. The clocks are asynchronous. There is no specific latency requirement, butack_bshould not be asserted before the property sees thereq_a. Write a propertya_req_ack_timingthat asserts: ifreq_arises, then at the nearest possibly overlapping edge ofclk_b,ack_bmust be low. (Hint: This is a perfect use case for the##0synchronization semantic). -
Debug the Broken Property: An engineer tried to write a property to check that if
event_aonclk_agoes high, thenevent_bonclk_bandevent_conclk_cmust both go high eventually. Their code is below. It has at least two fundamental errors. Identify them and rewrite the property correctly.// BROKEN CODE property a_multi_event_check; (@(posedge clk_a) $rose(event_a)) |-> ((@(posedge clk_b) s_eventually event_b) and (@(posedge clk_c) s_eventually event_c)); endproperty