FPGA -手写异步FIFO

一,FIFO原理

        FIFO(First In First Out)是一种先进先出的数据缓存器,没有外部读写地址线,使用起来非常简单,只能顺序写入数据顺序的读出数据,其数据地址内部读写指针自动加1完成,不能像普通存储器那样可以由地址线决定读取或写入某个指定的地址。也正是由于这个特性,使得FIFO可以用作跨时钟域数据传输和数据位宽变换

二,双端口RAM

        FIFO中用来存储数据的器件为双口RAM,首先搭建一个Dual Ram(双口RAM)。我们以一个深度为16,数据位宽为8的Dual Ram为例,框图和时序如下。

        

Dual Ram读端和写端采用两个时钟,可以实现读写时钟为异步时钟,也可以实现读写同时进行的功能。代码实现如下:

// -----------------------------------------------------------------------------
// Author : RLG
// File   : Dual_Ram.v
// -----------------------------------------------------------------------------
`timescale 1ns / 1ps
module Dual_Ram#(parameter 			ADDR_WIDTH = 4,parameter 			DATA_WIDTH = 8)(input 								wrclk	,input 								rdclk	,input 								wr_en	,input 								rd_en	,	input 		[ADDR_WIDTH-1:0]		wr_addr ,input 		[ADDR_WIDTH-1:0]		rd_addr ,input  		[DATA_WIDTH-1:0]		wr_data	,output 	reg [DATA_WIDTH-1:0]		rd_data);/*---------------输入数据打一拍-------------*/reg [ADDR_WIDTH-1:0]	wr_addr_d1;reg [ADDR_WIDTH-1:0]	rd_addr_d1;reg [DATA_WIDTH-1:0]	wr_data_d1;reg 					wr_en_d1  ;reg 					rd_en_d1  ;/*----------------数据寄存----------------*/  reg [DATA_WIDTH-1:0] rd_data_out; reg [DATA_WIDTH-1:0] Data_reg [2**ADDR_WIDTH-1:0];/*---------------输入数据打拍-------------*/always @(posedge wrclk ) begin wr_addr_d1 <= wr_addr;rd_addr_d1 <= rd_addr;wr_data_d1 <= wr_data;wr_en_d1   <= wr_en  ;rd_en_d1   <= rd_en  ;end/*-------------------写数据-----------------*/always @(posedge wrclk ) begin if(wr_en_d1)Data_reg[wr_addr_d1] <= wr_data_d1;end/*-------------------读数据-----------------*/always @(posedge rdclk ) begin if(rd_en_d1)rd_data_out <=  Data_reg[rd_addr_d1];end/*-----------------输出打一拍----------------*/always @(posedge rdclk ) begin rd_data <= rd_data_out;endendmodule

二、FIFO地址设计

        我们知道FIFO中是没有地址线的,地址靠自身计数器自加1来控制,那么我们很容易想到把外部输入信号wr_addr和rd_addr换成内部信号并用计数器来控制其自加,计数器加满之后直接清零,从0重新开始写/读,循环往复。由于写端和读端的时钟速率不同,就会有快慢的问题,        

        那么就出现了一个问题,以地址2为例,写入的数据还没有被读出,又被新的数据覆盖了,造成数据丢失;或者写入的数据已经被读出,新的数据还没有写进来,地址2的老数据又被读了一遍,造成数据重复。

        为了解决上述问题,引入 full empty 信号来表示内部RAM中的数据写满或者读空,新的框图如下所示。

        

        如何产生full和empty信号呢,我们可以用 wr_addrrd_addr 来做判断,当 wr_clk 大于 rd_clk 时,会产生写满的情况,如下图中黄色部分代表已经写入数据,还未被读取,白色代表数据已被读取,图1中当 waddr>raddr时,waddr-raddr → 1111 - 0001 = 1110 可以表示两者的差值。

        图2中当 waddr<raddr 时,计算两者的差值为16 – raddr + waddr → 10000 - 1100 +1010 = 1110,此时的 waddr – raddr → 1010-1100 →1010+0011+0001=1110,两者结果相同,所以无论 waddr 大于 raddr 还是小于 raddr,都可以用 waddr-raddr 来表示写比读多几个数据。此时再引入一个full_limit用来设置一个写满的阈值。当waddr – raddr >= full_limit 时,full信号拉高,停止写入。

        同理,读比写快的情况下引入一个empty_limit来作为读空的阈值,当 waddr – raddr <= empty_limit 时。empty信号拉高停止读出。在实际工程中可以根据实际需要和 fifo 的设计区别灵活设置 full_limit 和empty_limit 的数值

三、空满信号判断

        使用读写地址进行判断空满信号。读地址rd_addr是在读时钟域wr_clk内,空信号empty也是在读时钟域内产生的;而写地址wr_addr是在写时钟域内,且满信号full也是在写时钟域内产生的。 那么,要使用读地址rd_addr与写地址wr_addr对比产生空信号empty,可以直接对比吗?        

        答案是不可以。

        因为这两个信号处于不同的时钟域内,要做跨时钟域CDC处理,而多bit信号跨时钟域处理,常用的方法就是使用异步FIFO进行同步可是我们不是在设计异步FIFO吗?

        于是,在这里设计异步FIFO,多bit跨时钟域处理的问题可以转化单bit跨时钟域的处理,把读写地址转换为格雷码后再进行跨时钟域处理,因为无论多少比特的格雷码,每次加1,只改变1位。把读地址rd_addr转换为格雷码,然后同步到写时钟域wr_clk;同样的,把写地址指wr_addr转换为格雷码,然后同步到读时钟域rd_clk

        二进制转格雷码:二进制的最高位作为格雷码的最高位,次高位的格雷码为二进制的高位和次高位相异或得到,其他位与次高位相同。

        代码:

  	assign wr_gray = (wr_addr >> 1) ^ wr_addr;assign rd_gray = (rd_addr >> 1) ^ rd_addr;

        格雷码转二进制:使用格雷码的最高位作为二进制的最高位,二进制次高位产生过程是使用二进制的高位和次高位格雷码相异或得到,其他位的值与次高位产生过程相同。

        代码:

assign wr_bin[ADDR_WIDTH-1] = wr_gray_d2[ADDR_WIDTH-1];genvar i;generatefor ( i = 0; i < ADDR_WIDTH-1; i=i+1) beginassign wr_bin[i] = wr_bin[i+1] ^ wr_gray_d2[i];endendgenerateassign rd_bin[ADDR_WIDTH-1] = rd_gray_d2[ADDR_WIDTH-1];genvar j;generatefor ( j = 0; j < ADDR_WIDTH-1; j=j+1) beginassign rd_bin[j] = rd_bin[j+1] ^ rd_gray_d2[j];endendgenerate

四、跨时钟域同步

        如何避免漏采和重采,首先考虑一个问题,地址同步要在哪个时钟域进行呢,我们所期望的结果是慢时钟地址同步到快时钟域,以免发生快时钟域信号漏采导致的读空或者写满。至于重采的情况,即慢时钟域信号被多采了一次,只会在判断空满状态时更安全,不会导致读空和写满这种不安全现象的出现。不过这样会产生虚假的full和empty信号,即full信号已经拉高,但ram中仍存有可用的地址,或者empty信号已经拉高,但ram中仍存有可被读出的数据。虽然效率和资源上有一点浪费,但不会发生丢失数据或读错数据的不安全行为

        那怎么实现慢时钟域的信号同步到快时钟域呢?因为若同时读写时出现 empty 则一定是读时钟快于写时钟,所以在判断 empty 状态时,读时钟域为快时钟,把较慢的写时钟同步到读时钟域来判断 empty。同理,若同时读写时出现 full 则一定是写时钟快于读时钟,所以在判断 full 状态时,写时钟域为快时钟,把较慢的读时钟同步到写时钟域来判断 full。以判断empty状态为例,过程如下图所示:

        其中B2G模块(二进制转格雷码)G2B模块(格雷码转二进制)empty判断模块均为组合逻辑,所以加一级D触发器以满足时序。圈中的两级D触发器用作消除跨时钟域同步的亚稳态。empty信号在RCLK快于WCLK时产生,中间虽然加入了四级D触发器,导致写地址同步到读时钟域时是之前的老地址,这和之前采重的问题一样,只会让empty的判断更安全,但会造成少许的资源浪费,属于保守但安全的做法。

        至此,一个简易的异步fifo就被设计出来了,总体框图如下:

代码:

// -----------------------------------------------------------------------------
// Author : RLG
// File   : async_fifo.v
// -----------------------------------------------------------------------------
`timescale 1ns / 1ps
module async_fifo#(parameter       ADDR_WIDTH = 4    ,parameter       DATA_WIDTH = 8    ,parameter       EMPTY_LIMIT= 1'b1  ,parameter       FULL_LIMIT = 4'd15)(input                           wrclk   ,input                           rdclk   ,input                           wr_rst_n,input                           rd_rst_n,   input                           wr_en   ,input                           rd_en   , input       [DATA_WIDTH-1:0]    wr_data ,output  reg [DATA_WIDTH-1:0]    rd_data ,output  reg             		    empty   ,output  reg             		    full);/*---------------输入数据打一拍-----------*/reg   [DATA_WIDTH-1:0]  wr_data_d1  ;reg                     wr_en_d1    ;reg                     rd_en_d1    ;/*--  --------------数据寄存----------------*/  reg   [DATA_WIDTH-1:0]  Data_reg [2**ADDR_WIDTH-1:0];/*--  --------------读写地址----------------*/ reg   [ADDR_WIDTH-1:0]  wr_addr    ;reg   [ADDR_WIDTH-1:0]  rd_addr    ;/*--  --------------二进制转格雷码------------*/ wire  [ADDR_WIDTH-1:0]  wr_gray     ;wire  [ADDR_WIDTH-1:0]  rd_gray     ;reg   [ADDR_WIDTH-1:0]  wr_gray_d0  ;reg   [ADDR_WIDTH-1:0]  rd_gray_d0  ;reg   [ADDR_WIDTH-1:0]  wr_gray_d1  ;reg   [ADDR_WIDTH-1:0]  rd_gray_d1  ;reg   [ADDR_WIDTH-1:0]  wr_gray_d2  ;reg   [ADDR_WIDTH-1:0]  rd_gray_d2  ;/*--  --------------格雷码转二进制------------*/ wire  [ADDR_WIDTH-1:0]  wr_bin    ;wire  [ADDR_WIDTH-1:0]  rd_bin    ;reg   [ADDR_WIDTH-1:0]  rd_bin_d0   ;reg   [ADDR_WIDTH-1:0]  wr_bin_d0 ;/*----------------empty 判读---------------*/ wire          empty_logic ;/*----------------full 判读---------------*/ wire          full_logic  ;/*---------------------------------------*\输入数据打拍\*---------------------------------------*/always @(posedge wrclk ) begin wr_data_d1 <= wr_data;wr_en_d1   <= wr_en  ;rd_en_d1   <= rd_en  ;end/*---------------------------------------*\写地址\*---------------------------------------*/always @(posedge wrclk ) begin if(~wr_rst_n)wr_addr<= 0;else if(wr_en_d1 && ~full) beginif(wr_addr == 'd15)wr_addr <= 0;elsewr_addr <= wr_addr + 1'b1;endend/*---------------------------------------*\读地址\*---------------------------------------*/always @(posedge rdclk ) begin if(~rd_rst_n)rd_addr<= 0;else if(rd_en_d1 && ~empty) beginif(rd_addr == 'd15)rd_addr <= 0;elserd_addr <= rd_addr + 1'b1;endend/*---------------------------------------*\写数据\*---------------------------------------*/always @(posedge wrclk ) begin if(wr_en_d1 && ~full)Data_reg[wr_addr] <= wr_data_d1;end/*---------------------------------------*\读数据\*---------------------------------------*/always @(posedge rdclk ) begin if(rd_en_d1 && ~empty)rd_data <=  Data_reg[rd_addr];end/*---------------------------------------*\二进制转格雷码\*---------------------------------------*/assign wr_gray = (wr_addr >> 1) ^ wr_addr;assign rd_gray = (rd_addr >> 1) ^ rd_addr;always @(posedge wrclk ) begin wr_gray_d0 <= wr_gray;endalways @(posedge rdclk ) begin rd_gray_d0 <= rd_gray;end/*---------------------------------------*\格雷码转二进制\*---------------------------------------*/ always @(posedge wrclk ) begin if(!wr_rst_n)beginrd_gray_d1 <= 0;rd_gray_d2 <= 0;endelse beginrd_gray_d1 <= rd_gray_d0;rd_gray_d2 <= rd_gray_d1;endendalways @(posedge rdclk ) begin if (!rd_rst_n) beginwr_gray_d1 <= 0;wr_gray_d2 <= 0;endelse beginwr_gray_d1 <= wr_gray_d0;wr_gray_d2 <= wr_gray_d1;endendassign wr_bin[ADDR_WIDTH-1] = wr_gray_d2[ADDR_WIDTH-1];genvar i;generatefor ( i = 0; i < ADDR_WIDTH-1; i=i+1) beginassign wr_bin[i] = wr_bin[i+1] ^ wr_gray_d2[i];endendgenerateassign rd_bin[ADDR_WIDTH-1] = rd_gray_d2[ADDR_WIDTH-1];genvar j;generatefor ( j = 0; j < ADDR_WIDTH-1; j=j+1) beginassign rd_bin[j] = rd_bin[j+1] ^ rd_gray_d2[j];endendgeneratealways @(posedge wrclk) begin wr_bin_d0 <= wr_bin;endalways @(posedge rdclk) begin rd_bin_d0 <= rd_bin;end/*---------------------------------------*\empty\*---------------------------------------*/ assign empty_logic = ((wr_bin_d0 - rd_addr) <= EMPTY_LIMIT)? 1'b1 : 1'b0;always @(posedge rdclk) begin empty <= empty_logic;end/*---------------------------------------*\full\*---------------------------------------*/assign full_logic = ((wr_addr - rd_bin_d0) >=  FULL_LIMIT)? 1'b1 : 1'b0;always @(posedge wrclk) begin full <= full_logic;endendmodule 	

 仿真代码:

