4. 硬件描述语言基础(Verilog):模块、端口、赋值语句、always块、时序逻辑

好,咱们今天聊聊Verilog。说实话,很多初学者一上来就被各种语法吓住了。其实没那么复杂。你把它想象成画电路图,只不过是用文字来描述而已。

我刚开始学的时候,总觉得这玩意儿跟C语言差不多。后来被坑了几次才明白——Verilog是描述硬件,不是写软件。这个观念转不过来,后面会很难受。

4.1 模块与端口——芯片的“黑盒子”

Verilog里最基本的单元叫模块(module)。你可以把它想象成一个芯片的封装。外面的人只看到引脚,里面怎么实现的,那是你自己的事。

核心概念:模块 = 硬件功能的封装。一个模块可以是一个与门,也可以是一个完整的CPU。

看个最简单的例子——一个D触发器:

module d_flip_flop (
    input  wire clk,    // 时钟
    input  wire rst_n,  // 复位,低有效
    input  wire d,      // 数据输入
    output reg  q       // 数据输出
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            q <= 1'b0;
        else
            q <= d;
    end
endmodule

端口有三种方向:input、output、inout。我个人习惯把时钟和复位放在端口列表的最前面,这样一眼就能看到时序控制信号。

小技巧:端口类型声明时,wire是默认的。如果你不写,Verilog默认就是wire。但reg必须显式声明。我建议你养成好习惯,每个端口都写清楚类型。

4.2 赋值语句——两种完全不同的“等号”

这里有个大坑。Verilog里两种赋值方式,阻塞赋值(=)和非阻塞赋值(<=)。它们的行为完全不同。

类型 符号 执行顺序 典型用途
阻塞赋值 = 立即执行,顺序执行 组合逻辑(assign、always @(*))
非阻塞赋值 <= 并行执行,块结束后统一更新 时序逻辑(always @(posedge clk))

我曾经在项目里犯过一个低级错误——在时序逻辑里用了阻塞赋值。结果仿真没问题,上板子就跑飞了。查了两天才发现是赋值方式搞错了。嗯,从那以后我写always块时,第一件事就是确认用的是不是<=。

警告:在同一个always块里,不要混用阻塞和非阻塞赋值。这是铁律。你想想看,一个信号既要立即更新又要延迟更新,硬件怎么可能同时做到?

4.3 always块——硬件的“心脏”

always块是Verilog里最核心的语法。它描述的是什么时候做什么事。

常见的写法有三种:

  • 组合逻辑:always @(*) —— 输入一变,输出马上变
  • 时序逻辑:always @(posedge clk) —— 只在时钟上升沿变化
  • 异步复位:always @(posedge clk or negedge rst_n) —— 时钟或复位变化都触发

我个人的经验是:一个always块只做一件事。别把组合逻辑和时序逻辑混在一起写。虽然语法上允许,但后期维护起来你会想骂人的。

// 好的写法:分开写
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        count <= 4'b0;
    else if (en)
        count <= count + 1'b1;
end

// 不好的写法:混在一起
always @(posedge clk) begin
    if (rst_n == 1'b0)
        count <= 4'b0;
    else if (en)
        count <= count + 1'b1;
    // 这里还插了一段组合逻辑...
    data_out = count;  // 阻塞赋值混进来了!
end

记住:always块里的敏感列表决定了这个块什么时候被“唤醒”。敏感列表写错了,综合出来的电路可能跟你想象的天差地别。

4.4 时序逻辑——让电路“记住”状态

时序逻辑的核心是存储元件——触发器和锁存器。在FPGA里,我们几乎只用D触发器。

为什么?因为D触发器结构简单,时序清晰,不容易出问题。锁存器(latch)在FPGA里是“不受欢迎”的,因为它会导致时序分析困难。

我画了一张图,帮你理解时序逻辑的基本结构:

组合逻辑 (assign / always @(*)) D触发器 (always @(posedge clk)) 存储当前状态 输入 下一状态 输出 反馈(当前状态) 时钟 clk

这张图展示了一个典型的时序逻辑结构。组合逻辑计算“下一状态”,D触发器在时钟沿到来时把“下一状态”锁存为“当前状态”。反馈路径让电路有了“记忆”——这就是状态机的本质。

4.5 实战:写一个4位计数器

说了这么多,咱们来写个实际的东西。一个4位计数器,带使能和异步复位。

module counter_4bit (
    input  wire       clk,      // 时钟
    input  wire       rst_n,    // 异步复位,低有效
    input  wire       en,       // 计数使能
    output reg  [3:0] count     // 计数值
);

    // 时序逻辑:在时钟上升沿或复位下降沿触发
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            count <= 4'b0000;      // 复位到0
        end else if (en) begin
            count <= count + 1'b1; // 使能有效时加1
        end
        // 如果en=0,count保持不变
    end

endmodule

这段代码里,我用了非阻塞赋值(<=)。为什么?因为这是时序逻辑。所有赋值在同一个时钟沿“同时”发生,而不是顺序执行。

避坑指南:我曾经在写计数器时,把count <= count + 1'b1写成了count = count + 1'b1。仿真看起来没问题,但综合后电路多了一堆没用的锁存器。记住:时序逻辑用非阻塞,组合逻辑用阻塞。这是铁律。

4.6 总结一下

Verilog其实不复杂。你只要记住三件事:

  1. 模块是基本单位——一个文件一个模块,端口要写清楚方向
  2. 赋值方式要分清——时序逻辑用<=,组合逻辑用=,别混用
  3. always块要专一——一个块只做一种逻辑,别把组合和时序搅在一起

嗯,这些就是我这些年做FPGA设计总结出来的经验。刚开始可能会觉得别扭,但写多了你就会发现——硬件描述语言,描述的是硬件,不是软件。这个观念一旦建立起来,后面的路就好走了。

最后送你一句话:写Verilog的时候,脑子里要想着电路。每个always块对应一块硬件,每个赋值对应一条连线。想清楚了再下笔,比写完了再改要高效得多。

可可精品资料库