top of page
Search

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:

  1. Setup Phase: Master places address, write data, and control signals. PSEL and PENABLE are low.

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

  1. Visit EDAPlayground (edaplayground.com)

  2. Select 'SystemVerilog' as the language

  3. Copy all the code from Step 1 to Step 9 into the editor

  4. Select a simulator (VCS or Xcelium recommended)

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

 
 
 

Recent Posts

See All
UVM Sequences

let's break down the UVM sequence code in that format: ### UVM Sequence Tutorial for Beginners: ```systemverilog class my_sequence...

 
 
 

Comments


bottom of page