Yosys 单 always 多时钟报错:Multiple edge sensitive events 排查与拆进程修复
更新时间:2026-10-30。本文是 Yosys 单 always 多时钟报错所在的开源 EDA 排错中心 的过程转换节点。
always @(posedge clk_a or posedge clk_b)在仿真语义里像「任一边沿到来就执行」,但综合到寄存器时,Yosys 0.33 会在PROC_DFF阶段直接报ERROR: Multiple edge sensitive events found for this signal!并以状态 1 退出。这里不是多写一个or的语法小问题,而是过程块描述的状态更新无法映射成普通单时钟触发器。
本文要回答的问题
- 完整报错出现在哪个 pass,退出码是多少?
- 为什么两个时钟边沿不能驱动同一个时序过程?
- 多时钟状态、时钟二选一和异步复位应该怎样区分?
- 怎样按寄存器拆进程,并用真实日志确认修复?
这张图的主线是「双沿敏感表 → 单沿寄存器」。绿色收尾不是仅凭源码看起来顺眼,而是要求日志同时给出 DFF 创建记录、Found and reported 0 problems 和退出码 0。
一、Yosys 单 always 多时钟报错的完整现场
最小坏例让 q_a、q_b 在同一个过程中赋值,敏感表却列了两个互不相关的时钟:
module multi_clock_bad (
input wire clk_a,
input wire clk_b,
input wire d_a,
input wire d_b,
output reg q_a,
output reg q_b
);
always @(posedge clk_a or posedge clk_b) begin
q_a <= d_a;
q_b <= d_b;
end
endmodule在源码目录运行 yosys -p 'read_verilog multi_clock_bad.v; hierarchy -top multi_clock_bad; proc; check'。Verilog 前端和 hierarchy 都能通过,失败发生在 proc 内部;因此不要沿「模块没找到」或「语法不支持」方向排查。
这段输出有三个定位点:3.9. Executing PROC_DFF pass 指明正在把过程同步事件转换成触发器;ERROR 的宾语是 this signal,说明当前寄存器找不到唯一的边沿时钟;紧接着的 # bad-exit-code: 1 证明流程已经中止,后面的 check 没有机会执行。日志第 66 行还保留 $proc$multi_clock_bad.v:9$1,可回查第 9 行的 always。
还要注意失败位置不在 read_verilog。前端已经打印 Successfully finished Verilog frontend,说明源码能被解析;如果只在编辑器里做语法检查,可能完全看不到这个问题。把 2>&1 合并进日志也很重要:自动运行时既要保存诊断原文,又要保存进程退出码,不能只凭终端最后停在哪一行猜测结果。
二、根因:一个普通触发器只能有一个采样时钟
proc 不是单一动作,而是一组进程转换 pass。PROC_MUX 先处理决策树,PROC_DLATCH 处理电平保持,轮到 PROC_DFF 时,工具要把边沿敏感过程落成 D 触发器。常规 DFF 只有一个时钟端;clk_a 与 clk_b 任意一个都能改变状态,就意味着两个异步来源都能驱动存储单元,已经超出这个目标结构。
这也是「仿真能执行」与「综合能实现」之间的差别。仿真器可以在任一边沿到来时调度过程,再执行其中的非阻塞赋值;综合器却必须给出一块确定的硬件。若把两个时钟直接接到普通组合逻辑再送回时钟端,会产生由布线延迟决定的脉冲和毛刺,并没有获得仿真中那种理想事件调度语义。Yosys 在此处拒绝转换,是在阻止这种不确定结构悄悄进入网表。
下面的排错图把「两个独立状态时钟」与「合法多路时钟选择」分开。红色是报错与不支持的多沿状态,蓝色是判读动作,橙色是修复,绿色是合法分支。
读图时先问「两个边沿是两套状态,还是同一个状态的两路候选时钟」。前者应拆过程;后者是时钟选择问题,应使用目标器件的专用时钟 MUX,并处理切换毛刺和时序约束。把 or 留在敏感表里既不能实现无毛刺切换,也不能代替跨时钟域设计。
专用时钟 MUX 也不是随手写一个组合二选一就结束。选择信号变化时,如果两路时钟电平不同,普通 LUT 多路器可能制造窄脉冲;设计还需要确认器件是否提供无毛刺切换原语、切换时是否需要先关闭当前时钟,以及静态时序分析怎样识别生成时钟。这些属于时钟架构工作,不能靠修改敏感表省略。
三、排查清单:别把它和复位、多驱动混淆
大设计里可以按下表逐项收窄。重点是敏感表和赋值归属,不必先怀疑综合脚本。
| 检查项 | 看到什么 | 处置 |
|---|---|---|
| 边沿数量 | 同一过程出现两个时钟的 posedge/negedge | 按时钟域拆成多个过程 |
| 异步复位 | 一个时钟边沿,加一个 posedge rst | 保留明确的复位分支,不属于本文双时钟坏例 |
| 时钟选择 | 想在工作时钟 A/B 间二选一 | 用专用时钟 MUX,检查毛刺与约束 |
| 跨域数据 | d_a/d_b 来自不同异步域 | 另做同步或握手,拆进程不解决 CDC |
| 多驱动 | 不同过程给同一寄存器赋值 | 查驱动冲突,勿与单过程多时钟混为一谈 |
异步复位之所以常见且可以综合,是因为语义里仍有唯一采样时钟,另一个边沿被映射到异步复位端;它并不等于「任意两个时钟都能或在一起」。反之,拆成两个过程以后,如果仍让两个过程驱动同一个 q,问题就从多沿过程转成多驱动,需要继续合并逻辑,而不是机械复制代码。
四、修复代码与复跑 PASS
本例的两个寄存器本来就各吃一份数据,按归属拆开即可:
always @(posedge clk_a) begin
q_a <= d_a;
end
always @(posedge clk_b) begin
q_b <= d_b;
end拆开后保持原来的非阻塞赋值,不在两个过程之间交叉赋值。每个寄存器现在只响应自己的时钟,其他输入只是数据端;若原过程里两个输出存在联合条件,还应把条件明确复制到各自时钟域,并检查该条件是否跨域,而不是只按行数平均切开。
复跑 yosys -p 'read_verilog multi_clock_fixed.v; hierarchy -top multi_clock_fixed; proc; check'。下面的原始摘录保留了两个寄存器的转换记录、检查结果和各自退出码。
3.9. Executing PROC_DFF pass (convert process syncs to FFs).
Creating register for signal `\multi_clock_fixed.\q_b' using process `\multi_clock_fixed.$proc$multi_clock_fixed.v:13$2'.
created $dff cell `$procdff$3' with positive edge clock.
Creating register for signal `\multi_clock_fixed.\q_a' using process `\multi_clock_fixed.$proc$multi_clock_fixed.v:9$1'.
created $dff cell `$procdff$4' with positive edge clock.
3.10. Executing PROC_MEMWR pass (convert process memory writes to cells).
3.11. Executing PROC_CLEAN pass (remove empty processes).
Removing empty process `multi_clock_fixed.$proc$multi_clock_fixed.v:13$2'.
Removing empty process `multi_clock_fixed.$proc$multi_clock_fixed.v:9$1'.
Cleaned up 0 empty switches.
3.12. Executing OPT_EXPR pass (perform const folding).
Optimizing module multi_clock_fixed.
4. Executing CHECK pass (checking for obvious problems).
Checking module multi_clock_fixed...
Found and reported 0 problems.
End of script. Logfile hash: 4ea40b6109, CPU: user 0.01s system 0.00s, MEM: 9.25 MB peak
Yosys 0.33 (git sha1 2584903a060)
Time spent: 39% 1x opt_expr (0 sec), 25% 2x read_verilog (0 sec), ...
# fixed-exit-code: 0
RESULT: PASS (bad rejected with exit 1; fixed passed with exit 0)两个 $dff 都明确标为 positive edge clock,q_a 与 q_b 分别来自第 9、13 行的过程;check 报 0 个问题,修复例退出 0。这里的 PASS 只证明该 RTL 已通过本次 hierarchy/proc/check,没有另行运行功能仿真或形式等价检查,因此不声称拆进程前后功能等价。若两个时钟在系统里并非独立,而是需要跨域传递数据,还必须补充 CDC 验证。
代码获取
完整源码、运行脚本和真实日志已放在公开仓 experiments/ts-yosys-multiple-clocks-always。装好 Yosys 后执行 bash run.sh,脚本会检查坏例退出 1、修复例退出 0,并在两者符合预期时打印 RESULT: PASS。
实验环境
- 机器/系统:Mac mini M4 / macOS,Docker 容器内运行。
- 工具版本:Yosys 0.33 (git sha1 2584903a060),
geek-eda:0.2镜像。 - 日期:2026-10-30。
- 完整命令:
yosys -p 'read_verilog multi_clock_bad.v; hierarchy -top multi_clock_bad; proc; check';yosys -p 'read_verilog multi_clock_fixed.v; hierarchy -top multi_clock_fixed; proc; check'。两段输出合并记录,2>&1保留标准错误。
这一篇在知识体系里的位置
本篇处理的是「单个过程如何转换成 DFF」。它不同于 Yosys 单 always 多时钟报错之外的多驱动排查:后者关注多个来源共同驱动一个信号,本篇关注一个过程声明了多个时钟边沿。过程转换还会遇到 Yosys proc_dlatch 推断出锁存器,那是组合分支未覆盖导致存储元件被引入,不能把两类日志混用同一修复模板。
总结
Yosys 单 always 多时钟报错的根因是:PROC_DFF 无法把一个含两个独立时钟边沿的过程映射成普通单时钟 DFF。看到 Multiple edge sensitive events found for this signal!,先核对敏感表和设计意图:多时钟状态按寄存器拆进程;合法时钟二选一走专用时钟 MUX 与约束;跨域数据另做 CDC。本文实测坏例退出 1,拆进程后生成两个正沿 DFF、check 为 0 问题并退出 0,修复证据以这组日志为准。