【Verilog学习日常】—牛客网刷题—Verilog企业真题—VL68
同步FIFO
描述
请设计带有空满信号的同步FIFO,FIFO的深度和宽度可配置。双口RAM的参考代码和接口信号已给出,请在答案中添加并例化此部分代码。
电路的接口如下图所示。端口说明如下表。
接口电路图如下:
双口RAM端口说明:
端口名 | I/O | 描述 |
wclk | input | 写数据时钟 |
wenc | input | 写使能 |
waddr | input | 写地址 |
wdata | input | 输入数据 |
rclk | input | 读数据时钟 |
renc | input | 读使能 |
raddr | input | 读地址 |
rdata | output | 输出数据 |
同步FIFO端口说明:
端口名 | I/O | 描述 |
clk | input | 时钟 |
rst_n | input | 异步复位 |
winc | input | 写使能 |
rinc | input | 读使能 |
wdata | input | 写数据 |
wfull | output | 写满信号 |
rempty | output | 读空信号 |
rdata | output | 读数据 |
参考代码如下:
module dual_port_RAM #(parameter DEPTH = 16,
parameter WIDTH = 8)(
input wclk
,input wenc
,input [$clog2(DEPTH)-1:0] waddr
,input [WIDTH-1:0] wdata
,input rclk
,input renc
,input [$clog2(DEPTH)-1:0] raddr
,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
输入描述:
input clk ,
input rst_n ,
input winc ,
input rinc ,
input wdata ,
输出描述:
output reg wfull ,
output reg rempty ,
output wire rdata
解题思路:
同步FIFO的相关知识:
主要参考以下博文:
(博客园)数字设计——同步fifo
(知乎)手写同步FIFO
(知乎)同步FIFO笔记
(CSDN)FPGA基础知识极简教程(3)从FIFO设计讲起之同步FIFO篇
FIFO(First-In-Frist-Out)是一种先进先出的数据交互方式,在数字ASIC设计中常常被使用。
FIFO与普通存储器RAM的区别是没有外部读写地址线,使用起来非常简单,但缺点就是只能顺序写入数据,顺序的读出数据,其数据地址由内部读写指针自动加1完成。不能像普通存储器那样可以由地址线决定读取或写入某个指定的地址。FIFO本质上是由RAM(或者寄存器)加读写控制逻辑构成的一种先进先出的数据缓冲器;
同步FIFO的端口
- FIFO的宽度:即FIFO一次读写操作的数据位;
- FIFO的深度:FIFO可以存储多少个N位的数据(如果宽度为N)。
- 满标志:FIFO已满或将要满时由FIFO的状态电路送出的一个信号,以阻止FIFO的写操作继续向FIFO中写数据而造成溢出(overflow)
- 空标志:FIFO已空或将要空时由FIFO的状态电路送出的一个信号,以阻止FIFO的读操作继续从FIFO中读出数据而造成无效数据的输出(underflow)
- 读时钟:读操作所遵循的时钟,在每个时钟沿来临时读数据。
- 写时钟:写操作所遵循的时钟,在每个时钟沿来临时写数据。
- 将满标志(almost full):FIFO将要满时由FIFO的状态电路送出的一个信号。
- 将空信号(almost empty):FIFO将要空时由FIFO的状态电路送出的一个信号。
FIFO指针的工作方式
根据下面一张图我们来了解FIFO读写指针的工作方式;其中WP为写指针,RP为读指针;
写指针总是指向下一个时钟要写的地址,读指针总是指向下一个时钟要读的地址。读指针等于写指针的时候可能为空,有可能为满。(读指针也可以指向当前正在读的地址,但相应的根据地址读取数据的逻辑会有所不同,前面读指针指向下一个时钟要读的地址是用时序逻辑去读,指向当前正在读的地址用组合逻辑去读。)
当FIFO没有写满并且写使能拉高时,写指针加一,当FIFO没有读空并且读使能拉高时,读指针加一;
FIFO的空满检测
可以通过在FIFO内部设立一个计数器用来计数FIFO内的数据量;
- 当FIFO没有写满且写使能拉高时(或者写指针加一时),计数器加一;
- 当FIFO没有读空且读使能拉高时(或者读指针加一时),计数器减一;
- 当FIFO既没有读空又没有写满,且读写使能同时拉高有效时,这时的计数器不加也不减。
还可以采用在读写地址前增加一位的策略。如果FIFO的深度为16,则地址需要4位二进制数来表示,那么在表示FIFO地址时用5位二进制数来表示。
当有数据进入时,写指针继续增大,当写指针为15(01111)时,继续增大来到了0地址处,这时第五位置为1(10000),继续增大;
当读指针与写指针低四位相同,最高为相反时,表示fifo已经写满。
当读指针与写指针最高位相反,低四位不同时,使用写指针的低4位减去读指针的低4位(表示当前FIFO空余的位置),在加FIFO的深度,即可表示计数器cnt的值(即FIFO队列中有多少数据)
解题:
输出信号有:写满信号(wfull)、读空信号(rempty)、读数据(rdata)
①设计读指针和写指针:
指针指向的位置即为当前读数据或写数据的地址;
//读指针和写指针部分
reg [$clog2(DEPTH):0] wpoint, rpoint; //读指针、写指针
wire wenc, renc;
assign wenc = winc && (!wfull); //当写使能为1且FIFO未满时
assign renc = rinc && (!rempty);//当读使能为1且FIFO未空时
//读指针
always @(posedge clk or negedge rst_n) begin
if (!rst_n) wpoint <= 'd0;
else begin
if (wenc) wpoint <= wpoint + 1'd1;
end
end
//写指针
always @(posedge clk or negedge rst_n) begin
if (!rst_n) rpoint <= 'd0;
else begin
if (renc) rpoint <= rpoint + 1'd1;
end
end
②计数器cnt部分设置(难点)
判断WP指针的最高位与RP指针的最高位是否完全相等;当最高位为1时,说明指针已经经过了FIFO的最后一位数并且回到了最初;
①当WP [4]= RP[4]时,说明此时FIFO队列还未写满,cnt等于写指针地址-读指针地址;
例如:
②当WP[4]!=RP[4]时,说明此时FIFO队列中写指针比读指针先循环了一个FIFO周期;
例如:,表明写指针已经经过了最后一个地址01111,返回到0010地址,并将最高位标为1;
;
当读指针与写指针的后四位相同,但是最高位相反时,说明FIFO已满;
例如:
//cnt部分
wire [$clog2(DEPTH) : 0] cnt;
assign cnt = (wpoint[$clog2(DEPTH)] == rpoint[$clog2(DEPTH)]) ?
(wpoint[$clog2(DEPTH):0] - rpoint[$clog2(DEPTH):0]) :
(DEPTH + wpoint[$clog2(DEPTH)-1:0] - rpoint[$clog2(DEPTH)-1:0]);
②判断写满信号(wfull):
当计数器cnt计数达到FIFO深度值(DEPTH)时,则wfull = 1'b1,否则wfull = 1'b0;
//判断是否写满
always @(posedge clk or negedge rst_n) begin
if (!rst_n) wfull = 1'b0;
else begin
if (cnt == DEPTH) wfull = 1'b1;
else wfull = 1'b0;
end
end
③判断读空信号(rempty):
当计数器cnt计数为0时rempty = 1’b1,否则rempty = 1’b0;
//判断当前是否为空
always @(posedge clk or negedge rst_n) begin
if (!rst_n) rempty = 1'b0;
else begin
if (cnt == 4'd0) rempty = 1'b1;
else rempty = 1'b0;
end
end
④RAM例化部分
//例化RAM
dual_port_RAM #(.DEPTH(DEPTH),
.WIDTH (WIDTH))
RR(
.wclk(clk), //写数据时钟
.wenc(wenc), //写使能
.waddr(wpoint[$clog2(DEPTH)-1:0]),//写地址
.wdata(wdata), //输入数据
.rclk(clk), //读数据时钟
.renc(renc), //读使能
.raddr(rpoint[$clog2(DEPTH)-1:0]), //读地址
.rdata(rdata) //输出数据
);
代码如下
/**********************************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 //读数据
);
//读指针和写指针部分
reg [$clog2(DEPTH):0] wpoint, rpoint; //读指针、写指针
wire wenc, renc;
assign wenc = winc && (!wfull); //当写使能为1且FIFO未满时
assign renc = rinc && (!rempty);//当读使能为1且FIFO未空时
//读指针
always @(posedge clk or negedge rst_n) begin
if (!rst_n) wpoint <= 'd0;
else begin
if (wenc) wpoint <= wpoint + 1'd1;
end
end
//写指针
always @(posedge clk or negedge rst_n) begin
if (!rst_n) rpoint <= 'd0;
else begin
if (renc) rpoint <= rpoint + 1'd1;
end
end
//cnt部分
wire [$clog2(DEPTH) : 0] cnt;
assign cnt = (wpoint[$clog2(DEPTH)] == rpoint[$clog2(DEPTH)]) ?
(wpoint[$clog2(DEPTH):0] - rpoint[$clog2(DEPTH):0]) :
(DEPTH + wpoint[$clog2(DEPTH)-1:0] - rpoint[$clog2(DEPTH)-1:0]);
//判断是否写满
always @(posedge clk or negedge rst_n) begin
if (!rst_n) wfull = 1'b0;
else begin
if (cnt == DEPTH) wfull = 1'b1;
else wfull = 1'b0;
end
end
//判断当前是否为空
always @(posedge clk or negedge rst_n) begin
if (!rst_n) rempty = 1'b0;
else begin
if (cnt == 4'd0) rempty = 1'b1;
else rempty = 1'b0;
end
end
//例化RAM
dual_port_RAM #(.DEPTH(DEPTH),
.WIDTH (WIDTH))
RR(
.wclk(clk), //写数据时钟
.wenc(wenc), //写使能
.waddr(wpoint[$clog2(DEPTH)-1:0]),//写地址
.wdata(wdata), //输入数据
.rclk(clk), //读数据时钟
.renc(renc), //读使能
.raddr(rpoint[$clog2(DEPTH)-1:0]), //读地址
.rdata(rdata) //输出数据
);