Advanced Sequence Architectures - Layered, Pipelined, and Reactive Patterns

Different protocols demand different sequence architectures. PCIe needs layered sequences matching its Transaction/Data Link/Physical hierarchy. AXI needs pipelined sequences handling out-of-order responses. Memory controllers need reactive sequences responding to incoming traffic. This post maps these verification challenges to software design patterns.

Layered Sequences: Chain of Responsibility

Protocols like PCIe, USB 3.0, and MIPI have hierarchical definitions with distinct layers. Each layer transforms data before passing it down - a natural fit for the Chain of Responsibility pattern.

flowchart TB
    subgraph "PCIe Protocol Stack"
        TL[Transaction Layer
Memory Read/Write] --> DL[Data Link Layer
Packet Framing, CRC] DL --> PL[Physical Layer
8b/10b, Electrical] end subgraph "Layered Sequences" TS[Transaction Seq] -->|translates| DS[Data Link Seq] DS -->|translates| PS[Physical Seq] PS -->|drives| IF[Interface] end

Architecture: Translation Sequences

Higher-layer sequences generate abstract transactions; lower-layer sequences translate and execute them:

// Layer 3: Transaction Layer (highest abstraction)
class pcie_transaction_seq extends uvm_sequence #(pcie_tlp);
  `uvm_object_utils(pcie_transaction_seq)
  
  task body();
    pcie_tlp tlp = pcie_tlp::type_id::create("tlp");
    start_item(tlp);
    if (!tlp.randomize() with {
      tlp_type == MEMORY_READ;
      length inside {[1:128]};
    }) `uvm_fatal("SEQ", "TLP randomization failed")
    finish_item(tlp);
  endtask
endclass

// Layer 2: Data Link Layer (adds framing, sequence numbers)
class pcie_dllp_seq extends uvm_sequence #(pcie_dllp);
  `uvm_object_utils(pcie_dllp_seq)
  
  // Port to pull TLPs from upper layer
  uvm_seq_item_pull_port #(pcie_tlp) upper_layer_port;
  
  task body();
    pcie_tlp tlp;
    pcie_dllp dllp;
    
    forever begin
      // Pull from upper layer
      upper_layer_port.get_next_item(tlp);
      
      // Transform: Add DLLP wrapper
      dllp = pcie_dllp::type_id::create("dllp");
      dllp.sequence_num = next_seq_num++;
      dllp.payload = tlp;
      dllp.lcrc = calculate_lcrc(tlp);
      
      // Send to lower layer
      start_item(dllp);
      finish_item(dllp);
      
      upper_layer_port.item_done();
    end
  endtask
endclass

Layered Sequencer Architecture

class pcie_layered_env extends uvm_env;
  `uvm_component_utils(pcie_layered_env)
  
  // Sequencers for each layer
  uvm_sequencer #(pcie_tlp)  tlp_sequencer;   // Transaction
  uvm_sequencer #(pcie_dllp) dllp_sequencer;  // Data Link
  uvm_sequencer #(pcie_phy)  phy_sequencer;   // Physical
  
  // Layering sequences (run continuously)
  pcie_dllp_seq  dllp_layer;
  pcie_phy_seq   phy_layer;
  
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    tlp_sequencer  = uvm_sequencer#(pcie_tlp)::type_id::create("tlp_sqr", this);
    dllp_sequencer = uvm_sequencer#(pcie_dllp)::type_id::create("dllp_sqr", this);
    phy_sequencer  = uvm_sequencer#(pcie_phy)::type_id::create("phy_sqr", this);
  endfunction
  
  function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    // Connect layer pull ports
    dllp_layer.upper_layer_port.connect(tlp_sequencer.seq_item_export);
    phy_layer.upper_layer_port.connect(dllp_sequencer.seq_item_export);
  endfunction
  
  task run_phase(uvm_phase phase);
    // Start layering sequences (run forever in background)
    fork
      dllp_layer.start(dllp_sequencer);
      phy_layer.start(phy_sequencer);
    join_none
  endtask
endclass

Benefits of Layered Architecture

  • Error injection at any layer: Inject CRC errors at DL layer without modifying TL sequences
  • Layer-specific coverage: Track coverage at each abstraction level
  • Reusability: Same TL sequences work with different PHY implementations
  • Protocol compliance: Each layer enforces its own rules

Pipelined Sequences: Async Producer-Consumer

Protocols like AXI allow multiple outstanding transactions - requests and responses are decoupled. This requires the Producer-Consumer pattern with async handling.

