Quick Start
以下步骤在 Vivado 2026.1 与 Modelsim SE-64 2025.1 环境下,基于 Xilinx Artix-7 XC7A35T 开发板,实现一个 8-bit 数据宽度、16 深度、格雷码指针同步的异步 FIFO。从创建工程到上板验证,总耗时约 30 分钟。
- 新建 Vivado 工程,选择器件 xc7a35tcsg324-1,添加顶层文件 async_fifo_top.v、FIFO 核心模块 async_fifo.v、双端口 RAM 模块 dpram.v 与同步器模块 sync_bits.v。
- 编写约束文件 async_fifo.xdc,为 wr_clk 与 rd_clk 分别指定主时钟周期(例如 50MHz 与 25MHz),并设置异步时钟组:
set_clock_groups -asynchronous -group [get_clocks -include_generated_clocks wr_clk] -group [get_clocks -include_generated_clocks rd_clk]。 - 运行综合(Synthesis),检查无关键警告(Critical Warning)或 Latch 推断。
- 运行实现(Implementation),检查时序报告:确保 wr_clk 域到 rd_clk 域的路径被标记为“False Path”或“Asynchronous Clock Group”,无 setup/hold 违规。
- 编写 Testbench tb_async_fifo.v,实例化 FIFO,模拟写满、读空、同时读写场景,运行仿真至少 10 us。
- 观察波形:写指针与读指针在跨时钟域时保持格雷码编码,同步后无毛刺;full 与 empty 标志在正确时钟沿跳变。
- 上板验证:将 FIFO 输出连接到 8 个 LED,通过按键模拟写使能与读使能,观察 LED 数据流是否按预期顺序显示。
- 验收点:当写使能连续 16 次后 full 拉高;读使能连续 16 次后 empty 拉高;同时读写时数据不丢失。
前置条件与环境
| 项目 | 推荐值 | 说明 | 替代方案 |
|---|---|---|---|
| 器件/板卡 | Xilinx Artix-7 XC7A35T-1CSG324C | 主流低功耗 FPGA,资源适中 | Intel Cyclone IV / Lattice ECP5 |
| EDA 版本 | Vivado 2026.1 | 支持最新综合策略与异步时钟组约束 | Vivado 2024.2 / 2025.2 |
| 仿真器 | Modelsim SE-64 2025.1 | 支持 VHDL/Verilog 混合仿真 | Vivado Simulator / Questa |
| 时钟/复位 | wr_clk=50MHz, rd_clk=25MHz, 异步复位低有效 | 典型异步 FIFO 跨时钟域场景 | 任意频率比,建议 wr_clk ≥ rd_clk |
| 接口依赖 | 无外部 IP,纯 RTL 实现 | 所有模块手写,便于理解与移植 | 可使用 XPM_FIFO(Xilinx 原语) |
| 约束文件 | async_fifo.xdc | 必须包含异步时钟组与 false path 约束 | SDC 格式(Synopsys) |
目标与验收标准
- 功能正确性:写满标志(full)在 FIFO 存满 16 个数据后立即拉高;读空标志(empty)在 FIFO 无数据后立即拉高;同时读写时数据不丢失、不重复。
- 时序安全:跨时钟域路径(写指针到读时钟域、读指针到写时钟域)被综合工具识别为异步,无 setup/hold 违规;格雷码编码保证同步后最多一位变化。
- 资源与性能:在 Artix-7 上实现,Fmax(写时钟)≥ 200MHz,Fmax(读时钟)≥ 200MHz,LUT 消耗 ≤ 120,FF 消耗 ≤ 100,Block RAM 使用 1 个(16×8 配置)。
- 可移植性:RTL 代码无任何厂商原语(除 RAM 推断外),可移植到 Intel/Lattice/ASIC 流程。
实施步骤
工程结构与模块划分
- 顶层模块 async_fifo_top.v:实例化 FIFO 核心,连接外部端口(wr_clk, rd_clk, rst_n, wr_en, rd_en, data_in, data_out, full, empty)。
- FIFO 核心 async_fifo.v:包含双端口 RAM、写指针逻辑、读指针逻辑、空/满标志生成、同步器实例化。
- 双端口 RAM dpram.v:深度 16,位宽 8,写使能同步于 wr_clk,读使能同步于 rd_clk。
- 同步器 sync_bits.v:两级触发器链,用于将格雷码指针同步到目标时钟域。
- Testbench tb_async_fifo.v:生成两个独立时钟,模拟写/读操作,自动比对数据。
关键模块 RTL 实现
以下给出 async_fifo.v 的核心代码。为突出重点,省略了 RAM 实例化与同步器实例化的细节,完整代码请参考附录。
// async_fifo.v - 核心模块
module async_fifo #(
parameter DATA_WIDTH = 8,
parameter FIFO_DEPTH = 16,
parameter PTR_WIDTH = 4 // 2^4 = 16
)(
input wire wr_clk,
input wire rd_clk,
input wire rst_n,
input wire wr_en,
input wire rd_en,
input wire [DATA_WIDTH-1:0] data_in,
output reg [DATA_WIDTH-1:0] data_out,
output reg full,
output reg empty
);
// 内部信号
reg [PTR_WIDTH-1:0] wr_ptr, rd_ptr;
reg [PTR_WIDTH-1:0] wr_ptr_gray, rd_ptr_gray;
reg [PTR_WIDTH-1:0] wr_ptr_gray_sync, rd_ptr_gray_sync;
wire [PTR_WIDTH-1:0] wr_ptr_bin, rd_ptr_bin;
wire wr_enable, rd_enable;
// 写指针逻辑(wr_clk 域)
always @(posedge wr_clk or negedge rst_n) begin
if (!rst_n)
wr_ptr <= 0;
else if (wr_en && !full)
wr_ptr <= wr_ptr + 1;
end
// 读指针逻辑(rd_clk 域)
always @(posedge rd_clk or negedge rst_n) begin
if (!rst_n)
rd_ptr <= 0;
else if (rd_en && !empty)
rd_ptr <= rd_ptr + 1;
end
// 格雷码编码
function [PTR_WIDTH-1:0] bin2gray;
input [PTR_WIDTH-1:0] bin;
begin
bin2gray = bin ^ (bin >> 1);
end
endfunction
always @(posedge wr_clk or negedge rst_n)
if (!rst_n) wr_ptr_gray <= 0;
else wr_ptr_gray <= bin2gray(wr_ptr);
always @(posedge rd_clk or negedge rst_n)
if (!rst_n) rd_ptr_gray <= 0;
else rd_ptr_gray <= bin2gray(rd_ptr);
// 同步器实例化(两级触发器)
sync_bits #(.WIDTH(PTR_WIDTH)) u_sync_wr2rd (
.clk (rd_clk),
.rst_n (rst_n),
.data_in (wr_ptr_gray),
.data_out (wr_ptr_gray_sync)
);
sync_bits #(.WIDTH(PTR_WIDTH)) u_sync_rd2wr (
.clk (wr_clk),
.rst_n (rst_n),
.data_in (rd_ptr_gray),
.data_out (rd_ptr_gray_sync)
);
// 空/满标志生成
wire [PTR_WIDTH-1:0] wr_ptr_gray_sync_bin, rd_ptr_gray_sync_bin;
// 将同步后的格雷码转回二进制(用于比较)
function [PTR_WIDTH-1:0] gray2bin;
input [PTR_WIDTH-1:0] gray;
reg [PTR_WIDTH-1:0] bin;
integer i;
begin
bin[PTR_WIDTH-1] = gray[PTR_WIDTH-1];
for (i = PTR_WIDTH-2; i >= 0; i = i - 1)
bin[i] = bin[i+1] ^ gray[i];
gray2bin = bin;
end
endfunction
always @(posedge wr_clk or negedge rst_n)
if (!rst_n) full <= 1'b0;
else begin
wr_ptr_gray_sync_bin = gray2bin(wr_ptr_gray_sync);
// 满条件:写指针追上同步后的读指针,且最高两位相反
if ((wr_ptr[PTR_WIDTH-1] != wr_ptr_gray_sync_bin[PTR_WIDTH-1]) &&
(wr_ptr[PTR_WIDTH-2] != wr_ptr_gray_sync_bin[PTR_WIDTH-2]) &&
(wr_ptr[PTR_WIDTH-3:0] == wr_ptr_gray_sync_bin[PTR_WIDTH-3:0]))
full <= 1'b1;
else
full <= 1'b0;
end
always @(posedge rd_clk or negedge rst_n)
if (!rst_n) empty <= 1'b1;
else begin
rd_ptr_gray_sync_bin = gray2bin(rd_ptr_gray_sync);
// 空条件:读指针等于同步后的写指针
if (rd_ptr == rd_ptr_gray_sync_bin)
empty <= 1'b1;
else
empty <= 1'b0;
end
// 双端口 RAM 实例化(略)
endmodule逐行说明
- 第 1-6 行:模块定义与参数化。DATA_WIDTH 与 FIFO_DEPTH 可配置,PTR_WIDTH 为地址位宽(log2(DEPTH))。
- 第 7-15 行:端口声明。wr_clk 与 rd_clk 为异步时钟,rst_n 为异步复位低有效。wr_en/rd_en 为写/读使能,data_in/data_out 为数据总线,full/empty 为状态标志。
- 第 18-19 行:内部信号定义。wr_ptr/rd_ptr 为二进制指针,wr_ptr_gray/rd_ptr_gray 为格雷码指针,wr_ptr_gray_sync/rd_ptr_gray_sync 为同步后的格雷码指针。
- 第 22-26 行:写指针递增逻辑。在 wr_clk 上升沿,若 wr_en 有效且 FIFO 未满,则 wr_ptr 加 1。复位时清零。
- 第 29-33 行:读指针递增逻辑。在 rd_clk 上升沿,若 rd_en 有效且 FIFO 非空,则 rd_ptr 加 1。
- 第 36-43 行:格雷码编码函数 bin2gray。通过二进制右移一位后异或实现。例如 4'b0101 变为 4'b0111。
- 第 45-49 行:在每个时钟域内将二进制指针转换为格雷码,并寄存到 wr_ptr_gray/rd_ptr_gray 中。这是跨时钟域传输前的最后一级寄存器。
- 第 52-62 行:实例化两个同步器模块。sync_bits 内部为两级 D 触发器链,用于将格雷码指针同步到目标时钟域。同步后的值 wr_ptr_gray_sync 在 rd_clk 域使用,rd_ptr_gray_sync 在 wr_clk 域使用。
- 第 65-79 行:格雷码转二进制函数 gray2bin。从最高位开始,逐位异或。例如 4'b0111 转回 4'b0101。
- 第 81-91 行:满标志生成逻辑。在 wr_clk 域,将同步后的读指针(wr_ptr_gray_sync)转二进制,与写指针比较。满条件:写指针的最高两位与同步读指针的最高两位相反,且低位相等。这是经典的“MSB 翻转”判断法,确保 FIFO 满时不会误判。
- 第 93-100 行:空标志生成逻辑。在 rd_clk 域,将同步后的写指针(rd_ptr_gray_sync)转二进制,与读指针比较。若相等,则 FIFO 为空。
- 第 102 行:双端口 RAM 实例化(代码中省略,实际工程中需添加)。RAM 的写地址为 wr_ptr,读地址为 rd_ptr。
时序与 CDC 约束
在 Vivado 2026.1 中,异步 FIFO 的约束核心是声明异步时钟组,并设置 false path 以避免工具分析跨时钟域路径。以下为 async_fifo.xdc 关键内容:
# 时钟定义
create_clock -name wr_clk -period 20.000 [get_ports {wr_clk}]
create_clock -name rd_clk -period 40.000 [get_ports {rd_clk}]
# 异步时钟组
set_clock_groups -asynchronous
-group [get_clocks -include_generated_clocks wr_clk]
-group [get_clocks -include_generated_clocks rd_clk]
# 跨时钟域路径 false path(可选,时钟组已隐含)
set_false_path -from [get_clocks wr_clk] -to [get_clocks rd_clk]
set_false_path -from [get_clocks rd_clk] -to [get_clocks wr_clk]
# 复位约束(异步复位)
set_property ASYNC_REG TRUE [get_cells -hierarchical -filter {NAME =~ *sync_bits*}]逐行说明
- 第 1-2 行:为 wr_clk 和 rd_clk 创建时钟,周期分别为 20ns(50MHz)和 40ns(25MHz)。
- 第 5-7 行:使用 set_clock_groups 声明两个时钟组为异步关系。这是 Vivado 推荐的 CDC 约束方式,工具会自动忽略组间路径的时序分析。
- 第 10-11 行:显式设置 false path,作为冗余约束确保安全。注意:若已使用 set_clock_groups,则 false path 可选,但保留可增加可读性。
- 第 14 行:将同步器内的所有触发器标记为 ASYNC_REG,告知工具这些寄存器用于异步同步,不进行时序优化与检查。
验证与仿真
Testbench 需生成两个独立时钟,模拟写满、读空、同时读写等场景。以下为关键测试序列:
// tb_async_fifo.v - 测试序列片段
initial begin
// 复位
rst_n = 0;
#100 rst_n = 1;
#20;
// 写入 16 个数据,验证满标志
for (int i = 0; i < 16; i = i + 1) begin
@(posedge wr_clk);
wr_en = 1;
data_in = i;
end
@(posedge wr_clk);
wr_en = 0;
// 此时 full 应为 1
assert (full == 1) else $error("FIFO should be full");
// 读取 16 个数据,验证空标志
for (int i = 0; i < 16; i = i + 1) begin
@(posedge rd_clk);
rd_en = 1;
// 检查数据顺序
assert (data_out == i) else $error("Data mismatch at %0d", i);
end
@(posedge rd_clk);
rd_en = 0;
assert (empty == 1) else $error("FIFO should be empty");
// 同时读写 8 个周期
for (int i = 0; i < 8; i = i + 1) begin
fork
@(posedge wr_clk) wr_en = 1; data_in = $random;
@(posedge rd_clk) rd_en = 1;
join
end
#1000 $finish;
end逐行说明
- 第 3-5 行:异步复位,保持 100ns 后释放。
- 第 8-13 行:连续写入 16 个数据(0-15),每个 wr_clk 上升沿写入一个。写入完成后检查 full 是否为 1。
- 第 16-23 行:连续读取 16 个数据,验证输出顺序与写入一致。读取完成后检查 empty 是否为 1。
- 第 26-32 行:同时进行写和读操作 8 个周期,使用 fork-join 模拟异步并发。数据随机写入,读取后不比对(仅验证无死锁)。
- 第 33 行:仿真结束。
常见坑与排查
- 格雷码指针未在跨时钟域前寄存:若直接将二进制指针同步,多位变化会导致亚稳态传播。必须先将指针转为格雷码并寄存一拍再同步。
- 满/空标志判断逻辑错误:常见错误是直接用二进制指针比较。正确做法:在写时钟域用同步后的读指针判断满,在读时钟域用同步后的写指针判断空。且满条件需比较最高两位。
- 约束遗漏导致时序违规:若未设置异步时钟组,Vivado 会分析跨时钟域路径,可能报出大量 setup 违规。务必在 xdc 中添加 set_clock_groups。
- 仿真中 full/empty 跳变延迟:由于同步器引入 2 个时钟周期的延迟,full 与 empty 的跳变并非即时。这是正常现象,设计需容忍此延迟。
原理与设计说明
为什么用格雷码
跨时钟域同步时,若直接传输二进制指针(如从 4'b0111 变为 4'b1000),多位同时变化会导致同步器捕获到中间值(如 4'b1111),造成满/空判断错误。格雷码相邻状态仅一位变化,同步后最多出现一个比特的亚稳态,且两级触发器可将其概率降至极低(MTBF 满足要求)。
满标志的 MSB 翻转判断
FIFO 深度为 2^N,指针宽度为 N+1(多一位用于区分满与空)。当写指针超过读指针一圈时,二进制指针的最高位翻转,而格雷码的最高两位同时翻转。通过比较最高两位是否相反,可准确判断 FIFO 是否写满,避免误判。
扩展与优化
- 深度扩展:通过修改 FIFO_DEPTH 参数,可支持任意 2^N 深度。注意指针宽度需相应调整。
- 数据宽度扩展:修改 DATA_WIDTH 参数即可,不影响指针逻辑。
- 性能优化:若需更高频率,可考虑在 RAM 输出端添加流水线寄存器,减少组合逻辑延迟。
- 资源优化:对于小深度 FIFO,可用分布式 RAM 替代 Block RAM,减少 BRAM 占用。
参考与附录
完整工程代码(包括 dpram.v、sync_bits.v、async_fifo_top.v 及约束文件)可参考 Xilinx 官方应用笔记 XAPP1311 或相关开源仓库。建议在 Vivado 2026.1 中直接导入并运行综合、仿真与实现流程。


