4. 硬件描述语言入门(Verilog):模块与端口、assign 与 always 块、阻塞赋值与非阻塞赋值

好,咱们今天来聊聊 Verilog 最核心的几个概念。说实话,我刚学 Verilog 那会儿,觉得它跟 C 语言长得挺像,心想这有什么难的?结果一上手写 RTL 代码,就被综合器狠狠教育了一顿。嗯,硬件描述语言跟软件语言,骨子里就不是一回事。

这一节,我带你把地基打牢。我们只聊四件事:模块与端口assign 语句always 块,以及那个让无数新手翻车的阻塞赋值与非阻塞赋值

4.1 模块与端口:芯片的“黑盒子”

Verilog 里最基本的单元叫 module。你可以把它想象成一个黑盒子——外面的人只看到它的引脚(端口),里面具体怎么实现的,外部不关心。

一个模块的定义长这样:

module counter (
    input  wire       clk,      // 时钟
    input  wire       rst_n,    // 复位,低有效
    output reg  [3:0] count     // 4位计数器输出
);

    // 内部逻辑写在这里

endmodule

端口有三种方向:inputoutputinout。我个人习惯把时钟和复位放在端口列表的最前面,这样一眼就能看到时序逻辑的“心脏”。

小技巧:端口声明时,wirereg 的类型可以写在端口列表里,也可以分开写。我建议初学者统一写在端口声明里,代码更紧凑,不容易漏掉。

4.2 assign 语句:组合逻辑的“连线”

assign 用来描述组合逻辑。说白了,就是一根导线——左边是输出,右边是输入,信号一变,输出立刻跟着变。

assign led = (switch == 1'b1) ? clk_out : 1'b0;

上面这行代码的意思是:如果开关按下,LED 就输出时钟信号;否则输出低电平。没有时钟沿,没有延迟,纯粹的组合逻辑。

我在项目中遇到过一个问题:有人用 assign 写了很长的嵌套条件表达式,结果综合出来的电路又大又慢。我的建议是——assign 适合简单的逻辑,复杂的条件判断,交给 always 块里的 case 语句会更清晰。

4.3 always 块:时序逻辑的“心脏”

always 块是 Verilog 里最灵活、也最容易出错的构造。它既可以描述组合逻辑,也可以描述时序逻辑。区别在于敏感列表怎么写。

4.3.1 组合逻辑的 always 块

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

@(*) 表示对所有输入信号敏感。只要 selab 中任何一个发生变化,这个块就会重新执行。嗯,这里要注意:组合逻辑的 always 块里,必须把所有输入信号都列全,否则综合会生成锁存器(latch)。

避坑指南:我曾经因为漏写了一个 else 分支,导致综合出来一个 latch,仿真怎么都跑不对。排查了两天才发现——组合逻辑里,if 必须配 else,case 必须配 default。这是铁律。

4.3.2 时序逻辑的 always 块

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        count <= 4'b0000;
    else
        count <= count + 1'b1;
end

敏感列表里写 posedge clk,表示只在时钟上升沿触发。加上 negedge rst_n 是为了支持异步复位。这是最标准的写法,我建议你把它当成模板记下来。

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

这是 Verilog 里最经典的“坑”,没有之一。很多面试官就爱拿这个考人。

  • 阻塞赋值(=):顺序执行,前面的赋值会阻塞后面的赋值。用在组合逻辑的 always 块里。
  • 非阻塞赋值(<=):并行执行,所有赋值同时发生。用在时序逻辑的 always 块里。

看个例子你就明白了:

// 阻塞赋值 —— 组合逻辑
always @(*) begin
    a = b;
    c = a;  // c 得到的是 b 的新值
end

// 非阻塞赋值 —— 时序逻辑
always @(posedge clk) begin
    a <= b;
    c <= a;  // c 得到的是 a 的旧值
end

为什么会这样?因为非阻塞赋值在时钟沿来临时,先把所有右边的值“拍个快照”,然后统一更新到左边。而阻塞赋值是“来一个算一个”,立即生效。

黄金法则:时序逻辑用 <=,组合逻辑用 =。混着用?我劝你别试。我曾经在一个项目里把两者混在同一个 always 块里,仿真结果和实际芯片行为完全对不上,最后只好重写。

4.5 知识体系总览

下面这张图,帮你把这一节的核心逻辑串起来:

Verilog 硬件描述语言核心知识体系 module 模块 端口 (input/output/inout) assign 语句 always 块 组合逻辑 (阻塞赋值 =) 时序逻辑 (非阻塞赋值 <=) 组合逻辑:输出只取决于当前输入,无记忆 时序逻辑:输出取决于当前输入和过去状态,有记忆

4.6 实战建议

学到这里,你可能觉得知识点有点散。没关系,我帮你总结几条实战中一定会用到的原则:

  1. 模块化设计:每个模块只做一件事,端口尽量少。一个模块超过 20 个端口,就该考虑拆分了。
  2. 区分清楚“线”和“寄存器”wire 对应组合逻辑的输出,reg 对应时序逻辑的输出。但注意,reg 在 always 块里也可以描述组合逻辑。
  3. 敏感列表要完整:组合逻辑用 @(*),时序逻辑明确写 posedge clknegedge rst_n
  4. 赋值方式别混用:同一个 always 块里,要么全用 =,要么全用 <=。混着写,综合结果会让你怀疑人生。
我的个人习惯:写代码前,先在纸上画出模块的端口和内部的主要寄存器。想清楚了再动手,比边写边想要高效得多。毕竟,硬件设计不是写作文,改起来成本很高。

好了,这一节的内容就到这里。记住:模块是骨架,端口是接口,assign 和 always 是血肉,阻塞与非阻塞赋值是灵魂。把这几个概念吃透了,后面的路就好走了。


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