57 / 63 · 概念
单时钟 FIFO 的指针与边界策略
实现深度 4 FIFO 的指针与 occupancy,明确空、满时同时 push/pop 的接受条件。
课时内容可免费阅读,选课后可保存学习进度。
不移动数据也能保持顺序
FIFOFIFO 先存入的数据先取出的队列。满、空以及同时读写的允许规则,都是行为约定的一部分。先入先出。单时钟设计在同一上升沿决定读取与写入。本课固定 DEPTH=4、数据宽度为八位。rptr 指向下一个取出位置,wptr 指向下一个存入位置,均为两位,因而在 3 后自然回到 0。occupancy 要表示条目数 0~4,需要三位。
rptr=wptr 既可能表示空,也可能表示满。因此仅凭两地址相等判断 empty,会丢失满队列的数据。本例单独维护数量 ,用 、 区分。深度 时,地址宽度 与计数宽度 如下。
区分请求与接受的边界策略
push 和 pop 是请求。分别用 p、q 表示实际接受的 push、pop,则在非复位边沿之前,根据当前状态计算:
空队列同时收到两个请求时,没有已有数据可弹出,因此只接受 push,新数据不会在该边沿立即输出。满队列同时收到两个请求时,可先取出旧 head 腾出位置,因此两个都接受。full 表示“当前存有四项”,在此策略中并非无条件禁止 push 的信号。
| 边沿前状态 | 仅 push | 仅 pop | push 与 pop |
|---|---|---|---|
| C=0 | 存入一项 | 拒绝 | 只接受 push,C=1 |
| 0<C<4 | C+1 | C−1 | 都接受,C 不变 |
| C=4 | 拒绝 | C=3 | 都接受,C=4 |
推进时钟并检查边界条件
开始时同时请求 push 和 pop。队列为空,应只接受 push。随后关闭 pop,填满四个槽位,再提供新输入值并重新开启两个请求。占用量应保持为 4,输出为旧 head,存储空间中的队尾则为新数据。
四槽 FIFO,初始为空,读写指针均为 0。非空时接受 pop;有空位或同一边沿接受 pop 时,接受 push。
图中教学模型显示每个上升沿上升沿 时钟从 0 变为 1 的瞬间。它不同于表示整个 CLK=1 区间的电平。了解更多后的状态。pop 被接受时,output valid 为 1;下一边沿若未接受 pop,输出寄存器寄存器 存储多位状态的电路。本课程中的同步寄存器在时钟边沿存储指定输入。了解更多数值保持,但 valid 变为 0。空槽位中的旧比特被隐藏。“重新开始”会初始化整个实验,并不表示 RTL 的存储器复位复位 将状态恢复为规定初值的控制。必须同时规定其同步或异步性质,以及相对于其他控制的优先级。了解更多行为。
包含寄存输出的 RTL
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 的输出数值不纳入判分。