怎样设计一个同步FIFO?(1)

电子说

1.3w人已加入

描述

今天咱们开始聊聊FIFO的设计。FIFO是一个数字电路中常见的模块,主要作用是数据产生端和接受端在短期内速率不匹配时作为数据缓存。FIFO是指First In, First Out,即先进先出,跟大家排队一样。越早排队的人排在越前面,轮到他的次序也越早,所以FIFO有些时候也被称为队列queue。

这一篇我们先介绍利用Flip flop来作为FIFO存储单元的设计方法,这也是同步FIFO中最为简单的内容,内容比较基础。之后老李会带大家了解基于SRAM的FIFO设计。 而且我们这里只讲同步FIFO, 即写入端和读出端是属于同一个时钟域。如果写入和读出是不同的时钟域,那么就是异步FIFO。关于异步FIFO之前老李在CDC系列里有讲过,大家有兴趣可以直接在公众号底部点击CDC可以了解。

我们先来看一个FIFO模块需要那几个基本的输入输出。

其中写入端写入操作为push,要写入的data为D,当push为高时,数据D被写入FIFO。对于写入端,只需要在乎FIFO是不是满:如果FIFO已经满了,是不允许再写入的。对于读出端,数据读出为pop,Q为读出的数据。在读出端,只需要在乎FIFO是不是空:如果为空,则不能进行pop操作。

对于读出端来说,这里有一点需要明确:当FIFO里面有数据的时候,Q应该输出当前FIFO最前面(最早进入)的那个数据,而不是需要pop才能输出。也就是说,假设FIFO为空,这个时候我们写入一个数据D1,那么在下一个周期,Q应该立刻变为D1,同时empty为0。当只有读第二次写入的数据的时候,我们才需要pop第一次,Q才会指向D2。 这样的行为和一个D触发器非常类似,所以上面我们才将输入数据表示为D,输出数据表示为Q,便于和D触发器类比起来。为什么强调这一点,因为在后面利用SRAM来实现FIFO的时候如果要实现这一点是需要技巧的,我们后面会看到。(老李也见过要想读出第一个数必须要先pop一次的FIFO设计,这种设计就不是很高效,要多花一个周期来才能读出第一个数)。

另外FIFO还有一个特性,即当FIFO不是空也不是满的时候,是允许读和写发生在同一个周期的,即一边写入,一边读出。这个对于Flip Flop来实现的FIFO很容易做到,但是对于SRAM来实现的FIFO就不是那么容易了,特别是SRAM只有一个端口,一个周期内要么读,要么写。这样设计的时候就更需要技巧了,我们在后面的文章中再细聊。

下面我们来聊FIFO的内部细节。首先我们说存储单元,对于利用FlipFlop来实现的FIFO,存储单元就是一个flop array。

reg [DATA_WIDTH-1:0]    mem[DEPTH]

其中DATA_WIDTH和DEPTH都是两个参数parameter。

然后我们需要两个指针pointer,来分别用于读和写,分别为wr_ptr, rd_ptr。有人也喜欢用wr_addr, rd_addr。这两个指针的意义为:

wr_ptr: 接下来要写入的位置。

rd_ptr: 当前读出的位置。

初始的时候,wr_ptr和rd_ptr都被reset成0,那么可以理解为,第一个要写入的location是mem[0],第一个要读出的位置也是mem[0]。

当有一次push操作的时候,wr_ptr要加1。当有一次pop的时候,rd_ptr要+1。

那么我们可以写出下面的逻辑

always_ff@(posedge clk) begin

if(!rst_n)

for(int i = 0; i < DEPTH; i++)

   mem[i] <= '0;

else begin

if(push & ~full)

   mem[wr_ptr] <= d;

end

end

assign q = mem[rd_ptr];

那么接下来有两个问题:一是如何来判断空和满,另个一问题是如何给wr_ptr和rd_ptr加1。

在思考这两个问题之前,我们看我们需要几位来表示wr_ptr和rd_ptr。如果FIFO的深度是DEPTH,那么要来取址mem[DEPTH],我们需要的位数应该是$clog2(DEPTH)。比如DEPTH=8,那么需要3位ptr用来取址。

再来回答上面两个问题。通常我们有两种方式来处理。第一种方式,如果DEPTH刚好是2的幂次,那么做法是给wr_ptr和rd_ptr各多分配一位。比如DEPTH=8,则分配4位给wr_ptr和rd_ptr。这样做的好处是我们可以利用2进制的特性

空:wr_ptr == rd_ptr。

