APB protocol
- Define the APB signal interface: Define a module or interface for the APB signals that includes the necessary inputs and outputs, such as PCLK (APB clock), PSEL (chip select), PENABLE (enable signal), PADDR (address), PWRITE (write enable), and PWDATA (write data). You may also need to define additional signals for controlling the timing and behavior of the APB transactions.
- Implement the generator class: Implement a SystemVerilog class that generates APB transactions based on the APB protocol specifications. The class should have a method that generates a single transaction and another method that continuously generates transactions. The generate_transaction method should randomly generate values for the address and data signals, and should set the appropriate control signals (e.g. PWRITE, PSEL, PENABLE) based on the type of transaction (e.g. read or write).
- Instantiate the generator in the testbench: Instantiate the generator class in the testbench module and connect its outputs to the APB signal interface. Use a clock generator to generate the PCLK signal and set up the appropriate timing parameters for the APB transactions.
- Monitor the responses from the APB memory design: In the testbench, monitor the responses from the APB memory design to ensure that it is behaving correctly according to the AMBA protocol. You can use assertion statements or other debugging techniques to check for errors and identify any issues that may need to be addressed.
generating APB signals for a 1K x 32 memory with PADDR[9:0], PWDATA[31:0], and PRDATA[31:0] signals:
scssCopy code
module apb_memory (
input logic PCLK,
input logic PSEL,
input logic PENABLE,
input logic [9:0] PADDR,
input logic PWRITE,
input logic [31:0] PWDATA,
output logic [31:0] PRDATA
);
// Define internal memory array
logic [31:0] mem [0:1023]; // Generate APB transactions
class apb_generator;
function void generate_transaction();
automatic int unsigned addr = $random % 1024;
automatic int unsigned data = $random;
PADDR <= addr;
if (PWRITE) begin
PWDATA <= data;
mem[addr] <= data;
end else begin
PRDATA <= mem[addr];
end
PSEL <= 1;
PENABLE <= 1;
#1;
PSEL <= 0;
PENABLE <= 0;
endfunction task run();
repeat (100) begin
generate_transaction();
end
endtask
endclass // Instantiate the generator in the testbench
apb_generator generator = new();
always @(posedge PCLK) begin
if (PSEL && PENABLE) begin
if (PWRITE) begin
mem[PADDR] <= PWDATA;
end else begin
PRDATA <= mem[PADDR];
end
end
endendmodule
In this example code, the apb_memory
module includes an internal memory array that is indexed by the PADDR
signal. The apb_generator
class generates random transactions with randomly generated addresses and data, and sets the appropriate control signals (e.g. PSEL
, PENABLE
, and PWRITE
) based on the type of transaction. The testbench instantiates the generator and monitors the responses from the memory design. The always
block simulates the memory behavior by reading or writing to the internal memory array based on the PADDR
and PWRITE
signals.
Here’s an example code for an interface for APB signals in Verilog:
lessCopy code
interface apb_if #(parameter DATA_WIDTH=32)
(
input PCLK, // APB clock
input PRESETn, // Active low reset
output PSEL, // Peripheral select
output PENABLE, // Peripheral enable
output PWRITE, // Write strobe
output [DATA_WIDTH-1:0] PWDATA, // Write data
input [DATA_WIDTH-1:0] PRDATA // Read data
);
// APB Address Bus
logic [31:0] PADDR;
// APB Control Bus
logic PREADY;
// APB Interface
modport MASTER (
input PCLK,
input PRESETn,
output PSEL,
output PENABLE,
output PWRITE,
output [DATA_WIDTH-1:0] PWDATA,
input [DATA_WIDTH-1:0] PRDATA,
inout [31:0] PADDR,
inout PREADY
);
endinterface
This code defines an interface for an APB (Advanced Peripheral Bus) bus. The interface has a clock input PCLK
and an active low reset input PRESETn
. The interface also has an output PSEL
for selecting the peripheral device, an output PENABLE
for enabling the peripheral device, an output PWRITE
for signaling a write operation, an output PWDATA
for transmitting the data to be written, and an input PRDATA
for receiving the data to be read.
The interface also defines a 32-bit address bus PADDR
and a control signal PREADY
for signaling when the peripheral is ready to accept data.
The modport
definition provides a way to define different interface views. In this case, the MASTER
view provides all the inputs and outputs of the interface, as well as the address and ready signals, which are used by the master device that controls the bus.
systemverilog code for above
Here’s an example code for an interface for APB signals in SystemVerilog:
kotlinCopy code
interface apb_if #(parameter DATA_WIDTH=32)
(
input logic PCLK, // APB clock
input logic PRESETn, // Active low reset
output logic PSEL, // Peripheral select
output logic PENABLE, // Peripheral enable
output logic PWRITE, // Write strobe
output [DATA_WIDTH-1:0] PWDATA, // Write data
input [DATA_WIDTH-1:0] PRDATA, // Read data
inout [31:0] PADDR, // Address bus
inout PREADY // Control signal
);
endinterface
This code is very similar to the Verilog code above, with a few syntax differences.
In SystemVerilog, the logic
keyword is used instead of wire
or reg
to specify the data type of signals. The inout
keyword is used to specify a bidirectional signal, which can both drive and be driven by a module.
The parameterized data width is specified using the #(parameter ...)
syntax.
Other than these differences, the interface definition is the same as in Verilog, with clock and reset inputs, output signals for selecting and enabling the peripheral device, and input and output data signals.
Here’s an example SystemVerilog code for a driver module that drives an abstract transaction to the pin level, using a mailbox to communicate with the generator module:
scssCopy code
class apb_transaction;
// define the fields of the APB transaction
rand bit PWRITE;
rand bit [31:0] PADDR;
rand bit [31:0] PWDATA;
rand bit [31:0] PRDATA;
endclass
module apb_driver
#(
parameter DATA_WIDTH = 32,
parameter MAILBOX_DEPTH = 16
)
(
input logic CLK,
input logic RST,
output logic PCLK,
output logic PRESETn,
output logic PSEL,
output logic PENABLE,
output logic PWRITE,
output logic [DATA_WIDTH-1:0] PWDATA,
input logic [DATA_WIDTH-1:0] PRDATA
);
// Define the mailbox for communication with the generator
mailbox #(apb_transaction) mbox = new(1, MAILBOX_DEPTH);
// Define a handle to the current transaction
apb_transaction trans_h;
// Drive the APB signals based on the current transaction
task drive_apb_signals();
@(posedge PCLK);
PSEL <= trans_h.PADDR[31:29];
PENABLE <= 1'b1;
PWRITE <= trans_h.PWRITE;
PWDATA <= trans_h.PWDATA;
trans_h.PRDATA = PRDATA;
mbox.put(trans_h);
endtask
// Drive the current transaction to the pin level
task run_transaction(apb_transaction trans);
trans_h = trans;
PADDR = trans.PADDR;
@(posedge PCLK);
PSEL <= trans.PADDR[31:29];
PENABLE <= 1'b1;
PWRITE <= trans.PWRITE;
PWDATA <= trans.PWDATA;
mbox.put(trans_h);
repeat(2) @(posedge PCLK);
PENABLE <= 1'b0;
repeat(2) @(posedge PCLK);
mbox.get(trans_h);
trans.PRDATA = trans_h.PRDATA;
endtask
// Monitor the mailbox for incoming transactions
task run();
apb_transaction trans;
while(1) begin
mbox.get(trans);
@(posedge CLK);
drive_apb_signals();
repeat(2) @(posedge CLK);
drive_apb_signals();
end
endtask
endmodule
This code defines a SystemVerilog class apb_transaction
that represents an APB transaction, with fields for the address, write data, read data, and write enable signals. The apb_driver
module contains a mailbox for communicating with the apb_generator
module, which generates transactions and sends them to the driver. The driver module contains a task drive_apb_signals
that drives the APB signals based on the current transaction, and a task run_transaction
that drives a specific transaction to the pin level. The run
task monitors the mailbox for incoming transactions and drives them to the pin level.
The driver module has inputs for the clock and reset signals, and outputs for the APB signals, as well as a parameter for the data width and mailbox depth. The driver module drives the PSEL
and PENABLE
signals based on the address field of the transaction, and uses the mbox
mailbox to communicate with the generator.
Here’s an example SystemVerilog code for an APB monitor that collects all transactions from the interface:
cssCopy code
`ifndef APB_MONITOR_SV
`define APB_MONITOR_SV
module apb_monitor(
input clk,
input rst_n,
input apb_pclk,
input apb_preset_n,
input [31:0] apb_paddr,
input [1:0] apb_pprot,
input [2:0] apb_psel,
input [1:0] apb_ptrans,
input [3:0] apb_psize,
input [31:0] apb_pwrite_data,
output [31:0] apb_pread_data,
input apb_penable,
input apb_pready,
output apb_pslverr
); struct apb_transaction;
bit [31:0] paddr;
bit [1:0] pprot;
bit [2:0] psel;
bit [1:0] ptrans;
bit [3:0] psize;
bit [31:0] pwrite_data;
bit [31:0] pread_data;
bit penable;
bit pready;
bit pslverr;
endstruct apb_transaction apb_trans; always @(posedge clk) begin
if (!rst_n) begin
apb_trans.paddr <= 32'h0;
apb_trans.pprot <= 2'b0;
apb_trans.psel <= 3'b0;
apb_trans.ptrans <= 2'b0;
apb_trans.psize <= 4'b0;
apb_trans.pwrite_data <= 32'h0;
apb_trans.pread_data <= 32'h0;
apb_trans.penable <= 1'b0;
apb_trans.pready <= 1'b0;
apb_trans.pslverr <= 1'b0;
end
else if (apb_penable && apb_pready) begin
apb_trans.paddr <= apb_paddr;
apb_trans.pprot <= apb_pprot;
apb_trans.psel <= apb_psel;
apb_trans.ptrans <= apb_ptrans;
apb_trans.psize <= apb_psize;
apb_trans.pwrite_data <= apb_pwrite_data;
apb_trans.pread_data <= apb_pread_data;
apb_trans.penable <= apb_penable;
apb_trans.pready <= apb_pready;
apb_trans.pslverr <= apb_pslverr;
end
end initial begin
$monitor($time, " APB: addr=%h, pprot=%h, psel=%h, ptrans=%h, psize=%h, pwrite_data=%h, pread_data=%h, penable=%b, pready=%b, pslverr=%b\n",
apb_trans.paddr, apb_trans.pprot, apb_trans.psel, apb_trans.ptrans, apb_trans.psize, apb_trans.pwrite_data, apb_trans.pread_data, apb_trans.penable, apb_trans.pready, apb_trans.pslverr);
endendmodule`endif // AP
`timescale 1ns / 1ns
module apb_monitor (
input logic PCLK,
input logic PRESETn,
input logic [31:0] PADDR,
input logic [31:0] PWDATA,
input logic [31:0] PRDATA,
input logic PSEL,
input logic PENABLE,
input logic PWRITE
);
struct {
logic [31:0] addr;
logic [31:0] data;
logic write;
} transaction;
always_ff @(posedge PCLK) begin
if (!PRESETn) begin
transaction = '{0, 0, 0};
end else if (PSEL && PENABLE) begin
transaction.addr = PADDR;
transaction.data = PWRITE ? PWDATA : 'bx;
transaction.write = PWRITE;
// Collect transaction data here
end
end
endmodule
In this example, we define a struct
to hold the transaction data, including the address, data, and write flag. We then use an always_ff
block to capture transactions on the rising edge of the PCLK
signal. When PRESETn
is low, indicating a reset condition, we reset the transaction struct to all zeros. Otherwise, when PSEL
and PENABLE
are high, indicating a valid transaction, we capture the address, data, and write flag and store them in the transaction struct. You can then collect the transaction data as needed within the monitor module.
Here’s an example SystemVerilog code for an APB scoreboard that uses a mailbox to communicate between the monitor and the scoreboard. This scoreboard checks the written data versus the read data:
scssCopy code
// Define mailbox
mailbox scoreboard_mailbox;
// Define scoreboard module
module apb_scoreboard(input logic clk,
input logic rst,
input logic [31:0] apb_addr,
input logic [31:0] apb_wdata,
input logic [2:0] apb_psel,
input logic [1:0] apb_penable,
input logic [1:0] apb_pwrite,
input logic [1:0] apb_pready,
input logic [31:0] apb_rdata,
input logic [1:0] apb_pslverr); // Define variables
logic [31:0] expected_data; // Instantiate mailbox
scoreboard_mailbox = new(); // Define scoreboard process
always @(posedge clk) begin
if (rst) begin
expected_data <= 0;
end
else begin
// Check for valid write transaction
if (apb_psel == 3'b001 && apb_penable && apb_pwrite) begin
expected_data <= apb_wdata;
end
// Check for valid read transaction
else if (apb_psel == 3'b001 && apb_penable && !apb_pwrite) begin
// Compare read data with expected data
if (apb_rdata != expected_data) begin
// Send error message to monitor via mailbox
scoreboard_mailbox.put("Data mismatch detected!");
end
end
end
endendmodule
Here’s an example SystemVerilog code for adding a simple functional coverage model in a monitor and declaring bins for address and data for the APB interface:
scssCopy code
class apb_monitor extends uvm_monitor;
// Declare functional coverage model
covergroup cg;
// Declare address and data bins
// Change the range and width to match your design
bins addr_bin[$:0 to 1023];
bins data_bin[$:0 to 255];
// Add the bins to the coverage model
coverpoint addr_bin[0].size() {
bins = addr_bin;
}
coverpoint data_bin[0].size() {
bins = data_bin;
}
endgroup
// Declare the APB interface
`uvm_component_param_utils(apb_if)
uvm_analysis_port#(apb_transaction) analysis_port;
apb_if apb;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
task run_phase(uvm_phase phase);
forever begin
// Wait for a transaction on the APB interface
-> apb.apb_request;
// Record the address and data in the functional coverage model
cg.addr_bin[apb.addr].hit();
cg.data_bin[apb.data].hit();
// Send the transaction to the analysis port
analysis_port.write(apb);
end
endtask
endclass
here’s an example program block in SystemVerilog that wraps up all testbench components and adds test cases to verify an APB (Advanced Peripheral Bus) design:
systemverilogCopy code
module apb_testbench;
// Declare DUT (Design Under Test) ports
// ... // Declare testbench signals and components
// ... // Program block that contains all test cases
program automatic run_test(); // Test case 1: Write to a register and read back the value
// ... // Test case 2: Write to multiple registers and read back the values
// ... // Test case 3: Write to a read-only register and verify no change
// ... // Test case 4: Write to a write-only register and verify no read-back
// ... // Test case 5: Test burst transfer
// ... // Test case 6: Test bus errors (e.g., slave not responding)
// ... endprogram // Instantiate the DUT and connect the ports to testbench signals
// ... // Generate clock and reset signals
// ... // Run the test program
initial begin
run_test();
$finish;
endendmodule
In this example, the run_test()
program block contains all the test cases to be executed. Each test case can be implemented using tasks or functions within the program block, depending on the complexity of the test. The automatic
keyword indicates that the program block will be executed automatically when the simulation starts.
Before running the test program, the DUT is instantiated and its ports are connected to the testbench signals. Clock and reset signals are also generated to control the timing of the test cases. Finally, the initial
block is used to start the test program and call the run_test()
program block. The simulation will terminate when all test cases have been executed and $finish
is called.
The AMBA protocol family includes three main protocols developed by ARM Holdings:
- Advanced Peripheral Bus (APB) — a simple and low-cost interface designed for connecting low-bandwidth peripherals to a system-on-chip (SoC).
- Advanced High-performance Bus (AHB) — a high-performance bus protocol designed for connecting on-chip modules in SoCs, including memory, DMA controllers, and high-speed peripherals.
- Advanced eXtensible Interface (AXI) — a high-performance, high-bandwidth protocol designed for connecting high-performance modules in SoCs, including processors, memories, and high-speed peripherals.
In addition to these three protocols, ARM Holdings has also developed a number of other protocols and specifications that are related to the AMBA family, including:
- AHB-Lite — a simplified version of the AHB protocol that is designed for use with low-cost and low-power SoCs.
- AXI-Lite — a simplified version of the AXI protocol that is designed for use with low-cost and low-power peripherals.
- ACE — a protocol that combines the functionality of the AXI and AHB protocols to enable coherent data transfers between multiple processors and memory systems.
- AXI4-Stream — a protocol designed for streaming data between modules in an SoC, including audio and video streams.
- APB4 — a newer version of the APB protocol that is optimized for low-power and low-latency applications
These protocols and specifications are widely used in the design of SoCs for a variety of applications, including mobile devices, networking equipment, and automotive systems.
`timescale 1ns / 1ns // Define timescale for simulation
module top_tb; // Define testbench module
// Declare ports and signals for the DUT and testbench
// Example:
// interface_test i_test();
// apb_memory_dut apb_mem_dut();
// logic clock, reset;
// Instantiate DUT and interface test
// Example:
// i_test i_test_inst();
// apb_mem_dut apb_mem_dut_inst();
// Create clock and reset signals
// Example:
// always #5 clock = ~clock;
// initial begin
// reset = 1'b1;
// #10;
// reset = 1'b0;
// end
// Connect ports and signals
// Example:
// i_test_inst.clock = clock;
// i_test_inst.reset = reset;
// apb_mem_dut_inst.clock = clock;
// apb_mem_dut_inst.reset = reset;
// i_test_inst.interface_signal = apb_mem_dut_inst.interface_signal;
// Add any stimulus or test cases
// Example:
// initial begin
// // Write and read from memory using APB protocol
// // Check for expected results
// end
endmodule
`timescale 1ns / 1ps
module top_tb;
// Declare ports and signals for the DUT and testbench
logic clock;
logic reset;
interface apb_if;
logic PCLK;
logic PRESETn;
logic PSEL;
logic PENABLE;
logic [31:0] PADDR;
logic [31:0] PWDATA;
logic [31:0] PRDATA;
logic PWRITE;
logic PSTRB;
endinterface
// Instantiate interface test and APB memory DUT
interface_test i_test();
apb_memory_dut apb_mem_dut();
// Connect ports and signals
assign apb_if.PCLK = clock;
assign apb_if.PRESETn = reset;
assign i_test.apb_if = apb_if;
assign apb_mem_dut.apb_if = apb_if;
// Generate clock and reset
always #5 clock = ~clock;
initial begin
reset = 1'b1;
#10;
reset = 1'b0;
end
endmodule