2. Verilog HDL基础回顾:模块结构、数据类型(wire/reg)、运算符、always块与assign语句

好,咱们正式开始动手之前,得先把地基打牢。Verilog 这门语言,说白了就是用来描述数字电路的。你写出来的每一行代码,最终都会映射成实际的逻辑门和连线。我见过不少新手,上来就写代码,结果综合出来的电路跟自己想的完全不一样——嗯,这就是基础没打牢的后果。

2.1 模块结构:一切设计的起点

Verilog 的基本单元叫 模块(module)。你可以把它想象成一个黑盒子,有输入、有输出,里面装着具体的逻辑。我个人习惯,写任何模块之前,先画个端口图,再动手写代码,这样思路清晰很多。

一个完整的模块结构长这样:

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

    // 内部逻辑写在这里

endmodule

注意看,moduleendmodule 是成对出现的。端口列表里,我习惯把输入写在前面,输出写在后面,这样别人看你的代码一目了然。我在项目中遇到过,有人把几十个端口乱序排列,结果自己回头改代码时都找半天——千万别学这个。

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

这是新手最容易搞混的地方。我刚开始学的时候也迷糊过一阵子。

wire 代表连线,它本身不存储值。说白了,它只是把某个信号的值传递到另一个地方。比如:

wire a, b, c;
assign c = a & b;   // c 是 a 和 b 相与的结果

reg 代表寄存器,它能存储值。注意,这里的“寄存器”是逻辑上的概念,不一定真的映射成硬件寄存器。比如在 always 块里赋值的变量,必须声明为 reg:

reg [7:0] data_reg;
always @(posedge clk) begin
    data_reg <= 8'hFF;
end

我给大家一个简单的判断方法:在 assign 语句左边出现的,用 wire;在 always 块里被赋值的,用 reg。记住这个,90% 的情况不会错。

⚠ 避坑指南: 我曾经在项目中看到有人把 always 块里的变量声明成 wire,结果仿真报错,查了半天才发现是类型搞错了。综合工具虽然有时候能自动推断,但咱们写代码还是严谨点好。

2.3 运算符:从算术到位运算

Verilog 的运算符跟 C 语言很像,但有些细节不一样。我整理了一个常用运算符表:

类别 运算符 说明
算术 + - * / % 加减乘除取模
位运算 & | ~ ^ 与、或、非、异或
逻辑 && || ! 逻辑与、或、非
关系 > < >= <= == != 比较运算
移位 << >> 左移、右移
拼接 { } 将多个信号拼接成一个

这里有个容易踩的坑:位运算和逻辑运算的区别。比如 a & b 是对每一位做与操作,结果还是多位的;而 a && b 是把 a 和 b 当成整体做逻辑判断,结果只有 0 或 1。你想想看,如果 a=4'b1010, b=4'b0101,那 a & b 的结果是 4'b0000,而 a && b 的结果是 1(因为两者都不为 0)。

2.4 assign 语句:组合逻辑的利器

assign 用来描述组合逻辑。它的特点就是:只要右边信号变化,左边立刻更新。这跟硬件里的连线一模一样。

wire [3:0] sum;
assign sum = a + b;   // 组合加法器

我个人习惯,简单的组合逻辑用 assign,复杂的用 always 块。比如一个多路选择器:

wire [7:0] mux_out;
assign mux_out = (sel == 2'b00) ? data0 :
                 (sel == 2'b01) ? data1 :
                 (sel == 2'b10) ? data2 : data3;

这样写,是不是比一堆 if-else 清晰多了?

2.5 always 块:时序逻辑的核心

always 块是 Verilog 里最灵活、也最容易出问题的地方。它的基本格式是:

always @(敏感列表) begin
    // 逻辑代码
end

敏感列表决定了 always 块什么时候执行。常用的写法有:

  • 组合逻辑always @(*)always @(a or b or c)
  • 时序逻辑always @(posedge clk or negedge rst_n)

举个例子,一个带异步复位的 D 触发器:

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 1'b0;
    else
        q <= d;
end
💡 关键点: 时序逻辑里用 <=(非阻塞赋值),组合逻辑里用 =(阻塞赋值)。这个区别很重要,搞反了仿真结果会完全不一样。我曾经调试一个计数器,死活数不对,最后发现就是在 always 块里用了阻塞赋值——嗯,从那以后我再也不敢混用了。

2.6 知识体系结构图

为了让你更直观地理解本章的知识脉络,我画了一张图:

Verilog HDL 基础 模块结构 数据类型 运算符 module / endmodule 端口声明 (input/output) wire(连线) reg(寄存器) 算术 / 位 / 逻辑 关系 / 移位 / 拼接 核心语句 assign(组合逻辑) always(时序/组合) Verilog 基础 = 模块 + 数据类型 + 运算符 + 语句
📌 我的建议: 学 Verilog 最好的方法就是多写、多仿真。别怕犯错,我当年写第一个计数器时,仿真波形完全不对,后来发现是敏感列表写漏了一个信号。这些坑,踩过一次就记住了。

好了,这一章的内容就到这里。记住:模块是骨架,wire/reg 是血肉,运算符是工具,assign 和 always 是灵魂。把这几个东西搞明白了,后面的解析器开发才能顺利进行。


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