a UVM Testbench for Out-of-Order Transaction Verification

In modern SoC designs, transaction routing through multiple paths is a common scenario. When the same transaction can take different routes with variable delays, ensuring data consistency becomes challenging. This blog post explores a UVM testbench architecture designed to verify out-of-order transaction handling while maintaining correct ordering for transactions with matching IDs.

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 route
  • randomize_delay: Determines if delay is fixed or random
  • default_delay: Fixed delay value when randomization is disabled
  • min_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:

  1. 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:

  1. The test continues running until all transactions have been processed by both routes
  2. No additional bookkeeping is required to track transaction completion
  3. Test runtime accurately reflects actual transaction processing time
  4. 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:

  1. 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
  1. 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
  1. 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:

  1. 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.

Newsletter Updates

Enter your email address below and subscribe to my newsletter

Leave a Reply

Your email address will not be published. Required fields are marked *