AMBA APB is a widely used bus protocol by the company ARM Holdings, typically used to access control registers in a design.
The APB is part of the AMBA 3 protocol family. It provides a low-cost interface that is optimized for minimal power consumption and reduced interface complexity. The APB interfaces to any peripherals that are low-bandwidth and do not require the high performance of a pipelined bus interface. The APB has unpipelined protocol.
All signal transitions are only related to the rising edge of the clock to enable the integration of APB peripherals easily into any design flow. Every transfer takes at least two cycles.
Protocol Basics
Signals
| Signal | Description |
|---|---|
| PCLK | Clock. The rising edge of PCLK times all transfers on the APB. |
| PRESETn | Reset. The APB reset signal is active LOW. This signal is normally connected directly to the system bus reset signal. |
| PADDR | Address. This is the APB address bus. It can be up to 32 bits wide and is driven by the peripheral bus bridge unit. |
| PSELx | Select. The APB bridge unit generates this signal to each peripheral bus slave. It indicates that the slave device is selected and that a data transfer is required. There is a PSELx signal for each slave. |
| PENABLE | Enable. This signal indicates the second and subsequent cycles of an APB transfer. |
| PWRITE | Direction. This signal indicates an APB write access when HIGH and an APB read access when LOW. |
| PWDATA | Write data. This bus is driven by the peripheral bus bridge unit during write cycles when PWRITE is HIGH. This bus can be up to 32 bits wide |
| PREADY | Ready. The slave uses this signal to extend an APB transfer |
| PRDATA | Read Data. The selected slave drives this bus during read cycles when PWRITE is LOW. This bus can be up to 32-bits wide. |
| PSLVERR | This signal indicates a transfer failure. APB peripherals are not required to support the PSLVERR pin. This is true for both existing and new APB peripheral designs. Where a peripheral does not include this pin then the appropriate input to the APB bridge is tied LOW. |

The first phase is called setup phase where an APB master drives paddr, pwrite, penable, psel and pwdata regardless of whether slave is ready which is indicated by pready. Note that penable should always be driven low in the setup phase.
The second cycle is called access phase and is the time when write data is either captured by the slave or read data is provided by the slave. The slave can extend either phase by pulling pready low thereby indicating to the master that it is not ready to accept any transfer at the moment, and is called a wait state. The master should drive penable high in the access phase.

