
The Verification Challenge
Consider a design where:
- Transactions are broadcasted to multiple routes
- Each route adds variable delay before forwarding
- Transactions can arrive at their destination in different orders
- Transactions with the same ID must maintain relative order across routes
This scenario is common in:
- NoC (Network on Chip) verification
- Multi-channel communication protocols
- Split-transaction bus protocols
- Cache coherency verification
Key Design Patterns
1. Configurable Routes with Dynamic Behavior
The route components are highly configurable through the UVM configuration database:
enable_shuffle
: Controls whether transactions are reordered within a routerandomize_delay
: Determines if delay is fixed or randomdefault_delay
: Fixed delay value when randomization is disabledmin_delay
/max_delay
: Boundaries for random delay generation
This flexibility allows us to create both error-inducing and correct scenarios with the same testbench components.
2. Associative Arrays of Queues for Transaction Management
The comparator uses a sophisticated data structure to manage transactions:
txn_item id_to_trans_first[bit[`ID_LEN-1:0]][$], id_to_trans_second[bit[`ID_LEN-1:0]][$];
This creates associative arrays indexed by transaction ID, where each entry contains a queue of transactions with that ID. The structure allows:
- Grouping by transaction ID
- Maintaining order within each ID group
- Fast lookup for matching transactions
3. Transaction Matching Algorithm
The comparator implements a clever algorithm to match transactions across routes:
- When a transaction arrives from one route, check if the other route has a transaction with the same ID
- if not found – add the transaction to this route’s relevant ID queue for later comparison.
- If found, pop the front transaction from the queue and compare.
- If they match, processing is complete for that transaction.
- If they don’t match: throw a UVM_FATAL.
4. End-of-Test Verification
At the end of the test, the comparator verifies that all transactions were successfully matched:
function void report_phase(uvm_phase phase);
// Check if all arrays in both associative arrays are empty
if (!are_all_keys_empty(id_to_trans_first)) begin
`uvm_error(get_type_name(), "Unprocessed transactions in first route!")
end
if (!are_all_keys_empty(id_to_trans_second)) begin
`uvm_error(get_type_name(), "Unprocessed transactions in second route!")
end
endfunction
5.Test Completion Management with Objections
A critical aspect of our testbench is ensuring that all transactions are properly processed before the test ends. We implement this using UVM’s objection mechanism:
Driver Objection Management
In the driver, we raise an objection for each transaction with a count of 2 (one for each route):
task run_phase(uvm_phase phase);
txn_item txn;
forever begin
seq_item_port.get_next_item(txn);
phase.raise_objection(null, txn.convert2string(), 2);
drive_item(txn);
analysis_port.write(txn);
seq_item_port.item_done();
end
endtask
Route Objection Handling
Each route drops its portion of the objection once it has processed and forwarded the transaction:
task route_transaction();
txn_item item_to_write;
int delay;
// Wait until array is not empty
wait(tx_arr.size() > 0);
// Route transaction with delay
// ...
analysis_port.write(item_to_write);
phase.drop_objection(null, item_to_write.convert2string());
endtask
This elegant approach ensures:
- The test continues running until all transactions have been processed by both routes
- No additional bookkeeping is required to track transaction completion
- Test runtime accurately reflects actual transaction processing time
- Any stalled or lost transactions will prevent the test from completing
By using the transaction’s string representation in both the objection raise and drop calls, we maintain traceability between objections across components. The Verification Engineer could easily trace the problematic transaction/route for which the objection wasn’t dropped.
Testbench Architecture
Our UVM testbench uses a layered architecture with the following components:
Transaction Item
The foundation of the testbench is txn_item
, a simple transaction object with ID and data fields:
class txn_item extends uvm_sequence_item;
rand bit [`ID_LEN-1:0] id;
rand bit [`DATA_LEN-1:0] data;
`uvm_object_utils_begin(txn_item)
`uvm_field_int(id, UVM_ALL_ON)
`uvm_field_int(data, UVM_ALL_ON)
`uvm_object_utils_end
function new(string name = "txn_item");
super.new(name);
endfunction
function string convert2string();
return $sformatf("id=0x%0h data=0x%0h", id, data);
endfunction
endclass
Sequences
We have multiple sequence types to generate different test scenarios:
- Base Sequence: Provides common functionality for derived sequences
class txn_base_sequence extends uvm_sequence #(txn_item);
`uvm_object_utils(txn_base_sequence)
function new(string name = "txn_base_sequence");
super.new(name);
endfunction
task body();
// Empty in base class, implemented by derived sequences
endtask
endclass
- Same-ID Sequence: Generates transactions with identical IDs to test ordering constraints
class txn_same_id_seq extends txn_base_sequence;
task body();
txn_item txn;
repeat(20) begin
txn = txn_item::type_id::create("txn");
`uvm_do_with (txn, { id == 4'h1;})
end
endtask
endclass
- Random Sequence: Generates fully randomized transactions
class txn_full_random_seq extends txn_base_sequence;
task body();
txn_item txn;
repeat(20) begin
txn = txn_item::type_id::create("txn");
`uvm_do (txn)
end
endtask
endclass
The test uses a basic sequence that generates 20 transactions with ID=1:
Driver
The driver receives transactions from the sequencer and broadcasts them to both routes:
class txn_drv extends uvm_driver #(txn_item);
uvm_analysis_port #(txn_item) analysis_port;
task run_phase(uvm_phase phase);
txn_item txn;
forever begin
seq_item_port.get_next_item(txn);
phase.raise_objection(null, txn.convert2string(), 2); // raise objections the number of routes
drive_item(txn);
analysis_port.write(txn);
seq_item_port.item_done();
end
endtask
task drive_item(txn_item txn);
`uvm_info("DRV", $sformatf("Driving transaction: %s", txn.convert2string()), UVM_MEDIUM)
#10; // Simulate driving delay
endtask
endclass
Route Component
Each route component adds configurable delay and optional reordering before forwarding transactions:
class txn_route extends uvm_component;
uvm_analysis_imp #(txn_item, txn_route) analysis_export;
uvm_analysis_port #(txn_item) analysis_port;
txn_item tx_arr[]; // dynamic array for routed transactions
// Configuration parameters
bit enable_shuffle = 0; // When true, will shuffle transactions
bit randomize_delay = 0; // When true, will use random delays
int default_delay = 50; // Fixed delay when not randomizing
int min_delay = 10; // Minimum random delay
int max_delay = 100; // Maximum random delay
virtual function void write(txn_item txn);
txn_item txn_clone;
$cast(txn_clone, txn.clone());
tx_arr = {tx_arr, txn_clone};
if (enable_shuffle) begin
tx_arr.shuffle();
end
endfunction
task route_transaction();
txn_item item_to_write;
int delay;
// Wait until array is not empty
wait(tx_arr.size() > 0);
// Pop transaction from array
item_to_write = tx_arr[tx_arr.size()-1];
tx_arr = new[tx_arr.size()-1](tx_arr);
// Apply configurable delay
if (randomize_delay) begin
delay = $urandom_range(min_delay, max_delay);
else begin
delay = default_delay;
end
#(delay);
analysis_port.write(item_to_write);
phase.drop_objection(null, item_to_write.convert2string()); // every route component drops one objection of each transaction
endtask
endclass
Comparator
The heart of our verification is the txn_ooo_comp
component, which handles out-of-order transactions:
class txn_ooo_comp extends uvm_component;
// Queues to store transactions by ID for each route
txn_item id_to_trans_first[bit[`ID_LEN-1:0]][$], id_to_trans_second[bit[`ID_LEN-1:0]][$];
// Analysis ports
uvm_analysis_imp_first_route #(txn_item, txn_ooo_comp) first_route;
uvm_analysis_imp_second_route #(txn_item, txn_ooo_comp) second_route;
// Write function for first route
virtual function void write_first_route(txn_item t);
txn_item txn_clone;
$cast(txn_clone, t.clone());
// Initialize queue for this ID if it doesn't exist
if (!id_to_trans_first.exists(txn_clone.id)) begin
id_to_trans_first[txn_clone.id] = {};
end
// Check if transaction with same ID exists in the other route's queue
if (id_to_trans_second.exists(txn_clone.id) && id_to_trans_second[txn_clone.id].size() > 0) begin
pop_front_and_compare(id_to_trans_second[txn_clone.id], txn_clone);
end else begin
// No matching ID, store in queue
id_to_trans_first[txn_clone.id].push_back(txn_clone);
end
endfunction
// Similar implementation for second route...
// Check at the end of test if all transactions were processed
function void report_phase(uvm_phase phase);
// Check if all arrays in both associative arrays are empty
if (!are_all_keys_empty(id_to_trans_first)) begin
`uvm_error(get_type_name(), "Unprocessed transactions in first route!")
end
if (!are_all_keys_empty(id_to_trans_second)) begin
`uvm_error(get_type_name(), "Unprocessed transactions in second route!")
end
endfunction
endclass
Test Classes
We implement two test classes to verify different scenarios:
- Correct Test: Configures routes to maintain transaction order
class txn_correct_test extends txn_base_test;
virtual function void configure_routes();
// Configure routes with settings that maintain transaction order
uvm_config_db#(bit)::set(this, "first_route", "enable_shuffle", 1'b0);
uvm_config_db#(bit)::set(this, "first_route", "randomize_delay", 1'b0);
uvm_config_db#(int)::set(this, "first_route", "default_delay", 50);
// Similar config for second route...
endfunction
task run_phase(uvm_phase phase);
txn_base_sequence seq;
phase.raise_objection(this);
seq = txn_full_random_seq::type_id::create("seq");
seq.start(sequencer);
#100000;
phase.drop_objection(this);
endtask
endclass
Error Test: Configures routes to deliberately cause ordering issues
class txn_error_test extends txn_base_test;
virtual function void configure_routes();
// Configure routes with settings that will cause errors
uvm_config_db#(bit)::set(this, "first_route", "enable_shuffle", 1'b1);
uvm_config_db#(bit)::set(this, "first_route", "randomize_delay", 1'b1);
uvm_config_db#(int)::set(this, "first_route", "min_delay", 50);
uvm_config_db#(int)::set(this, "first_route", "max_delay", 150);
// Different config for second route to ensure order mismatch
uvm_config_db#(bit)::set(this, "second_route", "enable_shuffle", 1'b1);
uvm_config_db#(bit)::set(this, "second_route", "randomize_delay", 1'b1);
uvm_config_db#(int)::set(this, "second_route", "min_delay", 10);
uvm_config_db#(int)::set(this, "second_route", "max_delay", 50);
endfunction
// Similar run_phase as correct_test
endclass
Implementation Challenges and Solutions
Transaction Reference Management
One challenge was proper transaction reference management. When transactions are stored in queues for future matching, we need to ensure they’re properly cloned to prevent corruption:
txn_item txn_clone;
$cast(txn_clone, t.clone());
Transaction Matching Logic
The comparator needs to carefully maintain transaction order within each ID group. Our solution:
function automatic bit pop_front_and_compare(ref txn_item arr[$], input txn_item value);
txn_item front_item;
if (arr.size() == 0) begin
return 0; // Queue is empty
end
front_item = arr.pop_front();
if (front_item.compare(value)) begin
`uvm_info("COMPARATOR", $sformatf("Transaction with ID=%0h matched between routes", value.id), UVM_MEDIUM)
return 1; // Successfully matched
end
`uvm_fatal(get_type_name(), $sformatf("Transaction mismatch for ID=%0h: %s vs %s",
value.id,
front_item.convert2string(),
value.convert2string()))
return 0; // Popped but no match
endfunction
Debug and Reporting
As there is no DUT and no free tool in the market that supports UVM signals’ waveforms dumping (I’m looking at you Verilator! 😠), comprehensive debug features help diagnose issues when transactions don’t match expectations. for example:
function void print_assoc_array_queues(string array_name, ref txn_item assoc_arr[bit[`ID_LEN-1:0]][$]);
// Detailed reporting of queue contents
// ...
endfunction
Test Scenarios
1. Correct Test
The txn_correct_test
configures routes with identical timing and ordering behavior, ensuring that:
- Transactions are not shuffled
- Fixed delays are used
- Both routes process transactions in the same order
This provides a baseline to verify that our comparator works correctly with well-behaved routes.
2. Error Test
The txn_error_test
deliberately introduces conditions that will cause transaction ordering issues:
- One route uses significantly longer delays than the other
- Both routes shuffle transactions
- Random delay ranges ensure unpredictable arrival patterns
This test validates that our comparator correctly detects and reports ordering violations.
Conclusion
This UVM testbench demonstrates several advanced verification techniques:
- Configurable route behavior to test different scenarios
- Out-of-order transaction handling
- Transaction matching across multiple paths
- Efficient data structures for transaction management
- Robust error reporting and end-of-test verification
By implementing these patterns, we ensure that our design correctly handles transactions across multiple routes while maintaining the proper ordering constraints for transactions with the same ID.
The architecture is flexible and can be extended to more complex scenarios involving additional routes, different transaction types, or more sophisticated ordering rules.
Future Enhancements
Possible enhancements to this testbench could include:
- Support for more than two routes
- Functional coverage to ensure all interesting scenarios are tested
- Performance metrics for timing analysis
- Integration with a real DUT
For your next verification project involving out-of-order transactions, consider this architecture as a starting point for building robust and flexible testbenches.