Cummings——异步FIFO第一讲

0. 参考

Simulation and Synthesis Techniques for Asynchronous FIFO Design — Clifford E. Cummings, Sunburst Design

1. 异步FIFO

在跨时钟域传输的时候容易发生亚稳态。当在不同时钟域之间传递的多个信号时,需要用到异步FIFO。

异步FIFO的难点在于生成读写地址和空满指示位。

2. FIFO指针

2.1 同步fifo指针

​ 对于同步FIFO而言,读写时钟相同,可以用一个计数器来表示FIFO的状态,如果只写数据,则计数增加;如果只读,则计数减少;级读又写,则计数不变。当计数到某个值,表示FIFO为满;计数为零,表示FIFO空。此时的读地址在每次读有效的时候增加就可以了,当读到最高为会回到零;写地址一样。

2.2 异步FIFO指针

写指针:写指针指向下一个要写的地址。当FIFO reset之后,写指针为0。当写操作有效的时候,在下一个时钟沿出向写地址指向的位置写数据,然后写指针累加,指向下一个写地址。

读地址:读指针也要指向下一个要读的地址,FIFO reset之后,读指针也为0,此时空标志位有效,当写了一个数据后,空表示为清零。读操作有效的时候,在下一个时钟沿,向读地址指示的位置读数据,并使读地址累加。

满状态:如果写地址赶上读地址,此时读写地址相同,写地址将读地址套圈了,那么FIFO满了。

空状态:reset之后是空;当读地址赶上写地址,FIFO空。

地址上增加一位空满标志位:n位地址,最高位为标志位,低n-1位位真正的FIFO地址。除了最高位,读写地址相同,则为满;如果读写地址相同——最高位也相同,那么空。

2.3 二进制的地址问题

​ 二进制地址累加时,相邻地址经常存在多位同时跳变比如01到10,有两位同时跳变。在异步采样时可能没有采样到同时跳变后的值,可能采样到00,11的情况。

解决:用格雷码表示地址

3.格雷码计数器

3.0 跨时钟域中使用格雷码的作用:

  1. 相邻码只改变一位,降低亚稳态的发生。
  2. 减少地址翻转,降低功耗。
  3. 即使发生采样错误也不会产生功能错误。地址改变时,跨时钟采样可能采样到原来值,或者改变后的值。比如读地址同步到写时钟域,采样到了原来的值,产生最差的情况是不满的时候判断为满,只是会延迟一点写操作,不会产生功能错误。
  4. 并且,即使发生采样错误,读地址延迟了一个写时钟周期,那么在下一次采样读地址的时候,如果采样正确,那么不会存在地址延迟的情况;即使下一次还是采样到了亚稳态,那么地址还是延迟一个周期,并不是说延迟累加,两次采样错误延迟了两个周期。

特点
1.相邻码只改变1bit
2.只能用来记2的指数倍数,不能记奇数个数

如下图:

格雷码与二进制的转换:

格雷码计数器的功能:在写时钟域内,二进制地址累加,然后将二进制的写地址转换成格雷码地址,格雷码地址要传给读时钟域;在写时钟域中格雷码转换器将二进制的读地址转换成格雷码地址,格雷码地址要传给写时钟域。

3.1 第一种格雷码计数器

一种格雷码编码风格如下,只需要一组地址寄存器来保存地址:

