2. 硬件描述语言基础:Verilog语法速览、组合逻辑与时序逻辑、仿真与调试

好,咱们进入第二讲。说实话,很多初学者觉得Verilog就是另一门编程语言,跟C差不多。嗯,这个想法其实挺危险的。我见过太多人把Verilog写成C,结果综合出来的电路完全不是那么回事。

硬件描述语言,说白了就是「画电路」的另一种方式。你写的每一行代码,最终都会变成实实在在的门电路、触发器和连线。所以,脑子里要时刻有电路图。

核心思维转变:写Verilog不是在「编程」,而是在「描述硬件结构」。你的代码是给综合工具看的,不是给CPU跑的。

2.1 Verilog语法速览——够用就行

我不会给你列一本语法手册。咱们只挑FPGA行情解析中最常用的语法讲。其他的,用到再查。

2.1.1 模块结构

一个Verilog模块,就是一个硬件单元。它有输入、输出,还有内部逻辑。

module parser_core (
    input  wire        clk,          // 时钟
    input  wire        rst_n,        // 复位,低有效
    input  wire [7:0]  data_in,      // 8位数据输入
    output reg  [31:0] price_out     // 32位价格输出
);

    // 内部逻辑写在这里

endmodule

我个人习惯把端口声明和内部信号分开写。这样模块的接口一目了然,别人看你的代码也舒服。

2.1.2 常用数据类型

类型 说明 我的用法
wire 线网类型,用于组合逻辑连线 assign语句的输出、模块间的连接
reg 寄存器类型,用于存储状态 always块中的赋值对象
integer 32位有符号整数 仿真中的循环变量,不可综合

小技巧:很多新手搞不清wire和reg。你记住一条:assign左边用wirealways块里赋值用reg。综合工具会自动处理,但写清楚是职业素养。

2.1.3 运算符速览

Verilog的运算符跟C很像,但有几个坑要注意。

  • 位宽匹配:赋值时左右两边位宽必须一致。我曾经因为一个16位赋值给8位变量,查了整整一下午的波形。
  • 拼接运算符 {}行情解析中经常用到。比如把两个8位数据拼成16位:{high_byte, low_byte}
  • 三目运算符 ?:综合出来是一个多路选择器。比if-else更直观。
// 实际项目中的例子:解析行情包中的价格字段
wire [15:0] raw_price;
wire [31:0] scaled_price;

assign scaled_price = {16'h0000, raw_price};  // 高位补0

2.2 组合逻辑与时序逻辑——这是分水岭

搞不清这两个,你的FPGA设计就是一团浆糊。我面试新人时,第一个问题就是:「组合逻辑和时序逻辑的区别是什么?」

2.2.1 组合逻辑

组合逻辑的输出只取决于当前输入。没有记忆功能,没有时钟边沿。

写法有两种:

  1. assign语句:适合简单的逻辑
  2. always @(*):适合复杂的组合逻辑
// 方式一:assign
assign is_buy_order = (order_type == 2'b01) ? 1'b1 : 1'b0;

// 方式二:always @(*)
always @(*) begin
    if (price > 1000)
        high_price_flag = 1'b1;
    else
        high_price_flag = 1'b0;
end

注意:组合逻辑的always块里,敏感列表必须用@(*)。如果你手动写敏感列表漏了信号,仿真和综合结果会不一致。我吃过这个亏,真的会谢。

2.2.2 时序逻辑

时序逻辑的输出不仅取决于当前输入,还取决于之前的状态。说白了,它有「记忆」。

核心写法:always @(posedge clk or negedge rst_n)

// 一个简单的计数器,用于行情包序号检测
reg [7:0] pkt_cnt;

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

注意这里用的是 <=(非阻塞赋值),不是 =。为什么?

非阻塞赋值在always块结束时才统一更新。这模拟了触发器的行为——所有触发器在时钟沿同时采样、同时更新。如果你用阻塞赋值,综合出来的电路可能多出一级不必要的延迟。

2.2.3 组合 vs 时序:一张图说清楚

组合逻辑 输出 = f(当前输入) 时序逻辑 输出 = f(当前输入, 历史状态) 时钟驱动 组合逻辑 + 寄存器 = 时序逻辑 行情解析中,90%的逻辑都是时序逻辑

2.3 仿真与调试——别等上板才后悔

我见过太多人,代码写完直接烧到板子上,然后对着示波器一脸懵逼。仿真,是FPGA开发中最值得花时间的环节。

2.3.1 写一个简单的testbench

Testbench就是给你的设计喂数据、看结果。它本身不需要综合,所以可以写得随意一些。

`timescale 1ns / 1ps

module tb_parser_core;

    reg         clk;
    reg         rst_n;
    reg  [7:0]  data_in;
    wire [31:0] price_out;

    // 实例化待测模块
    parser_core uut (
        .clk      (clk),
        .rst_n    (rst_n),
        .data_in  (data_in),
        .price_out(price_out)
    );

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

    // 测试激励
    initial begin
        clk = 0;
        rst_n = 0;
        data_in = 8'd0;
        
        #20 rst_n = 1;  // 释放复位
        
        // 模拟一个行情包到达
        #10 data_in = 8'hA5;
        #10 data_in = 8'h3C;
        #10 data_in = 8'hF1;
        
        #100 $finish;
    end

    // 波形导出
    initial begin
        $dumpfile("wave.vcd");
        $dumpvars(0, tb_parser_core);
    end

endmodule

我的习惯:testbench里一定要加$dumpfile$dumpvars。这样仿真完直接打开波形看,比看打印日志高效十倍。

2.3.2 调试三板斧

遇到bug别慌。按这个顺序来:

  1. 看波形:打开VCD文件,看信号是不是按预期变化。重点关注时钟沿附近的数据采样点。
  2. 加断言:在testbench里写assert,让仿真自动检查关键条件。比如「价格字段不能为0」。
  3. 分段隔离:把大模块拆成小模块,逐个仿真。我一般先测数据接收部分,再测解析部分,最后测输出。

2.3.3 一个常见的坑:仿真通过,上板失败

为什么会这样?

仿真时,所有信号都是理想状态。但实际FPGA里有建立时间、保持时间、时钟抖动。最常见的问题是:

  • 异步信号没同步:外部输入直接接到触发器,没有打两拍。仿真时没问题,上板后偶尔出错。
  • 组合逻辑环路:仿真器能跑,但综合工具会报错或产生奇怪的振荡。

避坑指南:我曾经做一个高频行情解析项目,仿真完美,上板后每10万个包就丢一个。查了三天,发现是复位信号没做异步复位同步释放。从那以后,我的所有模块都加上了标准的复位同步器。

2.4 本章小结

Verilog语法不难,难的是硬件思维。组合逻辑是「线」,时序逻辑是「沿」。仿真不是走过场,是你和硬件对话的方式。

下一讲,我们会把这些基础用到真正的行情解析模块中。到时候你会发现,今天学的每一条规则,都是为性能服务的。


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