FIFO设计(Verilog)

版权声明:本文为CSDN博主「黄铚聪」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/huangzhicong3/article/details/108317910

为了应付找工作的需要,打算学习一些fifo相关的内容,首先是从fifo的设计开始:

fifo的种类
通常,可分为同步fifo和异步fifo,但是实际上我更倾向于称为共时钟fifo和分时钟fifo(xilinx的叫法:common clock FIFO and indpendent clock FIFO),下面的图片是xilinx的《FIFO Generator》文档中的FIFO框图


共时钟FIFO
从上面的框图可以看到,FIFO的组成部分主要为三块:存储模块、指针计数器、判断逻辑。其中,存储模块主要用于数据的暂时存储,深度可配置,实现方式为RAM;指针计数器主要用于计算当前的存储模块读写指针的位置;而判断逻辑则主要是为了比较读写指针,判断当前FIFO的状态(满?空?半满?半空?等)

存储模块的实现
存储模块的实现可以用双口RAM资源,在仿真的时候直接使用reg模拟其行为即可;同时,需要实现RAM的深度可配置,因此要加入一个parameter控制深度。
在实现这个模块的过程中,要考虑清楚深度和地址位宽的关系,因为不同的深度对应的地址位宽也不同:

因此,双口ram的实现代码如下:
module ram #(
parameter depth = 256,
parameter width = 8
)
(
input wr_clk,
input wr_en,
input [$clog2(depth)-1:0] wr_addr,
input [width-1:0] wr_data,

input rd_clk,
input rd_en,
input [$clog2(depth)-1:0] rd_addr,
output reg [width-1:0] rd_data
);

reg [width-1:0] mem [0:depth-1];

initial begin:init
integer i;
for (i=0; i mem[i] <= {width{1'b0}};
end

always @(posedge wr_clk) begin
if (wr_en)
mem[wr_addr] <= wr_data;
end

always @(posedge rd_clk) begin
if (rd_en)
rd_data <= mem[rd_addr];
end

endmodule

指针计数器和空满逻辑
这个模块的主要功能是计算FIFO当前读写的地址,因为FIFO是在使用的时候是不考虑地址的,因此:
每往FIFO写入一个数据,内部的写地址加1,读地址不变,但是当写地址再次等于读地址时(写完一圈),此时FIFO已满,不能再写了;
每往FIFO读出一个数据,内部的读地址加1,写地址不变,但是当读地址等于写地址时,此时FIFO已空,不能再读了;
在实现的时候有一种巧妙的方法,让读写地址的位宽增加一位最高位作为奇偶标记,便于空满的判断。

完整模块代码

module common_fifo#
(
parameter depth = 128,
parameter width = 9
)
(
input clk,

input wr_en,
input [width-1:0] din,

input rd_en,
output [width-1:0] dout,

output full,
output almost_full,

output empty,
output almost_empty,

output [$clog2(depth):0] data_count
);

reg [$clog2(depth)-1:0] wr_addr,rd_addr;
reg wr_flag,rd_flag;
initial begin
{wr_flag, wr_addr} <= 'b0;
{rd_flag, rd_addr} <= 'b0;
end

ram #(
.depth(depth),
.width(width)
) u1
(
.wr_clk(clk),
.wr_en(wr_en && (!full)),
.wr_addr(wr_addr),
.wr_data(din),

.rd_clk(clk),
.rd_en(rd_en && (!empty)),
.rd_addr(rd_addr),
.rd_data(dout)
);

// ----------------pointer counter-------------------
always @(posedge clk) begin
if (wr_en)
if (wr_addr != rd_addr)
{wr_flag, wr_addr} <= {wr_flag, wr_addr} + 'b1;
else if (wr_flag == rd_flag)
{wr_flag, wr_addr} <= {wr_flag, wr_addr} + 'b1;

if (rd_en)
if (wr_addr != rd_addr)
{rd_flag, rd_addr} <= {rd_flag, rd_addr} + 'b1;
else if (wr_flag != rd_flag)
{rd_flag, rd_addr} <= {rd_flag, rd_addr} + 'b1;
end
// ---------------------------------------------------

// ----------------Flag logic-------------------------
assign data_count = {wr_flag, wr_addr} - {rd_flag, rd_addr};
assign full = ((wr_addr == rd_addr) && (wr_flag != rd_flag));
assign empty = ((wr_addr == rd_addr) && (wr_flag == rd_flag));
// ---------------------------------------------------

endmodule

仿真波形:
将深度设置为4,往FIFO写值直至写满,然后从FIFO读回所有值

边读边写的情况:

分时钟FIFO
约等于异步FIFO,但是与异步FIFO仍有区别,通常情况下,异步FIFO指的是读写两个时钟是异步的,即在频率、相位等方面均没有任何联系,但是分时钟FIFO指的是读写时钟各自独立的FIFO,异步同步均可。因此,我更喜欢称它为分时钟FIFO。

分时钟FIFO默认考虑最坏的情况,即读写时钟是异步时钟,因此,对于一些信号在读写时钟域之间的传输需要做特殊的处理。

格雷码
格雷码的优点是:在递增或递减的过程中,每次只变化1位。其格式如下表所示:

从做到右依次是格雷码的0-1-2-3-4-…
可以看到,格雷码存在一个基本的循环00-01-11-10,并且,当格雷码到达最高值归零时,变化的位数同样是1位。这种特点使得格雷码在跨时钟域传输中更加稳定可靠:当格雷码数值发生变化时,由于每次只有一位发生改变,其格雷码数值传输错误的概率就是单比特信号传输出错的概率;同时,就算几次的传输出错也不会对器件功能造成影响,因为格雷码发生错误的情况只有一种,就是保持不变(比如000到001的变化,传输错误的情况只有一种000,即保持原状态),而这种错误在FIFO中影响不会很大。

那如何在二进制和格雷码之间进行转换?


上面展示的是两种最简单最基本的转换方式,其实现的代码如下:

module bin2gray #(
parameter width = 8
)
(
input [width-1:0] bin,
output [width-1:0] gray
);

assign gray[width-1] = bin[width-1];

generate
genvar i;
for (i = width-2; i >= 0 ; i = i - 1)
begin
gray[i] = bin[i+1] ^ bin[i];
end
endgenerate

endmodule

module gray2bin #(
parameter width = 8
) (
input [width-1:0] gray,
output [width-1:0] bin
);
assign bin[width-1] = gray[width-1];

