多比特跨时钟域翻车实证:为什么不能直接打两拍
更新时间:2026-10-25。本文是「集成电路大类 · 芯片设计」中间层(intermediate)的一篇实验文。跨时钟域 CDC 基础 第四节讲过结论:多 bit 总线不能逐位打两拍,因为各 bit 到达目的触发器的时间有偏移,可能采到「新旧混搭」的非法组合。那一篇给的是机理,本篇把这个翻车现场在仿真里真造出来:同一个 4 bit 计数器,一路直接逐位打两拍,一路先转格雷码再打两拍,两边同时采样 111 次,看哪边会采到总线上从未真实存在过的值。正确做法的深入讨论见 格雷码:跨时钟域为什么要用 Gray Code 与 多 bit CDC:异步 FIFO 与 Gray 码指针设计。
本文要回答的问题
- 多 bit 总线逐位打两拍,到底会错成什么样?错值是随机的还是有规律的?
- 「位间偏移(skew)」从哪里来,为什么每条 bit 线的延迟不可能完全一样?
- 格雷码为什么能兜住这个问题?它兜底的是「不出错」还是「错得安全」?
- 仿真里怎么给「这个采样值合不合法」下一个可判定的判据?
先把实验结果摆上来,四个数字全部出自后文日志:
这张图读法很直接:输入侧是 111 次采样这个分母;红色决策位的 4 是直接路径的翻车次数——分母不大、次数不小,换算下来大约每 28 拍就踩中一次;绿色 0 是格雷码路径在同一份激励、同一份偏移模型下的成绩。同样的物理条件,错的不是运气,是结构。
一、错误是怎么产生的:位间偏移加多 bit 跳变
单 bit 信号跨域,打两拍就够了——入门篇 两级触发器同步器:打两拍原理与亚稳态仿真实测 实测过,输出最坏两拍内稳定跟随。但一旦信号是多 bit 总线,事情就变了质。根因有两层:
第一,多 bit 值的一次变化往往不止翻一位。4 bit 计数器从 7(0111)数到 8(1000),四个 bit 同时翻转。第二,四条 bit 线到达目的触发器的时间不可能完全一样——走线长度不同、过孔数不同、驱动负载不同,物理上必然存在皮秒到纳秒级的偏移。两条叠加:目的时钟沿到来的瞬间,有的 bit 已经是新值、有的 bit 还是旧值,采到的就是一个源总线上从未存在过的组合——比如 7 跳 8 的中间,采到 0(新高位到了、旧低位还没走)或 15。
把这个机理画成对照图,红色是错误的直接路径,绿色是格雷码路径:
读这张图抓住分叉点:同一份源数据、同一份偏移模型(橙色块),唯一的区别是过偏移之前有没有先做格雷码编码。红色路径里四个 bit 各自独立翻转、各自带不同延迟,目的域采到的就是「部分新部分旧」的混搭;绿色路径里格雷码保证相邻计数值只差一个 bit,无论这个 bit 的沿落在采样沿哪一侧,采到的要么是旧码、要么是新码,都是真实出现过的值。
二、实验设计:怎么在仿真里「造」出翻车
理想数字仿真里连线没有延迟,必须显式建模。testbench 用连续赋值的延迟语法给四条 bit 线分别注入 1、2、3、4 ns 的传输延迟:
wire [3:0] bus_skew;
assign #1 bus_skew[0] = bus[0];
assign #2 bus_skew[1] = bus[1];
assign #3 bus_skew[2] = bus[2];
assign #4 bus_skew[3] = bus[3];源时钟 125 MHz(周期 8 ns),目的时钟 100 MHz(周期 10 ns),两者异步,相位关系随时间漂移——这样偏移窗口会被采样沿从各个角度「扫」到,不需要手工凑运气。格雷码支路用同样的 1-4 ns 偏移,唯一区别是先编码 gray = (bus >> 1) ^ bus 再过偏移、打两拍后解码。
「非法值」的判据也要可机检:testbench 维护一个深度 4 的历史环形缓冲,记录源总线最近真实持有过的 4 个值;目的域每拍把两条路径的采样值拿去和历史比对,不在历史里就是非法——这个值源端从未发出过,纯粹是跨域采样造出来的幻影。判据语句:
if (!is_legal(d_m2)) begin
illegal_direct = illegal_direct + 1;
$display("[%0t] DIRECT illegal intermediate sample: %0d (0x%h) ...", $time, d_m2, d_m2);
end最终 PASS 判据定成「直接路径必须翻车、格雷码路径必须零翻车」:samples > 50 && illegal_direct > 0 && illegal_gray == 0。这样设计是为了让 PASS 本身就携带信息量——它不是「没抓到错就放行」,而是「该翻的翻了、该稳的稳了」才放行,防止 testbench 根本没激励到位时假绿。
三、实测实录:4 次翻车全部落在同一类跳变上
这份实录最该盯住的是四个非法值的内容:0 和 8,交替出现。这不是随机噪声。7(0111)跳 8(1000)时四位全翻,若高位的 1 先到、低位的 0 还没走,中间瞬间就是 1111 即 15 往 1000 过渡的某个截面;而实际抓到的 0 与 8 正对应「新高位已生效、旧低位已撤走」和「只有新高位生效」两个相位截面。15(1111)跳 0(0000)的跳变是同样机理的镜像。四个采样点 226000、546000、866000、1186000 时刻间隔均为 320 ns——正是 125 MHz 源时钟 40 拍的整周期,说明每次翻车都发生在计数器绕回同相位的那一类跳变上,规律性和机理预测完全对得上。
把关键输出行原样摘录一段,便于逐条核对:
VCD info: dumpfile multibit_cdc.vcd opened for output.
[226000] DIRECT illegal intermediate sample: 0 (0x0) -- never a real bus value
[546000] DIRECT illegal intermediate sample: 8 (0x8) -- never a real bus value
[866000] DIRECT illegal intermediate sample: 0 (0x0) -- never a real bus value
[1186000] DIRECT illegal intermediate sample: 8 (0x8) -- never a real bus value
samples=111 illegal_direct=4 illegal_gray=0
RESULT: PASS这段摘录覆盖了从首次翻车到最终判据的完整证据链。最后一行计数是全文最重的一句话:111 次采样里直接路径 4 次非法、格雷码路径 0 次。注意格雷码路径经过的是同一个偏移模型、同一组两级触发器——它没有消灭偏移,而是让偏移的后果从「可能采到幻影值」降级为「最多偏一个码步」。对计数器这种单调变化的量,偏一个码步意味着读到的值「稍旧或稍新」,但仍然是一个真实值,下游用它做指针比较、门限判断都不会崩。这就是 格雷码指针 在异步 FIFO 里能扛大旗的原因。
| 对比维度 | 直接逐位打两拍(错误) | 格雷码 + 打两拍(正确) |
|---|---|---|
| 本次实测非法采样 | 4 / 111 | 0 / 111 |
| 多 bit 同时翻转时 | 各 bit 新旧混搭,出现幻影值 | 相邻值只差 1 bit,无混搭可能 |
| 采样值偏差 | 可以是任意非法组合 | 最多偏一个码步,恒为真实值 |
| 适用数据形态 | 不可用于多 bit 总线 | 单调变化的计数/指针 |
| 非单调数据怎么办 | —— | 走握手或异步 FIFO |
四、仿真抓到了什么、没抓到什么
需要先划清边界:本次抓到的 4 次非法采样不是亚稳态。本篇的两级触发器在理想仿真器里永远功能正确——它采到的值是确定性的,错的来源是 testbench 显式注入的位间偏移,即「四个 bit 不同时到达」这个结构事实。亚稳态(采样沿落在数据翻转窗口内、触发器输出短暂悬在中间电平)是另一层物理风险,理想数字仿真看不到,要靠带时序反标的门级仿真与静态 CDC 签核兜底。两层风险独立存在:即使亚稳态被两级触发器吸收(功能仿真里它本来就不出现),位间偏移照样把多 bit 总线打翻。这也是为什么 跨时钟域 CDC 基础 把「多 bit 怎么办」单列一节——它不是打几拍的问题,是编码与协议的问题。
反过来,本次格雷码路径的零非法也要正确解读:它证明的是「在 1-4 ns 偏移模型下,格雷码编码把混搭错误归约为至多一个码步的偏差」,这是结构性保证,与激励无关;但格雷码本身不解决亚稳态,指针跨域时每一根格雷码 bit 仍然要打两拍——本篇的 g_m1/g_m2 就是这个角色。两件事各管一层,合起来才是完整方案。
五、格雷码不万能:非单调数据走握手或 FIFO
格雷码的保命前提是「相邻值只差一位」,也就是数据必须单调连续变化——计数器、FIFO 指针天然满足。如果跨域的是任意数据总线(比如一个配置字、一笔传输载荷),前后值之间没有任何编码约束,格雷码帮不上忙。这时两条路:
低速、偶发的数据,用 req/ack 握手:源域把数据摆好不动、拉起 req,目的域打完两拍看到 req 后再采样数据——数据在采样前已经稳定了多个周期,偏移无从作乱;目的域 ack 回来源域才允许改下一个数。带宽低,但逻辑极小。高速、连续的数据流,用异步 FIFO:数据不过时钟域,过域的只有读写指针,而指针恰好是单调计数器——格雷码的主场。异步 FIFO 的完整设计见 多 bit CDC:异步 FIFO 与 Gray 码指针设计,可以看到本篇的格雷码支路就是 FIFO 里指针跨域链路的微缩模型。若数据来自片外异步接口、连源时钟都不存在,还要先过一級输入同步再谈协议,这类场景属于复位与异步接口设计,可对照复位域交叉篇目的思路处理。
三种方案的选型可以直接按数据形态查表:
| 数据形态 | 变化规律 | 推荐方案 | 代价 |
|---|---|---|---|
| 单 bit 电平/状态 | 任意 | 两级触发器打两拍 | 约 2 拍延迟 |
| 多 bit 计数/指针 | 单调连续 | 格雷码 + 打两拍 | 编解码逻辑 + 约 2 拍延迟 |
| 多 bit 偶发数据 | 任意、低频 | req/ack 握手 | 带宽极低、逻辑小 |
| 多 bit 连续流 | 任意、高频 | 异步 FIFO | BRAM + 指针控制逻辑 |
给读者一个可亲手复现的延伸练习:把 testbench 里的偏移模型从 1/2/3/4 ns 改成 1/1/2/2 ns,重跑后非法次数会下降但不为零;再改成全部 1 ns,非法次数归零——这演示了另一个工程结论:偏移量与翻转位的「密度」共同决定翻车概率,而 PCB/版图阶段的等长约束只能压缩偏移、不能消灭它,所以结构上仍然不许直接同步。还可以把计数器换成随机数发生器(非单调),观察格雷码路径也开始出现非法值——亲手打破格雷码的适用前提,比背十遍结论记得牢。
这一篇在知识体系里的位置
- 理论承接:跨时钟域 CDC 基础(亚稳态、位间偏移的机理讲法);
- 单 bit 前导实验:两级触发器同步器:打两拍原理与亚稳态仿真实测(本篇的两级链即该篇电路的逐位复制);
- 正确做法深挖:格雷码:跨时钟域为什么要用 Gray Code 与 多 bit CDC:异步 FIFO 与 Gray 码指针设计;
- 同族对照实验:同步 FIFO 的 iverilog 回归(读写同钟、无跨域问题的基准组)。
一句话总结
多 bit 总线跨时钟域逐位打两拍,错不在亚稳态而在「位间偏移 + 多位同翻」——目的域会采到源端从未发出的新旧混搭值;本篇用 1-4 ns 偏移模型实测 111 拍:直接路径 4 次非法采样(恰好全是 7↔8、15↔0 这类多位同翻跳变),格雷码路径零非法。单调数据用格雷码,非单调数据用握手或异步 FIFO。
实验环境
- 机器/系统:云主机 S6.4XLARGE32 16C32G / Ubuntu 22.04(eda-golden-v1 镜像)
- 工具版本:iverilog -g2012(eda-golden-v1 镜像自带 Icarus Verilog)
- 日期:2026-10-25
- 完整命令:
bash run.sh(内部执行iverilog -g2012 -o sim.vvp src_counter.v tb_multibit_cdc.v,随后vvp sim.vvp,最后grep -c 'DIRECT illegal' sim.log与grep 'RESULT: PASS' sim.log)
代码获取
完整 RTL 与测试平台已同步到公开仓库:geek-doc.cn/experiments/exp-multibit-cdc-glitch,克隆后进入该目录执行 bash run.sh 即可复现本文全部结果。