2. Verilog基础语法:模块结构、数据类型、运算符、assign语句、always块、阻塞与非阻塞赋值

好,咱们正式开始啃Verilog这块硬骨头。说实话,很多初学者觉得Verilog就是写代码,跟C语言差不多。嗯,这个想法很危险。我在FPGA这行摸爬滚打十几年,见过太多把Verilog当C写的工程师,最后综合出来的电路自己都看不懂。

Verilog本质上是描述硬件的语言。你写的每一行代码,最终都会变成实实在在的门电路、触发器和连线。所以,咱们得用硬件工程师的思维去理解它。

核心思想:Verilog不是"执行"代码,而是"描述"电路。这个观念转不过来,后面会踩很多坑。

2.1 模块结构——FPGA设计的基本单元

模块(module)是Verilog最基本的构建块。一个完整的FPGA设计,就是由大大小小的模块拼装起来的。我个人习惯把模块想象成一个"黑盒子"——它有输入、有输出,内部干些什么活,外面不用管。

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

module counter (
    input  wire       clk,      // 时钟信号
    input  wire       rst_n,    // 复位信号,低电平有效
    output reg  [7:0] count     // 8位计数器输出
);

    // 内部逻辑写在这里

endmodule

模块的骨架就三部分:端口声明、内部逻辑、结束标志。端口声明里,我建议你养成好习惯——每个端口都写清楚是input还是output,是wire还是reg。我在项目中遇到过有人偷懒不写数据类型,结果综合时报了一堆warning,查了半天才发现是端口类型不匹配。

我的习惯:所有输入端口都用wire,所有输出端口如果是在always块里赋值就用reg,否则用wire。这样不容易出错。

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

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

  • wire:相当于一根导线。它只能被连续赋值(assign)驱动,不能存储值。
  • reg:相当于一个寄存器。它可以在always块里被赋值,能保持上一次的值。

但要注意一个坑:reg不一定会被综合成寄存器!如果你在组合逻辑的always块里用reg,它最终只是根导线。我曾经在面试时问过这个问题,十个有八个答错。

看个例子:

wire       a, b;      // 两根导线
wire       sum;       // 导线
reg        carry;     // 寄存器(但未必是触发器)

// 连续赋值:只能用wire
assign sum = a ^ b;

// 组合逻辑always块:reg在这里只是导线
always @(*) begin
    carry = a & b;
end

注意:reg在组合逻辑always块里不会生成触发器。只有当时钟沿触发的always块里,reg才会被综合成真正的寄存器。

2.3 运算符——跟C语言差不多,但有坑

Verilog的运算符大部分跟C语言一样,但有几个需要特别注意:

运算符类型 符号 说明
算术运算符 + - * / % 乘除法综合出来很耗资源,慎用
位运算符 & | ~ ^ 按位操作,常用
逻辑运算符 && || ! 返回1位布尔值
关系运算符 > < >= <= 注意<=也是非阻塞赋值
拼接运算符 { } 非常实用,比如{a, b}
条件运算符 ? : 类似C的三目运算符

这里我要特别提一下除法。你想想看,除法在FPGA里有多贵?一个32位除法器综合出来可能要几百个LUT。我在做交易所行情解析时,遇到需要除法的场景,都尽量用移位或者查表代替。实在避不开,就用IP核。

2.4 assign语句——连续赋值

assign语句用来描述组合逻辑。它的意思是"左边的信号一直等于右边的表达式"。说白了,就是一根导线,只不过这根导线可以带点逻辑。

// 简单的组合逻辑
assign sum = a + b;
assign full = (count == 8'hFF) ? 1'b1 : 1'b0;

// 多位的拼接
assign {carry, result} = {a, b} + {c, d};

assign的左边必须是wire类型。我见过有人试图在assign左边放reg,编译直接报错。嗯,这个错误我也犯过,刚学的时候。

实用技巧:简单的组合逻辑用assign,复杂的用always块。assign写多了代码会变得很散,不好维护。

2.5 always块——时序逻辑和组合逻辑的容器

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

组合逻辑always块:

always @(*) begin
    // 敏感列表用*,表示所有输入信号变化都触发
    case (sel)
        2'b00: out = a;
        2'b01: out = b;
        2'b10: out = c;
        2'b11: out = d;
    endcase
end

时序逻辑always块:

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

这里有个关键点:组合逻辑的敏感列表用@(*),时序逻辑用时钟沿和复位沿。我曾经在项目中看到有人把组合逻辑的敏感列表写成@(a or b or c),结果漏了一个信号,仿真时死活不对。从那以后,我组合逻辑一律用@(*),省心。

2.6 阻塞与非阻塞赋值——最容易翻车的地方

好,重点来了。阻塞赋值(=)和非阻塞赋值(<=)的区别,是Verilog初学者最容易搞混的。我当年也在这上面栽过跟头。

阻塞赋值(=): 顺序执行,前面的赋值会立即影响后面的语句。用在组合逻辑always块里。

非阻塞赋值(<=): 并行执行,所有赋值在always块结束时同时更新。用时序逻辑always块里。

看个对比:

// 阻塞赋值——组合逻辑
always @(*) begin
    temp = a & b;
    out  = temp | c;   // 这里用的是更新后的temp
end

// 非阻塞赋值——时序逻辑
always @(posedge clk) begin
    temp <= a & b;
    out  <= temp | c;  // 这里用的是更新前的temp!
end

为什么会这样?因为非阻塞赋值在时钟沿来临时,先计算所有右边的值,然后统一赋给左边。所以第二个例子中,out用的是temp的旧值。

黄金法则:

  • 组合逻辑always块用阻塞赋值(=)
  • 时序逻辑always块用非阻塞赋值(<=)
  • 绝对不要混用!

我曾经在调试一个交易所的UDP协议解析模块时,发现数据总是错位。查了两天,最后发现是一个always块里同时用了阻塞和非阻塞赋值。综合出来的电路跟仿真完全不一样。嗯,从那以后我写代码都会多看一眼赋值符号。

知识体系总览

下面这张图是我自己整理的Verilog基础语法知识结构,你可以把它当作学习路线图:

Verilog基础语法知识体系 Verilog基础语法 模块结构 module/endmodule 端口声明 内部逻辑 数据类型 wire(导线) reg(寄存器) 参数parameter 运算符 算术/位/逻辑 关系/拼接/条件 assign语句 连续赋值 组合逻辑描述 左边必须是wire always块 组合逻辑@(*) 时序逻辑@(posedge) 阻塞赋值(=) vs 非阻塞赋值(<=) 组合逻辑用=,时序逻辑用<=,绝对不要混用 核心原则:Verilog描述硬件,不是执行软件 实践建议:多写多仿真,用波形验证你的理解

这张图把五个核心知识点串起来了。你学的时候可以对照着看,哪个环节薄弱就重点补哪个。

我的学习建议:别急着写复杂代码。先把模块结构、数据类型、阻塞非阻塞这三个基础打牢。这三个搞明白了,后面学状态机、FIFO什么的就顺了。

好了,这一章的内容就到这儿。记住,Verilog不是写代码,是画电路。你每写一行,都要想想它综合出来是什么样子。养成这个习惯,你离FPGA高手就不远了。


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