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 <= #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);
end
endtask : 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);
end
endtask : recv_bs
可以看到這里的寫響應由于是接收的原因,所以是拉高ready等待b_valid的.同時需要需要注意的是,在發送的過程,寫響應通道需要等待寫地址(aw)和寫通道同時完成才可以開始,所以實際上寫響應通道需要等待兩個隊列:b_queue和w_queue.
因此,最后的寫事務流程便變成了:
給定地址寫特定數據write
而以上所介紹的是隨機化寫事務的過程,但有時候如果僅需要向特定的地址發送特定的數據并返回響應的時候,則可以跳開上面的定義利用AXI中各個通道的獨立性進行輸出:
// write data to a specific address
task 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 <= #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);
end
endtask : 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
所以讀事務的流程如下:
給定地址讀特定數據read
而以上所介紹的是隨機化寫事務的過程,但有時候如果僅需要向特定的地址接收特定的數據時,則可以跳開上面的定義利用AXI中各個通道的獨立性進行輸出:
// read data from a specific location
task 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);
join
endtask
也可以通過特定的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);
join
endtask
如何使用
在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,則先定義以下內容:
// 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進行初始化設置,再回讀驗證.然后對數據流進行隨機化讀寫測試。
對懶一點的朋友也可以不采用隨機的方式直接在這里操作讀寫進行仿真,而不需要像以前一樣單獨操作每一個通道進行繁瑣驗證。
-
驅動器
+關注
關注
52文章
8164瀏覽量
146033 -
FPGA設計
+關注
關注
9文章
428瀏覽量
26487 -
比較器
+關注
關注
14文章
1636瀏覽量
107108 -
仿真器
+關注
關注
14文章
1016瀏覽量
83645 -
AXI總線
+關注
關注
0文章
66瀏覽量
14250
發布評論請先 登錄
相關推薦
評論