3. Verilog基础语法(上):模块结构、端口定义、assign语句、always块、阻塞与非阻塞赋值

各位同学,今天我们来聊聊Verilog最基础的东西。说实话,我刚开始学FPGA的时候,觉得Verilog跟C语言差不多,不就是写代码嘛。结果第一次上板调试,LED死活不亮,查了半天才发现是赋值方式用错了。嗯,从那以后我再也不敢小看这些基础语法了。

这一章我们不讲复杂的东西,就讲五个核心概念:模块结构、端口定义、assign语句、always块、阻塞与非阻塞赋值。把这五个搞明白,你就能看懂大部分Verilog代码了。

核心要点:Verilog是硬件描述语言,不是编程语言。你写的每一行代码,最终都会变成实际的电路。这一点请时刻记住。

3.1 模块结构——Verilog的基本单元

一个Verilog程序,说白了就是一个模块(module)。模块就像是一个黑盒子,有输入、有输出,内部实现具体的功能。

我个人的习惯是,每写一个模块,先画个框图,把输入输出标清楚。这样写代码的时候思路特别清晰。

来看一个最简单的模块结构:

module led_controller(
    input  wire       clk,      // 时钟信号
    input  wire       rst_n,    // 复位信号,低电平有效
    output reg        led       // LED控制信号
);

    // 内部逻辑写在这里

endmodule

结构很简单:module 开头,endmodule 结尾。中间是端口定义和内部逻辑。注意,每个模块都要有一个名字,这里我叫它 led_controller

小技巧:模块名最好跟文件名保持一致。比如这个模块放在 led_controller.v 文件里。这样找代码的时候不会乱。我在项目里见过有人把几十个模块全写在一个文件里,那叫一个酸爽...

3.2 端口定义——模块的"手脚"

端口就是模块跟外界通信的通道。有三种类型:

端口类型 关键字 说明
输入端口 input 数据从外部流入模块
输出端口 output 数据从模块流出到外部
双向端口 inout 既可以输入也可以输出(初学者先别碰)

端口还需要指定数据类型。最常用的两种:

  • wire:线网类型,可以理解为物理连线。用于assign语句赋值。
  • reg:寄存器类型,可以存储数据。用于always块中赋值。

这里有个容易混淆的地方:reg 并不一定对应实际的寄存器。它只是一个变量类型。我在项目中见过有人以为用了 reg 就一定会生成寄存器,结果综合出来的电路跟他想的不一样。其实,reg 在组合逻辑中也可以使用,它只是表示一个变量,而不是硬件寄存器。

3.3 assign语句——连续赋值

assign 语句用来描述组合逻辑。它的特点是:只要输入变了,输出立刻跟着变。就像一根导线,左边输入什么,右边就输出什么。

来看个例子:

module and_gate(
    input  wire a,
    input  wire b,
    output wire y
);

    assign y = a & b;  // 与门

endmodule

这个模块实现了一个与门。当 a 和 b 都是 1 时,y 输出 1;否则输出 0。注意,assign 左边的变量必须是 wire 类型。

重要:多个assign语句是并行执行的,不是顺序执行。这一点跟C语言完全不同。你写10个assign,它们同时工作,不分先后。

我曾经犯过一个错误:写了两个assign给同一个变量赋值,结果综合报错。因为一个变量只能被一个驱动源驱动,就像一根导线不能同时接两个电源一样。

3.4 always块——时序逻辑的核心

always 块是Verilog中最强大的结构之一。它既可以描述组合逻辑,也可以描述时序逻辑。关键看敏感列表怎么写。

基本语法:

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

敏感列表告诉always块什么时候执行。常用的写法:

  • always @(a or b):当 a 或 b 变化时执行(组合逻辑)
  • always @(posedge clk):当时钟上升沿时执行(时序逻辑)
  • always @(posedge clk or negedge rst_n):时钟上升沿或复位下降沿时执行(带复位的时序逻辑)

来看一个带复位的计数器:

module counter(
    input  wire       clk,
    input  wire       rst_n,
    output reg [3:0]  cnt
);

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            cnt <= 4'd0;      // 复位清零
        else
            cnt <= cnt + 1'b1; // 每个时钟加1
    end

endmodule

这个计数器在每个时钟上升沿加1。复位时(rst_n为低电平)清零。注意,这里用的是 <= 而不是 =,这就是我们接下来要讲的重点。

警告:always块中,如果描述组合逻辑,敏感列表要包含所有输入信号。漏掉一个,综合出来的电路可能跟你想的不一样。我吃过这个亏,仿真没问题,上板就出bug。

3.5 阻塞赋值与非阻塞赋值——新手最容易踩的坑

这是Verilog中最容易混淆的概念,也是面试必考题。说白了,就两句话:

  • 阻塞赋值(=):顺序执行,前面的赋值会阻塞后面的赋值。
  • 非阻塞赋值(<=):并行执行,所有赋值同时发生。

为什么会有这种区别?因为硬件电路是并行工作的,而软件是顺序执行的。Verilog用这两种赋值方式来区分不同的硬件行为。

来看一个经典的例子:

// 阻塞赋值示例
always @(posedge clk) begin
    a = b;
    c = a;
end

// 非阻塞赋值示例
always @(posedge clk) begin
    a <= b;
    c <= a;
end

猜猜结果有什么不同?

阻塞赋值:先执行 a = b,a 变成了 b 的值。然后执行 c = a,此时 a 已经是新值了,所以 c 也等于 b 的值。最终 a 和 c 都等于 b。

非阻塞赋值:两个赋值同时发生。a <= b 把 b 的值赋给 a,c <= a 把 a 的旧值赋给 c。所以 c 等于 a 原来的值,而不是 b 的值。

赋值方式 执行顺序 适用场景
阻塞赋值 = 顺序执行 组合逻辑(always @(*) 或 assign)
非阻塞赋值 <= 并行执行 时序逻辑(always @(posedge clk))

黄金法则:描述时序逻辑用非阻塞赋值(<=),描述组合逻辑用阻塞赋值(=)。这个规则请刻在脑子里。

我曾经在项目中把这两个搞混了,写了一个移位寄存器,结果仿真波形完全不对。查了两天才发现是赋值方式用错了。从那以后,我写代码前都会先问自己一句:这是组合逻辑还是时序逻辑?

3.6 本章小结

这一章我们讲了五个基础概念:

  • 模块结构:module...endmodule,每个模块一个文件
  • 端口定义:input、output、inout,配合wire和reg使用
  • assign语句:连续赋值,用于组合逻辑
  • always块:可描述组合逻辑和时序逻辑,看敏感列表
  • 阻塞与非阻塞赋值:时序逻辑用<=,组合逻辑用=

这些是Verilog的基石。你想想看,如果连这些都没搞明白,后面写复杂模块的时候肯定会出问题。我建议你把每个例子都自己敲一遍,跑一下仿真,看看波形是什么样的。光看是学不会的,得动手。

我的建议:刚开始学的时候,可以写一些简单的模块,比如与门、或门、计数器、移位寄存器。每个模块都仿真一下,看看波形对不对。这样练上十来个模块,基础就扎实了。

Verilog基础语法知识体系 Verilog基础语法 模块结构 module...endmodule 端口定义 input/output/inout assign语句 连续赋值 always块 敏感列表控制 阻塞与非阻塞赋值 = 与 <= 的区别 核心:硬件思维,并行执行

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