📺 图片来自【数字 IC 秋招经验:STA、CDC】https://www.bilibili.com/video/BV1aSRzY5E81?vd_source=1a46c7af8e528520d93f15dd5c760634
STA 与 CDC
先给你一张地图:
下面按"从零到能干活"的方式讲。先给一张地图:
STA = 静态时序分析 :不跑激励,检查电路在所有时序路径上是否满足建立/保持时间。
CDC = 跨时钟域检查/设计 :处理不同时钟域之间传信号时的亚稳态、数据丢失、数据错乱问题。
STA 保证同步时序正确;CDC 保证异步交互安全。两者互补,不能互相替代。
一、STA 静态时序分析
1. STA 是什么STA 不需要测试向量,它把电路拆成一条条时序路径,计算每条路径的延迟,然后判断:
STA 不需要测试向量,它把电路拆成一条条时序路径,计算每条路径的延迟,然后判断:
- 数据能不能在时钟沿前稳定下来?→ Setup 建立时间
- 数据能不能在时钟沿后保持足够久?→ Hold 保持时间
工具:PrimeTime、Tempus、OpenSTA 等。
2. 四种基本时序路径
-
输入端口 → 寄存器
-
寄存器 → 寄存器
-
寄存器 → 输出端口
-
输入端口 → 输出端口(纯组合穿透)
-
输入端口 → 寄存器
-
寄存器 → 寄存器
-
寄存器 → 输出端口
-
输入端口 → 输出端口(纯组合穿透)
STA 会报告每条路径的 slack:
- Setup slack = required time - arrival time
- Hold slack = arrival time - required time
slack 为正:满足。
slack 为负:违例。
最差的负 slack 叫:
- WNS:Worst Negative Slack,最差建立违例
- TNS:Total Negative Slack,所有建立违例总和
- WHS:Worst Hold Slack,最差保持违例
- THS:Total Hold Slack,所有保持违例总和
3. Setup 和 Hold
Setup 建立时间
时钟沿到来前,数据必须已经稳定一段时间。
简化公式:
text
T_clk + T_skew >= T_co + T_comb + T_setup
含义:
- 时钟周期要足够长
- 寄存器时钟到输出延迟 T_co 要小
- 组合逻辑 T_comb 要短
- 建立时间 T_setup 要满足
- 时钟偏斜 T_skew 要有利
Setup 违例怎么修:
- 降低时钟频率
- 插入流水线寄存器
- 优化组合逻辑
- 换更快单元
- 调整时钟树 skew
- 减少扇出
Hold 保持时间
时钟沿到来后,数据还必须再稳定一段时间。
简化公式:
text
T_co + T_comb >= T_hold + T_skew
Hold 违例通常发生在:
- 组合逻辑太短
- 时钟 skew 太大
- 路径太快
Hold 违例怎么修:
- 插入 buffer 增加延迟
- 换慢单元
- 调整时钟树
- 增加布线延迟
- 修时钟 skew
注意:Setup 违例可以降频救,Hold 违例降频通常没用,因为它是同沿竞争。


