AXI实战(一)-搭建简单仿真环境-Xilinx-AMD论坛-FPGA CPLD-ChipDebug

AXI实战(一)-搭建简单仿真环境

AXI实战(一)-搭建简单仿真环境

看完在本文后,你将可能拥有:

  1. 一个可以仿真AXI/AXI_Lite总线的完美主端(Master)或从端(Slave)
  2. 一个使用SystemVerilog仿真模块的船信体验
图片[1]-AXI实战(一)-搭建简单仿真环境-Xilinx-AMD论坛-FPGA CPLD-ChipDebug

小何的AXI实战系列开更了,以下是初定的大纲安排:

图片[2]-AXI实战(一)-搭建简单仿真环境-Xilinx-AMD论坛-FPGA CPLD-ChipDebug

欢迎感兴趣的朋友关注并支持,以下为正文部分

  • 写在前面
  • Driver
  • Axi-Lite Driver通道实现和顶层master使用
    • 写事务
    • 给定地址写特定数据write
    • 读事务
    • 给定地址读特定数据read
    • run!
  • 如何使用
    • 接口(interface)
    • 声明,创建与例化
  • 结语

写在前面

小何在初学AXI的时候就觉得,开发AXI最大的不方便点在于不知道如何进行仿真。因为仿真工作的缓慢,本来小何想要开展的AXI实战系列也随之搁浅。随着秋招的结束小何决定求助于更高级的验证语言SystemVerilog,也就开展了漫长的自学之旅。再随着借助于开源项目的实现,我们终于可以方便快捷地对所设计的AXI模块进行仿真验证。

为了让更多不认识SV的朋友也可以接入,本实验所采用的验证代码既不是专用的VIP也并不涉及UVM,只要学过一些面向对象语言的朋友估计都可以轻松看懂代码。如果不希望了解如何实现的朋友可以直接移步正文最后一小节.

再者,本节代码来源:

https://github.com/pulp-platform/axi

无条件科学上网的可以从本站下载

 

主要使用代码:

- include/axi/assign.svh
- include/axi/typedef.svh
- src/axi_pkg.sv
- src/axi_intf.sv
- src/axi_test.sv
- test/tb_axi_sim_mem.sv	// for axi
- test/tb_axi_lite_regs.sv 	// for axi-lite

 

本文使用

simulator: modelsim 2019.2

综合(compile)环境: Vivado 2019.2

Driver

在验证中有三个核心组件:Driver(驱动器/激励),Monitor(监测器),Checker(比较器)。在这里实际上我们只需要了解其中最核心的Driver就可以了。

回看axi_test.v,可以看到有12个类:

图片[3]-AXI实战(一)-搭建简单仿真环境-Xilinx-AMD论坛-FPGA CPLD-ChipDebug

对axi_rand_master而言,他的层次结构是长这样的:

图片[4]-AXI实战(一)-搭建简单仿真环境-Xilinx-AMD论坛-FPGA CPLD-ChipDebug

而对axi_lite_rand_master而言,他的层次结构和成员函数是长这样的:

图片[5]-AXI实战(一)-搭建简单仿真环境-Xilinx-AMD论坛-FPGA CPLD-ChipDebug

可以看到driver中主要实现和产生予硬件模块实际激励,交互的功能。而顶层的rand_master和rand_slave更多是利用driver的底层实现能力进行随机化测试的定义。接下来,以axi_lite为例(不采用axi为例是因为在axi中需要考虑不同id,transaction类型的影响,这部分会放到AXI-FULL实战中再介绍),可以通过看其一个通道的交互为例来看看他们是怎么实现的。

Axi-Lite Driver通道实现和顶层master使用

这节将手把手介绍读写事务

小手一点,为小何增加更新动力👇

写事务

这小节以master端(发送)写通道 和 (接收)写响应通道,以及write的task为例介绍:

首先回看axi_lite_driver中的任务:send_w():