sequenceDiagram
    participant S as Sequence
    participant D as Driver
    participant DUT as DUT
    
    S->>D: Request 1 (ID=0)
    S->>D: Request 2 (ID=1)
    S->>D: Request 3 (ID=2)
    Note over D,DUT: Requests pipelined
    DUT-->>D: Response 2 (ID=1)
    DUT-->>D: Response 1 (ID=0)
    DUT-->>D: Response 3 (ID=2)
    Note over D,S: Out-of-order responses

The Pipelining Challenge

Traditional finish_item() blocks until the driver completes - this serializes requests. For pipelining, we need non-blocking request submission and separate response handling.

Solution: Decoupled Request-Response

class axi_pipelined_seq extends uvm_sequence #(axi_transaction);
  `uvm_object_utils(axi_pipelined_seq)
  `uvm_declare_p_sequencer(axi_sequencer)
  
  // Track outstanding requests
  axi_transaction outstanding_q[$];
  int max_outstanding = 8;
  
  task body();
    fork
      request_thread();
      response_thread();
    join
  endtask
  
  // Producer: Issue requests as fast as possible
  task request_thread();
    repeat(100) begin
      axi_transaction req;
      
      // Backpressure: Wait if too many outstanding
      wait(outstanding_q.size() < max_outstanding);
      
      req = axi_transaction::type_id::create("req");
      start_item(req);
      if (!req.randomize() with {
        // Unique ID for tracking
        !(id inside {outstanding_q.id});
      }) `uvm_fatal("SEQ", "Randomization failed")
      
      outstanding_q.push_back(req);  // Track it
      finish_item(req);              // Non-blocking if driver supports
    end
  endtask
  
  // Consumer: Handle responses (possibly out-of-order)
  task response_thread();
    axi_transaction rsp;
    
    forever begin
      // Get response from driver via analysis port or rsp_port
      p_sequencer.rsp_fifo.get(rsp);
      
      // Match to outstanding request
      foreach(outstanding_q[i]) begin
        if (outstanding_q[i].id == rsp.id) begin
          // Process response
          check_response(outstanding_q[i], rsp);
          outstanding_q.delete(i);
          break;
        end
      end
    end
  endtask
  
  function void check_response(axi_transaction req, axi_transaction rsp);
    if (req.kind == READ && rsp.data != expected_data[req.addr])
      `uvm_error("SEQ", $sformatf("Data mismatch at 0x%0x", req.addr))
  endfunction
  
endclass

Driver Side: Using rsp_port

class axi_driver extends uvm_driver #(axi_transaction);
  `uvm_component_utils(axi_driver)
  
  task run_phase(uvm_phase phase);
    forever begin
      axi_transaction req, rsp;
      
      // Get request (non-blocking from sequencer's perspective)
      seq_item_port.get_next_item(req);
      
      // Drive request phase on interface
      drive_request(req);
      
      // Signal request sent (allows sequencer to issue next)
      seq_item_port.item_done();
      
      // Spawn response handler (runs in parallel)
      fork
        handle_response(req);
      join_none
    end
  endtask
  
  task handle_response(axi_transaction req);
    axi_transaction rsp;
    
    // Wait for actual response from DUT
    wait_for_response(req.id, rsp);
    
    // Send response back via rsp_port (analysis port)
    rsp_port.write(rsp);  // Sequence receives this
  endtask
  
endclass

When to Use Pipelined Architecture

  • AXI/AHB buses: Multiple outstanding transactions
  • PCIe: Split transactions with completions
  • Network protocols: TCP/UDP with async responses
  • Memory subsystems: Cache line fetches with variable latency

Reactive Sequences: Observer Pattern

Some components don't initiate - they respond. Memory controllers, slave interfaces, and interrupt handlers need reactive sequences that observe and react to incoming traffic.

flowchart LR
    M[Monitor] -->|observes| IF[Interface]
    M -->|publishes| AP[Analysis Port]
    AP -->|notifies| RS[Reactive Sequence]
    RS -->|generates response| SQR[Sequencer]
    SQR --> D[Driver]
    D -->|drives| IF

Architecture: Monitor-Driven Sequences

class memory_reactive_seq extends uvm_sequence #(mem_response);
  `uvm_object_utils(memory_reactive_seq)
  `uvm_declare_p_sequencer(mem_sequencer)
  
  // Memory model (could be associative array or external model)
  bit [31:0] memory[bit [31:0]];
  
  task body();
    mem_request  req;
    mem_response rsp;
    
    forever begin
      // Wait for request from monitor (via TLM port)
      p_sequencer.request_fifo.get(req);
      
      // Generate appropriate response
      rsp = mem_response::type_id::create("rsp");
      
      case (req.op)
        READ: begin
          rsp.data = memory.exists(req.addr) ? memory[req.addr] : 'x;
          rsp.resp = memory.exists(req.addr) ? OKAY : DECERR;
        end
        WRITE: begin
          memory[req.addr] = req.data;
          rsp.resp = OKAY;
        end
      endcase
      
      rsp.id = req.id;  // Correlation
      
      // Send response
      start_item(rsp);
      finish_item(rsp);
    end
  endtask
  
endclass

Reactive Sequencer with TLM Ports

class mem_sequencer extends uvm_sequencer #(mem_response);
  `uvm_component_utils(mem_sequencer)
  
  // TLM FIFO to receive requests from monitor
  uvm_tlm_analysis_fifo #(mem_request) request_fifo;
  
  function new(string name, uvm_component parent);
    super.new(name, parent);
    request_fifo = new("request_fifo", this);
  endfunction
  
