4. 同步FIFO格雷码指针
任务目标
在同步FIFO设计中,读写指针采用格雷码编码,替代传统二进制指针。格雷码相邻计数值仅1比特变化,可显著降低多比特同时翻转带来的亚稳态风险(尽管同步FIFO本身无跨时钟域问题,但该设计为后续异步FIFO打基础,且便于观察资源开销差异)。本案例实现一个深度16、数据宽度8的同步FIFO,完成读写功能验证,并对比格雷码与二进制指针的资源占用。
设计要点
- 指针宽度:深度16需4位地址,格雷码指针宽度同为4位(最高位用于区分空/满状态时需扩展1位,共5位)。
- 空满判断:读写指针格雷码相等为空;写指针格雷码与读指针格雷码高2位取反、低2位相同为满(针对深度为2的幂次方)。
- 指针转换:二进制指针递增后转换为格雷码存储,比较时直接使用格雷码值。
完整实现(FIFO模块 + 测试平台)
// sync_fifo_gray.v
module sync_fifo_gray #(
parameter DATA_WIDTH = 8,
parameter DEPTH = 16
)(
input wire clk,
input wire rst_n,
input wire wr_en,
input wire [DATA_WIDTH-1:0] wr_data,
input wire rd_en,
output wire [DATA_WIDTH-1:0] rd_data,
output wire full,
output wire empty
);
// 地址宽度(深度16需4位,扩展1位用于空满判断)
localparam ADDR_WIDTH = $clog2(DEPTH) + 1; // 5位
reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];
reg [ADDR_WIDTH-1:0] wr_ptr_gray, rd_ptr_gray;
reg [ADDR_WIDTH-2:0] wr_ptr_bin, rd_ptr_bin; // 4位二进制指针
wire [ADDR_WIDTH-2:0] wr_addr = wr_ptr_bin;
wire [ADDR_WIDTH-2:0] rd_addr = rd_ptr_bin;
// 二进制指针递增
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wr_ptr_bin <= 0;
rd_ptr_bin <= 0;
end else begin
if (wr_en && !full) wr_ptr_bin <= wr_ptr_bin + 1'b1;
if (rd_en && !empty) rd_ptr_bin <= rd_ptr_bin + 1'b1;
end
end
// 二进制转格雷码(扩展位参与转换)
always @(*) begin
wr_ptr_gray = {wr_ptr_bin[ADDR_WIDTH-2], wr_ptr_bin ^ (wr_ptr_bin >> 1)};
rd_ptr_gray = {rd_ptr_bin[ADDR_WIDTH-2], rd_ptr_bin ^ (rd_ptr_bin >> 1)};
end
// 空满判断(格雷码比较)
assign empty = (wr_ptr_gray == rd_ptr_gray);
assign full = (wr_ptr_gray[ADDR_WIDTH-1] != rd_ptr_gray[ADDR_WIDTH-1]) &&
(wr_ptr_gray[ADDR_WIDTH-2] != rd_ptr_gray[ADDR_WIDTH-2]) &&
(wr_ptr_gray[ADDR_WIDTH-3:0] == rd_ptr_gray[ADDR_WIDTH-3:0]);
// 写数据
always @(posedge clk) begin
if (wr_en && !full) mem[wr_addr] <= wr_data;
end
// 读数据(组合逻辑输出)
assign rd_data = mem[rd_addr];
endmodule
// tb_sync_fifo_gray.v
`timescale 1ns/1ps
module tb_sync_fifo_gray;
reg clk = 0, rst_n = 0;
reg wr_en = 0, rd_en = 0;
reg [7:0] wr_data = 0;
wire [7:0] rd_data;
wire full, empty;
sync_fifo_gray uut (.clk(clk), .rst_n(rst_n), .wr_en(wr_en), .wr_data(wr_data),
.rd_en(rd_en), .rd_data(rd_data), .full(full), .empty(empty));
always #5 clk = ~clk;
// 测试序列
initial begin
// 复位
#20 rst_n = 1;
// 写入16个数据(填满FIFO)
repeat (16) begin
@(posedge clk);
wr_en = 1;
wr_data = wr_data + 1;
end
@(posedge clk) wr_en = 0;
$display("After write 16: full=%b empty=%b", full, empty); // 预期 full=1
// 读出16个数据(清空FIFO)
repeat (16) begin
@(posedge clk);
rd_en = 1;
end
@(posedge clk) rd_en = 0;
$display("After read 16: full=%b empty=%b", full, empty); // 预期 empty=1
// 边写边读测试(写入5个同时读出5个)
repeat (5) begin
@(posedge clk);
wr_en = 1; rd_en = 1;
wr_data = wr_data + 1;
end
@(posedge clk) wr_en = 0; rd_en = 0;
$display("After simultaneous: full=%b empty=%b", full, empty); // 预期 full=0 empty=0
$finish;
end
// 监控读数据
always @(posedge clk) begin
if (rd_en && !empty) $display("Read data: %0d", rd_data);
end
endmodule
运行与验证
# 编译与仿真(使用Icarus Verilog)
iverilog -o sim_vvp sync_fifo_gray.v tb_sync_fifo_gray.v
vvp sim_vvp
通过条件:仿真输出显示写入16个数据后 full=1,读出16个数据后 empty=1,边写边读时 full=0 empty=0,且读出的数据顺序与写入顺序一致(0~15,再0~4)。
资源对比分析
在Vivado或Quartus中对两种实现(格雷码指针 vs 二进制指针)分别综合,观察资源报告。格雷码方案额外增加约ADDR_WIDTH个异或门用于二进制转格雷码,但寄存器数量不变。对于深度16的FIFO,格雷码方案仅多消耗约5个LUT,而异步FIFO中该设计可消除多比特跨时钟域亚稳态风险,收益远大于开销。
常见问题
- 空满判断错误:确保格雷码指针位宽为
ADDR_WIDTH(含扩展位),且满条件中高2位取反比较,低ADDR_WIDTH-2位相等。 - 读数据时序:本设计为组合逻辑读(
rd_data直接由地址译码),若需同步读需在输出加寄存器。 - 深度非2的幂次:格雷码空满判断仅适用于深度为2的幂次方,其他深度需额外计数器辅助。