Verilog 硬件描述语言基础:模块化设计、always块与assign语句、阻塞与非阻塞赋值、仿真testbench编写

各位同学,欢迎来到第三章。说实话,Verilog 这门语言,你把它当成软件来写,那基本就离流片失败不远了。我见过太多新手把 Verilog 当 C 语言写,结果综合出来的电路完全不是那么回事。今天我们就从 RISC-V 流水线设计的角度,把 Verilog 的核心概念彻底理清楚。

3.1 模块化设计:把大问题拆成小积木

模块(module)是 Verilog 的基本单元。一个 RISC-V 处理器,说白了就是几十个模块拼在一起。我个人习惯,每个模块只干一件事,比如 ALU 只做运算,寄存器堆只管读写。

模块化设计三原则:
  • 高内聚:一个模块的功能要单一、完整
  • 低耦合:模块之间只通过端口传递信号
  • 接口清晰:输入输出信号命名规范,一看就懂

来看一个最简单的模块例子——这是我们后面流水线中 ALU 的雏形:

module alu (
    input  [31:0] a,        // 操作数A
    input  [31:0] b,        // 操作数B
    input  [3:0]  alu_op,   // 运算控制码
    output reg [31:0] result // 运算结果
);

    always @(*) begin
        case (alu_op)
            4'b0000: result = a + b;      // ADD
            4'b0001: result = a - b;      // SUB
            4'b0010: result = a & b;      // AND
            4'b0011: result = a | b;      // OR
            default: result = 32'b0;
        endcase
    end

endmodule

嗯,这里要注意:端口声明里的 output reg 表示这个输出是寄存器类型,可以在 always 块里被赋值。如果你用 output wire,那就只能用 assign 语句驱动。

3.2 always 块与 assign 语句:两种描述方式

Verilog 里描述电路有两种基本方式:always 块和 assign 语句。它们对应不同的硬件结构。

描述方式 对应硬件 典型用途 赋值对象
assign 组合逻辑(连线) 多路选择器、译码器 wire 类型
always @(*) 组合逻辑(过程赋值) 复杂组合逻辑、状态机 reg 类型
always @(posedge clk) 时序逻辑(触发器) 寄存器、计数器、流水线级 reg 类型

assign 语句——说白了就是一根导线。左边是 wire,右边是表达式。综合工具会把它变成纯组合逻辑。

// 用 assign 实现一个 2 选 1 多路器
wire [31:0] mux_out;
assign mux_out = sel ? data_a : data_b;

always 块——这个就灵活多了。它既可以描述组合逻辑,也可以描述时序逻辑。区别在于敏感列表:

  • always @(*) 或 always @(a or b or sel):组合逻辑
  • always @(posedge clk) 或 always @(negedge clk or negedge rst_n):时序逻辑
我的经验:在 RISC-V 流水线中,控制信号(如写使能、选择信号)我习惯用 assign 实现,简单直观;而数据通路中的寄存器(如 PC、寄存器堆)必须用 always @(posedge clk) 实现,确保时序正确。

3.3 阻塞赋值与非阻塞赋值:新手最容易踩的坑

这个知识点,我当年刚入行时栽过跟头。阻塞赋值用 =,非阻塞赋值用 <=。它们的行为完全不同。

黄金法则:
  • 描述组合逻辑用 阻塞赋值(=)
  • 描述时序逻辑用 非阻塞赋值(<=)
  • 绝对不要混用在同一个 always 块里

为什么会这样?我们来看一个经典的例子——两个寄存器级联:

// 错误写法:阻塞赋值描述时序逻辑
always @(posedge clk) begin
    reg_a = data_in;   // 先执行
    reg_b = reg_a;     // 后执行,reg_b 拿到的是 reg_a 的新值
end
// 结果:reg_a 和 reg_b 在同一个时钟沿同时更新为 data_in
// 这不是我们想要的流水线行为!
// 正确写法:非阻塞赋值描述时序逻辑
always @(posedge clk) begin
    reg_a <= data_in;  // 先计算右侧值,但不立即更新
    reg_b <= reg_a;    // 同样先计算右侧值(此时 reg_a 还是旧值)
