3. 硬件描述语言基础(Verilog):模块化设计、组合逻辑与时序逻辑、阻塞与非阻塞赋值、Testbench编写

各位同学,咱们今天聊聊Verilog。说实话,很多新手觉得Verilog就是写代码,跟C语言差不多。嗯,这个想法很危险。我当年刚入行时也这么想,结果写出来的东西综合出来一堆莫名其妙的Latch,被老工程师骂得狗血淋头。

Verilog不是软件语言,它是用来描述硬件的。你写的每一行代码,最终都会变成实实在在的门电路、触发器和连线。脑子里没有电路图,代码写得再花哨也没用。

3.1 模块化设计:搭积木的艺术

模块化设计,说白了就是把大系统拆成小模块。每个模块干一件事,干好一件事。我在做行情加速卡时,会把整个系统拆成:UDP解析模块、行情解码模块、排序模块、分发模块。每个模块独立设计、独立验证,最后再拼起来。

一个标准的Verilog模块长这样:

module udp_parser (
    input  wire        clk,
    input  wire        rst_n,
    input  wire [7:0]  data_in,
    input  wire        data_valid,
    output reg  [31:0] payload,
    output reg         payload_valid
);

    // 内部逻辑写在这里

endmodule

注意看,模块的端口列表就是它的对外接口。我习惯把所有输入输出都写清楚,这样别人一看就知道这个模块干什么用。端口类型就三种:inputoutputinout。别乱用inout,那玩意儿容易出问题。

我的习惯:每个模块只做一件事。如果一个模块的代码超过200行,我就考虑拆分了。行情加速卡里最长的模块也就150行,清晰得很。

3.2 组合逻辑与时序逻辑:两个世界

这是Verilog里最重要的概念,没有之一。你想想看,数字电路就两种:组合逻辑时序逻辑

组合逻辑:输出只取决于当前输入。没有记忆功能,信号来了就反应。用assign或者always @(*)实现。

// 组合逻辑:一个简单的加法器
assign sum = a + b;

// 或者用always块
always @(*) begin
    if (sel)
        out = a;
    else
        out = b;
end

时序逻辑:输出不仅取决于当前输入,还取决于之前的状态。说白了就是有记忆,靠时钟沿触发。用always @(posedge clk)实现。

// 时序逻辑:一个D触发器
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 1'b0;
    else
        q <= d;
end

我曾经犯过一个低级错误:在组合逻辑里用了posedge clk,结果综合出来一堆奇怪的触发器。记住:组合逻辑用@(*),时序逻辑用@(posedge clk),这是铁律。

避坑指南:组合逻辑里不要产生latch!如果你在组合逻辑的always块里没有把所有分支写全,综合工具会给你生成锁存器。我曾经因为这个原因,一个模块的功耗超标了30%,查了两天才找到问题。

3.3 阻塞与非阻塞赋值:一个老生常谈的话题

这个知识点,面试必问,项目必用。但说实话,很多人用了好几年都没搞明白。

简单说:

  • 阻塞赋值 =:顺序执行,前面的赋值完才执行后面的。用于组合逻辑。
  • 非阻塞赋值 <=:并行执行,所有赋值同时发生。用于时序逻辑。

看个例子就明白了:

// 阻塞赋值:组合逻辑
always @(*) begin
    a = b + c;
    d = a + e;  // 这里用的是更新后的a
end

// 非阻塞赋值:时序逻辑
always @(posedge clk) begin
    a <= b + c;
    d <= a + e;  // 这里用的是更新前的a(旧值)
end

为什么会这样?因为硬件里,组合逻辑的信号是立即传播的,而触发器的输出是时钟沿后才更新的。非阻塞赋值模拟的就是触发器的行为。

我见过太多新手在时序逻辑里用阻塞赋值,结果仿真和实际行为对不上。记住一个口诀:组合用=,时序用<=。别问为什么,先记住,等你写了几万行代码自然就懂了。

核心原则:
  • 同一个always块里,不要混用=<=
  • 时序逻辑的always块里,全部用<=
  • 组合逻辑的always块里,全部用=

3.4 Testbench编写:验证才是硬道理

写代码只占30%的时间,剩下70%都在调试和验证。Testbench就是你的调试工具。

一个基本的Testbench结构:

module tb_udp_parser();

    // 1. 声明信号
    reg         clk;
    reg         rst_n;
    reg  [7:0]  data_in;
    reg         data_valid;
    wire [31:0] payload;
    wire        payload_valid;

    // 2. 实例化待测模块
    udp_parser u_dut (
        .clk           (clk),
        .rst_n         (rst_n),
        .data_in       (data_in),
        .data_valid    (data_valid),
        .payload       (payload),
        .payload_valid (payload_valid)
    );

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

    // 4. 测试激励
    initial begin
        // 初始化
        clk = 0;
        rst_n = 0;
        data_in = 0;
        data_valid = 0;

        // 复位
        #20 rst_n = 1;

        // 发送数据
        #10 data_in = 8'h55;
        data_valid = 1;
        #10 data_valid = 0;

        // 等待结果
        #100;
        $finish;
    end

    // 5. 监控输出
    always @(posedge clk) begin
        if (payload_valid)
            $display("Payload = %h", payload);
    end

endmodule

写Testbench有几个要点:

  • 时钟生成:用always块生成时钟,别用initial里的循环
  • 复位时序:先复位再释放,给模块一个确定的初始状态
  • 激励生成:用initial块按时间顺序发送激励
  • 结果检查:用$display$monitor打印关键信号
我的经验:写Testbench时,一定要覆盖边界情况。比如行情解析模块,我会测试:正常包、空包、超长包、乱序包。曾经有一个bug就是在超长包场景下才暴露出来的,幸亏Testbench覆盖到了。

3.5 知识体系总览

下面这张图,是我自己总结的Verilog基础体系。你照着这个框架去学,不会走偏。

Verilog硬件描述语言基础 模块化设计 • 模块端口定义:input/output/inout • 模块实例化:按名连接/按位连接 • 层次化设计:顶层→子模块 • 参数化模块:parameter/localparam • 生成语句:generate/endgenerate 组合逻辑 • assign连续赋值语句 • always @(*) 过程赋值 • 运算符:算术/逻辑/位/关系 • 条件语句:if-else / case • 避免latch:完整分支+默认值 时序逻辑 • 触发器:D/DFF/T触发器 • 同步复位 vs 异步复位 • 计数器/分频器设计 • 状态机:Moore/Mealy • 时钟域同步:打拍/异步FIFO Testbench验证 • 时钟/复位生成 • 激励生成:initial/task • 结果检查:$display/$monitor • 波形查看:dumpfile/dumpvars • 覆盖率:代码/功能/断言 阻塞赋值(=)用于组合逻辑,非阻塞赋值(<=)用于时序逻辑

这张图把Verilog基础分成了四大块:模块化设计、组合逻辑、时序逻辑、Testbench。每一块都有对应的核心知识点。你学的时候,可以对照这张图,看看自己哪个部分还没掌握。

最后说一句:Verilog不难,难的是用硬件的思维去思考。多写、多仿真、多看看综合后的电路图,慢慢就找到感觉了。

本章核心要点:
  1. 模块化设计:每个模块只做一件事,端口清晰
  2. 组合逻辑:用assignalways @(*),用阻塞赋值=
  3. 时序逻辑:用always @(posedge clk),用非阻塞赋值<=
  4. Testbench:时钟生成、激励发送、结果检查三步走
  5. 避免latch:组合逻辑里所有分支写全

好了,这一章就到这里。记住,写Verilog的时候,脑子里要有电路图。


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