generate
genvar i;
for (i = width-2; i >= 0 ; i = i - 1)
begin
bin[i] = bin[i+1] ^ gray[i];
end
endgenerate

endmodule

对于这两种算法,实际上还有很大的优化空间,这里我们暂时跳过,来看下功能仿真的结果:

可以看到,该模块在二进制和格雷码之间的相互转换结果是正确的(bin是输入的二进制数,gray是转换得到的格雷码,而bin_o是再次转换得到的二进制数),在modelsim的仿真中,没有加入门延时的考虑,但是对于gray2bin这个模块而言,由于存在串联的异或门路径,其生成二进制码的低位的延时会比较严重,在实际使用过程中需要格外注意。

跨时钟域处理
在异步FIFO中,读写指针计数器的格雷码需要跨时钟域传输,由于格雷码在递增或递减的过程中每次只变化1位,因此可以通过打两拍的方式,进行跨时钟域的传输。

读写指针计数器和空满判断逻辑
与共时钟FIFO一致,唯一变化是满标志和写数据计数由写时钟产生,空标志和读数据计数由读时钟产生。

完整模块代码
module independent_fifo #
(
parameter depth = 128,
parameter width = 9
)(
input wr_clk,
input wr_en,
input [width-1:0] din,

input rd_clk,
input rd_en,
output [width-1:0] dout,

output full,
output almost_full,

output empty,
output almost_empty,

output [$clog2(depth):0] rd_data_count,
output [$clog2(depth):0] wr_data_count
);

reg [$clog2(depth)-1:0] wr_addr,rd_addr;
reg wr_flag,rd_flag;
initial begin
{wr_flag, wr_addr} <= 'b0;
{rd_flag, rd_addr} <= 'b0;
end

// ----------------ram----------------------
ram #(
.depth(depth),
.width(width)
)ram(
.wr_clk(wr_clk),
.wr_en(wr_en && (!full)),
.wr_addr(wr_addr),
.wr_data(din),

.rd_clk(rd_clk),
.rd_en(rd_en && (!empty)),
.rd_addr(rd_addr),
.rd_data(dout)
);
// -----------------------------------------

// ------------------combine----------------
wire [$clog2(depth):0] wr_addr_bin, rd_addr_bin;
assign wr_addr_bin = {wr_flag, wr_addr};
assign rd_addr_bin = {rd_flag, rd_addr};
// -----------------------------------------

