FIFO设计
首先推荐一篇文献,Sunburst Design大佬的杰作,其在IC行业做出了很多漂亮的工作。
Simulation and Synthesis Techniques for Asynchronous FIFO Design
同步 FIFO 指针
同步 FIFO 设计(写入和读取 FIFO 缓冲区在同一时钟域中进行的 FIFO),一种实现计算写入和读取 FIFO 缓冲区的次数以递增(在 FIFO 写入时,但没有读取)、递减(在 FIFO 读取但不写入)或保持(不写入和读取,或同时写入和读取操作)FIFO 缓冲区的当前填充值。
当 FIFO 计数器达到预定的满值时,FIFO 是满的,而当 FIFO 计数器为零时,FIFO 是空的。不幸的是,对于异步 FIFO 设计,不能使用递增递减 FIFO 填充计数器,因为需要两个不同的异步时钟来控制计数器。要确定异步 FIFO 设计的满和空状态,必须比较写和读指针。
异步 FIFO 指针
写指针总是指向下一个要写的字;因此复位时,两个指针都设置为零,这也恰好是要写入的下一个 FIFO 字位置。在 FIFO 写入操作中,写入指针指向的内存位置 被写入,然后写入指针递增以指向下一个要写入的位置。
读指针始终指向要读取的当前 FIFO 字。再次复位时,两个指针都复位为零,FIFO 为空且读指针指向无效数据(因为 FIFO 为空且空标志置位)。第一个数据字写入 FIFO 后,写指针递增,清空标志,仍在寻址第一个 FIFO 存储器字内容的读指针立即将第一个有效字驱动到 FIFO数据输出端口,由接收逻辑读取。读取指针始终指向下一个要读取的 FIFO 字,这一事实意味着接收器逻辑不必使用两个时钟周期来读取数据字。如果接收器在读取FIFO数据字之前首先必须递增读指针,则接收器将计时一次以从FIFO输出数据字,并计时第二次以将数据字捕获到接收器中。这将是不必要的低效。
当读指针和写指针都相等时,FIFO为空。当两个指针在重置操作期间被重置为零时,或者当读指针在从FIFO读取最后一个字后赶上写指针时就为空。
当指针再次相等时,即当写指针绕回并赶上读指针时,FIFO是满的。这是个问题。当指针相等时,FIFO要么为空,要么为满,但具体是哪一个?
一种区分满和空的设计方法是向每个指针添加额外的位。当写指针递增超过最终FIFO地址时,写指针将递增未使用的MSB(二进制数的最左侧),同时将其余位回零,如图所示(FIFO已包裹并切换指针MSB)。
读指针也是这样。如果两个指针的MSB不同,则意味着写指针比读指针又绕回了一次。如果两个指针的MSB相同,则意味着两个指针的回绕次数相同。
使用n位指针,其中(n-1)是访问整个FIFO存储器缓冲区所需的地址位数,当两个指针(包括MSB)相等时,FIFO为空。除MSB外,当两个指针相同时,FIFO为满。本文中的FIFO设计对具有2(n-1)个可写位置的FIFO使用n位指针,以帮助处理满和空情况。
VL46 同步FIFO
根据题目提供的双口RAM代码和接口描述,实现同步FIFO,要求FIFO位宽和深度参数化可配置。电路的接口如下图所示。
注意:
在例化双口 RAM 的时候,读写使能 renc/wenc 信号如果只是传入 rinc 和 winc 也能通过题目的波形测试,但是仔细思考其实不对,此时如果是满的情况下有外部的写使能 winc,虽然写地址不变,但是对于 RAM 来讲写使能还是有效的,会覆盖数据,所以这里的 RAM 写使能信号 wenc = winc && (~wfull),RAM 读使能 renc = rinc && (~rempty),这样才能保证不会在空满情况下对RAM的误操作。
`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
/**********************************SFIFO************************************/
module sfifo#(
parameter WIDTH = 8,
parameter DEPTH = 16
)(
input clk ,
input rst_n ,
input winc ,
input rinc ,
input [WIDTH-1:0] wdata ,
output reg wfull ,
output reg rempty,
output wire [WIDTH-1:0] rdata
);
localparam addr_width = $clog2(DEPTH);
reg [addr_width:0] waddr;
reg [addr_width:0] raddr;
//写(对于地址的操作)
always @ (posedge clk or negedge rst_n) begin
if(~rst_n) begin
waddr <= 'b0;
end
else begin
if(winc && ~wfull) begin
waddr <= waddr +1'b1;
end
else begin
waddr <= waddr;
end
end
end
//读(对于地址的操作)
always @ (posedge clk or negedge rst_n) begin
if(~rst_n) begin
raddr <= 'b0;
end
else begin
if(rinc && ~rempty) begin
raddr <= raddr +1'b1;
end
else begin
raddr <= raddr;
end
end
end
//设定空和满两种状态
always @ (posedge clk or negedge rst_n) begin
if(~rst_n) begin
wfull <= 1'b0;
rempty <= 1'b0;
end
else begin
wfull <= (raddr == {~waddr[addr_width],waddr[addr_width-1:0]});
rempty <= (raddr == waddr);
end
end
//模块的调用与例化
dual_port_RAM
#(.DEPTH(DEPTH),
.WIDTH(WIDTH)
)
dual_port_RAM_U0
(
.wclk(clk),
.wenc(winc && ~wfull),
.waddr(waddr[addr_width-1:0]),
.wdata(wdata),
.rclk(clk),
.renc(rinc && ~rempty),
.raddr(raddr[addr_width-1:0]),//fifo的addr比RAM的addr多一位,最高位是空和满指示位。
.rdata(rdata)
);
endmodule
代码的最后一块是模块的例化与端口的连接。 接下来借用HDLbits的习题进行介绍。
例1:单模块连接
一般都使用通过端口名字的一一对应来实现连接,通过该方法就不需要考虑模块端口的位置变化。
module top_module ( input a, input b, output out );
mod_a ins2 (.in1(a),.in2(b),.out(out));
例2:多模块连接
module top_module ( input clk, input d, output q );
wire qd1,qd2;
my_dff ins1 (
.clk(clk),
.d(d),
.q(qd1)
);
my_dff ins2 (
.clk(clk),
.d(qd1),
.q(qd2)
);
my_dff ins3 (
.clk(clk),z
.d(qd2),
.q(q)
);
endmodule