第三讲:硬件描述语言Verilog入门

各位同学好,我是你们的CPU架构设计讲师。今天我们来聊聊Verilog——这门在数字电路设计领域里,几乎人手必备的硬件描述语言。

说实话,我刚开始接触Verilog的时候,总觉得它跟C语言差不多。后来被现实狠狠教育了一顿——硬件描述语言和软件编程语言,完全是两码事。你想想看,C语言是顺序执行的,而Verilog描述的是并行工作的硬件电路。这个思维转变,是入门的第一道坎。

Verilog语法基础:从“线”和“寄存器”开始

Verilog的核心,说白了就是描述“线”和“寄存器”怎么连、怎么工作。我们先看几个最基础的东西。

1. 模块与端口

每个Verilog设计,都是从module开始的。模块就像是一个黑盒子,有输入、输出,内部封装了逻辑。

module adder (
    input  [3:0] a,      // 4位输入
    input  [3:0] b,      // 4位输入
    output [4:0] sum     // 5位输出(考虑进位)
);
    assign sum = a + b;
endmodule

嗯,这里要注意:端口的方向一定要写清楚。input、output、inout,少写一个都不行。我见过太多新手因为漏了端口声明,仿真跑出来全是X态,急得满头大汗。

2. 数据类型:wire 与 reg

这是最容易混淆的地方。我简单总结一下:

  • wire:表示连线,由组合逻辑驱动。比如assign语句的输出,必须是wire类型。
  • reg:表示寄存器,在always块中被赋值。注意,reg不一定是真正的寄存器——在组合逻辑的always块中,它只是表示一个变量。

核心记忆法:在always块里赋值的变量,必须声明为reg;在assign语句里赋值的变量,必须声明为wire。

3. 运算符:跟C语言很像,但别大意

Verilog的运算符大部分跟C语言一样:+-*&|^等等。但有几个特殊的:

  • ===!==:全等比较,会考虑X态和Z态。我在调试总线协议时,经常用这个来检查信号是否处于高阻态。
  • {}:拼接运算符。比如 {a, b} 就是把a和b拼在一起。
  • <<>>:移位运算符。注意,算术右移 >>> 会保留符号位。

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

做CPU设计,不可能把所有逻辑写在一个模块里。模块化设计,说白了就是“分而治之”。

我个人习惯把设计分成三层:

  1. 顶层模块:负责连接各个子模块,就像主板上插芯片。
  2. 功能模块:比如ALU、寄存器堆、控制单元,每个模块完成一个独立功能。
  3. 基础单元:比如加法器、多路选择器、触发器,这些是最小的积木块。

举个例子,一个简单的ALU模块:

module alu (
    input  [3:0] a, b,
    input  [1:0] op,      // 00:加, 01:减, 10:与, 11:或
    output reg [4:0] result
);
    always @(*) begin
        case (op)
            2'b00: result = a + b;
            2'b01: result = a - b;
            2'b10: result = a & b;
            2'b11: result = a | b;
            default: result = 0;
        endcase
    end
endmodule

我的经验:写模块时,接口越简单越好。每个模块只做一件事,做好一件事。我曾经接手过一个同事写的模块,一个模块里塞了五个功能,调试了整整三天才理清楚。从那以后,我坚持“一个模块一个功能”的原则。

仿真与测试:不仿真的设计都是耍流氓

写完了代码,怎么知道对不对?仿真。说白了,就是给设计喂输入,看输出是不是你想要的。

1. Testbench 怎么写

Testbench也是一个模块,但它没有输入输出。它的任务就是:生成激励、检查结果。

module tb_adder;
    reg  [3:0] a, b;
    wire [4:0] sum;

    // 实例化被测试模块
    adder u_adder (
        .a(a),
        .b(b),
        .sum(sum)
    );

    // 生成测试激励
    initial begin
        a = 4'b0000; b = 4'b0000;
        #10;
        a = 4'b0011; b = 4'b0101;
        #10;
        a = 4'b1111; b = 4'b0001;
        #10;
        $finish;
    end

    // 监视输出
    initial begin
        $monitor("Time=%0t, a=%d, b=%d, sum=%d", $time, a, b, sum);
    end
endmodule

这里有几个关键点:

  • initial块只执行一次,适合用来初始化。
  • #10表示延迟10个时间单位。硬件世界里,时间就是一切。
  • $monitor是系统任务,每次信号变化时自动打印。

2. 仿真工具的选择

常用的仿真工具有:

工具 特点 适用场景
Vivado Simulator Xilinx家的,跟Vivado集成 Xilinx FPGA设计
ModelSim/Questa 业界标准,功能强大 通用数字设计
Icarus Verilog 开源免费,轻量级 学习和小型项目
Verilator 高性能,支持SystemVerilog 大型项目仿真加速

避坑指南:我曾经用Icarus Verilog仿真一个带X态传播的设计,结果发现它把X态当成0处理了,导致我误以为设计没问题。后来换用ModelSim才抓到真正的bug。所以,仿真工具的选择很重要,尤其是处理X态和Z态时

3. 仿真波形怎么看

仿真跑完了,会生成波形文件(.vcd或.wlf)。打开波形,你要关注的是:

  • 时钟和复位:是不是按预期工作?
  • 数据路径:输入到输出,延迟了几个周期?
  • 控制信号:使能、选择、读写信号,时序对不对?

我个人的习惯是:先看整体波形,确认基本功能;再放大看细节,检查时序边界。尤其是建立时间和保持时间,这是最容易出问题的地方。

知识体系总览

下面这张图,是我自己总结的Verilog入门知识体系。你可以把它当作学习路线图:

Verilog入门知识体系 语法基础 模块化设计 仿真与测试 模块与端口声明 wire 与 reg 数据类型 运算符与表达式 always 与 assign 顶层模块连接 功能模块划分 参数化设计 接口规范与复用 Testbench 编写 激励生成与检查 波形分析与调试 覆盖率与回归测试 核心思想:硬件是并行的,代码是并发的 理解硬件思维,才能写出好代码

写在最后

Verilog入门其实不难,难的是从软件思维切换到硬件思维。我见过很多有经验的软件工程师,写出来的Verilog代码跟C语言一样——到处都是for循环和function调用,综合出来一堆莫名其妙的电路。

记住一句话:你写的每一行Verilog,最终都会变成实实在在的电路。门、触发器、连线,它们都在那里。所以,写代码的时候,脑子里要想着电路图。

好了,这一讲就到这里。去写个加法器、写个计数器,跑一下仿真,看看波形。动手做一遍,比看十遍都管用。


专注资料整理