第三讲:硬件描述语言基础(Verilog)——模块化设计、组合逻辑与时序逻辑、仿真与Testbench编写

说实话,Verilog 这门语言,你把它当软件学就完了。我见过太多新手,上来就写 C 语言的思维,结果综合出来的电路一塌糊涂。这一讲,咱们就聊聊 Verilog 的核心三块:模块化怎么搭、组合和时序怎么分、仿真怎么写。

3.1 模块化设计——把大电路拆成小积木

模块化,说白了就是“分而治之”。一个复杂的 Zynq 系统,你不可能在一个文件里写完所有逻辑。我个人的习惯是:每个功能块一个模块,顶层只做连线。

举个例子,一个简单的计数器模块:

module counter #(
    parameter WIDTH = 8
)(
    input  wire             clk,
    input  wire             rst_n,
    input  wire             en,
    output reg  [WIDTH-1:0] count
);

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

endmodule

你看,这个模块只做一件事:计数。输入输出清清楚楚。我在项目中遇到过有人把 SPI 控制器、UART、GPIO 全写在一个模块里,结果调试时改一处崩三处。模块化设计,就是让你能“各个击破”。

小技巧:模块的端口尽量用 wire 和 reg 明确区分。输入用 wire,输出用 reg(如果是时序逻辑)。这样读代码的人一眼就知道信号流向。

3.2 组合逻辑 vs 时序逻辑——本质区别在哪?

很多初学者搞不清这两个概念。我换个说法:组合逻辑是“纯电路”,输入一变,输出立刻变,没有记忆。时序逻辑是“带寄存器的电路”,只在时钟沿更新,有记忆能力。

组合逻辑:

assign sum = a + b;  // 只要 a 或 b 变,sum 立刻变

时序逻辑:

always @(posedge clk) begin
    sum_reg <= a + b;  // 只在时钟上升沿更新
end

嗯,这里要注意:组合逻辑用 assign 或 always @(*),时序逻辑用 always @(posedge clk)。千万别混用。我曾经见过有人把组合逻辑写在 always @(posedge clk) 里,结果综合出来一堆奇怪的锁存器。

避坑指南:组合逻辑中,如果 if 或 case 没有覆盖所有分支,会综合出锁存器(latch)。我曾经因为这个原因,一个简单的译码器多花了半天时间排查。解决办法:要么补全所有分支,要么给默认值。

3.3 仿真与 Testbench 编写——不仿真就上板?你胆子真大

我刚开始做 FPGA 时,也是写完代码直接烧录。结果呢?板子冒烟了(夸张说法,但确实烧坏过一片)。后来我养成了习惯:先写 Testbench,再写设计代码。这叫“测试驱动开发”。

一个标准的 Testbench 长这样:

`timescale 1ns / 1ps

module tb_counter;

    // 信号声明
    reg         clk;
    reg         rst_n;
    reg         en;
    wire [7:0]  count;

    // 实例化被测试模块
    counter #(
        .WIDTH(8)
    ) uut (
        .clk   (clk),
        .rst_n (rst_n),
        .en    (en),
        .count (count)
    );

    // 时钟生成
    initial begin
        clk = 0;
        forever #5 clk = ~clk;  // 100MHz 时钟
    end

    // 测试激励
    initial begin
        // 初始化
        rst_n = 0;
        en    = 0;
        #20;
        rst_n = 1;
        #10;
        en = 1;
        #200;
        en = 0;
        #50;
        $finish;
    end

    // 波形监控
    initial begin
        $monitor("time=%0t, count=%d", $time, count);
    end

endmodule

你看,Testbench 没有端口,它自己产生时钟、复位、激励,然后观察输出。我个人习惯用 $monitor 打印关键信号,用波形查看器看时序细节。

核心要点:仿真不是为了“跑通”,而是为了“验证”。你要故意制造边界条件,比如复位释放瞬间、使能信号毛刺、时钟域切换等。我在项目中遇到过最坑的一次,就是仿真没测复位释放时序,结果上板后计数器初始值不对。

3.4 知识体系总览

下面这张图,是我自己总结的 Verilog 基础学习路线。你照着这个顺序学,不会走弯路。

Verilog 基础学习路线 模块化设计 组合逻辑 时序逻辑 仿真与Testbench 上板验证 先学模块化,再分组合/时序,然后仿真验证,最后上板

3.5 实战建议

最后,给你几个我踩过坑后的建议:

  • 代码风格统一:我习惯用下划线命名法(如 data_valid),模块名大写开头(如 UartCtrl)。团队协作时,风格比能力重要。
  • 仿真覆盖率:别只测正常情况。测一下复位释放瞬间、时钟抖动、输入毛刺。我吃过这个亏。
  • 版本管理:Verilog 代码一定要用 Git。我见过有人把 final_v2、final_v3_final 这种文件名传遍全组,太可怕了。
我的习惯:每个模块都配一个独立的 Testbench,文件名统一为 tb_模块名.v。这样找起来方便,别人接手也清楚。

好了,这一讲就到这里。记住:Verilog 不是软件,是硬件描述。你写的是电路,不是程序。多仿真、多思考、多动手,慢慢就有感觉了。

可可精品资料库