4. SDC 约束
STA 需要约束,最常用 SDC:
tcl
create_clock -name clk -period 10 [get_ports clk]
set_clock_uncertainty 0.2 [get_clocks clk]
set_input_delay -clock clk 1.5 [get_ports din]
set_output_delay -clock clk 2.0 [get_ports dout]
set_false_path -from [get_clocks clkA] -to [get_clocks clkB]
set_multicycle_path 2 -setup -from [get_clocks clkA] -to [get_clocks clkB]
set_clock_groups -asynchronous -group {clkA} -group {clkB}
关键约束:
| 约束 | 作用 |
|---|---|
create_clock |
定义主时钟 |
create_generated_clock |
定义分频/倍频时钟 |
set_input_delay |
输入端口外部延迟 |
set_output_delay |
输出端口外部延迟 |
set_clock_uncertainty |
时钟抖动/余量 |
set_false_path |
不做时序检查的路径 |
set_multicycle_path |
多周期路径 |
set_clock_groups |
声明异步时钟组 |
set_max_delay/min_delay |
自定义最大/最小延迟 |
5. 多模式多角
真实芯片要看:
- PVT:Process、Voltage、Temperature
- WC:Worst Case,慢工艺、低电压、高温,查 Setup
- BC:Best Case,快工艺、高电压、低温,查 Hold
- MMMC:多模式多角,多时钟、多约束、多 Corner
二、CDC 跨时钟域
1. 为什么需要 CDC
当信号从一个时钟域传到另一个时钟域,如果两个时钟异步,接收端触发器可能在数据变化时采样,导致:
- 亚稳态:输出既不是 0 也不是 1,振荡一段时间后随机稳定
- 数据丢失:慢时钟采不到快脉冲
- 数据错乱:多比特信号各位到达时间不同
- 毛刺:组合逻辑竞争
STA 通常用 set_clock_groups -asynchronous 把异步域切开,不做普通时序检查。但这不代表安全,必须靠 CDC 设计和检查。
2. 亚稳态
触发器有 setup/hold 窗口。如果数据在窗口内变化,输出可能进入亚稳态。
亚稳态不能完全消除,只能降低概率。常用指标:
text
MTBF = e^(t_r/τ) / (T0 * f_clk * f_data)
两级同步器可以大幅提高 MTBF。
3. 单比特同步器
两级触发器同步器
systemverilog
always_ff @(posedge clk_dst or negedge rst_n) begin
if (!rst_n) begin
sync1 <= 1'b0;
sync2 <= 1'b0;
end else begin
sync1 <= async_signal;
sync2 <= sync1;
end
end
用途:
- 单比特电平信号
- 慢到快通常没问题
- 快到慢要求信号宽度大于目标时钟周期
- 只能传电平,不能直接传脉冲
脉冲同步器
如果源域是脉冲,目标域时钟慢,可能漏采。常用:
- 源域把脉冲转成 toggle
- 目标域同步 toggle
- 目标域再还原成脉冲
握手同步
text
源域:数据稳定 → 拉 req
目标域:同步 req → 采数据 → 拉 ack
源域:同步 ack → 撤销 req
适合多比特数据,但吞吐率低。
4. 多比特 CDC
绝对不要对多比特总线直接逐位打两拍。
因为各位延迟不同,可能采到中间错误值。
多比特常用方案:
- 异步 FIFO
- 握手 + 数据保持
- 格雷码
- MUX 同步
- DMUX 同步
打两拍作为跨时钟的基本处理方式
一般认为,一个异步采集的数据打两拍,电路能获得稳定的值参与后续计算。

两 bit 数据进行跨时钟处理



