组合逻辑 vs 时序逻辑:一个管”算”,一个管”记”
💡 你写了一段看起来完全没问题的 Verilog 代码,仿真也跑过了,结果烧到板子上却时灵时不灵。排查半天,发现问题出在一个 always @(*) 块里——你不小心让一个信号的输出绕了一圈又回到了自己的输入,形成了组合逻辑环路。
这类 Bug 的根源,往往是对组合逻辑和时序逻辑的本质区别理解不够清晰。这两种电路是 FPGA 设计的基石——搞清楚它们,你就能避开大多数初级陷阱。
这篇文章带你从本质上理解这两种电路:它们分别是什么、怎么写、有什么坑。
1. 组合逻辑电路:纯粹的”计算器”
组合逻辑电路(Combinational Logic Circuit) 的核心特征只有一句话:输出完全由当前输入决定,与历史状态无关。
你可以把它想象成一个纯函数——给同样的输入,永远得到同样的输出,没有”记忆”。
电路特点:
- 没有反馈回路,没有存储元件
- 输入变化后,输出经过一段传播延迟后立即跟着变化
- 典型器件:加法器、多路选择器(MUX)、编码器、解码器、比较器等
1.1 Verilog 中怎么写组合逻辑
在 Verilog 中,有两种方式描述组合逻辑:
// 方式1:assign 连续赋值
assign y = a & b | c;
// 方式2:always @(*) 块 + 阻塞赋值
always @(*) begin
if (sel)
y = a;
else
y = b;
end
关键规则:
always块的敏感列表使用@(*)(自动包含所有输入信号)- 使用阻塞赋值
=,因为组合逻辑的本质就是”立即计算”
1.2 组合逻辑的头号陷阱:组合逻辑环路
组合逻辑最容易踩的坑就是组合逻辑环路(Combinational Loop)——信号的输出经过一系列逻辑门后,又绕回了自己的输入端。
// ❌ 错误示例:组合逻辑环路
module combinational_loop_bad (
input wire a,
input wire b,
output wire y
);
wire temp;
assign temp = a & y; // y 影响 temp
assign y = temp | b; // temp 又影响 y → 形成环路!
endmodule
还有一种更隐蔽的情况——always @(*) 块中遗漏 else 分支:
// ❌ 隐蔽的环路:缺少 else 分支导致 latch
module latch_bad (
input wire enable,
input wire data,
output reg q
);
always @(*) begin
if (enable)
q = data;
// else 缺失 → q 保持旧值 → q 依赖自身 → 环路!
end
endmodule
环路的危害:
- 振荡:输出在 0 和 1 之间快速翻转,永远无法稳定
- 时序分析失败:STA 工具无法分析没有起点和终点的路径
- 仿真与硬件行为不一致:仿真可能”看起来正常”,实际硬件完全失效
💡 工程师手记:刚开始学 FPGA 的时候,我在一个
always @(*)块里写了个条件赋值,忘了写else分支。仿真完全正常,但综合后 Vivado 报了一堆 latch warning。当时不理解为什么,后来才明白——缺少 else 就意味着信号要”记住”旧值,但组合逻辑没有记忆能力,综合工具只能推断出一个锁存器,而锁存器在同步设计中就是定时炸弹。
正确做法:用触发器代替锁存器
// ✅ 正确:使用时序逻辑实现"记忆"功能
module register_best (
input wire clk,
input wire rst,
input wire enable,
input wire data,
output reg q
);
always @(posedge clk) begin
if (rst)
q <= 1'b0;
else if (enable)
q <= data;
// enable=0 时 q 保持,这是寄存器的正常行为,不是环路
end
endmodule
避免组合逻辑环路的三条铁律:
always @(*)块中,所有条件分支都要赋值(写全if-else、加default)- 不要在组合逻辑中让信号依赖自身
- 在
always @(*)块开头给输出信号一个默认值
2. 时序逻辑电路:有”记忆”的电路
时序逻辑电路(Sequential Logic Circuit) 与组合逻辑的本质区别在于:输出不仅取决于当前输入,还取决于电路之前的状态。
换句话说,时序逻辑有”记忆”——它能记住上一个时钟周期发生了什么。
电路特点:
- 核心存储元件是 D 触发器(Flip-Flop)
- 输出只在时钟边沿(通常是上升沿)才更新
- 两个时钟沿之间,输出保持不变,不受输入变化影响
2.1 Verilog 中怎么写时序逻辑
// 时序逻辑:always @(posedge clk) + 非阻塞赋值
always @(posedge clk) begin
if (rst)
q <= 1'b0;
else
q <= d;
end
关键规则:
- 敏感列表是时钟边沿:
@(posedge clk)或@(negedge clk) - 使用非阻塞赋值
<=,因为硬件中所有触发器是同时翻转的 reg类型变量在时序逻辑中会被综合为真正的寄存器
2.2 为什么时序逻辑要用非阻塞赋值
这是初学者最常问的问题之一。看这个例子:
// ❌ 错误:时序逻辑中用阻塞赋值
always @(posedge clk) begin
a = b; // a 立即变成 b 的值
b = a; // b 拿到的是更新后的 a(也就是 b 自己)→ 交换失败!
end
// ✅ 正确:时序逻辑中用非阻塞赋值
always @(posedge clk) begin
a <= b; // 计划:a 将变为 b 的旧值
b <= a; // 计划:b 将变为 a 的旧值 → 完美交换!
end
非阻塞赋值的机制是”先全部读取旧值,最后统一更新”,这正好模拟了硬件中多个触发器在同一个时钟沿同时采样、同时翻转的物理行为。
💡 工程师手记:理解非阻塞赋值有一个很好的心智模型——想象所有触发器手里都拿着一张纸条,时钟沿到来时,它们同时看一眼自己的输入(读旧值),把结果写在纸条上,然后同时亮出纸条(更新输出)。没有谁先谁后,大家是并行的。
3. 核心对比:一个管”算”,一个管”记”
这是全文最重要的部分。理解了这张表,你就抓住了 FPGA 设计的基本功。
| 维度 | 组合逻辑 | 时序逻辑 |
|---|---|---|
| 本质 | 纯计算,无记忆 | 有记忆,能存状态 |
| 输出取决于 | 仅当前输入 | 当前输入 + 历史状态 |
| 核心元件 | 逻辑门(AND/OR/NOT/XOR) | D 触发器(Flip-Flop) |
| 更新时机 | 输入变化后立即更新(有传播延迟) | 仅在时钟边沿更新 |
| Verilog 写法 | assign 或 always @(*) | always @(posedge clk) |
| 赋值方式 | 阻塞赋值 = | 非阻塞赋值 <= |
| 典型应用 | 加法器、MUX、解码器 | 计数器、移位寄存器、状态机 |
| 常见陷阱 | 组合逻辑环路、意外生成 latch | 竞争条件(阻塞/非阻塞混用) |
一句话总结:组合逻辑是”没有记忆的即时计算”,时序逻辑是”在时钟节拍下有记忆地工作”。
💬 你可能会问:实际项目中,组合逻辑和时序逻辑的比例大概是多少?
在典型的 FPGA 设计中,两者是密不可分的。时序逻辑负责存储状态和同步,组合逻辑负责在两个时钟沿之间完成计算。一个模块通常是”组合逻辑计算 → 时序逻辑锁存”的循环结构。你可以把组合逻辑想象成”大脑在思考”,时序逻辑想象成”把思考结果记到笔记本上”。
4. 写代码时的实操指南
4.1 怎么判断该用哪种逻辑
- 如果你需要的功能是纯计算(给输入,立刻出结果)→ 组合逻辑
- 如果你需要记住状态或在特定时刻更新 → 时序逻辑
- 如果你不确定 → 默认用时序逻辑,这样更安全
4.2 编码规范速查
// ✅ 组合逻辑模板
always @(*) begin
y = 1'b0; // 先给默认值,避免 latch
if (condition)
y = expression;
end
// ✅ 时序逻辑模板
always @(posedge clk) begin
if (rst)
q <= 1'b0; // 复位
else
q <= next_value; // 正常更新
end
💬 你可能会问:Latch 到底是组合逻辑还是时序逻辑?
Latch(锁存器)是一种电平敏感的存储元件,它既不是纯组合逻辑,也不是时钟驱动的时序逻辑,而是介于两者之间的”灰色地带”。在同步设计中,latch 应该被严格避免——它的存在通常意味着你的组合逻辑代码写错了(分支不完整)。如果你需要存储功能,请使用 D 触发器。
5. 总结
| 你需要记住的 | 内容 |
|---|---|
| 组合逻辑 | 输出 = f(当前输入),用 assign 或 always @(*),阻塞赋值 = |
| 时序逻辑 | 输出 = f(当前输入, 历史状态),用 always @(posedge clk),非阻塞赋值 <= |
| 最大陷阱 | 组合逻辑里漏写分支 → 生成 latch → 环路 / 时序问题 |
| 铁律 | 组合用 =,时序用 <=,永远不要混用 |
下一步:
- 想深入理解组合逻辑的毛刺问题?→ 阅读下一篇《竞争与冒险》
- 想搞懂为什么时序逻辑会出现亚稳态?→ 阅读《时序逻辑电路的亚稳态》
- 动手练习:试着用 Verilog 写一个 4 位计数器,体会时序逻辑的”记忆”能力
常见问题
💬 组合逻辑的延迟会不会导致问题?
会。组合逻辑的路径延迟决定了电路的最高工作频率。如果组合逻辑链太长,信号无法在一个时钟周期内稳定下来,就会导致时序违例。解决办法是插入流水线寄存器,把长的组合逻辑路径拆成多段。
💬 为什么综合工具会报 latch warning?
因为你在
always @(*)块中没有覆盖所有条件分支。综合工具不得不推断出一个锁存器来”记住”未赋值时的旧状态。解决方法很简单:在always @(*)块开头给所有输出信号一个默认值。
💬 一个模块里可以同时包含组合逻辑和时序逻辑吗?
当然可以,而且这是最常见的写法。推荐的做法是将组合逻辑和时序逻辑分成独立的
always块,这样代码更清晰,综合工具也更容易优化。
参考资料
- David Harris & Sarah Harris, Digital Design and Computer Architecture, Morgan Kaufmann
- Clifford E. Cummings, Nonblocking Assignments in Verilog Synthesis, Coding Styles That Kill!, SNUG 2000
- Xilinx/AMD, UG901: Vivado Design Suite User Guide — Synthesis
系列导航:本文是「FPGA 入门系列」第 12 篇。
如果这篇文章对你有帮助,欢迎点赞、收藏,也欢迎在评论区交流你对组合逻辑和时序逻辑的理解。