/// Issue a beat on the W channel.task send_w (
    input logic [DW-1:0] data,
    input logic [DW/8-1:0] strb
);
    axi.w_data  <= #TA data;
    axi.w_strb  <= #TA strb;
    axi.w_valid <= #TA 1;
    cycle_start();
    while (axi.w_ready != 1) begin cycle_end(); cycle_start(); end
    cycle_end();
    axi.w_data  <= #TA '0;
    axi.w_strb  <= #TA '0;
    axi.w_valid <= #TA 0;
endtask

 

可以看到这个task所驱动的写通道是比较简单的,具体的点有两个:

  • 利用  while (axi.w_ready != 1) 实现axi中各个通道的握手
  • 利用cycle_start();和cycle_end();进行时钟的对齐

与之相对应在axi_lite_rand_master中,其利用写通道实现的操作为任务 send_ws :

task automatic send_ws(input int unsigned n_writes);
    automatic logic  rand_success;
    automatic addr_t aw_addr;
    automatic data_t w_data;
    automatic strb_t w_strb;
    repeat (n_writes) begin
        wait (aw_queue.size() > 0);
        rand_wait(RESP_MIN_WAIT_CYCLES, RESP_MAX_WAIT_CYCLES);
        aw_addr = aw_queue.pop_front();  
        rand_success = std::randomize(w_data); assert(rand_success);
        rand_success = std::randomize(w_strb); assert(rand_success);
        $display("%0t %s> Send  W with DATA: %h STRB: %h", $time(), this.name, w_data, w_strb);
        this.drv.send_w(w_data, w_strb); // drv -> driver
        w_queue.push_back(1'b1);
    endendtask : send_ws

 

可以看到在send_ws中,主要是针对写数据w_data(与w_strb)进行了随机化,且写地址上通过与aw通道共用的队列aw_queue进行交互(且因axi-lite并不需要支持乱序返回).此处对aw_queue进行简单的代码介绍:

addr_t         aw_queue[$];  // sv 中声明队列的方式task automatic send_aws(input int unsigned n_writes);
.....
 aw_addr = addr_t'($urandom_range(MIN_ADDR, MAX_ADDR));
    // -> push_back 指将当前发送的aw_addr放到队列aw_queue的后面
    this.aw_queue.push_back(aw_addr);  
.....
endtask

 

由于在master端中只有写响应通道是接收的,为了更好展示接收端的axi握手过程,这里也po出写响应通道的代码:

// Wait for a beat on the B channel.task recv_b (
    output axi_pkg::resp_t resp
);
    axi.b_ready <= #TA 1;
    cycle_start();
    while (axi.b_valid != 1) begin cycle_end(); cycle_start(); end
    resp = axi.b_resp;
    cycle_end();
    axi.b_ready <= #TA 0;
endtask

 

所对应的master中的接口代码为:

task automatic recv_bs(input int unsigned n_writes);
    automatic logic           go_b;
    automatic axi_pkg::resp_t b_resp;
    repeat (n_writes) begin
        wait (b_queue.size() > 0 && w_queue.size() > 0);
        go_b = this.b_queue.pop_front();
        go_b = this.w_queue.pop_front();
        rand_wait(RESP_MIN_WAIT_CYCLES, RESP_MAX_WAIT_CYCLES);
        this.drv.recv_b(b_resp);
        $display("%0t %s> Recv  B with RESP: %h", $time(), this.name, b_resp);
    endendtask : recv_bs

 

可以看到这里的写响应由于是接收的原因,所以是拉高ready等待b_valid的.同时需要需要注意的是,在发送的过程,写响应通道需要等待写地址(aw)和写通道同时完成才可以开始,所以实际上写响应通道需要等待两个队列:b_queue和w_queue.

因此,最后的写事务流程便变成了:

图片[6]-AXI实战(一)-搭建简单仿真环境-Xilinx-AMD论坛-FPGA CPLD-ChipDebug

