从 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、仿真、上板调试
全书章节结构
-
时序优化总览、Pipeline、Retiming、Replication、PhysOpt、高扇出与拥塞、综合案例、Vivado 命令。
-
CDC 跨时钟域设计:2FF、脉冲、握手、Gray Code、异步 FIFO、复位 CDC、约束。
-
XDC/SDC:create_clock、input/output delay、multicycle、false path、case analysis。
-
UltraScale+ / Versal:CLB、Clock Region、SLR、NoC 与架构级优化。
-
高速接口:LVDS、IDELAY/ISERDES、OSERDES、GT、MIG、约束模板。
-
补充实战案例和签核清单。
第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 等。
-
-
写约束的顺序很关键:
-
- 时钟定义
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 的复位顺序错了是初学者最常踩的坑。典型顺序:
- 等待外部 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]