工程里有时只需要缓存 3 个或 5 个数据,并不需要把逻辑容量扩成 4 或 8。真正容易出错的地方,是把“指针加一自然溢出”误当成任意深度的回绕规则,以及忽略满队列同拍读写、深度为 1 时的边界。
本文实现一个小容量、单时钟的 ready/valid FIFO。重点是把接口契约写清楚,再用独立于环形指针的参考队列验证。它不是异步 FIFO,也不承诺映射成 Block RAM。
**验证范围:**本文 RTL 与下方完整测试平台已在 Windows 上的 Vivado XSim 2022.1 原生运行。主测试覆盖 8 组参数、86,777 个周期步骤、603,760 次检查,其中随机激励为 80,000 个周期。另有独立编写的测试与技术复核。没有进行综合、布局布线、静态时序分析或上板测试,不能据此给出资源占用或最高频率。
一、先规定四个边界
所有传输都在 clk 上升沿采样;复位期间不接受传输。
- 输入接受条件:
s_valid && s_ready。 - 输出消费条件:
m_valid && m_ready。 - 空队列不做输入到输出的组合直通。空时即使双方都准备好,这个上升沿也只接收新数据;新数据随后成为有效队头,最早在下一个上升沿被消费。
- 满队列允许同拍先消费旧队头、再接收新数据,容量保持不变。
m_data 是已存储队头的组合读出,只在 m_valid=1 时有传输意义。这里的“空时不直通”指没有输入旁路,不等于必须先发一个读请求才能看到队头。无效期间本文实现把 m_data 置零。
复位也要说准确:指针和 level 使用同步高有效复位,只有上升沿才清零;但 rst 同时组合屏蔽 s_ready、m_valid 和 m_data。因此 rst 拉高时接口立即无效,level 到复位上升沿才归零。集成时应把复位作为本时钟域的受约束信号,不能随意异步切换。
二、用占用量区分满与空
读写指针相等既可能表示空,也可能表示满。这里不浪费一个存储位置,而是单独维护 level,范围为 0..DEPTH,逻辑容量正好等于 DEPTH。
| 事件 | 下一拍占用量 |
|---|---|
| 只写入 | level + 1 |
| 只读出 | level - 1 |
| 同时读写 | level |
| 都不发生 | level |
| 复位上升沿 | 0 |
非 2 次幂深度必须显式回绕。例如 DEPTH=3 的合法地址只有 0,1,2。两位指针的自然溢出是 0,1,2,3,0,其中地址 3 已越界。正确规则是:等于 DEPTH-1 时回到 0,否则加一。
还要分别计算两种位宽:
- 指针位宽:至少 1 位,否则
DEPTH=1时$clog2(1)=0会造成非法或意外的端口范围。 - 计数位宽:使用
$clog2(DEPTH+1),因为计数需要表示DEPTH本身。例如深度 8 的指针是 3 位,计数却需要 4 位。
三、完整 RTL
保存为 sync_fifo.sv。这是 SystemVerilog 文件,需要使用 xvlog -sv 编译。
`timescale 1ns/1ps
module sync_fifo #(
parameter integer DATA_W = 8,
parameter integer DEPTH = 3,
localparam integer PTR_W = (DEPTH < 2) ? 1 : $clog2(DEPTH),
localparam integer COUNT_W = (DEPTH < 2) ? 1 : $clog2(DEPTH+1)
)(
input wire clk,
input wire rst,
input wire s_valid,
output wire s_ready,
input wire [DATA_W-1:0] s_data,
output wire m_valid,
input wire m_ready,
output wire [DATA_W-1:0] m_data,
output reg [COUNT_W-1:0] level
);
reg [DATA_W-1:0] mem [0:DEPTH-1];
reg [PTR_W-1:0] rd_ptr, wr_ptr;
wire push = s_valid && s_ready;
wire pop = m_valid && m_ready;
// Small-FIFO asynchronous read; this does not promise BRAM inference.
assign m_valid = !rst && (level != 0);
assign m_data = m_valid ? mem[rd_ptr] : {DATA_W{1'b0}};
// When full, a same-edge pop frees a slot for a replacement word.
assign s_ready = !rst && ((level < DEPTH) || m_ready);
function automatic [PTR_W-1:0] advance(input [PTR_W-1:0] p);
if (p == DEPTH-1) advance = '0;
else advance = p + 1'b1;
endfunction
always @(posedge clk) begin
if (rst) begin
rd_ptr <= '0;
wr_ptr <= '0;
level <= '0;
end else begin
if (push) begin
mem[wr_ptr] <= s_data;
wr_ptr <= advance(wr_ptr);
end
if (pop) rd_ptr <= advance(rd_ptr);
case ({push, pop})
2'b10: level <= level + 1'b1;
2'b01: level <= level - 1'b1;
default: level <= level;
endcase
end
end
// Simulation guard. Integrators must also enforce positive parameters.
// synthesis translate_off
initial begin
if (DATA_W < 1 || DEPTH < 1)
$fatal(1, "DATA_W and DEPTH must both be positive");
end
// synthesis translate_on
endmodule
DATA_W 与 DEPTH 都必须是正整数。文件中的检查用于仿真诊断,不是硬件保护。非法参数可能在展开端口、数组或复制表达式时就被工具拒绝,未必能运行到 $fatal。本次实测中,DEPTH=0 在仿真时间 0 被检查捕获,DATA_W=0 则在静态展开阶段因零次复制表达式被拒绝。
为什么满时可以继续 ready
level < DEPTH 时一定还有空间;当队列满且 m_ready=1 时,m_valid 也为 1,旧队头会在该上升沿离开,所以新输入可以占用释放的位置。这个优化建立在本文单时钟、同沿握手的契约上。
它带来一条 m_ready → s_ready 的组合路径。不要把该模块直接当成切断 ready 时序路径的寄存器切片,也不能不经检查就称它符合某一完整 AXI 接口规范。多级级联时需要检查组合路径长度和潜在组合环;本文没有做时序收敛验证。
DEPTH=1 为什么也成立
读写指针始终为 0。队列满且双方握手时,下游在上升沿采样旧的 mem[0],非阻塞赋值随后将新输入写入 mem[0],占用量仍为 1。这里依赖的是所给 RTL 的事件语义和组合读结构,不能直接类推为任何同步 RAM 原语的同地址读写模式。
复位为什么不清空整个数组
有效数据由 level 和指针界定,复位只需要把队列标记为空。旧存储内容不会在无效时作为有效输出。这样避免给每个存储单元都加复位逻辑,但这不是安全擦除,存储内容本身没有被清除。
四、用深度 3 手算一次满队列替换
下面表格是在 rst=0 下说明握手语义的手算例子,不是另一份仿真日志。输入数值用十进制表示;表中队列按“队头在左”排列。
| 上升沿前队列 | 输入 valid/data | 输出 ready | 本沿输出 | 上升沿后队列 |
|---|---|---|---|---|
| 空 | 1 / 11 | 1 | 无 | [11] |
| [11] | 1 / 22 | 0 | 无 | [11,22] |
| [11,22] | 1 / 33 | 0 | 无 | [11,22,33] |
| [11,22,33] | 1 / 44 | 0 | 无,输入也未被接受 | [11,22,33] |
| [11,22,33] | 1 / 44 | 1 | 11 | [22,33,44] |
| [22,33,44] | 0 / 无关 | 1 | 22 | [33,44] |
第 4 行被背压时,生产者要保持 s_valid 与数据 44,直到第 5 行真正握手。接收者可以改变 m_ready,但 FIFO 在 m_valid=1 && m_ready=0 期间必须保持同一队头。
五、完整自检测试平台
保存为 tb_sync_fifo.sv。参考模型使用顺序数组和移动元素维护队列,不复制 DUT 的环形指针实现;每一步先比较接口,再按期望握手更新参考队列,最后在非阻塞赋值生效后复查状态。
测试不是看一张波形“似乎正确”,而是逐步检查容量、输出顺序、队头、ready/valid 和复位行为。随机段使用固定种子的 xorshift32,源端遇到背压会保留未接受的数据,因此可重复运行。
`timescale 1ns/1ps
module fifo_case #(
parameter integer W = 8,
parameter integer D = 3,
parameter integer ID = 0
)(output logic done);
localparam integer CW = (D < 2) ? 1 : $clog2(D+1);
logic clk = 0, rst = 1, sv = 0, mr = 0;
logic [W-1:0] sd = '0;
wire sr, mv;
wire [W-1:0] md;
wire [CW-1:0] level;
sync_fifo #(.DATA_W(W), .DEPTH(D)) dut
(.clk(clk), .rst(rst), .s_valid(sv), .s_ready(sr), .s_data(sd),
.m_valid(mv), .m_ready(mr), .m_data(md), .level(level));
always #5 clk = ~clk;
logic [W-1:0] q [0:D];
integer n = 0, cycles = 0, checks = 0;
integer pushes = 0, pops = 0, full_replace = 0, stalls = 0;
integer empty_both = 0, resets_nonempty = 0;
integer j, k, i;
bit er, ev, push, pop, old_accept = 1;
bit gen_v;
logic [W-1:0] gen_data;
int unsigned rng = 32'h49a2d73b + ID;
function automatic int unsigned rand32();
rng = rng ^ (rng << 13);
rng = rng ^ (rng >> 17);
rng = rng ^ (rng << 5);
return rng;
endfunction
task automatic ck(input bit ok, input string what);
checks++;
if (!ok) $fatal(1,"FAIL case=%0d W=%0d D=%0d cycle=%0d n=%0d %s", ID,W,D,cycles,n,what);
endtask
task automatic step(input bit r, input bit v,
input logic [W-1:0] data, input bit ready);
@(negedge clk);
rst = r; sv = v; sd = data; mr = ready;
#1;
er = !r && ((n < D) || ready);
ev = !r && (n != 0);
ck(sr === er, "s_ready");
ck(mv === ev, "m_valid");
ck(level === n, "pre-edge level");
if (ev) ck(md === q[0], "head/order/stall data");
else ck(md === {W{1'b0}}, "invalid data must be zero");
push = v && er;
pop = ready && ev;
old_accept = er;
if (r && n != 0) resets_nonempty++;
if (!r && n==D && push && pop) full_replace++;
if (!r && ev && !ready) stalls++;
if (!r && n==0 && v && ready) empty_both++;
@(posedge clk);
if (r) n = 0;
else begin
if (pop) begin
for (j=0; j<n-1; j++) q[j] = q[j+1];
n--; pops++;
end
if (push) begin q[n] = data; n++; pushes++; end
end
#1;
ck(level === n, "post-edge level");
ck(mv === (!r && n!=0), "post-edge valid");
if (!r && n!=0) ck(md === q[0], "post-edge head");
cycles++;
endtask
initial begin
done=0;
// First edge establishes the synchronous reset before assertions.
@(posedge clk); #1;
step(1,1,'1,1);
step(0,1,'h21,1); // Empty: enqueue only, no fall-through dequeue.
step(0,0,'0,1);
repeat (30) begin
for (i=0;i<D;i++) step(0,1,i+17,0);
repeat (9) step(0,1,'hA5,0); // Full, held producer, stalled consumer.
step(0,1,'hA5,1); // Accept the held word first.
repeat (D+3) step(0,1,rand32(),1); // Full replacement.
for (i=0;i<D+2;i++) step(0,0,'0,1);
end
for (i=0;i<D;i++) step(0,1,i+73,0);
step(1,1,'1,1); // Flush nonempty queue and reject both handshakes.
gen_v=0; gen_data='0; old_accept=1;
for(k=0;k<10000;k++) begin
if (!gen_v || old_accept) begin
gen_v = (rand32()%4 != 0);
gen_data = {rand32(),rand32()};
end
step(0,gen_v,gen_data,(rand32()%5 < 3));
end
// Stop source only after its pending transfer has been accepted.
if(gen_v && !old_accept) step(0,gen_v,gen_data,1);
for(i=0;i<D+2;i++) step(0,0,'0,1);
ck(n==0,"final drain");
ck(full_replace>0 && stalls>0 && empty_both>0 && resets_nonempty>0,"coverage");
$display("PASS case=%0d W=%0d D=%0d cycles=%0d checks=%0d pushes=%0d pops=%0d full_replace=%0d stalls=%0d empty_both=%0d resets_nonempty=%0d", ID,W,D,cycles,checks,pushes,pops,full_replace,stalls,empty_both,resets_nonempty);
done=1;
end
endmodule
module tb_sync_fifo;
wire [7:0] done;
fifo_case #(.W(1), .D(1), .ID(0)) c0(done[0]);
fifo_case #(.W(8), .D(3), .ID(1)) c1(done[1]);
fifo_case #(.W(8), .D(5), .ID(2)) c2(done[2]);
fifo_case #(.W(16),.D(8), .ID(3)) c3(done[3]);
fifo_case #(.W(33),.D(1),.ID(4)) c4(done[4]);
fifo_case #(.W(1), .D(3), .ID(5)) c5(done[5]);
fifo_case #(.W(16),.D(5),.ID(6)) c6(done[6]);
fifo_case #(.W(33),.D(8),.ID(7)) c7(done[7]);
initial begin wait (&done); $display("ALL_PASS"); $finish; end
initial begin #2000000; $fatal(1,"TIMEOUT"); end
endmodule
在已经配置好 Vivado 命令行环境的 PowerShell 中运行:
xvlog -sv sync_fifo.sv tb_sync_fifo.sv
if ($LASTEXITCODE -ne 0) { throw "xvlog failed" }
xelab tb_sync_fifo -s fifo_test -mt off
if ($LASTEXITCODE -ne 0) { throw "xelab failed" }
xsim fifo_test -runall | Tee-Object simulation.txt
if ($LASTEXITCODE -ne 0) { throw "xsim failed" }
$log = Get-Content simulation.txt -Raw
if ($log -notmatch '(?m)^ALL_PASS\s*$' -or $log -match 'Fatal:|ERROR:') {
throw "Functional test did not pass; inspect simulation.txt"
}
不要只检查进程退出码。本次 XSim 的部分 $fatal 失败运行也返回了 0,因此还必须检查明确的成功标记,并排除失败信息。正常结束应有 8 条 PASS case=... 和最后的 ALL_PASS。
本次主测试结果
下表直接由本次原生运行日志提取。周期是各配置执行的 step 次数;检查数包含一个周期内的多项断言,不等于独立测试用例数。
| DATA_W | DEPTH | 周期步骤 | 检查次数 | 满时同拍读写次数 |
|---|---|---|---|---|
| 1 | 1 | 10,549 | 72,567 | 4,604 |
| 8 | 3 | 10,732 | 74,773 | 3,316 |
| 8 | 5 | 10,916 | 76,273 | 3,255 |
| 16 | 8 | 11,192 | 78,241 | 3,164 |
| 33 | 1 | 10,548 | 72,550 | 4,517 |
| 1 | 3 | 10,732 | 74,832 | 3,493 |
| 16 | 5 | 10,916 | 76,281 | 3,404 |
| 33 | 8 | 11,192 | 78,243 | 3,407 |
合计 86,777 个周期步骤、603,760 次检查。每配置有 10,000 个随机周期,其余为定向填满、连续背压、满时替换、排空和非空复位等步骤。写入次数与读出次数不必完全相等,因为复位会按契约丢弃队列中尚未消费的数据。
另外注入了 3 类错误,确认测试会失败,而不是只会输出 PASS:
- 把回绕条件改成指针全 1:在深度 1 触发失败;单独运行深度 3 时也在第 16 号步骤发现队头错误。
- 满时即使下游 ready 也禁止输入:深度 1 测试立即发现
s_ready与契约不符。 - 只要输出 valid 就推进读指针:深度 3 的背压测试发现队头改变。
这些故障版本都先完成编译和展开,再被功能检查捕获。它们属于负向验证,不是最终 RTL 的失败。最终代码使用上文版本。另一个独立编写的测试平台也对深度 1、3、5、8 完成原生仿真;独立复核没有发现阻断性 RTL 问题。
六、集成前再检查三件事
- **时钟域。**该模块只能连接同一时钟域,不具备亚稳态处理和跨时钟域安全性。
- **存储结构。**这里是组合读队头的 RTL,适合小 FIFO 的教学与原型。换成同步读 Block RAM,需要重新设计读延迟、输出预取和满时同地址读写语义,不能只替换数组声明。
- **实现证据。**仿真验证的是给定激励下的逻辑行为。实际资源、时钟频率、器件读写模式和复位时序,仍需在目标工程完成综合、实现与硬件验证。
任意深度 FIFO 的关键不是多写一个参数,而是让容量、指针回绕、计数位宽和同拍读写在同一份接口契约下成立。先把深度 1、3、5 跑通,往往比只验证深度 16 更容易暴露边界错误。
参考:AMD《Vivado Design Suite User Guide: Logic Simulation (UG900)》2022.1,Vivado Simulator Quick Reference Guide。本文的 FIFO 实现与测试平台为围绕上述契约编写的示例,官方文档引用仅用于仿真工具用法。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/hxy_programmer/article/details/170230434




