APB Protocol Using UVM: A Beginner's Guide
- Mar 7
- 4 min read
Welcome to this comprehensive guide on implementing the AMBA APB (Advanced Peripheral Bus) protocol using UVM (Universal Verification Methodology). Whether you're new to verification or looking to deepen your understanding of APB protocol verification, this tutorial will walk you through everything step by step with complete, working code examples.
What is the APB Protocol?
The AMBA APB (Advanced Microcontroller Bus Architecture Advanced Peripheral Bus) is a simple, low-bandwidth bus protocol used for connecting peripherals in ARM-based systems. It's designed for low-power, low-complexity peripheral connections and is one of the most commonly used protocols in SoC (System-on-Chip) design.
Key Characteristics of APB:
Simple two-cycle handshake protocol
Supports read and write transactions
No burst transactions (single transfers only)
Low power consumption
Configurable data width (8, 16, 32 bits)
APB Protocol Signals
Before diving into the code, let's understand the key signals in the APB protocol:
PCLK: Clock signal
PRESETn: Active low reset signal
PADDR: Address bus (typically 32 bits)
PWRITE: Write enable signal (1=write, 0=read)
PSEL: Peripheral select signal
PENABLE: Enable signal (indicates second cycle of transaction)
PWDATA: Write data bus
PRDATA: Read data bus
PREADY: Ready signal from slave
PSLVERR: Slave error signal
APB Transaction Flow
APB transactions follow a simple two-cycle handshake:
Setup Phase: Master places address, write data, and control signals. PSEL and PENABLE are low.
Access Phase: PSEL goes high, then PENABLE goes high. Slave responds with PREADY and PRDATA (for reads).
Complete UVM Implementation
Now let's implement a complete APB verification environment using UVM. This code is ready to copy-paste into EDAPlayground.
Step 1: APB Interface Definition
First, define the APB interface with all the signals:
interface apb_if(input clk, input rst_n); logic [31:0] paddr; logic pwrite; logic psel; logic penable; logic [31:0] pwdata; logic [31:0] prdata; logic pready; logic pslverr; // Clocking block for synchronization clocking cb @(posedge clk); output paddr, pwrite, psel, penable, pwdata; input prdata, pready, pslverr; endclocking // Modport for master modport master(clocking cb, input clk, rst_n); // Modport for slave modport slave(input paddr, pwrite, psel, penable, pwdata, clk, rst_n, output prdata, pready, pslverr); endinterface
Step 2: APB Transaction Class
Define the transaction class that represents an APB transaction:
class apb_transaction extends uvm_sequence_item; `uvm_object_utils(apb_transaction) rand logic [31:0] addr; rand logic [31:0] data; rand logic write; logic [31:0] read_data; logic error; function new(string name = "apb_transaction"); super.new(name); endfunction function string convert2string(); return $sformatf("APB Transaction: addr=0x%h, data=0x%h, write=%b, read_data=0x%h, error=%b", addr, data, write, read_data, error); endfunction endclass
Step 3: APB Driver
The driver converts transactions into actual signal toggles:
class apb_driver extends uvm_driver #(apb_transaction); `uvm_component_utils(apb_driver) virtual apb_if.master vif; function new(string name, uvm_component parent); super.new(name, parent); endfunction function void build_phase(uvm_build_phase phase); super.build_phase(phase); if (!uvm_config_db#(virtual apb_if.master)::get(this, "", "vif", vif)) `uvm_fatal("NO_VIF", "Virtual interface not found") endfunction task run_phase(uvm_run_phase phase); forever begin seq_item_port.get_next_item(req); drive_transaction(req); seq_item_port.item_done(); end endtask task drive_transaction(apb_transaction txn); // Setup phase vif.cb.paddr <= txn.addr; vif.cb.pwdata <= txn.data; vif.cb.pwrite <= txn.write; vif.cb.psel <= 1'b0; vif.cb.penable <= 1'b0; @(vif.cb); // Access phase vif.cb.psel <= 1'b1; @(vif.cb); vif.cb.penable <= 1'b1; @(vif.cb); // Wait for ready while (!vif.cb.pready) @(vif.cb); // Capture read data if (!txn.write) txn.read_data = vif.cb.prdata; txn.error = vif.cb.pslverr; // Deassert signals vif.cb.psel <= 1'b0; vif.cb.penable <= 1'b0; @(vif.cb); endtask endclass
Step 4: APB Monitor
The monitor observes transactions on the bus:
class apb_monitor extends uvm_monitor; `uvm_component_utils(apb_monitor) virtual apb_if.slave vif; uvm_analysis_port #(apb_transaction) ap; function new(string name, uvm_component parent); super.new(name, parent); ap = new("ap", this); endfunction function void build_phase(uvm_build_phase phase); super.build_phase(phase); if (!uvm_config_db#(virtual apb_if.slave)::get(this, "", "vif", vif)) `uvm_fatal("NO_VIF", "Virtual interface not found") endfunction task run_phase(uvm_run_phase phase); forever begin apb_transaction txn = apb_transaction::type_id::create("txn"); wait_for_transaction(txn); ap.write(txn); end endtask task wait_for_transaction(apb_transaction txn); // Wait for PSEL wait (vif.psel); txn.addr = vif.paddr; txn.write = vif.pwrite; txn.data = vif.pwdata; // Wait for PENABLE @(posedge vif.clk); wait (vif.penable); @(posedge vif.clk); // Capture read data if (!txn.write) txn.read_data = vif.prdata; txn.error = vif.pslverr; // Wait for PSEL to deassert wait (!vif.psel); endtask endclass
Step 5: APB Agent
The agent combines the driver and monitor:
class apb_agent extends uvm_agent; `uvm_component_utils(apb_agent) apb_driver driver; apb_monitor monitor; uvm_sequencer #(apb_transaction) sequencer; function new(string name, uvm_component parent); super.new(name, parent); endfunction function void build_phase(uvm_build_phase phase); super.build_phase(phase); monitor = apb_monitor::type_id::create("monitor", this); if (get_is_active() == UVM_ACTIVE) begin driver = apb_driver::type_id::create("driver", this); sequencer = uvm_sequencer#(apb_transaction)::type_id::create("sequencer", this); end endfunction function void connect_phase(uvm_connect_phase phase); super.connect_phase(phase); if (get_is_active() == UVM_ACTIVE) driver.seq_item_port.connect(sequencer.seq_item_export); endfunction endclass
Step 6: APB Sequence
Define a simple sequence to generate transactions:
class apb_sequence extends uvm_sequence #(apb_transaction); `uvm_object_utils(apb_sequence) function new(string name = "apb_sequence"); super.new(name); endfunction task body(); repeat(10) begin req = apb_transaction::type_id::create("req"); start_item(req); if (!req.randomize()) `uvm_fatal("RAND_FAIL", "Randomization failed") finish_item(req); `uvm_info("SEQ", req.convert2string(), UVM_MEDIUM) end endtask endclass
Step 7: APB Environment
Create the environment that instantiates the agent:
class apb_env extends uvm_env; `uvm_component_utils(apb_env) apb_agent agent; function new(string name, uvm_component parent); super.new(name, parent); endfunction function void build_phase(uvm_build_phase phase); super.build_phase(phase); agent = apb_agent::type_id::create("agent", this); endfunction endclass
Step 8: APB Test
Create a test that runs the sequence:
class apb_test extends uvm_test; `uvm_component_utils(apb_test) apb_env env; function new(string name, uvm_component parent); super.new(name, parent); endfunction function void build_phase(uvm_build_phase phase); super.build_phase(phase); env = apb_env::type_id::create("env", this); endfunction task run_phase(uvm_run_phase phase); apb_sequence seq = apb_sequence::type_id::create("seq"); phase.raise_objection(this); seq.start(env.agent.sequencer); phase.drop_objection(this); endtask endclass
Step 9: Top-Level Testbench
Create the top-level testbench module:
module top; logic clk, rst_n; // Clock generation initial begin clk = 0; forever #5 clk = ~clk; end // Reset generation initial begin rst_n = 0; #20 rst_n = 1; end // Instantiate interface apb_if apb_intf(clk, rst_n); // Configure and run UVM initial begin uvm_config_db#(virtual apb_if.master)::set(null, "uvm_test_top.env.agent.driver", "vif", apb_intf.master); uvm_config_db#(virtual apb_if.slave)::set(null, "uvm_test_top.env.agent.monitor", "vif", apb_intf.slave); run_test("apb_test"); end endmodule
How to Run on EDAPlayground
Visit EDAPlayground (edaplayground.com)
Select 'SystemVerilog' as the language
Copy all the code from Step 1 to Step 9 into the editor
Select a simulator (VCS or Xcelium recommended)
Click 'Run' to execute the simulation
Key Takeaways
APB is a simple, two-cycle handshake protocol ideal for peripheral connections
UVM provides a structured framework for verification with reusable components
The driver converts high-level transactions into low-level signal toggles
The monitor observes the bus and collects transactions for analysis
Sequences generate stimulus in a controlled, repeatable manner
Next Steps
Now that you understand the basics of APB protocol verification with UVM, consider exploring:
Adding functional coverage to measure test completeness
Implementing assertions for protocol compliance checking
Creating a scoreboard to verify transaction correctness
Exploring other AMBA protocols like AXI and AHB
Happy verifying! Feel free to reach out if you have any questions about APB protocol or UVM verification.
Comments