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

SignalDescription
PCLKClock. The rising edge of PCLK times all transfers on the APB.
PRESETnReset. The APB reset signal is active LOW. This signal is normally connected directly to the system bus reset signal.
PADDRAddress. This is the APB address bus. It can be up to 32 bits wide and is driven by the peripheral bus bridge unit.
PSELxSelect. 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.
PENABLEEnable. This signal indicates the second and subsequent cycles of an APB transfer.
PWRITEDirection. This signal indicates an APB write access when HIGH and an APB read access when LOW.
PWDATAWrite 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
PREADYReady. The slave uses this signal to extend an APB transfer
PRDATARead Data. The selected slave drives this bus during read cycles when PWRITE is LOW. This bus can be up to 32-bits wide.
PSLVERRThis 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
Output
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 ---
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5