push进去和pop出来的不是一个值_如何设计一个同步FIFO(二)

上一篇如何设计一个同步FIFO(一)中老李介绍了利用Flip Flop来作为存储单元的同步FIFO设计。这一篇咱们来看如何利用2 Port SRAM来作为存储单元设计同步FIFO。

开始往下读之前,老李先问一个问题,假如现在让你设计一个深度为N的基于2port SRAM的同步FIFO,请问至少需要多大的SRAM? 假设SRAM的位宽就是你的数据宽度,那么问题就是问你需要的SRAM的行数至少是多少?如果你觉得答案是显而易见的N,那么你值得读完这一篇。

首先来说为什么要用SRAM设计FIFO,很简单为了省面积。我们说存1bit的数据,SRAM里一个bit cell经典结构是6个晶体管,而一个flip flop需要的晶体管在20个左右,那么从面积上来说,肯定是SRAM小对吧?但是这里面有个平衡点,对于一块SRAM,算面积的时候不能仅仅算里面存储单元bit cell的面积,还要算外围的decode逻辑的面积。当SRAM的row size x column size比较小的时候,外围的decode逻辑占的比重比较大,反而这个时候SRAM的总面积不如row x column个flop的面积小。至于这个转折点要看工艺和memory compiler的data sheet,具体面积要拿SRAM的data sheet去比较。老李自己总结的一个经验,不保证完全准确,在5nm工艺下,这个转折点大概在2k bit,低于2k bit,比如16x32的大小,那还是Flop划算,如果远大于2k bit,那么就用SRAM,差不多在2-3k bit量级的时候要根据memory datasheet里的area number来比较。

再来说说什么是2 port SRAM,2 port 通常也被称作dual port,一个端口为写端口,一个端口为读端口。这两个端口可以同时工作,同一个周期内既可以读,也可以写,简化的框图如下图所示

2b2bcde9c2665851de23222911d79f31.png

WCEN:Write Chip Enable Neg,当这个信号为0的时候,要写入数据

WDATA:  要写入的数据

WADDR:要写入的地址

RCEN:Read Chip Enable Neg,当这个 信号为0的时候,要读出数据

RDATA: 读出的数据

RADDR: 读出的地址

这里要注意,要写入的数据是指WCEN为0那个周期的WDATA,而读出的数据并不是RCEN为0那个周期的RDATA,而是下一个周期的RDATA。

时序图如下图所示

9ac5435c2adc38eb892783de8b577564.png

可以看到,cycle 1进行写操作,在地址A0写入数据D0。在cycle 2进行读操作,要在cycle 3才能读出D0。那你一定好奇如果给同一个地址在同一个周期又读又写怎么办呢?这个不同的foundary的sram可以有不同的实现,这里表示的是一个较为常见的实现,看cycle 7,读写同时发生并且在同一个地址,在cycle 8读出的还是之前写入在这个地址的数据D0,而不是cycle 7写入的新数据D1。只有再读一次A0,你才能看到D1。

下面我们来考虑如何设计基于2port SRAM的同步FIFO。既然FIFO有push和pop端,也有wdata和rdata,那刚好对应SRAM的write port和read port,看起来我们大概只需要直接把SRAM包起来,用两个计数器来分别计算write pointer和read pointer就可以了,如下图所示

f1cdfaa0dd76b2d0192f06749c3594b5.png

真的这么简单吗?我们来思考一个问题,用上面的FIFO来存第一个数D0,利用上面的结构,q能够在push的下一个周期变为D0吗?

回顾一下我们想要的FIFO的时序

0dcd0f4ec5c90e4d057c2979a61cf32e.png

在cycle1的时候我们push D0,我们期望Q在cycle 2的时候就可以输出FIFO最头上的数D0,而且注意,这个时候我们并没有进行pop操作。

可是从上面SRAM的时序图我们可以看到,要想让RDATA输出D0,我们至少要让RCEN为0一个周期,而且RCEN为0必须得在WCEN为0之后,让RDATA拿到D0最快也到了WCEN之后的2个周期,因而不满足在PUSH之后下一个周期Q就输出D0。

另外还有个问题,当我们push进去一个数之后,如果这个数写入了SRAM,那么我们必须要有一次读SRAM的操作,才能把数读出来,这在上面的框图里也就是要执行一次pop。这也和FIFO的工作相背离,因为FIFO可能并不是需要立刻pop。

