第二章 硬件描述语言基础(Verilog/VHDL):模块化设计、组合逻辑与时序逻辑、仿真与测试

好,咱们进入正题。这一章是FPGA设计的“内功心法”。你想想看,不管多复杂的交易信号处理系统,拆到最底层,无非就是一堆逻辑门和触发器在干活。而硬件描述语言,就是我们指挥这些门和触发器干活的语言。

我个人习惯用Verilog,因为它在量化交易领域更流行,代码也更简洁。当然VHDL也有它的铁粉,语法严谨,适合大型工程。但核心思想是一样的——描述硬件,而不是写软件。

2.1 模块化设计:把大问题拆成小积木

做FPGA设计,最忌讳的就是“一把梭”。把所有逻辑写在一个模块里,后期调试能让你怀疑人生。我早期做高频交易信号处理时,就吃过这个亏。一个模块几千行,改一个地方,仿真全崩。

模块化设计,说白了就是“分而治之”。把整个系统拆成功能独立的小模块,每个模块只干一件事,干好一件事。

核心原则:

  • 高内聚:一个模块内部的功能要紧密相关
  • 低耦合:模块之间的接口要简单清晰

举个例子,一个简单的交易信号接收模块,可以拆成这样:

// 顶层模块:信号接收与解析
module signal_receiver_top (
    input  wire        clk,
    input  wire        rst_n,
    input  wire        rx_data,
    output reg  [31:0] signal_price,
    output reg         signal_valid
);

    // 实例化子模块
    wire [7:0] byte_data;
    wire       byte_valid;

    // 串口接收模块
    uart_rx u_uart_rx (
        .clk        (clk),
        .rst_n      (rst_n),
        .rx_data    (rx_data),
        .byte_data  (byte_data),
        .byte_valid (byte_valid)
    );

    // 协议解析模块
    protocol_parser u_protocol_parser (
        .clk           (clk),
        .rst_n         (rst_n),
        .byte_data     (byte_data),
        .byte_valid    (byte_valid),
        .signal_price  (signal_price),
        .signal_valid  (signal_valid)
    );

endmodule

你看,顶层模块只负责“搭积木”,具体怎么收串口数据、怎么解析协议,都交给子模块去干。这样,哪个模块出了问题,直接定位到那个模块去修就行。

我的习惯:每个模块的端口尽量控制在20个以内。超过20个,我就会考虑是不是该拆分了。接口信号命名要有规律,比如所有输入信号加个 _i 后缀,输出加个 _o,双向加 _io。这样看代码一目了然。

2.2 组合逻辑与时序逻辑:一个管“算”,一个管“存”

这是FPGA设计的核心概念,也是很多新手容易搞混的地方。我简单给你捋一捋。

组合逻辑:输出只取决于当前的输入。说白了,就是“即时响应”。输入一变,输出立马跟着变。没有记忆功能。

时序逻辑:输出不仅取决于当前输入,还取决于之前的状态。它是有“记忆”的,靠时钟沿来触发更新。

为什么会这样?因为FPGA底层的基本单元就是查找表(LUT)和触发器(FF)。LUT实现组合逻辑,FF实现时序逻辑。

来看个对比:

特性 组合逻辑 时序逻辑
输出依赖 仅当前输入 当前输入 + 历史状态
触发方式 输入变化立即触发 时钟沿触发
典型实现 assign、always@(*) always@(posedge clk)
典型应用 加法器、译码器、多路选择器 计数器、状态机、寄存器

写代码时,有个黄金法则:组合逻辑用阻塞赋值(=),时序逻辑用非阻塞赋值(<=)。这个规矩千万别破,破了仿真结果和实际电路就对不上了。我曾经在项目里见过一个同事,时序逻辑里用了阻塞赋值,仿真跑得挺好,上板子就乱跳。查了两天才找到原因,教训深刻。

// 组合逻辑示例:加法器
always @(*) begin
    sum = a + b;  // 阻塞赋值
end

// 时序逻辑示例:寄存器
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 0;   // 非阻塞赋值
    else
        q <= d;
end

2.3 仿真与测试:不仿真,不上板

这是我在团队里定的一条死规矩。任何模块,没有经过仿真验证,绝对不允许综合和上板。为什么?因为FPGA调试太痛苦了。你想想看,信号在芯片内部跑,你看不见摸不着,只能靠逻辑分析仪抓几个关键信号。要是问题出在内部状态机里,抓都抓不到。

仿真,就是让你在电脑上把代码跑一遍,看看波形对不对。常用的仿真工具有ModelSim、Vivado Simulator、VCS等。我个人偏爱Vivado自带的仿真器,够用,还不用额外装软件。

写测试文件(Testbench)有几个要点:

  • 时钟和复位:先产生时钟和复位信号,这是所有时序逻辑的基础
  • 输入激励:模拟真实场景,给模块输入各种数据
  • 自动比对:别靠肉眼盯着波形看,写代码自动检查输出对不对
  • 边界条件:测试最大值、最小值、溢出、复位等极端情况
// 简单的Testbench示例
module tb_signal_receiver_top;

    reg  clk;
    reg  rst_n;
    reg  rx_data;
    wire [31:0] signal_price;
    wire signal_valid;

    // 实例化待测模块
    signal_receiver_top u_dut (
        .clk           (clk),
        .rst_n         (rst_n),
        .rx_data       (rx_data),
        .signal_price  (signal_price),
        .signal_valid  (signal_valid)
    );

    // 产生时钟
    always #5 clk = ~clk;  // 100MHz时钟

    // 测试过程
    initial begin
        clk = 0;
        rst_n = 0;
        rx_data = 1;
        #20 rst_n = 1;  // 释放复位

        // 模拟发送一个字节 0xA5
        send_byte(8'hA5);
        #100;

        // 检查结果
        if (signal_valid && signal_price == 32'hA5)
            $display("Test PASS: signal_price = %h", signal_price);
        else
            $display("Test FAIL: signal_price = %h, expected = 32'hA5", signal_price);

        #100 $finish;
    end

    // 串口发送任务
    task send_byte(input [7:0] data);
        integer i;
        begin
            rx_data = 0;  // 起始位
            #87;
            for (i = 0; i < 8; i = i + 1) begin
                rx_data = data[i];
                #87;
            end
            rx_data = 1;  // 停止位
            #87;
        end
    endtask

endmodule

注意:仿真通过不代表上板一定没问题。时序约束、时钟抖动、电源噪声这些,仿真里是模拟不出来的。但反过来,仿真都过不了,上板肯定不行。所以,仿真通过是“必要条件”,不是“充分条件”。

知识体系总览

为了让你对本章内容有个整体认识,我画了张图。你可以看到,模块化设计是骨架,组合逻辑和时序逻辑是血肉,仿真测试是检验手段。三者缺一不可。

FPGA硬件描述语言基础 模块化设计 组合逻辑 时序逻辑 高内聚 低耦合 接口清晰 即时响应 无记忆 阻塞赋值 有记忆 时钟触发 非阻塞赋值 仿真与测试 Testbench编写 波形分析 自动比对 边界测试 三者结合,构成FPGA设计的完整知识体系

嗯,这一章的内容就这些。模块化设计让你代码结构清晰,组合逻辑和时序逻辑让你明白硬件是怎么工作的,仿真测试帮你把bug扼杀在摇篮里。这三板斧练好了,后面的内容学起来就轻松多了。

一句话总结:写Verilog/VHDL,心里要时刻想着“这代码综合出来是什么电路”。想明白了,代码自然就写对了。


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