4. Verilog基础(二):组合逻辑与时序逻辑

好,咱们接着聊。上一章我们把Verilog的基本语法过了一遍,这一章要动真格的了——组合逻辑和时序逻辑,这是数字电路的两大基石。说白了,你写的所有RTL代码,最终都会映射成这两种逻辑的混合体。

我个人习惯把组合逻辑比作「直来直去的路」,信号进去,立马出来,不带任何记忆。时序逻辑呢,就像「带寄存器的流水线」,信号要等时钟沿来了才更新。搞懂这两者的区别,你写代码时心里就有底了。

4.1 组合逻辑基础

组合逻辑的特点是:输出只取决于当前输入,跟历史状态无关。没有时钟,没有存储,纯纯的「即时响应」。

4.1.1 基本门电路:与或非

先看最简单的与或非门。Verilog里用 &|~ 表示。我在项目中见过不少新手把位运算和逻辑运算搞混,这里要特别注意。

// 与或非门示例
module basic_gates (
    input  wire a, b,
    output wire y_and,
    output wire y_or,
    output wire y_not
);
    assign y_and = a & b;   // 与门
    assign y_or  = a | b;   // 或门
    assign y_not = ~a;      // 非门
endmodule

嗯,这里要注意:assign 语句是连续赋值,只要右边变了,左边立刻更新。这就是组合逻辑的本质。

4.1.2 多路选择器

多路选择器(MUX)是组合逻辑里的常客。我做过一个数据通路的设计,里面用了上百个MUX,选得我眼花缭乱。

always @(*) 配合 case 语句写MUX,是最直观的方式:

// 4选1多路选择器
module mux4 (
    input  wire [1:0] sel,
    input  wire [3:0] in0, in1, in2, in3,
    output reg  [3:0] out
);
    always @(*) begin
        case (sel)
            2'b00:   out = in0;
            2'b01:   out = in1;
            2'b10:   out = in2;
            2'b11:   out = in3;
            default: out = 4'b0;
        endcase
    end
endmodule

你想想看,为什么这里用 reg 类型?其实在 always 块里赋值的变量,必须声明为 reg,但它综合出来还是组合逻辑。别被名字骗了。

4.1.3 加法器

加法器是算术运算的核心。最简单的写法就是用 + 运算符,综合工具会自动帮你生成合适的加法器结构。

// 8位加法器
module adder8 (
    input  wire [7:0] a, b,
    input  wire       cin,
    output wire [7:0] sum,
    output wire       cout
);
    assign {cout, sum} = a + b + cin;
endmodule

我曾经在项目中遇到一个坑:两个16位数相加,结果只取了低16位,进位直接扔了。仿真没问题,上板子就出bug。后来查了半天,才发现是位宽没对齐。所以加法器一定要考虑进位,尤其是级联的时候。

组合逻辑避坑指南

  • 所有组合逻辑的输入变化,输出必须立刻变化——不能有锁存器(latch)
  • always @(*) 块里,每个分支都要给所有输出赋值,否则综合出latch
  • 位宽匹配:运算结果不要截断,除非你明确知道要丢弃高位

4.2 时序逻辑基础

时序逻辑就不一样了。它靠时钟驱动,有记忆能力。说白了,就是「等时钟来了再干活」。

4.2.1 触发器与寄存器

触发器(Flip-Flop)是时序逻辑的最小单元。寄存器就是一组触发器拼在一起。在Verilog里,用 always @(posedge clk) 来描述。

// D触发器
module dff (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       d,
    output reg        q
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            q <= 1'b0;
        else
            q <= d;
    end
endmodule

// 8位寄存器
module reg8 (
    input  wire       clk,
    input  wire       rst_n,
    input  wire [7:0] d,
    output reg  [7:0] q
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            q <= 8'b0;
        else
            q <= d;
    end
endmodule

注意看,这里用的是 <=(非阻塞赋值),不是 =。为什么?后面会细讲。

4.2.2 计数器

计数器是时序逻辑的经典应用。我做过一个分频器,计数器从0数到N-1,然后翻转输出时钟。看起来简单,但边界条件很容易搞错。

// 模16计数器
module counter16 (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       en,
    output reg  [3:0] count,
    output reg        overflow
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            count    <= 4'b0;
            overflow <= 1'b0;
        end else if (en) begin
            if (count == 4'd15) begin
                count    <= 4'b0;
                overflow <= 1'b1;
            end else begin
                count    <= count + 1'b1;
                overflow <= 1'b0;
            end
        end
    end
endmodule

嗯,这里有个细节:count == 4'd15 这个判断,如果你写成 count == 4'b1111 也行,但可读性差一些。我个人习惯用十进制数,一眼就能看出模值。

4.3 阻塞赋值与非阻塞赋值

这是Verilog里最容易踩坑的地方,没有之一。我见过太多人因为赋值方式用错,仿真和综合结果不一致,debug到崩溃。

4.3.1 阻塞赋值 =

阻塞赋值是「立即生效」的。执行完当前语句,变量立刻更新,后面的语句能看到新值。它用在组合逻辑的 always @(*) 块里。

// 阻塞赋值示例
always @(*) begin
    a = b;
    c = a;  // 这里c拿到的是b的新值
end

4.3.2 非阻塞赋值 <=

非阻塞赋值是「延迟生效」的。所有赋值语句同时计算右值,然后在时钟沿统一更新左值。它用在时序逻辑的 always @(posedge clk) 块里。

// 非阻塞赋值示例
always @(posedge clk) begin
    a <= b;
    c <= a;  // 这里c拿到的是a的旧值
end

黄金法则:

  • 组合逻辑用阻塞赋值 =
  • 时序逻辑用非阻塞赋值 <=
  • 同一个 always 块里,不要混用两种赋值

我曾经接手过一个项目,前任工程师在时序逻辑里用了阻塞赋值,结果仿真波形看起来没问题,但综合出来的电路多了一级不必要的延迟。后来花了整整两天才定位到问题。从那以后,我写代码前都会默念一遍:时序用非阻塞,组合用阻塞。

4.4 本章知识体系

下面这张图,是我梳理的本章核心逻辑。你可以把它当作一个「思维导图」,方便回顾。

Verilog基础(二) 组合逻辑 时序逻辑 与或非门 多路选择器 加法器 触发器 寄存器 计数器 赋值方式 阻塞赋值 = (组合逻辑用) 非阻塞赋值 <= (时序逻辑用) ⚠ 同一个always块中,禁止混用阻塞和非阻塞赋值

我的小建议:刚开始学的时候,可以先把组合逻辑和时序逻辑分开写。组合逻辑用 assignalways @(*),时序逻辑用 always @(posedge clk)。等熟练了再考虑混合使用。这样不容易出错。

好了,这一章的内容就到这儿。组合逻辑和时序逻辑是FPGA设计的左右手,缺一不可。赋值方式的选择更是决定了你的代码能不能正确综合。多写、多仿真、多对比,慢慢就有感觉了。


公众号:蓝海资料掘金营,微信deep3321