给定地址写特定数据write

而以上所介绍的是随机化写事务的过程,但有时候如果仅需要向特定的地址发送特定的数据并返回响应的时候,则可以跳开上面的定义利用AXI中各个通道的独立性进行输出:

// write data to a specific addresstask automatic write(input addr_t w_addr, input prot_t w_prot = prot_t'(0), input data_t w_data,
      input strb_t w_strb, output axi_pkg::resp_t b_resp);
    $display("%0t %s> Write to ADDR: %h, PROT: %b DATA: %h, STRB: %h",
    $time(), this.name, w_addr, w_prot, w_data, w_strb);
    fork
        this.drv.send_aw(w_addr, w_prot);
        this.drv.send_w(w_data, w_strb);
    join
    this.drv.recv_b(b_resp);
    $display("%0t %s> Received write response from ADDR: %h RESP: %h",
    $time(), this.name, w_addr, b_resp);
endtask : write

 

此时写事务简化为:

图片[7]-AXI实战(一)-搭建简单仿真环境-Xilinx-AMD论坛-FPGA CPLD-ChipDebug

读事务

在读事务的实现逻辑与写事务高度相似,此处先给出driver的读任务send_ar()和send_r():

/// Issue a beat on the AR channel.task send_ar (
    input logic [AW-1:0] addr,
    input prot_t         prot
);
    axi.ar_addr  <= #TA addr;
    axi.ar_prot  <= #TA prot;
    axi.ar_valid <= #TA 1;
    cycle_start();
    while (axi.ar_ready != 1) begin cycle_end(); cycle_start(); end
    cycle_end();
    axi.ar_addr  <= #TA '0;
    axi.ar_prot  <= #TA '0;
    axi.ar_valid <= #TA 0;
endtask

/// Issue a beat on the R channel.
task send_r (
input logic [DW-1:0] data,
input axi_pkg::resp_t resp
);
axi.r_data  <= #TA data;
axi.r_resp  <= #TA resp;
axi.r_valid <= #TA 1;
cycle_start();
while (axi.r_ready != 1) begin cycle_end(); cycle_start(); end
cycle_end();
axi.r_data  <= #TA '0;
axi.r_resp  <= #TA '0;
axi.r_valid <= #TA 0;
endtask

 

本质意义上,他们两者都只是在根据AXI的通道握手方式进行通信,与写事务不同,读事务的响应是master端发给slave端的.在master的定义中,依然如写地址与写通道中使用队列的方式进行交互一样,需要通过队列ar_queue进行顺序化控制:

task automatic send_ars(input int unsigned n_reads);
    automatic addr_t ar_addr;
    automatic prot_t ar_prot;
    repeat (n_reads) begin
        rand_wait(AX_MIN_WAIT_CYCLES, AX_MAX_WAIT_CYCLES);
        ar_addr = addr_t'($urandom_range(MIN_ADDR, MAX_ADDR));
        ar_prot = prot_t'($urandom());
        this.ar_queue.push_back(ar_addr);
        $display("%0t %s> Send AR with ADDR: %h PROT: %b", $time(), this.name, ar_addr, ar_prot);
        drv.send_ar(ar_addr, ar_prot);
    endendtask : send_ars

task automatic recv_rs(input int unsigned n_reads);
automatic addr_t          ar_addr;
automatic data_t           r_data;
automatic axi_pkg::resp_t  r_resp;
repeat (n_reads) begin
wait (ar_queue.size() > 0);
ar_addr = this.ar_queue.pop_front();
rand_wait(RESP_MIN_WAIT_CYCLES, RESP_MAX_WAIT_CYCLES);
drv.recv_r(r_data, r_resp);
$display("%0t %s> Recv  R with DATA: %h RESP: %0h", $time(), this.name, r_data, r_resp);
end
endtask : recv_rs

 

