跨时钟域的典型范例——异步FIFO
“时钟域(clock domain)”可以说是数字集成电路中一个非常重要的内容了,那么,何谓“跨时钟域”?
很好理解,在时序逻辑电路中,所有触发器、寄存器的运行都是由时钟激励而运行下去的。而一个大型的数字系统中不可能只有一种时钟:
例如Cortex-M3软核常常运行在50~100MHz、而UART串行口波特率要在921600以上的话,输入时钟频率最好高于200MHz、FPGA上的DDR3一般要求200MHz的时钟输入,不同PLL/MMCM输出的同频率时钟它们的相位也有可能不同……
这么多不同时钟激励的系统要组合在一起并能进行数据交互,那么势必会遇到“跨时钟域”的问题,解决这个问题的一大方法之一就是利用异步FIFO进行数据交互
什么是FIFO
FIFO(F irst I n F irst O ut),顾名思义,即先输入先输出的一种模块,FIFO一般分为同步FIFO和异步FIFO两种,同步FIFO即输入和输出在同一个时钟域中,异步FIFO即输入输出不处于同一个时钟域。
FIFO的存储模块可以看成一个二维寄存器(当然也可以通过生成ram代替),因此有些概念需要掌握,即:
FIFO宽度:指FIFO中一个空间容纳的数据比特数,即一个数据单位的大小,例如你要输入FIFO的是16位的数据,那么FIFO的宽度即为16bit
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的寄存器时,会出现什么情况?
用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,即将在写入和读出的下一个上升沿判断指针,而写入时钟频率高于读取时钟频率,那么判断的时候可能会出现的情况为:
最好的情况:刚好写入和读取的时钟上升沿在同一时刻了,并且数据都满足了建立和保持时间后开始进行指针之间的比较,完全没有问题,比较的结果也是正确的。
出了点状况:读取指针的时钟在这一时刻翻转了,有了一个上升沿,这个时候读取的数据还没有准备好 (DFF处于亚稳态),rd-ptr = 4’b0zzz(后三bit无法确定,因为不稳定),这下完了没法比较了。
又出了点状况:写入指针也恰好没法满足时序条件,而且写入指针的四位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, 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; 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; 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 assign empty = (rd_addr_gray == wr_addr_gray_reg2); 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 ]; reg wr_rstn; reg rd_rstn; reg [Data_Width-1 :0 ] rd_data_reg; always @(posedge wr_clk ) begin if (!reset_n) begin wr_rstn <= 1'b0 ; end else wr_rstn <= 1'b1 ; end 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 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
仿真测试结果: