Veloris.
返回索引
概念基础 2026-02-23

组合逻辑 vs 时序逻辑:一个管"算",一个管"记"

7 分钟
2.2k words

组合逻辑 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

避免组合逻辑环路的三条铁律:

  1. always @(*) 块中,所有条件分支都要赋值(写全 if-else、加 default
  2. 不要在组合逻辑中让信号依赖自身
  3. 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 写法assignalways @(*)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(当前输入),用 assignalways @(*),阻塞赋值 =
时序逻辑输出 = f(当前输入, 历史状态),用 always @(posedge clk),非阻塞赋值 <=
最大陷阱组合逻辑里漏写分支 → 生成 latch → 环路 / 时序问题
铁律组合用 =,时序用 <=,永远不要混用

下一步:

  • 想深入理解组合逻辑的毛刺问题?→ 阅读下一篇《竞争与冒险》
  • 想搞懂为什么时序逻辑会出现亚稳态?→ 阅读《时序逻辑电路的亚稳态》
  • 动手练习:试着用 Verilog 写一个 4 位计数器,体会时序逻辑的”记忆”能力

常见问题

💬 组合逻辑的延迟会不会导致问题?

会。组合逻辑的路径延迟决定了电路的最高工作频率。如果组合逻辑链太长,信号无法在一个时钟周期内稳定下来,就会导致时序违例。解决办法是插入流水线寄存器,把长的组合逻辑路径拆成多段。

💬 为什么综合工具会报 latch warning?

因为你在 always @(*) 块中没有覆盖所有条件分支。综合工具不得不推断出一个锁存器来”记住”未赋值时的旧状态。解决方法很简单:在 always @(*) 块开头给所有输出信号一个默认值。

💬 一个模块里可以同时包含组合逻辑和时序逻辑吗?

当然可以,而且这是最常见的写法。推荐的做法是将组合逻辑和时序逻辑分成独立的 always,这样代码更清晰,综合工具也更容易优化。


参考资料

  1. David Harris & Sarah Harris, Digital Design and Computer Architecture, Morgan Kaufmann
  2. Clifford E. Cummings, Nonblocking Assignments in Verilog Synthesis, Coding Styles That Kill!, SNUG 2000
  3. Xilinx/AMD, UG901: Vivado Design Suite User Guide — Synthesis

系列导航:本文是「FPGA 入门系列」第 12 篇。

如果这篇文章对你有帮助,欢迎点赞、收藏,也欢迎在评论区交流你对组合逻辑和时序逻辑的理解。

End of file.