Hello! Welcome back to the course.
In our last lesson, we established a clear framework for choosing between concurrent and immediate assertions by examining the SystemVerilog simulation scheduler. This understanding of when an assertion evaluates is the foundation for writing correct and robust verification code.
Today, we'll apply that knowledge to a critical and frequently interviewed class of properties: data integrity. We will move beyond simple state checks to verify that data remains correct as it moves through a design. By the end of this lesson, you will be able to write assertions that verify both data stability (ensuring data doesn't change when it shouldn't) and no-corruption (ensuring data isn't lost or altered between two points in time).
The Importance of Data Integrity
What do we mean by "data integrity"? In the context of hardware verification, it typically covers a few key properties:
- Data is not lost: A transaction that enters a block must eventually exit.
- Data is not corrupted: The data that exits a block is identical to what entered.
- Data is not reordered (unless allowed): For a protocol like a FIFO, transactions must exit in the same order they entered.
- Data is stable: Control or data signals remain constant during specific operational phases.
To see why this is so critical, let's consider a common design block like a FIFO.
FIFO Formal Verification Demystified: A Complete Code Breakdown
This short clip from the 'FIFO Formal Verification Demystified' video by Formal Intelligence introduces the concept of data integrity in a practical context.
Watch the segment from 00:16:52 to 00:17:51. The speaker briefly discusses what can go wrong with data in a FIFO—getting lost, overwritten, or reordered. This frames the problem we are about to solve with SVA.
These potential bugs—lost, overwritten, or reordered data—are exactly what data integrity assertions are designed to catch. Let's build the SVA patterns to do just that.
1. Checking Data Stability with $stable
The simplest form of data integrity is stability. You often need to verify that once a transaction starts, its associated control signals, address, or data payload remain constant until the transaction completes.
The SVA system function $stable() is the perfect tool for this. It evaluates to true if its argument has the same value as it did in the previous clock cycle. It's essentially a shorthand for (signal == $past(signal)).
Let's look at a typical bus protocol with a SETUP phase and an ACCESS phase. The rule is that during the ACCESS phase, the bus signals must remain stable.

