第二章 Verilog HDL核心语法(上):模块结构、数据类型、操作符、assign语句、always块

各位同学,咱们今天聊聊Verilog最基础的东西。说实话,很多新手一上来就急着写代码,结果连模块长什么样都没搞明白。我当年也犯过这毛病,被老工程师骂过好几次才改过来。咱们先把地基打牢,后面才能盖高楼。

2.1 模块结构——芯片设计的“黑盒子”

Verilog里最基本的设计单元就是模块(module)。你可以把它想象成一个黑盒子——外面只看到管脚(端口),里面藏着逻辑。我习惯把模块比作一个芯片的“功能块”,比如一个加法器、一个计数器,甚至一个完整的CPU。

一个标准的模块长这样:

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

    // 内部逻辑写在这里

endmodule

注意看,端口声明有input、output,还有inout(双向口)。我个人建议:能用input就别用inout,双向口处理起来麻烦得很,容易出时序问题。我在项目中见过有人为了省几个管脚用inout,结果调试时把自己坑惨了。

小技巧:端口类型声明时,wire和reg要分清楚。input只能是wire,output可以是wire或reg。我习惯把组合逻辑输出用wire,时序逻辑输出用reg,这样一眼就能看出逻辑类型。

2.2 数据类型——wire和reg的“爱恨情仇”

Verilog里最常用的数据类型就两个:wire和reg。很多初学者搞不清它们的区别,其实说白了:

  • wire:连线,用于组合逻辑。它不存储值,只是把信号连起来。
  • reg:寄存器,用于时序逻辑。它能记住上一次的值。

但注意!reg不一定真的综合成寄存器。如果你在always块里写组合逻辑,reg只是语法上的要求,综合出来还是组合电路。嗯,这里要特别提醒一下。

举个例子:

wire a, b, c;
assign c = a & b;   // 组合逻辑,用wire

reg [7:0] data_reg;
always @(posedge clk) begin
    data_reg <= data_in;  // 时序逻辑,用reg
end

我曾经遇到过一个同事,把所有信号都定义成reg,结果综合报告里一堆没必要的寄存器,面积大了30%。所以记住:该用wire就用wire,别偷懒

2.3 操作符——Verilog的“数学工具箱”

Verilog的操作符跟C语言很像,但有些地方不一样。我整理了一张表,方便你查阅:

类别 操作符 说明
算术 + - * / % 加减乘除取模。注意除法综合出来面积很大
位运算 & | ~ ^ 与、或、非、异或。组合逻辑常用
逻辑 && || ! 逻辑与、或、非。用于条件判断
关系 > < >= <= == != 比较大小、相等判断
移位 << >> 左移、右移。乘2除2的利器
拼接 { } 把多个信号拼在一起,非常实用

这里我要特别说一下拼接操作符{ }。我在做数据通路设计时,经常用它来组合控制信号。比如:

wire [7:0] high_byte, low_byte;
wire [15:0] word = {high_byte, low_byte};  // 拼成一个16位字

你想想看,如果没有拼接操作符,你得写多少assign语句?

避坑指南:我曾经在项目中用除法操作符“/”,结果综合出来的除法器占了好大一片面积,时序还跑不过。后来改成移位和查表,面积直接降了70%。所以记住:除法、取模、乘法(大位宽)尽量少用,能用移位就用移位。

2.4 assign语句——组合逻辑的“直通车”

assign语句用来描述组合逻辑。它的语法很简单:

assign 信号名 = 表达式;

比如:

wire [3:0] sum;
assign sum = a + b;

assign的左边必须是wire类型,右边可以是任何表达式。我习惯把简单的组合逻辑用assign写,比如地址译码、数据选择等。但复杂的组合逻辑,我建议用always块,可读性更好。

这里有个坑:多个assign不能对同一个信号赋值。如果你写了:

assign data = a;
assign data = b;  // 错误!多驱动

综合工具会报错,因为不知道到底该用哪个值。我在项目中见过有人不小心写了两个assign,结果仿真没问题,综合出来X态满天飞。

2.5 always块——时序逻辑的“心脏”

always块是Verilog里最强大的结构。它既可以描述时序逻辑,也可以描述组合逻辑。基本语法:

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

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

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

我个人强烈建议:时序逻辑用非阻塞赋值(<=),组合逻辑用阻塞赋值(=)。这是无数前辈用血泪换来的经验。为什么?

非阻塞赋值在时钟沿同时更新,模拟了寄存器的行为。而阻塞赋值是顺序执行的,用在时序逻辑里会出大问题。我曾经调试过一个bug,就是因为有人在always块里混用了阻塞和非阻塞赋值,结果仿真和综合结果完全不一样。

看个正确的例子:

// 时序逻辑:计数器
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        count <= 4'b0;
    else if (en)
        count <= count + 1'b1;
end

// 组合逻辑:译码器
always @(*) begin
    case (sel)
        2'b00: data_out = a;
        2'b01: data_out = b;
        2'b10: data_out = c;
        default: data_out = 4'b0;
    endcase
end

注意看,时序逻辑里用<=,组合逻辑里用=。这个习惯一定要养成。

核心要点:
  • 模块是Verilog的基本单元,端口要分清楚input/output
  • wire用于组合逻辑,reg用于时序逻辑(但reg也可以描述组合逻辑)
  • 操作符里注意除法、乘法要慎用
  • assign描述简单的组合逻辑,always块描述复杂的组合或时序逻辑
  • 时序逻辑用非阻塞赋值,组合逻辑用阻塞赋值

好了,这一章的内容就这些。语法这东西,光看没用,得多写。我建议你打开编辑器,把上面的例子都敲一遍,然后改改参数看看效果。遇到问题别怕,调试本身就是学习的过程。

Verilog HDL核心语法(上)知识体系 Verilog HDL核心语法 模块结构 module/endmodule 端口:input/output/inout 数据类型 wire:组合逻辑连线 reg:时序逻辑存储 操作符 算术/位/逻辑/关系 移位/拼接/条件 assign语句 组合逻辑直通车 左边wire,右边表达式 always块 时序逻辑:<=非阻塞 组合逻辑:=阻塞 核心原则 模块化设计 | 类型分明 | 操作符慎用 | 赋值规范

专注资料整理