3. 硬件描述语言基础:组合逻辑与时序逻辑、模块化设计、Testbench编写

各位同学好,我是你们的FPGA讲师。今天咱们聊聊硬件描述语言的基础。说白了,Verilog和VHDL就是用来描述数字电路的“语言”。你想想看,画原理图太慢,用代码描述电路才是现代工程师的标配。

我个人习惯用Verilog,因为它语法简洁,上手快。但VHDL在军工、航天领域用得更多,语法严谨。咱们课程以Verilog为主,但概念是通用的。

3.1 组合逻辑 vs 时序逻辑

这是数字电路最核心的两个概念。我刚开始学的时候也迷糊过,后来用一句话就记住了:组合逻辑看输入,时序逻辑看时钟

3.1.1 组合逻辑

组合逻辑的输出只取决于当前输入。没有记忆功能,没有时钟参与。说白了,就是一堆与门、或门、非门搭起来的电路。

举个例子,一个简单的与门:

// Verilog 组合逻辑示例
assign y = a & b;  // y 只由 a 和 b 决定

我在项目中遇到过一个问题:组合逻辑的路径太长,导致时序不满足。嗯,这里要注意,组合逻辑的级数不能太多,否则延迟会累积。

避坑指南:我曾经在一个高速ADC接口设计中,用了10级组合逻辑做数据对齐。结果时序分析一片红。后来改成流水线寄存器,问题就解决了。组合逻辑不是不能用,但要控制深度。

3.1.2 时序逻辑

时序逻辑就不一样了。它依赖时钟,有记忆功能。输出不仅和当前输入有关,还和之前的状态有关。

最简单的时序逻辑——D触发器:

// Verilog 时序逻辑示例
always @(posedge clk) begin
    q <= d;  // 每个时钟上升沿,q 变成 d 的值
end

你想想看,为什么回测系统需要时序逻辑?因为我们需要记住历史数据。比如K线数据、订单状态,这些都需要寄存器来存储。

核心区别:
  • 组合逻辑:用 assign 或 always @(*) 描述
  • 时序逻辑:用 always @(posedge clk) 描述
  • 组合逻辑用阻塞赋值 =
  • 时序逻辑用非阻塞赋值 <=

3.2 模块化设计

写FPGA代码和写软件一样,不能把所有东西都塞到一个文件里。模块化设计是基本功。我习惯把每个功能单元封装成一个模块,就像搭积木一样。

3.2.1 模块的基本结构

一个Verilog模块包含端口定义、内部信号、功能描述三部分。看个例子:

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

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

endmodule

这个计数器模块,输入有时钟、复位、使能,输出是8位计数值。模块化之后,其他地方要用计数器,直接实例化就行。

3.2.2 模块实例化

在顶层模块里,我们把各个子模块连起来:

module top (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       start,
    output wire [7:0] result
);

    wire [7:0] cnt_value;

    // 实例化计数器
    counter u_counter (
        .clk   (clk),
        .rst_n (rst_n),
        .en    (start),
        .count (cnt_value)
    );

    // 其他逻辑...
    assign result = cnt_value * 2;

endmodule
个人经验:我建议每个模块只做一件事,并且做好。比如一个模块只做加法,另一个只做比较。这样调试起来特别方便。我在做回测引擎时,把订单管理、行情处理、风险控制都拆成了独立模块,后期维护轻松很多。

3.3 Testbench编写

写代码不仿真,等于白写。Testbench就是用来验证你的设计对不对的。说白了,就是给设计加激励,看输出是否符合预期。

3.3.1 基本Testbench结构

一个Testbench通常包含:时钟生成、复位生成、激励输入、结果检查。看个例子:

`timescale 1ns / 1ps

module tb_counter;

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

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

    // 生成时钟
    initial begin
        clk = 0;
        forever #5 clk = ~clk;  // 10ns周期,100MHz
    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

3.3.2 仿真技巧

我在项目中常用的几个技巧:

  • 使用任务(task)封装常用操作:比如读写寄存器、发送数据包
  • 自动检查结果:用 $error 或 assert 语句,不要人工看波形
  • 随机测试:用 $random 生成随机激励,覆盖边界情况
我曾经踩过的坑:有一次仿真通过,但上板就挂。后来发现是Testbench里用了 #delay 来模拟时序,但实际电路没有这么理想的延迟。记住:仿真通过不代表上板没问题,时序约束和硬件测试同样重要。

3.4 知识体系总览

为了让大家更直观地理解本章的知识结构,我画了一张图:

FPGA硬件描述语言基础 组合逻辑 时序逻辑 模块化设计 assign / always @(*) 阻塞赋值 = always @(posedge clk) 非阻塞赋值 <= 端口定义 / 实例化 高内聚低耦合 Testbench 仿真验证

这张图把本章的核心内容串起来了。组合逻辑和时序逻辑是基础,模块化设计是组织代码的方法,Testbench是验证手段。四者缺一不可。

3.5 小结

今天的内容就到这里。总结一下:

  • 组合逻辑:输出只和当前输入有关,用 assign 或 always @(*)
  • 时序逻辑:输出和时钟、历史状态有关,用 always @(posedge clk)
  • 模块化设计:把功能拆成小模块,方便复用和维护
  • Testbench:仿真验证是保证设计正确性的关键

这些基础概念,在后续的FPGA回测加速课程中会反复用到。尤其是时序逻辑,回测引擎里的状态机、流水线、FIFO都离不开它。大家先把基础打牢,后面才能跑得快。

一句话记住:组合逻辑是“看当下”,时序逻辑是“记过去”,模块化是“拆任务”,Testbench是“验成果”。

专注资料整理