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

driver在验证中有三个核心组件:driver(驱动器/激励),monitor(监测器),checker(比较器)。在这里实际上我们只需要了解其中最核心的driver就可以了。
回看axi_test.v,可以看到有12个类:
对axi_rand_master而言,他的层次结构是长这样的:
而对axi_lite_rand_master而言,他的层次结构和成员函数是长这样的:
可以看到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 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 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.
因此,最后的写事务流程便变成了:
给定地址写特定数据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此时写事务简化为:
读事务在读事务的实现逻辑与写事务高度相似,此处先给出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 send ar with addr: %h prot: %b, $time(), this.name, ar_addr, ar_prot); drv.send_ar(ar_addr, ar_prot); endendtask : send_arstask 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); endendtask : recv_rs所以读事务的流程如下:
给定地址读特定数据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 : readendclass自此,读写事务都介绍完了,我们可以利用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_busaxi_bus_dvaxi_bus_asyncaxi_bus_async_grayaxi_liteaxi_lite_dvaxi_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 ), ..... );然后编写仿真代码,其中包括:
时钟驱动复位驱动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进行初始化设置,再回读验证.然后对数据流进行随机化读写测试。
对懒一点的朋友也可以不采用随机的方式直接在这里操作读写进行仿真,而不需要像以前一样单独操作每一个通道进行繁琐验证。

element14.com推出了全新的“车载物联网”设计挑战
中国电信宣布“三千兆”升级:正式进入“三千兆”时代
光伏玻璃的价格上涨约60%,带动了整个光伏概念的发展热潮
一种基于全电介质紧凑薄膜结构的计算重构微型光谱仪
磷酸铁锂电池组供电系统在变电站的应用
AXI实战(一)-搭建简单仿真环境
通过简单的保存-恢复策略避免UVM VIP的冗余仿真周期
浅谈产品电磁兼容性设计培训的重要性
旧手机别丢了!参与vivo以旧换新活动, 最高可享2020元补贴
如何用两种不同的方法列写双容水槽传递函数
AT89S51单片机最小系统电路的设计
浅谈红外增透膜
美国打压,台积电不能为国内的芯片提供有效的帮助
配备音乐定时器的蓝牙智能牙刷
WIPPO小品获千万融资
绝缘轴承的电气绝缘性能优越,有着更广泛的应用
PCB布线:50欧姆迹线宽度应该有多宽?
iQOO手机评测 “武林英雄”生而强悍
麒麟芯片量产计划将搁浅 鸿蒙系统或许是救星
长电科技推出XDFOI全系列产品 为全球客户提供更加优质的服务