一、同步 FIFO
1. 同步 FIFO 是什么
同步 FIFO :读和写用同一个时钟,所以不需要跨时钟域同步。
对比:
| 同步 FIFO | 异步 FIFO | |
|---|---|---|
| 时钟 | 读写同一时钟 | 读时钟、写时钟不同 |
| CDC | 不需要 | 需要 |
| 指针编码 | 二进制即可 | 通常用格雷码 |
| 空满判断 | 同域判断,简单 | 跨域同步,复杂 |
| 典型用途 | 同域缓冲、流水线解耦 | 跨时钟域数据传递 |
同步 FIFO 解决的是:
- 生产者和消费者速率不匹配
- 突发数据缓冲
- 反压和流控
2. 同步 FIFO 的结构
text
写侧 读侧
wr_en, wr_data rd_en, rd_data
│ ▲
▼ │
┌─────────┐ ┌──────────┐ ┌─────────┐
│ 写指针 │───▶│ 存储阵列 │──▶│ 读指针 │
└─────────┘ └──────────┘ └─────────┘
│ │
▼ ▼
满判断 空判断
│ │
└──────────┬──────────────────┘
▼
full / empty
核心组成:
- 存储阵列:寄存器堆或双端口 RAM
- 写指针:指向下一个写入位置
- 读指针:指向下一个读出位置
- 满判断:写侧
- 空判断:读侧
- 计数器(可选):记录当前元素个数
3. 空满判断的经典问题
如果只用 N 位指针表示深度 2^N 的 FIFO:
- 写指针追上读指针:可能是满,也可能是空
- 读指针追上写指针:可能是空,也可能是满
当 wr_ptr == rd_ptr 时,无法区分空和满。
解决方案一:多一位指针
指针用 N+1 位,地址用低 N 位,最高位作为"圈数"标志。
- 空 :
wr_ptr == rd_ptr,所有位相同 - 满:低 N 位相同,最高位不同
systemverilog
// 深度 8,指针 4 位
// 空:wr_ptr[3:0] == rd_ptr[3:0]
// 满:wr_ptr[3:0] == rd_ptr[3:0] 且 wr_ptr[3] != rd_ptr[3]
解决方案二:使用计数器
用一个 count 记录当前元素个数:
count == 0:空count == DEPTH:满
简单直观,但计数器在高速时可能成为关键路径。
4. 同步 FIFO 代码示例
下面用计数器法,深度 8:
systemverilog
module sync_fifo #(
parameter int DATA_WIDTH = 8,
parameter int DEPTH = 8,
parameter int ADDR_WIDTH = $clog2(DEPTH)
)(
input logic clk,
input logic rst_n,
input logic wr_en,
input logic [DATA_WIDTH-1:0] wr_data,
output logic full,
input logic rd_en,
output logic [DATA_WIDTH-1:0] rd_data,
output logic empty,
output logic [ADDR_WIDTH:0] count
);
logic [DATA_WIDTH-1:0] mem [0:DEPTH-1];
logic [ADDR_WIDTH-1:0] wr_ptr, rd_ptr;
logic [ADDR_WIDTH:0] cnt;
assign full = (cnt == DEPTH);
assign empty = (cnt == 0);
assign count = cnt;
// 写
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
wr_ptr <= '0;
else if (wr_en && !full) begin
mem[wr_ptr] <= wr_data;
wr_ptr <= wr_ptr + 1'b1;
end
end
// 读
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
rd_ptr <= '0;
else if (rd_en && !empty)
rd_ptr <= rd_ptr + 1'b1;
end
assign rd_data = mem[rd_ptr];
// 计数
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
cnt <= '0;
else begin
case ({wr_en && !full, rd_en && !empty})
2'b10: cnt <= cnt + 1'b1; // 只写
2'b01: cnt <= cnt - 1'b1; // 只读
default: cnt <= cnt; // 同读同写或都不动
endcase
end
end
endmodule
要点:
full时写无效,empty时读无效- 同拍读写且非空非满时,
cnt不变 rd_data用组合读,也可以加一级寄存器输出
5. 同步 FIFO 的验证要点
- 空时读:不应读出有效数据,
empty保持 - 满时写:不应覆盖数据,
full保持 - 同拍读写:计数不变,数据正确
- 复位后:
empty=1,full=0,指针归零 - 写满再读空:数据顺序正确
- 随机读写:与参考模型比对
- 覆盖率:满、空、同拍读写、边界地址
6. 同步 FIFO vs 异步 FIFO 的 CDC 关系
- 同步 FIFO 不涉及 CDC,STA 按同一时钟约束即可。
- 异步 FIFO 是 CDC 的典型方案,需要:
- 格雷码指针
- 两级同步器
set_clock_groups -asynchronous- CDC 工具检查


异步 FIFO
典型结构:
- 写指针:二进制转格雷码 → 同步到读域
- 读指针:二进制转格雷码 → 同步到写域
- 空判断在读域
- 满判断在写域
为什么用格雷码?
格雷码相邻值只变一位,跨域采样时最多错一位,不会出现多位同时变化的错乱。
FIFO 深度估算:
text
写快读慢:
深度 >= 突发长度 - 突发期间读出的数据量
还要考虑同步延迟、空满判断延迟。
5. 复位 CDC
异步复位,同步释放:
systemverilog
always_ff @(posedge clk or negedge rst_async_n) begin
if (!rst_async_n) begin
rst_sync1 <= 1'b0;
rst_sync2 <= 1'b0;
end else begin
rst_sync1 <= 1'b1;
rst_sync2 <= rst_sync1;
end
end
好处:
- 复位可以异步生效
- 释放与时钟同步,避免不同触发器退出复位时间不同
6. CDC 约束
常见写法:
tcl
set_clock_groups -asynchronous -group {clkA} -group {clkB}
或者:
tcl
set_false_path -from [get_clocks clkA] -to [get_clocks clkB]
但对同步器第一级,有些流程会要求:
tcl
set_max_delay -datapath_only -from [get_clocks clkA] -to [get_clocks clkB] 2.0
目的:限制跨域数据路径延迟,防止第一级同步器前组合逻辑太慢。
注意:
- 不要对同步器输出后级乱设 false path
- 同步器后的逻辑要按目标时钟域正常约束
set_clock_groups和set_false_path作用范围不同
7. CDC 检查工具
- SpyGlass CDC
- Meridian CDC
- Questa CDC
- Conformal CDC
- VC CDC
检查内容:
- 时钟域识别
- 跨域路径
- 同步器识别
- 亚稳态模型
- 数据一致性
- 复位跨域
- 毛刺
- 协议检查
验证工程师还要做:
- 仿真注入跨域场景
- 断言检查
- 覆盖率
- 形式验证
- 门级仿真
异步 FIFO


