// ============================================================
// 同步 FIFO (视见数量 / 水位线版)
//
// 设计依据
// 写端视见数据量 WCNT = WPTR - Delayed(RPTR) = 实际 + delta -> 写端多看数据、少看空位
// 读端视见数据量 RCNT = Delayed(WPTR) - RPTR = 实际 - delta -> 读端少看数据、多看空位
// 规定:full 类水线按【数据视见量】比较;
// empty 类水线按【空位视见量 = DEPTH - 视见数据量】比较。
//
// 推论(本设计的安全性质):
// 写端 o_wempty / o_waempty 一定是“真空”(可连续写);
// 写端 o_wfull / o_wafull 不一定“真满”(保守,会提前反压)。
// 读端 o_rfull / o_rafull 一定是“真满”(可连续读);
// 读端 o_rempty / o_raempty 不一定“真空”(保守,会提前报空)。
//
// 实现说明:
// 内部保留【精确计数 r_fifo_number】用于读写控制(保证不丢数/不重复读);
// 视见数量用“上一拍是否真的发生读/写”来构造(数学上等价于另一侧指针延迟一拍):
// WCNT = 精确计数 + 上一拍读 (写端把上一拍读“补回来”)
// RCNT = 精确计数 - 上一拍写 (读端把上一拍写“扣掉”)
// 该写法与 FIFO 深度是否 2 的幂无关。
// ============================================================
module sync_fifo
#(
parameter DATA_DEPTH = 16, // FIFO 深度
parameter DATA_WIDTH = 32, // 数据位宽
// ---- 写端口水线 ----
// full 类:按【数据视见量 WCNT】比较
// empty 类:按【空位视见量 DEPTH-WCNT】比较,等价于“剩余数据视见量 <= 此值”
parameter W_AEMPTY_THRESH = 1, // WCNT <= 此值 -> o_waempty
parameter W_AFULL_THRESH = 2, // WCNT >= DEPTH - 此值 -> o_wafull
// ---- 读端口水线 ----
parameter R_AEMPTY_THRESH = 3, // RCNT <= 此值 -> o_raempty
parameter R_AFULL_THRESH = 3 // RCNT >= DEPTH - 此值 -> o_rafull
)
(
/* ---- 系统 ---- */
input i_sys_clk,
input i_sys_rst_n,
/* ---- 写端口 ---- */
input i_wren,
input [DATA_WIDTH-1:0] i_wdata,
/* ---- 读端口 ---- */
input i_rden,
output [DATA_WIDTH-1:0] o_rdata,
/* ---- 精确空满(内部基线,供读写控制/兼容)---- */
output o_full, // 精确满
output o_empty, // 精确空
/* ---- 写端口:视见数量状态 ---- */
output o_wfull, // 写端视见满(可能保守,不一定真满)
output o_wempty, // 写端视见空(一定真空,可连续写)
output o_waempty, // write almost empty
output o_wafull, // write almost full
output o_whempty, // write half empty
output o_whfull, // write half full
/* ---- 读端口:视见数量状态 ---- */
output o_rfull, // 读端视见满(一定真满,可连续读)
output o_rempty, // 读端视见空(可能保守,不一定真空)
output o_raempty, // read almost empty
output o_rafull, // read almost full
output o_rhempty, // read half empty
output o_rhfull // read half full
);
localparam ADDR_W = $clog2(DATA_DEPTH); // 地址位宽
localparam CNT_W = $clog2(DATA_DEPTH)+1; // 计数位宽(需能数到 DEPTH)
localparam HALF = DATA_DEPTH / 2; // 一半分界
reg [DATA_WIDTH-1:0] mem_ram [0:DATA_DEPTH-1];
reg [ADDR_W-1:0] r_wr_ptr;
reg [ADDR_W-1:0] r_rd_ptr;
reg [CNT_W-1:0] r_fifo_number; // 精确计数:内部读写控制用
reg [DATA_WIDTH-1:0] r_rdata; // 读数据输出(打一拍)
integer i;
// ---- 精确空满:仅用于内部读写控制,保证不丢数 ----
wire full_exact = (r_fifo_number == DATA_DEPTH);
wire empty_exact = (r_fifo_number == {CNT_W{1'b0}});
// 有效读写:精确满时写被忽略,精确空时读被忽略
wire wren_eff = i_wren && !full_exact;
wire rden_eff = i_rden && !empty_exact;
// ---- 视见数量的“延迟”来源:上一拍是否真的发生读/写 ----
// 等价于“另一侧指针延迟一拍”:写端看不到本拍刚发生的读,读端看不到本拍刚发生的写
reg r_rd_pend;
reg r_wr_pend;
always @(posedge i_sys_clk or negedge i_sys_rst_n)
begin
if(!i_sys_rst_n)
begin
r_rd_pend <= 1'b0;
r_wr_pend <= 1'b0;
end
else
begin
r_rd_pend <= rden_eff;
r_wr_pend <= wren_eff;
end
end
// 写端视见数据量 WCNT = 精确计数 + 上一拍读(上限饱和到 DEPTH)
// 读端视见数据量 RCNT = 精确计数 - 上一拍写(下限饱和到 0)
wire [CNT_W-1:0] wcnt_inc = r_fifo_number + CNT_W'(r_rd_pend);
wire [CNT_W-1:0] rcnt_dec = r_fifo_number - CNT_W'(r_wr_pend);
wire [CNT_W-1:0] wcnt = (wcnt_inc > DATA_DEPTH) ? DATA_DEPTH : wcnt_inc;
wire [CNT_W-1:0] rcnt = (r_fifo_number == {CNT_W{1'b0}}) ? {CNT_W{1'b0}} : rcnt_dec;
// 空位视见量(empty 类水线按此比较)
wire [CNT_W-1:0] wfree = CNT_W'(DATA_DEPTH) - wcnt;
wire [CNT_W-1:0] rfree = CNT_W'(DATA_DEPTH) - rcnt;
// 写指针:仅负责写地址,到达末尾回卷,非 2 的幂深度也安全
always @(posedge i_sys_clk or negedge i_sys_rst_n)
begin
if(!i_sys_rst_n)
r_wr_ptr <= {ADDR_W{1'b0}};
else if(wren_eff)
r_wr_ptr <= (r_wr_ptr == DATA_DEPTH - 1'b1) ? {ADDR_W{1'b0}} : r_wr_ptr + 1'b1;
end
// 读指针
always @(posedge i_sys_clk or negedge i_sys_rst_n)
begin
if(!i_sys_rst_n)
r_rd_ptr <= {ADDR_W{1'b0}};
else if(rden_eff)
r_rd_ptr <= (r_rd_ptr == DATA_DEPTH - 1'b1) ? {ADDR_W{1'b0}} : r_rd_ptr + 1'b1;
end
// 写数据进 RAM
always @(posedge i_sys_clk or negedge i_sys_rst_n)
begin
if(!i_sys_rst_n)
begin
for(i = 0; i < DATA_DEPTH; i = i + 1)
mem_ram[i] <= 'd0;
end
else if(wren_eff)
mem_ram[r_wr_ptr] <= i_wdata;
end
// 读数据(输出打一拍):标准 FIFO,读使能后下一拍出数据
always @(posedge i_sys_clk or negedge i_sys_rst_n)
begin
if(!i_sys_rst_n)
r_rdata <= {DATA_WIDTH{1'b0}};
else if(rden_eff)
r_rdata <= mem_ram[r_rd_ptr];
end
// 精确计数:一条语句统吃 同时读写 / 只写 / 只读
always @(posedge i_sys_clk or negedge i_sys_rst_n)
begin
if(!i_sys_rst_n)
r_fifo_number <= {CNT_W{1'b0}};
else
r_fifo_number <= r_fifo_number + CNT_W'(wren_eff) - CNT_W'(rden_eff);
end
// ---- 输出:精确基线 ----
assign o_rdata = r_rdata;
assign o_full = full_exact;
assign o_empty = empty_exact;
// ---- 写端口状态标志(全部基于 WCNT / 空位视见量 wfree)----
assign o_wfull = (wcnt >= DATA_DEPTH);
assign o_wempty = (wcnt == {CNT_W{1'b0}});
assign o_wafull = (wcnt >= (DATA_DEPTH - W_AFULL_THRESH));
assign o_waempty = (wfree >= (DATA_DEPTH - W_AEMPTY_THRESH));
assign o_whfull = (wcnt >= HALF);
assign o_whempty = (wfree > HALF);
// ---- 读端口状态标志(全部基于 RCNT / 空位视见量 rfree)----
assign o_rfull = (rcnt >= DATA_DEPTH);
assign o_rempty = (rcnt == {CNT_W{1'b0}});
assign o_rafull = (rcnt >= (DATA_DEPTH - R_AFULL_THRESH));
assign o_raempty = (rfree >= (DATA_DEPTH - R_AEMPTY_THRESH));
assign o_rhfull = (rcnt >= HALF);
assign o_rhempty = (rfree > HALF);
endmodule
`timescale 1ns / 1ps
// ============================================================
// sync_fifo 测试平台(零警告版)
// 覆盖:复位状态 / 写满 / 读空 / 同时读写 / half 阈值翻转
// 数据宽度用 8bit 方便看波形;深度 16 与 DUT 默认一致
// 编译:iverilog -g2012 -o main.out .\sync_fifo.v .\tb_sync_fifo.v
// 运行:vvp main.out
// 波形:仿真后打开 tb_sync_fifo.vcd
//
// 【为什么不用 exp_q [$] + pop_front()】
// SV 队列的 pop_front() 是返回 void 的方法,直接当语句调用时
// iverilog 会报 "method function 'pop_front' is being called as a
// task" 警告;而本机这版 iverilog(12.0) 又不支持 void'() 强转、
// 也不支持队列切片 q[1:$],改法都会变成语法错误。
// 故改用「纯 Verilog 数组 + 头/尾指针」做期望值队列,所有版本通用。
//
// 【关键时序说明(新手必看)】
// DUT 在时钟上升沿采样 i_wren/i_rden,并在同一拍用非阻塞赋值更新
// 内部计数和 o_rdata。因此 tb 里:
// - 写:拉高 wren 等一拍(边沿采样写)->再等一拍让计数稳定;
// - 读:拉高 rden 等一拍(边沿采样读)->再等一拍 o_rdata 才有效;
// - 任何 burst 结束后,要多等一拍再检查 o_full/o_empty/状态标志,
// 否则读到的是更新前(旧)的值,会误报失败。
// ============================================================
module tb_sync_fifo;
localparam DATA_DEPTH = 16;
localparam DATA_WIDTH = 8;
// ---- 接口信号 ----
reg i_sys_clk;
reg i_sys_rst_n;
reg i_wren;
reg [DATA_WIDTH-1:0] i_wdata;
reg i_rden;
wire [DATA_WIDTH-1:0] o_rdata;
wire o_full;
wire o_empty;
wire o_waempty;
wire o_wafull;
wire o_whempty;
wire o_whfull;
wire o_raempty;
wire o_rafull;
wire o_rhempty;
wire o_rhfull;
// 视见数量版新增端口
wire o_wfull;
wire o_wempty;
wire o_rfull;
wire o_rempty;
// ---- 自校验用:期望值用「数组 + 头尾指针」实现队列(不用 SV 队列)----
integer exp_mem [0:1023]; // 存放期望读出的数据
// ---- 实例化 DUT ----
sync_fifo #(
.DATA_DEPTH (DATA_DEPTH),
.DATA_WIDTH (DATA_WIDTH),
.W_AEMPTY_THRESH (1),
.W_AFULL_THRESH (2),
.R_AEMPTY_THRESH (3),
.R_AFULL_THRESH (3)
) u_fifo (
.i_sys_clk (i_sys_clk),
.i_sys_rst_n(i_sys_rst_n),
.i_wren (i_wren),
.i_wdata (i_wdata),
.i_rden (i_rden),
.o_rdata (o_rdata),
.o_full (o_full),
.o_empty (o_empty),
.o_wfull (o_wfull),
.o_wempty (o_wempty),
.o_rfull (o_rfull),
.o_rempty (o_rempty),
.o_waempty (o_waempty),
.o_wafull (o_wafull),
.o_whempty (o_whempty),
.o_whfull (o_whfull),
.o_raempty (o_raempty),
.o_rafull (o_rafull),
.o_rhempty (o_rhempty),
.o_rhfull (o_rhfull)
);
// ---- 时钟 10ns(5ns 半周期)----
always #5 i_sys_clk = ~i_sys_clk;
// ---- 波形 dump ----
initial begin
$dumpfile("tb_sync_fifo.vcd");
$dumpvars(0, tb_sync_fifo);
end
// ---- 任务:写一笔(满则丢弃并报告)----
// 时序:拉高 wren -> 等一拍(DUT 采样并写入) -> 拉低 -> 再多等一拍让计数稳定
integer exp_tail; // 指向下一个空闲槽位
task write_one;
input [DATA_WIDTH-1:0] d;
begin
if(o_full) begin
@(posedge i_sys_clk);
$display("[%0t] WRITE blocked (full): data=%0d dropped", $time, d);
i_wren <= 1'b0;
@(posedge i_sys_clk);
end else begin
i_wdata <= d;
i_wren <= 1'b1;
@(posedge i_sys_clk); // DUT 在此拍采样 wren=1 -> 写入
i_wren <= 1'b0;
@(posedge i_sys_clk); // 计数 NBA 已稳定
exp_mem[exp_tail] = d; exp_tail = exp_tail + 1; // 入队
$display("[%0t] WRITE data=%0d", $time, d);
end
end
endtask
integer errors;//错误计数,
integer exp_head; // 指向下一个待比较元素
integer got;
// ---- 任务:读一笔(空则跳过并报告)----进行一个验证,判断是否可读
// 时序:拉高 rden -> 等一拍(DUT 采样并读出) -> 拉低 -> 再等一拍 o_rdata 才有效
task read_one;
reg blocked;//是否可读的信号
begin
@(posedge i_sys_clk);
blocked = o_empty;
i_rden <= !o_empty;
@(posedge i_sys_clk); // DUT 在此拍采样 rden=1 -> 读出,o_rdata 更新(NBA)
i_rden <= 1'b0;
@(posedge i_sys_clk); // o_rdata 此刻已是本拍读出的数据
if(!blocked)
begin
got = o_rdata;
if(exp_tail > exp_head) begin
if(exp_mem[exp_head] !== got) begin
$display(" *** DATA ERROR: expected %0d got %0d", exp_mem[exp_head], got);
errors = errors + 1;//预期的数和读的到数不一样,错误计数加一
end else begin
$display(" data OK: %0d", got);
end
exp_head = exp_head + 1; // 出队
end
end else begin
$display("[%0t] READ blocked (empty)", $time);
end
end
endtask
// ---- 测试主流程 ----
initial begin
i_sys_clk = 0;
i_sys_rst_n = 0;
i_wren = 0; i_rden = 0; i_wdata = 0;
errors = 0;
exp_head = 0; // 必须显式初始化:integer 默认是 x,不是 0
exp_tail = 0;
#20;
i_sys_rst_n = 1;
// ---------- TEST1: 复位状态 ----------
$display("\n=== TEST1: reset state ===");
@(posedge i_sys_clk);
@(posedge i_sys_clk);
$display("empty=%b full=%b waempty=%b wafull=%b raempty=%b rafull=%b whempty=%b whfull=%b",
o_empty,o_full,o_waempty,o_wafull,o_raempty,o_rafull,o_whempty,o_whfull);
if(o_empty !== 1'b1 || o_full !== 1'b0) begin
$display(" *** TEST1 FAIL: reset state wrong"); errors = errors + 1;
end else $display(" TEST1 PASS");
// ---------- TEST2: 连续写满 ----------
$display("\n=== TEST2: write until full ===");
begin: wloop
integer k;
for(k = 0; k < DATA_DEPTH; k = k + 1) write_one(k);
end
@(posedge i_sys_clk); // 等计数稳定
$display("after fill: empty=%b full=%b wafull=%b rafull=%b", o_empty,o_full,o_wafull,o_rafull);
if(o_full !== 1'b1) begin
$display(" *** TEST2 FAIL: not full after %0d writes", DATA_DEPTH); errors = errors + 1;
end else $display(" TEST2 PASS (full reached, no overflow)");
// 再多写 1 笔,应被挡掉
write_one(8'd99);
if((exp_tail - exp_head) !== DATA_DEPTH) begin
$display(" *** TEST2 FAIL: overflow data accepted"); errors = errors + 1;
end
// ---------- TEST3: 连续读空 ----------
$display("\n=== TEST3: read until empty ===");
begin: rloop
integer k;
for(k = 0; k < DATA_DEPTH; k = k + 1) read_one();
end
@(posedge i_sys_clk); // 等计数稳定
$display("after drain: empty=%b full=%b", o_empty,o_full);
if(o_empty !== 1'b1) begin
$display(" *** TEST3 FAIL: not empty after %0d reads", DATA_DEPTH); errors = errors + 1;
end else $display(" TEST3 PASS (empty reached, data order correct)");
// 再多读 1 笔,应被挡掉
read_one();
// ---------- TEST4: 同时读写(稳态)----------
$display("\n=== TEST4: simultaneous read/write (steady state) ===");
// 先填到一半
begin: fill_half
integer k;
for(k = 0; k < 8; k = k + 1) write_one(k);
end
@(posedge i_sys_clk);
$display("after half-fill: whempty=%b whfull=%b (expect 0/1)", o_whempty, o_whfull);
if(o_whfull !== 1'b1 || o_whempty !== 1'b0) begin
$display(" *** TEST4 FAIL: half flags wrong at count=8"); errors = errors + 1;
end else $display(" half flags OK");
// 并发读写各 10 笔,净变化应为 0,count 维持 ~8
fork
begin: wsim
integer k;
for(k = 100; k < 110; k = k + 1) write_one(k);
end
begin: rsim
integer k;
for(k = 0; k < 10; k = k + 1) read_one();
end
join
@(posedge i_sys_clk);
$display("after sim r/w: empty=%b full=%b whempty=%b whfull=%b",
o_empty,o_full,o_whempty,o_whfull);
$display(" TEST4 PASS (simultaneous r/w handled, data order correct)");
// 把剩下的读空,回到 empty
begin: drain_rest
integer k;
for(k = 0; k < 8; k = k + 1) read_one();
end
@(posedge i_sys_clk);
// ---------- TEST5: almost 阈值翻转 ----------
$display("\n=== TEST5: almost_empty / almost_full threshold toggle ===");
// 当前 empty。写 1 笔 -> count=1 -> waempty(<=1)=1
write_one(8'd11);
@(posedge i_sys_clk);
$display("count=1: waempty=%b raempty=%b (expect 1/1)", o_waempty, o_raempty);
if(o_waempty !== 1'b1 || o_raempty !== 1'b1) begin
$display(" *** TEST5 FAIL: almost_empty not set at count=1"); errors = errors + 1;
end else $display(" almost_empty OK");
// 再写到 count=14 -> wafull(>=14)=1, rafull(>=13)=1
begin: fill14
integer k;
for(k = 0; k < 13; k = k + 1) write_one(8'd20 + k);
end
@(posedge i_sys_clk);
$display("count=14: wafull=%b rafull=%b (expect 1/1)", o_wafull, o_rafull);
if(o_wafull !== 1'b1 || o_rafull !== 1'b1) begin
$display(" *** TEST5 FAIL: almost_full not set at count=14"); errors = errors + 1;
end else $display(" almost_full OK");
// ---------- TEST6: 视见数量语义(保守性验证)----------
$display("\n=== TEST6: visible-count semantics (conservative flags) ===");
begin: clr6
integer k;
for(k = 0; k < DATA_DEPTH + 2; k = k + 1) read_one();
end
@(posedge i_sys_clk);
$display("drained: exact_empty=%b", o_empty);
// 写 1 笔后【立刻采样】:
// 实际=1(精确empty=0),读端视见量 RCNT=实际-上一拍写=0 -> o_rempty=1
// 即:读端视见空是保守的,不一定真空
write_one(8'd55);
$display("1 write : exact_empty=%b o_rempty=%b o_wempty=%b (expect 0 / 1 / 0)",
o_empty, o_rempty, o_wempty);
if(o_empty !== 1'b0 || o_rempty !== 1'b1) begin
$display(" *** TEST6 FAIL: read-side o_rempty not conservative"); errors = errors + 1;
end else $display(" read-side o_rempty conservative OK");
// 读 1 笔后【立刻采样】:
// 实际=0(精确empty=1),写端视见量 WCNT=实际+上一拍读=1 -> o_wempty=0
// 即:写端视见空不会误报;反过来它一旦报空一定真空
read_one();
$display("1 read : exact_empty=%b o_wempty=%b o_rempty=%b (expect 1 / 0 / 1)",
o_empty, o_wempty, o_rempty);
if(o_empty !== 1'b1 || o_wempty !== 1'b0) begin
$display(" *** TEST6 FAIL: write-side o_wempty falsely asserted"); errors = errors + 1;
end else $display(" write-side o_wempty safe OK");
// ---------- 收尾 ----------
#50;
$display("\n=== DONE: total errors = %0d ===", errors);
if(errors == 0) $display(">>> ALL TESTS PASSED");
else $display(">>> SOME TESTS FAILED");
$finish;
end
endmodule