[Verilog] Practice 2 – APB interface design

Now, let's start designing the APB interface in earnest. For an overview of the interface and the BFM for verification, please refer to the previous post.

Register map

First, you need to create a register map for interface design. This determines which signals the interface will send and receive from the IP block. The register map is recorded in the data sheet describing the IP and is also required for developing the software that operates the IP, making it incredibly important.

글 설명 이미지, Register setting
Role of Register Map

Now, let's write the register map as follows.

  • Address 0x0: register a
Signal nameR/WDefault valueBit
Reserved31:11
a_3RW0x010:8
Reserved7:6
a_2RW0x05:4
Reserved3:1
a_1RW0x00
register a
  • Address 0x4: register b
Signal nameR/WDefault valueBit
Reserved31:11
b_3RW0x010:8
Reserved7:6
b_2RW0x05:4
Reserved3:1
b_1RW0x00
register b

Here, R/W stands for Read/Write access and the types are as follows:

  • RW: readable and writable
  • RO: read-only
  • WO: write-only
  • W1C: write 1 clear

APB interface design

Now, let's start designing the interface in earnest. First, let's set up the input and output ports.

module apb_if (
     //APB Interface
     input  wire        pclk
    ,input  wire        presetn
    ,input  wire        penable
    ,input  wire        psel
    ,input  wire [31:0] paddr
    ,input  wire        pwrite
    ,input  wire [31:0] pwdata
    ,output wire [31:0] prdata
 
     //Register Interface
    ,output wire        a_1
    ,output wire [ 1:0] a_2
    ,output wire [ 2:0] a_3
    ,output wire        b_1
    ,output wire [ 1:0] b_2
    ,output wire [ 2:0] b_3
);

There's a bus signal, and the signals set in the register map will be set as output and connected to the IP block. And we need to declare the data type, right? First, I set write_en and read_en.

Data type declaration

    //===================================================================
    // Internal Signals
    //===================================================================
    wire        write_en = psel & ~penable &   pwrite;
    wire        read_en  = psel & ~penable &  ~pwrite;

The enable signals are turned on when psel is high, that is, when the CPU selects this IP. Then, depending on pwrite, it decides whether to write or read.

APB write

Next is the Address related signal.

    //===================================================================
    // Address Decode 
    //===================================================================
    wire        a = (paddr == 32'h0);
    wire        b = (paddr == 32'h4);
         
    //===================================================================
    // Write Enable
    //===================================================================
    wire        we_a = write_en & a;
    wire        we_b = write_en & b;

The address set in the register map has been reflected.

Here's the actual register signal. Compare it with the generated register map.

    //===================================================================
    // Register Setting
    //===================================================================
    reg         a_one;
    always @(posedge pclk or negedge presetn) begin
        if(!presetn)   a_one <= 1'b0;
        else if (we_a) a_one <= pwdata[0];
    end
 
    reg  [ 1:0] a_two;
    always @(posedge pclk or negedge presetn) begin
        if(!presetn)   a_two <= 1'b0;
        else if (we_a) a_two <= pwdata[5:4];
    end
 
    reg  [ 2:0] a_three;
    always @(posedge pclk or negedge presetn) begin
        if(!presetn)   a_three <= 1'b0;
        else if (we_a) a_three <= pwdata[10:8];
    end
 
    reg         b_one;
    always @(posedge pclk or negedge presetn) begin
        if(!presetn)   b_one <= 1'b0;
        else if (we_b) b_one <= pwdata[0];
    end
 
    reg  [ 1:0] b_two;
    always @(posedge pclk or negedge presetn) begin
        if(!presetn)   b_two <= 1'b0;
        else if (we_b) b_two <= pwdata[5:4];
    end
 
    reg  [ 2:0] b_three;
    always @(posedge pclk or negedge presetn) begin
        if(!presetn)   b_three <= 1'b0;
        else if (we_b) b_three <= pwdata[10:8];
    end

When the CPU writes to register a (0x0), we_a turns on and specific bits of pwdata are written to a_one, a_two, and a_three. The same goes for register b (0x4). So, we need to connect this register file to the output, right?

    //===================================================================
    // Output Assign
    //===================================================================
    assign a_1 = a_one  ;
    assign a_2 = a_two  ;
    assign a_3 = a_three;
    assign b_1 = b_one  ;
    assign b_2 = b_two  ;
    assign b_3 = b_three;

APB Read

Now let's set the RD (Read Data) value.

    //===================================================================
    // Local Parameters
    //===================================================================
    localparam INVALID_DATA = 32'hDEAD_DEAD;
    
    //===================================================================
    // Read Decode
    //===================================================================
    reg [31:0] RD;
    always @(*) begin
        if(read_en) begin
            case (paddr)
                32'h0   : RD = {21'h0, a_three, 2'h0, a_two, 3'h0, a_one};
                32'h4   : RD = {21'h0, b_three, 2'h0, b_two, 3'h0, b_one};
                default : RD = INVALID_DATA;
            endcase
        end
        else RD = INVALID_DATA;
    end
    
    assign  prdata = RD;

Now, let's write a testbench and check whether the register setting is correct through simulation results.