为什么异步 FIFO 用写格雷码与打两拍的读格雷码进行比较?为什么在写时钟域做写满判断?写满信号产生的目的?
- 让外部不再对 FIFO 进行写操作,外部需要第一时间获得 FIFO 已满信息,哪怕信息是滞后的
三、STA 和 CDC 的关系
| STA | CDC | |
|---|---|---|
| 对象 | 同步时序路径 | 异步跨时钟域 |
| 是否跑激励 | 不需要 | 设计检查 + 仿真/形式 |
| 核心问题 | Setup/Hold | 亚稳态、数据一致性 |
| 典型工具 | PrimeTime、Tempus | SpyGlass CDC、Meridian |
| 约束 | SDC | 异步时钟组、同步器约束 |
| 输出 | slack 报告 | CDC 违例、同步器检查 |
| 关系 | 保证同步时序 | 保证异步安全 |
一句话:
STA 管"同一个时钟域内别迟到别早退";CDC 管"不同时钟域之间别传错"。
四、学习路线
第 1 阶段:数字时序基础
- 触发器、建立保持时间
- 时钟偏斜、抖动
- 组合逻辑延迟
- 同步/异步复位
第 2 阶段:STA
- 四种时序路径
- Setup/Hold 公式
- SDC 约束
- 读时序报告:WNS/TNS/WHS/THS
- 多周期、false path、时钟组
- PVT、OCV、MMMC
第 3 阶段:CDC
- 亚稳态
- 两级同步器
- 脉冲同步、握手
- 异步 FIFO、格雷码
- 复位同步
- CDC 约束
- CDC 工具检查
第 4 阶段:实践
- 写一个简单同步电路,跑 STA,看 slack。
- 故意加长组合逻辑,制造 setup 违例,再修。
- 写一个跨时钟域单比特同步器,跑 CDC 工具。
- 写一个异步 FIFO,仿真验证空满。
- 读一份 CDC 报告,解释每条违例。
五、自测题
- Setup 违例和 Hold 违例分别怎么修?
- 为什么 Hold 违例不能靠降频解决?
set_false_path和set_clock_groups有什么区别?- 为什么多比特不能直接打两拍?
- 异步 FIFO 为什么用格雷码?
- 慢到快和快到慢的单比特同步分别要注意什么?
- 什么是亚稳态?为什么不能完全消除?
- CDC 工具和 STA 工具各自检查什么?
- 同步 FIFO 为什么不需要格雷码?
- 同步 FIFO 中
wr_ptr == rd_ptr时,如何区分空和满? - 同步 FIFO 同拍读写时,计数器怎么变化?
- 同步 FIFO 和异步 FIFO 在 STA 约束上有什么区别?
- 同步 FIFO 验证需要覆盖哪些边界场景?
六、自测题答案
1. Setup 违例和 Hold 违例分别怎么修?
Setup 违例(数据到得太晚):
- 降低时钟频率
- 插入流水线寄存器,切短组合逻辑
- 优化组合逻辑,减少级数
- 换更快单元
- 减少扇出
- 调整时钟树 skew
- 使用多周期路径约束(如果逻辑允许多周期)
Hold 违例(数据变得太早):
- 插入 buffer 增加数据路径延迟
- 换慢单元
- 增加布线延迟
- 调整时钟树,修 skew
- 减小时钟 uncertainty
- 修 hold 通常不能靠降频
2. 为什么 Hold 违例不能靠降频解决?
Hold 检查的是同一个时钟沿附近数据是否保持稳定。
公式:
text
T_co + T_comb >= T_hold + T_skew
这个不等式里没有时钟周期 T_clk 。
降频只增大 T_clk,影响的是 setup:
text
T_clk + T_skew >= T_co + T_comb + T_setup
所以降频对 hold 违例无效。
3. set_false_path 和 set_clock_groups 有什么区别?
set_false_path |
set_clock_groups |
|
|---|---|---|
| 作用 | 指定某条/某组路径不做时序检查 | 声明时钟组之间异步 |
| 方向 | 有方向性,-from ... -to ... |
组之间双向异步 |
| 范围 | 可精细到端口/单元 | 通常按时钟组 |
| 典型用途 | 个别不需要检查的路径 | 异步时钟域 |
| 优先级 | 高 | 高 |
区别:
set_false_path是路径级裁剪,可以只切一个方向。set_clock_groups -asynchronous是时钟级声明,组间所有路径都不检查,双向。- 异步时钟域通常用
set_clock_groups。 - 个别假路径、测试逻辑、跨域已同步路径可用
set_false_path。
注意:两者都可能掩盖真实违例,要谨慎。
4. 为什么多比特不能直接打两拍?
因为多比特信号的每一位路径延迟不同,跨时钟域采样时:
- 有的位已经变成新值
- 有的位还是旧值
- 接收端可能采到新旧混合的中间值
例如 4 位数据从 0111 变到 1000,四位同时变化。
如果采样时刻正好在变化窗口,可能采到 0000、1111、0110 等错误值。
所以多比特跨域必须用:
- 异步 FIFO
- 握手
- 格雷码
- MUX/DMUX 同步
不能逐位打两拍。
5. 异步 FIFO 为什么用格雷码?
格雷码相邻两个数只变一位。
跨时钟域采样指针时:
- 即使采样时刻在变化窗口
- 最多只可能采到旧值或新值
- 不会出现多位同时跳变导致的错误中间值
这样空满判断才安全。
如果指针用二进制,多个位同时变化,跨域采样可能得到完全错误的值,导致:
- 假空
- 假满
- 数据覆盖
- 数据丢失
6. 慢到快和快到慢的单比特同步分别要注意什么?
慢到快:
- 源信号宽度远大于目标时钟周期
- 目标域能采到多个周期
- 两级同步器即可
- 注意信号是电平还是脉冲
快到慢:
- 源脉冲可能比目标时钟周期短
- 目标域可能完全采不到
- 必须用脉冲展宽、toggle 同步、握手
- 要求源信号保持足够长时间
共同点:
- 两级触发器同步
- 第一级可能亚稳态
- 第二级输出稳定
- 不能直接传多比特
7. 什么是亚稳态?为什么不能完全消除?
亚稳态:触发器在 setup/hold 窗口内采样到变化的数据,输出既不是稳定 0 也不是稳定 1,而是在中间电平振荡一段时间,最终随机稳定到 0 或 1。
不能完全消除的原因:
- 建立/保持窗口是物理存在的
- 异步信号与时钟沿的关系是随机的
- 只要存在异步采样,就有概率进入亚稳态
- 只能通过同步器、增加等待时间、提高 MTBF 来降低概率
- 无法把概率降到绝对零
8. CDC 工具和 STA 工具各自检查什么?
STA 工具:
- 同步时序路径的 setup/hold
- 时钟约束、输入输出延迟
- 多周期、false path
- 时钟偏斜、抖动
- PVT corner
- 报告 slack、WNS、TNS
CDC 工具:
- 时钟域识别
- 跨域路径
- 同步器结构识别
- 亚稳态风险
- 多比特数据一致性
- 复位跨域
- 毛刺
- 握手协议
- 格雷码使用
- FIFO 空满逻辑
关系:
- STA 通常把异步域用
set_clock_groups切开,不检查跨域时序 - CDC 工具专门检查这些被切开的路径
- 两者互补,缺一不可