所以读事务的流程如下:

图片[8]-AXI实战(一)-搭建简单仿真环境-Xilinx-AMD论坛-FPGA CPLD-ChipDebug

给定地址读特定数据read

而以上所介绍的是随机化写事务的过程,但有时候如果仅需要向特定的地址接收特定的数据时,则可以跳开上面的定义利用AXI中各个通道的独立性进行输出:

// read data from a specific locationtask automatic read(input addr_t r_addr, input prot_t r_prot = prot_t'(0),
                    output data_t r_data, output axi_pkg::resp_t r_resp);
    $display("%0t %s> Read from ADDR: %h PROT: %b",
             $time(), this.name, r_addr, r_prot);
    this.drv.send_ar(r_addr, r_prot);
    this.drv.recv_r(r_data, r_resp);
    $display("%0t %s> Recieved read response from ADDR: %h DATA: %h RESP: %h",
             $time(), this.name, r_addr, r_data, r_resp);
endtask : read
endclass

 

自此,读写事务都介绍完了,我们可以利用rand_master对所需要测试的slave模块进行随机化测试:

task automatic run(input int unsigned n_reads, input int unsigned n_writes);
    $display("Run for Reads %0d, Writes %0d", n_reads, n_writes);
    fork
        send_ars(n_reads);
        recv_rs(n_reads);
        send_aws(n_writes);
        send_ws(n_writes);
        recv_bs(n_writes);
    joinendtask

 

也可以通过特定的write与read函数进行读写验证。

run!

在设计完读写事务后,由于各通道中存在队列操控控制顺序,所以我们可以简单地同时控制五个通道:

task automatic run(input int unsigned n_reads, input int unsigned n_writes);
    $display("Run for Reads %0d, Writes %0d", n_reads, n_writes);
    fork
        send_ars(n_reads);
        recv_rs(n_reads);
        send_aws(n_writes);
        send_ws(n_writes);
        recv_bs(n_writes);
    joinendtask

 

如何使用

在SV中,我们只需要将上述所设计出的“完美”master和slave接入到testbench中即可。首先是完成接口(interface)的统一,再在testbench中进行声明创建和例化就可以了。

 

接口(interface)

在/src/axi_intf.sv中可以看到具体的axi总线的定义,文件中主要包括四个interface:

  • AXI_BUS
  • AXI_BUS_DV
  • AXI_BUS_ASYNC
  • AXI_BUS_ASYNC_GRAY
  • AXI_LITE
  • AXI_LITE_DV
  • AXI_LITE_ASYNC_GRAY

除去为异步设计的总线,我们主要关注AXI_BUS,AXI_BUS_DV,AXI_LITE和AXI_LITE_DV.其中,DV指driver的意思,与bus相比,主要多了一个输入的时钟信号.在interface中的信号未指明下是没有输入输出限定的,可以通过modport进行限制,如在AXI_LITE_DV中:

/// A clocked AXI4-Lite interface for use in design verification.interface AXI_LITE_DV #(
  parameter int unsigned AXI_ADDR_WIDTH = 0,
  parameter int unsigned AXI_DATA_WIDTH = 0
)(input logic clk_i);

localparam AXI_STRB_WIDTH = AXI_DATA_WIDTH / 8;
typedef logic [AXI_ADDR_WIDTH-1:0] addr_t;
typedef logic [AXI_DATA_WIDTH-1:0] data_t;
typedef logic [AXI_STRB_WIDTH-1:0] strb_t;

.....
.....
.....

modport Master (
output aw_addr, aw_prot, aw_valid, input aw_ready,
output w_data, w_strb, w_valid, input w_ready,
input b_resp, b_valid, output b_ready,
output ar_addr, ar_prot, ar_valid, input ar_ready,
input r_data, r_resp, r_valid, output r_ready
);