The property for this check would look something like this:
// Antecedent: The start of the ACCESS phase
sequence s_access_start;
!pready ##1 pready;
endsequence
// Consequent: During the ACCESS phase, signals must be stable
property p_access_stability;
@(posedge pclk)
s_access_start |=> $stable(paddr) &&
$stable(pwdata) &&
$stable(psel) &&
$stable(pwrite);
endproperty
a_access_stability: assert property (p_access_stability);
This is a clean and declarative way to specify data stability. When an interviewer asks you to verify configuration registers or bus signals during a transfer, $stable() should be your first thought.
2. Verifying No-Corruption in Pipelines
Now for a more complex scenario: data corruption. How do you verify that the data that comes out of a multi-stage pipeline is the same data that went in, especially when multiple transactions can be active at once?
This requires a powerful SVA feature: local variables. A local variable declared within a property is created for each evaluation attempt of that property. This allows us to "tag" a transaction when it starts and use that tag to check for the correct data when it finishes.
Let's apply this to a real-world, pipelined protocol.
SVA Properties for pipelined protocols
To handle data corruption in complex, overlapping protocols, we need a way to uniquely identify and track each transaction. The following article from Doulos demonstrates the canonical SVA pattern for this using local variables.
First, read the section 'Developing the Property'. It starts with a basic property and iteratively refines it to correctly handle a pipelined protocol. Focus on how the local variable number is used to capture the request counter's value. Then, read the following short section that introduces the intersect operator to check for a no-overwrite condition. This is a crucial refinement for data integrity.
The core pattern you just studied is a cornerstone of advanced SVA and a frequent topic in FV interviews. Let's break it down:
- Tag the Transaction: When a request (
REQ) occurs, capture its unique identifier (the value of the request counterreq_count) into a local variable.(REQ, local_req_id = req_count) - Specify the Consequent: Imply (
|=>) that at some point in the future, a grant (GRA) must occur where the grant counter (gra_count) matches the captured ID.|=> ... ##1 (GRA && gra_count == local_req_id) - Refine with Negative Constraints: As the article shows, you must also specify what should not happen. During the delay between the request and its corresponding grant, no other grant with this ID should appear. This prevents false passes.
(!GRA || gra_count != local_req_id)[*1:$]
This (antecedent, capture_data) |=> (delay_with_constraints) ##1 (consequent && check_data) pattern is incredibly versatile for verifying any pipelined data transfer, from CPU instruction pipelines to network-on-chip packets.
3. Advanced Integrity Checks with Reference Models
What if data isn't just flowing through a pipeline, but is being stored and retrieved from a memory-like structure (a register file, a cache, a reorder buffer)? The time between write and read can be variable and data can be accessed out of order.
For these scenarios, relying solely on local variables within a single property can become difficult. A more scalable approach, fundamental to both formal and simulation-based verification, is to use a reference model. This is auxiliary verification logic that mimics the behavior of the DUT.
In SystemVerilog, an associative array is an excellent tool for building a simple reference model for data storage.
DYNAMIC DATA STRUCTURES IN ASSERTIONS
For verifying data integrity in memory-like structures, a powerful technique is to use a reference model. This document explains how an associative array can act as a simple reference model to check write-read consistency.
First, read section 1.2.2 'What is an associative array?' to understand the concept and see the memory_data_integrity_check module example. Then, review the property p_aawr in the subsection '1. Assertion using associative array information' (part of the 'Associative Array Application EXAMPLE'). Focus on how the property uses a local variable vaddr to track an address, and then queries the associative array aa to check for data correctness upon a read.
The technique here is a bridge between formal properties and simulation scoreboards:
- Model the Storage: Create an associative array to act as a "shadow memory".
int mem_aarray[int]; // Key is address, value is data - Update the Model: In your verification environment, whenever a write occurs in the DUT, update the associative array with the same data.
if (write) mem_aarray[addr] = wdata; - Assert on Read: Write a property that, upon a read from the DUT, asserts that the data returned (
rddata) matches the data stored in your reference model at that address.
property p_read_after_write; (read && mem_aarray.exists(addr)) |=> rddata == mem_aarray[addr]; endproperty
This approach elegantly handles variable latency and out-of-order access, making it extremely powerful. Understanding it shows an interviewer you can think about verification architecture, not just SVA syntax.
Conclusion
Today we explored three powerful patterns for verifying data integrity, progressing from simple stability checks to complex, multi-transaction verification. These patterns are the bread and butter of verifying control and datapath logic.
Key Takeaways:
- Data Stability: Use
$stable(signal)to assert that a signal does not change during a specific phase. This is perfect for configuration registers and bus protocols. - Pipelined No-Corruption: Use local variables to "tag" transactions as they enter a pipeline and check that the correct data emerges at the end. This is the go-to pattern for request-grant or valid-ready protocols with overlapping transactions.
- Storage No-Corruption: For memory-like structures, use a reference model (e.g., an associative array) to shadow the DUT's state. Assert that the DUT's output matches the reference model's expected output.
In our next lesson, we will shift focus to liveness properties. We'll learn how to write assertions that guarantee something good eventually happens, such as a request eventually getting a grant, and we'll explore the critical difference between strong and weak properties.
🧠Interview & Practice Drill
Time to solidify these concepts.
🎤 Interview Drill Section
-
Rapid Fire Theory:
- What is the difference between
$stable(sig)andsig == $past(sig)? (Trick question: they are functionally very similar, but$stableis more declarative). - You need to verify a 4-entry FIFO. You write a property with a local variable to check that data pushed in is the same as data popped out. What is a key limitation of this approach if the FIFO depth was 1024 entries?
- An interviewer asks, "How would you verify data integrity on a bus that can return read data out of order?" Which of the three patterns we discussed is most suitable here? Why?
- What is the difference between
-
Resume Deep Dive Question:
- "Looking at your Multi-Client Arbiter project, you verified that requests were granted. How did you verify that the grant was for the correct requesting client? If two clients requested in consecutive cycles, how did your properties distinguish which grant belonged to which request? Walk me through the SVA you would write."
-
Whiteboard Problem:
- A memory-mapped register
REGat address16'hA004is writable. The specification says that after a write to this address, astatus_donebit should be asserted by the DUT within 1 to 5 clock cycles. The data bus is 32 bits wide. - Task: Write a single, robust SVA property to verify that:
- When a write occurs to address
16'hA004... - ...the
status_donebit goes high within 1 to 5 cycles. - ...the value written to the register is the same value that was on the
wdatabus during the write cycle. You will need a way to read back the register value, assume a signalreg_internal_valuegives you access to it for verification.
- When a write occurs to address
- A memory-mapped register
🧩 Coding Exercises
Assume a default clock @(posedge clk).
-
Configuration Stability: A module has a 32-bit configuration register
CFG. It is loaded once after reset. A signalcfg_load_donegoes high for one cycle when the load is complete. Write an assertion to ensure that aftercfg_load_donehas occurred, theCFGregister never changes its value for the rest of the simulation. -
Request-Data-Acknowledge: A simple protocol works as follows:
- A client asserts
req. - The server asserts
ackexactly 2 cycles later. - In the cycle that
ackis asserted, the server must present the correct data on therdatabus. The data should bereq_id + 10.req_idis an 8-bit bus asserted along withreq. - Write a property to verify that the
rdatais correct whenackis asserted.
- A client asserts
-
Write-Read Consistency: You are verifying a simple RAM block.
- Signals:
write_en,read_en,addr(8-bit),wdata(32-bit),rdata(32-bit). - For simplicity, a read to an address will always happen at least 4 cycles after a write to the same address. There are no other writes to that same address in between.
- Write an assertion that verifies if you write data
Dto addressA, a subsequent read from addressAreturns the same dataD.
- Signals: