简介工程文件围绕Vivado异步FIFO IP的乒乓操作展开面向已具备Verilog基础、希望理解跨时钟域数据缓冲与双FIFO交替切换的FPGA学习者。内容涉及读写指针同步、双端口RAM存储、乒乓状态机切换与判满判空、IP Catalog配置及仿真验证等环节可用于搭建可综合、可仿真的完整实验环境帮助梳理从代码到约束、综合实现的工程组织方式。压缩包共205个文件约17.82MB以Verilog与VHDL源码、XDC时序约束、XCI IP配置、TCL/BAT/DO流程脚本为主并含综合实现日志、仿真数据库与工程文件便于对照检查配置与复现结果。已有975人学习下载适合作为异步FIFO乒乓操作的入门范例和课程设计参考也可据此拆解FIFO深度、切换时序和边界条件验证思路。1. 两个时钟域之间最容易出问题的地方一块 FIFO 兜不住采集侧跑 100 MHz处理侧跑 75 MHz突发来的时候一口气写 512 拍中间还夹着几十微秒的空闲——这种数据流如果用一块异步 FIFO 硬扛仿真大概率没问题上板跑几分钟就开始丢帧。原因并不复杂异步 FIFO 只解决“跨时钟域安全搬运”不解决“读写同时进行”。真正让吞吐稳住的是 FPGA Verilog 乒乓操作这套结构两块 FIFO 轮转写 A 的时候读 B切换的瞬间两端都不停。这份工程文件就是围绕 Vivado 异步 FIFO IP 搭的一套可复现乒乓缓冲包含 IP 配置、双 bank 切换逻辑、跨域同步器和约束文件适合正在做跨时钟域数据搬运、又不想从零搭框架的 Verilog 开发者。它并不复杂如果你指望它顺手解决仲裁和反压协议那就想多了。2. 工程骨架与异步 FIFO IP 的参数明细拿到一份 Vivado 工程文件先别急着综合。工程能不能一次跑通八成取决于 IP 的状态和参数是否和当前工具版本对得上。这章先把目录结构、IP 配置和几个容易选错的开关说清楚后面写切换逻辑才有地基。2.1 目录结构与版本兼容处理工程按 Vivado 常规布局组织主要文件如下路径内容pingpong_fifo.xpr工程入口含综合/实现策略srcs/hdl/top_pingpong.v顶层例化双 FIFO 与读写控制srcs/hdl/pp_wr_ctrl.v写侧帧计数与 bank 切换srcs/hdl/pp_rd_ctrl.v读侧帧消费与 bank 轮转srcs/hdl/cdc_sync.v两级同步器带 ASYNC_REG 属性ip/fifo_bank/fifo_bank.xci异步 FIFO IP 配置两块 bank 共用同一份配置constrs/pin.xdc时钟定义、时钟组、CDC 例外sim/tb_pingpong.v自比对测试平台随机间隔写入.xci 是版本敏感文件。低版本工具打开高版本生成的 IP会在 Sources 面板显示为 Locked综合直接报错说找不到对应的 IP 输出产物。常见做法是先看状态再升级# 在工程目录下启动 VivadoTcl Console 里执行 open_project ./pingpong_fifo.xpr report_ip_status # Status 为 Locked 的 IP 需要升级升级会重新生成输出产物 upgrade_ip [get_ips fifo_bank] # 升级完成后重新生成确保 .dcp / 仿真模型都是当前版本 generate_target all [get_files ./ip/fifo_bank/fifo_bank.xci]report_ip_status的输出重点看两列Status 和 Change Log。Status 为Locked说明版本不匹配为Up to date但 Change Log 非空说明配置被改过但没重新生成。upgrade_ip只改 IP 的版本标记不自动重新生成所以后面那句generate_target不能省否则综合时用的还是旧产物。升级前建议先把整个工程复制一份IP 升级是不可逆的改坏了回不去。2.2 六个必须逐项核对的 IP 参数双击 IP 打开定制界面下面这些参数建议照着填填错任何一个后面都会以奇怪的方式表现出来参数建议值为什么这么选Interface TypeNativeAXI 版多一层握手除非直接接 DMA否则纯属加复杂度FIFO ImplementationIndependent Clocks Block RAM异步 FIFO 必须选独立时钟选 Common Clocks 出不来两个时钟口Read ModeStandard FIFO与 FWFT 差一拍读侧计数逻辑完全不同Data Width32与上游数据位宽一致改位宽要同步改顶层参数Depth1024取两倍帧长512 帧长留一半余量给切换抖动Almost Full Threshold32提前 32 拍给出水位提示给切换逻辑留反应时间Programmable Almost Empty4调试时看水位用逻辑上不依赖它Data Count勾选出问题时能直接看 FIFO 里还剩多少比猜强Reset Pin勾选不勾就没有wr_rst/rd_rst端口例化必报错Safety Circuit勾选溢出/下溢时冻结指针避免错误扩散到整帧Depth 和 BRAM 的关系值得单独算一笔。32 bit × 1024 32 Kb一个 36 Kb 的块 RAM 装得下剩下 4 bit 是校验位浪费就浪费了。但如果把位宽改成 40 bit1024 深度就需要 40 Kb一个 BRAM 装不下工具会拼两块时序和面积都会变。所以位宽和深度是一起看的别只改一个。Almost Full 的 32 不是随便填的。它决定了写侧在多早的时候知道“这一帧快写完了”。如果填 0等于关闭 almost_full写侧只能靠 full 判断而 full 拉高的那一拍wr_en往往已经被组合逻辑挡住了最后一拍数据就丢了——这是乒乓结构里最经典的一类丢帧原因。2.3 Read Mode、复位与 Safety Circuit 的取舍Read Mode 选 Standard 还是 FWFT是这份工程里第二个容易犹豫的地方。Standard FIFO 的时序是rd_en拉高后下一个时钟沿dout才有效。FWFTFirst Word Fall Through则是数据提前出现在dout上rd_en更像一个 ack。前者读侧计数和rd_en一一对应写状态机最直观后者少一拍延迟但读侧必须用rd_en !empty作为“消费了一拍”的判据否则会多读。工程里用的是 Standard读侧指针每发一次rd_en加一帧边界和指针完全对齐调试时对波形很友好。复位引脚勾上之后IP 会生成wr_rst和rd_rst两个高有效复位。这两个复位内部会同步到各自的时钟域但仍然要求两侧同时有效。运行中只复位一侧是异步 FIFO 最隐蔽的问题写指针清零了、读指针没清读侧会认为 FIFO 里有大量数据实际读出来全是无效值而且这个错误会一直传播下去直到下一次全局复位。所以顶层只留一个外部复位进两个时钟域各做一次同步释放再分别送给 IP 和控制逻辑。Safety Circuit 建议打开。它干的事是一旦检测到写满还写、读空还读就冻结内部指针并拉高错误标志防止错误状态蔓延。代价是多一点组合逻辑和几个触发器的面积。做原型阶段这点面积不算什么等上了量再评估要不要关。我一般会在调试版本里打开量产版本再根据资源报告决定。3. 乒乓切换的握手toggle 比 ready 电平可靠在哪结构定下来之后真正的难点不在 FIFO 本身而在“什么时候切换 bank”这件事上。写侧认为一帧写完了读侧怎么知道读侧读完了写侧又怎么知道这块 bank 可以复用了这两个问题都是跨时钟域的处理不好就会看到“仿真全对、上板偶发错帧”这种令人头疼的现象。3.1 单缓冲、双缓冲与乒乓的吞吐差别先把账算清楚才知道为什么要用两块 FIFO。假设写域 100 MHz、位宽 32 bit峰值写带宽 400 MB/s读域 75 MHz、位宽 32 bit峰值读带宽 300 MB/s。一帧 512 拍写满需要 5.12 μs读空需要 6.83 μs。单块 FIFO 的模型是“写满—读空—再写”。写侧写完 512 拍后必须等读侧把这 512 拍全部读走才能继续写等待时间 6.83 μs有效吞吐被读侧限死。更麻烦的是如果上游数据不能停这段等待期间的数据只能丢或者缓存在别处。乒乓的模型是“写 A—写 B同时读 A—写 A同时读 B”。写 B 的 5.12 μs 里读侧有 6.83 μs 的时间窗去读 A读得完等写侧要写第三帧时A 已经空了。稳态下写侧只需要在“读侧比写侧慢”的差值上等一小段两帧写入 10.24 μs两帧读出 13.66 μs差 3.42 μs摊到每帧 1.71 μs 的背压。这个数字直接决定了上游需不需要加缓冲。如果这个差值不可接受只有三条路提高读时钟、加宽读侧位宽、或者把 bank 数从 2 加到 3 以上。前两条是改物理条件第三条是改结构各有代价。3.2 写侧切换条件与背压写侧的状态机只有两个状态选 bank、写帧。选 bank 的条件是“这块 bank 里没有未被消费的帧”。判断方法不是看 FIFO 的 empty而是看一个跨域握手信号——这一点很关键因为 FIFO 的 empty 在读时钟域写侧看不到。握手用 toggle 最稳。每块 bank 配一对信号写侧提交一帧后翻转wr_tgl[k]读侧消费完一帧后翻转rd_tgl[k]。两个信号各自跨到对方时钟域做两级同步然后比较写侧看wr_tgl[k] rd_tgl_sync[k]相等说明这块 bank 的上一帧已经被读走可以写。读侧看wr_tgl_sync[k] ! rd_tgl[k]不等说明有一帧新数据待读。toggle 相比电平 ready 的优势在于它是事件语义。电平 ready 在高电平期间持续有效如果读时钟比较慢可能在两个采样沿之间错过一次拉高又拉低toggle 每帧只翻转一次只要一帧的持续时间大于两三个读时钟周期就一定能被采到。反过来说如果一帧只有一个读时钟周期那么短toggle 也会漏这属于设计前提不成立得回去加深度。背压就落在“选不到空闲 bank”这个分支上。此时写侧拉低in_ready上游必须停。上游停不下来的话正确做法是在更前面再加一级小 FIFO 做弹性缓冲而不是把 bank 深度加大到无限——深度加大只是把溢出时间往后推不改变读写速率的差值。3.3 读侧消费完成与跨域同步读侧的切换逻辑比写侧多一层考虑两块 bank 都有数据时先读哪块。答案是按写入顺序读否则输出数据会乱序。实现上维护一个last_sel记录上次读的 bank两块都有数据时选另一块保证轮转。读帧过程中要处理 empty。正常情况下不会读到空因为写侧提交一帧意味着 512 个数据已经全部写进 FIFO而 FIFO 深度是 1024读侧读 512 个一定读得到。但如果 FIFO 深度被改小到小于帧长读侧就会在帧中间撞上 empty此时如果继续发rd_en读出来的是无效数据而且rd_ptr已经加过了整帧错位。所以读侧必须有!empty保护撞上就停住等等不到就说明参数配错了。帧读完的判定是rd_ptr FRAME_LEN-1在这个时钟沿翻转rd_tgl[k]、清reading、更新last_sel。翻转动作必须和最后一次rd_en在同一个时钟沿完成早一拍会让写侧提前认为 bank 空闲晚一拍会让写侧多等一个周期。这种“差一拍”的问题在波形上极难发现靠的是代码里对时序的明确注释。3.4 复位释放与第一帧的初始状态上电之后、第一帧到来之前所有 toggle 必须初值一致否则读侧一上电就认为“有数据待读”冲进去读一块空 FIFO。工程里的做法是外部复位低有效进每个时钟域后做同步释放同步后的复位同时清 FIFO 的wr_rst/rd_rst和控制逻辑里的所有 toggle、指针、状态位。释放之后的头几个周期FIFO 内部还需要几个时钟做指针初始化这段时间empty可能短暂为低所以读侧状态机要等到复位释放后第一个稳定周期才允许启动。还有一个容易忽略的点两块 FIFO 的复位必须同时释放。如果 bank0 的复位早释放了几个周期、bank1 晚释放写侧可能已经往 bank0 写了一帧并翻转了 toggle而 bank1 的指针还没初始化完。虽然不会立刻出错但第一轮轮转时 bank1 的行为是不确定的。稳妥的做法是把两个 IP 的复位接同一根线保证同拍释放。4. 代码实现两个 bank 的例化、切换与约束原理讲完接下来是能直接抄的部分。代码分成四块顶层连线、写侧控制、读侧控制、约束。每块都给完整可综合的写法注释标出关键拍的位置。4.1 顶层双 FIFO 例化与数据通路选择顶层只做三件事例化两块 FIFO、把读写控制接上去、按当前 bank 选择输出数据。module top_pingpong #( parameter integer DW 32, parameter integer FRAME_LEN 512 )( input wire wr_clk, input wire rd_clk, input wire rst_n, // 低有效两个域各自同步释放 input wire din_valid, input wire [DW-1:0] din, output wire din_ready, output reg dout_valid, output reg [DW-1:0] dout ); wire [1:0] fifo_wr_en; wire [1:0] fifo_rd_en; wire [1:0] fifo_full; wire [1:0] fifo_empty; wire [2*DW-1:0] fifo_dout; // 两块 bank 的 dout 拼在一起 wire [1:0] wr_tgl, rd_tgl; wire [1:0] wr_tgl_sync, rd_tgl_sync; wire out_bank; // 两块 FIFO 用同一份 IP 配置generate 展开省得写两遍 genvar gi; generate for (gi 0; gi 2; gi gi 1) begin : g_fifo fifo_bank u_fifo ( .wr_clk (wr_clk), .rd_clk (rd_clk), .wr_rst (wr_rst_sync), .rd_rst (rd_rst_sync), .din (wr_din_r), .wr_en (fifo_wr_en[gi]), .rd_en (fifo_rd_en[gi]), .dout (fifo_dout[gi*DW : DW]), .full (fifo_full[gi]), .empty (fifo_empty[gi]) ); end endgenerate // 输出按读侧当前 bank 选择注意 out_bank 已与 FIFO 输出对齐一拍 always (posedge rd_clk) begin if (!rst_n) dout {DW{1b0}}; else if (fifo_rd_en ! 2b00) dout out_bank ? fifo_dout[2*DW-1:DW] : fifo_dout[DW-1:0]; end endmodulefifo_bank就是 IP 定制时填的 Component Name两块 bank 共用一份 .xci靠 generate 展开两次例化改配置只需要改一处。fifo_dout用位选拼接避免在 generate 里对同一根线做三元赋值——那种写法综合工具能过但仿真器行为不一定一致。dout的寄存条件写的是fifo_rd_en ! 2b00而不是dout_valid。原因是 Standard FIFO 下rd_en有效后的下一拍dout才更新如果等dout_valid再寄存会晚一拍数据就和 valid 错开了。这个细节在波形上表现为“valid 比 data 晚一个周期”接下游时会被当成丢拍。4.2 写侧帧计数、bank 选择与数据同拍寄存写侧最容易被写错的地方不是状态机而是wr_en和din的时序关系。FIFO 在时钟沿采样这两个信号必须同拍有效。如果wr_en是寄存器输出、din直接接上游组合数据那么wr_en有效的那个沿上游数据可能已经撤销了写进去的就是错值。module pp_wr_ctrl #( parameter integer DW 32, parameter integer FRAME_LEN 512 )( input wire wr_clk, input wire wr_rst, // 高有效同步释放后 input wire in_valid, input wire [DW-1:0] in_data, output wire in_ready, input wire [1:0] full, output reg [1:0] fifo_wr_en, output reg [DW-1:0] fifo_din, // 接 FIFO 的 din output reg wr_tgl0, output reg wr_tgl1, input wire rd_tgl0_sync, input wire rd_tgl1_sync ); localparam integer PW $clog2(FRAME_LEN); reg [PW-1:0] wr_ptr; reg writing; reg bank_sel; reg wen_r; wire bank0_free (wr_tgl0 rd_tgl0_sync); wire bank1_free (wr_tgl1 rd_tgl1_sync); // 只有正在写、且当前 bank 没满时才接数据 assign in_ready writing !full[bank_sel]; always (posedge wr_clk) begin if (wr_rst) begin wr_ptr {PW{1b0}}; writing 1b0; bank_sel 1b0; wen_r 1b0; wr_tgl0 1b0; wr_tgl1 1b0; fifo_wr_en 2b00; end else begin fifo_wr_en 2b00; if (!writing) begin // 优先续用当前 bank被占用则换另一块都占用就等 if (bank_sel 1b0 bank0_free) begin writing 1b1; wr_ptr {PW{1b0}}; end else if (bank_sel 1b1 bank1_free) begin writing 1b1; wr_ptr {PW{1b0}}; end else if (bank0_free) begin bank_sel 1b0; writing 1b1; wr_ptr {PW{1b0}}; end else if (bank1_free) begin bank_sel 1b1; writing 1b1; wr_ptr {PW{1b0}}; end end else if (in_valid in_ready) begin // 数据与使能同拍寄存下一拍一起进 FIFO fifo_din in_data; fifo_wr_en[bank_sel] 1b1; wr_ptr wr_ptr 1b1; if (wr_ptr FRAME_LEN-1) begin writing 1b0; if (bank_sel 1b0) begin wr_tgl0 ~wr_tgl0; // 提交 bank0读侧可消费 bank_sel 1b1; end else begin wr_tgl1 ~wr_tgl1; // 提交 bank1 bank_sel 1b0; end end end end end endmodulefifo_din和fifo_wr_en在同一个时钟沿寄存下一拍同时到达 FIFO 端口采样关系是确定的。wr_ptr的加一发生在提交那一拍和fifo_wr_en的寄存同拍所以指针数的是“已提交拍数”不是“已写入 FIFO 拍数”——两者差一拍但帧边界的判断用的是指针只要一致就没问题。in_ready是组合输出依赖writing和full。full来自 FIFO 的写时钟域同域组合没问题。如果时序紧张可以把in_ready也打一拍代价是握手多一个周期延迟上游要能接受。4.3 读侧轮转、empty 保护与输出对齐读侧比写侧多两件事两块都有数据时按顺序读以及dout_valid要和 Standard FIFO 的输出延迟对齐。module pp_rd_ctrl #( parameter integer DW 32, parameter integer FRAME_LEN 512 )( input wire rd_clk, input wire rd_rst, input wire [1:0] empty, output reg [1:0] fifo_rd_en, output wire dout_valid, input wire wr_tgl0_sync, input wire wr_tgl1_sync, output reg rd_tgl0, output reg rd_tgl1, output reg out_bank, output reg [DW-1:0] dout ); localparam integer PW $clog2(FRAME_LEN); reg [PW-1:0] rd_ptr; reg reading; reg rd_sel; reg last_sel; reg valid_r; wire bank0_has (wr_tgl0_sync ! rd_tgl0); wire bank1_has (wr_tgl1_sync ! rd_tgl1); assign dout_valid valid_r; always (posedge rd_clk) begin if (rd_rst) begin rd_ptr {PW{1b0}}; reading 1b0; rd_sel 1b0; last_sel 1b1; valid_r 1b0; fifo_rd_en 2b00; rd_tgl0 1b0; rd_tgl1 1b0; end else begin fifo_rd_en 2b00; valid_r 1b0; if (!reading) begin if (bank0_has !bank1_has) begin reading 1b1; rd_sel 1b0; rd_ptr {PW{1b0}}; end else if (bank1_has !bank0_has) begin reading 1b1; rd_sel 1b1; rd_ptr {PW{1b0}}; end else if (bank0_has bank1_has) begin // 两块都有数据从上次没读的那块开始保证先写先读 reading 1b1; rd_sel ~last_sel; rd_ptr {PW{1b0}}; end end else if (!empty[rd_sel]) begin fifo_rd_en[rd_sel] 1b1; // 本拍发读下拍 dout 有效 rd_ptr rd_ptr 1b1; valid_r 1b1; // 与 FIFO 输出延迟对齐 out_bank rd_sel; if (rd_ptr FRAME_LEN-1) begin reading 1b0; last_sel rd_sel; if (rd_sel 1b0) rd_tgl0 ~rd_tgl0; // 告诉写侧 bank0 空 else rd_tgl1 ~rd_tgl1; end end end end endmodulevalid_r在发rd_en的同一拍拉高下一拍和 FIFO 的dout一起有效。out_bank也同一拍更新用来在顶层选择数据。如果这里用reading !empty直接当 valid就会早一拍下游收到的第一个数据和 valid 对不上。!empty[rd_sel]这个保护不能省。它存在的意义是防止参数配错时读空 FIFO正常情况下永远为真。真撞上了读侧会卡住等写侧补数据此时如果写侧也在等读侧释放 bank就会死锁——但死锁至少能看到比读到一堆假数据强。4.4 约束时钟组与同步器例外两个时钟来自不同源时必须告诉工具它们是异步的否则会去分析本来就不该分析的路径要么报大量违例要么工具自己乱优化。# 写域 100 MHz读域 75 MHz两个时钟无相位关系 create_clock -period 10.000 -name wr_clk [get_ports wr_clk] create_clock -period 13.333 -name rd_clk [get_ports rd_clk] # 声明异步工具不再分析两组时钟之间的建立/保持 set_clock_groups -asynchronous -group [get_clocks wr_clk] -group [get_clocks rd_clk] # 同步器首级不参与时序分析避免工具把它当普通路径优化 set_false_path -to [get_cells -h p a hrefhttps://download.csdn.net/download/wczzmmp2/85695758 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p