FIFO我们一般的理解是缓存,也就是收发不能完全周期同步,先放在里面保存一下,保存的逻辑是先进来的先出去,这种缓冲队列的思路尤其适合于数据流特征的数字系统。
在双时钟使用的FIFO不但具有上述缓冲功能,并且还可以实现数据的时钟区域穿越,这就比较厉害了~~~。实现数据在不同时钟区域的穿越是异步设计中比较关键的问题,而FIFO就这样轻易解决了(当然具体实现时候还要配合必要的约束),所以就有了以异步时钟区域数据传递为目的的应用。这其实是弱化了DC_FIFO的FIFO缓冲功能,发挥了DC_FIFO的双时钟数据穿越DC功能。
看代码说话:
module cdc_fifo # (parameter aw =4,parameter dw =8 )(
input wr_clk, rst,wr_en ,
input [7:0] wr_dat,
input rd_clk,
output [7:0] rd_dat,
output reg rd_vd
);
wire rst_n = ~ rst ;
reg[7:0] st ;
wire rd_fifo = ~empty ;
always @ (posedge rd_clk ) rd_vd <= rd_fifo ;
wire [1:0]rd_level,wr_level;
generic_fifo_dc_gray #(.aw(aw),.dw(dw)) generic_fifo_dc_gray (
.rd_clk(rd_clk) ,
.wr_clk(wr_clk) ,
.rst(rst_n) ,
.clr(~rst_n) ,
.din(wr_dat ) ,
.we(wr_en) ,
.dout(rd_dat ) ,
.re(rd_fifo ) ,
.full(full) ,
.empty(empty ) ,
.wr_level(wr_level) ,
.rd_level(rd_level)
);
endmodule
这里是用在和PHY芯片连接的一个的一部分逻辑,这里很显然有两个特点:
1,只要FIFO非空就是被读出(并保持输出)。
2,因为1,FIFO永远为空,跟不会满,所以empty=1,full=0,也就没有必要引出这个full和empty信号了。
如果我们要在数据更新的那个周期告知rd_clk时钟区,就可以用一个将rd_vd用rd_clk打一个周期后输出(FIFO数据的输出是在rd_vd=1的下一个周期,详见上两篇中的代码分析和仿真)。
----------------------------------------------
我这几天在做UDP通讯的VERILOG实现,用到了一个时钟穿越,就是千兆以太网的PHY芯片传递给FPGA的数据段使用的自己的125M时钟,RX_CLK伴随这RX_DATA和RX_DV(DV=Data Valid)一起加在FPGA的引脚。我们必须使用RX_CLK在DV=1时采样和保存DATA数据,传递给UDP工作的主时钟区域。
DC_FIFO在这需求里刚好可以实现。在这个实现中,我们在使用DC FIFO时需要考虑一下几点具体问题:
1,要穿越时钟区域两端都是125M,但是不同源。
2,PHY一旦开始传输,就不会产生字节间断,直到此帧完全结束。
3,我们设计PHY上层的MAC层是2这样想得,所以MAC接受层也没有靠字节间断,所以要求经过DC FIFO帧内字节还是连续的。
结合这三点显然,上述三点的满足我们可以这样实现:FIFO本身就是队列,可以保存若干个条目,我们保存比如8个条目,我们先等待写入3个或者以上,之后再触发读使能,再非空前全部读完。因为时钟频率一样,因此不会产生写满也不会读空。
我们实现代码如下:
module cdc_fifo # (parameter aw =4,parameter dw =8 )(
input wr_clk, rst,wr_en ,
input [7:0] wr_dat,
input rd_clk,
output [7:0] rd_dat,
output reg rd_vd
);
wire rst_n = ~ rst ;
reg[7:0] st ;
wire rd_fifo = ( st == 20 ) && ( ~empty ) ;
always @ (posedge rd_clk ) rd_vd <= rd_fifo ;
wire [1:0]rd_level,wr_level;
generic_fifo_dc_gray #(.aw(aw),.dw(dw)) generic_fifo_dc_gray (
.rd_clk(rd_clk) ,
.wr_clk(wr_clk) ,
.rst(rst_n) ,
.clr(~rst_n) ,
.din(wr_dat ) ,
.we(wr_en) ,
.dout(rd_dat ) ,
.re(rd_fifo ) ,
.full(full) ,
.empty(empty ) ,
.wr_level(wr_level) ,
.rd_level(rd_level)
);
always @ (posedge rd_clk or posedge rst) if ( rst ) st <= 0;else case (st)
0: st<=10;
10: if (rd_level == 2'b10)st<=20;
20: if (empty) st<=10;
default st<=0;endcase
endmodule
这里使用状态机是最直接的思路实现,我们看到检测rd_level是2'B10
wr_level indicates the FIFO level:
2'b00 0-25% full
2'b01 25-50% full
2'b10 50-75% full
2'b11 %75-100% full
rd_level indicates the FIFO level:
2'b00 0-25% empty
2'b01 25-50% empty
2'b10 50-75% empty
2'b11 %75-100% empty
也就是有了25%-50%的数据存在FIFO里面,比如FIFO深度为8,那么就有2或者3或者4个内容已经被写入到FIFO里,这时候我们就进入状态20,允许读了,直到读空了才进入状态10从新判断先一次满足内存条目为2,3,4的情况。
当然这里可以省略状态机,直接用一个一位寄存器来实现;设置一个允许读寄存器allow_rd,在rd_level==2'b10时候就是有2,3,4个条目时候设置为1以表示允许读出,在empty为空的时候就设置为0,防止下次条目少于2时候被读出。
module cdc_fifo2 # (parameter aw =4,parameter dw =8 )(
input wr_clk, rst,wr_en ,
input [7:0] wr_dat,
input rd_clk,
output [7:0] rd_dat,
output reg rd_vd
);
wire rst_n = ~ rst ;
reg allow_rd ;
wire rd_fifo = allow_rd & ( ~empty ) ;
always @ (posedge rd_clk ) rd_vd <= rd_fifo ;
wire [1:0]rd_level,wr_level;
generic_fifo_dc_gray #(.aw(aw),.dw(dw)) generic_fifo_dc_gray (
.rd_clk(rd_clk) ,
.wr_clk(wr_clk) ,
.rst(rst_n) ,
.clr(~rst_n) ,
.din(wr_dat ) ,
.we(wr_en) ,
.dout(rd_dat ) ,
.re(rd_fifo ) ,
.full(full) ,
.empty(empty ) ,
.wr_level(wr_level) ,
.rd_level(rd_level)
);
always @ (posedge rd_clk or posedge rst) if ( rst ) allow_rd <= 0;
else if (rd_level == 2'b10)allow_rd<=1;else if (empty) allow_rd<=0;
endmodule
其实我们说rd_level是2'B10表示内部有2,3,4个条目,那会不会达到5,6,7,8个条目,这样我们的代码就没有对应的处理,代码就有BUG了。不会的,因为一旦满足有2个条目,触发了读,2个周期之后FIFO被一直读,两者读写速度相等,所以会保持一个FIFO内有固定4条,直到不再对FIFO写,读出端继续读,直到empty。
这里实际是一个读写频率都一样(也就是说短时间内差别可忽略)的特例,如果有一定的相差频率,则要从两个方面进行分析和解决
1,上层处理模块允许帧内字节数据有间断。
2,设置足够大的FIFO保证在慢速读出的时候FIFO不产生溢出。