//top.v
`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;

    wire        a_1;
    wire [ 1:0] a_2;
    wire [ 2:0] a_3;
    wire        b_1;
    wire [ 1:0] b_2;
    wire [ 2:0] b_3;

    //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 ) 
    );

    apb_if u_apb_if (
         .pclk    (pclk    )
        ,.presetn (presetn ) 
        ,.psel    (psel    ) 
        ,.penable (penable ) 
        ,.paddr   (paddr   ) 
        ,.pwrite  (pwrite  ) 
        ,.pwdata  (pwdata  ) 
        ,.prdata  (prdata  ) 

        ,.a_1     (a_1     )
        ,.a_2     (a_2     )
        ,.a_3     (a_3     )
        ,.b_1     (b_1     )
        ,.b_2     (b_2     )
        ,.b_3     (b_3     )
    );

    initial begin
        pclk    = 1'b0;
        presetn = 1'b0;

        #(period_pclk);
        presetn = 1'b1;

        //Register a write
        #(10*period_pclk);
        u_apb.apb_write(32'h0,32'haaa);

        #(10*period_pclk);
        u_apb.apb_write(32'h0,32'h555);

        //Register b write
        #(10*period_pclk);
        u_apb.apb_write(32'h4,32'haaa);

        #(10*period_pclk);
        u_apb.apb_write(32'h4,32'h555);

        #100 $finish;
    end

    initial begin
        $dumpfile ("test.vcd");
        $dumpvars();
    end

endmodule

//apb_if.v
module apb_if (
     //APB Interface
     input  wire        pclk
    ,input  wire        presetn
    ,input  wire        penable
    ,input  wire        psel
    ,input  wire [31:0] paddr
    ,input  wire        pwrite
    ,input  wire [31:0] pwdata
    ,output wire [31:0] prdata
 
     //Register Interface
    ,output wire        a_1
    ,output wire [ 1:0] a_2
    ,output wire [ 2:0] a_3
    ,output wire        b_1
    ,output wire [ 1:0] b_2
    ,output wire [ 2:0] b_3
);

    //===================================================================
    // Local Parameters
    //===================================================================
    localparam INVALID_DATA = 32'hDEAD_DEAD;
    
    //===================================================================
    // Internal Signals
    //===================================================================
    wire        write_en = psel & ~penable &   pwrite;
    wire        read_en  = psel & ~penable &  ~pwrite;
    reg  [31:0] RD ;
      
    //===================================================================
    // Address Decode 
    //===================================================================
    wire        a = (paddr == 32'h0);
    wire        b = (paddr == 32'h4);
     
    //===================================================================
    // Write Enable
    //===================================================================
    wire        we_a = write_en & a;
    wire        we_b = write_en & b;
    
    //===================================================================
    // Register Files 
    //===================================================================
    reg         a_one;
    always @(posedge pclk or negedge presetn) begin
        if(!presetn)   a_one <= 1'b0;
        else if (we_a) a_one <= pwdata[0];
    end
 
    reg  [ 1:0] a_two;
    always @(posedge pclk or negedge presetn) begin
        if(!presetn)   a_two <= 1'b0;
        else if (we_a) a_two <= pwdata[5:4];
    end
 
    reg  [ 2:0] a_three;
    always @(posedge pclk or negedge presetn) begin
        if(!presetn)   a_three <= 1'b0;
        else if (we_a) a_three <= pwdata[10:8];
    end
 
    reg         b_one;
    always @(posedge pclk or negedge presetn) begin
        if(!presetn)   b_one <= 1'b0;
        else if (we_b) b_one <= pwdata[0];
    end
 
    reg  [ 1:0] b_two;
    always @(posedge pclk or negedge presetn) begin
        if(!presetn)   b_two <= 1'b0;
        else if (we_b) b_two <= pwdata[5:4];
    end
 
    reg  [ 2:0] b_three;
    always @(posedge pclk or negedge presetn) begin
        if(!presetn)   b_three <= 1'b0;
        else if (we_b) b_three <= pwdata[10:8];
    end
 
    //===================================================================
    // Read Decode
    //===================================================================
    always @(*) begin
        if(read_en) begin
            case (paddr)
                32'h0   : RD = {21'h0, a_three, 2'h0, a_two, 3'h0, a_one};
                32'h4   : RD = {21'h0, b_three, 2'h0, b_two, 3'h0, b_one};
                default : RD = INVALID_DATA;
            endcase
        end
        else RD = INVALID_DATA;
    end
    
    assign  prdata = RD;
    
    //===================================================================
    // Output Assign
    //===================================================================
    assign a_1 = a_one  ;
    assign a_2 = a_two  ;
    assign a_3 = a_three;
    assign b_1 = b_one  ;
    assign b_2 = b_two  ;
    assign b_3 = b_three;

endmodule

By performing a write transfer to register a (address 0x0), you can control the a_1, a_2, and a_3 signals, and by performing a write transfer to register b (address 0x4), you can control the b_1, b_2, and b_3 signals.

Register a (0x0)
Register a (0x0)
Register b (0x4)
Register b (0x4)

These signals are input to the IP block, enabling IP control. The CPU controls the IP through the AMBA bus; this is the register setting.

This allows us to design IPs using the APB interface in earnest.

References: ARM® AMBA APB Protocol Specification

Similar Posts