FIFO(First In First Out)是异步数据传输时经常使用的存储器。该存储器的特点是数据先进先出(后进后出)。其实,多位宽数据的异步传输问题,无论是从快时钟到慢时钟域,还是从慢时钟到快时钟域,都可以使用 FIFO 处理。
FIFO 原理
工作流程
(1) 复位之后,在写时钟和非满状态信号的控制下,数据可以写入 FIFO 中。RAM 的写地址从 0 开始,每写一次数据写地址指针加一,指向下一个存储单元。当 FIFO 写满后,数据将不能再写入,否则数据会因覆盖而丢失。
(2) FIFO 数据为非空、或满状态时,在读时钟和非空状态信号的控制下,数据可以从 FIFO 中读出。RAM 的读地址从 0 开始,每读一次数据读地址指针加一,即指向下一个存储单元。当 FIFO 读空后,就不能再读数据,否则读出的数据将是错误的。
(3) FIFO 的存储结构为双口 RAM,允许读写同时进行。FIFO 的读写指针是循环计数的,即 读写指针对应的 RAM 地址超过 FIFO 深度时,会溢出归零,重新计数。
典型异步 FIFO 结构图如下所示。相关信号及空满状态的原理将在下面一一说明。
读写时刻
(1) 关于写时刻,只要 FIFO 中数据为非满状态,就可以进行写操作;如果 FIFO 为满状态,则禁止再写数据。
(2) 关于读时刻,只要 FIFO 中数据为非空状态,就可以进行读操作;如果 FIFO 为空状态,则禁止再读数据。
(3) 总之,如果一段时间内不间断的对 FIFO 同时进行读写操作,则要求写 FIFO 速率不能大于读 FIFO 速率。
读空状态
(1) 复位时,FIFO 中没有数据,空状态信号拉高。当 FIFO 被写入数据后,空状态信号拉低,表示非空状态。当读数据地址追赶上写地址,即读写地址都相等时,FIFO 为空状态。
(2) 因为 FIFO 是异步的,所以读写地址进行比较时,需要同步打拍逻辑,就需要耗费一定的时间。因此,空状态的指示信号不是实时的,会有一定的延时。如果在这段延迟时间内又有新的数据写入 FIFO,就会出现空状态指示信号有效,但实际上 FIFO 中存在数据的现象。
(3) 严格来讲该空状态指示是错误的。但是产生空状态的意义在于防止读操作对空状态的 FIFO 进行数据读取。产生空状态信号时,实际 FIFO 中有数据,相当于提前判断了空状态信号,此时不再进行读 FIFO 操作也是安全的。所以,该设计从应用上来说是没有问题的。
(4) 牢记,读空状态信号,是在读时钟域产生的。
写满状态
(1) 复位时,FIFO 中没有数据,满信号是拉低的,表示 FIFO 中的数据没有写满 (其实 FIFO 是空的 )。当 FIFO 开始写数据且读操作不进行或读速率相对较慢时,只要写数据地址超过读数据地址的 FIFO 深度时,便会产生满状态信号。此时写地址和读地址也是相等的,但是意义是不一样的。
(2) 此时经常使用多余的 1bit 分别当做读写地址的拓展位,来区分读写地址相同的时候,FIFO 的状态是空还是满状态。当读写地址与拓展位均相同的时候,表明读写数据的数量是一致的,则此时 FIFO 是空状态。如果读写地址相同,拓展位相反,表明写数据的数量已经超过读数据数量的一个 FIFO 深度了,此时 FIFO 是满状态。当然,此条件成立的前提是空状态禁止读操作、满状态禁止写操作。
(3) 同理,由于异步延迟逻辑的存在,满状态信号也不是实时的。但是也相当于提前判断了满状态信号,此时不再进行写 FIFO 操作也不会影响应用的正确性。
(4) 牢记,写满状态信号,是在写时钟域产生的。
格雷码
(1) 当读写时钟都是同一个时钟时,此时 FIFO 是同步的,直接对读写指针进行比对,产生空、满信号即可。
(2) 当读写时钟是异步的时候,因为读时钟域产生读空信号,写时钟域产生写满信号,所以产生空逻辑信号时,需要将写指针同步到读时钟域,再与读指针进行比较;产生满逻辑信号时,需要将读指针同步到写时钟域,再与写指针进行比较。
(3) 因为读写指针的信号宽度一般都是大于 1bit 的,所以同步处理时不能直接对多位宽的读写指针进行延迟打拍,需要借助格雷码对读写指针进行转换,保证每一个周期内地址指针只有 1bit 变化,然后再进行延迟打拍的同步处理。
(4) 4bit 的二进制码与格雷码之间的变化关系如下所示,其中 ⊕ 表示异或操作符。由图可知,二进制码对应的十六进制码递增时,二进制码对应的相邻的两个格雷码之间只有 1bit 数据有变化。当多位宽信号每次只有 1bit 数据变化时,可以使用延迟打拍的方法对其进行同步处理。
(5) 下面对空逻辑的产生进行举例说明:
5.1) 首先需要对写指针 waddr 进行组合逻辑的格雷码变换 waddr_gray。
5.2) 为了保证 waddr_gary 在读时钟域每次被采集时只有 1bit 数据变化,则 waddr_gray 需要在其源时钟域即写时钟域进行一拍缓存 waddr_gray_d。因为 waddr 到 waddr_gray 的组合逻辑变换时,每次两者之间不只是有 1bit 变化的。
5.3) 在读时钟域对 waddr_gray_d 进行打拍同步,得到读时钟域同步后的写指针为 waddr_gray_rclk。
5.4) 根据格雷码变换规则,空信号有效时二进制码相等的读写指针,变为格雷码之后仍然相等。所以直接使用 waddr_gray_rclk 与读指针进行组合逻辑变换后的格雷码进行相等比较,即可产生读空信号逻辑。
5.5) 需要说明的是,满信号有效时,带有拓展位的读写指针高 1bit 相反、低位相同。所以变为格雷码之后,写满信号产生的条件,则是读写指针高 2bit 相反、低位相同 (请读者思考一下为什么?)。
FIFO 设计
设计要求
为设计应用于各种场景的 FIFO,这里对设计提出如下要求:
(1) FIFO 是异步的,即读写控制信号来自不同的时钟域。
(2) FIFO 深度、宽度参数化,输出空、满状态信号,并输出一个可配置的满状态信号。当 FIFO 内部数据达到设置的参数数量时,该信号拉高,此时需要对格雷码进行反解码。
(3) 输入数据和输出数据位宽可以不一致,但要保证写数据、写地址位宽与读数据、读地址位宽的一致性。例如写数据位宽 8bit,写地址位宽为 6bit(64 个数据)。如果输出数据位宽要求 32bit,则输出地址位宽应该为 4bit(16 个数据)。
双口 RAM 设计
RAM 地址位宽、数据位宽等端口参数可配置,读写位宽一致。实际中 RAMDP(Dual Port) 是需要使用 Memory IP 的,这里创建的 RAM 并没有考虑到异步问题。
Verilog 描述如下。
module ramdp
#( parameter AW = 5 ,
parameter DW = 16
)
(
input CLK_WR , //写时钟
input [DW-1:0] D , //写数据
input WR_EN , //写使能
input [AW-1:0] ADDR_WR ,//写地址
input CLK_RD , //读时钟
input RD_EN , //读使能
input [AW-1:0] ADDR_RD ,//读地址
output reg [DW-1:0] Q //读数据
);
reg [DW-1:0] mem [(1<
计数器设计
计数器用于产生读写地址信息,位宽可配置,不需要设置结束值,让其溢出后自动重新计数即可。同时该计数器还具有格雷码转换与缓存的功能。
Verilog 描述如下。
module ccnt_gray
#(parameter W = 32'd8
)
(
input rstn ,
input clk ,
input en ,
output [W-1:0] cnt ,
output [W-1:0] cnt_gray ,
output [W-1:0] cnt_gray_d
);
reg [W-1:0] cnt_r ;
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
cnt_r <= 'b0 ;
end
else if (en) begin
cnt_r <= cnt_r + 1'b1 ;
end
end
assign cnt = cnt_r ;
assign cnt_gray = cnt ^ (cnt>>1);
reg [W-1:0] cnt_gray_buf ;
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
cnt_gray_buf <= 'b0 ;
end
else begin
cnt_gray_buf <= cnt_gray ;
end
end
assign cnt_gray_d = cnt_gray_buf ;
endmodule
多位宽数据同步设计
读写指针进行格雷码变换并缓存后,每一个计数周期内地址指针只有 1bit 变化,所以可以直接使用延迟打拍的方法进行同步。数据宽度、同步级数均可配置。
Verilog 描述如下。
module data1c_sync
#(parameter DW = 8,
parameter STAGE = 3
)
(
input rstn ,
input clk ,
input [DW-1:0] data_in ,
output [DW-1:0] data_out
);
reg [DW-1: 0] data_r [STAGE-1: 0];
integer i ;
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
for (i=0; i
格雷码反解码
因为该 FIFO 还存在一个可配置的满状态信号输出,所以需要对格雷码同步后的读指针进行反解码,然后在写时钟域与写指针进行比较,以判读当前 FIFO 中数据的具体个数。
module gray_decode
#(parameter W = 32'd8
)
(
input [W-1:0] gray ,
output [W-1:0] gray_decode
);
integer i ;
reg [W-1:0] gray_decode_r ;
always @(*) begin
gray_decode_r[W-1] = gray[W-1];
for (i=W-2; i>=0; i=i-1) begin
gray_decode_r[i] = gray_decode_r[i+1] ^ gray[i];
end
end
assign gray_decode = gray_decode_r ;
endmodule
FIFO 设计
该模块为 FIFO 的主体部分,产生读写控制逻辑,包括读写指针、读写有效时刻以及空、满、可编程满状态的逻辑。
实际上此模块已经是典型的 FIFO 设计,有需要的读者可以直接使用该层次的 FIFO 代码进行测试,甚至应用到自己的设计之中。
module fifo
#( parameter DW = 16 ,
parameter DEPTH = 32 ,
parameter PROG_DEPTH = 16) //可设置深度
(
input rstn, //读写使用一个复位
input wclk, //写时钟
input wren, //写使能
input [DW-1: 0] wdata, //写数据
output wfull, //写满标志
output prog_full, //可编程满标志
input rclk, //读时钟
input rden, //读使能
output [DW-1 : 0] rdata, //读数据
output rempty //读空标志
);
localparam AW = log2b(DEPTH);
//==================== push/wr counter ===============
//wptr/waddr using one more bit to indict new-loop
wire [AW:0] waddr ;
wire [AW:0] waddr_gray ;
wire [AW:0] waddr_gray_d ;
ccnt_gray #(.W(AW+1))
u_push_cnt(
.rstn (rstn),
.clk (wclk),
.en (wren && !wfull), //full 时禁止写
.cnt (waddr),
.cnt_gray (waddr_gray),
.cnt_gray_d (waddr_gray_d)
);
// sync: wptr from wclk to rclk
wire [AW:0] waddr_gray_rclk ;
data1c_sync #(.DW(AW+1), .STAGE(3))
u_waddr_to_rclk
(
.rstn (rstn),
.clk (rclk),
.data_in (waddr_gray_d),
.data_out (waddr_gray_rclk)
);
//============== pop/rd counter ===================
wire [AW:0] raddr ;
wire [AW:0] raddr_gray ;
wire [AW:0] raddr_gray_d ;
ccnt_gray #(.W(AW+1))
u_pop_cnt(
.rstn (rstn),
.clk (rclk),
.en (rden && !rempty), //full 时禁止写
.cnt (raddr),
.cnt_gray (raddr_gray),
.cnt_gray_d (raddr_gray_d)
);
// sync: rdtr from rclk to wclk
wire [AW:0] raddr_gray_wclk ;
data1c_sync #(.DW(AW+1), .STAGE(3) )
u_raddr_to_wclk
(
.rstn (rstn),
.clk (wclk),
.data_in (raddr_gray_d),
.data_out (raddr_gray_wclk)
);
//============== full/empty logic ===================
//(1) empty logic
assign rempty = (raddr_gray == waddr_gray_rclk);
//(2) full logic
assign wfull = (waddr_gray[AW:AW-1] == ~raddr_gray_wclk[AW:AW-1]) &&
(waddr_gray[AW-2:0] == raddr_gray_wclk[AW-2:0]) ;
//(3) porgrammable full
//waddr gray decode
wire [AW:0] raddr_degray_wclk ;
gray_decode #(.W(AW+1))
u_waddr_degray_rclk (
.gray (raddr_gray_wclk),
.gray_decode (raddr_degray_wclk)
);
//prog full
wire [AW:0] waddr_delta = waddr >= raddr_degray_wclk ?
(waddr - raddr_degray_wclk) :
((1<<(AW+1)) + waddr - raddr_degray_wclk) ;
assign prog_full = waddr_delta >= PROG_DEPTH ;
//双口 ram 例化
ramdp #(.AW(AW), .DW (DW))
u_ramdp
(
.CLK_WR (wclk),
.WR_EN (wren & !wfull), //写满时禁止写
.ADDR_WR (waddr[AW-1:0]),
.D (wdata[DW-1:0]),
.CLK_RD (rclk),
.RD_EN (rden & !rempty), //读空时禁止读
.ADDR_RD (raddr[AW-1:0]),
.Q (rdata[DW-1:0])
);
function integer log2b ;
input integer depth ;
for (log2b=0; (1<
FIFO 调用
下面可以调用设计的 FIFO,完成多位宽数据传输的异步处理。
写数据位宽为 4bit,写深度为 32。
读数据位宽为 16bit,读深度为 8,可配置 full 深度为 16。
该模块只是 FIFO 的一个具体应用,用于数据的异步传输、缓存与整合。
//ensure write rate < read rate
module fifo_buf
#( parameter DWI = 4 , //width 4
parameter AWI = 5 , //depth 32
parameter DWO = 16 ,
parameter AWO = 3 ,
parameter PROG_DEPTH = 16
)
(
input rstn, //读写使用一个复位
//data in
input din_clk, //写时钟
input din_en, //写使能
input [DWI-1: 0] din, //写数据
//data out
input dout_clk, //读时钟
output dout_valid, //读使能
output [DWO-1 : 0] dout //读数据
);
wire wfull ; //写满标志
wire prog_full ; //可编程满标志
wire rempty ; //读空标志
wire [DWI-1:0] rdata_fifo ;
wire rden_fifo ;
fifo #(.DW(DWI), .DEPTH(1<
testbench
testbench 描述如下,用于测试空、满逻辑信号,以及读写操作。测试中只列举了输入数据位宽小于输出数据位宽的情景。
`timescale 1ns/1ns
`define SMALL2BIG
module test ;
`ifdef SMALL2BIG
reg rstn ;
reg clk_slow, clk_fast ;
reg [3:0] din ;
reg din_en ;
wire [15:0] dout ;
wire dout_valid ;
//reset
initial begin
clk_slow = 0 ;
clk_fast = 0 ;
rstn = 0 ;
#50 rstn = 1 ;
end
//读时钟 clock_slow 较快于写时钟 clk_fast 的 1/4
//保证读数据稍快于写数据
parameter CYCLE_WR = 40 ;
always #(CYCLE_WR/2/4) clk_fast = ~clk_fast ;
always #(CYCLE_WR/2-1) clk_slow = ~clk_slow ;
//data generate
initial begin
din = 16'h4321 ;
din_en = 0 ;
wait (rstn) ;
//(1) 测试 full、prog_full、empyt 信号
force test.u_data_buf.u_fifo.rden = 1'b0 ;
repeat(32) begin
@(negedge clk_fast) ;
din_en = 1'b1 ;
din = {$random()} % 16;
end
@(negedge clk_fast) din_en = 1'b0 ;
//(2) 测试数据读写
#500 ;
rstn = 0 ;
#10 rstn = 1 ;
release test.u_data_buf.u_fifo.rden ;
repeat(60) begin
@(negedge clk_fast) ;
if (!test.u_data_buf.u_fifo.wfull) begin
din_en = 1'b1 ;
din = {$random()} % 16;
end
else begin
din_en = 1'b0 ;
end
end
//(3) 停止读取再一次测试 empyt、full、prog_full 信号
#800 ;
force test.u_data_buf.u_fifo.rden = 1'b0 ;
repeat(18) begin
@(negedge clk_fast) ;
din_en = 1'b1 ;
din = {$random()} % 16;
end
end
fifo_buf #(.DWI(4), .AWI(5), .DWO(16), .AWO(3), .PROG_DEPTH(16))
u_data_buf(
.rstn (rstn),
.din_clk (clk_fast),
.din (din),
.din_en (din_en),
.dout_clk (clk_slow),
.dout (dout),
.dout_valid (dout_valid));
`else // !`ifdef SMALL2BIG
`endif
//stop sim
initial begin
forever begin
#100;
if ($time >= 5000) $finish ;
end
end
endmodule
仿真分析
根据 testbench 中的 3 步测试激励,分析如下:
测试 (1) : FIFO 端口及一些内部信号时序结果如下。
由图可知,FIFO 内部开始写数据,空状态信号 rempty 拉低(红色 M1 ) 之前有一段时间延迟,这是写地址同步延时导致的。
由于此时没有进行读 FIFO 操作,所以 prog_full 与 full 拉高 (黄色 M2 与绿色 M3) 几乎没有延迟。
测试 (2) : FIFO 同时进行读写时,fifo 与 fifo_buf 模块的端口信号如下所示。
由图可知,数据开始传输时,fifo 模块的等位宽数据输出、fifo_buf 整合之后的数据输出,均与输入数据对应,没有传输错误。
测试 (3) :整个 FIFO 读写行为及读停止的时序仿真图如下所示。
由图可知,读写操作同时进行时,wfull 信号会不断翻转 (M4 时刻之前),这是因为此时 fifo 的使用方法是非满即写,非空即读。
M4 时刻禁止写操作后(din_en 恒为 0),由于读一致存在,所以 full 信号会拉低并保持,表示 fifo 中数据一直未满。而 prog_full 也会相继拉低 (M5 时刻),表示 FIFO 中的数据已经低于配置的数目。
当恢复写操作后 (din_en 恒为 1,对应 M6 时刻),prog_full 与 full 信号相继拉高。
-
存储器
+关注
关注
38文章
7487浏览量
163808 -
fifo
+关注
关注
3文章
388浏览量
43665 -
计数器
+关注
关注
32文章
2256浏览量
94523 -
时钟
+关注
关注
10文章
1733浏览量
131472 -
指针
+关注
关注
1文章
480浏览量
70560
发布评论请先 登录
相关推荐
评论