FPGA时序优化与高速接口实战手册

从 RTL 结构、CDC、XDC/SDC 到 UltraScale+/Versal 与高速接口实战,全网最全最细的时序学习手册

目录

  • 阅读说明
  • [第1-8章 FPGA 时序优化](#第1-8章 FPGA 时序优化)
  • [第 1 章 时序优化总览](#第 1 章 时序优化总览)
    • [1.1 什么是时序收敛](#1.1 什么是时序收敛)
    • [1.2 四种核心优化手段的定位](#1.2 四种核心优化手段的定位)
    • [1.3 优化的总体顺序](#1.3 优化的总体顺序)
  • [第 2 章 Pipeline(流水线)](#第 2 章 Pipeline(流水线))
    • [2.1 基本思想](#2.1 基本思想)
    • [2.2 典型场景一:宽位加法器流水线化](#2.2 典型场景一:宽位加法器流水线化)
    • [2.3 典型场景二:多级选择器的流水线化](#2.3 典型场景二:多级选择器的流水线化)
    • [2.4 流水线设计的原则](#2.4 流水线设计的原则)
    • [2.5 带 valid 的标准流水线模板](#2.5 带 valid 的标准流水线模板)
  • [第 3 章 Retiming(重定时)](#第 3 章 Retiming(重定时))
    • [3.1 概念](#3.1 概念)
    • [3.2 Retiming 的前提条件](#3.2 Retiming 的前提条件)
    • [3.3 Vivado 中启用 Retiming 的三种方式](#3.3 Vivado 中启用 Retiming 的三种方式)
    • [3.4 实战例子:前重后轻的关键路径](#3.4 实战例子:前重后轻的关键路径)
    • [3.5 什么时候 Retiming 无效](#3.5 什么时候 Retiming 无效)
    • [3.6 Retiming vs Pipeline 的区别](#3.6 Retiming vs Pipeline 的区别)
  • [第 4 章 Replication(逻辑复制)](#第 4 章 Replication(逻辑复制))
    • [4.1 概念](#4.1 概念)
    • [4.2 工具自动复制 vs 手动复制](#4.2 工具自动复制 vs 手动复制)
    • [4.3 典型场景:全局复位的复制](#4.3 典型场景:全局复位的复制)
    • [4.4 BRAM/DSP 控制信号的复制](#4.4 BRAM/DSP 控制信号的复制)
    • [4.5 Replication 的代价](#4.5 Replication 的代价)
  • [第 5 章 PhysOpt(物理优化)](#第 5 章 PhysOpt(物理优化))
    • [5.1 概念](#5.1 概念)
    • [5.2 PhysOpt 能做什么](#5.2 PhysOpt 能做什么)
    • [5.3 典型执行流程](#5.3 典型执行流程)
    • [5.4 PhysOpt 的 Directive 一览](#5.4 PhysOpt 的 Directive 一览)
    • [5.5 DSP/BRAM 寄存器吸收示例](#5.5 DSP/BRAM 寄存器吸收示例)
    • [5.6 incremental_compile 增量编译](#5.6 incremental_compile 增量编译)
  • [第 6 章 高扇出与拥塞优化](#第 6 章 高扇出与拥塞优化)
    • [6.1 高扇出诊断](#6.1 高扇出诊断)
    • [6.2 降低高扇出的三种思路](#6.2 降低高扇出的三种思路)
    • [6.3 拥塞分析](#6.3 拥塞分析)
    • [6.4 减少拥塞的常用手段](#6.4 减少拥塞的常用手段)
  • [第 7 章 综合案例:16 位乘加器的优化演进](#第 7 章 综合案例:16 位乘加器的优化演进)
    • [7.1 原始版本(单拍完成)](#7.1 原始版本(单拍完成))
    • [7.2 版本 2:加 Pipeline](#7.2 版本 2:加 Pipeline)
    • [7.3 版本 3:匹配 DSP48 流水结构](#7.3 版本 3:匹配 DSP48 流水结构)
    • [7.4 版本 4:多通道 + 复制](#7.4 版本 4:多通道 + 复制)
    • [7.5 版本 5:Vivado 实现命令](#7.5 版本 5:Vivado 实现命令)
    • [7.6 优化对比](#7.6 优化对比)
  • [第 8 章 Vivado 实战命令速查](#第 8 章 Vivado 实战命令速查)
    • [8.1 时序报告](#8.1 时序报告)
    • [8.2 高扇出与拥塞](#8.2 高扇出与拥塞)
    • [8.3 实现选项模板](#8.3 实现选项模板)
    • [8.4 常用 Verilog 属性](#8.4 常用 Verilog 属性)
  • [附录 A 时序优化检查清单](#附录 A 时序优化检查清单)
  • [附录 B 常见违例根因速查表](#附录 B 常见违例根因速查表)
  • [第九章 基于FPGA的CDC(跨时钟域)设计专题](#第九章 基于FPGA的CDC(跨时钟域)设计专题)
    • [9.1 为什么需要 CDC 同步:亚稳态的本质](#9.1 为什么需要 CDC 同步:亚稳态的本质)
    • [9.2 单 bit 电平信号同步(两级同步器)](#9.2 单 bit 电平信号同步(两级同步器))
    • [9.3 单 bit 脉冲同步](#9.3 单 bit 脉冲同步)
    • [9.4 多 bit 数据同步:错误示范与正确方式](#9.4 多 bit 数据同步:错误示范与正确方式)
    • [9.5 握手同步(Handshake)完整实现](#9.5 握手同步(Handshake)完整实现)
    • [9.6 格雷码(Gray Code)](#9.6 格雷码(Gray Code))
    • [9.7 异步 FIFO 完整实现](#9.7 异步 FIFO 完整实现)
    • [9.8 复位 CDC:异步复位、同步释放](#9.8 复位 CDC:异步复位、同步释放)
    • [9.9 Vivado 中的 CDC 约束写法](#9.9 Vivado 中的 CDC 约束写法)
    • [9.10 常见陷阱与调试方法](#9.10 常见陷阱与调试方法)
    • [9.11 CDC 模式速查表](#9.11 CDC 模式速查表)
    • [9.12 本章小结](#9.12 本章小结)
  • [第 10 章 时序约束(XDC/SDC)专题](#第 10 章 时序约束(XDC/SDC)专题)
    • [create_clock、set_input_delay、set_multicycle_path、set_case_analysis 全套写法](#create_clock、set_input_delay、set_multicycle_path、set_case_analysis 全套写法)
    • [10.0 学这一章前,先建立 5 个核心直觉](#10.0 学这一章前,先建立 5 个核心直觉)
    • [10.1 create_clock:一切的起点](#10.1 create_clock:一切的起点)
    • [10.2 set_input_delay:输入端口的时序契约](#10.2 set_input_delay:输入端口的时序契约)
    • [10.3 set_output_delay:对外发送数据的契约](#10.3 set_output_delay:对外发送数据的契约)
    • [10.4 set_multicycle_path:当一拍不够用](#10.4 set_multicycle_path:当一拍不够用)
    • [10.5 set_false_path:告诉工具"别管它"](#10.5 set_false_path:告诉工具“别管它”)
    • [10.6 set_clock_groups:异步时钟分组的优雅写法](#10.6 set_clock_groups:异步时钟分组的优雅写法)
    • [10.7 set_case_analysis:把电路"假设掉"](#10.7 set_case_analysis:把电路“假设掉”)
    • [10.8 set_max_delay / set_min_delay:精确控制](#10.8 set_max_delay / set_min_delay:精确控制)
    • [10.9 一个完整的 XDC 例子(必须能默写)](#10.9 一个完整的 XDC 例子(必须能默写))
    • [10.10 调试与验证:写完怎么知道对?](#10.10 调试与验证:写完怎么知道对?)
    • [10.11 高频翻车点速查表](#10.11 高频翻车点速查表)
    • [10.12 本章必背 10 条口诀](#10.12 本章必背 10 条口诀)
    • [10.13 一页速查卡(建议打印贴墙)](#10.13 一页速查卡(建议打印贴墙))
  • [第 11 章 UltraScale+ / Versal 架构专属优化](#第 11 章 UltraScale+ / Versal 架构专属优化)
    • [CLB 架构差异、Clock Region、SLR 跨越、NoC 使用](#CLB 架构差异、Clock Region、SLR 跨越、NoC 使用)
    • [11.0 开篇:为什么这一章必须单独讲](#11.0 开篇:为什么这一章必须单独讲)
    • [11.1 CLB 架构差异:7 系列 → UltraScale+ → Versal](#11.1 CLB 架构差异:7 系列 → UltraScale+ → Versal)
    • [11.2 Clock Region(时钟区):时钟是有地盘的](#11.2 Clock Region(时钟区):时钟是有地盘的)
  • [第12章 FPGA高速接口时序实战](#第12章 FPGA高速接口时序实战)
    • [12.1 高速接口的时序挑战总览](#12.1 高速接口的时序挑战总览)
    • [12.2 源同步接口与系统同步接口](#12.2 源同步接口与系统同步接口)
    • [12.3 LVDS 接收:IDELAY / ISERDES 实战](#12.3 LVDS 接收:IDELAY / ISERDES 实战)
    • [12.4 LVDS 发送:OSERDES 与 TX 对齐](#12.4 LVDS 发送:OSERDES 与 TX 对齐)
    • [12.5 动态位对齐(Bitslip)与字对齐](#12.5 动态位对齐(Bitslip)与字对齐)
    • [12.6 GT SerDes 收发器架构](#12.6 GT SerDes 收发器架构)
    • [12.7 GT 的复位序列与时钟规划](#12.7 GT 的复位序列与时钟规划)
    • [12.8 8b/10b 对齐、通道绑定、时钟补偿](#12.8 8b/10b 对齐、通道绑定、时钟补偿)
    • [12.9 MIG DDR3/DDR4 控制器时序要点](#12.9 MIG DDR3/DDR4 控制器时序要点)
    • [12.10 MIG 用户接口与跨域处理](#12.10 MIG 用户接口与跨域处理)
    • [12.11 高速接口的 XDC 约束模板](#12.11 高速接口的 XDC 约束模板)
    • [12.12 常见调试手段与问题定位](#12.12 常见调试手段与问题定位)
    • [12.13 本章小结](#12.13 本章小结)
    • [12.14 三种接口对比速查表](#12.14 三种接口对比速查表)
  • [第 13 章 补充实战案例库:从报告到改 RTL 的完整闭环](#第 13 章 补充实战案例库:从报告到改 RTL 的完整闭环)
    • [13.1 案例一:AXI-Stream 数据通路的流水线与反压](#13.1 案例一:AXI-Stream 数据通路的流水线与反压)
    • [13.2 案例二:复位同步释放与 reset fanout 优化](#13.2 案例二:复位同步释放与 reset fanout 优化)
    • [13.3 案例三:异步 FIFO 的约束闭环](#13.3 案例三:异步 FIFO 的约束闭环)
    • [13.4 案例四:SLR Crossing 违例的处理](#13.4 案例四:SLR Crossing 违例的处理)
    • [13.5 案例五:LVDS 源同步输入的 XDC 模板](#13.5 案例五:LVDS 源同步输入的 XDC 模板)
    • [13.6 案例六:10G XGMII/PCS 用户侧时序规划](#13.6 案例六:10G XGMII/PCS 用户侧时序规划)
  • [第 14 章 签核清单:交付前必须确认的 40 项](#第 14 章 签核清单:交付前必须确认的 40 项)
    • [14.1 时钟与约束](#14.1 时钟与约束)
    • [14.2 RTL 时序结构](#14.2 RTL 时序结构)
    • [14.3 CDC 与复位](#14.3 CDC 与复位)
    • [14.4 物理实现](#14.4 物理实现)
    • [14.5 报告与验证](#14.5 报告与验证)
  • [附录 C 常用 Tcl 命令索引](#附录 C 常用 Tcl 命令索引)

阅读说明

本手册由用户提供的多个 FPGA 时序专题章节合并、重排和补充而成,目标是形成一本可直接用于学习、设计审查和工程排障的核心手册。

使用建议

  • 如果你正在写 RTL:优先阅读第 1-8 章和第 13 章。

  • 如果你正在处理跨时钟域:优先阅读第 9 章。

  • 如果你正在写 XDC/SDC:优先阅读第 10 章。

  • 如果你使用 UltraScale+ / Versal:优先阅读第 11 章。

  • 如果你做 LVDS、GT、DDR/MIG 等高速接口:优先阅读第 12 章。

时序优化总路线

text 复制代码
确认时钟与约束完整
    ↓
查看 Timing Summary 和 Worst Path
    ↓
判断根因:逻辑深 / 布线长 / 高扇出 / 跨域 / I/O / SLR / 拥塞
    ↓
优先 RTL 架构优化:pipeline、数据控制对齐、CDC 正确结构
    ↓
再做综合与物理优化:retiming、replication、phys_opt、pblock
    ↓
最后签核:timing、CDC、exceptions、仿真、上板调试

全书章节结构

  1. 时序优化总览、Pipeline、Retiming、Replication、PhysOpt、高扇出与拥塞、综合案例、Vivado 命令。

  2. CDC 跨时钟域设计:2FF、脉冲、握手、Gray Code、异步 FIFO、复位 CDC、约束。

  3. XDC/SDC:create_clock、input/output delay、multicycle、false path、case analysis。

  4. UltraScale+ / Versal:CLB、Clock Region、SLR、NoC 与架构级优化。

  5. 高速接口:LVDS、IDELAY/ISERDES、OSERDES、GT、MIG、约束模板。

  6. 补充实战案例和签核清单。

第1-8章 FPGA 时序优化

推荐读者:FPGA 工程师、数字 IC 工程师、高速逻辑设计人员

第 1 章 时序优化总览

1.1 什么是时序收敛

在同步数字电路中,每条寄存器到寄存器之间的路径必须满足:

text 复制代码
T_clk  ≥  T_clk2q + T_logic + T_routing + T_setup − T_skew

其中: - T_clk 是时钟周期 - T_clk2q 是源寄存器的 Clock-to-Q 延迟 - T_logic 是组合逻辑延迟 - T_routing 是布线延迟 - T_setup 是目的寄存器的建立时间 - T_skew 是时钟偏斜

Setup 违例 :等号右侧总和大于 T_clk,数据到达太晚
Hold 违例:路径太短,新数据覆盖了还未被锁存的旧数据

1.2 四种核心优化手段的定位

技术 作用层次 核心目的 代价
Pipeline RTL 源码 降低单拍逻辑深度 增加延迟拍数、FF 资源
Retiming 综合/实现 重分布已有寄存器,均衡路径 少量 FF 增加
Replication 综合/实现 降低高扇出网的布线延迟 FF/LUT 复制
PhysOpt 布局布线后 物理层面修复关键路径 运行时间增加

1.3 优化的总体顺序

text 复制代码
RTL 架构 (Pipeline)
   ↓
综合阶段 (Retiming, Fanout Opt)
   ↓
布局阶段 (Replication, Placement)
   ↓
布线后 (PhysOpt)
   ↓
时序签核 (Timing Signoff)

越靠前的优化杠杆越大,越靠后的优化代价越小但威力有限。正确的顺序是自顶向下,而不是指望最后的 PhysOpt 救火。

第 2 章 Pipeline(流水线)

2.1 基本思想

Pipeline 就是把一段很长的组合逻辑拆成多个短段,每段之间插入寄存器。

代价是延迟增加 N 拍,收益是最高频率接近提升 N 倍(理想情况下)。

text 复制代码
[原始] REG → [A → B → C → D] → REG     f_max = 1 / T(A+B+C+D)

[流水] REG → A → REG → B → REG → C → REG → D → REG
        f_max ≈ 1 / max(T_A, T_B, T_C, T_D)

2.2 典型场景一:宽位加法器流水线化

未优化版本(单拍 64 位加法)
verilog 复制代码
module adder_flat (
    input  wire        clk,
    input  wire [63:0] a,
    input  wire [63:0] b,
    output reg  [63:0] sum
);
    always @(posedge clk) begin
        sum <= a + b;   // 64 位加法,进位链很长
    end
endmodule

在高频率(例如 500 MHz)下,64 位加法器的进位链往往成为关键路径。

优化版本(两级流水,分成高低 32 位)
verilog 复制代码
module adder_pipelined (
    input  wire        clk,
    input  wire [63:0] a,
    input  wire [63:0] b,
    output reg  [63:0] sum
);
    // 第一级:低 32 位相加 + 进位,高 32 位打一拍等待
    reg [32:0] sum_lo_r;   // 33 位,最高位是进位
    reg [31:0] a_hi_r, b_hi_r;

    always @(posedge clk) begin
        sum_lo_r <= {1'b0, a[31:0]} + {1'b0, b[31:0]};
        a_hi_r   <= a[63:32];
        b_hi_r   <= b[63:32];
    end

    // 第二级:高 32 位相加时把第一级的进位加上
    always @(posedge clk) begin
        sum <= {a_hi_r + b_hi_r + sum_lo_r[32], sum_lo_r[31:0]};
    end
endmodule
text 复制代码
收益:关键路径从 64 位进位链缩短为 ~33 位,Fmax 显著提升。
代价:延迟从 1 拍变为 2 拍。

2.3 典型场景二:多级选择器的流水线化

未优化版本
verilog 复制代码
// 1 中选 8,再选 1,单拍完成
module mux_chain (
    input  wire        clk,
    input  wire [7:0][31:0] din,
    input  wire [2:0]  sel,
    input  wire [31:0] mask,
    output reg  [31:0] dout
);
    always @(posedge clk) begin
        dout <= din[sel] & mask;   // mux8 + 32位与 + 寄存
    end
endmodule
流水线优化版本
verilog 复制代码
module mux_chain_pipe (
    input  wire        clk,
    input  wire [7:0][31:0] din,
    input  wire [2:0]  sel,
    input  wire [31:0] mask,
    output reg  [31:0] dout
);
    reg [31:0] mux_r;
    reg [31:0] mask_r;

    // Stage 1: 先完成 mux 选择,mask 同步打拍
    always @(posedge clk) begin
        mux_r  <= din[sel];
        mask_r <= mask;
    end

    // Stage 2: 再做与操作
    always @(posedge clk) begin
        dout <= mux_r & mask_r;
    end
endmodule

关键技巧 :数据路径打拍时,对应的控制信号必须同步打拍,否则功能出错。

2.4 流水线设计的原则

  • 数据和控制同步打拍。valid/ready 信号也要推进一级。

  • 复位不要到处加。越少强复位的流水线寄存器,工具越容易做 retiming。

  • 避免把分支条件留在最后一级。判断在前、数据运算在后,易于拆分。

  • 考虑流水线气泡。如果存在反压,需要设计 ready 回传机制(skid buffer)。

2.5 带 valid 的标准流水线模板

verilog 复制代码
module pipe_stage #(
    parameter WIDTH = 32
) (
    input  wire             clk,
    input  wire             rst_n,
    input  wire             in_valid,
    input  wire [WIDTH-1:0] in_data,
    output reg              out_valid,
    output reg  [WIDTH-1:0] out_data
);
    always @(posedge clk) begin
        if (!rst_n) begin
            out_valid <= 1'b0;
            // out_data 不复位,利于 retiming 和 SRL 推断
        end else begin
            out_valid <= in_valid;
            out_data  <= in_data;   // 这里可替换为任意组合逻辑
        end
    end
endmodule

要点

  • valid 必须复位,否则上电时可能误触发下游。

  • data 可以不复位,给工具更多优化空间。

第 3 章 Retiming(重定时)

3.1 概念

Retiming 是综合/物理优化阶段的自动技术,它在不改变电路功能的前提下,把寄存器沿着数据流方向前移或后移,以平衡前后级的组合延迟。

text 复制代码
优化前:   FF → [ 深逻辑 A ] → [ 浅逻辑 B ] → FF
          关键路径 = T_A(很长)

Retiming:FF → [ 部分 A ] → FF → [ 剩余 A + B ] → FF
          关键路径被均衡

3.2 Retiming 的前提条件

工具只有满足以下条件时才会做 retiming:

  • 寄存器之间是纯组合逻辑

  • 寄存器没有异步 set/reset(或复位可被吸收)

  • 没有 DONT_TOUCH、MARK_DEBUG 阻止移动

  • 寄存器不是 IO 边界上的 FF

  • 时序约束清晰

3.3 Vivado 中启用 Retiming 的三种方式

方式一:综合属性(局部控制)
verilog 复制代码
(* retiming_forward = 1 *)   reg [31:0] stage_a;
(* retiming_backward = 1 *)  reg [31:0] stage_b;
方式二:综合选项(全局开关)
tcl 复制代码
# 综合时启用 retiming
synth_design -top top -retiming
方式三:物理优化阶段(最常用)
tcl 复制代码
opt_design
place_design
phys_opt_design -retime
route_design
phys_opt_design -retime        ;# 布线后再做一次

3.4 实战例子:前重后轻的关键路径

问题代码
verilog 复制代码
module skewed_path (
    input  wire        clk,
    input  wire [15:0] a, b, c, d,
    output reg  [17:0] y
);
    reg [17:0] s1;

    // 前级:三个加法串联,非常深
    always @(posedge clk) begin
        s1 <= a + b + c + d;     // 3 级加法链
    end

    // 后级:只是取反,非常浅
    always @(posedge clk) begin
        y <= ~s1;
    end
endmodule

前级路径 = 3 级加法链,后级路径 = 1 个反相器,极度不均衡。

让工具做 Retiming

(* retiming_forward = 1 *) reg [17:0] s1;

或在实现阶段:

phys_opt_design -retime

Retiming 后的效果(工具等效改写)
text 复制代码
原:FF → (a+b+c+d) → FF → (~) → FF
后:FF → (a+b) → FF → ((a+b)+(c+d)) → FF → (~) → FF   [示意]

关键路径从 3 级加法变为 2 级加法,Fmax 显著提升。源码没变,只是寄存器位置被工具移动。

3.5 什么时候 Retiming 无效

场景 原因
寄存器带异步复位 复位信号阻止移动
关键路径跨越 DSP/BRAM 边界 工具不会把 FF 推进硬核
RTL 中写了 DONT_TOUCH 明确禁止
路径含反馈环 移动会破坏功能
I/O 边界寄存器 必须保持在 IOB

3.6 Retiming vs Pipeline 的区别

特征 Pipeline Retiming
何时做 RTL 设计阶段,人工 综合/实现阶段,工具自动
改变延迟 是,增加拍数 否,拍数不变
改变功能 改变时序语义 完全等价
需要重新验证 需要 不需要

核心结论:Pipeline 是"增加寄存器数量",Retiming 是"重新分布已有寄存器"。二者互补,不是替代。

第 4 章 Replication(逻辑复制)

4.1 概念

当一个寄存器或 LUT 的输出扇出(fanout)非常高时(比如一个信号驱动 200 个触发器),会出现两个问题:

  • 这个驱动源的负载电容很大,布线需要加缓冲,延迟增加

  • 这 200 个负载可能分布在芯片的不同角落,长距离布线无法避免

Replication 就是把这个高扇出驱动源复制多份,每一份只驱动一部分负载,从而缩短布线距离。

text 复制代码
原始:   REG_A → { L1, L2, ..., L200 }     单点 fanout=200

复制后: REG_A1 → { L1..L50 }
        REG_A2 → { L51..L100 }
        REG_A3 → { L101..L150 }
        REG_A4 → { L151..L200 }

4.2 工具自动复制 vs 手动复制

自动复制(推荐起点)

Vivado 会根据扇出阈值自动复制。默认阈值可以通过属性调整:

(* max_fanout = 50 *) reg ctrl_en; // 超过50就复制

或全局命令:

tcl 复制代码
# 综合阶段控制扇出
synth_design -top top -fanout_limit 400
手动复制(精确控制)

当自动复制效果不理想时,手动复制能精确控制布局。

verilog 复制代码
module manual_replication (
    input  wire clk,
    input  wire rst_n,
    input  wire trigger,
    output reg  [3:0] out_bank_a,
    output reg  [3:0] out_bank_b,
    output reg  [3:0] out_bank_c,
    output reg  [3:0] out_bank_d
);
    // 手动复制 4 份 enable 寄存器,每份驱动一个 bank
    (* dont_touch = "true" *) reg en_a, en_b, en_c, en_d;

    always @(posedge clk) begin
        if (!rst_n) begin
            en_a <= 1'b0;
            en_b <= 1'b0;
            en_c <= 1'b0;
            en_d <= 1'b0;
        end else begin
            en_a <= trigger;
            en_b <= trigger;
            en_c <= trigger;
            en_d <= trigger;
        end
    end

    always @(posedge clk) begin
        if (en_a) out_bank_a <= out_bank_a + 1;
        if (en_b) out_bank_b <= out_bank_b + 1;
        if (en_c) out_bank_c <= out_bank_c + 1;
        if (en_d) out_bank_d <= out_bank_d + 1;
    end
endmodule

关键:必须加 (* dont_touch = "true" *),否则综合器会把四个逻辑等价的寄存器合并回一个,复制白做。

4.3 典型场景:全局复位的复制

全局复位是最常见的高扇出信号之一。对于大型设计,全局复位扇出可能达到数万。

劣质写法(单一复位源)
verilog 复制代码
module reset_bad (
    input  wire clk,
    input  wire rst,
    // ... 大量逻辑
);
    always @(posedge clk) begin
        if (rst) begin
            // 数千个寄存器都在这里复位
            ...
        end
    end
endmodule
优化写法(分级复位树)
verilog 复制代码
module reset_tree (
    input  wire clk,
    input  wire rst_in
);
    // 第一级:一个复位源
    reg rst_r0;

    // 第二级:复制多份,物理上分布到不同区域
    (* dont_touch = "true", max_fanout = 100 *)
    reg rst_r1_a, rst_r1_b, rst_r1_c, rst_r1_d;

    always @(posedge clk) begin
        rst_r0   <= rst_in;
        rst_r1_a <= rst_r0;
        rst_r1_b <= rst_r0;
        rst_r1_c <= rst_r0;
        rst_r1_d <= rst_r0;
    end

    // rst_r1_a 驱动 bank A 中所有寄存器
    // rst_r1_b 驱动 bank B 中所有寄存器
    // ...
endmodule

4.4 BRAM/DSP 控制信号的复制

高性能设计中,BRAM 的地址/使能信号往往被大量存储器共享,成为关键路径。

verilog 复制代码
// 每 N 个 BRAM 配对一份地址寄存器
(* dont_touch = "true", max_fanout = 16 *)
reg [11:0] bram_addr_rep [0:3];

genvar i;
generate
    for (i = 0; i < 4; i = i + 1) begin : g_bram
        always @(posedge clk) bram_addr_rep[i] <= addr_in;

        // 每组 16 个 BRAM 用 bram_addr_rep[i]
        my_bram_inst u_bram (
            .clk  (clk),
            .addr (bram_addr_rep[i]),
            ...
        );
    end
endgenerate

4.5 Replication 的代价

代价 说明
资源 FF/LUT 数量按复制倍数增长
功耗 多个寄存器翻转,动态功耗上升
可能引入 hold 违例 新副本的布线比原来更短
调试复杂 信号探针需要指定具体副本

经验值:单个信号 fanout 超过 100~200 就应考虑复制。超过 1000 几乎必须复制。

第 5 章 PhysOpt(物理优化)

5.1 概念

PhysOpt 是布局(place)之后、或布线(route)之后执行的物理感知优化。它了解单元的真实坐标和布线延迟,能做出比综合阶段更精准的决策。

5.2 PhysOpt 能做什么

Vivado 的 phys_opt_design 命令提供以下常见优化:

选项 作用
-fanout_opt 高扇出网络复制
-placement_opt 关键路径单元重新放置
-rewire 重新布线关键网络
-retime 物理级重定时
-critical_cell_opt 关键单元替换(比如 LUT 换更快的实现)
-dsp_register_opt 把外部 FF 吸收到 DSP 内部
-bram_register_opt 把外部 FF 吸收到 BRAM 输出寄存器
-bram_enable_opt BRAM 使能路径优化
-shift_register_opt SRL 相关优化
-hold_fix 修复 hold 违例
-aggressive_hold_fix 激进 hold 修复

5.3 典型执行流程

基础流程
tcl 复制代码
# ---- 标准 Vivado 实现流程 ----
opt_design
place_design
phys_opt_design                          ;# 布局后优化(推荐)
route_design
phys_opt_design -directive Explore       ;# 布线后再优化(时序紧时使用)
按违例类型分开调用
tcl 复制代码
place_design

# 第一波:关键路径优化
phys_opt_design -directive AggressiveExplore

route_design

# 第二波:hold 修复
phys_opt_design -directive AggressiveFanoutOpt

# 第三波:如果还有剩余违例
phys_opt_design -directive AlternateReplication

5.4 PhysOpt 的 Directive 一览

Directive 是预设策略组合,常用的有:

Directive 用途
Default 默认策略
Explore 更多尝试,时间更长
ExploreWithHoldFix 同时修复 hold
AggressiveExplore 激进探索,最强优化
AggressiveFanoutOpt 专注高扇出
AlternateReplication 尝试不同复制策略
AddRetime 加入重定时

建议:时序收敛时,用不同 Directive 跑多个 run,比较结果。

5.5 DSP/BRAM 寄存器吸收示例

问题代码(DSP 外挂输出寄存器)
verilog 复制代码
module dsp_ext_reg (
    input  wire        clk,
    input  wire [17:0] a, b,
    input  wire [47:0] c,
    output reg  [47:0] p
);
    wire [47:0] p_int;

    // DSP 乘加(未打开内部寄存器)
    assign p_int = a * b + c;

    // 外挂寄存器:物理上可能远离 DSP
    always @(posedge clk) p <= p_int;
endmodule

DSP 到外挂 FF 之间可能有较长布线。

PhysOpt 优化后(工具自动吸收)

运行 phys_opt_design -dsp_register_opt 后,Vivado 会把外挂 FF 移入 DSP48 内部的 PREG,消除这段布线。

手动推断版本(更可控)
verilog 复制代码
module dsp_internal_reg (
    input  wire        clk,
    input  wire [17:0] a, b,
    input  wire [47:0] c,
    output reg  [47:0] p
);
    reg [17:0] a_r, b_r;
    reg [47:0] c_r;
    reg [35:0] ab_r;

    always @(posedge clk) begin
        a_r  <= a;          // A 输入寄存器(AREG)
        b_r  <= b;          // B 输入寄存器(BREG)
        c_r  <= c;          // C 输入寄存器(CREG)
        ab_r <= a_r * b_r;  // M 寄存器(MREG)
        p    <= ab_r + c_r; // P 寄存器(PREG)
    end
endmodule

5 级流水完全对应 DSP48 内部结构,综合器直接推断出一个 DSP 单元,无外部 FF,极高 Fmax。

5.6 incremental_compile 增量编译

每次修改 RTL 都全量跑 PhysOpt 代价很大。Vivado 的增量编译可以复用上一次的布局:

tcl 复制代码
read_checkpoint -incremental prev_route.dcp
place_design
phys_opt_design
route_design

对于大型设计,增量编译可以将运行时间缩短 30%~50%。

第 6 章 高扇出与拥塞优化

6.1 高扇出诊断

tcl 复制代码
# 报告 fanout 最高的网络
report_high_fanout_nets -max_nets 20 -load_types -timing

# 报告具体某个网络
report_property [get_nets my_ctrl_net]

6.2 降低高扇出的三种思路

思路一:控制信号局部化

:一个使能信号直接控制整个数据路径的 128 个寄存器
:数据路径各段自己产生局部使能

verilog 复制代码
// 好的做法:每级各有 enable_i
module pipe_local_en (
    input  wire clk,
    input  wire in_valid,
    input  wire [31:0] in_data,
    output reg  out_valid,
    output reg  [31:0] out_data
);
    reg s1_valid, s2_valid;
    reg [31:0] s1_data, s2_data;

    // 每级使能由本级 valid 生成,不是全局信号
    always @(posedge clk) begin
        if (in_valid) s1_data <= in_data;
        s1_valid <= in_valid;

        if (s1_valid) s2_data <= s1_data + 1;
        s2_valid <= s1_valid;

        if (s2_valid) out_data <= s2_data ^ 32'hA5A5;
        out_valid <= s2_valid;
    end
endmodule
思路二:用 max_fanout 属性

(* max_fanout = 64 *) reg global_enable;

综合器会自动按 64 为上限复制该寄存器。

思路三:让它走全局资源

如果信号本身就需要广播到整个芯片(比如时钟使能、异步复位),可以走 BUFG 等全局资源:

BUFG u_bufg_rst (

.I (rst_raw),

.O (rst_global)

);

6.3 拥塞分析

布线拥塞(congestion)会让工具不得不绕远路,间接破坏时序。

tcl 复制代码
# 布线后检查拥塞
report_design_analysis -congestion -min_congestion_level 5

# 检查是否有明显的拥塞热点
report_utilization -hierarchical

拥塞等级 0~7: - 0~3:正常 - 4~5:需要关注 - 6~7:严重拥塞,必须修改设计

6.4 减少拥塞的常用手段

  • 提高层次划分清晰度,避免模块间信号交叉过多

  • 降低模块间总线宽度,或分时复用

  • 避免在一个小区域堆叠过多 BRAM/DSP

  • 慎用强制 pblock,可能反而恶化拥塞

  • 打开 -directive Explore 或 AltRouting 尝试不同布线策略

第 7 章 综合案例:16 位乘加器的优化演进

7.1 原始版本(单拍完成)

verilog 复制代码
module macc_v1 (
    input  wire        clk,
    input  wire        rst_n,
    input  wire [15:0] a, b,
    input  wire [31:0] c,
    output reg  [31:0] y
);
    always @(posedge clk) begin
        if (!rst_n) y <= 32'd0;
        else        y <= a * b + c;   // 乘 + 加一拍完成
    end
endmodule

问题:乘法 + 加法 + 大位宽,单拍关键路径 > 6 ns。目标 400 MHz(2.5 ns)无法达成。

7.2 版本 2:加 Pipeline

verilog 复制代码
module macc_v2 (
    input  wire        clk,
    input  wire        rst_n,
    input  wire [15:0] a, b,
    input  wire [31:0] c,
    output reg  [31:0] y
);
    reg [31:0] mult_r;
    reg [31:0] c_r;

    always @(posedge clk) begin
        mult_r <= a * b;   // 第一级:乘
        c_r    <= c;       // 同步打拍
    end

    always @(posedge clk) begin
        if (!rst_n) y <= 32'd0;
        else        y <= mult_r + c_r;  // 第二级:加
    end
endmodule

Fmax 提升,但 DSP 外部仍有 FF。

7.3 版本 3:匹配 DSP48 流水结构

verilog 复制代码
module macc_v3 (
    input  wire        clk,
    input  wire [15:0] a, b,
    input  wire [31:0] c,
    output reg  [31:0] y
);
    reg [15:0] a_r, b_r;
    reg [31:0] c_r1, c_r2;
    reg [31:0] mult_r;

    always @(posedge clk) begin
        a_r    <= a;            // AREG
        b_r    <= b;            // BREG
        c_r1   <= c;            // CREG (第1拍对齐)
        mult_r <= a_r * b_r;    // MREG
        c_r2   <= c_r1;         // 对齐 MREG
        y      <= mult_r + c_r2;// PREG
    end
endmodule

此时综合器可以把整个模块映射为一个 DSP48E 单元,所有寄存器吸收进硬核,无外部路径。Fmax 可突破 600 MHz。

7.4 版本 4:多通道 + 复制

如果同一个 c 被多个 MACC 共享:

verilog 复制代码
module macc_v4 #(parameter N = 8) (
    input  wire        clk,
    input  wire [15:0] a [0:N-1],
    input  wire [15:0] b [0:N-1],
    input  wire [31:0] c_shared,   // 高扇出
    output reg  [31:0] y [0:N-1]
);
    // 为每个通道复制一份 c
    (* dont_touch = "true", max_fanout = 1 *)
    reg [31:0] c_rep [0:N-1];

    genvar i;
    generate
        for (i = 0; i < N; i = i + 1) begin : g_ch
            always @(posedge clk) c_rep[i] <= c_shared;

            reg [15:0] a_r, b_r;
            reg [31:0] mult_r;

            always @(posedge clk) begin
                a_r    <= a[i];
                b_r    <= b[i];
                mult_r <= a_r * b_r;
                y[i]   <= mult_r + c_rep[i];
            end
        end
    endgenerate
endmodule

每个通道就近取到自己专属的 c 副本,布线延迟最小。

7.5 版本 5:Vivado 实现命令

tcl 复制代码
# 综合
synth_design -top macc_v4 -retiming -fanout_limit 400

# 实现
opt_design -directive ExploreWithRemap
place_design -directive ExtraTimingOpt
phys_opt_design -directive AggressiveExplore
route_design -directive AggressiveExplore
phys_opt_design -directive AggressiveFanoutOpt

# 报告
report_timing_summary -file timing_summary.rpt
report_utilization   -file util.rpt

7.6 优化对比

版本 描述 目标 Fmax DSP 使用
v1 单拍 MACC ~150 MHz 1
v2 2 级 pipeline ~300 MHz 1(部分外部 FF)
v3 完全匹配 DSP48 >600 MHz 1(纯硬核)
v4 多通道 + 复制 >600 MHz N

第 8 章 Vivado 实战命令速查

8.1 时序报告

tcl 复制代码
# 总览
report_timing_summary -file ts.rpt

# 最差 50 条路径
report_timing -max_paths 50 -slack_lesser_than 0 -file worst.rpt

# 某条具体路径
report_timing -from [get_cells u_a/reg_x] -to [get_cells u_b/reg_y] \
              -delay_type max -nworst 1

# 专门看 hold
report_timing -max_paths 50 -delay_type min -slack_lesser_than 0

8.2 高扇出与拥塞

tcl 复制代码
report_high_fanout_nets -max_nets 20 -load_types -timing
report_design_analysis  -congestion
report_design_analysis  -complexity -hierarchical_depth 3

8.3 实现选项模板

面积友好
tcl 复制代码
opt_design  -directive ExploreArea
place_design -directive Default
route_design -directive Default
时序激进
tcl 复制代码
opt_design        -directive ExploreWithRemap
place_design      -directive ExtraTimingOpt
phys_opt_design   -directive AggressiveExplore
route_design      -directive AggressiveExplore
phys_opt_design   -directive AggressiveFanoutOpt
Hold 修复
tcl 复制代码
route_design -directive Default
phys_opt_design -directive AggressiveFanoutOpt -hold_fix

8.4 常用 Verilog 属性

属性 作用 示例
max_fanout 限制扇出,触发复制 (* max_fanout = 64 *)
dont_touch 禁止优化合并 (* dont_touch = "true" *)
keep 保留信号名 (* keep = "true" *)
keep_hierarchy 保留层次边界 (* keep_hierarchy = "yes" *)
retiming_forward 允许前向重定时 (* retiming_forward = 1 *)
retiming_backward 允许后向重定时 (* retiming_backward = 1 *)
shreg_extract 是否推断为 SRL (* shreg_extract = "no" *)
use_dsp 强制/禁止 DSP (* use_dsp = "yes" *)

附录 A 时序优化检查清单

RTL 阶段 - [ ] 单拍组合深度是否超过 8~10 级 LUT - [ ] 宽位算术运算是否加了流水 - [ ] DSP/BRAM 周围是否有对齐的输入/输出寄存器 - [ ] 是否避免了不必要的复位 - [ ] 控制信号是否跟数据同步打拍

约束阶段 - [ ] 所有时钟已定义 - [ ] 异步时钟组已声明 - [ ] IO 延迟已约束 - [ ] false_path/multicycle_path 是否真实合理

综合阶段 - [ ] 高扇出限制是否合理(默认/调整) - [ ] 是否启用 retiming - [ ] 是否检查了综合时序报告

实现阶段 - [ ] 使用了合适的 place/route directive - [ ] 是否调用了 phys_opt_design - [ ] 是否检查了拥塞报告 - [ ] hold 违例是否已修复

附录 B 常见违例根因速查表

违例现象 可能根因 推荐对策
Setup 违例,逻辑延迟 > 70% 组合太深 Pipeline / Retiming
Setup 违例,布线延迟 > 70% 高扇出 / 跨区域 Replication / Floorplan
Hold 违例遍布全设计 时钟约束错误 重审约束
Hold 违例集中少数路径 物理布局问题 PhysOpt -hold_fix
DSP 周围路径违例 外挂 FF 未吸收 PhysOpt -dsp_register_opt 或手动推断
BRAM 周围路径违例 输出寄存器未启用 打开 DOB 寄存器
拥塞等级 > 5 模块聚集 改善层次 / 降低扇入扇出
建立时间几乎都差一点 Fmax 目标过高 Pipeline 追加一级
仅在特定工艺角失败 工艺/温度/电压影响 加 margin,考虑全 corner

第九章 基于FPGA的CDC(跨时钟域)设计专题

本章聚焦跨时钟域设计的理论、模式、完整代码和约束写法。

9.1 为什么需要 CDC 同步:亚稳态的本质

当信号从时钟域 A 传输到时钟域 B,而两个时钟频率或相位不同步时,目的寄存器的 D 端数据可能在采样时刻正好处于跳变中间,违反了 setup/hold 要求。

这种情况下,触发器的 Q 端既不是稳定的 0 也不是稳定的 1,而是停留在中间电压一段时间,这个现象叫 亚稳态(Metastability)。亚稳态最终会坍缩到某一个稳定值,但坍缩方向是随机的。

亚稳态带来两个问题:

  • 采样值随机:同一个 D 值,两次采样可能一次得 0、一次得 1

  • 传播失真:亚稳态输出进入后续组合逻辑,会让多条路径采到不一致的值,导致状态机跑飞、数据错位

CDC 设计的核心目标只有两个:

  • 降低亚稳态发生概率(MTBF 够大)

  • 确保即使发生,也不会传播到影响功能的范围

9.2 单 bit 电平信号同步(两级同步器)

最常见的 CDC 结构,俗称 "打两拍"

9.2.1 标准模板
verilog 复制代码
module sync_2ff #(
    parameter INIT = 1'b0
) (
    input  wire clk_dst,     // 目的域时钟
    input  wire rst_dst_n,   // 目的域复位
    input  wire din_async,   // 源域来的异步信号
    output wire dout_sync    // 同步到目的域的信号
);
    (* ASYNC_REG = "TRUE" *) reg sync_ff1;
    (* ASYNC_REG = "TRUE" *) reg sync_ff2;

    always @(posedge clk_dst or negedge rst_dst_n) begin
        if (!rst_dst_n) begin
            sync_ff1 <= INIT;
            sync_ff2 <= INIT;
        end else begin
            sync_ff1 <= din_async;
            sync_ff2 <= sync_ff1;
        end
    end

    assign dout_sync = sync_ff2;
endmodule
9.2.2 关键点解析
  • ASYNC_REG = "TRUE":告诉 Vivado 这两个 FF 属于同步链,要物理上放在一起,布局工具会把它们放进同一个 SLICE,缩短第一级到第二级的布线距离,最大化亚稳态坍缩时间。

  • 两级足够吗?:对大多数设计(<500 MHz、低 MTBF 要求)两级够用。高频或高可靠系统可能需要三级。

  • 适用信号类型电平信号 (长时间保持稳定),或慢变化信号(变化频率远低于目的时钟)。

  • 不适用场景:脉冲信号、总线信号、有时序相关性的多 bit 信号。

9.2.3 使用示例
verilog 复制代码
// 外部按键(异步)同步到 100MHz 系统时钟
sync_2ff #(.INIT(1'b0)) u_btn_sync (
    .clk_dst   (clk_100m),
    .rst_dst_n (rst_n),
    .din_async (btn_raw),
    .dout_sync (btn_sync)
);

9.3 单 bit 脉冲同步

如果源域产生一个单周期脉冲 ,而目的域时钟比源域慢,两级同步器可能采不到这个脉冲。

9.3.1 快到慢:脉冲展宽 + 同步 + 边沿检测
verilog 复制代码
module pulse_sync_fast2slow (
    input  wire clk_src,
    input  wire rst_src_n,
    input  wire pulse_in,       // 源域单周期脉冲

    input  wire clk_dst,
    input  wire rst_dst_n,
    output reg  pulse_out       // 目的域单周期脉冲
);
    // ---- 源域:用 toggle 把脉冲转成电平翻转 ----
    reg toggle_src;
    always @(posedge clk_src or negedge rst_src_n) begin
        if (!rst_src_n)       toggle_src <= 1'b0;
        else if (pulse_in)    toggle_src <= ~toggle_src;
    end

    // ---- 目的域:两级同步 ----
    (* ASYNC_REG = "TRUE" *) reg sync1, sync2, sync3;
    always @(posedge clk_dst or negedge rst_dst_n) begin
        if (!rst_dst_n) {sync1, sync2, sync3} <= 3'b0;
        else            {sync1, sync2, sync3} <= {toggle_src, sync1, sync2};
    end

    // ---- 边沿检测:toggle 的每次翻转 = 源域的一次脉冲 ----
    always @(posedge clk_dst or negedge rst_dst_n) begin
        if (!rst_dst_n) pulse_out <= 1'b0;
        else            pulse_out <= sync2 ^ sync3;   // 检测任意方向的边沿
    end
endmodule
9.3.2 关键思路
  • Toggle 电平化:源脉冲翻转 toggle,这个 toggle 是长期稳定的电平,可以被慢时钟采到

  • 异或边沿检测:sync2 ^ sync3 检测 toggle 翻转

  • 限制 :两次脉冲间隔必须 > 2 个目的时钟周期,否则会漏脉冲

9.3.3 慢到快:两级同步器即可

如果源时钟比目的时钟慢,脉冲本身就跨越多个目的时钟周期,直接用两级同步器就行。

9.4 多 bit 数据同步:错误示范与正确方式

9.4.1 错误示范:各 bit 独立打两拍
verilog 复制代码
// ⚠️ 错误写法 ------ 绝对不要这样做
module multibit_wrong (
    input  wire        clk_dst,
    input  wire [7:0]  data_async,
    output reg  [7:0]  data_sync
);
    (* ASYNC_REG = "TRUE" *) reg [7:0] sync1, sync2;
    always @(posedge clk_dst) begin
        sync1 <= data_async;
        sync2 <= sync1;
    end
    assign data_sync = sync2;
endmodule

为什么错:源域 data_async 从 0x7F → 0x80 变化时,8 个 bit 不会在同一瞬间改变。由于每个 bit 的路径延迟不同,某一拍目的域可能采到 0x00、0xFF、0x80 等任意中间值。

9.4.2 三种正确方式
方式 适用场景 吞吐率
握手同步(Handshake) 偶尔传输的控制/配置数据
格雷码同步 连续变化的计数器
异步 FIFO 高速连续数据流

9.5 握手同步(Handshake)完整实现

9.5.1 原理

源域在 req 拉高时保持数据稳定,目的域收到 req 后采样数据并回送 ack,源域看到 ack 后撤销 req。关键点:req/ack 是单 bit 同步,数据不经同步器但满足 req 期间稳定

9.5.2 源域发送方
verilog 复制代码
module handshake_src #(
    parameter W = 32
) (
    input  wire           clk_src,
    input  wire           rst_src_n,
    input  wire           load,        // 用户请求发送
    input  wire [W-1:0]   data_in,
    output reg            busy,        // 正在传输,不能再 load
    output reg            req,         // 到目的域的请求
    input  wire           ack_sync,    // 目的域回来的 ack(已同步)
    output reg  [W-1:0]   data_bus     // 稳定数据总线
);
    localparam S_IDLE = 2'd0, S_REQ = 2'd1, S_WAIT_ACK_LOW = 2'd2;
    reg [1:0] state;

    always @(posedge clk_src or negedge rst_src_n) begin
        if (!rst_src_n) begin
            state    <= S_IDLE;
            req      <= 1'b0;
            busy     <= 1'b0;
            data_bus <= {W{1'b0}};
        end else begin
            case (state)
                S_IDLE: begin
                    if (load) begin
                        data_bus <= data_in;    // 锁存数据,保持稳定
                        req      <= 1'b1;
                        busy     <= 1'b1;
                        state    <= S_REQ;
                    end
                end
                S_REQ: begin
                    if (ack_sync) begin         // 目的域采到了
                        req   <= 1'b0;
                        state <= S_WAIT_ACK_LOW;
                    end
                end
                S_WAIT_ACK_LOW: begin
                    if (!ack_sync) begin        // 等 ack 回到 0,完成一轮
                        busy  <= 1'b0;
                        state <= S_IDLE;
                    end
                end
                default: state <= S_IDLE;
            endcase
        end
    end
endmodule
9.5.3 目的域接收方
verilog 复制代码
module handshake_dst #(
    parameter W = 32
) (
    input  wire           clk_dst,
    input  wire           rst_dst_n,
    input  wire           req_sync,    // 源域同步过来的 req
    input  wire [W-1:0]   data_bus,    // 源域的稳定数据
    output reg            ack,         // 回给源域
    output reg            data_valid,
    output reg  [W-1:0]   data_out
);
    reg req_sync_d;
    always @(posedge clk_dst or negedge rst_dst_n) begin
        if (!rst_dst_n) begin
            ack        <= 1'b0;
            req_sync_d <= 1'b0;
            data_valid <= 1'b0;
            data_out   <= {W{1'b0}};
        end else begin
            req_sync_d <= req_sync;
            data_valid <= 1'b0;

            // 检测 req 上升沿:此时 data_bus 已稳定足够久
            if (req_sync & ~req_sync_d) begin
                data_out   <= data_bus;
                data_valid <= 1'b1;
                ack        <= 1'b1;
            end

            if (!req_sync) ack <= 1'b0;   // req 撤销后撤 ack
        end
    end
endmodule
9.5.4 顶层装配
verilog 复制代码
module handshake_top #(parameter W = 32) (
    input  wire         clk_src, rst_src_n,
    input  wire         clk_dst, rst_dst_n,
    input  wire         load,
    input  wire [W-1:0] data_in,
    output wire         busy,
    output wire         data_valid,
    output wire [W-1:0] data_out
);
    wire req, ack;
    wire req_sync, ack_sync;
    wire [W-1:0] data_bus;

    // req 从 src 同步到 dst
    sync_2ff u_req_sync (.clk_dst(clk_dst), .rst_dst_n(rst_dst_n),
                         .din_async(req), .dout_sync(req_sync));

    // ack 从 dst 同步到 src
    sync_2ff u_ack_sync (.clk_dst(clk_src), .rst_dst_n(rst_src_n),
                         .din_async(ack), .dout_sync(ack_sync));

    handshake_src #(W) u_src (
        .clk_src(clk_src), .rst_src_n(rst_src_n),
        .load(load), .data_in(data_in),
        .busy(busy), .req(req), .ack_sync(ack_sync),
        .data_bus(data_bus)
    );

    handshake_dst #(W) u_dst (
        .clk_dst(clk_dst), .rst_dst_n(rst_dst_n),
        .req_sync(req_sync), .data_bus(data_bus),
        .ack(ack), .data_valid(data_valid), .data_out(data_out)
    );
endmodule
9.5.5 特点
  • 无需对 data_bus 做同步:因为 req 同步延迟 + ack 往返延迟的时间内,data_bus 一直保持稳定,目的域采样时已经是稳定值

  • 吞吐率低:一次传输需要 4~6 个时钟周期往返

  • 适合:配置寄存器写入、低速状态传递、中断事件

9.6 格雷码(Gray Code)

9.6.1 为什么用格雷码

普通二进制计数器,相邻数值之间可能有多 bit 同时翻转,比如 0111 → 1000 翻转了 4 个 bit。这些 bit 如果异步采样到目的域,会出现中间值。

格雷码(Gray Code)的性质 :相邻两个数之间只有 1 个 bit 不同。即使异步采样发生亚稳态,最多就是采到"前一个值"或"后一个值",绝不会采到任意中间值。

9.6.2 二进制 ↔ 格雷码转换
verilog 复制代码
// 二进制转格雷码
// gray = bin ^ (bin >> 1)
function automatic [W-1:0] bin2gray(input [W-1:0] bin);
    bin2gray = bin ^ (bin >> 1);
endfunction

// 格雷码转二进制
// bin[i] = ^ (gray[W-1:i])
function automatic [W-1:0] gray2bin(input [W-1:0] gray);
    integer i;
    begin
        gray2bin[W-1] = gray[W-1];
        for (i = W-2; i >= 0; i = i - 1)
            gray2bin[i] = gray2bin[i+1] ^ gray[i];
    end
endfunction
9.6.3 格雷码计数器
verilog 复制代码
module gray_counter #(parameter W = 4) (
    input  wire         clk,
    input  wire         rst_n,
    input  wire         inc,
    output reg  [W-1:0] gray_cnt
);
    reg [W-1:0] bin_cnt;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            bin_cnt  <= 0;
            gray_cnt <= 0;
        end else if (inc) begin
            bin_cnt  <= bin_cnt + 1;
            gray_cnt <= (bin_cnt + 1) ^ ((bin_cnt + 1) >> 1);
        end
    end
endmodule
9.6.4 格雷码同步示例
verilog 复制代码
// 源域产生格雷码计数,目的域同步并转回二进制
module gray_sync #(parameter W = 4) (
    input  wire         clk_src, rst_src_n,
    input  wire         clk_dst, rst_dst_n,
    input  wire         inc,
    output wire [W-1:0] bin_in_dst
);
    wire [W-1:0] gray_src;
    gray_counter #(W) u_cnt (.clk(clk_src), .rst_n(rst_src_n),
                             .inc(inc), .gray_cnt(gray_src));

    (* ASYNC_REG = "TRUE" *) reg [W-1:0] gray_sync1, gray_sync2;
    always @(posedge clk_dst or negedge rst_dst_n) begin
        if (!rst_dst_n) {gray_sync1, gray_sync2} <= 0;
        else            {gray_sync2, gray_sync1} <= {gray_sync1, gray_src};
    end

    // 目的域得到的格雷码值转回二进制
    genvar i;
    generate
        for (i = 0; i < W; i = i + 1) begin : g_g2b
            assign bin_in_dst[i] = ^ gray_sync2[W-1:i];
        end
    endgenerate
endmodule

9.7 异步 FIFO 完整实现

异步 FIFO 是 CDC 的终极武器,支持高吞吐连续数据传输。核心思想:读写指针用格雷码跨域比较,用 BRAM/LUTRAM 做双端口存储

9.7.1 顶层结构
text 复制代码
        wr_clk                          rd_clk
         │                                │
  ┌──────▼──────┐                  ┌──────▼──────┐
  │  wr_ptr     │                  │  rd_ptr     │
  │ (binary+    │                  │ (binary+    │
  │  gray)      │                  │  gray)      │
  └──┬───────┬──┘                  └──┬───────┬──┘
     │       │  gray 跨域同步            │       │
     │       └────────►────────┐     ┌──┘       │
     │                         │     │          │
     │       ┌─────◄─────┐     ▼     ▼          │
     │       │ gray 同步 │ rd_ptr_gray_sync      │
     ▼       ▼           wr_ptr_gray_sync        ▼
  full 判断                                   empty 判断
     │                                           │
     │          ┌───────────────┐                │
     └────────► │  Dual-port    │ ◄──────────────┘
                │  RAM (BRAM)   │
                └───────────────┘
9.7.2 完整代码
verilog 复制代码
module async_fifo #(
    parameter DW = 32,          // 数据宽度
    parameter AW = 8            // 地址宽度,深度 = 2^AW
) (
    // 写端
    input  wire          wr_clk,
    input  wire          wr_rst_n,
    input  wire          wr_en,
    input  wire [DW-1:0] wr_data,
    output wire          full,
    output wire          almost_full,

    // 读端
    input  wire          rd_clk,
    input  wire          rd_rst_n,
    input  wire          rd_en,
    output reg  [DW-1:0] rd_data,
    output wire          empty,
    output wire          almost_empty
);
    localparam DEPTH = 1 << AW;

    // ---- 存储 ----
    (* ram_style = "block" *) reg [DW-1:0] mem [0:DEPTH-1];

    // ---- 写指针(二进制 + 格雷码),注意是 AW+1 位 ----
    reg [AW:0] wr_ptr_bin, wr_ptr_gray;
    wire [AW:0] wr_ptr_bin_next  = wr_ptr_bin + (wr_en & ~full);
    wire [AW:0] wr_ptr_gray_next = wr_ptr_bin_next ^ (wr_ptr_bin_next >> 1);

    always @(posedge wr_clk or negedge wr_rst_n) begin
        if (!wr_rst_n) begin
            wr_ptr_bin  <= 0;
            wr_ptr_gray <= 0;
        end else begin
            wr_ptr_bin  <= wr_ptr_bin_next;
            wr_ptr_gray <= wr_ptr_gray_next;
        end
    end

    always @(posedge wr_clk) begin
        if (wr_en & ~full)
            mem[wr_ptr_bin[AW-1:0]] <= wr_data;
    end

    // ---- 读指针(二进制 + 格雷码)----
    reg [AW:0] rd_ptr_bin, rd_ptr_gray;
    wire [AW:0] rd_ptr_bin_next  = rd_ptr_bin + (rd_en & ~empty);
    wire [AW:0] rd_ptr_gray_next = rd_ptr_bin_next ^ (rd_ptr_bin_next >> 1);

    always @(posedge rd_clk or negedge rd_rst_n) begin
        if (!rd_rst_n) begin
            rd_ptr_bin  <= 0;
            rd_ptr_gray <= 0;
        end else begin
            rd_ptr_bin  <= rd_ptr_bin_next;
            rd_ptr_gray <= rd_ptr_gray_next;
        end
    end

    always @(posedge rd_clk) begin
        if (rd_en & ~empty)
            rd_data <= mem[rd_ptr_bin[AW-1:0]];
    end

    // ---- 格雷码跨域同步 ----
    (* ASYNC_REG = "TRUE" *) reg [AW:0] rd_ptr_gray_sync1, rd_ptr_gray_sync2;
    always @(posedge wr_clk or negedge wr_rst_n) begin
        if (!wr_rst_n) {rd_ptr_gray_sync2, rd_ptr_gray_sync1} <= 0;
        else {rd_ptr_gray_sync2, rd_ptr_gray_sync1} <= {rd_ptr_gray_sync1, rd_ptr_gray};
    end

    (* ASYNC_REG = "TRUE" *) reg [AW:0] wr_ptr_gray_sync1, wr_ptr_gray_sync2;
    always @(posedge rd_clk or negedge rd_rst_n) begin
        if (!rd_rst_n) {wr_ptr_gray_sync2, wr_ptr_gray_sync1} <= 0;
        else {wr_ptr_gray_sync2, wr_ptr_gray_sync1} <= {wr_ptr_gray_sync1, wr_ptr_gray};
    end

    // ---- full 判断 ----
    // 写指针格雷码 == 读指针格雷码(高 2 位取反,其余相等)
    assign full = (wr_ptr_gray_next == {~rd_ptr_gray_sync2[AW:AW-1],
                                         rd_ptr_gray_sync2[AW-2:0]});

    // ---- empty 判断 ----
    // 读指针格雷码 == 写指针格雷码(完全相等即空)
    assign empty = (rd_ptr_gray_next == wr_ptr_gray_sync2);

    // ---- 可选:almost_full / almost_empty ----
    // 将格雷码转回二进制做距离判断
    wire [AW:0] rd_ptr_bin_in_wr;
    genvar i;
    generate
        for (i = 0; i <= AW; i = i + 1) begin : g_g2b_r
            assign rd_ptr_bin_in_wr[i] = ^ rd_ptr_gray_sync2[AW:i];
        end
    endgenerate
    wire [AW:0] used_w = wr_ptr_bin - rd_ptr_bin_in_wr;
    assign almost_full = (used_w >= (DEPTH - 4));

    wire [AW:0] wr_ptr_bin_in_rd;
    generate
        for (i = 0; i <= AW; i = i + 1) begin : g_g2b_w
            assign wr_ptr_bin_in_rd[i] = ^ wr_ptr_gray_sync2[AW:i];
        end
    endgenerate
    wire [AW:0] used_r = wr_ptr_bin_in_rd - rd_ptr_bin;
    assign almost_empty = (used_r <= 4);

endmodule
9.7.3 设计要点
  • 指针位宽 = AW + 1:最高位用于区分"一轮过完"还是"真空"

  • full 条件:写指针和读指针的高两位相反、其余相同 ------ 意味着写比读快了整整一圈

  • empty 条件:读指针 == 同步过来的写指针 ------ 真的读空了

  • 格雷码:保证跨域采样不会出现任意中间值,只会看到新值或旧值。读到旧值时最坏后果是"假满/假空",不会数据错位

  • almost_full / almost_empty:用在流水控制,避免踩到真正的满/空边界

9.7.4 最小深度选择
text 复制代码
DEPTH >= ceil(burst_size * (1 + clk_ratio)) + 4

经验值:快写慢读场景,深度至少取写入突发长度的 2 倍。

9.8 复位 CDC:异步复位、同步释放

复位本身也是跨时钟域信号。常见错误是外部复位直接喂给所有触发器,导致释放时刻不同步,部分 FF 已经退出复位、部分还在复位中,状态机跑飞。

9.8.1 标准模板
verilog 复制代码
module reset_sync (
    input  wire clk,
    input  wire rst_async_n,    // 异步复位输入(低有效)
    output wire rst_sync_n      // 同步释放、异步断言
);
    (* ASYNC_REG = "TRUE" *) reg ff1, ff2;

    always @(posedge clk or negedge rst_async_n) begin
        if (!rst_async_n) begin
            ff1 <= 1'b0;
            ff2 <= 1'b0;
        end else begin
            ff1 <= 1'b1;
            ff2 <= ff1;
        end
    end

    assign rst_sync_n = ff2;
endmodule
9.8.2 原理
  • 断言(拉低):rst_async_n 一拉低,无需等时钟,ff1/ff2 立即清零,复位立刻生效

  • 释放(拉高) :rst_async_n 拉高后,必须等两个 clk 上升沿,ff2 才变 1,复位同步释放

  • 所有目的域 FF 都用 rst_sync_n,就保证退出复位时的同一时刻整齐释放

9.8.3 每个时钟域用一个复位同步器

reset_sync u_rst_sync_100m (.clk(clk_100m), .rst_async_n(rst_ext_n),

.rst_sync_n(rst_100m_n));

reset_sync u_rst_sync_200m (.clk(clk_200m), .rst_async_n(rst_ext_n),

.rst_sync_n(rst_200m_n));

reset_sync u_rst_sync_usr (.clk(clk_usr), .rst_async_n(rst_ext_n),

.rst_sync_n(rst_usr_n));

9.9 Vivado 中的 CDC 约束写法

CDC 路径在时序分析中必须被正确处理,否则工具会按同步路径报一堆无法修复的违例。

9.9.1 声明异步时钟组

最彻底的方式:告诉工具两个时钟是异步的,跨域路径不做时序分析。

tcl 复制代码
# XDC 文件
create_clock -name clk_src -period 5.000 [get_ports clk_src_in]
create_clock -name clk_dst -period 3.333 [get_ports clk_dst_in]

set_clock_groups -asynchronous \
    -group [get_clocks clk_src] \
    -group [get_clocks clk_dst]
9.9.2 对单条路径设 false_path

更细粒度:只放过特定同步器链。

tcl 复制代码
# 针对两级同步器第一级的 D 端
set_false_path -to [get_pins -hier -filter {NAME =~ "*sync_ff1_reg*/D"}]
9.9.3 对握手数据总线设 max_delay

握手场景下,data_bus 虽然不做同步,但必须保证 req 到达前数据已稳定:

tcl 复制代码
# 限制 data_bus 的最大传输延迟不超过一个源时钟周期
set_max_delay -from [get_cells {u_src/data_bus_reg[*]}] \
              -to   [get_cells {u_dst/data_out_reg[*]}] \
              -datapath_only 5.000
9.9.4 ASYNC_REG 属性的作用

(* ASYNC_REG = "TRUE" *) reg sync_ff1, sync_ff2;

效果: - 告诉布局工具这些 FF 是同步器链,必须放在同一个 SLICE - 避免工具把这些 FF 做复制/重定时优化 - 让 Vivado 的 CDC 报告识别这是合法同步结构,不再报 CDC 违例

9.9.5 使用 report_cdc 命令
tcl 复制代码
# 在 opt_design 之后运行
report_cdc -severity {Critical Warning} -file cdc.rpt
report_cdc -details -file cdc_detail.rpt

输出分类: - CDC-1/2 :两级/三级同步器(安全) - CDC-10/11 :握手同步(需人工确认) - CDC-4 :无同步的跨域路径(危险 ,必须修复) - CDC-6:发散同步,单个源信号被多个同步器采样(可能引起不一致,需重构)

9.10 常见陷阱与调试方法

陷阱 1:同步器后再放组合逻辑分叉
verilog 复制代码
// ⚠️ 错误
reg sync1, sync2;
wire branch_a = sync2 & cond_a;
wire branch_b = sync2 & cond_b;
// sync2 输出如果是亚稳态,branch_a 和 branch_b 可能采到不一致的值

修复:先同步再用 FF 隔离一拍,再分叉。

陷阱 2:多个相关信号分别同步
verilog 复制代码
// ⚠️ 错误
sync_2ff u1 (.din(data_valid), .dout(data_valid_sync));
sync_2ff u2 (.din(data_last),  .dout(data_last_sync));
// valid 和 last 的同步延迟可能不同,目的域看到的时序关系被破坏

修复:要么合并成一个握手,要么通过 FIFO 打包传输。

陷阱 3:ASYNC_REG 忘加

工具默认会做 retiming 和复制,把同步链拆开甚至移动到不同 SLICE,亚稳态坍缩时间急剧下降。一定要加 ASYNC_REG = "TRUE"

陷阱 4:异步 FIFO 复位没处理好

两个域必须被复位一次,否则指针不一致直接假满/假空。推荐做法:

verilog 复制代码
// 写端复位:wr_rst_n 经自身域同步
// 读端复位:rd_rst_n 经自身域同步
// 两者共享同一个外部 rst_async_n
reset_sync u_wr_rst (.clk(wr_clk), .rst_async_n(rst_ext_n), .rst_sync_n(wr_rst_n));
reset_sync u_rd_rst (.clk(rd_clk), .rst_async_n(rst_ext_n), .rst_sync_n(rd_rst_n));
陷阱 5:同步器前后接组合逻辑
verilog 复制代码
// ⚠️ 错误
always @(posedge clk_dst) begin
    sync1 <= data_a & data_b;   // 同步器前做运算,组合延迟吃掉坍缩时间
end

修复 :同步器的 D 端必须直连源域寄存器的 Q 端,中间不能有组合逻辑。

调试建议
  • 静态检查:用 report_cdc 跑一遍,确保没有 CDC-4 违例

  • 第三方工具:SpyGlass CDC、Questa CDC 可做更完整的结构检查

  • 仿真:在跨域路径上加随机延迟模型,观察是否出现数据错位

  • ILA 抓取:在目的域抓取 data_sync、valid_sync,看是否有非预期毛刺

9.11 CDC 模式速查表

场景 推荐方案 关键代码模块
单 bit 电平信号 两级同步器 sync_2ff
快时钟脉冲 → 慢时钟 toggle + 同步 + 边沿检测 pulse_sync_fast2slow
慢时钟脉冲 → 快时钟 两级同步器 + 边沿检测 同上简化
多 bit 配置/控制字 握手同步 handshake_src/dst
连续变化的计数器 格雷码 + 两级同步 gray_sync
高吞吐数据流 异步 FIFO async_fifo
异步复位 异步断言同步释放 reset_sync

9.12 本章小结

CDC 的核心原则可以浓缩成三条:

  • 不让亚稳态传播:所有跨域单 bit 都要经过至少两级同步器,并加 ASYNC_REG

  • 不让多 bit 各自为战:多 bit 数据必须用握手、格雷码或 FIFO 打包传递

  • 不让复位成为漏网之鱼:每个时钟域都要有自己的 "异步断言、同步释放" 复位同步器

做好这三条,CDC 就从"玄学问题"变成"结构化问题"。

第 10 章 时序约束(XDC/SDC)专题

create_clock、set_input_delay、set_multicycle_path、set_case_analysis 全套写法

10.0 学这一章前,先建立 5 个核心直觉

很多人学 SDC/XDC 学不会,根本原因是没有建立这 5 个直觉

  • 时序约束不是"描述电路",而是"描述时序假设"。
    你是在告诉工具:"我假设这个信号在这个时间到达。"
    工具据此判断有没有违例。
text 复制代码
没约束的路径,工具默认按最严格方式分析,或者干脆不分析。
不写 create_clock → 工具不知道这是时钟。
不写 set_input_delay → 输入路径要么按 0 处理,要么报 unconstrained。
  • 所有时序分析的本质都是一个公式:
text 复制代码
到达时间(Arrival) ≤ 要求时间(Required)
tcl 复制代码
create_clock 决定周期。
set_input_delay / set_output_delay 决定起点和终点偏移。
set_multicycle_path / set_false_path 修改要求时间。
set_case_analysis 决定哪条路径根本不存在。
  • XDC 是 Xilinx 版本的 SDC,语法基本相同,差异在对象查询命令。

    • SDC 通用:get_pins, get_ports, get_clocks, get_cells

    • Vivado 特有:get_nets, all_fanout, all_fanin, get_debug_cores 等。

  • 写约束的顺序很关键:

    1. 时钟定义
      2) 时钟关系(生成时钟、异步时钟分组)
      3) 输入/输出延迟
      4) 例外(false path / multicycle / max_delay / min_delay)
      5) 物理约束(pin / IO standard / location)
      6) case_analysis(如果有静态配置信号)
  • 顺序乱了,工具可能按错误的时钟做分析。

10.1 create_clock:一切的起点

10.1.1 基本语法

create_clock -name <clk_name> -period [-waveform {rise fall}] [get_ports ]

10.1.2 最常见写法
单端 100 MHz 输入时钟

create_clock -name sys_clk -period 10.000 [get_ports sys_clk_p]

10.000 ns = 100 MHz。占空比默认 50%。

差分输入时钟

create_clock -name sys_clk -period 10.000 [get_ports sys_clk_p]

注意:差分时钟只需要约束 P 端,N 端由 IBUFDS 自动推断,不要重复写。

自定义占空比

create_clock -name slow_clk -period 20.000 -waveform {0 5} [get_ports slow_clk]

意思:周期 20ns,从 0 上升,到 5ns 下降。占空比 25%。

10.1.3 你必须知道的"两类时钟"
类型 命令 用途
主时钟 (Primary Clock) create_clock 来自芯片外部的时钟
生成时钟 (Generated Clock) create_generated_clock PLL/MMCM/分频器产生的时钟

误区 :很多人在 PLL 输出上又写一次 create_clock,这是错的

正确做法:

  • PLL/MMCM 的输出在 Vivado 中自动生成 generated clock,通常不需要手写。

  • 如果是 RTL 实现的分频器(如 clk_div2),必须手写 create_generated_clock。

10.1.4 RTL 分频器的约束

例:在 RTL 里写了一个二分频。

always @(posedge clk) clk_div2 <= ~clk_div2;

约束写法:

tcl 复制代码
create_generated_clock -name clk_div2 \
    -source [get_pins clk_div2_reg/C] \
    -divide_by 2 \
    [get_pins clk_div2_reg/Q]

关键点: - -source 是源时钟到达的引脚 - 目标对象是分频器输出引脚(通常是 FF 的 Q) - -divide_by 是分频比

10.1.5 常见错误
text 复制代码
在 PLL 输出再写一次 create_clock
→ 工具会把它当成异步时钟,跨时钟域路径全部报违例。
text 复制代码
忘记给 IO 时钟写 create_clock
→ 整个工程 unconstrained,时序报告全绿,但芯片上跑不起来。
text 复制代码
周期单位写错
→ period 10 表示 10 ns(默认),不要写 10ns。

10.2 set_input_delay:输入端口的时序契约

10.2.1 它到底在说什么?
tcl 复制代码
set_input_delay 不是说"信号延迟了多久",
而是说"相对于参考时钟边沿,数据在 input port 上稳定到达的时间"。

典型场景:上游芯片用某时钟在 T 时刻发出数据,数据经过 PCB 走线到达 FPGA 输入引脚。

10.2.2 基本语法
tcl 复制代码
set_input_delay -clock <clk_name> -max <ns> [get_ports <port>]
set_input_delay -clock <clk_name> -min <ns> [get_ports <port>]
  • -max 用于建立时间分析(最坏情况下数据到达多晚)

  • -min 用于保持时间分析(最好情况下数据到达多早)

10.2.3 经典写法:源同步接口

假设上游芯片以 100 MHz(10 ns 周期)发数据,输出延迟范围 2~6 ns(含 PCB 走线)。

tcl 复制代码
create_clock -name src_clk -period 10.000 [get_ports src_clk]

set_input_delay -clock src_clk -max 6.000 [get_ports din[*]]
set_input_delay -clock src_clk -min 2.000 [get_ports din[*]]

意义:

  • 最坏:数据在时钟沿后 6 ns 才稳定 → FPGA 端只剩 4 ns 来满足建立时间

  • 最好:数据可能在时钟沿后 2 ns 就变了 → 保持时间要求

10.2.4 系统同步 vs 源同步
类型 特征 约束写法
系统同步 上下游用同一个时钟源 用主时钟做 -clock
源同步 数据和时钟一起从上游发出 用输入时钟做 -clock

源同步是高速接口(DDR、千兆 MII、LVDS 接口)的常见模式。

10.2.5 双沿采样
tcl 复制代码
set_input_delay -clock src_clk -max 3.0 [get_ports din[*]]
set_input_delay -clock src_clk -max 3.0 -clock_fall -add_delay [get_ports din[*]]
set_input_delay -clock src_clk -min 1.0 [get_ports din[*]]
set_input_delay -clock src_clk -min 1.0 -clock_fall -add_delay [get_ports din[*]]

-add_delay 是关键。没加它,新的约束会覆盖旧的。

10.2.6 常见错误
text 复制代码
写了 -max 没写 -min
→ 保持时间不分析,硅后可能出现 hold 违例。
text 复制代码
参考了错误的时钟
→ 用 sys_clk 约束一个用 src_clk 采样的接口,分析完全失真。
text 复制代码
忘记 -add_delay 导致只剩最后一条
→ 双沿接口尤其常见。

10.3 set_output_delay:对外发送数据的契约

10.3.1 直觉
tcl 复制代码
set_output_delay 描述的是:
下游芯片要求"数据在它的时钟沿之前 X ns 必须稳定"。
10.3.2 基本语法
tcl 复制代码
set_output_delay -clock <clk_name> -max <ns> [get_ports <port>]
set_output_delay -clock <clk_name> -min <ns> [get_ports <port>]
10.3.3 经典写法

假设下游芯片要求:建立时间 2 ns,保持时间 0.5 ns,PCB 走线 1 ns。

tcl 复制代码
set_output_delay -clock sys_clk -max 3.0 [get_ports dout[*]]
set_output_delay -clock sys_clk -min -0.5 [get_ports dout[*]]
  • max = 下游建立时间 + PCB 走线 = 2 + 1 = 3

  • min = -(下游保持时间) = -0.5(可以为负)

set_output_delay 的 min 经常是负值,是正常的。

10.3.4 接口约束的完整套路(必须背下来)

任何一个对外接口,至少要写 4 条

tcl 复制代码
set_input_delay  -clock <clk> -max <a> [get_ports <input_port>]
set_input_delay  -clock <clk> -min <b> [get_ports <input_port>]
set_output_delay -clock <clk> -max <c> [get_ports <output_port>]
set_output_delay -clock <clk> -min <d> [get_ports <output_port>]

少一条都可能漏分析。

10.4 set_multicycle_path:当一拍不够用

10.4.1 这是什么场景?

有些路径逻辑上不需要一个周期就完成。例如:

  • 配置寄存器:写一次后好几百周期不变

  • 乘法器流水:实际允许 2~3 拍完成

  • 慢速接口:实际有 enable,几拍才采样一次

如果不告诉工具,它会按 1 个周期硬卡,导致:

  • 时序报告飘红

  • 工具拼命堆资源去满足一个根本不需要的约束

  • 实际可以跑得飞快,但综合不过

10.4.2 基本语法
tcl 复制代码
set_multicycle_path <N> -setup -from <start> -to <end>
set_multicycle_path <M> -hold  -from <start> -to <end>
  • N 是 setup 倍数

  • M 通常是 N-1

10.4.3 必须记住的"-2 法则"

只要写了 setup multicycle,必须同时写 hold multicycle,而且 hold 通常 = setup - 1。

正确套路:

tcl 复制代码
set_multicycle_path 2 -setup -from [get_cells src_reg] -to [get_cells dst_reg]
set_multicycle_path 1 -hold  -from [get_cells src_reg] -to [get_cells dst_reg]

如果只写 setup 不写 hold,hold 会被自动当成 N-1 周期的"严格保持",经常意外失败

10.4.4 实战例子 1:单周期乘法器实际允许 2 拍
verilog 复制代码
always @(posedge clk) begin
    if (mul_en) result <= a * b;  // mul_en 每两拍才高一次
end

约束:

tcl 复制代码
set_multicycle_path 2 -setup -from [get_cells {a_reg[*] b_reg[*]}] -to [get_cells result_reg[*]]
set_multicycle_path 1 -hold  -from [get_cells {a_reg[*] b_reg[*]}] -to [get_cells result_reg[*]]
10.4.5 实战例子 2:慢速控制寄存器
tcl 复制代码
set_multicycle_path 4 -setup -through [get_pins config_reg/*/D]
set_multicycle_path 3 -hold  -through [get_pins config_reg/*/D]
10.4.6 实战例子 3:跨时钟域(异步握手)

跨域同步链路径不要用 multicycle,要用 set_false_path 或 set_max_delay -datapath_only。

后面专门讲。

10.4.7 常见错误
  • 只写 -setup 不写 -hold → hold 违例

  • 倍数搞反(hold > setup)→ 直接违反语义

  • 用 multicycle 处理异步路径 → 错误工具,应该用 false_path

  • 忘记 -through 或 -from/-to 指向错对象 → 约束范围错位

10.5 set_false_path:告诉工具"别管它"

10.5.1 什么时候用?
  • 异步复位的释放路径(已经做了同步器)

  • 配置寄存器到逻辑(CPU 写完很久才用)

  • 跨时钟域路径(已经做了 2FF 同步器或异步 FIFO)

  • 调试信号(ILA tap、LED 输出)

10.5.2 基本语法
tcl 复制代码
set_false_path -from <start> -to <end>
set_false_path -from [get_clocks clk_a] -to [get_clocks clk_b]
10.5.3 异步复位经典写法

set_false_path -from [get_ports rst_n]

或者更精细:

tcl 复制代码
set_false_path -from [get_ports rst_n] -to [get_pins */PRE]
set_false_path -from [get_ports rst_n] -to [get_pins */CLR]
10.5.4 跨时钟域(CDC)路径

如果 clk_a 和 clk_b 完全异步:

tcl 复制代码
set_false_path -from [get_clocks clk_a] -to [get_clocks clk_b]
set_false_path -from [get_clocks clk_b] -to [get_clocks clk_a]
text 复制代码
注意:这种写法会让工具完全不管这两个域之间的路径。
前提是你在 RTL 已经做好了 CDC 同步(2FF 同步器 / 异步 FIFO / 握手)。
10.5.5 更安全的 CDC 写法:set_max_delay

set_max_delay -datapath_only -from [get_clocks clk_a] -to [get_clocks clk_b] 5.0

-datapath_only 表示:

  • 忽略时钟偏斜

  • 只看数据路径延迟是否在 5 ns 内

这样可以限制 2FF 同步器之间的走线,不会被工具拉到天涯海角,比 false_path 更稳

10.6 set_clock_groups:异步时钟分组的优雅写法

10.6.1 痛点

如果你工程里有 5 个异步时钟,写 set_false_path 要写 20 条(两两组合,双向)。

10.6.2 一条解决
tcl 复制代码
set_clock_groups -asynchronous \
    -group [get_clocks clk_a] \
    -group [get_clocks clk_b] \
    -group [get_clocks clk_c]

意思:a、b、c 互相之间都是异步关系,所有跨域路径自动 false_path。

10.6.3 三种 group 类型
选项 含义
-asynchronous 三组之间互为异步(最常用)
-physically_exclusive 物理上不会同时存在(如 MUX 选择不同时钟)
-logically_exclusive 逻辑上不会同时活跃(与 case_analysis 配合用)

10.7 set_case_analysis:把电路"假设掉"

10.7.1 这是什么神器

set_case_analysis 告诉工具:

"在我这个工程里,这个引脚永远是 0(或 1),请按这个假设做时序分析。"

最经典用法:多时钟 MUX 切换器

10.7.2 经典场景

assign clk_out = sel ? clk_a : clk_b;

如果不约束,工具会同时分析 clk_a 和 clk_b 经过 MUX 后产生的两个生成时钟,可能造成假违例。

如果硬件上 sel 在上电时由外部跳线决定,永远是 1:

set_case_analysis 1 [get_ports sel]

工具就只分析 clk_a 这条路径。

10.7.3 另一个场景:模式选择信号
verilog 复制代码
generate
    if (MODE == "FAST") begin : g_fast
        ...
    end else begin : g_slow
        ...
    end
endgenerate

如果 MODE 是 RTL 参数,没问题;

但如果 MODE 是一个静态拨码开关:

set_case_analysis 1 [get_ports mode_sel]

工具就只分析那一路。

10.7.4 配合 set_clock_groups 使用
tcl 复制代码
set_clock_groups -logically_exclusive \
    -group [get_clocks clk_a] \
    -group [get_clocks clk_b]

set_case_analysis 1 [get_ports clk_sel]

意义:两个时钟逻辑互斥,外加 sel 固定,工具完全清楚"只分析这一路"。

10.7.5 常见错误
  • 把动态信号当静态用 → 实际芯片中 sel 会切换,但你写了 case_analysis,硅后挂掉

  • case_analysis 写在错误的对象上 → 应该在引脚或网络上写,不是寄存器输出

  • 忘记同时设置 clock_groups → 时钟还是会同时分析

10.8 set_max_delay / set_min_delay:精确控制

10.8.1 用途

当 multicycle 不够灵活,false_path 太放任时,用 max_delay/min_delay 精确控制:

tcl 复制代码
set_max_delay 8.0 -from [get_cells src_reg] -to [get_cells dst_reg]
set_min_delay 1.0 -from [get_cells src_reg] -to [get_cells dst_reg]
10.8.2 -datapath_only 选项

CDC 路径上几乎必加:

tcl 复制代码
set_max_delay -datapath_only 5.0 \
    -from [get_clocks clk_a] -to [get_clocks clk_b]

含义: - 不考虑时钟偏斜 - 仅约束数据路径的物理延迟 - 用于约束 2FF 同步器之间的走线

10.9 一个完整的 XDC 例子(必须能默写)

tcl 复制代码
###############################################
# 1) 主时钟定义
###############################################
create_clock -name sys_clk_100  -period 10.000 [get_ports sys_clk_p]
create_clock -name src_clk_50   -period 20.000 [get_ports src_clk]

###############################################
# 2) 生成时钟(手写分频器才需要)
###############################################
create_generated_clock -name clk_div2 \
    -source [get_pins div2_reg/C] \
    -divide_by 2 \
    [get_pins div2_reg/Q]

###############################################
# 3) 异步时钟分组
###############################################
set_clock_groups -asynchronous \
    -group [get_clocks sys_clk_100] \
    -group [get_clocks src_clk_50]

###############################################
# 4) 输入/输出延迟
###############################################
set_input_delay  -clock src_clk_50 -max 6.0 [get_ports din[*]]
set_input_delay  -clock src_clk_50 -min 2.0 [get_ports din[*]]

set_output_delay -clock sys_clk_100 -max 3.0  [get_ports dout[*]]
set_output_delay -clock sys_clk_100 -min -0.5 [get_ports dout[*]]

###############################################
# 5) 例外
###############################################
# 复位异步释放
set_false_path -from [get_ports rst_n]

# 配置寄存器是慢速路径
set_multicycle_path 4 -setup -through [get_pins cfg_reg/*/D]
set_multicycle_path 3 -hold  -through [get_pins cfg_reg/*/D]

# CDC 路径:限制数据走线
set_max_delay -datapath_only 5.0 \
    -from [get_clocks src_clk_50] -to [get_clocks sys_clk_100]

###############################################
# 6) case_analysis(静态配置)
###############################################
set_case_analysis 1 [get_ports clk_sel]

10.10 调试与验证:写完怎么知道对?

10.10.1 Vivado 必看命令
tcl 复制代码
report_clocks
report_clock_networks
report_clock_interaction
report_timing_summary
check_timing
report_exceptions
10.10.2 必看的 4 张报告
  • Clock Interaction Report

    看每对时钟之间的关系:Synchronous / Asynchronous / No common period / Ignored

    有任何意外关系,立即修。

  • Check Timing

    会列出:

    • unconstrained endpoints

    • no_clock

    • no_input_delay

    • no_output_delay

      这四个为 0 才算约束完整。

  • Timing Summary

    WNS / WHS / TNS / THS 必须为正。

  • Report Exceptions

    你写过的 multicycle / false_path / max_delay 都列出来。

    看是否有"invalid"或"covered by stronger exception"。

10.10.3 SDC/XDC 优先级(必背)

当多个约束作用于同一条路径,优先级从高到低:

tcl 复制代码
set_false_path
  > set_max_delay / set_min_delay
    > set_multicycle_path
      > 默认时序约束(来自 create_clock + set_input/output_delay)

强者覆盖弱者。

10.11 高频翻车点速查表

翻车现象 真实原因
时序报告全绿但硅后跑不起来 unconstrained 路径,没建立时钟约束
WNS 一直为负,怎么改 RTL 都过不了 漏写 multicycle,工具按 1 拍卡
CDC 路径偶发出错 用了 false_path 没限走线,2FF 之间被拉得太远
Hold 违例 写了 setup multicycle 没写 hold multicycle
时钟域意外被当作同步 没写 clock_groups -asynchronous
PLL 输出路径全部红 在 PLL 输出又写了一次 create_clock
MUX 时钟乱报违例 没写 case_analysis 和 logically_exclusive
输入路径报 no_input_delay 漏写 set_input_delay

10.12 本章必背 10 条口诀

  • 没 create_clock,一切免谈。

  • PLL 输出别再 create_clock,要用 generated_clock。

  • set_input_delay 写最大也要写最小。

  • set_output_delay 的 min 经常是负的,正常。

  • 写了 multicycle setup 必须写 multicycle hold(hold = setup - 1)。

  • CDC 用 set_max_delay -datapath_only,比 false_path 更稳。

  • 多个异步时钟用 set_clock_groups -asynchronous 一行搞定。

  • 静态选择信号一律 set_case_analysis。

  • 写完跑 check_timing,看 unconstrained 是不是 0。

  • 优先级:false_path > max_delay > multicycle > 默认。

10.13 一页速查卡(建议打印贴墙)

text 复制代码
┌──────────────────────────────────────────────────────────────┐
│  时序约束顺序                                                │
│  ─────────────────────────────────────────────────────────  │
│  1. create_clock                                             │
│  2. create_generated_clock                                   │
│  3. set_clock_groups / set_false_path (时钟域之间)            │
│  4. set_input_delay  -max/-min                               │
│  5. set_output_delay -max/-min                               │
│  6. set_multicycle_path -setup / -hold                       │
│  7. set_max_delay -datapath_only (CDC)                       │
│  8. set_case_analysis                                        │
│  9. 物理约束 (PACKAGE_PIN / IOSTANDARD)                      │
│                                                              │
│  自检:                                                      │
│   check_timing → unconstrained 必须为 0                      │
│   report_clock_interaction → 没有 unexpected sync             │
│   report_timing_summary → WNS/WHS/TNS/THS 全部为正           │
└──────────────────────────────────────────────────────────────┘

第 11 章 UltraScale+ / Versal 架构专属优化

CLB 架构差异、Clock Region、SLR 跨越、NoC 使用

11.0 开篇:为什么这一章必须单独讲

如果你之前只做过 7 系列(Artix-7 / Kintex-7 / Virtex-7),你在 UltraScale+ 或 Versal 上一定踩过这几类坑:

  • 时序报告突然出现 SLR crossing 违例,以前从没见过这个词。

  • 一个时钟 fanout 一高,Vivado 就开始报 BUFG 不够用

  • 放置 100 万个 FF 的设计,综合 5 分钟,布线 3 小时

  • Versal 上的 DDR/HBM 必须走 NoC,但你根本不知道 NoC 是什么

  • 明明逻辑只占 30%,时序就是收不住

这些问题的根本原因不是 RTL 不好,而是:

UltraScale+ 和 Versal 是"多 die 封装 + 分区时钟 + 硬核网络"的芯片,它们的优化方法和 7 系列完全不同。

这一章的目标是建立 4 个核心直觉:

  • CLB 的改变意味着逻辑密度和搬运方式变了

  • Clock Region 的引入意味着时钟是有"地盘"的

  • SLR 的存在意味着物理上是多颗芯片拼起来的

  • NoC 的出现意味着Versal 上的高带宽通信不再走 PL fabric

11.1 CLB 架构差异:7 系列 → UltraScale+ → Versal

11.1.1 7 系列的 CLB(作为对比基准)

7 系列 CLB 里:

  • 每个 CLB 包含 2 个 Slice

  • 每个 Slice 包含 4 个 6-input LUT + 8 个 FF

  • Slice 分 SLICEL 和 SLICEM(后者支持分布式 RAM / SRL)

关键特征: - 进位链在 Slice 内部是 4 位 - LUT/FF 比 = 4:8 ,FF 较富裕 - 控制信号(CE/SR)每个 Slice 一组

11.1.2 UltraScale / UltraScale+ 的 CLB

结构性变化非常大

对比项 7 系列 UltraScale+
每个 CLB 的 Slice 数 2 1(只是命名改了,里面更大)
每个 Slice 的 LUT 4 8
每个 Slice 的 FF 8 16
进位链宽度 4 8
控制信号组 1 2(CE 独立分组)
LUT 输入 6 6(但连接更灵活)

你必须记住三件事:

① 一个 Slice 里有 8 个 LUT 和 16 个 FF
text 复制代码
这意味着LUT/FF 比是 1:2,FF 相对更多。
很多时序优化来自在 LUT 后多加一级 FF,UltraScale+ 天生就鼓励流水。
② 进位链从 4 位变成 8 位

以前 32 位加法器要 8 级进位 Slice,现在只要 4 级。

这直接导致:

  • 加法器延迟降低

  • DSP 外围逻辑更紧凑

  • 但也意味着一条进位链更长、更"硬",一旦拉长就跨 Slice 成本高

③ 控制信号分成两组

一个 Slice 内的 16 个 FF 分成 两组 8 个每组有独立的 CE(时钟使能)和 SR(复位)

这个变化非常关键,直接影响 RTL 写法(见 11.1.4)。

11.1.3 Versal 的 CLB:又一次重构

Versal 的 CLB 进一步优化:

  • 每个 CLB 仍是 1 个 Slice

  • 每个 Slice 32 个 FF + 8 个 LUT6

  • LUT/FF 比飙升到 1:4

  • 进位链 8 位,但专门的 IMR(Imux Register) 被提出来,用来做流水线插入

  • 新增 LUTRAM 的灵活性,分布式 RAM 更强

直觉上:

Versal 的 fabric 被设计成**"寄存器非常多,LUT 是稀缺资源"**,所以 Versal 鼓励大量流水。

11.1.4 RTL 写法的直接影响
规则 1:控制信号要"统一"

7 系列你可以随便写:

verilog 复制代码
always @(posedge clk) begin
    if (rst_a) q1 <= 0;
    else if (ce_a) q1 <= d1;
end

always @(posedge clk) begin
    if (rst_b) q2 <= 0;
    else if (ce_b) q2 <= d2;
end

在 UltraScale+ 上,如果 q1 和 q2 被放到同一组 8 个 FF 里,但它们的 CE/SR 不同:

Vivado 无法打包,会把它们拆到不同 Slice,资源浪费显著。

优化写法:同一个模块里的寄存器组,尽量共享 CE 和 SR。

verilog 复制代码
always @(posedge clk) begin
    if (rst_common) begin
        q1 <= 0;
        q2 <= 0;
    end else if (ce_common) begin
        q1 <= d1;
        q2 <= d2;
    end
end

Vivado 报告中看什么?

report_utilization -hierarchical

text 复制代码
看 "Control Sets" 数量。
一般经验:Control Sets 数量 > FF 总数 / 8 时,说明控制信号太碎,Slice 打包率会掉。
规则 2:流水线要舍得插

UltraScale+ 的 FF 资源非常富余(比 7 系列多一倍),而时序收敛主要靠缩短 LUT 级联深度

写 RTL 时的直觉:

每 2~3 级组合逻辑后,强制插一级 FF。

特别是在 DSP 输出、BRAM 输出、宽加法器后,不插 FF 后果非常严重

规则 3:进位链不要跨 Slice

加法器宽度尽量是 8 的倍数(8/16/32/64),不要写 35 位加法器,会导致进位链跨 Slice 效率下降。

11.2 Clock Region(时钟区):时钟是有地盘的

11.2.1 这是什么?

UltraScale+ 芯片不是一块均匀的 fabric,它被物理分割成若干 Clock Region,每个区大约:

  • 60 行 CLB × 一定列数

  • 包含 24 个 BUFCE_LEAF

  • 自己的时钟布线基础设施

一个典型的 XCZU9EG:

  • 共 14 个 Clock Region

  • 每个 Region 都有独立的时钟网络入口

11.2.2 BUFG/BUFGCE/BUFGCE_DIV/BUFG_GT

UltraScale+ 上时钟缓冲器不只是 "BUFG"。

类型 用途
BUFGCE 最常用的全局时钟缓冲,支持 CE
BUFGCE_DIV 带分频的全局时钟缓冲(分频 1~8)
BUFG_PS PS(ZYNQ)专用
BUFG_GT 给 GT(Transceiver)用
BUFGCTRL 时钟 MUX

每个 Clock Region 支持的时钟数量是有限的

  • 一个 Clock Region 内最多 24 个独立时钟

  • 全芯片大约 32 个 BUFGCE

超了会怎样?→ 工具会报 clock placement 错误,或自动把 BUFGCE 换成 BUFH/BUFCE_LEAF,时序质量直线下降

11.2.3 Clock Region 相关命令(必会)
tcl 复制代码
# 查看时钟资源使用
report_clock_utilization

# 查看某个时钟覆盖哪些 region
report_clocks -object_list [get_clocks sys_clk]

# 强制时钟放在某个 region
set_property

第12章 FPGA高速接口时序实战

本章聚焦 Xilinx 7 系列 / UltraScale+ 架构下三类典型高速接口的时序处理方法。

12.1 高速接口的时序挑战总览

普通同步电路里,setup/hold 的分析对象是 FPGA 内部 FF 到 FF 的路径,由工具自动完成。但一旦涉及到外部高速接口,设计师要亲自处理以下问题:

问题类别 具体表现
接收端采样相位 数据和时钟边沿的对齐关系未知,需要动态校准
位错位(bit slip) 串行流里数据位的起始位置不确定
字边界(word align) 解串后不知道哪 8/10 个 bit 是一个字符
通道间偏差(skew) 多条差分对之间的到达时间不一致
时钟恢复 接收端没有独立时钟,必须从数据里恢复
多时钟域交界 PHY 域、用户域、系统域之间的 CDC
复位时序 各级 PLL/PHY 有严格的复位释放先后要求

高速接口的"时序"不再是简单的 T_clk ≥ T_logic + ...,而是涵盖眼图、抖动、偏斜、校准、对齐的系统级时序。

12.2 源同步接口与系统同步接口

12.2.1 系统同步(System Synchronous)

发送端和接收端共享一个系统时钟,数据在发送端对齐到时钟边沿,接收端用同一个时钟采样。

  • 适用:低速接口(< 200 MHz)
  • 优点:约束简单,set_input_delay 就能搞定
  • 缺点:时钟频率越高,PCB 走线偏差越致命
12.2.2 源同步(Source Synchronous)

发送端在发送数据的同时送出一个随路时钟(DQS、CLK_FWD),接收端用这个随路时钟采样。

  • 适用:LVDS 视频接口、DDR、高速并行总线
  • 优点:数据和时钟在 PCB 上走相同长度,偏差小
  • 缺点:接收端需要额外的相位调整电路
12.2.3 数据与时钟的相位关系

源同步接口又细分两种:

类型 特征 例子
Edge-Aligned(边沿对齐) 数据和时钟边沿同时翻转 多数视频 LVDS
Center-Aligned(中心对齐) 时钟边沿位于数据稳定中心 DDR 写数据、某些 ADC

接收端必须知道自己面对的是哪种,才能正确选择采样边沿或移相方式。

12.3 LVDS 接收:IDELAY / ISERDES 实战

12.3.1 典型数据路径

差分 Pin ──► IBUFDS ──► IDELAYE2/3 ──► ISERDESE2/3 ──► 并行数据 ▲ bitslip / train

  • IBUFDS:差分输入缓冲
  • IDELAY:可编程抽头延迟线,用于调整采样相位
  • ISERDES:串并转换,把串行比特拆成并行字
12.3.2 Verilog 实例化(7 系列为例)
verilog 复制代码
// 1. 差分输入
wire data_p, data_n;
wire data_ibuf;
IBUFDS #(.DIFF_TERM("TRUE"), .IOSTANDARD("LVDS_25"))
    u_ibufds (.I(data_p), .IB(data_n), .O(data_ibuf));

// 2. 可变延迟线
wire data_delayed;
wire [4:0] delay_tap;     // 用户控制的 tap 值
IDELAYE2 #(
    .IDELAY_TYPE   ("VAR_LOAD"),     // 支持运行时加载
    .DELAY_SRC     ("IDATAIN"),
    .IDELAY_VALUE  (0),
    .HIGH_PERFORMANCE_MODE ("TRUE"),
    .SIGNAL_PATTERN("DATA"),
    .REFCLK_FREQUENCY (200.0),       // 必须匹配 IDELAYCTRL 参考时钟
    .CINVCTRL_SEL  ("FALSE"),
    .PIPE_SEL      ("FALSE")
) u_idelay (
    .DATAOUT    (data_delayed),
    .IDATAIN    (data_ibuf),
    .C          (clk_div),
    .CE         (1'b0),
    .INC        (1'b0),
    .CNTVALUEIN (delay_tap),
    .LD         (tap_load),
    .LDPIPEEN   (1'b0),
    .CINVCTRL   (1'b0),
    .REGRST     (1'b0),
    .DATAIN     (1'b0),
    .CNTVALUEOUT()
);

// 3. 串并转换(8:1 DDR 示例,输入 bit 率 = 8 × 并行字率)
wire [7:0] rx_data;
wire bitslip;
ISERDESE2 #(
    .DATA_RATE        ("DDR"),
    .DATA_WIDTH       (8),
    .INTERFACE_TYPE   ("NETWORKING"),
    .IOBDELAY         ("BOTH"),
    .NUM_CE           (2),
    .SERDES_MODE      ("MASTER")
) u_iserdes (
    .Q1(rx_data[0]), .Q2(rx_data[1]), .Q3(rx_data[2]), .Q4(rx_data[3]),
    .Q5(rx_data[4]), .Q6(rx_data[5]), .Q7(rx_data[6]), .Q8(rx_data[7]),
    .CLK     (clk_fast),    // 位速率时钟
    .CLKB    (~clk_fast),
    .CLKDIV  (clk_div),     // 字速率时钟 = 位速率 / 4(DDR 时 × 2)
    .DDLY    (data_delayed),
    .BITSLIP (bitslip),
    .CE1(1'b1), .CE2(1'b1),
    .RST     (rst),
    .OCLK(1'b0), .OCLKB(1'b0), .D(1'b0), .OFB(1'b0), .DYNCLKDIVSEL(1'b0),
    .DYNCLKSEL(1'b0), .SHIFTIN1(1'b0), .SHIFTIN2(1'b0)
);

// 4. IDELAYCTRL 必须例化一次(每个 IO Bank Group)
IDELAYCTRL u_idelayctrl (
    .RDY    (idelayctrl_rdy),
    .REFCLK (clk_ref_200m),     // 固定 200 MHz 或 300 MHz
    .RST    (rst_idelayctrl)
);

#### 12.3.3 关键参数说明

- **CLK vs CLKDIV**:CLK 是高速时钟,CLKDIV 是并行字时钟;两者频率比由 DATA_WIDTH 和 DATA_RATE 决定

- **IDELAYCTRL**:所有 IDELAY 必须关联到一个 IDELAYCTRL,后者需要精确的 200/300 MHz 参考,用于内部电压/温度补偿

- **DIFF_TERM**:打开片内 100Ω 差分终端,省掉板上电阻

### 12.4 LVDS 发送:OSERDES 与 TX 对齐

#### 12.4.1 典型数据路径

```text
并行数据 ──► OSERDESE2/3 ──► ODELAYE2/3 ──► OBUFDS ──► 差分 Pin
                                                  ▲
                                           随路时钟前向
12.4.2 代码片段
verilog 复制代码
// 8:1 OSERDES 发送
wire tx_serial;
OSERDESE2 #(
    .DATA_RATE_OQ ("DDR"),
    .DATA_RATE_TQ ("SDR"),
    .DATA_WIDTH   (8),
    .TRISTATE_WIDTH(1),
    .SERDES_MODE  ("MASTER")
) u_oserdes (
    .OQ      (tx_serial),
    .OFB     (),
    .TQ      (),
    .CLK     (clk_fast),
    .CLKDIV  (clk_div),
    .D1(tx_par[0]), .D2(tx_par[1]), .D3(tx_par[2]), .D4(tx_par[3]),
    .D5(tx_par[4]), .D6(tx_par[5]), .D7(tx_par[6]), .D8(tx_par[7]),
    .OCE     (1'b1),
    .RST     (rst),
    .TBYTEIN(1'b0), .T1(1'b0), .T2(1'b0), .T3(1'b0), .T4(1'b0),
    .TCE(1'b0), .SHIFTIN1(1'b0), .SHIFTIN2(1'b0)
);

OBUFDS u_obufds (.I(tx_serial), .O(tx_p), .OB(tx_n));

// 时钟前向:发送一路固定 pattern (1010...) 作为接收端随路时钟
wire clk_fwd_serial;
OSERDESE2 #(
    .DATA_RATE_OQ("DDR"), .DATA_WIDTH(8), .SERDES_MODE("MASTER")
) u_clk_fwd (
    .OQ(clk_fwd_serial),
    .D1(1'b1), .D2(1'b0), .D3(1'b1), .D4(1'b0),
    .D5(1'b1), .D6(1'b0), .D7(1'b1), .D8(1'b0),
    .CLK(clk_fast), .CLKDIV(clk_div),
    .OCE(1'b1), .RST(rst),
    /* 其余端口省略 */
);
OBUFDS u_clk_obufds (.I(clk_fwd_serial), .O(clk_p), .OB(clk_n));
12.4.3 发送端的相位对齐

发送端一般不需要动态调整,但要在约束里声明输出数据相对于前向时钟的对齐关系(边沿对齐/中心对齐),让 Vivado 为 PCB 扇出算好 max_delay/min_delay。

12.5 动态位对齐(Bitslip)与字对齐

LVDS 接口上电后,接收端的 ISERDES 产生的并行字字边界位置随机。比如真实字符 0xA5 = 10100101,接收端可能输出 01011010(移位了 4 bit)。

12.5.1 对齐流程
text 复制代码
    ┌──────────────────────────────┐
    │  1. 发送端持续发送训练字符    │
    │     (通常是 0xBC、K28.5 等)   │
    └──────────────┬───────────────┘
                   ▼
    ┌──────────────────────────────┐
    │  2. 接收端循环 bitslip         │
    │     直到采到期望训练字符       │
    └──────────────┬───────────────┘
                   ▼
    ┌──────────────────────────────┐
    │  3. 对齐后切换为正常数据流     │
    └──────────────────────────────┘
12.5.2 简化对齐状态机
verilog 复制代码
module bitslip_align #(
    parameter TRAIN_WORD = 8'hBC,
    parameter MAX_TRY    = 16
) (
    input  wire       clk_div,
    input  wire       rst_n,
    input  wire [7:0] rx_word,
    output reg        bitslip,
    output reg        aligned
);
    localparam S_SEARCH = 2'd0, S_CHECK = 2'd1, S_DONE = 2'd2;
    reg [1:0]  state;
    reg [3:0]  try_cnt;
    reg [3:0]  good_cnt;

    always @(posedge clk_div or negedge rst_n) begin
        if (!rst_n) begin
            state    <= S_SEARCH;
            bitslip  <= 1'b0;
            aligned  <= 1'b0;
            try_cnt  <= 0;
            good_cnt <= 0;
        end else begin
            bitslip <= 1'b0;
            case (state)
                S_SEARCH: begin
                    if (rx_word == TRAIN_WORD) begin
                        good_cnt <= good_cnt + 1;
                        if (good_cnt == 4'd15) begin
                            aligned <= 1'b1;
                            state   <= S_DONE;
                        end
                    end else begin
                        good_cnt <= 0;
                        bitslip  <= 1'b1;       // 拨一位再试
                        try_cnt  <= try_cnt + 1;
                        if (try_cnt == MAX_TRY-1)
                            state <= S_CHECK;   // 拨了一圈还不行
                    end
                end
                S_CHECK: begin
                    // 可能需要调 IDELAY tap 重新对齐;此处略
                    state <= S_SEARCH;
                    try_cnt <= 0;
                end
                S_DONE: /* 对齐完成 */ ;
            endcase
        end
    end
endmodule
12.5.3 眼图扫描与 IDELAY 校准

真实工程里,还要扫描 IDELAY tap 找到"眼睛中心":

for tap = 0 to 31:

load tap to IDELAY

capture N words, count errors

record error count[tap]

eye_center = 找出 error=0 的连续区间中点

load eye_center to IDELAY

这一步能显著提高 PVT 变化下的稳定性。

12.6 GT SerDes 收发器架构

Xilinx 的高速 SerDes 家族(GTX / GTH / GTY / GTM)速率从 6 Gbps 到 112 Gbps,内部结构高度复杂。

12.6.1 分层模型
text 复制代码
┌───────────────────────────────────────┐
│ 用户逻辑 (User Logic)                 │
│   TXUSRCLK / RXUSRCLK 域              │
├───────────────────────────────────────┤
│ PCS  物理编码子层                     │
│  - 8b/10b 或 64b/66b 编解码           │
│  - 弹性缓冲 / 时钟补偿                │
│  - Comma 对齐                          │
├───────────────────────────────────────┤
│ PMA  物理介质附着子层                 │
│  - 串并/并串转换                       │
│  - CDR 时钟恢复                        │
│  - 均衡 (DFE/CTLE)                     │
│  - 差分驱动/接收                       │
└───────────────────────────────────────┘
       ▲                      ▲
       │                      │
   MGTREFCLK            差分 RX/TX Pin
12.6.2 两种用户接口速率
接口类型 举例 用户数据位宽
8b/10b PCIe Gen1/2、SATA、SDI 16/32/64 bit
64b/66b 10GbE、Interlaken 32/64 bit
12.6.3 三个关键时钟
  • MGTREFCLK:外部晶振喂入的参考时钟,GT PLL 的输入

  • TXUSRCLK / RXUSRCLK:用户逻辑读写 GT 的时钟,从 GT 的内部 PLL 分频得到

  • TXOUTCLK / RXOUTCLK:GT 输出的恢复时钟,通常经 BUFG_GT 后驱动 TXUSRCLK/RXUSRCLK

要点 :RX 恢复出来的时钟和 TX 参考时钟是异步的(除非使用 buffer bypass 和共享 PLL),因此 RX → TX 的内部数据通路需要做 CDC 或者使用弹性 buffer。

12.7 GT 的复位序列与时钟规划

GT 的复位顺序错了是初学者最常踩的坑。典型顺序:

  1. 等待外部 MGTREFCLK 稳定
    2. 释放 GTTXRESET / GTRXRESET (PMA 级)
    3. 等待 TXRESETDONE / RXRESETDONE 拉高
    4. 启动 TX/RX PCS 部分,等待 PLL 锁定
    5. 等待 RXCDRLOCK(CDR 锁定到数据)
    6. 释放用户逻辑侧复位
    7. 开始 comma 对齐 / 通道绑定 / 缓冲初始化

Vivado 生成的 GT Wizard Example Design 已经包含标准复位控制器(*_gtwizard_ultrascale_reset),建议直接用而不是自己写。

12.7.1 用 Wizard 的推荐流程
text 复制代码
Vivado IP Catalog → UltraScale FPGAs Transceivers Wizard
  ├─ 设置协议/线速率
  ├─ 选择参考时钟来源
  ├─ 配置 8b/10b 或 64b/66b
  ├─ 勾选 "Include Reset Controller"
  └─ 生成后调用示例顶层集成
12.7.2 BUFG_GT 使用

GT 输出的时钟不能直接接全局时钟树,必须经过 BUFG_GT:

BUFG_GT u_bufg_txusr (

.I (txoutclk),

.CE (1'b1),

.CEMASK (1'b0),

.CLR (1'b0),

.CLRMASK (1'b0),

.DIV (3'd0), // 可分频 1~8

.O (txusrclk)

);

12.8 8b/10b 对齐、通道绑定、时钟补偿

12.8.1 Comma 对齐

8b/10b 协议定义了 K28.5(十六进制 0xBC)作为 comma 字符,在 10 bit 编码里 bit 模式唯一,不会出现在任何普通数据组合中。GT 的 comma detect 单元扫描串行流,找到 comma 后把字边界对齐到 comma 位置。

配置关键参数:

  • ALIGN_COMMA_ENABLE = TRUE

  • ALIGN_COMMA_WORD = 1/2/4(每几字节允许对齐一次)

  • ALIGN_MCOMMA_VALUE、ALIGN_PCOMMA_VALUE:正/负 comma 值

12.8.2 通道绑定(Channel Bonding)

多通道协议(如 PCIe、Aurora)要求多个 lane 在字边界上对齐。做法:发送端周期性插入 ||A|| 等对齐字符,接收端的各 lane 独立对齐后,再用弹性缓冲把所有 lane 的 ||A|| 对齐到同一周期。

12.8.3 时钟补偿(Clock Correction)

TX 和 RX 参考时钟通常有几十 ppm 的频率差。长时间运行后,RX 一侧的弹性缓冲会越塞越满或越抽越空。解决方法:发送端周期性发送 skip 字符(如 K28.0),接收端的时钟补偿逻辑在缓冲偏离中心时插入或删除 skip 字符,保持缓冲平衡。

协议通常规定 skip 字符的最小间隔(比如每 ≤ 5000 个字符至少有一组 skip)。

12.9 MIG DDR3/DDR4 控制器时序要点

MIG(Memory Interface Generator)生成的控制器包含 PHY 校准、时序控制和用户接口,大部分复杂度被 IP 隐藏,但几个关键点仍需开发者理解。

12.9.1 DDR 接口的时序难点
信号 特点
DQ (数据) 双向、源同步、双沿
DQS (数据选通) 双向、写时中心对齐、读时边沿对齐
CK/CK# (命令时钟) 差分、单沿命令
ADDR/CMD 系统同步于 CK

写数据时 FPGA 把 DQS 摆到 DQ 的中心;读数据时 DRAM 把 DQS 摆到 DQ 的边沿,FPGA 必须自己把 DQS 延迟 90° 再去采样 DQ。

12.9.2 MIG 的校准阶段

MIG 上电后的自动校准序列(用户只看到 init_calib_complete 拉高):

  • Write Leveling:调整 DQS 相对 CK 的相位,让 DQS 上升沿对齐 CK 上升沿

  • Read Gate Calibration:找到 DQS 读选通窗口

  • Read Leveling (Per-bit DQ Deskew):对每条 DQ 单独 deskew

  • Write DQ Deskew:写方向 DQ 间偏差校准

  • Complex Pattern Calibration:用伪随机数据做 PVT 余量评估

这几个阶段加起来通常需要毫秒级时间,设计时要考虑上电后的等待。

12.9.3 时钟规划

MIG 内部有三个主要时钟:

  • sys_clk:用户喂给 MIG 的参考时钟(如 200 MHz)

  • ui_clk:MIG 产生的用户接口时钟(通常 = DDR 频率 / 4)

  • ref_clk:200 MHz 给 IDELAYCTRL 用

用户逻辑必须工作在 ui_clk 域,或者通过异步 FIFO 接入 ui_clk。

12.10 MIG 用户接口与跨域处理

12.10.1 用户接口信号(AXI 或 Native)

Native 接口关键信号:

app_cmd[2:0] // 命令 (读/写)

app_addr[ADDR_W] // 地址

app_en // 命令有效

app_rdy // 控制器准备好接受

app_wdf_data[DW] // 写数据

app_wdf_wren // 写数据有效

app_wdf_rdy // 写 FIFO 准备好

app_rd_data[DW] // 读数据

app_rd_data_valid // 读数据有效

协议:

  • 发命令:app_en & app_rdy 握手

  • 写数据:app_wdf_wren & app_wdf_rdy 握手(可与命令同拍或延后 1~2 拍)

  • 读数据:app_rd_data_valid 拉高时锁存 app_rd_data

12.10.2 用户域不是 ui_clk 的跨接方案

如果用户逻辑跑在自己的时钟(比如 PCIe 的 250 MHz),要和 MIG 的 ui_clk 跨域。推荐做法:

text 复制代码
用户域                                MIG 域 (ui_clk)
─────────────────                     ───────────────────
  命令 FIFO ───── async_fifo ────────► app_cmd/addr
  写 FIFO  ───── async_fifo ────────► app_wdf_data
                                                         
  读 FIFO  ◄──── async_fifo ──────── app_rd_data

三个异步 FIFO(见第 9 章)分别承担命令/写/读通道,既隔离时序也天然解决背压。

12.10.3 AXI 接口版本

Vivado MIG 也可以直接导出 AXI4 Memory Mapped 接口,这样用户逻辑可以通过 AXI Interconnect 连接,省掉手写 FIFO。代价是 AXI 协议本身有一定的延迟和资源开销。

12.11 高速接口的 XDC 约束模板

12.11.1 LVDS 源同步接收约束
tcl 复制代码
# 假设随路时钟 400 MHz DDR,数据相对时钟是中心对齐
create_clock -name clk_fwd -period 2.500 [get_ports clk_fwd_p]

# 输入数据的最大/最小延迟(相对前向时钟)
# 中心对齐时,数据窗口 = 时钟周期的一半(单沿)
set_input_delay -clock clk_fwd -max  0.600 [get_ports data_p[*]]
set_input_delay -clock clk_fwd -min -0.600 [get_ports data_p[*]]
set_input_delay -clock clk_fwd -max  0.600 [get_ports data_p[*]] -clock_fall -add_delay
set_input_delay -clock clk_fwd -min -0.600 [get_ports data_p[*]] -clock_fall -add_delay

# IDELAY 参考时钟
create_clock -name clk_ref200 -period 5.000 [get_ports clk_ref_p]
12.11.2 GT 参考时钟约束
tcl 复制代码
# 156.25 MHz MGT 参考时钟(10GbE 典型)
create_clock -name mgtrefclk0 -period 6.400 [get_ports mgt_refclk_p]

# GT 恢复时钟通常由 Wizard 自动约束;用户补上 bufg_gt 的时钟周期
create_clock -name txusrclk -period 3.103 [get_pins u_bufg_gt_tx/O]

# TX 与 RX 参考时钟异步
set_clock_groups -asynchronous \
    -group [get_clocks mgtrefclk0] \
    -group [get_clocks txusrclk] \
    -group [get_clocks rxusrclk]
12.11.3 MIG 约束

MIG 在生成时已经自带完整的 XDC(包含 DQ/DQS 约束、终端设置、Pin 分配),用户一般不要手动修改这些约束。只需要在顶层声明:

tcl 复制代码
# 用户域与 ui_clk 的异步关系
set_clock_groups -asynchronous \
    -group [get_clocks -include_generated_clocks {sys_clk_i}] \
    -group [get_clocks -include_generated_clocks {clk_pll_i}]

12.12 常见调试手段与问题定位

12.12.1 LVDS 对齐失败

症状:训练字符找不到、bitslip 循环满仍不对齐。

排查顺序:

  • 查 IDELAYCTRL 的 RDY:没拉高说明参考时钟没给对(必须 200/300 MHz 且精度达标)

  • 查 ISERDES 的 CLKDIV:频率是否是 CLK 的 1/4(SDR)或 1/2(DDR × 4-bit)

  • 查差分终端:DIFF_TERM = TRUE 开了吗,或板上 100Ω 加了吗

  • 示波器看眼图:确认物理信号幅度、抖动、占空比

  • 扫 IDELAY tap:用软件写一个从 0 到 31 的扫描程序,观察哪段 tap 数据稳定

12.12.2 GT 链路不通

使用 Vivado 的 IBERT (Integrated Bit Error Ratio Tester) IP 是排查 GT 的首选工具。IBERT 可以:

  • 实时查看每路 eye scan

  • 自动调整均衡参数

  • 测量误码率

其他排查项:

  • MGTREFCLK 频率是否和 Wizard 配置一致

  • 参考时钟是否从正确的 quad 引入

  • 协议 tx_diffctrl / txpre / txpost 是否合理

  • 复位序列是否按照 Wizard 顺序

  • 对端的 polarity 是否需要反转(板上差分对可能接反)

12.12.3 MIG 校准失败

症状:init_calib_complete 长时间不拉高。

排查:

  • sys_clk 频率:是否与 IP 配置匹配,抖动是否超标

  • 复位:sys_rst 是否接正确(有的版本是低有效)

  • Pin 分配:是否和 MIG 生成的 XDC 一致(MIG 对 pin 位置有严格要求)

  • 板级信号完整性:VREF、ZQ 电阻是否正确

  • DRAM 型号:选错型号会导致 MR 寄存器写错

  • 看 ILA:抓 MIG 内部的 dbg_* 调试信号,Xilinx 文档 UG586/UG1412 有完整调试流程

12.12.4 通用建议
  • 所有高速接口尽量用官方 Wizard/IP,不要自己从零写 PHY

  • Example Design 先跑通,再往自己的设计里迁移

  • 板级信号完整性不能省:阻抗匹配、差分走线长度、回流路径都影响时序裕量

  • 预留 ILA 接入口:高速接口调试几乎一定需要 ILA + IBERT

12.13 本章小结

高速接口的时序处理和普通同步电路有本质不同,核心观念:

  • 时序不再是一个数字,而是一个窗口:采样点要落在数据眼图中央,而不是单纯满足 setup/hold

  • 校准是常态:IDELAY 扫描、GT 均衡、MIG 校准都是上电必须走完的流程

  • 跨域几乎不可避免:无论是 LVDS 的快慢时钟、GT 的 RX/TX,还是 MIG 的 ui_clk,都需要 CDC 处理

  • IP 优先,约束谨慎:Wizard/MIG 生成的约束不要乱改,自己只补顶层的时钟关系和跨域声明

  • 调试要有手段:IBERT / ILA / 眼图扫描 / 示波器,四件套缺一不可

12.14 三种接口对比速查表

维度 LVDS (SelectIO) GT SerDes MIG (DDR3/4)
典型速率 1~1.6 Gbps/lane 6~112 Gbps/lane 1600~3200 Mbps/pin
物理 LVDS 差分 CML 差分 SSTL 单端 + 差分 DQS
编码 无(原始并行) 8b/10b 或 64b/66b
时钟 随路前向时钟 嵌入式 CDR 系统同步 CK + DQS
校准 IDELAY 扫描 CDR + DFE + eye scan 多阶段自动校准
典型应用 视频、ADC、板间并行 PCIe、以太网、光模块 DRAM
用户接口 自己写 TXUSRCLK/RXUSRCLK 域 ui_clk + native/AXI

第 12 章 完

第 13 章 补充实战案例库:从报告到改 RTL 的完整闭环

本章用于补齐前面章节中容易"只懂概念、不知道怎么落地"的部分。建议把每个案例都按同一个流程处理:先读 Timing Summary,再定位 Worst Path,再判断根因,最后选择 RTL、约束或物理优化手段。

13.1 案例一:AXI-Stream 数据通路的流水线与反压

问题现象

某 256 bit AXI-Stream 数据通路在 300 MHz 下出现 setup 违例。Worst Path 显示 tdata 经过多级 mux、mask、checksum 计算后进入下游寄存器,逻辑级数超过 6 级。

错误思路

只给 tdata 加一级寄存器,但没有同步处理 tvalid、tlast、tkeep,结果仿真出现帧尾错位。

正确设计原则
  • tdata/tkeep/tlast/tuser 必须和 tvalid 同步推进。

  • 如果下游存在 tready 反压,需要 skid buffer 或完整 ready/valid 流水。

  • 只要路径是主数据面关键路径,优先从 RTL 插流水,而不是依赖 phys_opt。

简化 RTL 模板
verilog 复制代码
module axis_pipe_stage #(
    parameter DATA_W = 256,
    parameter KEEP_W = DATA_W/8
) (
    input  wire              clk,
    input  wire              rst_n,

    input  wire [DATA_W-1:0] s_tdata,
    input  wire [KEEP_W-1:0] s_tkeep,
    input  wire              s_tvalid,
    input  wire              s_tlast,
    output wire              s_tready,

    output reg  [DATA_W-1:0] m_tdata,
    output reg  [KEEP_W-1:0] m_tkeep,
    output reg               m_tvalid,
    output reg               m_tlast,
    input  wire              m_tready
);
    wire pipe_en = !m_tvalid || m_tready;
    assign s_tready = pipe_en;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            m_tvalid <= 1'b0;
            m_tlast  <= 1'b0;
        end else if (pipe_en) begin
            m_tvalid <= s_tvalid;
            m_tdata  <= s_tdata;
            m_tkeep  <= s_tkeep;
            m_tlast  <= s_tlast;
        end
    end
endmodule
验证要点
  • 连续包无反压:输出延迟固定 1 拍。

  • 中间随机拉低 m_tready:数据不丢、不重、不乱序。

  • tlast 与最后一个 tdata/tkeep 必须对齐。

13.2 案例二:复位同步释放与 reset fanout 优化

问题现象

工程中把外部 rst_n 直接接到多个时钟域,仿真正常,但上板偶发状态机跑飞。实现后还出现 reset 扇出过高、布线延迟过大。

正确处理

每个时钟域单独做"异步进入、同步释放"。对于大规模逻辑,再按区域复制 reset。

verilog 复制代码
module rst_sync_low_active (
    input  wire clk,
    input  wire rst_n_async,
    output wire rst_n_sync
);
    (* ASYNC_REG = "TRUE" *) reg [1:0] rst_pipe;

    always @(posedge clk or negedge rst_n_async) begin
        if (!rst_n_async)
            rst_pipe <= 2'b00;
        else
            rst_pipe <= {rst_pipe[0], 1'b1};
    end

    assign rst_n_sync = rst_pipe[1];
endmodule
XDC 建议
tcl 复制代码
set_false_path -from [get_ports rst_n_async]
set_property ASYNC_REG TRUE [get_cells -hier *rst_pipe_reg*]

如果 reset 扇出非常高,可以在每个模块或物理分区内增加一级本地 reset 寄存器,并检查 report_high_fanout_nets。

13.3 案例三:异步 FIFO 的约束闭环

问题现象

异步 FIFO 功能仿真正确,但 Vivado 报跨时钟域路径违例,尤其是写指针同步到读域、读指针同步到写域。

处理方法
  • 指针用 Gray Code。

  • 指针同步链加 ASYNC_REG。

  • 两个异步时钟用 set_clock_groups -asynchronous 或只对同步链前级 false path。

  • 不要用 multicycle path 处理真正异步的 CDC。

tcl 复制代码
set_clock_groups -asynchronous \
    -group [get_clocks wr_clk] \
    -group [get_clocks rd_clk]

set_property ASYNC_REG TRUE [get_cells -hier *wptr_gray_sync_reg*]
set_property ASYNC_REG TRUE [get_cells -hier *rptr_gray_sync_reg*]
验证要点
  • 写快读慢,FIFO 从空到满再到空。

  • 读快写慢,不能出现假数据。

  • 随机写读使能,比较参考队列。

  • 复位在两个时钟域不同时间释放,FIFO 状态必须最终恢复正常。

13.4 案例四:SLR Crossing 违例的处理

问题现象

在 UltraScale+ 大器件上,逻辑利用率只有 40%,但某条路径跨越 SLR,WNS 为 -0.8 ns。报告中出现 SLR crossing 或跨 die 互连。

优先级最高的处理方式
  • 架构上减少跨 SLR 通信。

  • 对必须跨 SLR 的总线插入 1-2 级寄存器。

  • 用 Pblock 将强相关逻辑约束在同一 SLR 或相邻区域。

  • 对高带宽跨区域数据流考虑 NoC、AXI NoC、硬核 DMA 或专用互连。

verilog 复制代码
// 跨 SLR 总线建议至少两级 pipeline
always @(posedge clk) begin
    slr_pipe0 <= src_bus;
    slr_pipe1 <= slr_pipe0;
    dst_bus   <= slr_pipe1;
end
Vivado 检查命令
tcl 复制代码
report_timing -slr_crossing -max_paths 20
report_design_analysis -congestion
report_qor_suggestions

13.5 案例五:LVDS 源同步输入的 XDC 模板

场景

外部 ADC 输出 8 路 LVDS 数据和一路随路时钟,数据相对随路时钟中心对齐。FPGA 使用 IBUFDS + IDELAY + ISERDES 接收。

约束模板
tcl 复制代码
# 随路时钟输入,假设 250 MHz
create_clock -name adc_dco -period 4.000 [get_ports adc_dco_p]

# 差分管脚与电平标准
set_property IOSTANDARD LVDS [get_ports {adc_dco_p adc_dco_n adc_data_p[*] adc_data_n[*]}]
set_property DIFF_TERM TRUE [get_ports {adc_data_p[*]}]

# 输入延迟:必须根据 ADC datasheet 的 tCO 和 PCB 走线差计算
set_input_delay -clock adc_dco -max 1.200 [get_ports adc_data_p[*]]
set_input_delay -clock adc_dco -min 0.200 [get_ports adc_data_p[*]]
调试顺序
  • 先确认 IDELAYCTRL RDY。

  • 发送固定训练码,扫描 IDELAY tap。

  • 找到 error-free 区间中点。

  • 再做 bitslip 字对齐。

  • 最后切换到真实数据。

13.6 案例六:10G XGMII/PCS 用户侧时序规划

场景

10G Ethernet MAC 用户侧常见 64 bit XGMII,时钟为 156.25 MHz。虽然频率不算极端,但数据面宽、控制路径多,常见问题是 tdata/tkeep/tlast 与 txc 控制字符错位。

设计建议
  • XGMII 的 txd[8*i+:8] 与 txc[i] 必须同拍对齐。

  • /S/、/T/、/I/ 这类控制字符不要经过与数据不同的流水级。

  • 如果在 MAC 和 PCS 之间增加 pipeline,需要同时打拍 txd 和 txc。

verilog 复制代码
always @(posedge clk_156m25) begin
    xgmii_txd_pipe <= xgmii_txd_next;
    xgmii_txc_pipe <= xgmii_txc_next;
end
约束建议

create_clock -name xgmii_clk -period 6.400 [get_ports clk_156m25]

如果 XGMII 与用户 AXI-Stream 不在同一时钟域,必须通过异步 FIFO 或完整 CDC,而不是直接跨域连接。

第 14 章 签核清单:交付前必须确认的 40 项

14.1 时钟与约束

  • 所有主时钟都有 create_clock。

  • PLL/MMCM 输出没有被错误地重复 create_clock。

  • RTL 分频时钟已用 create_generated_clock 描述。

  • 异步时钟之间已明确 set_clock_groups -asynchronous。

  • 所有外部接口都有 set_input_delay / set_output_delay 的 -max 和 -min。

  • 双沿接口使用了 -clock_fall -add_delay。

14.2 RTL 时序结构

  • 关键数据通路不超过 2-3 级 LUT 后未打拍。

  • 控制信号与数据流水级数一致。

  • 反压路径没有形成过长组合 ready 链。

  • DSP/BRAM 输出侧已根据频率插入寄存器。

  • 宽 mux、宽加法器、跨模块总线有 pipeline 规划。

14.3 CDC 与复位

  • 单 bit 电平使用 2FF/3FF 同步器。

  • 脉冲跨域使用 toggle 或握手。

  • 多 bit 数据跨域不做逐 bit 打两拍。

  • 高吞吐跨域使用异步 FIFO。

  • 每个时钟域都有独立复位同步释放。

  • CDC 同步寄存器加 ASYNC_REG。

14.4 物理实现

  • 高扇出 net 已检查并复制或区域化。

  • UltraScale+/Versal 设计检查了 Clock Region、SLR crossing 和拥塞。

  • 大型设计查看了 report_qor_suggestions。

  • 布线后运行过 phys_opt_design,并比较 WNS/TNS 改善。

  • 没有滥用 DONT_TOUCH / MARK_DEBUG 阻止优化。

14.5 报告与验证

  • report_timing_summary 中无 unconstrained path。

  • report_cdc 中无未处理严重 CDC。

  • report_exceptions 中 false path/multicycle 范围符合预期。

  • 关键路径已按根因分类:逻辑深、布线长、高扇出、跨 SLR、I/O 约束、CDC。

  • 仿真覆盖 reset、反压、边界长度、随机输入和异常输入。

附录 C 常用 Tcl 命令索引

tcl 复制代码
# 时钟与约束
report_clocks
report_clock_interaction
report_exceptions
report_timing_summary -delay_type max
report_timing_summary -delay_type min
report_timing -max_paths 20 -sort_by group

# 物理与 QoR
report_utilization -hierarchical
report_high_fanout_nets
report_design_analysis -congestion
report_qor_suggestions
report_methodology

# CDC
report_cdc
report_cdc -details

# 调试对象定位
get_ports <name>
get_cells -hier -filter {NAME =~ *xxx*}
get_pins -hier -filter {NAME =~ *xxx*}
all_fanin  -to  [get_pins xxx/D]
all_fanout -from [get_pins xxx/Q]
复制代码
相关推荐
upper20201 小时前
从零开始做Verilog实验--01--4位计数器
fpga开发
upper20201 小时前
从零开始动手做Verilog实验--02--模为60的BCD加法器
fpga开发
问心无愧05131 小时前
ctf show web 入门39
android·前端·笔记
Yeh2020581 小时前
Mybatis笔记一
java·笔记·mybatis
羊群智妍1 小时前
2026 AI搜索优化技术:GEO监测工具选型与应用
笔记
wuxinyan1231 小时前
工业级大模型学习之路011:RAG 零基础入门教程(第七篇):查询优化技术
人工智能·学习·rag
chen_ever2 小时前
大模型学习规划
人工智能·python·学习
山西瀚辰信安科技有限公司2 小时前
git下载安装及使用
git·学习
半导体守望者2 小时前
MKS elite 300 600 750W RF Plasma Generator 射频电源 OPERATIONMANUAL
经验分享·笔记·机器人·自动化·制造