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/Design | Architecture | Pattern | Key Characteristic |
|---|---|---|---|
| PCIe, USB 3.0, MIPI | Layered | Chain of Responsibility | Hierarchical protocol layers |
| AXI, AHB, PCIe TLP | Pipelined | Producer-Consumer (async) | Out-of-order responses |
| Memory, Slave I/F | Reactive | Observer | Response to external stimulus |
| Interrupt Controller | Reactive + Priority | Observer + Strategy | Event-driven with priority |
| DMA Controller | Virtual + Pipelined | Mediator + Producer-Consumer | Multi-interface coordination |
| Network Stack | Layered + Virtual | Chain + Mediator | Protocol 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
Comments (0)
Leave a Comment