1.简介
FIFO,先进先出存储,没有地址。何为异步?即读写为不同的时钟。
我们在FPGA的设计中经常会使用到FIFO,一般的做法都是直接调用IP核,可为什么要动手亲自写代码呢?1.当我们更换芯片厂家时,IP是不可移植的,这时候就需要重新改代码,工作量大.;2.有的面试可能会让你手撕代码。以上个人见解。
2.同步FIFO
FIFO的读写均由同一个时钟控制,实现如下(高位判别法)
module syn_fifo #(
parameter width = 8,
parameter depth = 64)(
input clk ,
input rstn ,
input wr_en ,
input rd_en ,
input [width - 1 : 0] wr_data ,
output reg [width - 1 : 0] rd_data ,
output fifo_full ,
output fifo_empty
);
localparam addr_width = $clog2(depth);
//定义读写指针
reg [addr_width : 0] wr_ptr;
reg [addr_width : 0] rd_ptr;
//定义读写地址
wire [addr_width - 1 : 0] wr_addr;
wire [addr_width - 1 : 0] rd_addr;
//定义存储
reg [width - 1 : 0] fifo [depth - 1 : 0];
assign wr_addr = wr_ptr[addr_width - 1:0];
assign rd_addr = rd_ptr[addr_width - 1:0];
//写地址操作
always @ (posedge clk or negedge rstn) begin
if(!rstn)
wr_ptr <= 0;
else if(wr_en && !fifo_full) //写使能,且fifo未写满
wr_ptr <= wr_ptr + 1;
else
wr_ptr <= wr_ptr;
end
//读地址操作
always @ (posedge clk or negedge rstn) begin
if(!rstn)
rd_ptr <= 0;
else if(rd_en && !fifo_empty) //读使能,且fifo不为空
rd_ptr <= rd_ptr + 1;
else
rd_ptr <= rd_ptr;
end
//写数据
integer i;
always @ (posedge clk or negedge rstn) begin
if(!rstn) begin
for(i = 0; i < depth; i = i + 1)
fifo[i] <= 0;
end
else if(wr_en)
fifo[wr_addr] <= wr_data;
end
//读数据
always @ (posedge clk or negedge rstn) begin
if(!rstn)
rd_data <= 0;
else if (rd_en)
rd_data <= fifo[rd_addr];
else
rd_data <= rd_data;
end
//空满判断
assign fifo_empty = ( wr_ptr == rd_ptr ) ? 1'b1 : 1'b0;
assign fifo_full = ( rd_ptr == {!wr_ptr[addr_width],wr_ptr[addr_width-1:0]})? 1'b1 : 1'b0;
endmodule
仿真结果分析:初始设置FIFO深度为64,tb仿真写入了66个数据。由于代码设定fifo写满后wr_ptr就不再增加了,因此重复向0地址写入数据。当开始读出数据时,0地址读出65。
tb代码:
module tb_fifo();
reg clk;
reg rstn;
reg wr_en;
reg rd_en;
reg [7:0]wr_data;
wire [7:0]rd_data;
wire fifo_full,fifo_empty;
initial begin
clk=1;
rstn=0;
wr_en=0;
rd_en=0;
wr_data='b0;
#100
rstn = 1;
@(posedge clk);
wr();
@(posedge clk);
rd_en = 1;
#100
rd_en = 0;
end
integer i;
task wr();
begin
for(i = 0; i < 66 ;i = i + 1)begin
wr_en <= 1;
wr_data <= i;
@(posedge clk);
end
wr_en <= 0;
end
endtask
always #10 clk =~clk;
syn_fifo #(
.width(8),
.depth(64))
syn_fifo_u(
.clk (clk),
.rstn (rstn),
.wr_en (wr_en),
.rd_en (rd_en),
.wr_data (wr_data),
.rd_data (rd_data),
.fifo_full (fifo_full),
.fifo_empty (fifo_empty)
);
endmodule
3.异步FIFO原理
首先奉上大牛文章:http://www.sunburst-design.com/papers/CummingsSNUG2002SJ_FIFO1.pdf 原理和方法都讲得很清楚。
异步FIFO整体架构
观察FIFO结构,主要由以下几部分组成:
1.双口RAM
2.读地址(指针)控制
3.写地址(指针)控制
4.跨时钟域处理
异步FIFO中主要技术
1.读空与写满
先做个比喻,异步FIFO的读写你可以想象成两个人在操场上跑,写在前面跑,读在后面追。因为操场是圆的,所以有两种情况发生:1.读追上了写,两个人跑了一样的距离,表示读空;2. 写追上了读,写多跑了一圈,表示写满。
读空很好判断,就是两个地址一样就表示读空,可是写满呢?很简单,就是地址多加一位,用最高位表示是否多跑了一圈。如下图:
写满标志就是读写地址最高位不同,其余位相同。
2.地址指针使用格雷码
地址指针使用格雷码的原因主要是因为要进行跨时钟域的处理。因为读写指针是不同的时钟域,跨时钟域传递会出现亚稳态现象,但是因为相邻的格雷码只改变一位,可以降低亚稳态的风险。
3.Verilog实现
这个代码是我在牛客刷题碰到的,写的很不错,推荐!
`timescale 1ns/1ns
/***************************************RAM*****************************************/
module dual_port_RAM #(parameter DEPTH = 16,
parameter WIDTH = 8)(
input wclk
,input wenc
,input [$clog2(DEPTH)-1:0] waddr //深度对2取对数,得到地址的位宽。
,input [WIDTH-1:0] wdata //数据写入
,input rclk
,input renc
,input [$clog2(DEPTH)-1:0] raddr //深度对2取对数,得到地址的位宽。
,output reg [WIDTH-1:0] rdata //数据输出
);
reg [WIDTH-1:0] RAM_MEM [0:DEPTH-1];
always @(posedge wclk) begin
if(wenc)
RAM_MEM[waddr] <= wdata;
end
always @(posedge rclk) begin
if(renc)
rdata <= RAM_MEM[raddr];
end
endmodule
/***************************************AFIFO*****************************************/
module asyn_fifo#(
parameter WIDTH = 8,
parameter DEPTH = 16
)(
input wclk ,
input rclk ,
input wrstn ,
input rrstn ,
input winc ,
input rinc ,
input [WIDTH-1:0] wdata ,
output wire wfull ,
output wire rempty ,
output wire [WIDTH-1:0] rdata
);
parameter ADDR_WIDTH = $clog2(DEPTH);
/**********************addr bin gen*************************/
reg [ADDR_WIDTH:0] waddr_bin;
reg [ADDR_WIDTH:0] raddr_bin;
always @(posedge wclk or negedge wrstn) begin //写判断
if(~wrstn) begin
waddr_bin <= 'd0;
end
else if(!wfull && winc)begin
waddr_bin <= waddr_bin + 1'd1;
end
end
always @(posedge rclk or negedge rrstn) begin //读判断
if(~rrstn) begin
raddr_bin <= 'd0;
end
else if(!rempty && rinc)begin
raddr_bin <= raddr_bin + 1'd1;
end
end
/**********************addr gray gen*************************/
wire [ADDR_WIDTH:0] waddr_gray;
wire [ADDR_WIDTH:0] raddr_gray;
reg [ADDR_WIDTH:0] wptr;
reg [ADDR_WIDTH:0] rptr;
assign waddr_gray = waddr_bin ^ (waddr_bin>>1); //转格雷码
assign raddr_gray = raddr_bin ^ (raddr_bin>>1);
always @(posedge wclk or negedge wrstn) begin
if(~wrstn) begin
wptr <= 'd0;
end
else begin
wptr <= waddr_gray;
end
end
always @(posedge rclk or negedge rrstn) begin
if(~rrstn) begin
rptr <= 'd0;
end
else begin
rptr <= raddr_gray;
end
end
/**********************syn addr gray*************************/
reg [ADDR_WIDTH:0] wptr_buff;
reg [ADDR_WIDTH:0] wptr_syn;
reg [ADDR_WIDTH:0] rptr_buff;
reg [ADDR_WIDTH:0] rptr_syn;
always @(posedge wclk or negedge wrstn) begin //打拍做跨时钟域处理
if(~wrstn) begin
rptr_buff <= 'd0;
rptr_syn <= 'd0;
end
else begin
rptr_buff <= rptr;
rptr_syn <= rptr_buff;
end
end
always @(posedge rclk or negedge rrstn) begin
if(~rrstn) begin
wptr_buff <= 'd0;
wptr_syn <= 'd0;
end
else begin
wptr_buff <= wptr;
wptr_syn <= wptr_buff;
end
end
/**********************full empty gen*************************/
assign wfull = (wptr == {~rptr_syn[ADDR_WIDTH:ADDR_WIDTH-1],rptr_syn[ADDR_WIDTH-2:0]}); //写满标志判断
assign rempty = (rptr == wptr_syn); //读空标志判断
/**********************RAM*************************/
wire wen ;
wire ren ;
wire wren;//high write
wire [ADDR_WIDTH-1:0] waddr;
wire [ADDR_WIDTH-1:0] raddr;
assign wen = winc & !wfull;
assign ren = rinc & !rempty;
assign waddr = waddr_bin[ADDR_WIDTH-1:0];
assign raddr = raddr_bin[ADDR_WIDTH-1:0];
dual_port_RAM #(.DEPTH(DEPTH),
.WIDTH(WIDTH)
)dual_port_RAM(
.wclk (wclk),
.wenc (wen),
.waddr(waddr), //深度对2取对数,得到地址的位宽。
.wdata(wdata), //数据写入
.rclk (rclk),
.renc (ren),
.raddr(raddr), //深度对2取对数,得到地址的位宽。
.rdata(rdata) //数据输出
);
endmodule