# 异步FIFO的纯verilog实现 **Repository Path**: wirelesssun/DC_fifo ## Basic Information - **Project Name**: 异步FIFO的纯verilog实现 - **Description**: 纯verilog构建异步fifo,附带仿真脚本。读写端口各有一组时钟、读写使能、读写端口、满空指示、fifo使用量。源码结构清晰,注释完备,易于理解。 - **Primary Language**: Verilog - **License**: LGPL-2.1 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 9 - **Created**: 2023-06-30 - **Last Updated**: 2023-06-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 已应用于实际项目 ## 视频教程 [B站视频](https://www.bilibili.com/video/BV1t34y1B7vE/) ## 介绍 使用verilog编写的异步fifo,读写端口各有一组时钟、读写使能、读写端口、满空指示、fifo使用量。在源码中对每个模块都进行注释,易于学习参考。 fifo_async.v为源文件,fifo_async.pdf为RTL视图。 testbench文件夹中有建立好的仿真工程,分别是VCS+Verdi和iverilog+gtkwave。喜欢哪个用哪个,配好环境make就行了。 iverilog+gtkwave加入windows支持。 日后还会持续更新完善 ## 模块接口 | 参数 | | |---------|---------| | DSIZE | FIFO的数据宽度 | | ASIZE | 2的ASIZE次方为FIFO深度 | 即,ASIZE=10,对应FIFO深度2^10=1024 | 写入侧 | | |---------|---------| | wclk | 写时钟 | | wdata | 数据输入端口 | | w_en | 置1写使能 | | w_full | 置1为写满 | | wuse | fifo使用量 | | 读取侧 | | |---------|---------| | rclk | 读时钟 | | rdata | 数据输出端口 | | r_en | 置1读使能 | | r_empty | 置1为读空 | | r_valid | 数据有效 | | ruse | fifo使用量 | ## 讲解 #### 何为FIFO FIFO( First Input First Output),说人话就是搞一个容器,先进去的东西先出来。当数据生产与数据使用的动作不同步的时候,就需要FIFO作为缓存。 举个例子,当我们看直播的时候,观看到的画面是连续而且平稳的,但是实际网络传输,速度有波动,时快时慢。为了让我们看直播的时候,画面不会因为网络波动而卡卡卡,就使用了FIFO的思想。FIFO是一段存储器,有一定容量。来自网络端的直播画面,时快时慢地往FIFO里面写,只要别写爆了或者网络太烂数据跟不上,电脑显示端就能非常平稳地接收画面。一边写一边收,先写进去的画面会被先接收,FIFO本身的容量抹平了网络波动的影响,带来了流畅的画面。 这就是FIFO。 #### 同步与异步 在同一个时钟域下,一边是时快时慢地生产数据,另一边需要能平稳地使用数据,使用同步FIFO就可以满足数据缓冲的需求。FIFO的空间不宜太大,满足需求即可,大了浪费资源;也不宜太小,太小FIFO的缓冲作用不明显。同步FIFO解决的是同一个时钟域下数据收发的问题。 在大型系统中,不可避免的会遇到数据跨时钟域通信的问题。不同的时钟域,若频率不同,则相互之间肯定是不同步的;若频率相同,时钟源不同,也存在频率漂移和相位偏差,同样需要看作不同步。如果只是单bit控制信号,拓展有效周期、打两拍就能解决问题了。而面对大量数据的跨时钟域传输,就需要使用异步FIFO。 #### 逻辑框图 注:本逻辑框图仅与本人提供的源码匹配,若有更好的方案欢迎讨论。 注1:本模型的重点放在FIFO的逻辑结构上,双端口RAM的具体结构不作研究。 ![异步FIFO逻辑结构](https://images.gitee.com/uploads/images/2021/1117/190539_b31c8fdf_8241888.png "fifo.png") 异步FIFO的逻辑框图如图所示。 左侧为写控制器,用于接收写入的数据、写地址控制、写满状态判定、写地址转格雷码等。 中间是双端口RAM,特性是可以用异步的时钟进行读写。 右侧为读控制器,用于读出FIFO的数据、读地址控制、读空状态判定、读地址转格雷码等。 下方是转换成格雷码的读写地址,通过打两拍的方式,将数据同步到对面的控制器。 #### 细节分析 ##### FIFO与双端口RAM FIFO的功能是数据先入先出,而双端口RAM可以异步地进行读写数据,这两者是如何结合的? 参考下面的循环队列视频,就能理解FIFO如何用线性存储器实现了。 [循环队列](https://www.bilibili.com/video/BV1ob411T7Uk) ##### 读写控制器 读写控制器是FIFO功能的重要组成部分。 以写控制器为例。从端口可以看出,FIFO的数据输入输出是不需要地址的。因此若需要将数据正确地写入双端口RAM,写控制器需要生成对应的写指针。当FIFO写入数据的时候,写指针+1。 为了防止读得太慢导致FIFO被写爆,写控制器会根据读控制器传来的读指针进行判定。如果写指针比读指针快了一圈,快要追上读指针的时候,会发出写满信号(w_full),通知外部设备不要再写了。 读控制器的功能也是同理,只不过读写指针互换,而且读指针追上写指针的时候,进行的是读空判定(r_empty)。 我编写的异步FIFO,和读写指针相关的所有寄存器,都比双端口RAM的地址宽度多1位,且多出的1位在最高位上。对于读写控制器来说,只需要对比读写指针的最高位,就能知道读写指针是不是跑在同一圈,简化了读空写满的检测步骤。 ![极简FIFO](https://images.gitee.com/uploads/images/2021/1117/190920_471149b0_8241888.png "马上读空.png") 下面举一个例子,建立一个深度为8的FIFO,RAM的地址宽度为3位,读写指针的宽度为4位。 此时写指针wptr与读指针rptr指向RAM的同一个地址。 若wptr=4'b0100,rptr=4'b0100,两者在同一圈,读空标志置1。 若wptr=4'b0100,rptr=4'b1100,读指针快一圈,写满标志置1。 ##### 二进制与格雷码 能看到这里的人,想必应该都明白二进制编码是什么。 格雷码和二进制编码的不同之处在于,二进制编码的数值+1,连续进位会导致多个位的状态发生变化,而格雷码数值+1在任何情况下都只会有1位发生变化。读写指针跨时钟域同步会遇到各种亚稳态的情况,指针的变化又只会不断+1,因此利用格雷码的特性可以极大降低数据出错的概率。 二进制与格雷码的相互转换可以使用组合逻辑完成。 二进制转格雷码,将二进制码右移一位,然后与原二进制码进行异或。 ![二进制转格雷码](https://images.gitee.com/uploads/images/2021/1117/191114_eee1aecb_8241888.png "rptr_grey.png") 读指针转换的verilog实现`assign rptr_grey = (rptr >> 1) ^ rptr; ` ![格雷码转二进制](https://images.gitee.com/uploads/images/2021/1117/191221_9ccb7fd1_8241888.png "grey_bin.png") 格雷码转二进制,稍微麻烦一点。 读时钟域的写指针格雷码转二进制的verilog使用行为级描述实现,实际综合成组合逻辑。 ``` integer j; always @ (rq2_wptr_grey) begin w2rptr[ASIZE-1]=rq2_wptr_grey[ASIZE-1]; for(j=ASIZE-2;j>=0;j=j-1) w2rptr[j]=w2rptr[j+1]^rq2_wptr_grey[j]; end ``` 如果觉得不错,可以去B站支持一下[详解异步FIFO原理与Verilog模型](https://www.bilibili.com/read/cv13048274)。 ## 功能仿真 进入两种仿真文件夹查看