4. 硬件描述语言入门:Verilog/VHDL基础语法、模块化设计、Testbench编写

好,咱们进入正题。硬件描述语言,说白了就是给芯片画电路图的「代码版」。你想想看,总不能真拿笔在纸上画几万个晶体管吧?所以我们需要 Verilog 或 VHDL 这种语言,把电路行为描述出来,然后交给工具去综合成真实的门级网表。

我个人习惯用 Verilog,因为它语法更灵活,在工业界和学术界都占主流。当然 VHDL 也有它的优势,比如强类型检查更严格。但今天咱们以 Verilog 为主线,VHDL 我会在关键地方做个对比。

4.1 基础语法:先跑通一个最小系统

先看一个最简单的例子——一个与门。别小看它,这是所有复杂逻辑的基石。

// Verilog 与门
module and_gate (
    input  wire a,
    input  wire b,
    output wire y
);
    assign y = a & b;
endmodule

嗯,这里要注意:assign 是连续赋值语句,它描述的是组合逻辑。只要 a 或 b 变了,y 立刻跟着变——这就像一根导线直接连过去。

VHDL 版本长这样:

-- VHDL 与门
entity and_gate is
    port (
        a : in  std_logic;
        b : in  std_logic;
        y : out std_logic
    );
end entity;

architecture rtl of and_gate is
begin
    y <= a and b;
end architecture;

看到区别了吗?VHDL 把端口声明和功能实现分成了 entity 和 architecture 两部分。Verilog 则合在一个 module 里。我个人觉得 Verilog 写起来更顺手,但 VHDL 在大型项目中更容易维护——因为结构更清晰。

核心概念:硬件描述语言的核心是「并行」和「时序」。软件代码是顺序执行的,但硬件里所有 assign 和 always 块都是同时运行的。这一点刚转行的人最容易搞混。

4.2 模块化设计:像搭积木一样建芯片

我在项目中遇到过最头疼的事,就是有人把整个设计写在一个几千行的 module 里。调试起来简直噩梦。所以模块化设计不是可选项,是必选项。

模块化设计的原则很简单:

  • 高内聚低耦合——每个模块只做一件事,接口尽量简单
  • 层次化——顶层模块只负责连线,具体功能交给子模块
  • 参数化——用 parameter 定义位宽、深度等常量,方便复用

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

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

然后顶层模块可以这样例化:

module top (
    input  wire clk,
    input  wire rst_n,
    input  wire en,
    output wire [7:0] result
);
    counter #(.WIDTH(8)) u_counter (
        .clk  (clk),
        .rst_n(rst_n),
        .en   (en),
        .count(result)
    );
endmodule

你看,顶层只负责把信号连进去,具体怎么计数是 counter 的事。这样如果以后要改成 16 位计数器,改一个参数就行,不用动顶层。

我的经验:模块的端口数量最好控制在 10 个以内。超过这个数,连线的可读性会急剧下降。我曾经接手过一个模块,端口有 40 多个,光看接口定义就花了半天。后来我把它拆成了 5 个小模块,每个模块 8 个端口,世界清净了。

4.3 Testbench 编写:没有仿真,一切都是空谈

写完了代码,怎么知道它对不对?靠眼睛看?别闹。必须写 Testbench 做仿真。

Testbench 的核心思路是:

  1. 例化待测模块(DUT)
  2. 生成时钟和复位信号
  3. 施加激励(输入信号的变化)
  4. 检查输出是否符合预期

一个简单的 Testbench 示例:

`timescale 1ns / 1ps

module tb_counter;

    reg  clk;
    reg  rst_n;
    reg  en;
    wire [7:0] count;

    // 例化 DUT
    counter #(.WIDTH(8)) u_dut (
        .clk  (clk),
        .rst_n(rst_n),
        .en   (en),
        .count(count)
    );

    // 生成时钟:周期 10ns
    initial begin
        clk = 0;
        forever #5 clk = ~clk;
    end

    // 施加激励
    initial begin
        rst_n = 0;
        en    = 0;
        #20;
        rst_n = 1;
        #10;
        en = 1;
        #100;
        en = 0;
        #50;
        $finish;
    end

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

endmodule

这里有几个关键点:

  • `timescale 定义了时间单位和精度。1ns/1ps 表示单位是 1ns,精度到 1ps
  • initial 块是顺序执行的,但多个 initial 块之间是并行的
  • forever 用来生成周期信号,比如时钟
  • $monitor 会在信号变化时自动打印,调试利器

避坑指南:我曾经犯过一个低级错误——在 Testbench 里用 #5 延时,但忘了加 `timescale。结果仿真器默认单位是 1ps,实际延时只有 5ps,时钟跑得飞快,DUT 根本来不及响应。所以记住:写 Testbench 第一行必须是 `timescale

4.4 知识体系总览

下面这张图概括了本章的核心内容。你可以把它当作一个思维导图,方便回顾。

硬件描述语言入门 基础语法 模块化设计 Testbench编写 module/endmodule assign 连续赋值 always 过程块 参数化 #() 模块例化 层次化设计 时钟生成 激励施加 结果检查 核心思想:并行执行 + 时序控制 Verilog 和 VHDL 只是语法不同,底层逻辑相通

4.5 实战建议:从哪开始练手?

光看不练是学不会的。我建议你按这个顺序来:

  1. 先写一个 8 位加法器——用 assign 实现,跑仿真验证
  2. 再写一个带使能的计数器——用 always 块,理解时序逻辑
  3. 然后写一个状态机——比如序列检测器,这是数字设计的核心
  4. 最后把这三个模块组合起来——练练模块化设计和 Testbench 的层次化

每个练习都写对应的 Testbench,确保覆盖率。我当年刚入行时,带我的老工程师说了一句话,我一直记着:「没有 Testbench 的代码,等于没写。」

总结一下:Verilog/VHDL 是硬件工程师的「普通话」。语法不难,难的是思维方式的转变——从软件的顺序思维切换到硬件的并行思维。多写、多仿真、多踩坑,自然就熟了。


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