Now, let's dive into some practical Verilog. The goal is to understand what an APB interface is and the concept of register settings. So, let's get started.
Counter RTL design
First, let's design a simple counter.
//top.v
`timescale 1ns/10ps
module top();
parameter period_clk = 10;
reg clk;
reg resetn;
wire [7:0] test;
//clk
always #(period_clk*0.5) clk = ~clk;
//instance
counter u_counter (
.clk (clk )
,.resetn (resetn)
,.test (test )
);
initial begin
clk = 1'b0;
resetn = 1'b0;
#(period_clk);
resetn = 1'b1;
#1000 $finish;
end
initial begin
$dumpfile ("test.vcd");
$dumpvars();
end
endmodule
//counter.v
module counter (
input wire clk
,input wire resetn
,output wire [ 7:0] test
);
reg [ 7:0] cnt;
always @(posedge clk or negedge resetn) begin
if (!resetn) cnt <= 8'h0;
else if (cnt == 8'd10) cnt <= 8'h0;
else cnt <= (cnt + 1'b1);
end
assign test = cnt;
endmoduleIn Testbench, a clock is created and reset is released after a certain amount of time after the simulation starts.
In the Counter module, when reset is released, the cnt value increases in synchronization with the clock. And when it reaches 0x10, it is initialized to 0x0. Let's check the simulation results.
This is the signal waveform of Top.v. You can see that the test value assigned to the counter's cnt changes as desired.
Counter modification
But, can't the user change the value that initializes cnt (0x10 above) as they wish? Let's modify the code as follows.
//top.v
`timescale 1ns/10ps
module top();
parameter period_clk = 10;
reg clk;
reg resetn;
reg [7:0] value; //additional
wire [7:0] test;
//clk
always #(period_clk*0.5) clk = ~clk;
//instance
counter u_counter (
.clk (clk )
,.resetn (resetn)
,.value (value ) //additional
,.test (test )
);
initial begin
clk = 1'b0;
resetn = 1'b0;
value = 8'h5; //additional
#(period_clk);
resetn = 1'b1;
#100 //additional
value = 8'hA; //additional
#100 $finish;
end
initial begin
$dumpfile ("test.vcd");
$dumpvars();
end
endmodule
//counter.v
module counter (
input wire clk
,input wire resetn
,input wire [ 7:0] value //additional
,output wire [ 7:0] test
);
reg [ 7:0] cnt;
always @(posedge clk or negedge resetn) begin
if (!resetn) cnt <= 8'h0;
else if (cnt == value) cnt <= 8'h0; //modified
else cnt <= (cnt + 1'b1);
end
assign test = cnt;
endmoduleNow, cnt is initialized by the input value, not a fixed value. Let's check the simulation results.
Do you see the difference? You can change how the module works by adjusting the Value value.
CPU control, not testbench
In the example above, the module's operational behavior was altered by changing the required signals in the testbench. In an actual chip, the CPU performs this role. The CPU issues commands to each module (IP) via a bus, and among buses, it controls the IP by issuing commands via the AMBA bus, a standardized bus.
However, each IP requires different signals. The counter designed above requires a value signal to initialize cnt, while a UART, for example, would require a signal related to the baud rate.
As such, although the signals required for each IP are different, the bus connecting the CPU and IP uses the standardized AMBA Bus, so the signals need to be converted to suit the IP.
APB interface
That's why we need the APB interface, which we'll be studying from now on. If the IP is APB bus-based, there's an APB interface; if it's AHB bus-based, there's an AHB interface; and if it's AXI bus-based, there's an AXI interface. We'll look at the simplest APB bus-based interface.
APB BFM (Bus functional Model)
The CPU issues commands to the IP on the chip. Therefore, designing and verifying interfaces for use on the IP requires a CPU to issue the commands. However, it's inefficient to acquire a CPU solely for verification purposes. Therefore, interface verification can be accomplished through a BFM module that can function as a CPU.
module apb_bfm (
input wire pclk
,input wire presetn
,output reg psel
,output reg penable
,output reg [31:0] paddr
,output reg pwrite
,output reg [31:0] pwdata
,input wire pready
,input wire [31:0] prdata
,input wire pslverr
);
parameter delay = 1;
initial begin
psel = 1'b0;
penable = 1'b0;
paddr = 32'b0;
pwrite = 1'b0;
pwdata = 32'b0;
end
//--------------------------------------------------
// task : APB single write
//--------------------------------------------------
task apb_write;
input [31: 0] addr;
input [31: 0] data;
begin
wait (pready == 1'b1);
@(posedge pclk);
psel <= #(delay) 1'b1 ;
penable <= #(delay) 1'b0 ;
paddr <= #(delay) addr ;
pwrite <= #(delay) 1'b1 ; // WRITE
pwdata <= #(delay) data ;
@(posedge pclk);
psel <= #(delay) 1'b1 ;
penable <= #(delay) 1'b1 ;
paddr <= #(delay) addr ;
pwrite <= #(delay) 1'b1 ; // WRITE
pwdata <= #(delay) data ;
wait (pready == 1'b1);
@(posedge pclk);
psel <= #(delay) 1'b0 ;
penable <= #(delay) 1'b0 ;
paddr <= #(delay) 32'h0 ;
pwrite <= #(delay) 1'b0 ;
pwdata <= #(delay) 32'h0 ;
end
endtask
//--------------------------------------------------
// task : APB single read
//--------------------------------------------------
task apb_read;
input [31: 0] addr;
output [31: 0] result_data;
begin
wait (pready == 1'b1);
@(posedge pclk);
psel <= #(delay) 1'b1 ;
penable <= #(delay) 1'b0 ;
paddr <= #(delay) addr ;
pwrite <= #(delay) 1'b0 ; // READ
pwdata <= #(delay) 32'h0 ;
@(posedge pclk);
psel <= #(delay) 1'b1 ;
penable <= #(delay) 1'b1 ;
paddr <= #(delay) addr ;
pwrite <= #(delay) 1'b0 ; // READ
pwdata <= #(delay) 32'h0 ;
wait (pready == 1'b1);
@(posedge pclk);
paddr <= #(delay) 32'h0 ;
pwrite <= #(delay) 1'b0 ; // READ
psel <= #(delay) 1'b0 ;
penable <= #(delay) 1'b0 ;
pwdata <= #(delay) 32'h0 ;
result_data = prdata;
end
endtask
endmoduleLooking at the code above, you can see that the module has two tasks: apb_write and apb_read. Remember when I explained the APB Bus, I said it was a very simple bus? Even with this simple module, the CPU can execute commands sent to the APB bus. Now, let's write a testbench.
`timescale 1ns/10ps
module top();
parameter period_pclk = 10;
reg pclk;
reg presetn;
wire psel;
wire penable;
wire [31:0] paddr;
wire pwrite;
wire [31:0] pwdata;
wire pready;
wire [31:0] prdata;
wire pslverr;
//clk
always #(period_pclk*0.5) pclk = ~pclk;
assign pready = 1'b1;
assign pslverr = 1'b0;
//instance
apb_bfm u_apb (
.pclk (pclk )
,.presetn (presetn )
,.psel (psel )
,.penable (penable )
,.paddr (paddr )
,.pwrite (pwrite )
,.pwdata (pwdata )
,.pready (pready )
,.prdata (prdata )
,.pslverr (pslverr )
);
initial begin
pclk = 1'b0;
presetn = 1'b0;
#(period_pclk);
presetn = 1'b1;
//u_apb(apb_bfm)의 apb_write task 실행
#(10*period_pclk);
u_apb.apb_write(32'h1 ,32'hAA);
#(10*period_pclk);
u_apb.apb_write(32'h10,32'h55);
#100 $finish;
end
initial begin
$dumpfile ("test.vcd");
$dumpvars();
end
endmoduleLooking at the testbench, you can see that pready is tied to 1'b1 and pslverr is tied to 1'b0 through the assign statement. pslverr does not need to be tied, but pready must be tied because it is used in apb_bfm. And in practice, it is tied as well.
task apb_write;
input [31: 0] addr;
input [31: 0] data;
begin
wait (pready == 1'b1);When you run the simulation, it releases reset and executes the apb_write task twice. So, let's run the testbench above.
Comparing the APB bus specification and testbench sim results, you can see that they are identical. This simulation inputs 0xAA data to address 0x1.
Now that we've established an environment for testing the interface, let's design our own module.
References: ARM® AMBA APB Protocol Specification