满:wr_ptr把rd_ptr套圈了,即低位相等,但是MSB相反。

举个例子当把mem[0]到mem[7]都写完之后,wr_ptr 由4’b0111再加1就来到了4'b1000,而如果我们还没有pop过的话rd_ptr就还停留在4‘b0000,这样就是达到了套圈,FIFO变满了。

而且这样做简化了rd_ptr和wr_ptr加1的操作,直接利用2进制的进位加法,当记到4‘b0111的时候再加1就直接变为4'b1000,这样MSB自动表示是不是套圈,而低位可以直接用来取址mem。

localparam PTR_WIDTH = $clog2 (DEPTH) + 1;

logic [DATA_WIDTH-1:0] mem[DEPTH];

logic [PTR_WIDTH-1:0] wr_ptr;

logic [PTR_WIDTH-1:0] rd_ptr;

always_ff@(posedge clk) begin

if(!rst_n)

for(int i = 0; i < DEPTH; i++)

   mem[i] <= '0;

else begin

if(push & ~full)

   mem[wr_ptr[PTR_WIDTH-2:0]] <= d;

end

end

always_ff@(posedge clk) begin

if(!rst_n)

wr_ptr <= '0;

else

if(push & ~full)

  wr_ptr <= wr_ptr + 1'b1;

end

always_ff@(posedge clk) begin

if(!rst_n)

rd_ptr <= '0;

else

if(pop & ~empty)

  rd_ptr <= rd_ptr + 1'b1;

end

assign q = mem[rd_ptr[PTR_WIDTH-2:0]];

assign full = (rd_ptr[PTR_WIDTH-1] ^ wr_ptr[PTR_WIDTH-1]) &&

(rd_ptr[PTR_WIDTH-2:0] == wr_ptr[PTR_WIDTH-2:0]);

assign empty = rd_ptr == wr_ptr;

但是这样做的限制在于DEPTH必须是2的幂次方个。 如果不是,比如是6,那么当wr_ptr记到3'b101的时候,下一次写入就不能直接二进制加1了,而是要回到3'b000。这个时候稍微方便一点的做法是设计一个计数器,用来计数当前FIFO已经被写入但是还未读出的数据个数。这样做的好处是FIFO的空满可以直接利用这个计数器与0和与DEPTH相比较而得到。老李更推荐这一种写法,而且这个时候wr_ptr和rd_ptr也不需要多加1位。

localparam PTR_WIDTH = $clog2 (DEPTH);

logic [DATA_WIDTH-1:0] mem[DEPTH];

logic [PTR_WIDTH-1:0] wr_ptr;

logic [PTR_WIDTH-1:0] rd_ptr;

logic [PTR_WIDTH:0] cnt; //current fifo count

always_ff@(posedge clk) begin

if(!rst_n)

for(int i = 0; i < DEPTH; i++)

   mem[i] <= '0;

else begin

if(push & ~full)

   mem[wr_ptr] <= d;

end

end

always_ff@(posedge clk) begin

if(!rst_n)

wr_ptr <= '0;

else

if(push & ~full)

  wr_ptr <= (wr_ptr == DEPTH-1) ? '0 : (wr_ptr + 1'b1);

end

always_ff@(posedge clk) begin

if(!rst_n)

rd_ptr <= '0;

else

if(pop & ~empty)

  rd_ptr <= (rd_ptr == DEPTH-1) ? '0 : (rd_ptr + 1'b1);

end

always_ff@(posedge clk) begin

if(!rst_n)

cnt <= '0;

else begin

//only push, no pop

if(push && !pop && !full)

  cnt <= cnt + 1'b1;

//only pop, no push

else if(!push && pop && !empty)

  cnt <= cnt - 1'b1;

//no pop or push,

//pop and push in the same cycle

// else cnt <= cnt;

end

end

assign q = mem[rd_ptr];

assign full = cnt == DEPTH;

assign empty = cnt == '0;

这就是基于Flip Flop的同步FIFO的基本原理,还是比较简单直接的,RTL code加起来也就几十行。下面老李希望大家思考几个问题:

  1. 什么时候使用基于Flip-flop的同步FIFO?什么时候使用基于SRAM的FIFO?
  2. 最后的q是来自于寄存器输出还是来自于组合逻辑电路输出?如果是来自于组合逻辑输出,如何优化?
  3. 如果希望full和empty也直接来自寄存器的输出,要怎么更改设计?

最后再附送一个老李一个老朋友的作为面试官的出的面试题,大家可以自己思考一下:如何设计一个深度为1的同步FIFO?

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分