Facade Pattern in UVM: Simplifying Complex Subsystems with Virtual Sequences
Your memory subsystem has three agents — a CPU interface, a DMA engine, and a memory controller. A simple DMA transfer test requires configuring the memory controller's timing parameters, programming the DMA with source, destination, and length, triggering the CPU to initiate the transfer, and polling for completion. Four agents, twelve sequence calls, and a web of event synchronization — all for what should be a one-line operation. The Facade pattern wraps that subsystem behind a virtual sequence that exposes exactly the operations your tests need, hiding every protocol detail underneath.
- The Problem: When Tests Become Multi-Agent Spaghetti
- Gang of Four: The Facade Pattern
- UVM's Facade: Virtual Sequences and Virtual Sequencers
- Building Memory Subsystem Facades
- Scaling Up: Composing and Parameterizing Facades
- Advanced: Facade + Factory
- Quick Reference
The Problem: When Tests Become Multi-Agent Spaghetti
You have a memory subsystem testbench. Three agents verify the design: a CPU agent drives read/write transactions on the processor interface, a DMA agent programs and monitors bulk data transfers, and a memory controller agent configures timing parameters, refresh intervals, and ECC settings. Each agent has its own sequencer, its own transaction types, and its own protocol quirks. The agents work. The problem is making them work together.
Consider the most common test scenario: configure the memory controller, then run a DMA transfer from one memory region to another, then verify the data via CPU reads. Here is what that test looks like when you coordinate the agents directly:
class dma_transfer_test extends base_test;
`uvm_component_utils(dma_transfer_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
task run_phase(uvm_phase phase);
mem_ctrl_timing_seq mc_seq;
dma_program_seq dma_prog;
dma_start_seq dma_start;
dma_poll_seq dma_poll;
cpu_write_seq cpu_wr;
cpu_read_seq cpu_rd;
phase.raise_objection(this);
// Step 1: Configure memory controller timing
mc_seq = mem_ctrl_timing_seq::type_id::create("mc_seq");
mc_seq.cas_latency = 11;
mc_seq.ras_to_cas = 13;
mc_seq.write_recovery = 8;
mc_seq.refresh_interval = 7800;
mc_seq.start(env.mem_ctrl_agent.sequencer);
// Step 2: Write test data to source region via CPU
for (int i = 0; i < 64; i++) begin
cpu_wr = cpu_write_seq::type_id::create("cpu_wr");
cpu_wr.addr = 32'h0000_1000 + (i * 4);
cpu_wr.data = $urandom();
cpu_wr.start(env.cpu_agent.sequencer);
end
// Step 3: Program DMA — source, destination, length
dma_prog = dma_program_seq::type_id::create("dma_prog");
dma_prog.src_addr = 32'h0000_1000;
dma_prog.dst_addr = 32'h0000_2000;
dma_prog.length = 256; // bytes
dma_prog.direction = DMA_MEM_TO_MEM;
dma_prog.start(env.dma_agent.sequencer);
// Step 4: Start DMA transfer
dma_start = dma_start_seq::type_id::create("dma_start");
dma_start.channel = 0;
dma_start.start(env.dma_agent.sequencer);
// Step 5: Poll for DMA completion
dma_poll = dma_poll_seq::type_id::create("dma_poll");
dma_poll.channel = 0;
dma_poll.timeout_cycles = 10000;
dma_poll.start(env.dma_agent.sequencer);
if (!dma_poll.completed)
`uvm_error("DMA", "Transfer did not complete within timeout")
// Step 6: Read back destination region and verify
for (int i = 0; i < 64; i++) begin
cpu_rd = cpu_read_seq::type_id::create("cpu_rd");
cpu_rd.addr = 32'h0000_2000 + (i * 4);
cpu_rd.start(env.cpu_agent.sequencer);
end
phase.drop_objection(this);
endtask
endclass
Fifty lines for a basic DMA transfer test. And this is the simple version — no error handling, no edge cases, no parameterization. Every test that involves a DMA transfer repeats this same choreography. Want to test different burst sizes? Copy the sequence, change two numbers. Want to test DMA with ECC enabled? Add eight more lines of memory controller configuration at the top. Want to run the same transfer pattern across different address ranges? Good luck extracting that from the middle of the coordination logic.
The problems compound:
- Protocol leakage — Every test writer must understand the memory controller's timing register layout, the DMA's programming model, and the CPU interface's transaction format. Three protocols, three APIs, three sets of field names.
- Duplicated choreography — The "configure → program → start → poll → verify" sequence appears in every DMA test with minor variations. Change the polling mechanism and you update twenty tests.
- Fragile coordination — The steps must happen in order. Program the DMA before starting it. Configure memory timing before writing data. Miss one dependency and the test silently produces wrong results.
- Sequencer coupling — Tests reach directly into
env.cpu_agent.sequencer,env.dma_agent.sequencer, andenv.mem_ctrl_agent.sequencer. Change the environment hierarchy and every test breaks.
The core issue is clear: tests are doing two jobs at once — defining what to test and orchestrating how the subsystem components interact. The orchestration logic does not belong in the test. It belongs behind a simplified interface that hides the subsystem's internal complexity.
There is a pattern for exactly this.
Gang of Four: The Facade Pattern
"Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use."
— Design Patterns: Elements of Reusable Object-Oriented Software (Gamma et al., 1994)
The Facade pattern places a single object in front of a complex subsystem. Clients talk to the Facade. The Facade talks to the subsystem components. Clients never interact with the subsystem directly — they don't need to know how many components exist, what order they must be called in, or what protocols they use internally. The Facade absorbs all of that complexity.
classDiagram
class Client {
+doWork()
}
class Facade {
-subsystemA : SubsystemA
-subsystemB : SubsystemB
-subsystemC : SubsystemC
+simplifiedOperation()
}
class SubsystemA {
+operationA1()
+operationA2()
}
class SubsystemB {
+operationB1()
+operationB2()
}
class SubsystemC {
+operationC1()
}
Client --> Facade : uses
Facade --> SubsystemA : delegates
Facade --> SubsystemB : delegates
Facade --> SubsystemC : delegates
The key insight is what the Facade does not do. It does not add new behavior — that is the Decorator. It does not translate between incompatible interfaces — that is the Adapter. It does not enable bidirectional communication between subsystem components — that is the Mediator. The Facade simply provides a simpler interface to an existing subsystem. The subsystem components still exist, still function independently, and can still be accessed directly when needed. The Facade is a convenience layer, not a wall.
Facade vs. Adapter vs. Decorator
Three structural patterns, three different intents:
- Adapter (Post 5) — changes the interface to make two incompatible types work together. Your
apb_reg_adapterconverteduvm_reg_bus_opintoapb_txn. Different types, same operation. - Decorator (Post 6) — preserves the interface and adds behavior. Your coverage subscriber observed the same
axi_txnand added coverage sampling. Same type, new behavior. - Facade (this post) — reduces interface complexity by wrapping a subsystem. A virtual sequence hides three agents behind a single
dma_transfer()call. Same behavior, simpler access.
If you are making two things compatible, that is an Adapter. If you are adding responsibilities to an existing object, that is a Decorator. If you are simplifying access to a complex subsystem, that is a Facade.
Facade vs. Mediator
The Facade and the Mediator both sit between clients and subsystem components. The difference is the direction of communication.
A Facade is unidirectional. The client calls the Facade, and the Facade calls subsystem components. The subsystem components do not call back to the Facade, and they do not communicate with each other through it. The Facade is a front door — you walk in, it routes you to the right room, and the rooms do not know the front door exists.
A Mediator is bidirectional. Subsystem components register with the Mediator and communicate through it. When component A needs to coordinate with component B, it sends a message to the Mediator, and the Mediator routes it to B. The Mediator actively manages interactions between components — it is a switchboard, not a front door.
flowchart LR
subgraph Facade Pattern
direction LR
C1[Client] -->|calls| F[Facade]
F -->|delegates| S1[Subsystem A]
F -->|delegates| S2[Subsystem B]
F -->|delegates| S3[Subsystem C]
end
flowchart LR
subgraph Mediator Pattern
direction LR
CA[Component A] <-->|communicates| M[Mediator]
CB[Component B] <-->|communicates| M
CC[Component C] <-->|communicates| M
end
In UVM terms: a virtual sequence that orchestrates agents by calling sub-sequences in order is a Facade — unidirectional delegation. A uvm_event_pool or a shared scoreboard that components use to coordinate with each other is closer to a Mediator — bidirectional communication through a central point.
When Facade Shines
- Multi-agent subsystems — When a test scenario requires coordinating three or more agents, the coordination logic overwhelms the test intent. A Facade separates the "what" from the "how."
- Test API simplification — When you want test writers to call
dma_transfer(src, dst, len)instead of knowing the DMA's programming model, the Facade is the right pattern. - Protocol isolation — When your test should not break because the DMA agent changed from register-based programming to a descriptor ring model, the Facade absorbs that change.
- Layered abstraction — When you need low-level access for some tests and high-level access for others, a Facade gives you the high level without removing the low level.
UVM's Facade: Virtual Sequences and Virtual Sequencers
UVM does not have a class called uvm_facade. But its virtual sequence and virtual sequencer mechanism implements the Facade pattern naturally. A virtual sequence coordinates multiple agents by starting sub-sequences on their respective sequencers, hiding the multi-agent choreography behind a single body() method. This is the Facade pattern — a unified interface to a set of interfaces in a subsystem.
GoF Role Mapping
Map the GoF participants to UVM:
- Client — Your test. It creates the virtual sequence, configures high-level parameters, and calls
start(). - Facade — The virtual sequence. Its
body()method orchestrates sub-sequences across multiple agents, exposing only the parameters the test needs. - Subsystem components — The individual agent sequences.
mem_ctrl_timing_seq,dma_program_seq,cpu_write_seq— each one knows its own protocol but nothing about the overall coordination. - Subsystem registry — The virtual sequencer. It holds handles to the sub-sequencers, giving the Facade access to each subsystem component without the test needing to know the environment hierarchy.
The virtual sequencer is the wiring between the Facade and the subsystem. It does not contain logic — it holds references. The environment creates it and assigns the sub-sequencer handles in connect_phase. The virtual sequence accesses those handles through p_sequencer to start sub-sequences on the correct sequencers.
Why Virtual Sequences Are Facades
Virtual sequences satisfy the Facade contract precisely:
- Unified interface — The test calls one
start()on one virtual sequence. It does not know how many agents are involved or what sub-sequences run underneath. - No new behavior — The virtual sequence does not add protocol-level behavior. It delegates to sub-sequences that do the actual work. Each sub-sequence still generates the same transactions it always did.
- Subsystem still accessible — The Facade does not hide the agents. Tests that need low-level control can still run individual agent sequences directly. The Facade is a convenience layer, not a restriction.
- Coordination encapsulated — The ordering, timing, and dependencies between sub-sequences live inside the Facade. Tests express intent; the Facade handles choreography.
Virtual Sequence vs. Regular Sequence
A regular sequence runs on one sequencer and generates transactions for one agent. A virtual sequence runs on a virtual sequencer and starts sub-sequences on multiple agent sequencers. The virtual sequence itself does not generate any transactions — it coordinates the sequences that do. This is the structural difference that makes virtual sequences natural Facades: they sit above the agent layer and orchestrate it.
Building Memory Subsystem Facades
Let's build the Facade for the memory subsystem. We start with the base transaction types and agent sequences, then build the virtual sequencer, then the Facade virtual sequence, and finally show how the test simplifies from fifty lines to ten.
Base Transaction Types
Each agent has its own transaction type. These are the subsystem components — each speaks its own protocol:
// CPU agent transaction
class cpu_txn extends uvm_sequence_item;
rand bit [31:0] addr;
rand bit [31:0] data;
rand bit write; // 1 = write, 0 = read
rand bit [3:0] byte_en;
bit [31:0] rdata; // Read response
bit error; // Error response
`uvm_object_utils(cpu_txn)
function new(string name = "cpu_txn");
super.new(name);
endfunction
endclass
// DMA agent transaction
typedef enum bit [1:0] { DMA_MEM_TO_MEM = 0, DMA_MEM_TO_IO = 1,
DMA_IO_TO_MEM = 2 } dma_dir_e;
class dma_txn extends uvm_sequence_item;
rand bit [31:0] src_addr;
rand bit [31:0] dst_addr;
rand bit [15:0] length; // Transfer length in bytes
rand dma_dir_e direction;
rand bit [2:0] channel;
bit complete; // Set by DUT on completion
bit error;
`uvm_object_utils(dma_txn)
function new(string name = "dma_txn");
super.new(name);
endfunction
endclass
// Memory controller agent transaction
class mem_ctrl_txn extends uvm_sequence_item;
rand bit [7:0] cas_latency;
rand bit [7:0] ras_to_cas;
rand bit [7:0] write_recovery;
rand bit [15:0] refresh_interval;
rand bit ecc_enable;
rand bit [1:0] ecc_mode; // 0=SEC, 1=SECDED, 2=Chipkill
bit cfg_done; // Handshake from DUT
`uvm_object_utils(mem_ctrl_txn)
function new(string name = "mem_ctrl_txn");
super.new(name);
endfunction
endclass
Three transaction types. Three protocols. Three sets of field names and semantics. A test writer working at the subsystem level should not need to know any of these.
Agent Sequences (Subsystem Components)
Each agent has focused sequences that know one protocol. These are the subsystem components the Facade delegates to:
// Memory controller: configure timing parameters
class mem_ctrl_timing_seq extends uvm_sequence #(mem_ctrl_txn);
`uvm_object_utils(mem_ctrl_timing_seq)
bit [7:0] cas_latency = 11;
bit [7:0] ras_to_cas = 13;
bit [7:0] write_recovery = 8;
bit [15:0] refresh_interval = 7800;
function new(string name = "mem_ctrl_timing_seq");
super.new(name);
endfunction
task body();
mem_ctrl_txn txn = mem_ctrl_txn::type_id::create("txn");
start_item(txn);
txn.cas_latency = cas_latency;
txn.ras_to_cas = ras_to_cas;
txn.write_recovery = write_recovery;
txn.refresh_interval = refresh_interval;
finish_item(txn);
endtask
endclass
// DMA: program and start a transfer
class dma_transfer_seq extends uvm_sequence #(dma_txn);
`uvm_object_utils(dma_transfer_seq)
bit [31:0] src_addr;
bit [31:0] dst_addr;
bit [15:0] length;
dma_dir_e direction = DMA_MEM_TO_MEM;
bit [2:0] channel = 0;
bit completed;
bit error;
function new(string name = "dma_transfer_seq");
super.new(name);
endfunction
task body();
dma_txn txn = dma_txn::type_id::create("txn");
start_item(txn);
txn.src_addr = src_addr;
txn.dst_addr = dst_addr;
txn.length = length;
txn.direction = direction;
txn.channel = channel;
finish_item(txn);
completed = txn.complete;
error = txn.error;
endtask
endclass
// CPU: write a single word
class cpu_write_seq extends uvm_sequence #(cpu_txn);
`uvm_object_utils(cpu_write_seq)
bit [31:0] addr;
bit [31:0] data;
function new(string name = "cpu_write_seq");
super.new(name);
endfunction
task body();
cpu_txn txn = cpu_txn::type_id::create("txn");
start_item(txn);
txn.addr = addr;
txn.data = data;
txn.write = 1;
txn.byte_en = 4'hF;
finish_item(txn);
endtask
endclass
// CPU: read a single word
class cpu_read_seq extends uvm_sequence #(cpu_txn);
`uvm_object_utils(cpu_read_seq)
bit [31:0] addr;
bit [31:0] rdata;
function new(string name = "cpu_read_seq");
super.new(name);
endfunction
task body();
cpu_txn txn = cpu_txn::type_id::create("txn");
start_item(txn);
txn.addr = addr;
txn.write = 0;
finish_item(txn);
rdata = txn.rdata;
endtask
endclass
Each sequence does one thing well. The memory controller sequence configures timing. The DMA sequence programs and executes a transfer. The CPU sequences do single-word reads and writes. None of them know about each other. The Facade's job is to compose them.
The Virtual Sequencer (Subsystem Registry)
The virtual sequencer holds handles to each agent's sequencer. It does not contain logic — it is a registry of subsystem components that the Facade uses to reach each agent:
class mem_subsystem_vsequencer extends uvm_sequencer;
`uvm_component_utils(mem_subsystem_vsequencer)
uvm_sequencer #(cpu_txn) cpu_sqr;
uvm_sequencer #(dma_txn) dma_sqr;
uvm_sequencer #(mem_ctrl_txn) mem_ctrl_sqr;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
endclass
The environment wires these handles in connect_phase:
class mem_subsystem_env extends uvm_env;
`uvm_component_utils(mem_subsystem_env)
cpu_agent cpu_agt;
dma_agent dma_agt;
mem_ctrl_agent mem_ctrl_agt;
mem_subsystem_vsequencer vsequencer;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
cpu_agt = cpu_agent::type_id::create("cpu_agt", this);
dma_agt = dma_agent::type_id::create("dma_agt", this);
mem_ctrl_agt = mem_ctrl_agent::type_id::create("mem_ctrl_agt", this);
vsequencer = mem_subsystem_vsequencer::type_id::create("vsequencer", this);
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
vsequencer.cpu_sqr = cpu_agt.sequencer;
vsequencer.dma_sqr = dma_agt.sequencer;
vsequencer.mem_ctrl_sqr = mem_ctrl_agt.sequencer;
endfunction
endclass
Three assignments in connect_phase. The virtual sequencer now has access to all three agents. Any virtual sequence running on this sequencer can reach any agent's sequencer through p_sequencer.
The Facade Virtual Sequence
This is the Facade itself. It exposes high-level operations that hide the multi-agent choreography:
class mem_subsystem_vseq extends uvm_sequence;
`uvm_object_utils(mem_subsystem_vseq)
`uvm_declare_p_sequencer(mem_subsystem_vsequencer)
// High-level parameters — the only knobs the test sees
bit [31:0] src_addr = 32'h0000_1000;
bit [31:0] dst_addr = 32'h0000_2000;
bit [15:0] transfer_len = 256;
bit [31:0] test_data[]; // Optional: pre-loaded data pattern
bit verify_data = 1;
// Results
bit transfer_ok;
int errors;
function new(string name = "mem_subsystem_vseq");
super.new(name);
endfunction
task body();
configure_memory();
write_source_data();
run_dma_transfer();
if (verify_data)
verify_destination();
endtask
// --- Facade methods: each hides one subsystem interaction ---
virtual task configure_memory();
mem_ctrl_timing_seq mc_seq;
mc_seq = mem_ctrl_timing_seq::type_id::create("mc_seq");
mc_seq.start(p_sequencer.mem_ctrl_sqr);
endtask
virtual task write_source_data();
cpu_write_seq wr;
int num_words = transfer_len / 4;
if (test_data.size() == 0) begin
test_data = new[num_words];
foreach (test_data[i]) test_data[i] = $urandom();
end
foreach (test_data[i]) begin
wr = cpu_write_seq::type_id::create("wr");
wr.addr = src_addr + (i * 4);
wr.data = test_data[i];
wr.start(p_sequencer.cpu_sqr);
end
endtask
virtual task run_dma_transfer();
dma_transfer_seq dma_seq;
dma_seq = dma_transfer_seq::type_id::create("dma_seq");
dma_seq.src_addr = src_addr;
dma_seq.dst_addr = dst_addr;
dma_seq.length = transfer_len;
dma_seq.start(p_sequencer.dma_sqr);
transfer_ok = dma_seq.completed && !dma_seq.error;
if (!transfer_ok)
`uvm_error("FACADE", $sformatf(
"DMA transfer failed: src=0x%08h dst=0x%08h len=%0d",
src_addr, dst_addr, transfer_len))
endtask
virtual task verify_destination();
cpu_read_seq rd;
int num_words = transfer_len / 4;
errors = 0;
foreach (test_data[i]) begin
rd = cpu_read_seq::type_id::create("rd");
rd.addr = dst_addr + (i * 4);
rd.start(p_sequencer.cpu_sqr);
if (rd.rdata !== test_data[i]) begin
`uvm_error("FACADE", $sformatf(
"Data mismatch at 0x%08h: exp=0x%08h got=0x%08h",
rd.addr, test_data[i], rd.rdata))
errors++;
end
end
endtask
endclass
The Facade's body() reads like a specification: configure memory, write source data, run DMA transfer, verify destination. Four method calls. Each method hides the protocol details and agent-specific sequencing underneath. The test never touches a sub-sequencer directly. It never constructs a mem_ctrl_txn or a dma_txn. It never coordinates timing between agents. All of that lives inside the Facade.
Notice that every internal method is virtual. This matters for the Factory composition we cover in the Advanced section — subclasses can override individual Facade methods without replacing the entire sequence.
The Clean Test
Compare this with the fifty-line spaghetti from the opening:
class dma_transfer_test extends base_test;
`uvm_component_utils(dma_transfer_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
task run_phase(uvm_phase phase);
mem_subsystem_vseq vseq;
phase.raise_objection(this);
vseq = mem_subsystem_vseq::type_id::create("vseq");
vseq.src_addr = 32'h0000_1000;
vseq.dst_addr = 32'h0000_2000;
vseq.transfer_len = 256;
vseq.start(env.vsequencer);
phase.drop_objection(this);
endtask
endclass
Ten lines. The test says what — transfer 256 bytes from address 0x1000 to 0x2000. The Facade handles how — configure memory, write data, program DMA, poll completion, verify results. Different test scenarios become trivial variations:
class large_dma_test extends base_test;
`uvm_component_utils(large_dma_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
task run_phase(uvm_phase phase);
mem_subsystem_vseq vseq;
phase.raise_objection(this);
vseq = mem_subsystem_vseq::type_id::create("vseq");
vseq.src_addr = 32'h0001_0000;
vseq.dst_addr = 32'h0002_0000;
vseq.transfer_len = 4096;
vseq.start(env.vsequencer);
phase.drop_objection(this);
endtask
endclass
Same Facade, different parameters. The test writer needs to know zero protocol details.
Common Pitfalls
- Null sequencer handles — If the environment forgets to assign
vsequencer.cpu_sqrinconnect_phase, the Facade hits a null pointer when it callsp_sequencer.cpu_sqr. No compile-time error, no warning — just a runtime crash. Always verify yourconnect_phasewiring. - Forgetting
`uvm_declare_p_sequencer— Without this macro,p_sequenceris typed asuvm_sequencer_base, which has nocpu_sqrordma_sqrhandles. You need the macro to downcastp_sequencerto your virtual sequencer type. - Blocking coordination errors — If you need two agent sequences to run in parallel (e.g., CPU writes while DMA is active), use
fork...joininside the Facade. Starting sequences sequentially when they should be parallel causes deadlocks or incorrect timing. - Leaking protocol details into the Facade interface — If your Facade exposes
cas_latencyandras_to_casas top-level parameters, it is not a Facade — it is a pass-through. Expose the abstraction (timing_profile), not the raw fields. - Creating sub-sequences with
new()— Always usetype_id::create()for sub-sequences inside the Facade. This ensures Factory overrides on the sub-sequences are respected.
Scaling Up: Composing and Parameterizing Facades
A single Facade simplifies one subsystem. Real SoC verification involves multiple subsystems — memory, interrupts, clocking, power management — and tests that span several of them. The Facade pattern scales in two directions: composition (combining Facades) and parameterization (controlling Facade behavior through configuration).
Composing Multiple Facades
When a test scenario spans multiple subsystems, build a higher-level Facade that composes lower-level ones. The memory Facade handles DMA transfers. An interrupt Facade handles interrupt configuration and servicing. A system-level Facade composes both:
class sys_dma_with_interrupt_vseq extends uvm_sequence;
`uvm_object_utils(sys_dma_with_interrupt_vseq)
`uvm_declare_p_sequencer(sys_vsequencer)
bit [31:0] src_addr;
bit [31:0] dst_addr;
bit [15:0] transfer_len;
function new(string name = "sys_dma_with_interrupt_vseq");
super.new(name);
endfunction
task body();
mem_subsystem_vseq mem_vseq;
intr_config_vseq intr_vseq;
intr_wait_vseq intr_wait;
// Configure interrupt controller for DMA completion
intr_vseq = intr_config_vseq::type_id::create("intr_vseq");
intr_vseq.enable_irq = 1;
intr_vseq.irq_source = IRQ_DMA_COMPLETE;
intr_vseq.start(p_sequencer.intr_sqr);
// Run DMA transfer (using the memory subsystem Facade)
mem_vseq = mem_subsystem_vseq::type_id::create("mem_vseq");
mem_vseq.src_addr = src_addr;
mem_vseq.dst_addr = dst_addr;
mem_vseq.transfer_len = transfer_len;
mem_vseq.verify_data = 0; // Verify after interrupt
fork
mem_vseq.start(p_sequencer.mem_vsqr);
begin
intr_wait = intr_wait_vseq::type_id::create("intr_wait");
intr_wait.expected_irq = IRQ_DMA_COMPLETE;
intr_wait.timeout_ns = 10000;
intr_wait.start(p_sequencer.intr_sqr);
end
join
// Verify data after interrupt confirms completion
mem_vseq.verify_destination();
endtask
endclass
This is a Facade of Facades. The system-level virtual sequence uses the memory Facade and the interrupt Facade as building blocks. It coordinates their interaction — start DMA, wait for the completion interrupt, then verify — without exposing any protocol details. Each layer of Facade adds one level of abstraction.
Parameterized Facades
Use a configuration object to control Facade behavior without exposing subsystem details. The configuration captures intent rather than raw protocol fields:
typedef enum bit [1:0] {
TIMING_FAST = 0, // Low latency, high frequency
TIMING_NORMAL = 1, // Balanced
TIMING_SAFE = 2 // Conservative, maximum margins
} mem_timing_profile_e;
class mem_subsystem_cfg extends uvm_object;
`uvm_object_utils(mem_subsystem_cfg)
mem_timing_profile_e timing_profile = TIMING_NORMAL;
bit ecc_enable = 0;
bit verify_transfers = 1;
function new(string name = "mem_subsystem_cfg");
super.new(name);
endfunction
endclass
The Facade translates configuration intent into protocol-specific actions:
class mem_subsystem_configured_vseq extends mem_subsystem_vseq;
`uvm_object_utils(mem_subsystem_configured_vseq)
mem_subsystem_cfg cfg;
function new(string name = "mem_subsystem_configured_vseq");
super.new(name);
endfunction
virtual task configure_memory();
mem_ctrl_timing_seq mc_seq;
mc_seq = mem_ctrl_timing_seq::type_id::create("mc_seq");
case (cfg.timing_profile)
TIMING_FAST: begin
mc_seq.cas_latency = 9;
mc_seq.ras_to_cas = 10;
mc_seq.write_recovery = 6;
mc_seq.refresh_interval = 3900;
end
TIMING_NORMAL: begin
mc_seq.cas_latency = 11;
mc_seq.ras_to_cas = 13;
mc_seq.write_recovery = 8;
mc_seq.refresh_interval = 7800;
end
TIMING_SAFE: begin
mc_seq.cas_latency = 14;
mc_seq.ras_to_cas = 18;
mc_seq.write_recovery = 12;
mc_seq.refresh_interval = 7800;
end
endcase
mc_seq.start(p_sequencer.mem_ctrl_sqr);
endtask
endclass
The test says TIMING_FAST. The Facade translates that into cas_latency = 9, ras_to_cas = 10, and the rest of the protocol-specific fields. The test writer never sees a timing register. This is the Facade doing its job: absorbing subsystem complexity behind a meaningful abstraction.
Layered Facades
In practice, verification testbenches develop a natural layering:
| Layer | What It Exposes | Example |
|---|---|---|
| Agent sequences | Single-protocol operations | cpu_write_seq, dma_transfer_seq |
| Subsystem Facades | Multi-agent operations | mem_subsystem_vseq — configure + transfer + verify |
| System Facades | Use-case scenarios | sys_dma_with_interrupt_vseq — DMA + interrupt coordination |
Each layer is a Facade over the layer below it. Tests pick the layer that matches their intent. A protocol-level test runs agent sequences directly. A subsystem test uses the subsystem Facade. A system integration test uses the system Facade. The Facade pattern does not force you into one abstraction level — it gives you a stack of them.
Advanced: Facade + Factory
The Factory pattern controls what gets created. The Facade pattern controls how complex subsystems are accessed. When you combine them, the Factory can swap in different Facade implementations at test time — changing the subsystem's coordination strategy without touching the environment or the tests that use it.
Performance vs. Debug Facades
Different test phases have different needs. Regression tests want speed — skip unnecessary verification, minimize logging, run transfers back-to-back. Debug tests want visibility — log every transaction, check every intermediate result, add timing assertions. Rather than cluttering the base Facade with conditional logic, create specialized subclasses and let the Factory choose:
// Performance Facade — skip verification, minimal overhead
class mem_subsystem_perf_vseq extends mem_subsystem_vseq;
`uvm_object_utils(mem_subsystem_perf_vseq)
function new(string name = "mem_subsystem_perf_vseq");
super.new(name);
verify_data = 0; // Skip read-back verification
endfunction
// Skip memory configuration — assume already configured
virtual task configure_memory();
`uvm_info("FACADE", "Perf mode: skipping memory config", UVM_LOW)
endtask
endclass
// Debug Facade — extra logging and checks at every step
class mem_subsystem_debug_vseq extends mem_subsystem_vseq;
`uvm_object_utils(mem_subsystem_debug_vseq)
function new(string name = "mem_subsystem_debug_vseq");
super.new(name);
endfunction
virtual task write_source_data();
`uvm_info("FACADE", $sformatf(
"Debug: Writing %0d words to source 0x%08h",
transfer_len / 4, src_addr), UVM_LOW)
super.write_source_data();
`uvm_info("FACADE", "Debug: Source data write complete", UVM_LOW)
endtask
virtual task run_dma_transfer();
`uvm_info("FACADE", $sformatf(
"Debug: Starting DMA src=0x%08h dst=0x%08h len=%0d",
src_addr, dst_addr, transfer_len), UVM_LOW)
super.run_dma_transfer();
`uvm_info("FACADE", $sformatf(
"Debug: DMA complete, transfer_ok=%0b", transfer_ok), UVM_LOW)
endtask
virtual task verify_destination();
`uvm_info("FACADE", "Debug: Starting data verification", UVM_LOW)
super.verify_destination();
`uvm_info("FACADE", $sformatf(
"Debug: Verification complete, errors=%0d", errors), UVM_LOW)
endtask
endclass
Both subclasses extend the base Facade. The performance version skips steps. The debug version adds instrumentation around every step. The base Facade's interface — the parameters and the body() flow — is unchanged in both.
Swapping Facades via Factory Override
Tests choose which Facade implementation to use with a single Factory override:
class regression_test extends base_test;
`uvm_component_utils(regression_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
mem_subsystem_vseq::type_id::set_type_override(
mem_subsystem_perf_vseq::get_type());
super.build_phase(phase);
endfunction
endclass
class debug_test extends base_test;
`uvm_component_utils(debug_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
mem_subsystem_vseq::type_id::set_type_override(
mem_subsystem_debug_vseq::get_type());
super.build_phase(phase);
endfunction
endclass
The test that uses the Facade does not change at all. It still calls mem_subsystem_vseq::type_id::create("vseq"). The Factory returns the performance or debug variant. One override line controls which Facade implementation the entire test uses.
The Override Flow
sequenceDiagram
participant Test
participant Factory
participant VSeqr as Virtual Sequencer
participant Facade as mem_subsystem_perf_vseq
participant CPU as CPU Agent
participant DMA as DMA Agent
participant MC as Mem Ctrl Agent
Test->>Factory: set_type_override(mem_subsystem_vseq → perf_vseq)
Test->>Factory: create("vseq")
Factory-->>Test: returns mem_subsystem_perf_vseq
Test->>VSeqr: vseq.start(vsequencer)
Facade->>Facade: configure_memory() — skipped in perf mode
Facade->>CPU: write_source_data() via cpu_sqr
Facade->>DMA: run_dma_transfer() via dma_sqr
Facade->>Facade: verify_destination() — skipped (verify_data=0)
The Factory and Facade compose because they operate at different levels. The Factory controls which class gets instantiated. The Facade controls how the subsystem is accessed. The Factory picks the right Facade variant; the virtual sequencer routes it to the agents. Neither mechanism needs to know about the other.
Series Tie-Back
Four structural and creational patterns, four orthogonal concerns:
- Factory (Post 1) — controls what gets created. Swap implementations without changing the environment.
- Adapter (Post 5) — controls interface translation. Bridge incompatible types without modifying either side.
- Decorator (Post 6) — controls behavior addition. Layer concerns onto objects without touching their classes.
- Facade (this post) — controls access simplification. Hide multi-agent subsystem complexity behind a single virtual sequence.
These four patterns compose in real testbenches. A test can use the Factory to override a Facade (swap performance for debug mode), where the Facade coordinates agents whose analysis output is observed by Decorators (coverage subscribers), and whose register access is translated by Adapters (uvm_reg_adapter). Each pattern handles one dimension of variation. Together they give you a testbench that is flexible in what it creates, how it translates, what behavior it adds, and how simply it can be accessed.
Quick Reference
UVM Facade Components
| UVM Component | GoF Role | What It Does |
|---|---|---|
| Virtual sequence | Facade | Orchestrates sub-sequences across multiple agents behind a single body() |
| Virtual sequencer | Subsystem registry | Holds handles to agent sequencers — the wiring between Facade and subsystem |
p_sequencer | Subsystem access | Typed handle to the virtual sequencer, gives Facade access to sub-sequencers |
| Agent sequences | Subsystem components | Protocol-specific sequences that know one agent's interface |
`uvm_declare_p_sequencer | Type binding | Macro that types p_sequencer to your virtual sequencer class |
uvm_config_db | Configuration | Pass Facade configuration from test to virtual sequence via config objects |
Common Mistakes
| Mistake | Fix |
|---|---|
| Null sequencer handles at runtime | Always assign sub-sequencer handles in connect_phase. Add null checks in the Facade's body() if defensive coding is needed. |
Missing `uvm_declare_p_sequencer | Without this macro, p_sequencer is typed as uvm_sequencer_base and has no sub-sequencer handles. Add the macro to every virtual sequence. |
| Leaking protocol details into Facade API | Expose abstractions (timing_profile), not raw fields (cas_latency). If your Facade has 20 parameters, it is not simplifying anything. |
| Sequential sub-sequences that should be parallel | Use fork...join inside the Facade when agent sequences must run concurrently. Sequential start when parallel is needed causes deadlocks. |
Creating sub-sequences with new() | Always use type_id::create() inside the Facade so Factory overrides on sub-sequences are respected. |
| Monolithic Facade with no virtual methods | Make internal methods virtual so subclasses can override individual steps. A Facade with a 200-line non-virtual body() cannot be extended cleanly. |
Facade vs. Adapter vs. Decorator vs. Proxy
| Pattern | Intent | Interface Change? | Behavior Change? | UVM Example |
|---|---|---|---|---|
| Adapter | Make incompatible interfaces work together | Yes | No | uvm_reg_adapter — reg2bus() / bus2reg() |
| Decorator | Add responsibilities dynamically | No | Yes | uvm_subscriber — coverage, checking, logging |
| Facade | Simplify a complex subsystem | Reduces complexity | No | Virtual sequences — multi-agent coordination |
| Proxy | Control access to an object | No | Maybe | uvm_reg_field access policies — RW, RO, WO |
Structural Patterns Progress
| Pattern | Status | UVM Focus |
|---|---|---|
| Adapter | Done — Post 5 | uvm_reg_adapter, reg2bus() / bus2reg() |
| Decorator | Done — Post 6 | Analysis subscribers, transaction wrappers |
| Facade | This post | Virtual sequences, multi-agent coordination |
| Proxy | Coming soon | uvm_reg_field access policies |
Previous: Decorator Pattern — Adding behavior without changing the class with analysis subscribers
Next: Proxy Pattern — Controlling access with register models and sequencers
Comments (0)
Leave a Comment