那么我们怎么做才能让Q在push的下一个周期输出D0呢?环顾四周,好像除了利用Flip Flop也没有别的办法了。也就是说,第一个数我们把数不写进SRAM,而是写到一个Flop里,然后让Q从Flop输出

等等,说好了用SRAM来存数据的,怎么又把数据存到Flop里面去了呢?

ff9939adb58fe088bae6cd75db254bb3.png

别急,这里确实是没有办法的办法,如果我们有可以和Flop时序一样的SRAM,那么也可以不用这种办法,但是在接受SRAM的时序是这样的情况下,我们必须借助Flop来实现第一个数在push之后下一个周期就能够出现在Q上。

当然,我们不是说所有的数都要用Flop来存,接下来push的数据我们还是要放在SRAM里面的,我们把上面的结构改一改,可以先设计出下面的FIFO结构。

55752e55f9e73c879ab630ab27b887f9.png

在这个结构中,我们从D可以直接把数据存到输出级的这个Flop中,相当于bypass了SRAM,然后我们的想法是:当第二次push的时候,我们再把数据存到SRAM里,同时,当我们把第一个数据pop出来之后,我们就把数据从SRAM里拿到这个输出级的Flop中,这样是不是就对了呢?

不好意思,其实还是有问题,我们假设FIFO里面已经存了2个数据,D0存在输出级的Flop里,D1存在SRAM里,如下图所示

c430ee1ca328165907ab9417634f4240.png

那么当我们要pop一次之后,我们期望的FIFO的时序是:在pop的下一个周期,Q就应该是D1了,因为D0被弹出,FIFO最头上的数是D1了。如下图所示

1bd1553dbe3f194dc10adeaff280fa6e.png

但是如果在pop为1的那个周期去读SRAM,则要在下一个周期RDATA才能读出D1,再还需要一个周期才能把D1存到输出级的Flop上,也就是pop之后两个周期才能看到D1, 这样就不满足FIFO的时序了。

那怎么办呢?我们只好省去把D1从RDATA存到Flop上的那一步,而是把RDATA当做Q来直接输出。电路结构变成了下面

f9ac289fe02258c9227e27c4d75aa8fb.png

我们的设计思路就变成了

  1. 往FIFO里push的第一个数要bypass SRAM,把数据直接存到输出级Flop去。

  2. 如果SRAM里面存了数,那么pop一次,就要把RDATA直接输出到Q端。

看起来没有问题了吧?其实还有一个特殊情况没有考虑到,也就是当FIFO里只有一个数据,而在push下一个数据的时候同时来了pop。如下图所示

0dcd0f4ec5c90e4d057c2979a61cf32e.png

那么这个时候要push的D1是不能往SRAM里写的,一旦写进去要读出来还得多花一个周期,所以这个时候也是要bypass SRAM。

所以更加严谨的条件是

  1. 往FIFO里push的第一个数要bypass SRAM,或者FIFO里只有1个数据,而且在push新数据的同时pop,那么把数据直接存到输出级Flop去。

  2. 当FIFO里只有一个数据的时候,Q端来自于输出级Flop

  3. 如果SRAM里面存了数,那么pop一次,就要把RDATA直接输出到Q端。

至此,我们利用一级flip flop来实现了省去一个读SRAM的周期,可以实现FIFO的时序。

现在可以回答文章开头的问题了,因为有了输出级Flop来存第一个数据,那么SRAM其实并不需要存N个数据,只需要存N-1个数据就好了。但是在实际工作中,Width x N 和Width x (N-1)的面积其实没有差很多,你用Width xN的SRAM完全没有问题。当然要注意,这个时候memory 的address pointer和我们上一讲里的wr_ptr, rd_ptr就不是完全一样了,因为第一个数据并不是存在SRAM里。

写到这里,是不是就完全搞定问题了呢?我们来分析一下,我们这样设计可行其实是有一个前提,即SRAM的RDATA的timing是下面的。这里把开头的图再看一遍

9ac5435c2adc38eb892783de8b577564.png

我们说FIFO的Q在不pop的时候是稳定不变的,是利用了这里SRAM的特性,即读完一次之后,RDATA的值会保持不变,一直到下一次读操作(图中的cycle 3-7),这样我们就可以直接把RDATA输出到Q。

但是并不是所有厂家的SRAM的时序是这样的,大家要看厂家的memory的datasheet来确认时序(TSMC家的是这样的, 但是可能别的厂家并不保证)如果SRAM时序是下面这样,那么我们的设计就不能满足FIFO的要求了

aeff1952719c7aa7e66f35a580fa6323.png

