[Verilog] Practice 1 – APB interface intro, BFM

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;

endmodule

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

Simulation result
Simulation result

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;

endmodule

Now, cnt is initialized by the input value, not a fixed value. Let's check the simulation results.

Simulation result
Simulation result

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.

Testbench VS Chip
Testbench VS Chip

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 interface
APB 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
 
endmodule

Looking 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

endmodule

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

APB write transfer 비교
APB write transfer comparison

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

Similar Posts