FPGA中的亚稳态与跨时钟域数据撕裂现象
什么是亚稳态
在 数字电路里,亚稳态(Metastability) 是指 触发器在采样异步信号时,由于 setup/hold 时间被破坏,导致寄存器的输出进入既不是稳定的 0,也不是稳定的 1 的不确定状态。
亚稳态的最直观现象
其最明显的现象就是:同一个寄存器在短时间内不可重复读。
- 这里的不可重复读 并不是因为寄存器内容被外部逻辑改写了;
而是因为该寄存器在采样异步信号时进入了亚稳态,输出值还没有稳定下来; - 在解析过程中,它的电平可能在阈值附近晃动、延迟收敛,所以不同时间点读到的结果可能不同。
什么是 CDC
CDC = Clock Domain Crossing,中文通常叫 时钟域交叉。
在 FPGA/ASIC 里,常常存在多个不同时钟域(例如 50 MHz 外设时钟、100 MHz 核心时钟、125 MHz 千兆网时钟)。
当一个信号或数据从 源时钟域 传到 目的时钟域 时,就发生了 CDC。
为什么要关心 CDC?
- 异步关系:两个时钟频率可能不同,或者相位不固定,无法保证采样点和数据稳定时间对齐。
- 亚稳态 (Metastability):目的域触发器在采到不稳定电平时,可能进入亚稳状态,导致输出不可预期。
- 多比特一致性问题:单 bit 信号用双触发同步器基本能解决,但 多比特总线 如果直接采样,会出现“撕裂”现象。
常见的 CDC 类型
-
单 bit 信号跨域
- 典型处理:双触发同步器(2-FF synchronizer)。
- 用途:复位、标志位、中断请求。
-
多比特总线跨域
- 问题:多个比特不能保证同时到达目的域。
- 典型处理:握手协议 / 异步 FIFO / Gray 码编码。
-
数据流跨域
- 大量连续数据(如音视频、网络数据)。
- 典型处理:异步 FIFO,读写两端独立时钟。
CDC 处理的目标
- 避免亚稳态:降低亚稳态被扩散的概率。
- 保证一致性:目的域拿到的数据必须是源域的“一个合法快照”。
- 系统可靠:即使不同频率、不同相位,跨域传输也要稳定、可预测。
什么是撕裂?
- 定义:目的时钟域在源时钟域 总线翻转的瞬间 采样,捕获到“部分新值 + 部分旧值”的混合结果。
- 特征:这种混合结果在源域的逻辑里 永远不会出现。
- 又叫 混码、非原子采样。
撕裂是如何产生的?
- 源域:多比特计数器
例如 4 位计数器从0111 (7)
→1000 (8)
。 - 实际硬件:各位翻转有延迟差
高位翻转慢一点,低位翻转快一点。 - 目的域:在翻转过程中采样
- 可能采到
1111 (15)
或0000 (0)
。 - 这些数是源域计数器 不可能生成的非法值。
- 可能采到
撕裂的危害
-
数据错误:比如地址总线撕裂 → 访问错误的存储单元。
-
状态机异常:非法状态进入 → 系统死锁。
-
调试困难:RTL 仿真正常,上板波形偶发异常
撕裂仿真实验:
实验方案
- clk1 = 50 MHz,16 位计数器,计数范围 0…1000。
- clk2 = 100 MHz,异步采样该计数器。
两种路径
- 理想直连:RTL 仿真几乎看不到问题(所有位同时翻转)。
- 带偏斜延迟:人为给总线每位注入不同延迟,更容易复现撕裂。
检测方法
因为 clk2 频率比 clk1 快,所以clk2不会漏检
故撕裂判据为:相邻采样差值 ∉ {0, 1} → 撕裂。
当撕裂次数超过 3 次时,则结束仿真
撕裂测试
counter_clk1.v
`timescale 1ns/1ps// ================== clk1 域:16位计数器,0~1000 循环 ==================
module counter_clk1 #(parameter MAX = 1000
)(input wire i_clk1,input wire i_rstn,output reg [15:0] o_cnt
);always @(posedge i_clk1 or negedge i_rstn) beginif (!i_rstn) o_cnt <= 16'd0;else if (o_cnt == MAX) o_cnt <= 16'd0;else o_cnt <= o_cnt + 16'd1;end
endmodule
unsafe_sampler_clk2.v
`timescale 1ns/1ps// ================== clk2 域:不安全直采样(统计撕裂次数) ==================
module unsafe_sampler_clk2 #(parameter MAX = 1000 // 计数器最大值
)(input wire i_clk2, // 目的时钟input wire i_rstn, // 复位,低有效input wire [15:0] i_bus_async, // 来自异步域的总线(危险)output reg [31:0] o_tearing_cnt, // 撕裂次数(非法跳变)output reg [15:0] o_last_sample // 最近一次采样值
);reg [15:0] r_sample, r_prev;// 计算 (a - b) mod (MAX+1)function automatic [15:0] moddiff;input [15:0] a, b;reg [16:0] t;beginif (a >= b) t = a - b;else t = a + (MAX+1) - b;moddiff = t[15:0];endendfunctionalways @(posedge i_clk2 or negedge i_rstn) beginif (!i_rstn) beginr_sample <= 16'd0;r_prev <= 16'd0;o_last_sample <= 16'd0;o_tearing_cnt <= 32'd0;end else beginr_prev <= r_sample;r_sample <= i_bus_async; // 危险:可能撕裂o_last_sample <= r_sample;// 合法跳变只能是 0 或 +1 (mod MAX+1)if (moddiff(r_sample, r_prev) != 0 &&moddiff(r_sample, r_prev) != 1) begino_tearing_cnt <= o_tearing_cnt + 1;// 调试打印(可选)// $display("[%0t ns] Tear: prev=%0d -> now=%0d (diff=%0d)",// $time, r_prev, r_sample, moddiff(r_sample,r_prev));endendend
endmodule
bus_skew.v
`timescale 1ns/1ps// ================== 位间偏斜注入器(仿真用) ==================
// 功能:给每一位加不同固定延迟,模拟 FPGA 中布线/门延
// 例:BASE=0, STEP=2ns -> bit0=0ns, bit1=2ns, bit2=4ns ...
module bus_skew #(parameter integer W = 16, // 位宽parameter integer BASE = 0, // 起始延迟 (ns)parameter integer STEP = 2 // 位间递增延迟 (ns) —— 建议 2ns 起步
)(input wire [W-1:0] i_bus,output wire [W-1:0] o_bus
);genvar k;generatefor (k = 0; k < W; k = k + 1) begin : g_skewlocalparam integer DLY = BASE + k*STEP;assign #(DLY) o_bus[k] = i_bus[k]; // 固定延迟(连续赋值延迟)endendgenerate
endmodule
tb.sv
`timescale 1ns/1ps// ================== Testbench ==================
module tb;// ====== 时钟与复位 ======reg clk1 = 0, clk2 = 0, rstn = 0;// clk1: 50MHz -> 周期 20nsalways #10 clk1 = ~clk1;// clk2: 100MHz -> 周期 10ns,但添加 3ns 相位偏移(关键)initial beginclk2 = 0;#3; // 相位偏移 3ns,让采样点卡进翻转传播窗口forever #5 clk2 = ~clk2;end// 复位流程:先保持若干个 clk1 周期的低电平initial beginrstn = 0;repeat (5) @(posedge clk1);rstn = 1;end// ====== 源域计数器 ======wire [15:0] w_cnt_clk1;counter_clk1 #(.MAX(1000)) u_cnt (.i_clk1 (clk1),.i_rstn (rstn),.o_cnt (w_cnt_clk1));// ====== 路径A:理想直连(无位间偏斜)用于对比 ======wire [31:0] w_tearing_ideal;wire [15:0] w_last_ideal;unsafe_sampler_clk2 #(.MAX(1000)) u_ideal (.i_clk2 (clk2),.i_rstn (rstn),.i_bus_async (w_cnt_clk1),.o_tearing_cnt (w_tearing_ideal),.o_last_sample (w_last_ideal));// ====== 路径B:带位间偏斜(稳稳复现撕裂) ======wire [15:0] w_cnt_skewed;// 提示:若你一开始仍看不到撕裂,可把 STEP 再加大到 3 或 4bus_skew #(.W(16), .BASE(0), .STEP(2)) u_skew (.i_bus (w_cnt_clk1),.o_bus (w_cnt_skewed));wire [31:0] w_tearing_skew;wire [15:0] w_last_skew;unsafe_sampler_clk2 #(.MAX(1000)) u_skewed (.i_clk2 (clk2),.i_rstn (rstn),.i_bus_async (w_cnt_skewed),.o_tearing_cnt (w_tearing_skew),.o_last_sample (w_last_skew));// 2) 限时运行;也可在命中多次撕裂后提前结束initial begin// 运行 30 ms(按需调整)#30000_000;$display("\n================= SUMMARY =================");$display("Ideal path : tearing=%0d, last=%0d",w_tearing_ideal, w_last_ideal);$display("Skewed path : tearing=%0d, last=%0d",w_tearing_skew , w_last_skew );$display("===========================================\n");$finish;end// 3) 命中多次撕裂后提前退出(例如 >= 3 次)initial beginwait (rstn == 1'b1);wait (w_tearing_skew >= 3);$display("[%0t ns] Enough tearing hits (>=3). Stopping...", $time);$finish;end
endmodule
仿真结果
# [1388000 ns] Enough tearing hits (>=3). Stopping...
# ** Note: $finish : ../tb.sv(84)
# Time: 1388 ns Iteration: 2 Instance: /tb
亚稳态测试
亚稳态现象只能在真机上复现。
例如,用一个按键作为异步输入信号:
- o_led_a 表示本拍寄存器读取的值;
- o_led_b 表示上一拍寄存器读取的值。
- 当连续两个时钟周期对同一个寄存器 r_meta 进行读取时,如果两次结果不同,就说明该寄存器在采样异步信号时进入了亚稳态。此时,通过翻转 o_led_diff 显示,能直观地观察到这一现象。
meta_read_twice_demo.v
`timescale 1ns/1ps
// ============================================================
// Single-Clock Metastability Read-Diff Demo (50MHz, 低电平=按下)
// - 只有一个系统时钟 i_clk50
// - 异步按键 i_btn_n(低=按下),不做消抖,便于“贴边”触发
// - r_meta 为“同一个寄存器”
// - 连续两拍分别读取 r_meta,比对是否不同;不同时翻转 o_led_diff
// - o_led_a 显示“本拍读取值”,o_led_b 显示“上一拍读取值”
// ============================================================
module meta_read_twice_demo (input wire i_clk50, // 50MHz 单时钟input wire i_btn_n, // 异步按键(低=按下),建议上拉电阻output reg o_led_a, // 本拍读取显示output reg o_led_b, // 上一拍读取显示output reg o_led_diff // 发现“两次读取不同”就翻转,便于目视计数
);// 归一化按键:高=按下(仅用于内部逻辑,实际 i_btn_n 低=按下)wire btn = ~i_btn_n;// “同一个寄存器”:用系统时钟直接采样异步输入// 这里最容易发生 setup/hold 违例 -> 触发器可能亚稳reg r_meta /* synthesis keep = 1 */;always @(posedge i_clk50) beginr_meta <= btn; // 不消抖,不同步,刻意“糙”来放大问题end// 连续两拍“读取同一个寄存器”的值// read_now : 本拍对 r_meta 的读取// read_prev : 上一拍对 r_meta 的读取(=“第二次读取”的对照)reg read_now, read_prev;always @(posedge i_clk50) beginread_now <= r_meta; // 第一次读取(本拍)read_prev <= read_now; // 第二次读取(下一拍对比上一拍读取的结果)end// 可视化:把两次读取结果各接一颗 LEDalways @(posedge i_clk50) begino_led_a <= read_now;o_led_b <= read_prev;end// 只要发现“同一个寄存器两次读取不同”,就翻转 o_led_diff// (翻转比点亮更显眼,方便你数“发生了几次”)reg mismatch_armed;always @(posedge i_clk50) beginif (read_now != read_prev) beginif (!mismatch_armed) begino_led_diff <= ~o_led_diff;mismatch_armed <= 1'b1; // 边沿一次只记一次endend else beginmismatch_armed <= 1'b0;endendendmodule
HC_FPGA_Demo_Top.v
`timescale 1ns/1ps
module HC_FPGA_Demo_Top
(input CLOCK_XTAL_50MHz,input RESET,input KEY2,output LED0,output LED1,output LED2
);// 实例化亚稳态演示模块meta_read_twice_demo u_meta_demo (.i_clk50 (CLOCK_XTAL_50MHz), // 板载 50MHz 时钟.i_btn_n (KEY2), // 按键低电平=按下.o_led_a (LED0), // 本拍读取.o_led_b (LED1), // 上一拍读取.o_led_diff(LED2) // 不同翻转);endmodule
测试结果
三个灯都会亮,说明发生了亚稳态