endclass

class mem_agent extends uvm_agent;
  `uvm_component_utils(mem_agent)
  
  mem_monitor   monitor;
  mem_sequencer sequencer;
  mem_driver    driver;
  
  function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    // Connect monitor to sequencer (Observer pattern)
    monitor.ap.connect(sequencer.request_fifo.analysis_export);
    driver.seq_item_port.connect(sequencer.seq_item_export);
  endfunction
  
endclass

Advanced: Reactive with Configurable Behavior

Use the Strategy Pattern for pluggable response behaviors:

// Strategy interface
virtual class response_strategy extends uvm_object;
  pure virtual function mem_response generate_response(mem_request req);
endclass

// Concrete strategies
class normal_response_strategy extends response_strategy;
  bit [31:0] memory[bit [31:0]];
  
  virtual function mem_response generate_response(mem_request req);
    mem_response rsp = new();
    rsp.data = memory[req.addr];
    rsp.resp = OKAY;
    return rsp;
  endfunction
endclass

class error_injection_strategy extends response_strategy;
  int error_rate = 10;  // 10% errors
  
  virtual function mem_response generate_response(mem_request req);
    mem_response rsp = new();
    if ($urandom_range(100) < error_rate) begin
      rsp.resp = SLVERR;
    end else begin
      rsp.data = $urandom();
      rsp.resp = OKAY;
    end
    return rsp;
  endfunction
endclass

// Sequence uses strategy
class configurable_reactive_seq extends uvm_sequence #(mem_response);
  response_strategy strategy;
  
  task body();
    forever begin
      mem_request req;
      p_sequencer.request_fifo.get(req);
      
      mem_response rsp = strategy.generate_response(req);
      start_item(rsp);
      finish_item(rsp);
    end
  endtask
endclass

Choosing the Right Architecture

Protocol/DesignArchitecturePatternKey Characteristic
PCIe, USB 3.0, MIPILayeredChain of ResponsibilityHierarchical protocol layers
AXI, AHB, PCIe TLPPipelinedProducer-Consumer (async)Out-of-order responses
Memory, Slave I/FReactiveObserverResponse to external stimulus
Interrupt ControllerReactive + PriorityObserver + StrategyEvent-driven with priority
DMA ControllerVirtual + PipelinedMediator + Producer-ConsumerMulti-interface coordination
Network StackLayered + VirtualChain + MediatorProtocol hierarchy + routing

Combining Architectures

Real designs often need combinations. A PCIe endpoint might use:

  • Layered sequences for TLP generation through protocol stack
  • Pipelined handling for split transactions
  • Reactive sequences for completion generation
  • Virtual sequences for coordinating with system memory

Key Takeaways

  • Layered = Chain of Responsibility: Each layer transforms and passes down
  • Pipelined = Async Producer-Consumer: Decouple requests from responses
  • Reactive = Observer: Monitor triggers sequence execution
  • Match architecture to protocol: Don't force-fit patterns
  • Use Strategy for flexibility: Pluggable behaviors without inheritance explosion
  • Combine patterns for complex designs: Real systems need multiple approaches

Previous: Part 2 - Virtual Sequences | Next: Part 4 - Sequencer Arbitration & Control

Sources consulted: Verification Academy, Truechip, Doulos

Author
Mayur Kubavat
VLSI Design and Verification Engineer sharing knowledge about SystemVerilog, UVM, and hardware verification methodologies.

Comments (0)

Leave a Comment