跨时钟域的典型:异步FIFO的设计

跨时钟域的典型范例——异步FIFO

“时钟域(clock domain)”可以说是数字集成电路中一个非常重要的内容了,那么,何谓“跨时钟域”?

很好理解,在时序逻辑电路中,所有触发器、寄存器的运行都是由时钟激励而运行下去的。而一个大型的数字系统中不可能只有一种时钟:

例如Cortex-M3软核常常运行在50~100MHz、而UART串行口波特率要在921600以上的话,输入时钟频率最好高于200MHz、FPGA上的DDR3一般要求200MHz的时钟输入,不同PLL/MMCM输出的同频率时钟它们的相位也有可能不同……

这么多不同时钟激励的系统要组合在一起并能进行数据交互,那么势必会遇到“跨时钟域”的问题,解决这个问题的一大方法之一就是利用异步FIFO进行数据交互

什么是FIFO

FIFO(First In First Out),顾名思义,即先输入先输出的一种模块,FIFO一般分为同步FIFO和异步FIFO两种,同步FIFO即输入和输出在同一个时钟域中,异步FIFO即输入输出不处于同一个时钟域。

FIFO的存储模块可以看成一个二维寄存器(当然也可以通过生成ram代替),因此有些概念需要掌握,即:

  1. FIFO宽度:指FIFO中一个空间容纳的数据比特数,即一个数据单位的大小,例如你要输入FIFO的是16位的数据,那么FIFO的宽度即为16bit
  2. FIFO深度:指FIFO中能容纳多少个数据单元,在异步FIFO中这个值往往是可以被计算出来的,例如:
    在一个FIFO中,写时钟为100MHz,读时钟为50MHz,突发长度(一个时间段内需要读写的数据量)为120,那么写一个数据需要1/100MHz=10ns,读一个数据需要1/50MHz=20ns,写完一个突发长度耗时120*10=1200ns,而在写入的这段时间中只能读出1200ns/20ns=60个数据,那么在这一次突发传输中还有120-60=60个数据没有被读走,因此这个情况下FIFO的最小深度为60

到底如何解决跨时钟域的问题

有的同志可能会立刻回答:用FIFO!那好,FIFO为什么能解决跨时钟域的问题呢?异步FIFO自己又是如何解决输入输出时钟不同域的问题呢?

还是得从跨时钟域的深层次原因来看这个问题:

单bit跨时钟域

当以时钟域CLK-A为激励源的寄存器输出信号给时钟域为CLK-B的寄存器时,会出现什么情况?

DFF跨时钟域

用verilog描述为:

1
2
3
4
5
6
7
8
reg Q1,Q2;
always@(posedge clk_a) begin
Q1 <= data;
end

always@(posedge clk_b) begin
Q2 <= Q1;
end

显然,Q1上的数据跨过了两个时钟域,那么如果当经过clk-a时输出的Q1不稳定时,clk-b端的DFF采样到的Q1数据将会是不可预测的,因为这两个时钟并非同源,所以可能无法满足第二个DFF的建立和保持时间,会让第二个DFF处于亚稳态。

那么如何解决这个问题呢?这个问题实际上无法解决,但我们可以让它(指不满足另一个DFF的setup和hold time,处于亚稳态)发生的概率几乎为零,有没有方法呢?有!“打两拍”处理即可

“打两拍”指让数据经过两个后级寄存器(两级DFF同步),具体为什么这样做就能有想降低出错概率,可以参考这篇文章:知乎-跨时钟域同步,为什么两级寄存器结构能够降低亚稳态?具体的电路是这样的:

打两拍消除亚稳态

用verilog描述为:

1
2
3
4
5
6
7
8
9
reg Q1,Q2,Q3;
always@(posedge clk_a) begin
Q1 <= data;
end

always@(posedge clk_b) begin
Q2 <= Q1;
Q3 <= Q2;
end

