3. Verilog基础语法(上):模块结构、数据类型、assign语句、always块、阻塞与非阻塞赋值

好,咱们正式开始写代码了。

这一章是Verilog的根基。说白了,你后面所有的图像加速模块——从简单的像素操作到复杂的卷积加速器——都得靠这些基础语法搭起来。我见过不少新手一上来就急着写复杂逻辑,结果综合出来一堆莫名其妙的错误。嗯,咱们还是先把地基打牢。

3.1 模块结构——Verilog的“黑盒子”

Verilog里最基本的单元叫模块(module)。你可以把它想象成一个黑盒子:有输入、有输出,里面装着你要实现的逻辑。

一个完整的模块长这样:

module led_controller (
    input  wire       clk,      // 时钟信号
    input  wire       rst_n,    // 复位信号(低有效)
    input  wire [7:0] brightness, // 亮度值
    output reg  [3:0] led       // 4位LED输出
);

    // 内部逻辑写在这里

endmodule

我个人习惯把端口声明和内部逻辑分开写。端口列表里只写名字,类型和位宽在下面单独声明。这样代码结构更清晰,尤其是模块端口多的时候——我在项目中遇到过一个大模块有50多个端口,全挤在一行里,那叫一个酸爽。

小技巧:端口声明时,input默认是wire类型,output可以是wire或reg。但为了可读性,我建议都显式写出来。

3.2 数据类型——wire和reg

Verilog里最常用的数据类型就两个:wire 和 reg。别被名字骗了,它们的区别不是“线”和“寄存器”。

  • wire:用于组合逻辑。它只是物理连线的抽象,不存储值。你用assign语句驱动的信号,必须是wire类型。
  • reg:用于时序逻辑。它可以在always块里被赋值,并且能保持上一次的值。注意,reg不一定会被综合成寄存器——如果你在组合逻辑的always块里用reg,它综合出来还是连线。

举个例子:

wire       a, b;      // 两个连线
wire       sum;       // 连线
reg  [3:0] counter;   // 4位寄存器

assign sum = a ^ b;   // wire用assign驱动

always @(posedge clk) begin
    if (!rst_n)
        counter <= 4'd0;
    else
        counter <= counter + 1'b1;
end

你想想看,如果我把counter声明成wire,在always块里赋值,综合工具会直接报错。为什么?因为wire不能保持状态,它必须被持续驱动。

注意:我曾经犯过一个低级错误——把always块里赋值的信号声明成wire,结果仿真能过,综合报错。查了半天才发现是类型写错了。所以,always块里赋值的信号用reg,assign语句驱动的信号用wire,这个规则记牢。

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

assign语句用来描述组合逻辑。它的执行是连续赋值——只要右边表达式里的信号变了,左边立即更新。

说白了,它就像一根导线,输入变了,输出马上跟着变。

assign led[0] = (brightness > 8'd128) ? 1'b1 : 1'b0;
assign sum    = a + b;
assign flag   = &data;  // 按位与,检测data是否全1

我在做图像二值化加速时,就用assign直接算阈值比较:

assign pixel_out = (pixel_in > threshold) ? 8'hFF : 8'h00;

一行搞定,干净利落。assign语句非常适合这种简单的组合逻辑。

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

always块是Verilog里最灵活、也最容易出错的语法。它既可以描述组合逻辑,也可以描述时序逻辑。区别在于敏感列表怎么写。

常见的写法有三种:

类型 敏感列表 示例
时序逻辑 posedge clk 或 negedge clk always @(posedge clk)
组合逻辑 @(*) 或列出所有输入信号 always @(*)
锁存器(不推荐) 电平敏感 always @(a or b)

我个人强烈建议:时序逻辑只用时钟沿触发,组合逻辑用@(*)。别搞什么电平敏感的always块,那玩意儿综合出来大概率是锁存器,你不想在芯片里见到它的。

来看一个典型的时序逻辑例子:

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        counter <= 8'd0;
        flag    <= 1'b0;
    end else begin
        if (counter == 8'd255) begin
            counter <= 8'd0;
            flag    <= 1'b1;
        end else begin
            counter <= counter + 1'b1;
            flag    <= 1'b0;
        end
    end
end

注意这里的敏感列表:posedge clk or negedge rst_n。这是标准的异步复位写法。时钟上升沿触发,复位是低有效。

核心要点:always块里的多条语句,必须用begin...end包起来。如果只有一条语句,可以省略begin...end,但我建议永远都写上——你永远不知道什么时候会加第二条。

3.5 阻塞赋值与非阻塞赋值——FPGA工程师的“分水岭”

这个知识点,是区分新手和老手的关键。我面试FPGA工程师时,必问这个问题。

先看定义:

  • 阻塞赋值(=):顺序执行。上一条赋值完成,才执行下一条。
  • 非阻塞赋值(<=):并行执行。所有赋值同时计算,在时钟沿统一更新。

听起来简单,但实际用起来很容易翻车。

我给你们看一个经典的反例:

// 错误写法:用阻塞赋值描述移位寄存器
always @(posedge clk) begin
    a = b;
    c = a;
end

你期望的是:b的值先给a,然后a的值再给c。这样c得到的是b的旧值吗?不是!因为阻塞赋值是顺序执行的,所以c得到的是b的新值。这根本不是移位寄存器的行为。

正确的写法:

// 正确写法:用非阻塞赋值
always @(posedge clk) begin
    a <= b;
    c <= a;
end

非阻塞赋值会在时钟上升沿同时采样b和a的旧值,然后同时更新a和c。这样c得到的是a的旧值,也就是b上一拍的值。这才是真正的移位寄存器。

铁律:时序逻辑的always块里,永远用非阻塞赋值(<=)。组合逻辑的always块里,用阻塞赋值(=)。别混用,别问为什么,这是无数前辈用流片失败换来的教训。

我曾经接手过一个项目,前同事在时序逻辑里混用了阻塞和非阻塞赋值。仿真没问题,但上板后数据总是错位。我花了整整两天,一条条语句排查,才发现是赋值方式用错了。从那以后,我写代码前都会先确认:这个always块是时序还是组合?

3.6 知识体系总览

这一章的内容比较多,我画了一张图帮你理清思路:

Verilog基础语法(上)知识体系 模块结构 (module) 数据类型 赋值方式 端口声明 input / output / inout 内部逻辑实现 wire:组合逻辑连线 reg:时序逻辑存储 (注意:reg不一定是寄存器) 阻塞赋值 (=) 非阻塞赋值 (<=) 时序逻辑用 <= assign 语句 always 块 组合 vs 时序 核心原则:时序用 <=,组合用 =,assign 驱动 wire

这张图把这一章的核心知识点串起来了。你写代码时,先想清楚:这个信号是组合逻辑还是时序逻辑?用wire还是reg?用assign还是always?赋值用=还是<=?想清楚了再动笔。

好了,这一章的内容就到这里。记住我今天说的这些,尤其是阻塞和非阻塞赋值的区别——这是你从FPGA新手迈向老手的第一步。

可可精品资料库