而且上面的设计可能还有一个STA上的问题,因为Q来自于RDATA,那么SRAM内部的clock-to-rdata的timing可能比较大,这样给后面FIFO输出的Q后面所留有的空间就比较小了。如果Q之后还要做逻辑运算,或者再下一级Flop距离比较远,那么修timing的时候就比较挑战。

要解决上面两个问题,一个必然的思路是不能直接输出RDATA到Q,还是要把RDATA给flop住,这样一方面使得输出Q可以保持稳定,另一方面,这一级flop依然在FIFO内部,从Flop直接输出对于FIFO后级的timing有帮助。你看,兜兜转转,似乎还是要回到我们前面要毙掉的方案。那么我们要怎么设计,才能解决前面那个方案不满足FIFO时序要求的缺陷呢?大家可以自己先思考一下,老李下篇再带来更深入的讲解。

感谢阅读到最后的你。如果你觉得老李的文章有用,不妨点个?和“在看”,更欢迎分享到群和朋友圈。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个同步复位的FIFO的 Verilog 设计代码和对应的 testbench: ```verilog module fifo_sync_rst ( input clk, // 时钟信号 input rst, // 复位信号 input we, // 写使能信号 input [7:0] din, // 写数据信号 input re, // 读使能信号 output [7:0] dout, // 读数据信号 input full, // FIFO满信号 input empty // FIFO空信号 ); parameter DEPTH = 16; // FIFO深度 reg [7:0] mem [0:DEPTH-1]; // FIFO存储单元 reg [3:0] wr_ptr = 0; // 写指针 reg [3:0] rd_ptr = 0; // 读指针 reg [3:0] count = 0; // FIFO计数器 // 写操作 always @(posedge clk) begin if (rst) begin wr_ptr <= 0; count <= 0; end else if (we && !full) begin mem[wr_ptr] <= din; wr_ptr <= wr_ptr + 1; count <= count + 1; end end // 读操作 always @(posedge clk) begin if (rst) begin rd_ptr <= 0; count <= 0; end else if (re && !empty) begin dout <= mem[rd_ptr]; rd_ptr <= rd_ptr + 1; count <= count - 1; end end // FIFO满和空信号 assign full = (count == DEPTH); assign empty = (count == 0); endmodule module fifo_sync_rst_tb; reg clk, rst, we, re; reg [7:0] din; wire [7:0] dout; wire full, empty; fifo_sync_rst dut (.clk(clk), .rst(rst), .we(we), .din(din), .re(re), .dout(dout), .full(full), .empty(empty)); // 时钟信号 always #5 clk = ~clk; // 重置信号 initial begin rst = 1; #50 rst = 0; end // 写操作 initial begin we = 1; din = 8'h01; #10; din = 8'h02; #10; din = 8'h03; #10; din = 8'h04; #10; din = 8'h05; #10; din = 8'h06; #10; din = 8'h07; #10; din = 8'h08; #10; din = 8'h09; #10; din = 8'h0A; #10; din = 8'h0B; #10; din = 8'h0C; #10; din = 8'h0D; #10; din = 8'h0E; #10; din = 8'h0F; #10; din = 8'h10; #10; din = 8'h11; #10; din = 8'h12; #10; din = 8'h13; #10; din = 8'h14; #10; din = 8'h15; #10; din = 8'h16; #10; din = 8'h17; #10; din = 8'h18; #10; din = 8'h19; #10; din = 8'h1A; #10; din = 8'h1B; #10; din = 8'h1C; #10; din = 8'h1D; #10; din = 8'h1E; #10; din = 8'h1F; end // 读操作 initial begin re = 1; #60; re = 0; #10; re = 1; #10; re = 0; #20; re = 1; #10; re = 0; #30; re = 1; #10; re = 0; #40; re = 1; #10; re = 0; #50; re = 1; #10; re = 0; #60; re = 1; #10; re = 0; #70; re = 1; #10; re = 0; end // 输出状态 always @(posedge clk) begin $display("count=%d full=%d empty=%d", dut.count, full, empty); end endmodule ``` 在设计中,FIFO 采用双时钟同步存储器的方式进行存储,读写指针和计数器用于维护 FIFO 的状态,full 和 empty 信号用于表示 FIFO 的满和空状态。在 testbench 中,使用了一个时钟信号和一个复位信号,以及写使能、写数据、读使能等信号来模拟 FIFO 的读写操作。利用 $display 函数输出 FIFO 的状态信息。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值