end
// 结果:reg_a 更新为 data_in,reg_b 更新为 reg_a 的旧值
// 这才是真正的流水线行为!

我记得有一次做项目,一个同事在流水线寄存器里用了阻塞赋值,结果仿真波形看起来完全正确,但综合后功能就不对了。查了两天才发现是赋值方式的问题。从那以后,我写时序逻辑时都会下意识检查一遍赋值符号。

3.4 仿真 testbench 编写:验证你的设计

写 Verilog 不仿真,就像造飞机不试飞。testbench 就是你的试飞场。它没有端口,内部实例化被测试模块(DUT),然后产生激励信号。

一个标准的 testbench 结构:

`timescale 1ns / 1ps

module tb_alu();

    // 信号声明
    reg  [31:0] tb_a, tb_b;
    reg  [3:0]  tb_alu_op;
    wire [31:0] tb_result;

    // 实例化 DUT
    alu u_alu (
        .a      (tb_a),
        .b      (tb_b),
        .alu_op (tb_alu_op),
        .result (tb_result)
    );

    // 产生时钟(如果需要)
    reg clk;
    always #5 clk = ~clk;  // 周期 10ns

    // 测试激励
    initial begin
        // 初始化
        tb_a = 32'h0;
        tb_b = 32'h0;
        tb_alu_op = 4'b0;

        // 等待复位结束
        #20;

        // 测试 ADD: 5 + 3 = 8
        tb_a = 32'h5;
        tb_b = 32'h3;
        tb_alu_op = 4'b0000;
        #10;
        $display("ADD: %d + %d = %d", tb_a, tb_b, tb_result);

        // 测试 SUB: 10 - 4 = 6
        tb_a = 32'hA;
        tb_b = 32'h4;
        tb_alu_op = 4'b0001;
        #10;
        $display("SUB: %d - %d = %d", tb_a, tb_b, tb_result);

        // 测试 AND
        tb_a = 32'hFF00FF00;
        tb_b = 32'h0F0F0F0F;
        tb_alu_op = 4'b0010;
        #10;
        $display("AND: %h & %h = %h", tb_a, tb_b, tb_result);

        #20;
        $finish;
    end

    // 波形输出(可选)
    initial begin
        $dumpfile("tb_alu.vcd");
        $dumpvars(0, tb_alu);
    end

endmodule
写 testbench 的几个习惯:
  • 用 $display 打印关键信息,方便调试
  • 用 $dumpfile 和 $dumpvars 生成 VCD 波形文件
  • 测试用例要覆盖边界条件(全0、全1、最大值等)
  • 别忘了加 $finish,否则仿真不会自动结束

3.5 本章知识体系总览

下面这张图把本章的核心知识点串起来了。你想想看,从模块化设计开始,到两种描述方式,再到赋值方式的区别,最后用 testbench 验证——这就是一个完整的 Verilog 设计闭环。

Verilog 基础知识体系 Verilog 硬件描述语言基础 — 知识体系 Verilog 设计基础 模块化设计 always 与 assign 阻塞与非阻塞 仿真 testbench 高内聚 低耦合 组合逻辑 时序逻辑 阻塞 = 非阻塞 <= DUT 实例化 激励生成 波形输出 结果检查 核心:组合逻辑用阻塞赋值,时序逻辑用非阻塞赋值

这张图把今天的内容串在了一起。从模块化设计出发,到两种描述方式,再到赋值方式的区别,最后用 testbench 验证——这就是一个完整的 Verilog 设计闭环。后面的流水线设计,每一步都会用到这些基础概念。

本章要点回顾:
  • 模块化设计:每个模块只干一件事,接口清晰
  • assign 用于组合逻辑,always 块可描述组合和时序
  • 组合逻辑用阻塞赋值(=),时序逻辑用非阻塞赋值(<=)
  • testbench 没有端口,实例化 DUT 后产生激励并检查结果