文章目录
FIFO 用Verilog实现
- 简介:https://www.jianshu.com/p/80f09a832b86
- 异步FIFO:https://www.cnblogs.com/BitArt/archive/2013/04/10/3010073.html
- 同步FIFO:《Verilog HDL数字集成电路设计与应用》
- https://www.cnblogs.com/mikewolf2002/p/10945488.html
一、FIFO 整理
1、FIFO简介
- FIFO:First In First Out,一种先进先出的数据缓存器。
- 与普通存储器的区别:
- 没有外部读写地址线,使用起来简单
- 缺点:只能顺序写入数据、顺序读出数据,其数据地址由内部读写指针自动 +1 完成,不能读写某个指定的地址
- 用途:
- 1、异步时钟之间的接口电路,在两个不同时钟系统之间快速而方便的传输实时数据。
- 2、对于不同宽度的数据接口用FIFO。如单片机8位数数据输出,DSP可能是16位数据输入,之间可用FIFO来达到数据匹配。
- 分类:
- 同步FIFO:读时钟 和 写时钟为同一个时钟,在时钟沿来临时同时发生读、写操作
- 能很好的避免毛刺,但存在 Clock skew
- 异步FIFO:读写时钟相互独立
- 容易产生毛刺
- 同步FIFO:读时钟 和 写时钟为同一个时钟,在时钟沿来临时同时发生读、写操作
- FIFO 常见参数
- FIFO宽度:指 FIFO一次读写操作的数据位;
- FIFO深度:指 FIFO可用存储多少个N位的数据(如果宽度为 N)
- 满标志:FIFO已满或将要满时,由FIFO的状态电路送出的一个信号,以阻止 FIFO 的写操作继续向 FIFO 中写数据而造成溢出(overflow)。
- 空标志:FIFO已空或者将要空时由FIFO的状态电路送出的一个信号,以阻止FIFO的读操作继续从 FIFO中读出数据而造成无效数据的读出(underflow)
- 读时钟、写时钟
- 写指针:总是指向下一个将要被写入的单元,复位时,指向第1个单元(编号为0)
- 读指针:总是指向当前要被读出的数据。复位时,指向第1个单元(编号为0)。
- FIFO设计的难点:判断FIFO的 满/空状态。
- 异步时钟要用两级触发器解决跨时钟域传输的亚稳态问题
- 用格雷码进行 空/满比较,防止多位同时变化导致采样出错。
2、多时钟域设计中,不同时钟域数据如何进行交换?
- 跨时钟域传输,要防止亚稳态的出现和传播
- 单个信号跨时钟域传输,采用**两级触发器(一位同步器)**来同步
- 数据流或地址总线跨时钟域,可采用异步FIFO或双口RAM实现时钟同步
- 多位数据,可采用保持寄存器 加 握手信号的方法
3、时序电路如何实现延时?
- 异步电路:
- 可通过1个buffer、两级非门等 —— 不合适同步电路
- 同步电路:(时序控制)
- 1、对于比较大和特殊要求的延时,可通过高速时钟产生计数器,通过计数器控制延时
- 2、对于比较小的延时,可通过D触发器打一拍,延时一个时钟周期,而且完成了信号与时钟的初次同步,在输入信号采样和增加时序约束余量中使用
- 注意:# 5 是行为级描述方式,可用于仿真,不能用于综合
4、非同源时钟同步化:
- 非同源时钟,数据的建立时间和保持时间难以得到保证
- 解决:带使能端的D触发器 + 高频时钟(频率高于系统所有源时钟),便可使系统中所有源时钟同步。
- 例:系统时钟设计
- 系统时钟有两个不同源时钟,一个为 3MHz,一个为 5MHz,不同的触发器使用不同的时钟。为了使系统稳定,假设引入一个20MHz的时钟,则这个20MHz的时钟怎样才能将3MHz 和 5MHz时钟同步化呢?
- 解:20MHz的高频时钟将作为系统时钟,输入到所有触发器的时钟端。3M_EN 和 5M_EN 将控制所有触发器的使能端。即可将任何非同源时钟同步化
- 异步信号输入总是无法满足数据的建立保持时间,建议将所有的异步输入都先经过双触发器同步。
- 系统时钟有两个不同源时钟,一个为 3MHz,一个为 5MHz,不同的触发器使用不同的时钟。为了使系统稳定,假设引入一个20MHz的时钟,则这个20MHz的时钟怎样才能将3MHz 和 5MHz时钟同步化呢?
二、异步FIFO设计
1、FIFO的 空/满 检测
- FIFO设计的关键:产生可靠的FIFO读写指针和生成FIFO 空/满 状态标志。
FIFO为空:读写指针相等时。
- 发生情况:
- 复位操作时
- 或 当读指针读出FIFO中最后一个字后,追赶上了写指针时。
FIFO为满:读写指针再次相等时
- 发生情况:写指针转了一圈,折回来(wrapped around)又追上了读指针
区分满状态 和 空状态
- 指针中添加一个额外的位(extra bit):
- 当写指针增加并越过最后一个FIFO地址时,就将写指针这个未用的MSB加1,其他位回零。读指针也执行类似操作。
- 对深度为 2n的FIFO,需要的读/写 指针位宽为 (n+1) 位。如深度为8的FIFO需要4bit的计数器,0000 ~ 1000,1001~ 1111,MSB作为折回标志位,低3位为指针
- 说明:
- 如果两个指针的MSB 不同,说明写指针比读指针多折回了一次(写指针折回次数 ≥ 读指针折回次数)
- 如果两个指针的MSB相同,说明两个指针的折回次数相等。
二进制FIFO指针的考虑
- 将一个二进制数从一个时钟域同步到另一个时钟域的时候很容易出现问题——采用二进制计数器时所有位可能同时变化,在同一个时钟沿同步多个信号的变化会产生亚稳态问题。
- 解决:(单bit可用两级触发器同步)多 bit 用格雷码。用二进制码来定位地址,用二进制码转换的格雷码同步地址到另一个时钟域来判断空满状态。
- 需要一个二进制到格雷码的转换电路,将地址变为相应的格雷码。
- 解决:(单bit可用两级触发器同步)多 bit 用格雷码。用二进制码来定位地址,用二进制码转换的格雷码同步地址到另一个时钟域来判断空满状态。
用格雷码对比,判断空/满状态
- 使用格雷码
- 优点:解决了跨时钟与地址传输问题
- 缺点:新问题——格雷码域如何判断空和满
- FIFO为空:读、写指针完全相等(包括MSB)
- FIFO为满:格雷码除了MSB外,具有镜像对称的特点。当读指针指向7,写指针指向8时,除了MSB,其余位皆相等,但是不为满。Gray域判断为满的条件:
- wptr 和 rptr 的MSB不相等,因为 wptr 必须比 rptr 多折回一次
- wptr 和 rptr 的次高位不相等。如 0100和 1000,转化为二进制对应0111 和 1111,MSB不同说明多折回一次,111代表同一位置。
- 剩下的其余位完全相等。
2、异步FIFO结构
6个模块:顶层、DualRAM、2个跨时钟域同步指针模块、空/满逻辑判断
3、异步FIFO设计步骤
- 写时钟域:
- (1)根据写使能wr_en和写满标志位wr_full产生二进制写指针
- (2)根据二进制写指针产生双端口RAM的写地址
- (3)由二进制写指针转换成格雷码写指针
- (4)对格雷码读指针在写时钟域中进行两级同步得同步后格雷码读指针
- (5)同步后格雷码读指针转化成同步后二进制读指针
- (6)步骤(3)与步骤(4)比较得写满标志位wr_full
- (7)步骤(1)与步骤(5)相减得指示写FIFO的数据量
- 读时钟域:
- (8)根据读使能rd_en和读空标志位rd_empty产生二进制读指针
- (9)根据二进制读指针产生双端口RAM的读地址
- (10)由二进制读指针转换成格雷码读指针
- (11)对格雷码写指针在读时钟域中进行两级同步得同步后格雷码写指针
- (12)同步后格雷码写指针转化成同步后二进制写指针
- (13)步骤(10)与步骤(11)比较得读空标志位rd_empty
- (14)步骤(8)与步骤(12)相减得指示读FIFO的数据量
4、异步FIFO设计
// 1、顶层模块
module AsyncFIFO
#(parameter ASIZE = 4, //地址位宽
parameter DSIZE = 8) // 数据位宽
(input [DSIZE-1:0] wdata,
input winc,wclk,wrst_n, //写请求信号、写时钟、写复位
input rinc,rclk,rrst_n,
output [DSIZE-1:0] rdata,
output wfull,
output rempty);
wire [ASIZE-1:0] waddr,raddr;
wire [ASIZE:0] wptr,rptr,wq2_rptr,rq2_wptr;
//检测空满状态之前,需要将指针同步到另一个时钟域时,使用格雷码,可以降低同步过程中亚稳态出现的概率
sync_r2w I1_sync_r2w(
.wq2_rptr(wq2_rptr), //{wq2_rptr,wq1_rptr}是两级触发器,用来将读地址同步到写时钟域
.rptr(rptr),
.wclk(wclk),
.wrst_n(wrst_n));
sync_w2r I2_sync_w2r(
.rq2_wptr(rq2_wptr), // {rq2_wptr,rq1_wptr}是两级触发器,将写地址同步到读时钟域
.wptr(wptr),
.rclk(rclk),
.rrst_n(rrst_n));
//DualRam
DualRAM #(DSIZE,ASIZE) I3_DualRAM(
.rdata(rdata),
.wdata(wdata),
.waddr(waddr),
.raddr(raddr),
.wclken(winc), //写时钟使能
.wclk(wclk));
// 空、满比较逻辑
rptr_empty #(ASIZE) I4_rptr_empty(
.rempty(rempty),
.raddr(raddr), // 二进制地址
.rptr(rptr), // 格雷码地址(比二进制多一位)
.rq2_wptr(rq2_wptr), //跨时钟域传输过来的地址
.rinc(rinc), // 在前一个二进制地址基础上增加的量,到下一个二进制地址
.rrst_n(rrst_n));
wptr_full #(ASIZE) I5_wptr_full(
.wfull(wfull),
.waddr(waddr),
.wptr(wptr),
.wq2_rptr(wq2_rptr),
.winc(winc),
.wclk(wclk),
.wrst_n(wrst_n));
endmodule
//2、DualRAM 模块
module DUalRAM
#(
parameter DATA_SIZE = 8, //数据位宽
parameter ADDR_SIZE = 4 //地址位宽
)
(input wclken,wclk,
input [ADDR_SIZE-1:0] raddr,waddr,
input [DATA_SIZE-1:0] wdata,rdata
);
localparam RAM_DEPTH = 1 << ADDR_SIZE; //RAM深度=2*ADDR_WIDTH
reg [DATA_SIZE-1:0] Mem [RAM_DEPTH-1:0];
always@(posedge wclk)
begin
if(wclken)
Mem[waddr] <= wdata;
end
assign rdata = Mem[raddr];
endmodule
// 3、4、同步模块
module sync_r2w // 读地址同步到写时钟域
#(parameter ADDRSIZE = 4)
(
output reg [ADDRSIZE:0] wq2_rptr,
input [ADDRSIZE:0] rptr,
input wclk,wrst_n
);
reg [ADDRSIZE:0] wq1_rptr;
always@(posedge wclk or negedge wrst_n)
begin
if(!wrst_n)
{wq2_rptr,wq1_rptr} <= 0;
else
{wq2_rptr,wq1_rptr} <= {wq1_rptr,rptr};
end
endmodule
module sync_w2r // 写地址同步到读时钟域
#(parameter ADDRSIZE = 4)
(
output reg [ADDRSIZE:0] rq2_wptr,
input [ADDRSIZE:0] wptr,
input rclk,rrst_n
);
reg [ADDRSIZE:0] rq1_wptr;
always@(posedge rclk or negedge rrst_n)
begin
if(!rrst_n)
{rq2_wptr,rq1_wptr} <= 0;
else
{rq2_wptr,rq1_wptr} <= {rq1_wptr,wptr};
end
endmodule
// 5、空判断逻辑
module rptr_empty
#(parameter ADDRSIZE = 4)
(
output reg rempty,
output [ADDRSIZE-1:0] raddr,
output reg [ADDRSIZE:0] rptr,
input [ADDRSIZE:0] rq2_wptr,
input rinc,rclk,rrst_n);
reg [ADDRSIZE:0] rbin;
wire [ADDRSIZE:0] rgraynext,rbinnext;
wire rempty_val;
//格雷码 读地址指针
always@(posedge rclk or negedge rrst_n) begin
if(!rrst_n)
begin
rbin <= 0;
rptr <= 0;
end
else
begin
rbin <= rbinnext;
rptr <= rgraynext;
end
end
//gray码计数逻辑:根据是否为空得到二进制地址,然后转化为gray码地址
assign rbinnext = !rempty ?(rbin + rinc):rbin;
assign rgraynext = (rbinnext >>1) ^ rbinnext; //二进制到格雷码的转换
assign raddr = rbin[ADDRSIZE-1:0];
// 读指针是一个 n 位的gray码,比FIFO寻址所需的位宽大一位
// 当读指针和同步过来的写指针完全相等时(包括MSB),说明二者折回次数一致,FIFO为空
assign rempty_val = (rgraynext == rq2_wptr);
always@(posedge rclk or negedge rrst_n)
begin
if(!rrst_n)
rempty <= 1'b1;
else
rempty <= rempty_val;
end
endmodule
//6、满判断逻辑
module wptr_full
#(parameter ADDRSIZE = 4)
(
output reg wfull,
output [ADDRSIZE-1:0] waddr,
output [ADDRSIZE:0] wptr,
input [ADDRSIZE:0] wq2_rptr,
input winc,wclk,wrst_n);
reg [ADDRSIZE:0] wbin;
wire [ADDRSIZE:0] wgraynext,wbinnext;
wire wfull_val;
//格雷码指针
always@(posedge wclk or negedge wrst_n)
if(!wrst_n)
begin
wbin <= 0;
wptr <= 0;
end
else
begin
wbin <= wbinnext;
wptr <= wgraynext;
end
//gray码计数器
assign wbinnext = !wfull ? (wbin + winc) : wbin;
assign wgraynext = (wbinnext >>1)^wbinnext;
assign waddr = wbin[ADDRSIZE-1:0];
// 判断满:最高位和次高位不同,其余位完全相等
assign wfull_val = (wgarynext == (~wq2_rptr[ADDRSIZE:addrsize-1],wq2_rptr[ADDRSIZE-2:0]));
always@(posedge wclk or negedge wrst_n)
if(!wrst_n)
wfull <= 1'b0;
else
wfull <= wfull_val;
endmodule
5、FIFO相关问题
-
例:用 Verilog实现一个 FIFO控制器(包括空,满,半满信号)?
- 8个always模块实现。两个用于读写FIFO,两个用于产生头地址head 和 尾地址tail,一个产生 counter计数,剩下三个根据 counter的值产生 空、满、半满信号。
-
例:用系统人物 $readmemb 初始化 memory.mem :
reg[DATA_SIZE-1:0] mem_name[RAM_Depth-1:0]; //RAM深度=2^ADDR_WIDTH $readmemb("init.dat",mem_name)
三、同步FIFO
- 同步FIFO因为RAM是在同一时钟下进行读写,所以读写指针直接比较,产生空满信号
module fifo_sync
#(
parameter FIFO_WIDTH = 32,
parameter ADDR_WIDTH = 3,
parameter FIFO_DEPTH = 8
)
(
input clk,
input rst_n,
input [FIFO_WIDTH-1:0] wr_data,
input rq, //read request
input wq, //write request
output reg [FIFO_WIDTH-1:0] rd_data,
output full,
output empty
);
//internal signal
reg[FIFO_WIDTH-1:0] fifo_mem[FIFO_DEPTH-1:0];
reg[ADDR_WIDTH:0] counter; //extra one bit for counter
reg[ADDR_WIDTH-1:0] rd_ptr;
reg[ADDR_WIDTH-1:0] wr_ptr;
//set full and empty
assign full=(counter==FIFO_DEPTH);
assign empty=(counter==0);
//set current fifo counter value
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
counter<=0;
else if((wq && full)&&(rq && empty))
counter <= counter;
else if(rq&&!empty)
counter <= counter - 1;
else if(wq&&!full)
counter <= counter + 1;
else
counter <= counter; //no read, no write, keep no change
end
//read data if no empty and read enable
always @(posedge clk or negedge rst_n )
begin
if(!rst_n) begin
rd_data <= 0;
end
if(rq && !empty)
rd_data <= fifo_mem[rd_ptr];
end
//write data if no full and write enable
always @(posedge clk)
begin
if(wq && !full)
fifo_mem[wr_ptr] <= wr_data;
end
//update read and write ptr
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
wr_ptr <= 0;
rd_ptr <= 0;
end
else
begin
if(!full && wq)
begin
wr_ptr <= wr_ptr + 1;
// we can omit these two lines, for it will change to 0 if overflow.
//if(wr_ptr==(FIFO_DEPTH-1))
// wr_ptr<=0;
end
else if(!empty && rq)
begin
rd_ptr <= rd_ptr + 1;
//if(rd_ptr==(FIFO_DEPTH-1))
// rd_ptr<=0;
end
end
end
endmodule