3. Verilog基础语法(上):模块结构、数据类型、运算符、always块与assign语句

各位同学,欢迎来到实战课的第一章。说实话,每次讲Verilog基础语法,我都想起自己刚入行时踩过的坑。那时候总觉得语法简单,结果写出来的代码要么综合不过,要么仿真对不上。嗯,今天咱们就把这些基础打扎实了。

3.1 模块结构——Verilog的骨架

Verilog里最基本的单元就是模块(module)。你可以把它想象成一个黑盒子:有输入、有输出,里面装着逻辑。我习惯把模块比作一个芯片的“功能块”,每个模块完成一件事。

一个标准的模块长这样:

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

    // 内部逻辑写在这里

endmodule

注意看,moduleendmodule是成对出现的。端口列表里,我习惯把输入写在前面,输出写在后面。这不是强制要求,但团队协作时,统一的风格能省不少事。

我的习惯:每个模块只做一件事。比如计数器模块就只计数,不要在里面混着串口收发逻辑。这样调试起来,定位问题快得多。

3.2 数据类型——wire和reg的区别

这是新手最容易搞混的地方。说白了,wire就是一根导线,reg就是一个寄存器。但注意,这里的“寄存器”不是硬件里的寄存器,而是Verilog里的一个变量类型。

类型 特点 典型用法
wire 组合逻辑,连续赋值 assign语句、模块端口连接
reg 时序逻辑,过程赋值 always块内赋值

我曾经犯过一个低级错误:在always块里给wire赋值,结果综合报错。后来才明白,wire只能用assign驱动,reg才能在always里用。

注意:在Verilog 2001之后,端口声明可以直接写input wireoutput reg。我建议你养成这个习惯,代码更清晰。

3.3 运算符——别小看这些符号

Verilog的运算符和C语言很像,但有几个坑要注意。我列一下常用的:

  • 算术运算符:+、-、*、/。注意除法在FPGA里很耗资源,能用移位就别用除法。
  • 位运算符:&、|、^、~。这些是逐位操作的。
  • 逻辑运算符:&&、||、!。结果是布尔值(0或1)。
  • 关系运算符:>、<、>=、<=。注意<=也是非阻塞赋值,别搞混了。
  • 移位运算符:<<、>>。左移一位相当于乘2,右移一位相当于除2。

举个例子,你想想看:

wire [3:0] a = 4'b1010;
wire [3:0] b = 4'b1100;

// 位运算
wire [3:0] c = a & b;  // 结果是 4'b1000

// 逻辑运算
wire       d = a && b; // 结果是 1(因为a和b都不为0)

为什么会这样?因为位运算是逐位比较,逻辑运算是把整个数当成布尔值。我在项目中见过有人把&&&混用,仿真没问题,综合出来逻辑全错了。

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

always块是Verilog里最强大的结构之一。它有两种主要用法:

组合逻辑:

always @(*) begin
    // 敏感列表用 *,表示所有输入信号变化都触发
    if (sel) 
        out = a;
    else 
        out = b;
end

时序逻辑:

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

注意看,时序逻辑里我用的是<=(非阻塞赋值),组合逻辑里用的是=(阻塞赋值)。这个区别很重要。我曾经在项目里把两者混用,结果仿真波形怎么看都不对,折腾了两天才发现是赋值方式的问题。

核心原则:时序逻辑用非阻塞赋值(<=),组合逻辑用阻塞赋值(=)。别问为什么,这是硬件描述语言的规矩,违反了你就会看到各种奇怪的现象。

3.5 assign语句——连续赋值的艺术

assign语句用来描述组合逻辑。它和always块的区别在于:assign是连续赋值,只要右边变了,左边立刻更新;always块是过程赋值,需要敏感列表触发。

举个例子:

// 用assign实现一个2选1多路选择器
wire [3:0] mux_out;
assign mux_out = sel ? a : b;

这比用always块写简洁多了。我个人的习惯是:简单的组合逻辑用assign,复杂的用always @(*)。这样代码可读性更好。

避坑指南:我曾经在assign语句里写了很长的条件表达式,结果综合出来的逻辑特别大。后来拆成多个assign,不仅代码好读,综合面积也小了。所以,别在一个assign里塞太多逻辑。

3.6 知识体系总览

下面这张图是我自己总结的Verilog基础语法结构,你可以把它当作学习地图:

Verilog基础语法 模块结构 (module) 端口声明 (input/output) 内部信号定义 实例化子模块 数据类型 wire (组合逻辑) reg (时序逻辑) parameter (参数) 运算符 算术/位/逻辑运算符 关系/移位运算符 拼接/条件运算符 always块 时序逻辑 (posedge) 组合逻辑 (@*) assign语句 连续赋值 组合逻辑描述

这张图把本章的核心内容串起来了。模块是骨架,数据类型是血肉,运算符是工具,always和assign是表达逻辑的方式。你想想看,写Verilog其实就是用这些元素搭积木。

好了,这一章的内容就到这里。记住,基础语法虽然简单,但它是你后续写复杂逻辑的根基。我建议你每学一个知识点,都打开仿真工具跑一遍,亲眼看到波形变化,印象才深刻。