The complete APB specification can be downloaded here.
Interface
The interface is used to connect between design blocks that use APB protocol or connect with a testbench. Hence the interface should have all signals supported within APB protocol specification. The clocking block is primarily used by testbench to sample and drive signals relative to a clocking event, which in this case is the positive edge of pclk.
interface apb_if (input pclk);
logic presetn;
logic [31:0] paddr;
logic psel;
logic penable;
logic pwrite;
logic [31:0] pwdata;
logic pready;
logic [31:0] prdata;
logic pslverr;
clocking cb @ (posedge pclk);
default input #1step output #1ns;
input pready, prdata, pslverr;
output paddr, psel, penable, pwrite, pwdata;
endclocking
endinterface
APB Sequence Item
APB signals can be grouped into either control or data signals. pwdata and prdata are data signals while rest of it decide how an APB master should initiate a transfer. Based on this protocol, there are only 4 signals that really need to be driven with random value which are paddr, pwrite and the two data signals. All other signals has to be driven with a fixed value based on protocol definition and hence do not need to be randomized as a transaction object.
This seqeunce item will be started on an APB sequencer, which ultimately gets driven onto the bus by an APB driver.
class apb_pkt extends uvm_sequence_item;
function new(string name = "apb_pkt");
super.new(name);
endfunction
rand bit [15:0] m_addr;
rand bit [31:0] m_wdata;
bit [31:0] m_rdata;
rand bit m_write;
`uvm_object_utils_begin(apb_pkt)
`uvm_field_int (m_addr, UVM_DEFAULT)
`uvm_field_int (m_wdata, UVM_DEFAULT)
`uvm_field_int (m_rdata, UVM_DEFAULT)
`uvm_field_int (m_write, UVM_DEFAULT)
`uvm_object_utils_end
virtual function string convert2string();
return $sformatf("addr=0x%0h write=0x%0h wdata=0x%0h rdata=0x%0h",
m_addr, m_write, m_wdata, m_rdata);
endfunction
endclass
APB Configuration Object
class apb_cfg extends uvm_object;
`uvm_object_utils(apb_cfg)
function new(string name = "apb_cfg");
super.new(name);
endfunction
virtual apb_if m_apb_vif;
bit m_vld_val_only;
endclass
APB Monitor
A monitor is expected to run as long as simulation is active and hence is launched as a forever block during run_phase. Because the phase objection is neither raised nor dropped, the run_phase is allowed to end whenever the test sequence ends. At every positive edge of the clock, the monitor figures out if there is a valid transaction, and captures it into a transaction object. After the transfer is complete, the transaction object is written out through its analysis port using the write method.
class apb_monitor extends uvm_monitor;
`uvm_component_utils(apb_monitor)
function new(string name="apb_monitor", uvm_component parent=null);
super.new(name, parent);
endfunction
apb_cfg m_cfg;
virtual apb_if m_vif;
uvm_analysis_port #(apb_pkt) m_analysis_port;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
m_analysis_port = new("m_analysis_port", this);
m_vif = m_cfg.m_apb_vif;
endfunction
virtual task main_phase(uvm_phase phase);
super.main_phase(phase);
forever begin
@(m_vif.cb);
if (m_vif.psel & m_vif.penable & m_vif.cb.pready & m_vif.presetn) begin
apb_pkt m_apb_pkt = apb_pkt::type_id::create("m_apb_pkt");
m_apb_pkt.m_addr = m_vif.paddr;
m_apb_pkt.m_write = m_vif.pwrite;
if (m_cfg.m_vld_val_only) begin
if (m_apb_pkt.m_write)
m_apb_pkt.m_wdata = m_vif.pwdata;
else
m_apb_pkt.m_rdata = m_vif.prdata;
end else begin
m_apb_pkt.m_wdata = m_vif.pwdata;
m_apb_pkt.m_rdata = m_vif.prdata;
end
m_analysis_port.write(m_apb_pkt);
end
end
endtask
endclass
APB Driver
class apb_driver extends uvm_driver #(apb_pkt);
`uvm_component_utils(apb_driver)
function new(string name="apb_driver", uvm_component parent=null);
super.new(name, parent);
endfunction
virtual apb_if m_vif;
apb_cfg m_cfg;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
m_vif = m_cfg.m_apb_vif;
endfunction
virtual task reset_phase(uvm_phase phase);
super.reset_phase(phase);
phase.raise_objection(this);
m_vif.cb.paddr <= 0;
m_vif.cb.psel <= 0;
m_vif.cb.pwdata <= 0;
m_vif.cb.penable <= 0;
m_vif.cb.pwrite <= 0;
phase.drop_objection(this);
endtask
virtual task main_phase(uvm_phase phase);
super.main_phase(phase);
forever begin
seq_item_port.get_next_item(req);
drive_item(req);
seq_item_port.item_done();
end
endtask
virtual task drive_item(REQ req);
m_vif.cb.paddr <= req.m_addr;
m_vif.cb.psel <= 1;
m_vif.cb.pwrite <= req.m_write;
m_vif.cb.pwdata <= req.m_wdata;
m_vif.cb.penable <= 0;
// At the next posedge, raise penable high
@(m_vif.cb);
m_vif.cb.penable <= 1;
// Hold addr/data until pready is 1
do begin
@(m_vif.cb);
end while (! m_vif.cb.pready);
if (! req.m_write)
req.m_rdata = m_vif.cb.prdata;
endtask
endclass
APB Agent
class apb_agent extends uvm_agent;
`uvm_component_utils(apb_agent)
function new(string name="apb_agent", uvm_component parent=null);
super.new(name, parent);
endfunction
apb_cfg m_cfg;
apb_driver m_drv;
apb_monitor m_mon;
uvm_sequencer #(apb_pkt) m_seqr;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(apb_cfg)::get(this, "*", "m_apb_cfg", m_cfg))
`uvm_fatal(get_type_name(), $sformatf("[CFG] Did not find m_apb_cfg in config_db"))
if (get_is_active()) begin
m_drv = apb_driver::type_id::create("m_drv", this);
m_seqr = uvm_sequencer#(apb_pkt)::type_id::create("m_seqr", this);
end
m_mon = apb_monitor::type_id::create("m_mon", this);
// Configuration might be needed to build it
m_drv.m_cfg = m_cfg;
m_mon.m_cfg = m_cfg;
endfunction
virtual function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
if (get_is_active) begin
m_drv.seq_item_port.connect(m_seqr.seq_item_export);
end
endfunction
endclass
Testbench
Scoreboard
class base_scoreboard extends uvm_scoreboard;
`uvm_component_utils(base_scoreboard)
function new(string name = "base_scoreboard", uvm_component parent=null);
super.new(name, parent);
endfunction
uvm_analysis_imp #(apb_pkt, base_scoreboard) m_analysis_imp;
apb_pkt m_apb_pkt_q[$];
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
m_analysis_imp = new("m_analysis_imp", this);
endfunction
virtual function write(apb_pkt l_apb_pkt);
`uvm_info(get_type_name(), $sformatf("[write] Got packet %s", l_apb_pkt.convert2string()), UVM_MEDIUM)
m_apb_pkt_q.push_back(l_apb_pkt);
endfunction
endclass
Environment
class base_env extends uvm_env;
`uvm_component_utils(base_env)
function new(string name = "base_env", uvm_component parent=null);
super.new(name, parent);
endfunction
apb_agent m_apb_agent;
base_scoreboard m_base_scbd;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
m_apb_agent = apb_agent::type_id::create("m_apb_agent", this);
m_base_scbd = base_scoreboard::type_id::create("m_base_scbd", this);
endfunction
virtual function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
m_apb_agent.m_mon.m_analysis_port.connect(m_base_scbd.m_analysis_imp);
endfunction
endclass
Test
The test class will release the agent from reset, and start a basic sequence that will initiate 10 transactions to the APB driver. The test also instantiates the base environment which contains our APB agent. The agent can be configured using a configuration object that is first created, configured and set into the config_db. Since the agent requires an interface to drive out transactions, the virtual interface is packaged into the configuration object so that the agent gets it after it takes the configuration object from config_db.
This is just a neat way to pass everything required for the agent to operate through a single object. However, you can also keep them separate and set both virtual interface and the configuration object as separate entities in the config_db using two separate keys.
class base_test extends uvm_test;
`uvm_component_utils(base_test)
function new(string name = "base_test", uvm_component parent=null);
super.new(name, parent);
endfunction
virtual apb_if m_apb_vif;
apb_cfg m_apb_cfg;
apb_seq m_apb_seq;
base_env m_env;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
// Get virtual interface from tb_top
if (!uvm_config_db #(virtual apb_if)::get(this, "", "m_apb_if", m_apb_vif))
`uvm_fatal(get_type_name(), $sformatf("[CFG] Did not find m_apb_if in config_db"))
// Set up APB agent configuration, assign virtual interface handle and set in config_db
m_apb_cfg = apb_cfg::type_id::create("m_apb_cfg");
m_apb_cfg.m_apb_vif = m_apb_vif;
uvm_config_db #(apb_cfg)::set(this, "*apb_agent*", "m_apb_cfg", m_apb_cfg);
// Create base env, test sequence
m_apb_seq = apb_seq::type_id::create("m_apb_seq");
m_env = base_env::type_id::create("m_env", this);
endfunction
virtual task reset_phase(uvm_phase phase);
super.reset_phase(phase);
phase.raise_objection(this);
m_apb_vif.presetn = 0;
repeat(5) @ (m_apb_vif.cb);
m_apb_vif.presetn = 1;
repeat(5) @ (m_apb_vif.cb);
phase.drop_objection(this);
endtask
virtual task main_phase(uvm_phase phase);
super.main_phase(phase);
phase.raise_objection(this);
m_apb_seq.start(m_env.m_apb_agent.m_seqr);
phase.drop_objection(this);
endtask
endclass
Sequence
The sequence will be executed on our APB sequencer in the agent which will send the uvm_sequence_item to the APB driver.
class apb_seq extends uvm_sequence;
`uvm_object_utils(apb_seq)
function new(string name = "apb_seq");
super.new(name);
endfunction
virtual task body();
`uvm_info(get_type_name(), $sformatf("Start sequence"), UVM_MEDIUM)
repeat (10) begin
apb_pkt m_apb_pkt = apb_pkt::type_id::create("m_apb_pkt");
start_item(m_apb_pkt);
m_apb_pkt.randomize();
finish_item(m_apb_pkt);
end
`uvm_info(get_type_name(), $sformatf("End sequence"), UVM_MEDIUM)
endtask
endclass
Testbench Top
For the purposes of this example, let us just instantiate the interface, create a clock and drive APB master inputs with some valid values. There is no self checks enabled in this testbench for which we would need a valid slave agent or design that would respond to our APB master agent.
module tb;
bit clk;
// Create a clock
always #10 clk = ~clk;
// Instantiate the interface
apb_if m_apb_if ( .pclk (clk));
// Drive input signals to APB master agent with some value
assign m_apb_if.pslverr = 0;
// Drive pready after some random cycles
initial begin
m_apb_if.pready <= 0;
forever begin
repeat ($urandom_range(2,6)) @ (posedge clk);
m_apb_if.pready <= ~m_apb_if.pready;
end
end
// Drive some valid values after every N clocks
initial begin
m_apb_if.prdata <= 0;
forever begin
repeat ($urandom_range(2,6)) @ (posedge clk);
m_apb_if.prdata <= $random;
end
end
// Set APB interface in config_db and run the test
initial begin
uvm_config_db #(virtual apb_if)::set(null, "uvm_test_top", "m_apb_if", m_apb_if);
run_test("base_test");
end
endmodule

UVM_INFO @ 0: reporter [RNTST] Running test base_test... UVM_INFO testbench.sv(182) @ 190: uvm_test_top.m_env.m_apb_agent.m_seqr@@m_apb_seq [apb_seq] Start sequence UVM_INFO testbench.sv(208) @ 270: uvm_test_top.m_env.m_base_scbd [base_scoreboard] [write] Got packet addr=0x3ebd write=0x0 wdata=0x8d290875 rdata=0x8484d609 UVM_INFO testbench.sv(208) @ 310: uvm_test_top.m_env.m_base_scbd [base_scoreboard] [write] Got packet addr=0xa834 write=0x1 wdata=0xf588a117 rdata=0x8484d609 UVM_INFO testbench.sv(208) @ 350: uvm_test_top.m_env.m_base_scbd [base_scoreboard] [write] Got packet addr=0x7b22 write=0x0 wdata=0xc647d25b rdata=0x8484d609 UVM_INFO testbench.sv(208) @ 450: uvm_test_top.m_env.m_base_scbd [base_scoreboard] [write] Got packet addr=0xb787 write=0x0 wdata=0xff669c41 rdata=0x6b97b0d UVM_INFO testbench.sv(208) @ 490: uvm_test_top.m_env.m_base_scbd [base_scoreboard] [write] Got packet addr=0x5d63 write=0x1 wdata=0xa0e4fec9 rdata=0x6b97b0d UVM_INFO testbench.sv(208) @ 530: uvm_test_top.m_env.m_base_scbd [base_scoreboard] [write] Got packet addr=0x6cb5 write=0x0 wdata=0xaac2f9f3 rdata=0x46df998d UVM_INFO testbench.sv(208) @ 610: uvm_test_top.m_env.m_base_scbd [base_scoreboard] [write] Got packet addr=0xe57f write=0x1 wdata=0x1d008dbf rdata=0xb2c28465 UVM_INFO testbench.sv(208) @ 650: uvm_test_top.m_env.m_base_scbd [base_scoreboard] [write] Got packet addr=0xc7c0 write=0x1 wdata=0xf79dba2d rdata=0xb2c28465 UVM_INFO testbench.sv(208) @ 690: uvm_test_top.m_env.m_base_scbd [base_scoreboard] [write] Got packet addr=0x1377 write=0x1 wdata=0x3a9a7f3d rdata=0xb2c28465 UVM_INFO testbench.sv(208) @ 810: uvm_test_top.m_env.m_base_scbd [base_scoreboard] [write] Got packet addr=0xc8a6 write=0x1 wdata=0xe5f6dcef rdata=0xf3e301 UVM_INFO testbench.sv(189) @ 810: uvm_test_top.m_env.m_apb_agent.m_seqr@@m_apb_seq [apb_seq] End sequence UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_report_server.svh(847) @ 810: reporter [UVM/REPORT/SERVER] --- UVM Report Summary ---