`timescale 1ns / 1ps
module tb_async_fifo;parameter  	ADDR_WIDTH   = 4;parameter  	DATA_WIDTH   = 8;parameter 	EMPTY_LIMIT  = 1'b1;parameter  	FULL_LIMIT   = 4'd15;reg                   wr_rst_n;reg                   rd_rst_n;reg                   wr_en;reg                   rd_en;reg  [DATA_WIDTH-1:0] wr_data;wire [DATA_WIDTH-1:0] rd_data;wire                  empty;wire                  full;reg 					wr_clk;reg 					rd_clk;initial begin wr_clk = 0;rd_clk = 0;wr_rst_n = 0;rd_rst_n = 0;wr_en = 0;rd_en = 0;#20wr_rst_n = 1;rd_rst_n = 1;#20wr_en = 1;#30rd_en = 1;endalways #10 wr_clk = ~wr_clk;always #5  rd_clk = ~rd_clk;always @(posedge wr_clk ) begin if(!wr_rst_n)wr_data <= 0;else if(wr_data == 15)wr_data <= 0;else if(wr_en)wr_data <= wr_data + 1;endasync_fifo #(.ADDR_WIDTH(ADDR_WIDTH),.DATA_WIDTH(DATA_WIDTH),.EMPTY_LIMIT(EMPTY_LIMIT),.FULL_LIMIT(FULL_LIMIT)) inst_async_fifo (.wrclk    (wr_clk),.rdclk    (rd_clk),.wr_rst_n (wr_rst_n),.rd_rst_n (rd_rst_n),.wr_en    (wr_en),.rd_en    (rd_en),.wr_data  (wr_data),.rd_data  (rd_data),.empty    (empty),.full     (full));endmodule

 仿真波形:

五,总结

        在处理跨时钟域时,转换为格雷码处理。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/681043.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Netty 实现dubbo rpc

一、RPC 的基本介绍 RPC (Remote Procedure Call) 远程过程调用&#xff0c;是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序&#xff0c;而程序员无需额外的为这个交互编程。也就是说可以达到两个或者多个应用程序部署在不同的服务器上&…

YOLOv5入门 | 重要性能衡量指标、训练结果评价及分析及影响mAP的因素

在深度学习目标检测领域&#xff0c;YOLOv5成为了备受关注的模型之一。训练结束后&#xff0c;对训练结果的仔细分析至关重要。这就涉及到了重要性能的衡量指标。本文将手把手教学如何进行YOLOv5的结果分析和重要性能指标的参考&#xff0c;以帮助您更好地学习深度学习目标检测…

操作系统原理与实验——实验十分段存储管理

实验指南 运行环境&#xff1a; Dev c 算法思想&#xff1a; 本实验是模拟分段存储管理&#xff0c;系统需要建立两张分区表&#xff0c;分别是已分配和未分配分区表&#xff0c;首先根据装入作业的大小判断是否小于空闲分区的总容量&#xff0c;若满足&#xff0c;则对该作业继…

自注意力架构大成者_Transformer(Pytorch 17)

1 模型简介 在上节比较了 卷积神经网络&#xff08;CNN&#xff09;、循环神经网络&#xff08;RNN&#xff09;和 自注意力&#xff08;self‐attention&#xff09;。值得注意的是&#xff0c; 自注意力同时具有并行计算和最短的最大路径长度这两个优势。因此&#xff0c;使…

数字工厂管理系统如何实现生产过程透明化

随着科技的飞速发展&#xff0c;数字化转型已成为制造业不可逆转的趋势。数字工厂管理系统作为实现生产自动化、智能化的重要工具&#xff0c;其在提升生产效率、降低运营成本、优化资源配置等方面的作用日益凸显。其中&#xff0c;实现生产过程的透明化是数字工厂管理系统的重…

Netty详解,含EventLoop、Channel、Handler、Pipeline和ByteBuf等组件详解(长文)

前言&#xff1a;本文是博主学习视频后所整理的笔记&#xff0c;用于回顾。 Netty中的工作原理&#xff1a;首先使用 Bootstrap/ServerBootstrap 启动器启动&#xff0c;通过使用包含了多个EventLoop 的 EventLoopGroup 去处理多组 Channel 的事件循环。而每个 Channel 是一个产…

Java设计模式 _结构型模式_外观模式

一、外观模式 1、外观模式 外观模式&#xff08;Facade Pattern&#xff09;是一种结构型模式。主要特点为隐藏系统的复杂性&#xff0c;并向客户端提供了一个客户端可以访问系统的接口。这有助于降低系统的复杂性&#xff0c;提高可维护性。当客户端与多个子系统之间存在大量…

46. UE5 RPG 增加角色受击反馈

在前面的文章中&#xff0c;我们实现了对敌人的属性的初始化&#xff0c;现在敌人也拥有的自己的属性值&#xff0c;技能击中敌人后&#xff0c;也能够实现血量的减少。 现在还需要的就是在技能击中敌人后&#xff0c;需要敌人进行一些击中反馈&#xff0c;比如敌人被技能击中后…

社交新时代:Facebook如何塑造我们的互动方式

在当今社交媒体充斥着人们日常生活的情况下&#xff0c;Facebook作为影响力最大的社交平台之一&#xff0c;已经深深地影响了我们的互动方式和社交行为。从初期的大学校园社交网络发展到如今的全球社交巨头&#xff0c;Facebook已经成为许多人日常生活中不可或缺的组成部分。本…

学习大数据,所需更要的shell基础(2)

文章目录 read读取控制台输入函数系统函数bashnamedirname 自定义函数Shell工具&#xff08;重点&#xff09;cutawk 正则表达式入门常规匹配常用特殊字符 read读取控制台输入 1&#xff09;基本语法 read (选项) (参数) ①选项&#xff1a; -p&#xff1a;指定读取值时的提示…

补强板大全

一&#xff0e;名词介绍&#xff1a; 补强板又叫Stiffeners&#xff0c;加强板&#xff0c;增强板&#xff0c;支撑板&#xff0c;保强板&#xff0c;裙托板&#xff0c;撑托板&#xff0c;托强板&#xff0c;加强筋。补强板主要用在建筑&#xff0c;石油管道&#xff0c;机工设…

STL 总结

STL 在 C 标准模板库&#xff08;STL&#xff09;中&#xff0c;主要包含了一系列的容器、迭代器、算法、函数对象、适配器。 容器 容器是用于存储数据的类模板。STL 容器可以分为序列型容器、关联型容器和链表型容器三类&#xff1a;序列型容器&#xff1a;vector、deque、…