这样,第三个DFF处于亚稳态的概率大幅降低了,然而这是单个bit跨时钟域的情况,如果是多个bit呢?

多bit跨时钟域

在异步FIFO中,不论是写入数据还是取出数据,都是对同一个Memory操作的,两者彼此独立,不会产生跨时钟域的问题,那么异步FIFO中跨时钟域的地方在哪里呢?在FIFO状态的判断上

应该了解到,FIFO是有几个状态判断标志的,例如:全空、全满、即将空和即将满等等状态,这些状态是根据写入和读取指针决定的,当写入指针等于读取指针的时候,FIFO为全空;当写入指针超过读取指针一圈(指写到FIFO底部后又重新从头开始写,再一次与读取指针相遇)的时候,FIFO为全满。那么显然,我们要获取FIFO的状态就得对读和写指针进行判断了,但是这两个指针分别是在不同时钟域内的,所以这时候便产生了多bit(正经fifo指针不可能只有1bit)跨时钟域的问题。

那么如何解决这个问题呢?打拍还能解决吗?我们来分析一下:

假如现在的写入指针wr-ptr = 4’b0111,读取指针rd-ptr = 4’b0011,即将在写入和读出的下一个上升沿判断指针,而写入时钟频率高于读取时钟频率,那么判断的时候可能会出现的情况为:

  1. 最好的情况:刚好写入和读取的时钟上升沿在同一时刻了,并且数据都满足了建立和保持时间后开始进行指针之间的比较,完全没有问题,比较的结果也是正确的。
  2. 出了点状况:读取指针的时钟在这一时刻翻转了,有了一个上升沿,这个时候读取的数据还没有准备好(DFF处于亚稳态),rd-ptr = 4’b0zzz(后三bit无法确定,因为不稳定),这下完了没法比较了。
  3. 又出了点状况:写入指针也恰好没法满足时序条件,而且写入指针的四位BCD码都即将进行翻转(0111->1000),这下彻底完蛋了,写入和读取指针在这个状态下完全是不可测的

显然,出现第一种情况的概率小到可以忽略(😅),那么这么严重的问题怎么解决呢?一个个看:

解决第2种情况的方法只需要我们进一步思考就行了,既然是读取指针没法满足写入指针的DFF输出数据的建立和保持条件,那么我再加一个DFF把读取指针同步到写入时钟域(或者把写入指针同步到读取时钟域)不就行了?没错,但是这样的话依旧无法解决第三个问题,怎么办?

这个问题再进一步思考,其实是BCD码自身的缺陷问题,从0111自增一次变成1000的时候,四个位都变化了,这就导致有16种可能,那么有没有一种编码方式能让每次自增只变化一位码的呢?还真有,格雷码

格雷码每自增一次,只会翻转一个bit,具体可以自己查一下,这样的话,如果从格雷码0111(0101BCD)跳转到下一状态0101(0110BCD)的时候,只有一位改变了,不论你建立和保持时间是否满足,其余三位都是不会变的,也就是说即使处于亚稳态,这个时候另一个时钟域读取到的数据也只可能是0111或者0101,况且我们还有对单bit跨时钟的解决办法—打两拍,因此这对我之后的比较几乎没有影响(关于利用格雷码解决异步FIFO的问题的详细解释,这个回答知乎-异步fifo格雷码同步问题讲解的非常清楚)

那么这个问题就解决了,可以开始着手设计FIFO了。

异步FIFO的设计

为了遵循模块化和参数化的设计标准,同时方便以后直接生成BRAM实现FIFO,我将异步FIFO拆分成控制端和数据端,控制端负责接收写入读取信号,产生写入读取指针,并转换成格雷码判断FIFO状态;数据端负责接受写入读取指针和写入读取信号以及写入的数据,输出读取的数据。把上面的过程分析清楚了,写出整个设计毫无困难:

异步FIFO控制模块

