2. 硬件描述语言基础(Verilog):模块结构、数据类型、连续赋值与过程块

好,咱们正式开始啃 Verilog 这块硬骨头。很多同学觉得 Verilog 就是写代码,跟 C 语言差不多。嗯,这个想法很危险。我当年刚入行时也这么想,结果写出来的东西综合出来一堆莫名其妙的锁存器,被老工程师骂得狗血淋头。

Verilog 不是软件语言。它描述的是硬件——连线、触发器、组合逻辑。你写的每一行代码,最终都会变成芯片上实实在在的电路。想清楚这个,再往下学。

2.1 模块结构:Verilog 的基本单元

Verilog 里最基本的单位叫 模块(module)。一个模块就是一个硬件功能块,小到一个与门,大到整个 CPU,都是用模块搭起来的。

模块长什么样?看这个例子:

module and_gate (
    input  wire a,      // 输入端口 a
    input  wire b,      // 输入端口 b
    output wire y       // 输出端口 y
);

    assign y = a & b;   // 连续赋值:y 等于 a 与 b

endmodule

结构很简单:module 关键字 + 模块名 + 端口列表 + 内部逻辑 + endmodule。端口要声明方向——input、output、inout。这个别搞反了,我见过有人把输出写成输入,仿真没问题,一上板子直接短路。

模块结构要点:

  • 每个 .v 文件最好只放一个模块,模块名和文件名保持一致
  • 端口列表写在括号里,每个端口单独声明方向
  • 内部逻辑写在 module 和 endmodule 之间

2.2 数据类型:wire 与 reg 的区别

Verilog 里最常用的两种数据类型是 wire 和 reg。很多新手在这上面栽跟头,包括我。

wire 代表连线。它本身不存储值,只是把两个点连起来。用 assign 赋值的就是 wire。

reg 代表寄存器。它能在时钟沿到来时保持值不变。在 always 块里赋值的变量必须是 reg 类型。

但注意——reg 不一定是真正的寄存器。在组合逻辑的 always 块里,reg 综合出来只是连线。这个坑我踩过,写了个组合逻辑用了 reg,以为会生成触发器,结果综合报告里一个 flip-flop 都没有。

特性 wire reg
默认值 高阻态 z 未知态 x
赋值方式 连续赋值 assign 过程赋值 always
综合结果 连线 连线 或 触发器
能否在 always 里赋值 不能 能

我的习惯:组合逻辑用 wire + assign,时序逻辑用 reg + always。这样一眼就能看出代码意图,别人 review 也轻松。

2.3 连续赋值:assign 语句

assign 是 Verilog 里最直观的语句。它描述的是组合逻辑——输入一变,输出立刻跟着变。

assign sum = a + b;
assign full = (a & b) | (cin & (a ^ b));

连续赋值的特点:

  • 等号左边必须是 wire 类型
  • 等号右边可以是表达式、运算符、函数调用
  • 赋值是并行的,没有先后顺序

为什么会强调并行?因为硬件里所有连线都是同时工作的。你写 assign y = a & b,a 和 b 一变,y 立刻更新,不需要等什么时钟。这和软件里一行一行执行完全不同。

我曾经犯过的错:在同一个模块里对同一个 wire 用了两个 assign。综合工具直接报错,说有多驱动源。记住——一根线只能被一个 assign 驱动,否则综合工具不知道听谁的。

2.4 过程块:always 语句

always 块是 Verilog 里最灵活、也最容易出问题的地方。它既能描述组合逻辑,也能描述时序逻辑。区别在于敏感列表怎么写。

2.4.1 组合逻辑的 always 块

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

@(*) 表示对所有输入信号敏感。只要 a、b、sel 任何一个变化,这个块就会重新执行。注意这里用的是阻塞赋值 =。

2.4.2 时序逻辑的 always 块

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 1'b0;
    else
        q <= d;
end

这里用了非阻塞赋值 <=。为什么?因为时序逻辑里所有赋值应该同时发生。非阻塞赋值能保证在同一个时钟沿,所有右边的值先被读取,然后再统一更新到左边。这样就不会出现赋值顺序导致的竞争。

黄金法则:

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

2.5 知识体系总览

下面这张图把本章的核心知识点串起来了。你对照着看,心里就有谱了。

Verilog 基础知识体系 Verilog 基础 模块结构 数据类型 连续赋值 过程块 端口声明 内部逻辑 模块实例化 wire 连线 reg 寄存器 向量与标量 assign 语句 表达式运算 并行赋值 always 块 敏感列表 阻塞/非阻塞 核心原则 组合逻辑用 assign 或 always @(*),时序逻辑用 always @(posedge clk) wire 用于连续赋值,reg 用于过程赋值 阻塞赋值 = 用于组合逻辑,非阻塞赋值 <= 用于时序逻辑

2.6 避坑指南

最后分享几个我这些年踩过的坑,你记下来,能省不少 debug 时间。

  • 敏感列表不完整:组合逻辑的 always 块里,如果敏感列表漏了某个输入,综合出来的电路和仿真结果不一致。用 @(*) 最保险。
  • 锁存器意外生成:在组合逻辑的 always 块里,如果某个分支没给所有输出赋值,综合工具会推断出锁存器。我当年写了一个 case 语句忘了 default,结果多出来 8 个 latch,被 mentor 骂了一周。
  • 赋值混用:同一个 always 块里混用阻塞和非阻塞赋值,仿真结果会变得不可预测。记住——要么全用 =,要么全用 <=。
  • 端口方向搞反:顶层模块的端口方向一定要和测试文件里对应好。我见过有人把时钟写成 output,仿真波形全是高阻态,查了两天才发现。

嗯,这一章的内容就到这。Verilog 基础打牢了,后面写仿真脚本、做联调才能顺手。别急,慢慢来。