脚 本:makefile
工 具:vcs 和 verdi
文 章:1. 同步FIFO的设计和功能验证(附源码)
2. Verilog的亚稳态现象和跨时钟域处理方法
博文的代码附Bug解决方法or自行下载(完整代码)
图片来源:原创
文章目录
- 一、学习内容
- 二、异步FIFO
- (1)FIFO基本概念
- (2)异步FIFO基本概念
- (3)异步FIFO的作用
- (4)异步FIFO的读/写指针
- (5)异步FIFO空/满标志
- (6)指针计数器的选择
- (7)二进制与格雷码相互转换
- 三、Spec
- (1)Function descripton
- (2)Feature list
- (3)Block Diagram
- (4)Interface description
- (5)Timing
- 四、Design and Verification
- (1)RTL
- (2)Test bench
- (3)Analyse
- 五、Result
- (1)Write
- (2)Read
- (3)Write&Read
- (4)假满假空现象
一、学习内容
二、异步FIFO (1)FIFO基本概念
在写同步FIFO的时候,已经讲过FIFO的相关概念,可以参考(点击直达)。
(2)异步FIFO基本概念对于同步FIFIO,主要是实现速率匹配,起到数据缓冲的作用。设计的关键在于array存储阵列或RAM空满标志的产生。设计的思路大概可以描述为:设置计数器elem_cnt,计数器的最小计数值为0,最大计数值,是array的最大存储深度。当写使能时,计数器计数+1,读使能时,计数器计数-1,读/写同时使能时,计数器计数值不变。当计数器的值为0时,表明此时的array没有存储数据,产生空标志;当计数值为最大存储深度值时,array存满了,此时产生满标志。详细可参考文章《同步FIFO设计与功能验证》
对于异步FIFO,主要是实现不同时钟域之间的数据交互。与同步FIFO有着明显的区别,同步FIFO是使用一个时钟,读写在同一个时钟域内。而异步FIFO使用两个时钟,读/写在不同时钟域内,这个过程就涉及了跨时钟域处理的过程,跨时钟域又会产生亚稳态问题,所以这是异步FIFO设计的一个重点,与同步FIFO一样,通过空满标志衡量存储器的使用情况,那么在异步FIFO中,空满标志产生的条件和方式是什么呢,这也是设计的重点。
- 为什么需要异步FIFO?
用于在不同的时钟域(clock domain)之间安全地传输数据。而同步FIFO主要是解决数据传输速率匹配问题。 - 同步器(synchronizer)
对于跨时钟域之间的信号传输,需要进行同步(synchronize)处理;一般来讲,我们可以采用同步器(由2~3级FF组成)对单bit的信号进行同步操作。注意,这里的打拍子是针对单bit信号而已的。
那么为什么不能用简单的同步器(synchronizer)对数据总线 ( 大于1bit)进行同步呢?下面分析一下。
问※题:那么对于多bit信号的跨时钟域同步,可采用异步FIFO或者双口RAM。详细可参考另一篇文章《亚稳态和跨时钟域处理》。
(4)异步FIFO的读/写指针
- 写指针(write pointer)
▷ 始终指向下一次将要写入的数据的地址;
▷ 系统复位后(FIFO为空),写指针指向0地址;
▷ 每写入一笔数据,写指针地址加1; - 读指针(read pointer)
▷ 始终指向当前要读出的数据的地址;
▷ 系统复位后(FIFO为空),读指针指向0地址;
▷ 此时的数据是无效的,因为还没有数据写入,空标志有效;
(5)异步FIFO空/满标志
- 空标志(empty)
▷ 情形一,复位时,两指针都为0;
▷ 情形二,当读指针和写指针相等时,空标志=1; - 满标志(full)
▷ 当写指针和读指针最高位不同,其他相等时,满标志=1;
▷ 例如,写入的速度快,写指针转了一圈(wrap around),又追上了读指针; - 空满标志处理
▷ 把读、写指针都额外增加1bit,假如FIFO的深度为8,理论上指针位只需要[2:0]。为了能够正确甄别空、满,需要将指针都扩展到[3:0]。
▷ 其中额外引入的最高位[3],用于辅助甄别是否已经发生了回环(wrap around)的情形。当指针计满FIFO的深度,折回头重新开始时,最高位MSB加1,其它位清0。
▷ 如果读写指针的最高位不同,就意味着写指针速度快,并已经多完成一次回环。
▷ 如果两个指针的最高位相同,就意味着双方完成了相同次数的回环。
(6)指针计数器的选择
- 普通二进制计数器(Binary counter)
- 在异步FIFO的设计中,读写两边要互相参考对方的指针,以便生成空、满标志;
- 数据同步问题:> 1 bit,从一个clock domain到另一个clock domain,由于亚稳态现象的出现,会导致数据出错; 极端情形:所有的数据位都变化;
- 解决办法:采用sample & hold机制,引入保持寄存器和握手机制,以确保接收端正确地采集到想要的数据,之后通知发送端,进行下一轮的数据传输;
- 格雷码计数器(Gray code counter):每次当从一个值变化到相邻的一个值时,有且仅有一位发生变化;
- 由于格雷码的这种特性,我们就可以通过简单的synchronizer对指针(多位宽)进行同步操作了,而不用担心由于发生亚稳态而出现数据错误的情形;
- 但是对于我们习惯了二进制码的风格,这种码易读性稍差;
- 对于2的整数次幂的FIFO,采用格雷码计数器器; 接近2的整数次幂的FIFO, 采用接近2的幂次方格雷码修改实现;如果这两种都满足不了,就设计一种查找表的形式实现。所以,一般采用2的幂次方格雷码实现FIFO,会浪费一些地址空间,但可以简化控制电路;
需要注意:格雷码计数器适用于地址范围空间为2的整数次幂的FIFO,例如8, 16, 32, 64…
(7)二进制与格雷码相互转换
二进制到格雷码:二进制码字整体右移一位,再与原先的码字按位做异或操作。
- 二进制转格雷码
- 格雷码转二进制
三、Spec (1)Function descripton
Asynchronization First in First out 通过控制两个不同时钟域的读/写操作,完成了两个时钟域之间数据的同步处理。
(2)Feature list
- 存储器采用宽度为16,深度为8的regs
- FIFO宽度、深度可配置
- 写时钟为3MHz,读时钟为2MHz
(3)Block Diagram
☛ 模块设计可细分为写入接口(Push Interface)、读出接口(Pop Interface)、同步器(sync)和存储介质RAM(regs_array)
(4)Interface description
Signal Name | Width | Direction | Description |
---|---|---|---|
wr_clk_i | 1 | input | write clock,2MHz |
wr_rst_n_i | 1 | input | write reset signal |
wr_en_i | 1 | input | write enable signal |
wr_data_i | 16 | input | write data |
full_o | 1 | output | full flag of regs_array |
rd_clk_i | 1 | input | read clock,3MHz |
rd_rst_n_i | 1 | input | read reset signal |
rd_en_i | 1 | input | read enable signal |
rd_data_o | 16 | input | write data |
empty_o | 1 | output | empty flag of regs_array |
(5)Timing
- Write timing
- Read Timing
四、Design and Verification (1)RTL
//-- modified by xlinxdu, 2022/05/17 module async_fifo #( parameter DATA_WIDTH = 16 , parameter FIFO_DEPTH = 8 , parameter PTR_WIDTH = 4 , parameter ADDR_DEPTH = $clog2(FIFO_DEPTH) ) ( //reset signal input wire wr_rst_n_i, input wire rd_rst_n_i, //write interface input wire wr_clk_i , input wire wr_en_i , input wire [DATA_WIDTH-1:0] wr_data_i, //read interface input wire rd_clk_i , input wire rd_en_i , output reg [DATA_WIDTH-1:0] rd_data_o, //flag output reg full_o , output reg empty_o ); //-- memery reg [DATA_WIDTH-1:0] regs_array [FIFO_DEPTH-1:0] ; //-- memery addr wire [ADDR_DEPTH-1:0] wr_addr ; wire [ADDR_DEPTH-1:0] rd_addr ; //-- write poiter,write poiter of gray and sync reg [PTR_WIDTH -1:0] wr_ptr ; wire [PTR_WIDTH -1:0] gray_wr_ptr ; reg [PTR_WIDTH -1:0] gray_wr_ptr_d1 ; reg [PTR_WIDTH -1:0] gray_wr_ptr_d2 ; //-- read poiter,read poiter of gray and sync reg [PTR_WIDTH -1:0] rd_ptr ; wire [PTR_WIDTH -1:0] gray_rd_ptr ; reg [PTR_WIDTH -1:0] gray_rd_ptr_d1 ; reg [PTR_WIDTH -1:0] gray_rd_ptr_d2 ; always @ (posedge wr_clk_i or negedge wr_rst_n_i) begin if (!wr_rst_n_i) begin wr_ptr <= {(PTR_WIDTH){1'b0}}; end else if (wr_en_i && !full_o) begin wr_ptr <= wr_ptr + 1'b1; end end assign gray_wr_ptr = wr_ptr ^ (wr_ptr >> 1'b1); always @ (posedge wr_clk_i or negedge wr_rst_n_i) begin if (!wr_rst_n_i) begin gray_wr_ptr_d1 <= {(PTR_WIDTH){1'b0}}; gray_wr_ptr_d2 <= {(PTR_WIDTH){1'b0}}; end else begin gray_wr_ptr_d1 <= gray_wr_ptr ; gray_wr_ptr_d2 <= gray_wr_ptr_d1; end end always @ (posedge rd_clk_i or negedge rd_rst_n_i) begin if (!rd_rst_n_i) begin rd_ptr <= {(PTR_WIDTH){1'b0}}; end else if (rd_en_i && !empty_o) begin rd_ptr <= rd_ptr + 1'b1; end end assign gray_rd_ptr = rd_ptr ^ (rd_ptr >> 1'b1); always @ (posedge rd_clk_i or negedge rd_rst_n_i) begin if (!rd_rst_n_i) begin gray_rd_ptr_d1 <= {(PTR_WIDTH){1'b0}}; gray_rd_ptr_d2 <= {(PTR_WIDTH){1'b0}}; end else begin gray_rd_ptr_d1 <= gray_rd_ptr ; gray_rd_ptr_d2 <= gray_rd_ptr_d1; end end assign full_o = (gray_wr_ptr == {~gray_rd_ptr_d2[PTR_WIDTH-1],gray_rd_ptr_d2[PTR_WIDTH-2:0]})? 1'b1 : 1'b0; assign empty_o = (gray_rd_ptr == gray_wr_ptr_d2)? 1'b1 : 1'b0; assign wr_addr = wr_ptr[PTR_WIDTH-2:0]; assign rd_addr = rd_ptr[PTR_WIDTH-2:0]; integer [PTR_WIDTH-1:0] i; always @ (posedge wr_clk_i or negedge wr_rst_n_i) begin if (!wr_rst_n_i) begin for(i=0;i(2)Test bench(DATA_WIDTH){1'b0}}; end end else if (wr_en_i && !full_o) begin regs_array[wr_addr] <= wr_data_i; end end always @ (posedge rd_clk_i or negedge rd_rst_n_i) begin if (!rd_rst_n_i) begin rd_data_o <= {(DATA_WIDTH){1'b0}}; end else if (rd_en_i && !empty_o) begin rd_data_o <= regs_array[rd_addr]; end end endmodule
//-- modified by xlinxdu, 2022/05/17 module tb_async_fifo; reg rst_n_i ; reg wr_clk_i ; reg wr_en_i ; reg [15:0] wr_data_i; reg rd_clk_i ; reg rd_en_i ; wire [15:0] rd_data_o; reg full_o ; reg empty_o ; initial begin rst_n_i = 1; wr_clk_i = 0; wr_en_i = 0; wr_data_i= 16'b0; rd_clk_i = 0; rd_en_i = 0; # 1 rst_n_i = 0; # 2 rst_n_i = 1; end initial begin #20 wr_en_i = 1; rd_en_i = 0; #40 wr_en_i = 0; rd_en_i = 1; #30 wr_en_i = 1 ; rd_en_i = 0 ; #13 rd_en_i = 1 ; #10 repeat(100) begin #5 wr_en_i = {$random}%2 ; rd_en_i = {$random}%2 ; end end always #1.5 wr_clk_i = ~wr_clk_i ; always #1 rd_clk_i = ~rd_clk_i ; always #3 wr_data_i = {$random}%16'hFF; async_fifo u_async_fifo( .wr_rst_n_i(rst_n_i ), .wr_clk_i (wr_clk_i ), .wr_en_i (wr_en_i ), .wr_data_i (wr_data_i), .rd_rst_n_i(rst_n_i ), .rd_clk_i (rd_clk_i ), .rd_en_i (rd_en_i ), .rd_data_o (rd_data_o), .full_o (full_o ), .empty_o (empty_o ) ); initial begin #1000 $finish ; $fsdbDumpfile("async.fsdb"); $fsdbDumpvars ; $fsdbDumpMDA ; end endmodule(3)Analyse
bug1:可以看到本该同步到异时钟域的格雷码写指针和格雷码读指针,却在自己的时钟域下打拍,导致后面产生的空满标志错误。
定位到读/写指针格雷码同步代码块
always @ (posedge wr_clk_i or negedge wr_rst_n_i) begin if (!wr_rst_n_i) begin gray_wr_ptr_d1 <= {(PTR_WIDTH){1'b0}}; gray_wr_ptr_d2 <= {(PTR_WIDTH){1'b0}}; end else begin gray_wr_ptr_d1 <= gray_wr_ptr ; gray_wr_ptr_d2 <= gray_wr_ptr_d1; end end always @ (posedge rd_clk_i or negedge rd_rst_n_i) begin if (!rd_rst_n_i) begin gray_rd_ptr_d1 <= {(PTR_WIDTH){1'b0}}; gray_rd_ptr_d2 <= {(PTR_WIDTH){1'b0}}; end else begin gray_rd_ptr_d1 <= gray_rd_ptr ; gray_rd_ptr_d2 <= gray_rd_ptr_d1; end end
两处更改了对应的时钟沿触发条件
always @ (posedge wr_clk_i or negedge wr_rst_n_i) begin if (!wr_rst_n_i) begin gray_rd_ptr_d1 <= {(PTR_WIDTH){1'b0}}; gray_rd_ptr_d2 <= {(PTR_WIDTH){1'b0}}; end else begin gray_rd_ptr_d1 <= gray_rd_ptr ; gray_rd_ptr_d2 <= gray_rd_ptr_d1; end end always @ (posedge rd_clk_i or negedge rd_rst_n_i) begin if (!rd_rst_n_i) begin gray_wr_ptr_d1 <= {(PTR_WIDTH){1'b0}}; gray_wr_ptr_d2 <= {(PTR_WIDTH){1'b0}}; end else begin gray_wr_ptr_d1 <= gray_wr_ptr ; gray_wr_ptr_d2 <= gray_wr_ptr_d1; end end
bug2:在只读情况下,写满之后,没有产生满标志。
定位到空满标志代码块
assign full_o = (gray_wr_ptr == {~gray_rd_ptr_d2[PTR_WIDTH-1],gray_rd_ptr_d2[PTR_WIDTH-2:0]})? 1'b1 : 1'b0;
更改满标志的条件,因为在这里是比较格雷码,和原来二进制比较不同,二进制是高位取反,然后所有位相等即产生满标志。格雷码是最高位和次高位取反,然后所有位相同,即产生满标志。
assign full_o = (gray_wr_ptr == {~gray_rd_ptr_d2[PTR_WIDTH-1:PTR_WIDTH-2],gray_rd_ptr_d2[PTR_WIDTH-3:0]})? 1'b1 : 1'b0;
五、Result (1)Write
在只读阶段,读使能,写指针和写地址开始递增,并将data_i依次写入regs_array。在地址增加到写满了8个地址(0~7)后,写指针继续增加(回环,表示第二圈了),写地址回到第一个地址,并产生满标志。(写指针总指向将要写入的地址,前面有说)
(2)Read
在只读阶段,这个阶段regs_array已经存满,开始读出数据,读指针和读地址开始递增,当读到regs_array的第八个存储地址(0~7)后,读指针继续增加(回环,表示第二圈了),读地址回到第一个地址,并产生空标志。(读指针总指向将要读出的地址,前面有说)
(3)Write&Read
在读/写同时使能的时候,写操作依次往存储单元里面写入新数据,读操作依次往存储单元里读出数据,结果见上图。
(4)假满假空现象
由上图标号1可以知道,在满标志拉低时刻之前,其实regs_array是一个假满的状态,因为这时刻之前,已经读出了三个数据了;由上图标号2可以知道,读空的时候,产生了空标志,其实regs_array是一个假空的状态,因为这时刻之前,写操作已经写入了两个数据。
※原因:因为在格雷码指针同步的时候,格雷码指针信号在两个时钟域之间存在两个时钟周期的延时,但这并不会影响FIFO的正常工作逻辑。
作者:xlinxdu
版权:本文版权归作者所有
转载:未经作者允许,禁止转载,转载必须保留此段声明,必须在文章中给出原文连接。