直接上代码:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
module fifo_ctrl #(
parameter Addr_Width = 5
) (
input reset_n,
input wr_clk,
input wr_ena,
input rd_clk,
input rd_ena,
output empty,
output full,
output [Addr_Width:0] wr_addr_ptr,//写地址指针,比地址位多1位是为了检测到回环
output [Addr_Width:0] rd_addr_ptr
);
reg wr_rstn;
reg rd_rstn;
reg [Addr_Width:0] wr_addr_count;
reg [Addr_Width:0] rd_addr_count;
wire [Addr_Width:0] wr_addr_gray; //转换成格雷码
reg [Addr_Width:0] wr_addr_gray_reg1;
reg [Addr_Width:0] wr_addr_gray_reg2;
wire [Addr_Width:0] rd_addr_gray;
reg [Addr_Width:0] rd_addr_gray_reg1;
reg [Addr_Width:0] rd_addr_gray_reg2;


/*写入指针控制*/
//异步复位,同步释放,相当于把resetn打一拍避免亚稳态
always @(posedge wr_clk ) begin
if (!reset_n)
wr_rstn <= 1'b0;
else
wr_rstn <= 1'b1;
end

//写入指针变化,并记录写入地址
always @(posedge wr_clk or negedge wr_rstn) begin
if (!wr_rstn) begin
wr_addr_count <= 0;
wr_addr_gray_reg1 <= 0;
wr_addr_gray_reg2 <= 0;
end
else if (wr_ena & ~full) begin
wr_addr_count = wr_addr_count + 1'b1;
end
else
wr_addr_count = wr_addr_count;
end
assign wr_addr_ptr = wr_addr_count - rd_addr_count;

/*读取指针控制*/
//异步复位,同步释放,相当于把resetn打一拍避免亚稳态
always @(posedge rd_clk ) begin
if (!reset_n)
rd_rstn <= 1'b0;
else
rd_rstn <= 1'b1;
end

//读取指针变化,并记录读取地址
always @(posedge rd_clk or negedge rd_rstn) begin
if (!rd_rstn) begin
rd_addr_count <= 0;
rd_addr_gray_reg1 <= 0;
rd_addr_gray_reg2 <= 0;
end
else if (rd_ena & ~empty) begin
rd_addr_count = rd_addr_count + 1'b1;
end
else if (rd_addr_count >= 'd31) begin
rd_addr_count = 0;
end
else
rd_addr_count = rd_addr_count;
end
assign rd_addr_ptr = rd_addr_count;

/*转换成格雷码并同步到对应时钟域*/
//写入指针转为格雷码
assign wr_addr_gray = (wr_addr_count >> 1) ^ wr_addr_count;
//读取指针转为格雷码
assign rd_addr_gray = (rd_addr_count >> 1) ^ rd_addr_count;

//将写入指针的格雷码同步到读取时钟域,打两拍
always @(posedge rd_clk ) begin
wr_addr_gray_reg1 <= wr_addr_gray;
wr_addr_gray_reg2 <= wr_addr_gray_reg1;
end

//将读取指针的格雷码同步到写入时钟域,打两拍
always @(posedge wr_clk ) begin
rd_addr_gray_reg1 <= rd_addr_gray;
rd_addr_gray_reg2 <= rd_addr_gray_reg1;
end

/*判断FIFO空满状态*/
//如果读取和写入格雷码相同,则说明FIFO为空,rd_addr_gray和wr_addr_gray_reg2在同一时钟域下,不能用rd_addr_gray_reg2
assign empty = (rd_addr_gray == wr_addr_gray_reg2);
//当写指针超过读指针一个轮回后即为写满,也即高两位相反,低位相同,FIFO满
assign full = (wr_addr_gray == {~rd_addr_gray_reg2[Addr_Width:Addr_Width-1],rd_addr_gray_reg2[Addr_Width-2:0]});

endmodule

FIFO数据模块

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
module fifo_mem #(
parameter Data_Width = 16,
parameter Fifo_Depth = 32,
parameter Addr_Width = 5
) (
input reset_n,
input wr_clk,
input rd_clk,
input rd_ena,
input wr_ena,
input full,
input empty,
input [Data_Width-1:0] wr_data,
input [Addr_Width:0] wr_addr_ptr,
input [Addr_Width:0] rd_addr_ptr,
output [Data_Width-1:0] rd_data
);

reg [Data_Width-1:0] fifo_ram [Fifo_Depth-1:0]; //fifo存储空间 32*16bit

reg wr_rstn;
reg rd_rstn;
reg [Data_Width-1:0] rd_data_reg;

/*写入数据*/
//异步复位,同步释放,相当于把resetn打一拍避免亚稳态
always @(posedge wr_clk ) begin
if (!reset_n) begin
wr_rstn <= 1'b0;
end
else
wr_rstn <= 1'b1;
end

//复位,将ram清空;未满时根据wr_ptr写入data
genvar i;
generate
for (i = 0;i < Fifo_Depth; i = i + 1 ) begin
always @(posedge wr_clk or negedge wr_rstn) begin
if (!wr_rstn) begin
fifo_ram[i] <= 0;
end
end
end
endgenerate

always @(posedge wr_clk ) begin
if (wr_ena && !full) begin
fifo_ram[wr_addr_ptr] <= wr_data;
end
else
fifo_ram[wr_addr_ptr] <= fifo_ram[wr_addr_ptr];
end


/*读取数据*/

//异步复位,同步释放,相当于把resetn打一拍避免亚稳态
always @(posedge rd_clk ) begin
if (!reset_n) begin
rd_data_reg <= 0;
rd_rstn <= 1'b0;
end
else
rd_rstn <= 1'b1;
end

always @(posedge rd_clk or negedge rd_rstn) begin
if (!rd_rstn) begin
rd_data_reg <= 0;
end
else if (rd_ena && !empty) begin
rd_data_reg <= fifo_ram[rd_addr_ptr];
end
else
rd_data_reg <= 0;
end

assign rd_data = rd_data_reg;

endmodule

TOP_MODULE

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
`timescale 1ns/100ps

module asyn_fifo_top #(
parameter Data_Width = 16,
parameter Fifo_Depth = 32,
parameter Addr_Width = 5
) (
input reset_n,
input wr_clk,
input wr_ena,
input [Addr_Width-1:0] wr_data,
input rd_clk,
input rd_ena,
output [Addr_Width-1:0] rd_data,
output valid,
output empty,
output full
);

wire [Addr_Width:0] wr_addr_ptr;
wire [Addr_Width:0] rd_addr_ptr;


fifo_mem
#(
.Data_Width(Data_Width ),
.Fifo_Depth(Fifo_Depth ),
.Addr_Width (Addr_Width )
)
fifo_mem_dut (
.reset_n (reset_n ),
.wr_clk (wr_clk ),
.rd_clk (rd_clk ),
.rd_ena (rd_ena ),
.wr_ena (wr_ena ),
.full (full ),
.empty (empty ),
.wr_data (wr_data ),
.wr_addr_ptr (wr_addr_ptr ),
.rd_addr_ptr (rd_addr_ptr ),
.rd_data ( rd_data)
);

fifo_ctrl
#(
.Addr_Width (Addr_Width )
)
fifo_ctrl_dut (
.reset_n (reset_n ),
.wr_clk (wr_clk ),
.wr_ena (wr_ena ),
.rd_clk (rd_clk ),
.rd_ena (rd_ena ),
.empty (empty ),
.full (full ),
.wr_addr_ptr (wr_addr_ptr ),
.rd_addr_ptr ( rd_addr_ptr)
);

endmodule

仿真测试结果:

异步FIFO仿真测试

跨时钟域的典型:异步FIFO的设计

http://example.com/2021/09/01/异步FIFO的设计/

作者

Hank.Gan

发布于

2021-09-01

更新于

2021-09-02

许可协议

评论