Altifigence Academy

57 / 63 · 概念

单时钟 FIFO 的指针与边界策略

实现深度 4 FIFO 的指针与 occupancy,明确空、满时同时 push/pop 的接受条件。

不移动数据也能保持顺序

FIFOFIFO 先存入的数据先取出的队列。满、空以及同时读写的允许规则,都是行为约定的一部分。先入先出。单时钟设计在同一上升沿决定读取与写入。本课固定 DEPTH=4、数据宽度为八位。rptr 指向下一个取出位置,wptr 指向下一个存入位置,均为两位,因而在 3 后自然回到 0。occupancy 要表示条目数 0~4,需要三位。

rptr=wptr 既可能表示空,也可能表示满。因此仅凭两地址相等判断 empty,会丢失满队列的数据。本例单独维护数量 CC,用 empty=(C=0)\mathrm{empty}=(C=0)、full=(C=4)\mathrm{full}=(C=4) 区分。深度 D=4D=4 时,地址宽度 WaddrW_{\mathrm{addr}} 与计数宽度 WcountW_{\mathrm{count}} 如下。

Waddr=⌈log⁡2D⌉=2Wcount=⌈log⁡2(D+1)⌉=3\begin{aligned} W_{\mathrm{addr}}&=\lceil\log_2 D\rceil=2\\ W_{\mathrm{count}}&=\lceil\log_2(D+1)\rceil=3 \end{aligned}

区分请求与接受的边界策略

push 和 pop 是请求。分别用 p、q 表示实际接受的 push、pop,则在非复位边沿之前,根据当前状态计算:

q=pop∧(C>0)p=push∧((C<4)∨q)C+=C+p−q\begin{aligned} q&=\mathrm{pop}\land(C>0)\\ p&=\mathrm{push}\land((C<4)\lor q)\\ C^{+}&=C+p-q \end{aligned}

空队列同时收到两个请求时,没有已有数据可弹出,因此只接受 push,新数据不会在该边沿立即输出。满队列同时收到两个请求时,可先取出旧 head 腾出位置,因此两个都接受。full 表示“当前存有四项”,在此策略中并非无条件禁止 push 的信号。

边沿前状态仅 push仅 poppush 与 pop
C=0存入一项拒绝只接受 push,C=1
0<C<4C+1C−1都接受,C 不变
C=4拒绝C=3都接受,C=4

推进时钟并检查边界条件

开始时同时请求 push 和 pop。队列为空,应只接受 push。随后关闭 pop,填满四个槽位,再提供新输入值并重新开启两个请求。占用量应保持为 4,输出为旧 head,存储空间中的队尾则为新数据。

逐时钟观察 FIFO

四槽 FIFO,初始为空,读写指针均为 0。非空时接受 pop;有空位或同一边沿接受 pop 时,接受 push。

图中教学模型显示每个上升沿上升沿 时钟从 0 变为 1 的瞬间。它不同于表示整个 CLK=1 区间的电平。了解更多后的状态。pop 被接受时,output valid 为 1;下一边沿若未接受 pop,输出寄存器寄存器 存储多位状态的电路。本课程中的同步寄存器在时钟边沿存储指定输入。了解更多数值保持,但 valid 变为 0。空槽位中的旧比特被隐藏。“重新开始”会初始化整个实验,并不表示 RTL 的存储器复位复位 将状态恢复为规定初值的控制。必须同时规定其同步或异步性质,以及相对于其他控制的优先级。了解更多行为。

包含寄存输出的 RTL

SystemVerilog
module fifo4 (
  input  logic       clk, rst, push, pop,
  input  logic [7:0] din,
  output logic [7:0] dout,
  output logic       dout_valid, full, empty,
  output logic [2:0] occupancy
);
  timeunit 1ns;
  timeprecision 1ps;
  logic [7:0] mem [0:3];
  logic [1:0] rptr, wptr;
  logic take_push, take_pop;

  assign empty = (occupancy == 3'd0);
  assign full  = (occupancy == 3'd4);
  assign take_pop  = !rst && pop && !empty;
  assign take_push = !rst && push && (!full || take_pop);

  always_ff @(posedge clk) begin
    if (rst) begin
      rptr <= 2'd0;
      wptr <= 2'd0;
      occupancy <= 3'd0;
      dout_valid <= 1'b0;
    end else begin
      dout_valid <= take_pop;
      if (take_push) begin
        mem[wptr] <= din;
        wptr <= wptr + 2'd1;
      end
      if (take_pop) begin
        dout <= mem[rptr];
        rptr <= rptr + 2'd1;
      end
      case ({take_push, take_pop})
        2'b10: occupancy <= occupancy + 3'd1;
        2'b01: occupancy <= occupancy - 3'd1;
        default: occupancy <= occupancy;
      endcase
    end
  end
endmodule

这里没有用两个独立 if 分别给 occupancy 加一和减一。因为同时接受时,对同一寄存器执行两次非阻塞非阻塞赋值 时序 RTL 中以 <= 表示的赋值。先计算右侧,再预约更新,使同一边沿的各寄存器能够依据旧状态计算。了解更多赋值,最后一次会覆盖前一次。两个指针在各自接受时前进,数量则统一按四种情况决定。接受条件也防止在 0 时递减或在 4 时递增。

满状态下同址读写

依次放入 A、B、C、D 后,rptr=wptr=0、occupancy=4。此时 pop=push=1、din=E,则边沿后 dout=A、dout_valid=1,逻辑内容为 [B,C,D,E]。两个指针都变为 1,数量保持 4。mem[0] 改为 E,但 dout 的非阻塞赋值右侧读取旧 A。这种 read-first 冲突行为是满状态替换策略所必需的。

接着 push=0、pop=1,会输出 B,数量变为 3。相反,空队列时给出 din=Z 和两个请求,只使数量变为 1,dout_valid=0;必须下一边沿再次请求 pop 才输出 Z。空队列立即转发与满队列替换是两个独立设计选择。

rst 为同步高有效复位,初始化指针、数量和 dout_valid,不清除存储器与 dout 数值。复位后即使旧比特仍在,empty 会阻止 pop,后续也只读取新 push 写过的位置,所以正常功能不要求清零整个数组。dout_valid=0 时不得消费输出。valid=1 是已接受 pop 的响应,输出侧没有独立 ready,因此消费者必须接收该响应。

DEPTH 不是 2 的幂时,不能复用简单的两位溢出回绕。例如深度为 3 时,必须显式在地址 2 后返回 0。若本例综合为 RAM,也必须确认其支持同址 old-data 行为。这是单时钟代码,不能直接用于异步 FIFO 的指针同步。

自己试试

复位后逐边沿施加:(1) push A 与 pop 同时,(2) 仅 push B,(3) 仅 push C,(4) 仅 push D,(5) push E 与 pop 同时,(6) 仅 pop。写出每步后的队列内容、occupancy、dout_valid 及有效输出值,并解释第 5 步为何需要 read-first。

阅读解释

(1) [A],1,valid=0。(2) [A,B],2,valid=0。(3) [A,B,C],3,valid=0。(4) [A,B,C,D],4,valid=0。(5) [B,C,D,E],4,valid=1,dout=A。(6) [C,D,E],3,valid=1,dout=B。第 1 步无已有条目,因此拒绝 pop。第 5 步 rptr=wptr=0,同一槽位发生读写,必须读出旧 head A。若 write-first 返回 E,就会破坏 FIFO 的顺序与内容。valid=0 的输出数值不纳入判分。

你的选择适用于此浏览器,可随时在页脚更改。