3. 硬件描述语言基础(HDL):Verilog/VHDL核心语法回顾,组合逻辑与时序逻辑设计,仿真与测试平台编写

好,咱们正式开始进入硬件描述语言的世界。说实话,很多刚接触FPGA的同学,总觉得Verilog或者VHDL就是写代码,跟C语言差不多。嗯,这个想法其实挺危险的。我经常在项目里看到有人把HDL当软件写,结果综合出来的电路完全不是那么回事。

今天这一章,我带大家快速回顾一下HDL的核心语法,重点放在组合逻辑和时序逻辑的设计上。最后再聊聊仿真和测试平台怎么写。这些都是基本功,但也是决定你设计质量的关键。

3.1 Verilog与VHDL:选哪个?

我个人习惯用Verilog,因为它在数字电路设计领域更流行,语法也更简洁。但VHDL在军工、航天这些领域依然很强势。你想想看,如果你去欧洲做项目,大概率要用VHDL。

其实两者本质是一样的,都是描述硬件。我建议你至少精通一种,另一种能看懂就行。我自己就是Verilog为主,VHDL能读能改。

核心观点:HDL不是编程语言,是硬件描述语言。你写的每一行代码,最终都会变成实实在在的门电路和触发器。

3.2 Verilog核心语法速览

咱们快速过一下Verilog里最常用的东西。我不会讲太细,重点是你得知道这些语法对应什么硬件。

3.2.1 模块与端口

一个Verilog设计的基本单元就是模块。模块就像是一个黑盒子,有输入、输出,内部实现具体功能。

module counter (
    input  wire       clk,
    input  wire       rst_n,
    output reg  [7:0] count
);

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            count <= 8'd0;
        else
            count <= count + 1'b1;
    end

endmodule

看到没?inputoutput定义了端口方向。wirereg是数据类型。这里有个坑:reg并不一定代表寄存器,它只是表示在always块里被赋值。我在项目中遇到过有人以为reg就是触发器,结果组合逻辑也用reg,综合出来一堆没必要的锁存器。

3.2.2 组合逻辑:assign与always@(*)

组合逻辑就是输出只取决于当前输入,没有记忆功能。Verilog里两种写法:

  • assign语句:用于连续赋值,适合简单的逻辑。
  • always@(*)块:用于过程赋值,适合复杂的组合逻辑。
// 方式一:assign
assign sum = a + b;
assign carry = a & b;

// 方式二:always@(*)
always @(*) begin
    if (sel)
        out = a;
    else
        out = b;
end

我的小技巧:写组合逻辑时,always块里一定要用阻塞赋值=,别用非阻塞<=。我曾经因为这个问题,仿真怎么都对不上,查了半天才发现是赋值方式用错了。

3.2.3 时序逻辑:always@(posedge clk)

时序逻辑就不一样了,它依赖时钟边沿来更新状态。说白了,就是触发器在干活。

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 1'b0;
    else
        q <= d;
end

这里必须用非阻塞赋值<=。为什么?因为非阻塞赋值能保证在同一个时钟周期内,所有赋值同时生效。你想想看,如果多个触发器级联,用阻塞赋值就会产生竞争,仿真结果和实际电路完全不一样。

3.3 组合逻辑与时序逻辑的设计要点

很多新手分不清什么时候用组合逻辑,什么时候用时序逻辑。我给大家一个简单的判断标准:

逻辑类型 特点 典型应用 赋值方式
组合逻辑 无记忆,输出即时响应 加法器、多路选择器、译码器 阻塞赋值 =
时序逻辑 有记忆,依赖时钟边沿 计数器、移位寄存器、状态机 非阻塞赋值 <=

避坑指南:我曾经在一个项目里,把组合逻辑和时序逻辑混写在同一个always块里,结果综合出来的电路多了一堆没用的锁存器。后来我养成了一个习惯:组合逻辑和时序逻辑分开写,一个always块只做一件事。

3.4 仿真与测试平台编写

写完了设计,怎么知道它对不对?仿真!说白了,仿真就是给设计喂激励,看它输出是不是符合预期。

3.4.1 测试平台的基本结构

测试平台(testbench)本身也是一个模块,但它没有输入输出端口。它的任务就是实例化被测试的设计,然后产生激励。

`timescale 1ns / 1ps

module tb_counter;

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

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

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

    // 产生复位和激励
    initial begin
        rst_n = 0;
        #20;
        rst_n = 1;
        #200;
        $finish;
    end

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

endmodule

你看,测试平台里用initial块来产生激励,用forever来产生时钟。这些都是不可综合的,只用于仿真。

3.4.2 仿真中的常见陷阱

嗯,这里要注意几个点:

  • 时间尺度:`timescale一定要写,不然仿真时间单位默认是1ns,但精度可能不够。
  • 初始化:仿真里reg默认是X态,所以一定要给初始值或者复位。
  • 仿真时间:别设得太短,也别太长。我一般先跑100个时钟周期看看波形,没问题再跑长一点。

我的经验:写测试平台时,我习惯把激励分成几个阶段:复位、配置、正常工作、异常情况。这样仿真结果一目了然,出了问题也容易定位。

3.5 小结

这一章咱们回顾了Verilog的核心语法,重点区分了组合逻辑和时序逻辑的设计方法,最后聊了仿真测试平台的写法。说实话,这些内容看起来简单,但真正用好需要大量实践。

我建议大家动手写几个小设计,比如一个4位加法器、一个8位计数器,再写个测试平台仿真一下。只有亲手写过,你才能真正理解HDL和软件语言的区别。

下一章咱们会深入讲状态机的设计,那可是FPGA设计的核心技能之一。到时候我会分享一些我在实际项目中踩过的坑,保证让你少走弯路。