modport Slave (
input aw_addr, aw_prot, aw_valid, output aw_ready,
input w_data, w_strb, w_valid, output w_ready,
output b_resp, b_valid, input b_ready,
input ar_addr, ar_prot, ar_valid, output ar_ready,
output r_data, r_resp, r_valid, input r_ready
);

 

然后在driver中,即可以使用以下方式进行定义:

/// A driver for AXI4-Lite interface.class axi_lite_driver #(
    parameter int  AW = 32  ,
    parameter int  DW = 32  ,
    parameter time TA = 0ns , // stimuli application time
    parameter time TT = 0ns   // stimuli test time
);
    virtual AXI_LITE_DV #(
        .AXI_ADDR_WIDTH(AW),
        .AXI_DATA_WIDTH(DW)
    ) axi;

function new(
virtual AXI_LITE_DV #(
.AXI_ADDR_WIDTH(AW),
.AXI_DATA_WIDTH(DW)
) axi );
this.axi = axi;
endfunction
.....

 

这就是上文中各个axi.啥啥啥的由来了

除此以外,有兴趣查看此项目完整代码的朋友会发现,在这套代码中为了进一步省略各个通道中繁杂的定义,将各个通道的信号分为了Request和Respond两大类,以AXI_LITE为例:

`define AXI_LITE_TYPEDEF_REQ_T(req_lite_t, aw_chan_lite_t, w_chan_lite_t, ar_chan_lite_t)  
  typedef struct packed {                                                                  
    aw_chan_lite_t aw;                                                                     
    logic          aw_valid;                                                               
    w_chan_lite_t  w;                                                                      
    logic          w_valid;                                                                
    logic          b_ready;                                                                
    ar_chan_lite_t ar;                                                                     
    logic          ar_valid;                                                               
    logic          r_ready;                                                                
  } req_lite_t;

`define AXI_LITE_TYPEDEF_RESP_T(resp_lite_t, b_chan_lite_t, r_chan_lite_t)  
typedef struct packed {
logic          aw_ready;
logic          w_ready;
b_chan_lite_t  b;
logic          b_valid;
logic          ar_ready;
r_chan_lite_t  r;
logic          r_valid;
} resp_lite_t;

 

但如果我们是采用Verilog编写的设计代码中往往没有sv这种方便的输入输出接口定义,所以就不详述了.

声明,创建与例化

到这里就到了各大FPGAer喜闻乐见的testbench编写环节了,假设我们需要一个master来验证我们所编写的slave,则先定义以下内容:

  • 地址,数据位宽
  • 随机读写地址范围
  • 仿真用时序参数
  • outstanding参数(本节中未介绍,将于下下节中介绍)
// AXI configuration
  localparam int unsigned AxiAddrWidth = 32'd32;    // Axi Address Width
  localparam int unsigned AxiDataWidth = 32'd32;    // Axi Data Width
  localparam int unsigned AxiStrbWidth = AxiDataWidth / 32'd8;
  // timing parameters
  localparam time CyclTime = 10ns;
  localparam time ApplTime =  2ns;
  localparam time TestTime =  8ns;

typedef logic [7:0]              byte_t;
typedef logic [AxiAddrWidth-1:0] axi_addr_t;
typedef logic [AxiDataWidth-1:0] axi_data_t;
typedef logic [AxiStrbWidth-1:0] axi_strb_t;

// simulation address range
localparam axi_addr_t StartAddr = axi_addr_t'(0);
localparam axi_addr_t EndAddr   = axi_addr_t'(StartAddr + 3);

typedef axi_test::axi_lite_rand_master #(
// AXI interface parameters
.AW ( AxiAddrWidth ),
.DW ( AxiDataWidth ),
// Stimuli application and test time
.TA ( ApplTime  ),
.TT ( TestTime  ),
.MIN_ADDR ( StartAddr ),
.MAX_ADDR ( EndAddr   ),
.MAX_READ_TXNS  ( 10 ),
.MAX_WRITE_TXNS ( 10 )
) rand_lite_master_t;

 

