4. Verilog基础与组合逻辑:模块化设计,assign语句,always块,阻塞与非阻塞赋值,组合逻辑电路设计

各位同学,欢迎来到第四章。这一章我们开始真正动手写代码了。说实话,Verilog 这门语言,你把它当成硬件描述语言,而不是编程语言,很多困惑就迎刃而解了。我见过不少新手,上来就把它当 C 语言写,结果综合出来的电路完全不是那么回事。

这一章我们聚焦组合逻辑。组合逻辑说白了就是:输入一变,输出立马跟着变,没有记忆功能。你想想看,加法器、多路选择器,都是典型的组合逻辑。好,我们开始。

4.1 模块化设计:把大电路拆成小积木

做芯片设计,没人会写一个几万行的文件。都是拆成模块,一个模块干一件事。模块化设计的好处很明显:好调试、好复用、好维护。

一个 Verilog 模块长这样:

module adder (
    input  [3:0] a,
    input  [3:0] b,
    output [4:0] sum
);
    assign sum = a + b;
endmodule

嗯,这里要注意:moduleendmodule 是成对出现的。端口列表里,inputoutputinout 三种方向。我个人习惯把输入放前面,输出放后面,这样读起来顺。

模块化设计还有个关键点:一个文件只放一个模块。我在项目中遇到过有人把十几个模块塞一个文件里,结果后来改一个模块,整个文件都得重新编译,浪费时间不说,还容易改出 bug。

小技巧:模块名和文件名保持一致。比如模块叫 adder,文件就叫 adder.v。这是行业惯例,别问为什么,照做就行。

4.2 assign 语句:连续赋值,硬件连线

assign 语句,说白了就是一根导线。左边是输出,右边是输入。只要右边变了,左边立刻更新。这就是组合逻辑的本质。

assign y = (sel) ? a : b;

这行代码综合出来就是一个二选一多路选择器。你想想看,如果 sel 是 1,y 就连到 a;如果是 0,就连到 b。硬件上就是一堆晶体管开关。

使用 assign 有几个要点:

  • 左边必须是 wire 类型,不能是 reg
  • 右边可以是任何表达式,包括运算符、函数调用
  • 多个 assign 语句是并行执行的,没有先后顺序
避坑指南:我曾经犯过一个低级错误——在 assign 语句里给同一个信号赋值两次。综合工具直接报错,说有多驱动源。记住:一个 wire 只能被一个 assign 驱动。

4.3 always 块:过程赋值,更灵活的描述方式

always 块比 assign 灵活得多。它可以描述组合逻辑,也可以描述时序逻辑。这里我们先讲组合逻辑的用法。

always @(*) begin
    if (sel)
        y = a;
    else
        y = b;
end

注意看,@(*) 表示敏感列表里包含所有输入信号。只要 selab 中任何一个变了,这个块就会重新执行。我个人习惯写组合逻辑时都用 @(*),省得漏掉信号。

组合逻辑的 always 块里,必须把所有输入都列在敏感列表里。漏掉一个,综合出来的电路可能就是个锁存器。嗯,这里要特别小心。

核心原则:组合逻辑的 always 块中,所有被赋值的变量都必须是 reg 类型。但注意,这里的 reg 并不代表寄存器,只是语法要求。综合工具会把它综合成组合逻辑。

4.4 阻塞赋值与非阻塞赋值:一字之差,天壤之别

这是新手最容易踩的坑。阻塞赋值用 =,非阻塞赋值用 <=。区别在哪?

特性 阻塞赋值 (=) 非阻塞赋值 (<=)
执行顺序 顺序执行,阻塞后续语句 并行执行,不阻塞
适用场景 组合逻辑 always 块 时序逻辑 always 块
综合结果 组合逻辑 寄存器/触发器

为什么会这样?你想想看,阻塞赋值就像写软件代码,一行一行执行。而非阻塞赋值,所有赋值操作在同一个时间点同时完成。硬件上,寄存器就是这样的:时钟沿到来时,所有 D 端数据同时打到 Q 端。

我曾经在项目中看到有人把非阻塞赋值用在组合逻辑里,结果仿真和实际电路行为不一致,查了两天才找到原因。记住这个铁律:

铁律:组合逻辑用阻塞赋值 (=),时序逻辑用非阻塞赋值 (<=)。别问为什么,这是硬件描述语言的约定,违反它你会后悔的。

4.5 组合逻辑电路设计实战

理论讲完了,我们动手写几个经典电路。

4.5.1 加法器

最简单的加法器,一行代码搞定:

module adder_4bit (
    input  [3:0] a,
    input  [3:0] b,
    input        cin,
    output [3:0] sum,
    output       cout
);
    assign {cout, sum} = a + b + cin;
endmodule

这里用到了拼接运算符 {},把进位和结果拼在一起。综合工具会自动优化成进位链,你不用担心性能问题。

4.5.2 多路选择器

多路选择器,简称 MUX。用 assign 实现最简单:

module mux_4to1 (
    input  [1:0] sel,
    input  [3:0] d0, d1, d2, d3,
    output [3:0] y
);
    assign y = (sel == 2'b00) ? d0 :
               (sel == 2'b01) ? d1 :
               (sel == 2'b10) ? d2 : d3;
endmodule

always 块实现也可以:

module mux_4to1_always (
    input  [1:0] sel,
    input  [3:0] d0, d1, d2, d3,
    output reg [3:0] y
);
    always @(*) begin
        case (sel)
            2'b00: y = d0;
            2'b01: y = d1;
            2'b10: y = d2;
            2'b11: y = d3;
            default: y = 4'b0;
        endcase
    end
endmodule

注意 case 语句要写全所有分支,否则综合出来可能多出锁存器。我习惯加一个 default 分支,哪怕理论上不会执行到,也能让综合工具放心。

个人经验:写组合逻辑时,我更喜欢用 assign 加三元运算符。代码简洁,而且不容易漏掉分支。但如果是复杂逻辑,比如状态机,那就用 always 块加 case,可读性更好。

4.6 本章知识体系

下面这张图总结了本章的核心内容,你可以把它当作一个快速参考。

第四章:Verilog基础与组合逻辑 模块化设计 assign语句 always块 组合逻辑 组合逻辑 阻塞赋值 (=) vs 非阻塞赋值 (<=) 组合逻辑用 = ,时序逻辑用 <= 组合逻辑电路设计 加法器 · 多路选择器 核心原则:组合逻辑无记忆,输入变输出即变 ⚠ 避坑:组合逻辑中漏掉敏感信号 → 锁存器;多驱动源 → 综合报错 Verilog基础与组合逻辑 · 知识体系图

这张图把本章的核心知识点串起来了。模块化设计是骨架,assignalways 是两种描述方式,阻塞与非阻塞赋值是语法关键,加法器和多路选择器是实战例子。你把这几个点吃透了,组合逻辑这块就算入门了。

好,这一章就到这里。下一章我们讲时序逻辑,到时候会看到非阻塞赋值的真正威力。记住今天说的铁律,写代码时多留个心眼。