// -------------bin2gray--------------------
wire [$clog2(depth):0] wr_addr_gray, rd_addr_gray;
bin2gray #(.width($clog2(depth)+1))
u1 (
.bin(wr_addr_bin),
.gray(wr_addr_gray)
);

bin2gray #(.width($clog2(depth)+1))
u2 (
.bin(rd_addr_bin),
.gray(rd_addr_gray)
);
// -----------------------------------------

// ------------------CDC--------------------
reg [$clog2(depth):0] wr_addr_gray_d0, wr_addr_gray_d1;
always @(posedge rd_clk) begin
wr_addr_gray_d0 <= wr_addr_gray;
wr_addr_gray_d1 <= wr_addr_gray_d0;
end

reg [$clog2(depth):0] rd_addr_gray_d0, rd_addr_gray_d1;
always @(posedge wr_clk) begin
rd_addr_gray_d0 <= rd_addr_gray;
rd_addr_gray_d1 <= rd_addr_gray_d0;
end
// -----------------------------------------

// -------------gray2bin--------------------
wire [$clog2(depth):0] wr_addr_bin_d1, rd_addr_bin_d1;
gray2bin #(.width($clog2(depth)+1))
u3 (
.gray(wr_addr_gray_d1),
.bin(wr_addr_bin_d1)
);

gray2bin #(.width($clog2(depth)+1))
u4 (
.gray(rd_addr_gray_d1),
.bin(rd_addr_bin_d1)
);
// -----------------------------------------

// -------------addr_dounter----------------
always @(posedge wr_clk) begin
if (wr_en)
if (wr_addr != rd_addr_bin_d1[$clog2(depth)-1:0])
{wr_flag, wr_addr} <= {wr_flag, wr_addr} + 'b1;
else if (wr_flag == rd_addr_bin_d1[$clog2(depth)])
{wr_flag, wr_addr} <= {wr_flag, wr_addr} + 'b1;
end

always @(posedge rd_clk) begin
if (rd_en)
if (wr_addr_bin_d1[$clog2(depth)-1:0] != rd_addr)
{rd_flag, rd_addr} <= {rd_flag, rd_addr} + 'b1;
else if (wr_addr_bin_d1[$clog2(depth)] != rd_flag)
{rd_flag, rd_addr} <= {rd_flag, rd_addr} + 'b1;
end
// -----------------------------------------

// ------------------Flag logic-------------
assign wr_data_count = wr_addr_bin - rd_addr_bin_d1;
assign rd_data_count = wr_addr_bin_d1 - rd_addr_bin;
assign full = ((wr_addr == rd_addr_bin_d1[$clog2(depth)-1:0]) && (wr_flag != rd_addr_bin_d1[$clog2(depth)]));
assign empty = ((wr_addr_bin_d1[$clog2(depth)-1:0] == rd_addr) && (wr_addr_bin_d1[$clog2(depth)] == rd_flag));
// -----------------------------------------

// wr_addr_bin : wr_clk;
// rd_addr_bin_d1 : wr_clk;

// rd_addr_bin : rd_clk;
// wr_addr_bin_d1 : rd_clk;

endmodule

在代码中,各个信号的时钟域如下表所示

可以看到,CDC发生在 wr_addr_gray 到wr_addr_gray _d* 和 rd_addr_gray到rd_addr_gray_d* 之间,这种方式可以减少亚稳态出现的概率,确保电路功能正确。但是由于打了两拍,从wr_addr_bin 到 wr_addr_bin_d1 和从rd_addr_bin 到 rd_addr_bin_d1 各会出现两个时钟周期的延时,从下面的仿真波形也可以看出:

这种延时在某些情况下会导致一些数据丢失(当FIFO的深度很小时)。

再来看看连续读写的情况:

虽然FIFO的读使能和写使能同时置高,但是由于在读时钟域需要等待wr_addr_bin_d1传递过来,因此数据的输出会慢两拍。
同时,我们注意到,当数据进行连续传输时,wr_data_count会稳定在5,而rd_data_count会稳定在1。
造成这个现象的原因也是由于读写指针的addr在跨时钟域传输中的延时导致的。

上面有提到的丢失数据的情况也与此相关,当你的异步FIFO深度小于这个稳定的wr_data_count(本实例中为5)时,就会导致数据丢失。