Vivado [Synth 8-91] ambiguous clock:一个 always 挂两个时钟沿的综合报错与时钟域拆分修法
更新时间:2026-11-08。排错速查页,属于开源 EDA 排错系列,讲 Vivado 综合期一类典型的「仿真能跑、硬件无解」报错。同一份 Verilog 在仿真器里波形正常,丢给
synth_design却在敏感列表那一行甩出ERROR: [Synth 8-91] ambiguous clock in event control,后面还跟着[Synth 8-285]与[Common 17-69]两条连锁报错。Vivado Synth 8-91 涉及的时钟与复位基本写法见 时钟与复位设计,这一报错背后的单时钟工程纪律见 同步设计原则。
首屏答案:ERROR: [Synth 8-91] ambiguous clock in event control 的意思是——一个 always 块的敏感列表里出现了两个时钟边缘(本例是 @(posedge clk1 or posedge clk2)),而 FPGA 里的每个触发器只有一个时钟输入引脚,综合器无法把「同一个寄存器被两个时钟驱动」映射成真实电路,于是直接拒收。注意它和合法的异步复位写法 @(posedge clk or negedge rstn) 形似神不似:复位沿走的是触发器的预置/清零引脚,不是第二个时钟。修法是时钟域拆分:每个时钟域各写一个单时钟 always 块、各自养一个 flop,再在数据通路上用组合 mux 合并输出(assign q = sel ? q1 : q2)。本例按此改写后重跑综合,结尾是 Synthesis finished with 0 errors, 0 critical warnings and 0 warnings.,网表落地 2 个 FDRE 触发器、1 个 LUT3、2 个 BUFG,与「两个 flop 加一个 mux」的设计意图完全对得上。
本文要回答的问题
[Synth 8-91]报错钉在第几行,三条 ERROR 之间谁是根因、谁是连锁反应?- 为什么
@(posedge clk1 or posedge clk2)仿真能跑出波形,综合却说 ambiguous clock? - 异步复位也是敏感列表里放两个沿,
@(posedge clk or negedge rstn)为什么合法? - 「按 sel 选择两路数据」这个真实需求,在可综合 RTL 里应该怎么表达?
- 拆成两个 flop 再加 mux 之后,跨时钟域的尾巴怎么处理?
一、报错原文与现象
最小复现模块 ambclk_bad 有两个时钟输入 clk1、clk2,两组数据 d1、d2,一个选择信号 sel。设计者的意图看起来很自然:哪个时钟跳变就醒来一次,按 sel 决定把哪路数据存进 q。问题就出在第 11 行的敏感列表:
// ambclk_bad.v —— 一个 always 块同时敏感于两个时钟上升沿
module ambclk_bad (
input wire clk1,
input wire clk2,
input wire sel,
input wire d1,
input wire d2,
output reg q
);
always @(posedge clk1 or posedge clk2) begin
if (sel)
q <= d1;
else
q <= d2;
end
endmodule用两行 Tcl 走最朴素的批处理综合:read_verilog ambclk_bad.v 读文件,synth_design -top ambclk_bad -part xc7a35tcpg236-1 启动综合。读文件阶段一声不吭,语法解析完全通过;进入模块综合后立刻失败,stdout 只给计数,真正的 ERROR 解码在 stderr 段:
读这段输出抓三个要点。第一,stdout 的 3 Infos, 0 Warnings, 0 Critical Warnings and 3 Errors encountered. 说明这不是警告级的风格问题,而是硬失败,synth_design failed 后直接释放许可证退出。第二,三条 ERROR 是逐级上报的链条:[Synth 8-91] 钉在 ambclk_bad.v:11——正是敏感列表那一行,它是根因;[Synth 8-285] failed synthesizing module 'ambclk_bad' 钉在模块入口 :3,意思是模块级综合没做完;[Common 17-69] Command failed 则是 Tcl 命令层的收场白,本身不带新信息。排查时永远顺着链条找最前面那条带具体行号的 ERROR。第三,read_verilog 不拦、synth_design 才炸,因为这不是语法错误——Verilog 标准允许这样写事件控制,仿真器也能执行,死的是「RTL 到硬件原语的映射」这一步。
一个常见误诊值得先排除:有人看到 if (sel) 会怀疑是分支不完整推断锁存器(那是 [Synth 8-327] 的领地),于是去补 else。本例 if/else 两支齐全,且 always 块带时钟沿、本意就是时序逻辑,问题与锁存器无关;把注意力放回敏感列表本身才是对的方向。
二、根因分析:一个 always 块为什么不能挂两个时钟
要理解 Vivado [Synth 8-91],先看 FPGA 触发器的物理事实。7 系列器件里的时序单元是 FDRE/FDCE 这类原生触发器,每个单元只有一个时钟引脚 CLK,外加一个使能端 CE 和一个置位/清零端。综合器处理时序 always 块的工作,是把块内对 reg 的赋值翻译成「这些触发器在 CLK 沿采样 D、更新 Q」。那么 @(posedge clk1 or posedge clk2) 要求综合器回答一个问题:这个 flop 的 CLK 引脚到底接谁?
- 接 clk1,clk2 的沿怎么唤醒它?没有第二个 CLK 引脚。
- 接 clk2,clk1 的沿同样无处安放。
- 先在两个时钟之间插一个 mux 再进 CLK?这等于让综合器自行推断时钟切换电路。门控/切换时钟是必须由设计者显式承担的决策(glitch、时钟树约束、时序分析全部受影响),Vivado 的 RTL 综合不会从一个事件控制表达式里偷偷帮你造,它选择报错。
三种可能都走不通,于是有了 ambiguous clock——时钟归属有歧义,无法映射。
这里必须拆开一个极易混淆的对照:异步复位。下面的写法完全合法,Vivado 不会报 8-91:
// 合法:两个沿,但第二个沿接的是复位引脚,不是第二个时钟
always @(posedge clk or negedge rstn) begin
if (!rstn) q <= 1'b0;
else q <= d;
end原因在综合器的识别规则:敏感列表里第一个沿是时钟,其余沿只有在块内被用作异步置位/复位条件时才合法,它们映射到 FDCE 的预置/清零引脚(PRE/CLR),与 CLK 是不同的物理端口。判断标准不是「列表里有几个沿」,而是「除时钟沿之外的沿有没有声称自己要驱动寄存器采样」。posedge clk2 后面跟着的是普通数据赋值 q <= d2,这就要求它扮演时钟角色,一个 flop 两个时钟,8-91 必然触发。
再补一个仿真视角的解释,回答「为什么 iverilog/xsim 能跑」。仿真器是事件驱动的软件模型:敏感列表只是一串等待事件,clk1 或 clk2 任何一个跳变都唤醒进程,按当前 sel 执行赋值,根本不存在「CLK 引脚接谁」的物理约束。所以这类 bug 的典型特征就是仿真波形自洽、综合期才被硬件现实顶回来——这也是入门阶段最反直觉的一类报错。Vivado Synth 8-91 拦得这么硬,背后的单时钟纪律在 同步设计原则 里有系统讲解。下图把从报错到修复的完整决策路径画成流程,颜色按角色区分:红是报错入口与根因,蓝是排查动作,橙是修复动作,绿是复验闭环,紫是防复发沉淀。
读这张流程卡抓住一个关键判断:第二步「判时钟角色」是整条路径的分叉点。如果第二个沿其实是复位,正确动作是补规范的异步复位结构,与 8-91 无关;只有确认两个沿都在声称时钟身份时,才进入拆分修复。
三、排查与收敛路径
拿到 8-91 后的机内排查动作可以进一步细化成下图。这张图与流程卡的视角不同:流程卡讲修复步骤,这里讲排查时的判定分支,包括一个真实工程里常见的替代方案——如果你的本意根本不是「两个时钟域各采各的数据」,而是「想在两个时钟之间切换着用」,那修法不是拆 flop,而是显式做时钟选择。
读这张图抓住两个分叉。第一个分叉在「设计本意」:ambclk_bad 的端口结构(两路独立数据加一个 sel)说明本意是两域并存,走修复 A;如果工程里 clk1、clk2 是同频不同源的备选时钟、下游只有一套逻辑,那应该走修复 B,用 Clocking Wizard 或 BUFGCTRL 显式生成一个干净的切换后时钟,再让所有 flop 挂这一个时钟——时钟上的 mux 永远要显式、要过约束检查,绝不能藏在 always 的事件控制里让综合器猜。第二个分叉在复验:绿色节点不只是「没报错」,还要打开资源报告核对原语数量,确认综合出来的电路和你脑子里的电路图一致,这一步的实测结果放在下一节。
排查时还有两个实用动作。一是全工程扫同类写法:grep -n "posedge.*or.*posedge" *.v 一遍就能把所有双时钟沿的 always 块拎出来人工过一遍,合法的异步复位会带 negedge rstn 之类的复位命名,很好区分。二是区分报错家族:敏感列表问题归 8-91,分支不完整推断锁存器归 Synth 8-327 推断锁存器排查,同一根网线被多个 always 驱动归 8-3352,别混着修。
四、修复:时钟域拆分加 mux 合并
按修复 A 改写后的 ambclk_fixed 做了三件事:q1 只在 clk1 沿采 d1,q2 只在 clk2 沿采 d2,选择逻辑全部下沉到时钟之外的一条连续赋值:
// ambclk_fixed.v —— 两个单时钟 flop,加一个数据通路 mux
module ambclk_fixed (
input wire clk1,
input wire clk2,
input wire sel,
input wire d1,
input wire d2,
output wire q
);
reg q1;
reg q2;
always @(posedge clk1) begin
q1 <= d1;
end
always @(posedge clk2) begin
q2 <= d2;
end
assign q = sel ? q1 : q2;
endmodule改写前后的结构差异值得对着硬件再讲一遍。坏版本里一个 flop 试图有两个 CLK,无器件可映射;好版本里两个 always 块各自只有一个时钟沿,分别干净地映射成两个 FDRE,sel ? q1 : q2 是纯组合选择,映射成一个 LUT3(两个数据输入加一个选择输入,正好三输入)。选择动作发生在 D 端的数据通路上,时钟树各走各的,两个时钟各自上 BUFG,互不打扰。重跑综合,这次一路走到网表写出:
这段输出里有三个验证锚点。第一,Cell Usage 表列出 FDRE 2、LUT3 1、BUFG 2,加上 IBUF/OBUF 这些 IO 原语,与「两个单时钟 flop、一个二选一 mux、两棵时钟树」的设计意图逐项吻合;RTL Component Statistics 也独立给出 1 Bit Registers := 2 和 2 Input 1 Bit Muxes := 1,两份报告互相印证。第二,结尾明写 Synthesis finished with 0 errors, 0 critical warnings and 0 warnings.——注意警告数也是 0,说明拆分没有引入新的多驱动、位宽截断之类的副作用。第三,synth_design completed successfully 取代了坏版本的 Command failed,Tcl 流程正常退出,随后的 report_utilization 显示 Slice Registers 用 2 个、LUT 用 1 个,器件占用率均低于 0.01%。
把两次运行并排看,差异收敛得非常干净:
| 维度 | ambclk_bad(双沿 always) | ambclk_fixed(双 flop 加 mux) |
|---|---|---|
| 敏感列表 | posedge clk1 or posedge clk2 | 每块一个 posedge,共两个 always |
| 综合结果 | 3 Errors,模块综合失败 | 0 errors / 0 warnings,成功出网表 |
| 触发器 | 无法映射 | FDRE × 2 |
| 选择逻辑 | 混在时钟行为里 | LUT3 × 1(纯组合 mux) |
| 时钟树 | 归属歧义 | BUFG × 2,各域独立 |
| 报错链条 | 8-91 → 8-285 → 17-69 | 无 |
五、修后余波:数据 mux 跨时钟域的注意事项
综合过了不等于设计完全没问题,拆分修法留下一条必须正视的尾巴:q1 和 q2 生活在两个不同的时钟域里,mux 输出 q 是两路异步数据的组合选择。如果 q 直接被第三个时钟域(甚至 clk1/clk2 自己)的触发器采样,而 sel 恰好在数据跳变附近翻转,下游就可能采到亚稳态或毛刺。Vivado [Synth 8-91] 解决的是「写不出来的电路」,CDC(Clock Domain Crossing,跨时钟域)问题则是「电路能写出来但行为可能不可靠」,两者是不同层面的关卡。
工程上按场景分三档处理:
- sel 静态或极慢变化(如上电配置一次、软件寄存器偶尔切换):本例的纯 mux 够用,但要保证 sel 切换时刻与两路数据的有效窗口错开,下游采样寄存器留足收敛时间。
- sel 跟随某个时钟同步变化:先把 sel 在目标时钟域打两拍同步,再让同步后的选择信号去控 mux,避免选择线本身带着亚稳态进 LUT。
- 两路数据都要被同一个下游时钟连续消费:别用组合 mux 硬切,改成标准 CDC 结构——异步 FIFO 或带握手的双口 RAM,让数据而不是波形在域间安全传递。
CDC 结构检查的完整方法论与常见失效波形(数据保持不足、收敛 Glitch 等)见 CDC 数据保持丢失与毛刺模式,约束生成流程见 CDC 约束生成流程。入门阶段先养成两个习惯:每个 always 块只挂一个时钟,跨域数据先问「谁采样它、用哪个时钟采」。
六、预防清单
- 敏感列表纪律:时序 always 的事件控制里只许出现一个时钟沿;异步置位/复位沿可以并列,但命名和块内结构必须明确表达复位身份,评审时见到两个
posedge并排一律停下来人工确认。 - 需求先分类再动手:「两域各采各的数据」走 flop 拆分加数据 mux;「两个时钟切换着用」走 MMCM/Clocking Wizard 或 BUFGCTRL 显式时钟选择,两条路不可混用。
- 选择逻辑放数据通路:任何 mux 都出现在 D 端的组合逻辑里,时钟路径上只允许出现专用时钟资源,不靠 always 的事件控制隐式造时钟 mux。
- 提交线外拦截:把
grep -n "posedge[[:space:]]*[A-Za-z0-9_]*[[:space:]]*or[[:space:]]*posedge" *.v加进 lint 脚本,双时钟沿写法在进综合前就被拦下;综合日志里 8-91/8-285/17-69 任一出现即判流水线失败。 - 复验看资源报告:修复后的 Cell Usage 要与电路图对得上(本例 FDRE 2 加 LUT3 1),「0 errors」加上原语数量正确才算闭环;多驱动类的相邻坑见 Vivado 多驱动报错 8-3352 排查。
- CDC 单列检查:拆分时钟域之后,把 mux 输出和所有跨域信号列入 CDC 评审清单,需要同步器或异步 FIFO 的位置在评审阶段补齐,别等到板级随机死机再回溯。
实验环境
- 机器/系统:Windows(64 位)
- 工具版本:Vivado 2017.4(SW Build 2086221,batch 模式
synth_design,目标器件 xc7a35tcpg236-1) - 日期:2026-11-08
- 完整命令:
vivado.bat -mode batch -source v5_err.tcl(报错复现,Tcl 内容为read_verilog ambclk_bad.v加synth_design -top ambclk_bad -part xc7a35tcpg236-1);vivado.bat -mode batch -source v5_fix.tcl(修复复现,Tcl 内容为read_verilog ambclk_fixed.v加同器件synth_design加report_utilization)
代码获取:完整复现工程(ambclk_bad.v / ambclk_fixed.v / 两份综合 Tcl,以及两份实测日志)在公开仓 https://github.com/derekzhuo/geek-doc.cn/tree/main/experiments/vivado-ambiguous-clock,装好 Vivado 2017.4(或任意含 Vivado 综合的版本)后在工程目录按 README 跑两组批处理命令即可复现报错与修复。
这一篇在知识体系里的位置
本篇是 Vivado 综合报错簇的一员,从实跑视角讲「单 always 双时钟沿无法映射触发器」的成因、时钟域拆分修法与修后的 CDC 尾巴。概念层的时钟与复位写法基础见 时钟与复位设计,单时钟同步设计的工程纪律见 同步设计原则,always 块的三种典型场景见 always 块的三种典型场景。同簇相邻排错:组合分支不完整推断锁存器见 Vivado [Synth 8-327] 推断锁存器排查,同一 reg 被多个 always 驱动见 Vivado 多驱动报错 8-3352 排查;跨时钟域的系统方法论见 CDC 数据保持丢失与毛刺模式。
总结
ERROR: [Synth 8-91] ambiguous clock in event control 是综合器发现一个时序 always 块的敏感列表里有两个信号都在扮演时钟角色,而 FPGA 触发器只有一个 CLK 引脚、综合器也拒绝隐式推断时钟 mux,于是在 ambclk_bad.v:11 拒收,并连锁抛出 8-285 与 17-69;它和合法的异步复位 @(posedge clk or negedge rstn) 的本质区别是复位沿走 PRE/CLR 引脚、不声称时钟身份,仿真能跑而综合失败正是「事件驱动模型没有引脚约束」的典型表现。修法是按时钟域拆成两个单时钟 always、各养一个 flop,再用 assign q = sel ? q1 : q2 在数据通路上合并;实测修复后综合 0 errors、0 warnings,网表为 FDRE 两个加 LUT3 一个加 BUFG 两个,与设计意图逐项吻合。留下的尾巴是 mux 输出的跨时钟域可靠性:sel 静态可直接用,动态切换先同步 sel,连续数据流走异步 FIFO,把 Vivado [Synth 8-91] 的「能综合」与 CDC 的「综合出来可靠」分成两道关卡分别守住。