一、前言
应聘IC前端相关岗位时,FIFO是最常考也是最基本的题目。FIFO经常用于数据缓存、位宽转换、异步时钟域处理。随着芯片规模的快速增长,灵活的system verilog成为设计/验证人员的基本功。本文从简易版的同步FIFO开始,熟悉IP设计与验证的基础技能。
二、IP设计
FIFO这一IP核已经相当成熟,因此网上资料也是一抓一大把。其中笔者认为较好的一个在文末附录中,需要详细了解FIFO工作原理的朋友可以仔细看看。这里简单介绍下本文设计FIFO的原理与结构。FIFO的内部存储单元是常见的双口RAM,这个IP的精髓在于读写地址的对外屏蔽与自动管理。避免写满、读空至关重要。本文设计的FIFO顶层例化双口RAM和FIFO控制两大模块:前者仅作为存储单元响应读写信号,后者根据读写计数器产生读写指针和重要的空满指示信号。
代码如下:
存储模块:
1 `timescale 1ns/1ps
2 module dpram
3 #(parameter D_W=8,
4 A_W=8)
5 (
6 input clk,
7 input rst_n,
8 //write ports
9 input wr_en,
10 input [D_W-1:0] wr_data,
11 input [A_W-1:0] wr_addr,
12 //read ports
13 input rd_en,
14 input [A_W-1:0] rd_addr,
15 output reg [D_W-1:0] rd_data
16 );
17 //RAM
18 reg [D_W-1:0] memory [0:2**A_W-1];
19
20 //write operation
21 always@(posedge clk)begin
22 if(wr_en)begin
23 memory[wr_addr] <= wr_data;
24 end
25 end
26
27 //read operation
28 always@(posedge clk or negedge rst_n)begin
29 if(~rst_n)
30 rd_data <= 0;
31 else if(rd_en)begin
32 rd_data <= memory[rd_addr];
33 end
34 else if(rd_addr == 1)
35 rd_data <= memory[0];
36 end
37
38 endmodule
知识兔dpramFIFO控制模块:
1 `timescale 1ns/1ps
2 module fifo_ctrl
3 #(parameter A_W = 8,
4 parameter [0:0] MODE = 0//0- standard read 1- first word fall through
5 )
6 (
7 input clk,
8 input rst_n,
9
10 output [A_W-1:0] wr_addr,
11 output [A_W-1:0] rd_addr,
12
13 output empty,
14 output full,
15 input wr_en,
16 input rd_en
17 );
18 localparam MAX_CNT = 2**A_W;
19 localparam FD_W = A_W;
20
21 function [FD_W-1:0] abs;
22 input signed [FD_W-1:0] data;
23 begin
24 assign abs = data >= 0 ? data : -data;
25 end
26 endfunction
27
28 reg [A_W-1:0] wr_cnt;
29 wire add_wr_cnt,end_wr_cnt;
30 reg wr_flag;
31 reg [A_W-1:0] rd_cnt;
32 wire add_rd_cnt,end_rd_cnt;
33 reg rd_flag;
34 wire [A_W+1-1:0] wr_ptr,rd_ptr;
35
36 always@(posedge clk or negedge rst_n)begin
37 if(~rst_n)begin
38 wr_cnt <= 0;
39 end
40 else if(add_wr_cnt)begin
41 if(end_wr_cnt)
42 wr_cnt <= 0;
43 else
44 wr_cnt <= wr_cnt + 1'b1;
45 end
46 end
47
48 assign add_wr_cnt = wr_en & ~full;
49 assign end_wr_cnt = add_wr_cnt && wr_cnt == MAX_CNT - 1;
50
51 always@(posedge clk or negedge rst_n)begin
52 if(~rst_n)begin
53 wr_flag <= 0;
54 end
55 else if(end_wr_cnt)begin
56 wr_flag <= ~wr_flag;
57 end
58 end
59
60 always@(posedge clk or negedge rst_n)begin
61 if(~rst_n)begin
62 rd_cnt <= 0;
63 end
64 else if(add_rd_cnt)begin
65 if(end_rd_cnt)
66 rd_cnt <= 0;
67 else
68 rd_cnt <= rd_cnt + 1'b1;
69 end
70 end
71
72 assign add_rd_cnt = rd_en & ~empty;
73 assign end_rd_cnt = add_rd_cnt && rd_cnt == MAX_CNT - 1;
74
75 always@(posedge clk or negedge rst_n)begin
76 if(~rst_n)begin
77 rd_flag <= 0;
78 end
79 else if(end_rd_cnt)begin
80 rd_flag <= ~rd_flag;
81 end
82 end
83
84 assign wr_ptr = {wr_flag,wr_cnt};
85 assign rd_ptr = {rd_flag,rd_cnt};
86
87 assign wr_addr = wr_cnt;
88 assign rd_addr = rd_cnt + MODE;
89
90 assign empty = wr_ptr == rd_ptr;
91 assign full = (abs(wr_ptr[A_W-1:0] - rd_ptr[A_W-1:0]) < 1) && (wr_ptr[A_W] != rd_ptr[A_W]);
92
93 endmodule
知识兔fifo_ctrl同步FIFO顶层:
1 `timescale 1ns/1ps
2 module fifo_sync
3 #(parameter D_W = 8,
4 LOG_2_DEPTH = 8,//2^8 = 256
5 parameter [0:0] MODE = 0
6 )
7 (
8 input clk,
9 input rst_n,
10
11 input wr_en,
12 input [D_W-1:0] wr_data,
13 input rd_en,
14 output [D_W-1:0] rd_data,
15 output wr_full,
16 output rd_empty
17 );
18 wire [LOG_2_DEPTH-1:0] wr_addr,rd_addr;
19
20 dpram #(.D_W(D_W),
21 .A_W(LOG_2_DEPTH))
22 dpram
23 (
24 .clk (clk),
25 .rst_n (rst_n),
26 .wr_en (wr_en),
27 .wr_data (wr_data),
28 .wr_addr (wr_addr),
29 .rd_en (rd_en),
30 .rd_addr (rd_addr),
31 .rd_data (rd_data)
32 );
33
34 fifo_ctrl #(.A_W(LOG_2_DEPTH),
35 .MODE(MODE))
36 fifo_ctrl
37 (
38 .clk (clk),
39 .rst_n (rst_n),
40 .wr_addr (wr_addr),
41 .rd_addr (rd_addr),
42 .empty (rd_empty),
43 .full (wr_full),
44 .wr_en (wr_en),
45 .rd_en (rd_en)
46 );
47
48 endmodule
知识兔fifo_sync之前在使用FPGA做项目时,经常看到厂商提供的FIFO IP提供“首字跌落”模式,故在本设计中也提供了这个模式,即在读信号有效前便送出第一个写入的数据。另外,为提高代码的通用性,在设计中尽量使用parameter而不是固定数值作为信号位宽。
三、SV搭建testbench
一般来说使用verilog非综合子集也能编写testbench来验证设计的正确性,但当DUT较为复杂时就显得不够灵活。设计同步FIFO也是为了学习利用system verilog编写testbench的一些技巧。
首先明确验证方案。同步FIFO无非就是读写操作,只要每次都能将写入的数据读出就认为设计无误。我们可以通过SV的约束性随机特性完成任意长度以及任意间隔的读写操作。数据较多时逐一比较数据困难,testbench也应有自动对比数据并统计错误的机制。
采用OOP思想,设计descriptor transcation scorebord三个类,因此是随机产生读写操作的访问器,根据访问器信息的读写操作以及自动对比读写数据的计分板。SV语法非常灵活,各个类可以的方法不仅包括function,也支持task,这为时序操作带来了便利。还有一点较为重要的是,选择合适的数据类型。由于待写入数据长度不固定,使用动态数组比较恰当。而不断增加的读取数据信息,放置在队列中会有更高的效率。FIFO是否选择“首字跌落”模式,对读操作时序有直接影响,testbench中采用宏定义方式条件编译参数和读取采集逻辑。
代码如下:
1 `timescale 1ns/1ps
2 `define VERDI
3 //`define FW
4
5 module testbench();
6
7 parameter CYC = 20,
8 RST_TIM = 2;
9 parameter D_W = 8,
10 LOG_2_DEPTH = 8;
11
12 `ifdef FW
13 parameter [0:0] MODE = 1'b1;//1'b1 1'b0
14 `else
15 parameter [0:0] MODE = 1'b0;
16 `endif
17 parameter MAX_LEN = 2**LOG_2_DEPTH;
18
19 typedef int unsigned uint32;
20 typedef enum {true,false} status_e;
21
22 bit clk,rst_n;
23 bit wr_en;
24 bit [D_W-1:0] wr_data;
25 bit rd_en;
26 logic [D_W-1:0] rd_data;
27 logic wr_full;
28 logic rd_empty;
29 reg rd_en_t;
30
31 `ifdef VERDI
32 initial begin
33 $fsdbDumpfile("wave.fsdb");
34 $fsdbDumpvars("+all");
35 end
36 `endif
37
38 initial begin
39 clk = 1;
40 forever #(CYC/2.0) clk= ~clk;
41 end
42
43 initial begin
44 rst_n = 1;
45 #1;
46 rst_n = 0;
47 #(RST_TIM*CYC) rst_n = 1;
48 end
49
50 class Descriptor;
51 rand bit [16-1:0] len_w,len_r,interval;
52
53 constraint c {
54 len_w inside {[1:20]};
55 len_r inside {[0:20]};
56 interval inside {[2:6]};
57 }
58 function new;
59 $display("Created a object");
60 endfunction
61 endclass:Descriptor
62
63 class Transcation;
64 bit [D_W-1:0] data_packet[];
65 static uint32 q_len[$];
66 static uint32 q_rd_data[$];
67 uint32 q_ref_data[$];
68
69 Descriptor dp;
70
71 function new();
72 dp = new();
73 assert(dp.randomize());
74 q_len.push_back(dp.len_w);
75 endfunction
76
77 extern task wri_oper;
78 extern task rd_oper;
79 extern task wr_rd_operation;
80 extern function void ref_gen(ref uint32 q_ref_data[$]);
81
82 endclass:Transcation
83
84 task Transcation::wri_oper;
85 uint32 wr_num;
86 $display("Write:%d",$size(tr.data_packet));
87 @(posedge clk);
88 #1;
89 while(wr_num < dp.len_w)begin
90 if(~wr_full)begin
91 wr_en = 1;
92 wr_data = tr.data_packet[wr_num];
93 wr_num++;
94 end
95 else begin
96 wr_en = 0;
97 wr_data = tr.data_packet[wr_num];
98 end
99 #(CYC*1);
100 end
101 wr_en = 0;
102 endtask
103
104 task Transcation::rd_oper;
105 uint32 rd_num;
106 $display("Read: %d",dp.len_r);
107 @(posedge clk);
108 #1;
109 #(dp.interval*CYC);
110 while(rd_num < dp.len_r)begin
111 if(~rd_empty)begin
112 rd_en = 1;
113 rd_num++;
114 end
115 else
116 rd_en = 0;
117 #(CYC*1);
118 end
119 rd_en = 0;
120 endtask
121
122 task Transcation::wr_rd_operation;
123 tr.data_packet = new[dp.len_w];
124 $display("len_w = %d, len_r = %d, inverval = %d",dp.len_w,dp.len_r,dp.interval);
125 foreach(tr.data_packet[i])begin
126 tr.data_packet[i] = i+1;
127 //$display(tr.data_packet[i]);
128 end
129 fork
130 wri_oper;
131 rd_oper;
132 join
133 endtask
134
135 function void Transcation::ref_gen(ref uint32 q_ref_data[$]);
136 integer j;
137 foreach(q_len[i])begin
138 for(j=0;j<q_len[i];j++)begin
139 q_ref_data = {q_ref_data,j+1};
140 end
141 end
142 endfunction
143
144 class Scoreboard;
145 uint32 total_num,error_num = 0;
146
147 function compare(ref uint32 q_data[$],ref uint32 q_ref[$]);
148 uint32 comp_num;
149 uint32 i;
150 uint32 data_len,ref_len;
151 status_e status;
152 data_len = $size(q_data);
153 ref_len = $size(q_ref);
154 $display("The lengths of q_data and q_ref are %d,%d",$size(q_data),$size(q_ref));
155 if(data_len >= ref_len)
156 comp_num = ref_len;
157 else
158 comp_num = data_len;
159 total_num = comp_num;
160 for(i=0;i<comp_num;i++)begin
161 if(q_data[i] != q_ref[i])begin
162 error_num++;
163 $display("The %dth data is different between the two!",i);
164 status = false;
165 return status;
166 end
167 end
168 status = true;
169 return status;
170 endfunction
171 endclass
172
173 //Descriptor dp;
174 Transcation tr;
175 Scoreboard sb;
176
177 //main
178 initial begin
179 //int status;
180 status_e status;
181 wr_en = 0;
182 rd_en = 0;
183 wr_data = 0;
184 #1;
185 #(2*CYC);
186 repeat(2)begin
187 tr = new();
188 tr.wr_rd_operation;
189 #(50*CYC);
190 end
191 #20;
192 tr.ref_gen(tr.q_ref_data);
193
194 //soreboard
195 sb = new();
196 status = sb.compare(tr.q_rd_data,tr.q_ref_data);
197 if(status == true)
198 $display("Simulation success!");
199 else
200 $display("Simulation filure!");
201 $stop;
202 end
203
204 //save readed data
205 initial begin
206 forever begin
207 @(posedge clk);
208 `ifdef FW
209 if(rd_en)
210 `else
211 if(rd_en_t)
212 `endif
213 tr.q_rd_data = {tr.q_rd_data,rd_data};
214 end
215 end
216
217 always@(posedge clk)begin
218 rd_en_t <= rd_en;
219 end
220
221 fifo_sync
222 #(.D_W(D_W),
223 .LOG_2_DEPTH(8),//256
224 .MODE(MODE)
225 )uut
226 (
227 .clk (clk),
228 .rst_n (rst_n),
229 .wr_en (wr_en),
230 .wr_data (wr_data),
231 .rd_en (rd_en),
232 .rd_data (rd_data),
233 .wr_full (wr_full),
234 .rd_empty (rd_empty)
235 );
236
237 endmodule:testbench
知识兔testbench.sv四、VCS+Verdi工具使用
不得不说大多EDA工具确实没有IT行业的开发工具友好,用起来着实费了一番功夫。VCS这一仿真工具有自己的GUI debug tool,但功能不够强大。这里我们使用Verdi来debug。在上一节的SV代码中有一段fsdb的代码是专门产生Verdi波形文件的。因SV本身并没有这两个system function,使用时需要指定两个库文件路径。笔者直接将冗长的命令和选项定义一个alias:(bash shell)
alias vcs_verdi="vcs -full64 -sverilog -debug_all -P ${NOVAS_HOME}/share/PLI/VCS/linux64/novas.tab ${NOVAS_HOME}/share/PLI/VCS/linux64/pli.a +define+DUMPFSDB"
.bashrc file:
这个路径名好像必须是NOVAS_HOME,否则会报错,也是挺坑。利用上边的指令完成第一步代码编译,之后依次是执行仿真程序和调用Verdi GUI界面观察波形。命令依次是:
./simv
verdi -sv -f filename -ssf wave.fsdb
执行仿真后会产生testbench中指定的波形文件。第三步命令执行后verdi界面被打开。
通过波形及执行仿真后的Log可以看出仿真通过,在读写FIFO过程中没有产生错误。
这里分享一些使用verdi的基本技巧。
观察指定信号波形:选中代码中变量,ctrl+w添加该变量到波形窗口。
保存波形配置文件:在波形界面,按下shift+s保存.rc文件。
调取存储的配置文件:点击r,选中存储的.rc文件并打开。
笔者第一次利用SV采用OOP思想搭建testbench,也是首次使用VCS+Verdi工具链进行仿真调试。虽然设计验证都非常简单,但还是卡住了很多次。之后会尝试异步FIFO设计,以及基于UVM的可重用testbench编写。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
附录
1 [图文]同步FIFO - 百度文库 https://wenku.baidu.com/view/620e3934a32d7375a4178037.html
2 linux下的EDA——VCS与Verdi仿真 - moon9999的博客 - CSDN博客 https://blog.csdn.net/moon9999/article/details/76615869