用verilog/systemverilog 设计fifo (1)
fifo的基本原理
FIFO(first in first out),即先进先出存储器,功能与数据结构中的队列相似。
在IC设计中,FIFO常用来缓冲突发数据,流式数据与块数据的转换等等。
比如上图中,在两个block之间,通过输入命令fifo来缓存block1的输入请求命令。
基于计数器的同步fifo实现(1)
在这种fifo实现方法中,我们用读写计数(或者说读写指针)来实现fifo的读写。
- 初始读计数
rd_cnt=0
,写计数wr_cnt=0
,fifo中数据计数为:data_cnt=wr_cnt-rd_cnt=0
。 - 写入四个数据,每写入一个数据时,
ram[wr_cnt]<=din;wr_cnt <= wr_cnt + 1
,所以写入四个数据后,wr_cnt=4
, fifo中数据计数为:data_cnt=wr_cnt-rd_cnt=4
。 - 假设地址位为
3
位,3
位地址最大能表示深度为8
的fifo,但是在这种实现fifo方法中,fifo容量只能是7
或者7
以下的值。因为wr_cnt
为7
时,如果再写一个数据,wr_cnt + 1= 3'b111 + 1'b1 = 3'000
,如果此时rd_cnt=0
,则fifo中数据量为wr_cnt-rd_cnt=0
,但实际上我们写了8
个数据,且没有读取1
个数据。这个时候会发生fifo溢出。 - 通过限制fifo 容量为
7
或以下的值,可以防止fifo溢出,比如fifo容量为7
,当ram[6]
被写以后,wr_cnt=7,rd_cnt=0,data_cnt=wr_cnt-rd_cnt=7
,此时fifo full标志会被置位,不能继续写fifo。 - 接着两个读fifo,
rd_cnt=2
,这个时候fifo full又被清零,可以继续写fifo,写入1个数据后, wr_cnt=0, 这个时候fifo中数据data_cnt = wr_cnt-rd_cnt=3'b000-3'b010=3'110=6
,无符号数减法,高位借位。
这种基于计数器的fifo实现方法中,假设fifo地址位AW=n
,则fifo容量为0<CAPACITY<2^n
,下面是实现的verilog代码。
文件名称:code4_40.v
`timescale 1ns/1ps
module SyncFifo_tb;
logic clk = 1'b0;
logic rst_n = 1'b0;
initial begin
$display("start a clock pulse");
$dumpfile("sync_fifo_1.vcd");
$dumpvars(0, SyncFifo_tb);
#300 $finish;
end
always begin
#5 clk = ~clk;
end
logic [7:0] din='0,dout;
logic wr='0,rd='0;
logic [2:0] wc,rc,dc;
logic fu, em;
initial begin
//write 10 data
repeat(10) begin
@ (negedge clk) begin
rst_n = 1'b1;
wr <= 1'b1;
rd <= 1'b0;
din <= 8'($random());
end
end
//重复读取5次
repeat(5) begin
@(negedge clk) begin
wr <= 1'b0;
rd <= 1'b1;
end
end
//再写2次
repeat(2) begin
@(negedge clk) begin
wr <= 1'b1;
rd <= 1'b0;
din <= $random();
end
end
//重复读取4次,读空
repeat(4) begin
@(negedge clk) begin
wr <= 1'b0;
rd <= 1'b1;
end
end
//空4个周期
repeat(4) begin
@(negedge clk) begin
wr <= 1'b0;
rd <= 1'b0;
end
end
//写一个,读一个
forever begin
@(negedge clk) begin
wr <= 1'b1;
rd <= 1'b1;
din <= $random();
end
end
end
ScFifo #(.DW(8),.AW(3),.CAPACITY(7)) the_fifo(.clk(clk),.rst_n(rst_n),.din(din),.write(wr),
.read(rd),.dout(dout),.wr_cnt(wc),.rd_cnt(rc),
.data_cnt(dc),.full(fu),.empty(em));
endmodule
//DW是data width, AW是地址宽度
//CAPACITY是fifo容量,要求CAPACITY>=1 and CAPACITY<=2**AW-1
module ScFifo #(parameter DW=8, AW=10, CAPACITY=2**AW-1) (
input wire clk,
input wire rst_n,
input wire [DW-1:0] din,
input wire write,
input wire read,
output logic [DW-1:0] dout,
output logic [AW-1:0] wr_cnt='0,
output logic [AW-1:0] rd_cnt='0,
output logic [AW-1:0] data_cnt,
output logic full,empty
);
if(CAPACITY>2**AW-1) begin
$error("CAPACITY must be less than 2**AW-1");
end
if(CAPACITY<1) begin
$error("CAPACITY must be greater than 0");
end
logic [DW-1:0] ram[2**AW];
always_ff @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
wr_cnt <= '0;
end
else begin
if(write && !full) wr_cnt <= wr_cnt + 1'b1;
end
end
always_ff @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
rd_cnt <= '0;
end
else begin
if(read && !empty) rd_cnt <= rd_cnt + 1'b1;
end
end
assign data_cnt = wr_cnt - rd_cnt;
assign full =(data_cnt==CAPACITY);
assign empty = (data_cnt==0);
always_ff @(posedge clk) begin
if(write && !full) ram[wr_cnt] <= din;
end
always_ff @(posedge clk) begin
if(read && !empty) dout <= ram[rd_cnt];
end
endmodule
在vscode中,使用下面命令编译,运行代码,然后用gtkwave 打开波形文件:
iverilog -o myrun -g 2012 -s TestMem code4_40.v
vvp myrun
gtkwave sync_fifo_1.vcd
- 在第2个时钟上升沿开始写fifo,输入10个数据,但在第8时钟上升沿时,fu信号已经被置位,所以只写入7个数据。
- 在第12时钟上升沿开始,从fifo中读取5个数据,此时fifo剩下两个数据。
- 在第17个时钟上升沿,再写入两个数据。
- 在第19个时钟上升沿,连续读取4个数据,在第22个时钟上升沿,fifo为空,em信号被置位。
- 在第27时钟上升沿,开始写一个数据,读一个数据。
我们尝试设置fifo容量为8,地址宽度为3,在vscode用iverlog仿真,发现并没有弹出异常,竟然真的用容量8做了仿真,得到了错误的fifo结果。
ScFifo #(.DW(8),.AW(3),.CAPACITY(8)) the_fifo(.clk(clk),.rst_n(rst_n),.din(din),.write(wr),
.read(rd),.dout(dout),.wr_cnt(wc),.rd_cnt(rc),
.data_cnt(dc),.full(fu),.empty(em));
这是因为iverilog并不支持$error,我们可以在modelsim中建立工程(使用Modelsim进行简单仿真),仿真上面的代码,可以得到下面的结果:
vsim -voptargs=+acc work.TestMem
# vsim -voptargs="+acc" work.TestMem
# Start time: 09:05:42 on Jun 14,2024
# ** Note: (vsim-3812) Design is being optimized...
# ** Error: CAPACITY must be less than 2**AW-1
# Scope: TestMem.the_fifo.genblk1 File: D:\xxx\verilog\modelsim\sync_fifo.sv Line: 18
# Optimization failed
# ** Note: (vsim-12126) Error and warning message counts have been restored: Errors=1, Warnings=0.
# Error loading design
# End time: 09:05:43 on Jun 14,2024, Elapsed time: 0:00:01
# Errors: 1, Warnings: 6
基于计数器的同步fifo实现(2)
在前面第一种同步fifo实现中,fifo地址宽度AW是固定的,它决定了fifo的深度。如果我们要实现指定fifo深度,而不需要指定fifo读写地址宽度,可以用下面的实现方法。
指定fifo深度
parameter FIFO_DEPTH=16
fifo地址宽度为
logic [$clog2(FIFO_DEPTH)-1:0] wr_addr, rd_addr;
我们用一个变量指定fifo中的数据数目
output logic [$clog2(FIFO_DEPTH):0] fifo_cnt
注意fifo_cnt的位宽是$clog2(FIFO_DEPTH):0
,比如FIFO_DEPTH=16,则是output logic [4:0] fifo_cnt
,如果FIFO_DEPTH=15到9都是output logic [4:0] fifo_cnt
,因为$clog2(n)
函数是向上取整。
这样,多一位可以保证fifo_cnt
不会溢出。
实现的verilog代码文件名称code4_37.v
`timescale 1ns/1ps
module sync_fifo_tb;
logic clk=0;
logic rst_n;
logic wr_en,rd_en;
logic [7:0] data_in;
logic [7:0] data_out;
logic full,empty;
//fifo cnt 位数是fifo深度$clog2(fifo_depth)+1
//保证fifo_cnt不会溢出(如果fifo_depth不为2的幂次,其实不会溢出,因为$clog2函数向上取整)
logic [3:0] fifo_cnt;
always #5 clk = ~clk;
initial begin
$display("start a clock pulse");
$dumpfile("sync_fifo_2.vcd");
$dumpvars(0, sync_fifo_tb);
#300 $finish;
end
initial begin
rst_n=0;
//在时钟下降沿改变数据
//重复10次,将会full
repeat(10) begin
@(negedge clk) begin
rst_n <= 1'b1;
wr_en <= 1'b1;
rd_en <= 1'b0;
data_in <= $random();
end
end
//重复读取6次
repeat(6) begin
@(negedge clk) begin
wr_en <= 1'b0;
rd_en <= 1'b1;
end
end
//再写2次
repeat(2) begin
@(negedge clk) begin
wr_en <= 1'b1;
rd_en <= 1'b0;
data_in <= $random();
end
end
//重复读取4次,读空
repeat(4) begin
@(negedge clk) begin
wr_en <= 1'b0;
rd_en <= 1'b1;
end
end
//空4个周期
repeat(4) begin
@(negedge clk) begin
wr_en<= 1'b0;
rd_en <= 1'b0;
end
end
//写一个,读一个
forever begin
@(negedge clk) begin
wr_en <= 1'b1;
rd_en <= 1'b1;
data_in <= $random();
end
end
end
sync_fifo_cnt #(.DATA_WIDTH(8),.FIFO_DEPTH(8)) the_sync_fifo_cnt
(
.clk(clk),
.rst_n(rst_n),
.wr_en(wr_en),
.rd_en(rd_en),
.data_in(data_in),
.data_out(data_out),
.full(full),
.empty(empty),
.fifo_cnt(fifo_cnt)
);
endmodule
module sync_fifo_cnt
#(
parameter DATA_WIDTH=8,
parameter FIFO_DEPTH=16
)
(
input wire clk,
input wire rst_n,
input wire wr_en,
input wire rd_en,
input wire [DATA_WIDTH-1:0] data_in,
output logic [DATA_WIDTH-1:0] data_out,
output logic full,
output logic empty,
output logic [$clog2(FIFO_DEPTH):0] fifo_cnt
);
logic [$clog2(FIFO_DEPTH)-1:0] wr_addr, rd_addr;
logic [DATA_WIDTH-1:0] fifo_buffer[FIFO_DEPTH];
//写地址时序逻辑
always_ff @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
wr_addr <= '0;
end
else if(!full && wr_en ) begin
//先读后写模式,所以写入的是加1前的wr_addr
wr_addr <= wr_addr + 1'b1;
fifo_buffer[wr_addr] <= data_in;
end
end
//读时序逻辑
always_ff @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
rd_addr <= '0;
end
else if(!empty && rd_en) begin
rd_addr <= rd_addr + 1;
data_out <= fifo_buffer[rd_addr];
end
end
//更新计数器
always_ff @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
fifo_cnt <=0;
end
else begin
case ({wr_en,rd_en})
2'b00: fifo_cnt <= fifo_cnt;
2'b01:
if(fifo_cnt!=0)
fifo_cnt <= fifo_cnt-1'b1;
2'b10:
if(fifo_cnt!=FIFO_DEPTH)
fifo_cnt <= fifo_cnt + 1'b1;
2'b11:
begin
if(empty)
//有写没有读
fifo_cnt <= fifo_cnt + 1'b1;
else if(full)
//有读没有写
fifo_cnt <= fifo_cnt - 1'b1;
else
fifo_cnt <= fifo_cnt;
end
endcase
end
end
assign full = (fifo_cnt == FIFO_DEPTH) ? 1'b1 : 1'b0;
assign empty = (fifo_cnt == 0)? 1'b1 : 1'b0;
endmodule
在vscode中,使用下面命令编译,运行代码,然后用gtkwave 打开波形文件:
iverilog -o myrun -g 2012 -s TestMem code4_37.v
vvp myrun
gtkwave sync_fifo_2.vcd
- 第2个时钟上升沿开始写fifo
- 第9个时钟上升沿fifo写满,共写了8个数组,full信号置位
- 第12个时钟上升沿开始,连续读6个数据
- 第18个时钟上升沿开始,再写两个数据
- 第20个时钟上升沿开始,读取4个数据,fifo读空,信号empty置位
- 第28个时钟上升沿开始,写一个数据,读一个数据
基于高位补偿法的fifo实现
前面两种同步fifo实现方法中,读写地址位数都是$clog2(DEPTH)
,这样可能会出现fifo地址溢出的问题,我们可以增加一位,这样rd_addr/wr_addr都是$clog2(DEPTH)+1
位,我们用最高位msb来作为指示位。
- 读写地址高位相同,其它位也相同,则fifo为空。
- 读写地址高位不同,其它位相同,则fifo为满。
verilog实现代码文件为:
code4_41.v
`timescale 1ns/1ps
module sync_fifo_tb;
logic clk=0;
logic rst_n;
logic wr_en,rd_en;
logic [7:0] data_in;
logic [7:0] data_out;
logic full,empty;
//fifo cnt 位数是fifo深度$clog2(fifo_depth)+1
//保证fifo_cnt不会溢出(如果fifo_depth不为2的幂次,其实不会溢出,因为$clog2函数向上取整)
logic [3:0] fifo_cnt;
always #5 clk = ~clk;
initial begin
$display("start a clock pulse");
$dumpfile("sync_fifo_3.vcd");
$dumpvars(0, sync_fifo_tb);
#300 $finish;
end
initial begin
rst_n=0;
//在时钟下降沿改变数据
//重复10次,将会full
repeat(10) begin
@(negedge clk) begin
rst_n <= 1'b1;
wr_en <= 1'b1;
rd_en <= 1'b0;
data_in <= $random();
end
end
//重复读取6次
repeat(6) begin
@(negedge clk) begin
wr_en <= 1'b0;
rd_en <= 1'b1;
end
end
//再写2次
repeat(2) begin
@(negedge clk) begin
wr_en <= 1'b1;
rd_en <= 1'b0;
data_in <= $random();
end
end
//重复读取4次,读空
repeat(4) begin
@(negedge clk) begin
wr_en <= 1'b0;
rd_en <= 1'b1;
end
end
//空4个周期
repeat(4) begin
@(negedge clk) begin
wr_en<= 1'b0;
rd_en <= 1'b0;
end
end
//写一个,读一个
forever begin
@(negedge clk) begin
wr_en <= 1'b1;
rd_en <= 1'b1;
data_in <= $random();
end
end
end
sync_fifo_hibit_ext #(.DATA_WIDTH(8),.FIFO_DEPTH(8)) the_sync_fifo_ext
(
.clk(clk),
.rst_n(rst_n),
.wr_en(wr_en),
.rd_en(rd_en),
.data_in(data_in),
.data_out(data_out),
.full(full),
.empty(empty)
);
endmodule
//高位扩展法,读写地址增加一位,用高位表示满或空
module sync_fifo_hibit_ext
#(
parameter DATA_WIDTH=8,
parameter FIFO_DEPTH=16
)
(
input wire clk,
input wire rst_n,
input wire wr_en,
input wire rd_en,
input wire [DATA_WIDTH-1:0] data_in,
output logic [DATA_WIDTH-1:0] data_out,
output logic full,
output logic empty
);
logic [$clog2(FIFO_DEPTH):0] wr_addr, rd_addr;
logic [DATA_WIDTH-1:0] fifo_buffer[FIFO_DEPTH];
//低位地址,和深度匹配
logic [$clog2(FIFO_DEPTH)-1:0] wr_addr_true, rd_addr_true;
logic msb_wr_addr, msb_rd_addr;
//连续赋值得到高位地址和低位地址
assign {msb_wr_addr,wr_addr_true} = wr_addr;
assign {msb_rd_addr,rd_addr_true} = rd_addr;
//写地址时序逻辑
always_ff @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
wr_addr <= '0;
end
else if(!full && wr_en ) begin
//先读后写模式,所以写入的是加1前的wr_addr
wr_addr <= wr_addr + 1'b1;
fifo_buffer[wr_addr_true] <= data_in;
end
end
//读时序逻辑
always_ff @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
rd_addr <= '0;
end
else if(!empty && rd_en) begin
rd_addr <= rd_addr + 1;
data_out <= fifo_buffer[rd_addr_true];
end
end
//高位相同,且读地址=写地址,fifo空
assign empty = (rd_addr == wr_addr) ? 1'b1 : 1'b0;
//高位不同,读地址等于写地址,fifo满
assign full = ((msb_rd_addr!=msb_wr_addr) && (rd_addr_true==wr_addr_true))? 1'b1 : 1'b0;
endmodule
在vscode中,使用下面命令编译,运行代码,然后用gtkwave 打开波形文件:
iverilog -o myrun -g 2012 -s TestMem code4_41.v
vvp myrun
gtkwave sync_fifo_3.vcd
我们可以得到和第二种实现方法一样的波形。