​ 上图中上半部分表示生成n位的格雷码;下半部分红框部分将格雷码最高位与次高位异或,然后将生成的addrmsb与格雷码的低n-2位合并成一个N-1位的格雷码地址,不知有什么用??

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 上半图的n位格雷码计数器
wire clk,rst_n;
wire [n-1:0] bin,bnext,gnext;
wire inc,full; // 写时钟域的格雷码转换器
reg [n-1:0] ptr; //输出的n位格雷码
integer i;
always@(ptr) begin //格雷码转二进制
bin[n-1] = ptr[n-1];
for(i=n-2;i>=0;i=i+1)
bin[i] = bin[i+1] ^ bin[i];
end
assign bnext = bnext+(inc && !full)?1:0;
assign gnext = (bnext>>1) ^ bnext; //二进制转格雷码
always @(posedge clk or negedge rst_n) begin
if(rst_n==1'b0) ptr<=0;
else begin
ptr <= bnext;
end
end

3.2 第二种格雷码计数器

用寄存器保存二进制码,去除格雷码到二进制的转换逻辑。

减小寄存器之间的组合逻辑长度,可以增加频率,特别是在FPGA中。

如下图:

1
2
3
4
5
6
7
8
9
10
11
12
13
wire clk,rst_n;
wire [n-1:0] bnext,gnext;
wire inc,full; // 写时钟域的格雷码转换器
reg [n-1:0] ptr; //输出的n位格雷码
reg [n-1:0] bin;
integer i;

assign bnext = bnext+(inc && !full)?1:0;
assign gnext = (bnext>>1) ^ bnext; //二进制转格雷码
always @(posedge clk or negedge rst_n) begin
if(rst_n==1'b0) {ptr,bin}<=0;
else {ptr,bin} <={gnext,bnext};
end

4. fifo结构图

5. 空满标志位

空标志位在读时钟域产生;满标志位在写时钟域产生。

5.1 空标志位

地址比实际地址增加一位。

读时钟域的格雷码读地址 与 同步过来的格雷码写地址相同,说明满

1
2
3
4
assign rempty_val = (rgraynext == rq2_wptr); // 读时钟域的格雷码读地址 与 同步过来的格雷码写地址相同,说明满
always @(posedge rclk or negedge rrst_n) // 读时钟
if (!rrst_n) rempty <= 1'b1;
else rempty <= rempty_val;

5.2 满标志位

地址比实际地址增加一位。

根据格雷码的特性,写时钟域与的格雷码写地址与同步过来的格雷码读地址相比,最高两位都不同,其他位相同,说明满了。

1
2
3
4
assign wfull_val = (wgraynext=={~wq2_rptr[ADDRSIZE:ADDRSIZE-1],wq2_rptr[ADDRSIZE-2:0]});
always @(posedge wclk or negedge wrst_n)
if (!wrst_n) wfull <= 1'b0;
else wfull <= wfull_val;

5.3 不同时钟频率的考虑

问题一:快时钟是慢时钟的两倍,那么快域地址变化两次,慢时钟域采样一次,前后采样值变化了两次,会产生多位同步的问题吗?

​ 不会。快时钟域第一次改变一位,比如从A到B,慢时钟域没有采样,当快时钟域改变第二次B到C之后,慢时钟域才采样,虽然这期间快时钟域的地址从A到C变了两次,但是慢时钟域只看到第二次B到C,只跳变了一位,因此不会产生多位同步问题。

问题二:快时钟域是否会引起full+1的情况——写溢出,或者empty+1——读溢出?

​ 不会。对于满是在写时钟域产生,如果写时钟比读时钟快,如果waddr追上raddr,那么full有效,此时不能再写了,也就不会产生full+1情况。对于空是在写时钟域产生,如果读时钟比写时钟快,如果raddr赶上waddr,那么就不能再读,也就不会产生empty+1.

5.4 空满标志位取消

​ 空满的set是立即生效的。比如读时钟域中,将采样到的写时钟域的格雷码写地址 与当前的读地址比较,如果相等,则立马使empty有效;full类似。但是读写标志位clear是有延迟的。

​ 当empty有效的时候,读时钟域采样的写地址与读地址相同,如果此时写入数据,那么在写时钟域里写地址是增加的,但是这个增加了的写地址需要两个读时钟的同步才能让读时钟域里的地址比较器看到,所以empty的clear有两个读时钟周期的延迟;同样full的clear有两个写时钟周期的延迟。

​ 但这不会使FIFO发生功能错误,可忽略。

5.5 reset时候地址多位跳变

没影响,因为reset就表明FIFO里的数据是无效的,此时不对FIFO进行读写。

6. 代码

这些代码是Cummings论文中给的。

6.1 顶层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
module fifo1 #(parameter DSIZE = 8,
parameter ASIZE = 4)
(output [DSIZE-1:0] rdata,
output wfull,
output rempty,
input [DSIZE-1:0] wdata,
input winc, wclk, wrst_n,
input rinc, rclk, rrst_n);
wire [ASIZE-1:0] waddr, raddr;
wire [ASIZE:0] wptr, rptr, wq2_rptr, rq2_wptr;
sync_r2w sync_r2w (.wq2_rptr(wq2_rptr), .rptr(rptr),
.wclk(wclk), .wrst_n(wrst_n));
sync_w2r sync_w2r (.rq2_wptr(rq2_wptr), .wptr(wptr),
.rclk(rclk), .rrst_n(rrst_n));
fifomem #(DSIZE, ASIZE) fifomem
(.rdata(rdata), .wdata(wdata),
.waddr(waddr), .raddr(raddr),
.wclken(winc), .wfull(wfull),
.wclk(wclk));
rptr_empty #(ASIZE) rptr_empty
(.rempty(rempty),
.raddr(raddr),
.rptr(rptr), .rq2_wptr(rq2_wptr),
.rinc(rinc), .rclk(rclk),
.rrst_n(rrst_n));
wptr_full #(ASIZE) wptr_full
(.wfull(wfull), .waddr(waddr),
.wptr(wptr), .wq2_rptr(wq2_rptr),
.winc(winc), .wclk(wclk),
.wrst_n(wrst_n));
endmodule

6.2 内存读写模块

​ 读数据是直接从mem中读,不需要时钟。读地址A指示的是下一次要读的地址——此时FIFO不为空,读A地址是有效的。当前rdata信号上保存的是下一次要读的数据。如果外部读时钟域打算读数据,那么给一个读有效rinc,在rclk上升沿就可以直接把rdata取走,在rinc上升沿FIFO内部会根据A+1地址判断是否empty。(可以参考3.2节)。

​ 写数据winc有效时,下一个写时钟沿wclk要写入数据。满标志表示如果在下一个时钟沿写数据,就写到读地址处(读写地址相同)。如果满,则下一个时钟沿不能写。所以在下一个写时钟沿到来时要判断满标志,如果满了,则不能写。

​ 同样,读地址表示下一个读时钟沿要读的数据,空表示下一个时钟沿是否可以读。对于读数据的设备,它需要在读的时候判断是否空了,至于FIFO的rdata输出端则不需要进行empty判断,FIFO将下一次要读的数据放在rdata处,如果读数据的设备要读,就在rclk时钟沿读就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module fifomem #(parameter DATASIZE = 8, // Memory data word width
parameter ADDRSIZE = 4) // Number of mem address bits
(output [DATASIZE-1:0] rdata,
input [DATASIZE-1:0] wdata,
input [ADDRSIZE-1:0] waddr, raddr,
input wclken, wfull, wclk);
`ifdef VENDORRAM
// instantiation of a vendor's dual-port RAM
vendor_ram mem (.dout(rdata), .din(wdata), //这一块不用管
.waddr(waddr), .raddr(raddr),
.wclken(wclken),
.wclken_n(wfull), .clk(wclk));
`else
// RTL Verilog memory model
localparam DEPTH = 1<<ADDRSIZE; //将地址位数转化成FIFO深度
reg [DATASIZE-1:0] mem [0:DEPTH-1];
assign rdata = mem[raddr]; //直接读,empty标志位影响raddr的累加
always @(posedge wclk)
if (wclken && !wfull) mem[waddr] <= wdata;//写需要判断full,并且在时钟沿处写
`endif
endmodule

6.3 读地址到写时钟域同步

写时钟控制,打两拍

1
2
3
4
5
6
7
8
9
module sync_r2w #(parameter ADDRSIZE = 4)
(output reg [ADDRSIZE:0] wq2_rptr,
input [ADDRSIZE:0] rptr,
input wclk, wrst_n);
reg [ADDRSIZE:0] wq1_rptr;
always @(posedge wclk or negedge wrst_n)
if (!wrst_n) {wq2_rptr,wq1_rptr} <= 0;
else {wq2_rptr,wq1_rptr} <= {wq1_rptr,rptr};
endmodule

6.4 写地址到读时钟域同步

读时钟控制,打两拍

1
2
3
4
5
6
7
8
9
module sync_w2r #(parameter ADDRSIZE = 4)
(output reg [ADDRSIZE:0] rq2_wptr,
input [ADDRSIZE:0] wptr,
input rclk, rrst_n);
reg [ADDRSIZE:0] rq1_wptr;
always @(posedge rclk or negedge rrst_n)
if (!rrst_n) {rq2_wptr,rq1_wptr} <= 0;
else {rq2_wptr,rq1_wptr} <= {rq1_wptr,wptr};
endmodule

6.5 空标志位产生

产生空标志位、n-1位当前的二进制读地址、n位格雷码读地址

产生空标志位的结构框图如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
module rptr_empty #(parameter ADDRSIZE = 4)
(output reg rempty,
output [ADDRSIZE-1:0] raddr,
output reg [ADDRSIZE :0] rptr,
input [ADDRSIZE :0] rq2_wptr,
input rinc, rclk, rrst_n);
reg [ADDRSIZE:0] rbin;
wire [ADDRSIZE:0] rgraynext, rbinnext;
wire rempty_val;
//-------------------
// GRAYSTYLE2 pointer
//-------------------
always @(posedge rclk or negedge rrst_n) //格雷码计数器的第二种写法
if (!rrst_n) {rbin, rptr} <= 0;
else {rbin, rptr} <= {rbinnext, rgraynext};
// Memory read-address pointer (okay to use binary to address memory)
assign raddr = rbin[ADDRSIZE-1:0];//给到memory模块
assign rbinnext = rbin + (rinc & ~rempty);
assign rgraynext = (rbinnext>>1) ^ rbinnext;
//---------------------------------------------------------------
// FIFO empty when the next rptr == synchronized wptr or on reset
//---------------------------------------------------------------
assign rempty_val = (rgraynext == rq2_wptr);//空判断是根据下一次要读的地址来判断的
always @(posedge rclk or negedge rrst_n)
if (!rrst_n) rempty <= 1'b1;
else rempty <= rempty_val;
endmodule

6.6 满标志产生

产生满标志、n-1位当前写地址、n位格雷码写地址

产生满标志位的结构框图如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
module wptr_full #(parameter ADDRSIZE = 4)
(output reg wfull,
output [ADDRSIZE-1:0] waddr,
output reg [ADDRSIZE :0] wptr,
input [ADDRSIZE :0] wq2_rptr,
input winc, wclk, wrst_n);
reg [ADDRSIZE:0] wbin;
wire [ADDRSIZE:0] wgraynext, wbinnext;
wire wfull_val;
// GRAYSTYLE2 pointer
always @(posedge wclk or negedge wrst_n)
if (!wrst_n) {wbin, wptr} <= 0;
else {wbin, wptr} <= {wbinnext, wgraynext};
// Memory write-address pointer (okay to use binary to address memory)
assign waddr = wbin[ADDRSIZE-1:0];
assign wbinnext = wbin + (winc & ~wfull);
assign wgraynext = (wbinnext>>1) ^ wbinnext;
//------------------------------------------------------------------
// Simplified version of the three necessary full-tests:
// assign wfull_val=((wgnext[ADDRSIZE] !=wq2_rptr[ADDRSIZE] ) &&
// (wgnext[ADDRSIZE-1] !=wq2_rptr[ADDRSIZE-1]) &&
// (wgnext[ADDRSIZE-2:0]==wq2_rptr[ADDRSIZE-2:0]));
//------------------------------------------------------------------
assign wfull_val = (wgraynext=={~wq2_rptr[ADDRSIZE:ADDRSIZE-1],wq2_rptr[ADDRSIZE-2:0]});
// 空判断是根据下一次要写的地址判断的。
always @(posedge wclk or negedge wrst_n)
if (!wrst_n) wfull <= 1'b0;
else wfull <= wfull_val;
endmodule

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!