本篇是继可综合的async_fifo设计(一)的下半篇,给出了testbench代码、测试波形及几个注意事项。
整个设计前后,提出以下几个问题:
// 1、空满标志产生(关键)
答:比较读写格雷码产生,满标志wr_full在写时钟域产生,写指针多轮回一圈,最高位标志位相异,其余位相同;空标志rd_empty在读时钟域产生,读写指针在同一奇偶圈,所有位相同时表示读指针追赶上写指针,此时读空标志产生。
// 2、空满标志产生后,持续时间是多长?
答:本设计采用的方法是:通过将一部时钟域的读写使能信号同步到本时钟域,写满后直到读使能,才清零wr_full,读空后直到写使能信号到来,才清零rd_empty。异步信号的同步处理打2拍处理,会延迟异步读写使能信号的到来,所以,标志的清空会有一定延迟。
// 3、是否有空满数据保护(满了不准写,或者空了不再读)?
答:有,通过格雷码反转为二进制地址,排除了空满标志延迟产生以及空满标志产生后继续的读写操作等造成地址的冗余增加,进而实现了空满保护。
// 4、地址跨时钟域同步是否对空满判断产生影响?
答:同步固然带来延迟,延迟直接对空满标志的产生造成影响。以rd_empty为例,若在延迟间隔内,写使能无效(不继续写),rd_empty正常产生(读、写不同时进行),若在间隔内,写使能有效,则写指针会递增,则传递到读时钟域的写指针一定小于或等于当前时刻的实际写指针,产生的读空则为"假空",因为实际上写地址已经变化了;同理,写满时,也会产生"假满"。“假空”、"假满"是一种保守操作,只是产生了假的空满标志,不会影响实际的fifo的数据读写。
// 5、同步后的地址有延迟,会造成地址多增加(如读空判断模块),怎么解决?
答:同步后的地址会导致空满标志滞后产生,从而影响地址多增加,属于空满保护的范畴,将gray地址转换成bin地址,限制地址过增。
// 6、同步器带来的影响是什么?是否致命?出现"假"空"假"满–>保守。
答:同4,是不致命的,实际上,异步fifo本身就不是100%的可靠性ram设计,有些问题必然存在。
// 7、快时钟域地址同步到慢时钟域,产生地址遗漏现象,漏掉的地址会产生致命影响吗?
答:本设计中,读时钟慢与写时钟(写的快,读的慢),在rd_logic中判断rd_empty的产生时,写指针同步到读时钟域,存在遗漏现象(慢时钟采样快时钟),在读写操作不同时进行时(若同时进行写操作),问题不大(不会影响空满标志产生),同时进行需要考虑是否会漏掉进行空标志产生所需要的关键指针(此时等待不到相等,则标志就产生不了)。
遗漏现象还会导致,格雷码的优势发挥不出,因为格雷码漏掉之后,相邻码之间就不是唯一1bit不同了,这就增加了重汇聚现象发生的概率。
// 8、判空判满标志产生逻辑的异同要理解
答:两者的判断相似却不同,结合代码仔细推敲。
// 9、读写同时进行如何考虑,本设计有隐患吗?
答:一般读慢,写快。当读得快写得慢时,为了避免出现问题需要扩展fifo_deepth。读写同时进行时,具体讨论结合仿真波形分析。
// 10、读写时钟的快慢是如何影响读写功能的?这种影响是否可以不考虑?
答:慢时钟域到快时钟域,只会出现保守问题,快时钟域到慢域,将出现漏采,漏掉的地址一般不会影响fifo的行为,但会影响fifo的效率。但如果漏掉关键指针则会出现逻辑错误。
实际上,这部分测试对FIFO的读写是没有太大影响的,但一般来说,FIFO是应该具有初始化功能的,这部分功能在RTL代码中描述不方便(容易造成额外的硬件资源开销)。故将该部分功能放在tb中,所以用户根据需求在操作时可以添加初始化操作。初始化的测试波形如下图4-1.1所示。观察波形,0-1023地址都被初始化为0,知初始化功能正常。
特别地,初始化与否并不影响FIFO的正常读写。若使用了初始化,因为这是写操作的一种特例,当写完所有地址后,会产生wr_full标志,且circle_high会取反置1,这样,后续进行用户数据的写入时,如果不做逻辑复位,则会使逻辑功能出错。故作如下处理:
添加init_en信号,作为逻辑复位的使能信号。该信号在tb中,初始化结束后产生,并持续1个wr_clk。复位逻辑清除的波形如下图4-1.2所示。
Tb中init_en的产生:
@ (posedge wr_clk); //等待wr_clk上升沿
init_en = 1; // init状态下的使能信号,用于wr_full和circle_high的复位
@ (posedge wr_clk); // 持续1个clk_high
init_en = 0;
初始化后完成逻辑复位,写入100个随机数据,而后依次读出数据直到产生rd_empty,如下图4-1.3所示。
读空后,写入1024个数据,直到写地址指向地址100,写指针再次追赶上读指针,FIFO写满标志wr_full产生,如下图4-1.4所示。
上述操作,写满标志wr_full产生后,再次使能写使能信号,观察FIFO中数据的变化情况,看到FIFO中100以后的地址并没有被新的数据给覆盖,则知写满保护功能有效。写满保护的仿真波形如下图4-1.5所示。
测试读空后是否有读空保护功能,测试波形如下图4-1.6所示。
Rd_en起来之后,wr_en一直有效连续写入数据,可以预测,这种情形下写的速度快于读的速度,最终的结果是,连续的wr_full产生,然后再rd_en下清零,写入数据后wr_full又产生wr_full,如此循环,此case下FIFO的效率低,不建议使用。测试的波形如下图4-2.1所示。
仿真结果与理论相符合,但出现了逻辑错误,即同一个地址吃掉了2-3个数据(发生在连续周期的wr_full信号段),使得写入的数据发生丢失现象,这在对数据要求场合较高时是不允许的。
逻辑协调wr_en间断有效,保证写入的数据快被读完后者读完后再次写入数据,如此循环控制,可使FIFO连续的读写。测试的波形如下图4-2.2所示。
观察细节,发现,写完数据后作读操作,最终读出的地址和数据会比上次写到的地址多1,原因是rd_en一直有效,在rd_empty产生间隙时,多读了一个数据,如果rd_en做好时序控制,可以避免该现象。此外,读空后再写入数据,而后读操作的起始地址和刚刚多读的地址一样,这相当于将该地址的数据读了两次,分别是写之前和写之后的。其中,多读数据的情况如下图4-2.3所示。在大多数情况下,这种情况应该没有太大的影响。
`timescale 1ns / 1ps
//--------------------------
// 问题记录:2020-06-19 20:00
// 1、空满标志产生(关键)
// 2、空满标志产生后,持续时间是多长?
// 3、是否有空满数据保护(满了不准写,或者空了不再读)?
// 4、地址跨时钟域同步是否对空满判断产生影响?
// 5、同步后的地址有延迟,会造成地址多增加(如读空判断模块),怎么解决
// 6、同步器带来的影响是什么?是否致命?出现“假”空“假”满-->保守
// 7、快时钟域地址同步到慢时钟域,产生地址遗漏现象,漏掉的地址会产生致命影响吗?
// 8、判空标志产生逻辑要理解
//--------------------------
module async_fifo_tb # (
parameter DAT_WID = 8 ,
parameter ADDR_WID = 11 ,
parameter FIFO_DEEPTH = 1024 ,
parameter PIPE_NUM = 3
);
reg rst_n;
reg wr_clk;
reg wr_en;
reg [DAT_WID - 1 : 0]wr_data;
wire [DAT_WID - 1 : 0]rd_data;
reg rd_en;
reg rd_clk;
reg init_en;
wire wr_full;
wire rd_empty;
wire [ADDR_WID - 1 : 0] rd_addr;
wire [ADDR_WID - 1 : 0] wr_addr;
integer tb_start;
//----------------------------
// 激励产生及初始化
//----------------------------
initial begin
rst_n = 0;
wr_clk = 0;
rd_clk = 0;
tb_start = 0;
wr_en = 0;
rd_en = 0;
init_en = 0;
wr_data = 1;
#200 rst_n = 1;
#300 tb_start = 1;
end
assign rd_addr = async_fifo_tb.async_fifo_test.rd_addr_bin;
assign wr_addr = async_fifo_tb.async_fifo_test.wr_addr_bin;
assign wr_en_latch_tb = async_fifo_tb.async_fifo_test.wr_logic_inst.wr_en_latch;
assign rd_en_latch_tb = async_fifo_tb.async_fifo_test.rd_logic_inst.rd_en_latch;
initial begin
wait(tb_start);
// 测试task用例
// basic_rd_wr_nocom();
ext_wr_rd_com();
end
// 生成读写时钟
always #20 wr_clk = ~wr_clk;
always #50 rd_clk = ~rd_clk;
//----------------------------
// task:fifo初始化
// 使用初始化功能吗,需要在逻辑上作
// 单独处理,因为初始化也是写fifo的一种
// 形式,初始化所有ram后,fifo的状态处于
// wr_full,如果继续写的话,新的内容被写进
// ram,但是wr_full需要rd_en有效才会清零
//----------------------------
task fifo_init (
);
begin
#100 ;
@ (negedge wr_clk);
wr_en = 1;
repeat (FIFO_DEEPTH) begin
wr_data = 0;
@ (posedge wr_clk);
end
@ (negedge wr_clk);
wr_en = 0;
wait(wr_full); // 等待写满标志产生
// 以下实现fifo_init造成的冗余逻辑的复位
@ (posedge wr_clk);
init_en = 1; // init状态下的使能信号,用于wr_full和circle_high的复位
@ (posedge wr_clk); // 持续1个clk_high
init_en = 0;
wait(~wr_full); // 等待写满标志清零
end
endtask
//----------------------------
// task:读写指针显示
//----------------------------
task display_ptr (
input [ADDR_WID -1 : 0] addr_ptr
);
reg wr_ptr;
begin
if (wr_en | wr_en_latch_tb) begin
wr_ptr = 1;
$display("(%0t) now ptr is wr_ptr", $time);
end else if (rd_en | rd_en_latch_tb) begin
wr_ptr = 0;
$display("(%0t) now ptr is rd_ptr", $time);
end
$display("(%0t) now addr_ptr is %d", $time, addr_ptr);
end
endtask
//----------------------------
// task:读写同时进行的逻辑测试
//----------------------------
task ext_wr_rd_com ();
begin
fifo_init(); // fifo清零初始化
$display("(%0t) wr_full = %b", $time, wr_full);
#200 wr_2_fifo(100); // 写入100个数据
wait (~wr_en_latch_tb); // 等待指针指向将要写入的地址
display_ptr(wr_addr);
#200 rd_en = 1; // 开始读fifo
wait(wr_addr == rd_addr);
// wait(rd_empty); // 等待rd_empty产生
display_ptr(rd_addr);
rd_en = 0;
$display("(%0t) rd_empty = %b", $time, rd_empty);
// 由于写时钟快于读时钟,为了避免写入数据过快覆盖掉旧的
// 未读出的数据,需要间隔wr_en信号
fork
begin
#200 wr_2_fifo(1000); // 写入1000个数据
end
begin
wait(wr_addr == 11'd500); // 中途开始读
rd_en = 1;
end
join
wait(rd_empty);
// wr_2_fifo(150); // 写入150个数据,用于间隔控制测试
wr_2_fifo(1000); // 不做wr_en间隔控制的测试
wr_2_fifo(1000); // 不做wr_en间隔控制的测试
#20000 wr_2_fifo(100); // 写入100个数据
rd_en = 0;
#1000 $stop;
end
endtask
//----------------------------
// task:读写分开的逻辑测试
//----------------------------
task basic_rd_wr_nocom ();
begin
fifo_init(); // fifo清零初始化
$display("(%0t) wr_full = %b", $time, wr_full);
#200 wr_2_fifo(100); // 写入100个数据
wait (~wr_en_latch_tb); // 等待指针指向将要写入的地址
display_ptr(wr_addr);
#200 rd_en = 1; // 开始读fifo
wait(wr_addr == rd_addr);
// wait(rd_empty); // 等待rd_empty产生
display_ptr(rd_addr);
rd_en = 0;
$display("(%0t) rd_empty = %b", $time, rd_empty);
#200 wr_2_fifo(924); // 写入100个数据
#200 wr_2_fifo(100); // 写入100个数据
$display("(%0t) wr_full = %b", $time, wr_full); // 观察wr_full状态
wr_2_fifo(10); // fifo满后,测试是否具有写满保护功能
#200 rd_en = 1; // 开始读数据
$display("(%0t) rd_empty = %b", $time, rd_empty);
wait(rd_en_latch_tb); // 让读指针至少增加1,防止rd_en马上变成0
wait(wr_addr == rd_addr);
// wait (rd_empty);
rd_en = 0;
wait(rd_empty); // 等待rd_empty产生
#200 rd_en = 1; // 测试读空后是否有读空保护(实际上,fifo数据被读出,内容还是存在ram中,如果读空后继续读,则会读到重复的数据)
#1000 rd_en = 0;
#400 $stop;
end
endtask
//----------------------------
// task:数据产生,送入fifo中
//----------------------------
task wr_2_fifo (
input [ADDR_WID - 1 : 0] num // 向fifo中存入数据的个数
);
// 两种产生数据激励的方式都可以
// begin
// #100 ;
// @ (negedge wr_clk);
// wr_en = 1;
// repeat (num) begin
// wr_data = {$random} % 127;
// @ (posedge wr_clk);
// end
// @ (negedge wr_clk);
// wr_en = 0;
// end
begin
#100 ;
@ (posedge wr_clk);
wr_en = 1;
repeat (num) begin
wr_data = {$random} % 127;
@ (posedge wr_clk);
end
wr_en = 0;
end
endtask
//----------------------------
// 顶层功能模块例化
//----------------------------
async_fifo # (
.DAT_WID (DAT_WID) ,
.ADDR_WID (ADDR_WID) ,
.PIPE_NUM (PIPE_NUM)
) async_fifo_test (
.rst_n (rst_n) ,
// 外部输入
.wr_data (wr_data) ,
.wr_en (wr_en) ,
.wr_clk (wr_clk) ,
.init_en (init_en) ,
.rd_en (rd_en) ,
.rd_clk (rd_clk) ,
// fifo输出
.rd_data (rd_data) ,
.wr_full (wr_full) ,
.rd_empty (rd_empty)
);
endmodule
以上只给出了部分case的功能测试,一定存在考虑不周之处,欢迎指正!
当读速率慢于写速率时,FIFO便可被用作系统中的缓冲元件或队列。因此FIFO的大小基本上暗示了所需缓存数据的容量,该容量取决于读写数据的速率。据统计,系统的数据速率取决于系统的负载能力。因此为了保证FIFO的大小,我们需要考虑FIFO传输的最坏情况下。所谓最坏的情况就是使得写速率最大,读速率最小;通常是考虑突发传输。
4.4.1 读写不同时进行
这种情形,突发wr_en到来,在接下来的一段时间内,FIFO一直被写入数据,直到写满。故FIFO_deepth=突发数据个数。
4.4.2 读写同时进行
这种情形,写的快,读的慢,但由于rd_en的存在,影响了整体的数据处理速度,推导前设以下几个变量:
1、写时钟wr_clk的频率Fwr;
2、读时钟rd_clk的频率Frd;
3、突发数据个数N_burst,突发时间为T_burst;
4、Y个rd_clk内读出的数据个数X,每个数据读出的时间为trd;
5、符合要求的FIFO深度最小值Deepth;
则,最小深度=突发数据个数-突发时间内被读出的数据个数,即,
举例, 如果100个写时钟周期可以写入80个数据,10个读时钟可以读出8个数据。令wclk=rclk ,考虑背靠背(20个clk不发数据+80clk发数据+80clk发数据+20个clk不发数据的200个clk,背靠背即最坏情况)
代入公式可计算FIFO的深度:
特别地,若Y个rd_clk时钟内读出的数据Y个,即每个时钟读出一个数据,则上述式4-1变成:
例,写50MHz,读40MHz,要不丢失读出10万个数据,则至少设置D =?
答:D=100000*(1-0.8)=20000=20K,需要设置20K深度的FIFO。
实际应用中往往是以半空半满信号来指示fifo的空满状态的,所以实际设计fifo的时候会至少留下一半深度度的裕量。或者,设置的FIFO深度比最大突发数据量多1倍,以避免数据被覆盖。
最后,给出整个工程的RTL代码链接:async_fifo_sys下载链接(点击跳转到下载界面),欢迎下载和交流!