FIFO设计(Verilog)


为了应付找工作的需要,打算学习一些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控制深度。
在实现这个模块的过程中,要考虑清楚深度和地址位宽的关系,因为不同的深度对应的地址位宽也不同:

深度地址范围地址位宽
640-636
320-315
160-154
80-73
40-32
20-11

因此,双口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<depth; i=i+1)
    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位。其格式如下表所示:

3bit000-001-011-010-110-111-101-100
4bit0000-0001-0011-0010-0110-0111-0101-0100-
1100-1101-1111-1110-1010-1011-1001-1000
十进制01234567
二进制000001010011100101110111
格雷码000001011010110111101100

从做到右依次是格雷码的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

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

wr_clkrd_clk
wr_addrrd_addr
wr_flagrd_flag
wr_addr_binrd_addr_bin()
wr_addr_grayrd_addr_gray
rd_addr_gray_d*wr_addr_gray_d*
rd_addr_bin_d1wr_addr_bin_d1

可以看到,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)时,就会导致数据丢失。

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值