此时我们为driver的生成做好了一切准备,下一步便是将driver连接到总线上:

// -------------------------------
  // AXI Interfaces
  // -------------------------------
  AXI_LITE #(
    .AXI_ADDR_WIDTH ( AxiAddrWidth      ),
    .AXI_DATA_WIDTH ( AxiDataWidth      )
  ) master ();
  AXI_LITE_DV #(
    .AXI_ADDR_WIDTH ( AxiAddrWidth      ),
    .AXI_DATA_WIDTH ( AxiDataWidth      )
  ) master_dv (clk);
  `AXI_LITE_ASSIGN(master, master_dv)

 

这里的AXI_LITE_ASSIGN在文件/axi/assign.svh中定义,但是在这里就不多介绍了. 这类工作属于simple but not easy

在流程上此时需要接入我们自己编写的slave模块:

Axil_lite_top 
  #(
    .C_AXI_ADDR_WIDTH(AxiAddrWidth ),
    .C_AXI_DATA_WIDTH(AxiDataWidth ),
    .....
  )
  Axil_uart_top_dut (
    .S_AXI_ACLK (clk ),
    .S_AXI_ARESETN (rst_n ),
    .S_AXI_AWVALID (master.aw_valid ),
    .S_AXI_AWREADY (master.aw_ready ),
    .S_AXI_AWADDR (master.aw_addr ),
    .S_AXI_AWPROT (master.aw_prot ),
    .S_AXI_WVALID (master.w_valid ),
    .S_AXI_WREADY (master.w_ready),
    .S_AXI_WDATA (master.w_data ),
    .S_AXI_WSTRB (master.w_strb ),
    .S_AXI_BVALID (master.b_valid ),
    .S_AXI_BREADY (master.b_ready ),
    .S_AXI_BRESP (master.b_resp ),
    .S_AXI_ARVALID (master.ar_valid ),
    .S_AXI_ARREADY (master.ar_ready ),
    .S_AXI_ARADDR (master.ar_addr ),
    .S_AXI_ARPROT (master.ar_prot ),
    .S_AXI_RVALID (master.r_valid),
    .S_AXI_RREADY (master.r_ready ),
    .S_AXI_RDATA ( master.r_data ),
    .S_AXI_RRESP (master.r_resp ),
    .....
  );

 

然后编写仿真代码,其中包括:

  1. 时钟驱动
  2. 复位驱动
  3. Master例化与测试逻辑

这里主要写写第三点:

可以通过initial块:

initial begin : proc_generate_axi_traffic
    automatic rand_lite_master_t lite_axi_master = new ( master_dv, "Lite Master");
    automatic axi_data_t      data = '0;
    automatic axi_pkg::resp_t resp = '0;
    end_of_sim <= 1'b0;
    lite_axi_master.reset();
    @(posedge rst_n);
    repeat (5) @(posedge clk);

lite_axi_master.write(axi_addr_t'(32'h0011_4514), axi_pkg::prot_t'('0),
axi_data_t'(32'h0011_4514), axi_strb_t'(4'hF), resp);
.....
lite_axi_master.read(axi_addr_t'(32'h0011_4514), axi_pkg::prot_t'('0), data, resp);
.....
// Let random stimuli application checking is separate.
lite_axi_master.run(TbNoReads, TbNoWrites);
end_of_sim <= 1'b1;
end

 

即先手动写AXI配置IP进行初始化设置,再回读验证.然后对数据流进行随机化读写测试。

对懒一点的朋友也可以不采用随机的方式直接在这里操作读写进行仿真,而不需要像以前一样单独操作每一个通道进行繁琐验证。

结语

在本小节中,我们介绍了用于仿真axi总线的小几个sv模块。也意味着小何的axi总线实战系列开始更新,欢迎大家关注。

 

请登录后发表评论

    没有回复内容