• 异步FIFO实验小结


    目录

    写在前面 

    一、概述 

    1. 异步/同步FIFO

    2. 异步fifo的特点

    3. 异步fifo设计结构图

    4. FIFO的常见参数

     5. 异步fifo空/满判断

     6.指针计数器的选择

     7. 格雷码/二进制码相互转换

    8. RTL代码 

    9. DUT debug 

    二、验证平台构建

    1.验证代码详解

    2.验证功能点

    3. 验证结构 

    4. Analysis 

    5.部分功能点验证

    1 时钟功能点验证

    ​2 复位功能点验证

    6.覆盖率分析

    1.代码覆盖率 

    2.功能覆盖率 

    三、整体结构图 

    四、Markfile


    写在前面 

    本小节内容主要针对异步FIFO进行设计验证,设计结构较为简单。作为SV学习阶段的大练习,其主要目的更多的是对SV基础语法的巩固,以及对验证功能点提取,覆盖率收集的学习。一些提供的代码和分析可能有误,也请阅读该文的小伙伴积极提出问题,一起进步。

    RTL代码获取:

    TB代码获取:

    一、概述 

    1. 异步/同步FIFO

    FIFO在硬件上是一种地址依次自增的Simple Dual Port RAM,按读数据和写数据工作的时钟域是否相同分为同步FIFO和异步FIFO,其中同步FIFO是指读时钟和写时钟为同步时钟,常用于数据缓存和数据位宽转换;异步FIFO通常情况下是指读时钟和写时钟频率有差异,即由两个异步时钟驱动的FIFO,由于读写操作是独立的,故常用于多比特数据跨时钟域处理。

    2. 异步fifo的特点

    异步fifo用于在不同的时钟域(clock domain)之间安全地传输数据。而同步FIFO主要是解决数据传输速率匹配问题;因此,异步fifo需要进行同步(synchronize)处理;一般来讲,我们可以采用同步器(由2FF组成)对单bit的信号进行同步操作。注意,这里的打拍是针对单bit信号而已的。

     但是,二进制数的自增一位变化位数不仅仅是一位,所以在同步的过程中我们使用的格雷码或者独热码,由于独热码占位较多,所以格雷码被广泛应用。在使用格雷码的同时,还需要注意fifo的深度必须是2^n,具体原因后边将展开解释。

    3. 异步fifo设计结构图

    由结构图可知,异步FIFO主要划分两个时钟域

    写时钟域:

            always@(posedge wclk);  if(wclken && !wfull) mem[waddr]<=wdata;,表示只要MEM非满且写使能有效,即可往MEM写数据;而wfull_val =(wgraynext=={~wq2_rptr[ADDRSIZE:ADDRSIZE-1],wq2_rptr[ADDRSIZE-2:0]}); 表示读写指针之间的位移等于FIFO的最大深度,至于gray码高2bit相反原因下面详解。

    读时钟域:

            assign rdata<=mem[raddr];,表示只要MEM 有数据,即可往MEM读数据(此处加上rempty的判断会更好);而rempty_val = (rgraynext==rq2_wptr); 表示读指针追上写指针,即MEM内没有数据。

    4. FIFO的常见参数

    FIFO的宽度(datasize):即FIFO一次读写操作的数据位;
    FIFO的深度(depth):指的是FIFO可以存储多少个N位的数据(如果宽度为N)。
    满标志(wfull):FIFO已满或将要满时由FIFO的状态电路送出的一个信号,以阻止FIFO的写操作继续向FIFO中写数据而造成溢出(overflow)。
    空标志(rempty):FIFO已空或将要空时由FIFO的状态电路送出的一个信号,以阻止FIFO的读操作继续从FIFO中读出数据而造成无效数据的读出(underflow)。
    读时钟(rclk):读操作所遵循的时钟,在每个时钟沿来临时读数据。
    写时钟(wclk):写操作所遵循的时钟,在每个时钟沿来临时写数据。

    读使能(rcin):读操作有效,允许从fifo mem中读取数据。
    写使能(wcin):写操作有效,允许向fifo mem中写入数据。

    读指针(rptr):每读出一个数据。读指针加1,始终指向当前要读出的数据的位置。
    写指针(wptr):每写入一个数据。写指针加1,始终指向下一次将要写入的数据的位置。

     5. 异步fifo空/满判断

    读空状态可以理解为读地址指针追上写地址指针

    实现代码:rempty_val = (rgraynext==rq2_wptr);

    写满状态可以理解为写地址指针再次追上读地址指针 

    实现代码:wfull_val =(wgraynext=={~wq2_rptr[ADDRSIZE:ADDRSIZE-1],wq2_rptr[ADDRSIZE-2:0]});

     满标志判断,gray码高2bit相反分析:

    细心的小伙伴应该可以发现,上面在提到写满时,说的是写指针再次追上读指针,也就是说,写满时,写指针比读指针多走一圈,为了便于区分,将地址位宽从4 bit拓宽到5 bit,因此此时的写指针地址可以认为是11010

    11010的格雷码是10111, 01010的格雷码是01111,对比两个格雷码是不是可以发现,此时高两位相反,低两位相同,这便是格雷码下写满的判断条件;

    空/满标志处理细节:

    ▷ 把读、写指针都额外增加1bit,假如FIFO的深度为8,理论上指针位只需要[2:0]。为了能够正确甄别空、满,需要将指针都扩展到[3:0]。
    ▷ 其中额外引入的最高位[3],用于辅助甄别是否已经发生了回环(wrap around)的情形。当指针计满FIFO的深度,折回头重新开始时,最高位MSB加1,其它位清0。
    ▷ 如果读写指针的最高位不同,就意味着写指针速度快,并已经多完成一次回环。
    ▷ 如果两个指针的最高位相同,就意味着双方完成了相同次数的回环。

     6.指针计数器的选择

    两种计数方法对比:

            普通二进制计数

         数据同步问题:> 1 bit,从一个clock domain到另一个clock domain,由于亚稳态现象的出现,会导致数据出错; 极端情形:所有的数据位都变化;

            格雷码计数

         每次当从一个值变化到相邻的一个值时,有且仅有一位发生变化;

    1. 由于格雷码的这种特性,我们就可以通过简单的synchronizer对指针(多位宽)进行同步操作了,而不用担心由于发生亚稳态而出现数据错误的情形(前边有解释);
    2. 对于2的整数次幂的FIFO,采用格雷码计数器; 接近2的整数次幂的FIFO, 采用接近2的幂次方格雷码修改实现;如果这两种都满足不了,就设计一种查找表的形式实现。所以,一般采用2的幂次方格雷码实现FIFO,会浪费一些地址空间,但可以简化控制电路;
      需要注意:格雷码计数器适用于地址范围空间为2的整数次幂的FIFO,例如8, 16, 32, 64…

     7. 格雷码/二进制码相互转换

    二进制码转格雷码只需将二进制码字整体右移一位,最高位补0,再与原先的码字按位做异或操作(如下图)

    实现代码:rgraynext = (rbinnext>>1)^rbinnext;

    格雷码转二进制码,将格雷码的最高位作为二进制的最高位,将格雷码次高位与二进制码最高位异或运算,得到二进制码次高位,以此类推(如图)。

    8. RTL代码 

    1. module fifo1 #(parameter DSIZE = 8, parameter ASIZE = 4)
    2. (input [DSIZE-1:0] wdata,
    3. input winc,wclk,wrst_n,
    4. input rinc,rclk,rrst_n,
    5. output[DSIZE-1:0] rdata,
    6. output wfull,
    7. output rempty
    8. );
    9. wire [ASIZE-1:0] waddr,raddr;
    10. wire [ASIZE:0] wptr,rptr,wq2_rptr,rq2_wptr;
    11. sync_r2w sync_r2w (.rptr(rptr),
    12. .wclk(wclk),
    13. .wrst_n(wrst_n),
    14. .wq2_rptr(wq2_rptr)
    15. );
    16. sync_w2r sync_w2r (.wptr(wptr),
    17. .rclk(rclk),
    18. .rrst_n(rrst_n),
    19. .rq2_wptr(rq2_wptr)
    20. );
    21. rptr_empty #(ASIZE) rptr_empty(.rclk(rclk),
    22. .rrst_n(rrst_n),
    23. .rinc(rinc),
    24. .rq2_wptr(rq2_wptr),
    25. .raddr(raddr),
    26. .rempty(rempty),
    27. .rptr(rptr)
    28. );
    29. wptr_full #(ASIZE) wptr_full(.wq2_rptr(wq2_rptr),
    30. .wclk(wclk),
    31. .wrst_n(wrst_n),
    32. .winc(winc),
    33. .wfull(wfull),
    34. .waddr(waddr),
    35. .wptr(wptr)
    36. );
    37. fifomem #(DSIZE,ASIZE) fifomem (.wdata(wdata),
    38. .waddr(waddr),
    39. .raddr(raddr),
    40. .wclken(winc),
    41. .wfull(wfull),
    42. .wclk(wclk),
    43. .rdata(rdata)
    44. );
    45. endmodule
    46. module fifomem #(parameter DATASIZE = 8,parameter ADDRSIZE = 4) //mem data word width,number od mem address bits
    47. (input [DATASIZE-1:0] wdata,
    48. input [ADDRSIZE-1:0] waddr,raddr,
    49. input wclken, wfull, wclk,
    50. output[DATASIZE-1:0] rdata);
    51. `ifdef VENDORRAM
    52. //instantiation of a vendor's dual-port RAM
    53. vendor_ram mem(.dout(rdata),
    54. .din(wdata),
    55. .waddr(waddr),
    56. .raddr(raddr),
    57. .wclken(wclken),
    58. .wclken_n(wfull),
    59. .clk(wclk)
    60. );
    61. `else
    62. //RTL verilog mem model
    63. localparam DEPTH = 1<
    64. reg [DATASIZE-1:0] mem [0:DEPTH-1];
    65. assign rdata<=mem[raddr];
    66. always@(posedge wclk);
    67. if(wclken && !wfull)
    68. mem[waddr]<=wdata;
    69. `endif
    70. endmodule
    71. module rptr_empty #(parameter ADDRSIZR = 4)
    72. (input rclk,rrst_n,rinc,
    73. input [ADDRSIZR-1:0] rq2_wptr,
    74. output[ADDRSIZR-1:0] raddr,
    75. output reg rempty,
    76. output reg [ADDRSIZR:0] rptr
    77. );
    78. reg [ADDRSIZR:0] rbin;
    79. wire[ADDRSIZR:0] rgraynext, rbinnext;
    80. wire rempty_val;
    81. //mem read-address pointer
    82. assign raddr = rbin[ADDRSIZR-1:0];
    83. assign rbinnext = rbin + (rinc & ~rempty);
    84. assign rgraynext = (rbinnext>>1)^rbinnext;
    85. //--------------------------------------------------------
    86. //fifo empty when the next rptr == synchronized wptr or on reset
    87. //--------------------------------------------------------
    88. assign rempty_val = (rgraynext==rq2_wptr);
    89. //-----------------------
    90. //GRAYSTYLE2 pointer
    91. //-----------------------
    92. always@(posedge rclk or negedge rrst_n)
    93. if(!rrst_n)
    94. {rbin,rptr} <= 0;
    95. else
    96. {rbin,rptr} <= {rbinnext,rgraynext};
    97. always@(posedge rclk or negedge rrst_n)
    98. if(!rrst_n)
    99. rempty<=1'b1;
    100. else
    101. rempty<=rempty_val;
    102. endmodule
    103. module wptr_full #(parameter ADDRSIZE = 4)
    104. (input [ADDRSIZE:0] wq2_rptr,
    105. input wclk,wrst_n,winc,
    106. output reg wfull,
    107. output [ADDRSIZE-1:0] waddr,
    108. output reg [ADDRSIZE:0] wptr
    109. );
    110. reg [ADDRSIZE:0] wbin;
    111. reg [ADDRSIZE:0] wbinnext,wgraynext;
    112. wire wfull_val;
    113. //mem write-address pointer
    114. assign waddr = wbin[ADDRSIZE-1:0];
    115. assign wbinnext = wbin + (winc & ~wfull);
    116. assign wgraynext = (wbinnext>>1) ^ wbinnext;
    117. //--------------------------------------------
    118. assign wfull_val =(wgraynext=={~wq2_rptr[ADDRSIZE:ADDRSIZE-1],wq2_rptr[ADDRSIZE-2:0]});
    119. //GRAYSTYLE2 pointer
    120. always@(posedge rclk or negedge wrst_n)
    121. if(!wrst_n)
    122. {wbin,wptr} <= 0;
    123. else
    124. {wbin,wptr} <= {wbinnext,wgraynext};
    125. always@(posedge rclk or negedge wrst_n)
    126. if(!wrst_n)
    127. wfull<= 1'b0;
    128. else
    129. wfull<= wfull_val;
    130. endmodule
    131. module sync_r2w #(parameter ADDRSIZE = 4)
    132. (input [ADDRSIZE:0] rptr,
    133. input wclk,wrst_n,
    134. output reg [ADDRSIZE:0] wq2_rptr
    135. );
    136. reg [ADDRSIZE:0] wq1_rptr;
    137. always@(posedge wclk or negedge wrst_n)
    138. if(!wrst_n)
    139. {wq2_rptr,wq1_rptr} <= 0;
    140. else
    141. {wq2_rptr,wq1_rptr} <= {wq1_rptr,rptr};
    142. endmodule
    143. module sycn_w2r #(parameter ADDRSIZE = 4)
    144. (input [ADDRSIZE:0] wptr,
    145. input rclk, rrst_n,
    146. output[ADDRSIZE:0] rq2_wptr
    147. );
    148. reg [ADDRSIZE:0] rq1_wptr;
    149. always@(posedge rclk or negedge rrst_n)
    150. if(!rrst_n)
    151. {rq2_wptr,rq1_wptr} <= 0;
    152. else
    153. {rq2_wptr,rq1_wptr} <= {rq1_wptr,wptr};
    154. endmodule

    9. DUT debug 

    bug1:wdata没有数据写入时,只要写指针循环一圈后,wfull就会拉高,此时实际上没有数据写入,fifo实际为空; 

    原因分析:主要是在waddr 和wptr 设置为自增形式,没有考虑wdata的因素,所以将wdata加入作为判断即可

    代码定位: 

               

    此处添加了对wdata的判断,只有wdata有数据是,写指针以及写地址才会加1.

     dubug后代码:

    bug2: 在winc拉高的前提下,当wrst_n和rrst_n在拉低过程中没有交集时,在后边即使二者都拉高,且wdata有值,fifo依然不会读入数据,显示为不定态;

    原因分析:从代码的分析中不难发现,当wrst_n拉高后,wfull取决于wgraynext的状态,换句话说,如果wgraynext为不定态,那wfull就是不定态;而wgraynext又取决于wfull的状态,这样就形成了一个闭环,所以需要有一个值提前初始化,打破这种闭环。

    代码定位:

     加入了对wdata的判断,如果有wdata写入,就认为fifo非空,接下来判断是否为空。

    前些日子突然有小伙伴问我这个问题,后来仔细研究一番发现有一种更简单的修改方法,将wfull_val赋值变量语句中的“=="改为”===“,问题就解决了。

    wfull_val =(wgraynext=={~wq2_rptr[ADDRSIZE:ADDRSIZE-1],wq2_rptr[ADDRSIZE-2:0]})

    wfull_val =(wgraynext==={~wq2_rptr[ADDRSIZE:ADDRSIZE-1],wq2_rptr[ADDRSIZE-2:0]})

    期待大家向我提问哦!

    debug后代码:

    二、验证平台构建

    1.验证代码详解

    由于篇幅受限,验证代码详解请点此链接

    2.验证功能点

    功能类:

    • 一、时钟测试点提取:
    1. read/write相同时钟和不同时钟同时读写;
    2. Read时钟快,write时钟慢,同时读写;
    3. Read时钟慢,write时钟快,同时读写;
    4. 大比例时钟时正常工作:1)write时钟频率是read的4倍,同时读写;2).write时钟频率是read的1/4倍,同时读写.
    • 二、复位测试点提取:
    1. read端口reset测试;
    2. Write端口reset测试;
    3. 先reset read端口,在reset write端口;
    4. 先reset write端口,在reset read端口;
    5. 同时将read和write 进行reset。
    • 三、其他功能测试点提取:
    1. 基本的写功能验证;
    2. 基本的读功能验证;
    3. 读写功能同时进行的验证;

    接口类:

    • 一、Clk验证点提取:

    验证不同频率下功能点是否正确;

    • 二、Reset验证点提取:

    验证不同时刻reset,功能是否正确;

    • 三、寄存器接口验证:

    验证寄存器访问是否正确;

    • 四、串行接口:

    验证发送接收是否符合预期;

    • 五、中断接口:

    验证产生中断触发条件,中断输出以及中断清除是否符合预期;

    场景类:

    • 一、验证收发功能:

    验证是否可以正常接收发送数据,是否符合通信协议;

    异常类:

    1. 空状态下读;
    2. 满状态下写。

    3. 验证结构 

    4. Analysis 

    S:     if(!wrst_n) {wbin,wptr} <= 0; if(!wrst_n)  wfull<= 1'b0;

    M0: if(!rrst_n) {rbin,rptr} <= 0;   if(!rrst_n)  rempty<=1'b1;

    M1: wrst_n拉高,{wbin,wptr} <= {wbinnext,wgraynext}; wfull<= wfull_val;注意waddr=wbin,

    wbinnext= wbin + (winc & ~wfull);此时,winc为高,wfull非空,所以接下来随着wclk的增加,waddr也会自加,直到wfull拉高为止,即M1-M2阶段;此时虽然为满,但内部没有数据;

    M2:随着waddr的增加,当满足wfull<= wfull_val(full_val =(wgraynext=={~wq2_rptr[ADDRSIZE:

    ADDRSIZE-1],wq2_rptr[ADDRSIZE-2:0]})时,wfull拉高;

    M3:rrst_n拉高,由TB文件激励发送协议,repeat(5)@(posedge itf.wclk); nv.fifowrite(26);等待5个wclk时钟周期,开始发送激励,即M4;

    M4:开始发送激励;

    M5: rrst_n拉高后两个rclk时钟周期,{rq2_wptr,rq1_wptr} <= {rq1_wptr,wptr}; rq2_wptr= wptr,wptr为格雷码11000;此时rempty<=rempty_val (rempty_val = (rgraynext==rq2_wptr))结果为0;rempty拉低;

    M6:写入数据后,随着rclk的增加,raddr开始自加,所以在M6 site raddr指针指向1,但是此时mem内部并没有数据,所以没有读回data;

    M7: rrst_n拉高两个rclk时钟周期后,理论上写入一个数据会被读走一个数据,但是由于wclk的频率是rclk的三倍,所以每次只能是前边的一个数据被写入mem中,但是由于此时写入addr是1,而此时raddr指针指向2,所以读不出数据,将一直保持读指针和写指针差一个地址单位,所以只有读指针循环一圈才可以读出数据,这也是M10读回data的原因;

    M8:wfull拉低的时间为一个wclk周期;

    M9:当所有的data写完,winc拉低,此时实际上mem中只有部分数据写入,其余数据丢失。

    M10:等待raddr循环一个fifo深度,开始读回数据。 

    5.部分功能点验证

    1 时钟功能点验证

    case1:读写相同时钟频率同时读写

     case2:读时钟快,写时钟慢,同时读写

    case3:读时钟慢,写时钟快,同时读写

    case4: 大比例时钟验证 write时钟频率是read的4倍,同时读写

     case5: 大比例时钟验证 write时钟频率是read的1/4倍,同时读写

     2 复位功能点验证

    case1:先读复位再写复位

    case2:读写同时复位

    6.覆盖率分析

    1.代码覆盖率 

    2.功能覆盖率

    三、整体结构图 

    四、Markfile

    RTL代码详解:点此链接

    TB代码详解:点此链接

  • 相关阅读:
    产品设计的流程:发觉需求、需求调查-需求剖析
    【解读】基于PREEvision的诊断设计
    【深度学习】 Python 和 NumPy 系列教程(十六):Matplotlib详解:2、3d绘图类型(2)3D散点图(3D Scatter Plot)
    BusyBox编译时选择合适的编译器
    智能运维应用之道,告别企业数字化转型危机
    Vue 全套教程(二),入门 Vue 必知必会
    第九章 动态规划 part16(编辑距离专题)583. 两个字符串的删除操作 72. 编辑距离 编辑距离总结篇
    数据结构笔记(王道考研) 第六章:图
    一种基于双MCU协同的多功能押解脚环
    form组件 输入才校验,不输入不校验
  • 原文地址:https://blog.csdn.net/weixin_45680021/article/details/126916297