2. Verilog HDL 进阶语法(一):`generate` 语句、`function` 与 `task`、多维数组与结构体

各位同学,咱们今天聊点硬核的。前面基础语法大家应该都摸熟了,但说实话,真正到了项目里,光靠 always 和 assign 是远远不够的。今天讲的这三个东西——generate、function/task、多维数组与结构体——是我个人认为从「小白」到「能干活」必须跨过的三道坎。

嗯,咱们一个一个来。

2.1 generate 语句:让代码学会「复制粘贴」

先问大家一个问题:如果你要例化 8 个一模一样的 D 触发器,你会怎么写?

新手可能会写 8 遍 dff u1(...),对吧?但要是 64 个呢?128 个呢?手写不累死才怪。

generate 就是干这个用的。它本质上是个「编译时的循环」或「编译时的条件判断」。注意,是编译时,不是运行时。这意味着它生成的电路在仿真开始前就已经固定了。

2.1.1 generate for:批量例化

我最常用的就是 generate for。举个例子,一个 8 位的流水线寄存器:

module pipeline_reg #(
    parameter WIDTH = 8
)(
    input  clk,
    input  rst_n,
    input  [WIDTH-1:0] din,
    output [WIDTH-1:0] dout
);

    reg [WIDTH-1:0] pipe [0:2];  // 三级流水

    genvar i;
    generate
        for (i = 0; i < 3; i = i + 1) begin : stage
            always @(posedge clk or negedge rst_n) begin
                if (!rst_n)
                    pipe[i] <= 'b0;
                else if (i == 0)
                    pipe[i] <= din;
                else
                    pipe[i] <= pipe[i-1];
            end
        end
    endgenerate

    assign dout = pipe[2];

endmodule

这里有个细节要注意:begin : stage 这个标签不是随便写的。它会给每个循环体生成一个唯一的层次化名称,比如 stage[0]、stage[1]、stage[2]。这在调试时特别有用——你可以直接在波形里看到每一级寄存器的信号。

我的习惯: 只要用 generate for,我一定给 begin 加标签。不加标签,综合工具可能会报奇怪的错误,而且仿真时你根本找不到信号在哪。

2.1.2 generate if 与 generate case:条件编译

有时候,同一个模块要根据参数不同生成不同的电路。比如一个加法器,参数 WIDTH 小于等于 8 时用行波进位,大于 8 时用超前进位。这时候 generate if 就派上用场了。

module adder #(
    parameter WIDTH = 8
)(
    input  [WIDTH-1:0] a, b,
    input  cin,
    output [WIDTH-1:0] sum,
    output cout
);

    generate
        if (WIDTH <= 8) begin : ripple
            ripple_adder #(.WIDTH(WIDTH)) u_ripple (
                .a(a), .b(b), .cin(cin),
                .sum(sum), .cout(cout)
            );
        end else begin : carry_lookahead
            cla_adder #(.WIDTH(WIDTH)) u_cla (
                .a(a), .b(b), .cin(cin),
                .sum(sum), .cout(cout)
            );
        end
    endgenerate

endmodule

我曾经在一个项目中遇到过这样的情况:芯片面积超标,后来发现是某个模块的 generate if 条件写反了,导致所有实例都用了大面积的超前进位加法器。嗯,那一次教训让我养成了每次写完 generate 都要检查综合报告的习惯。

避坑指南: generate if 的条件必须是常量表达式(参数或局部参数),不能是信号。我曾经见过有人写 if (en),结果综合报了一堆错——因为 en 是 wire,不是常量。

2.2 function 与 task:把重复代码「封装」起来

写 RTL 和写软件其实有共通之处:最怕重复代码。Verilog 提供了 function 和 task 两种「子程序」机制。但很多人搞不清它们的区别,我简单总结一下。

对比项 function task
返回值 必须有,且只有一个 无返回值,通过 output 端口传结果
时序控制 不能包含 #、@、wait 等 可以包含时序控制
调用方式 作为表达式的一部分 作为独立语句
可综合性 通常可综合(纯组合逻辑) 部分可综合(需谨慎)
典型用途 组合逻辑计算、位宽转换 仿真激励、复杂时序操作

2.2.1 function:纯组合逻辑的好帮手

我个人习惯把 function 用在那些需要反复计算的组合逻辑上。比如计算奇偶校验位:

function parity;
    input [7:0] data;
    integer i;
    begin
        parity = 1'b0;
        for (i = 0; i < 8; i = i + 1) begin
            parity = parity ^ data[i];
        end
    end
endfunction

// 使用
wire odd_parity = parity(tx_data);

注意,function 里不能有 always 块,也不能有 @(posedge clk)。它天生就是组合逻辑。如果你在 function 里写了时序控制,仿真器会报错——我当年第一次写 function 时就踩过这个坑。

2.2.2 task:仿真中的瑞士军刀

task 比 function 灵活得多。它可以在仿真中模拟复杂的总线时序。比如模拟一个 SPI 写操作:

task spi_write;
    input [7:0] addr;
    input [7:0] data;
    output      ack;
    begin
        cs_n = 1'b0;
        sclk = 1'b0;
        // 发送地址
        for (int i = 7; i >= 0; i--) begin
            mosi = addr[i];
            #10 sclk = 1'b1;
            #10 sclk = 1'b0;
        end
        // 发送数据
        for (int i = 7; i >= 0; i--) begin
            mosi = data[i];
            #10 sclk = 1'b1;
            #10 sclk = 1'b0;
        end
        cs_n = 1'b1;
        ack = 1'b1;
    end
endtask

你想想看,如果没有 task,每次写 SPI 都要复制粘贴这一大段 #10 的代码,多痛苦。

重要提醒: task 在综合时要格外小心。带有时序控制的 task 通常不可综合。只有纯组合逻辑的 task 才能被综合工具接受。所以我的原则是:设计代码用 function,仿真代码用 task。

2.3 多维数组与结构体:让数据组织更清晰

说实话,Verilog 的数组和结构体支持一直比较弱。但 SystemVerilog 大大改善了这一点。不过咱们今天还是先讲 Verilog 本身能用的东西。

2.3.1 多维数组

Verilog 支持一维数组,比如 reg [7:0] mem [0:255] 就是一个 256 字节的存储器。但二维数组呢?

标准 Verilog 其实不支持真正的二维数组作为端口。但我们可以用「数组的数组」来模拟。比如一个 8×8 的寄存器文件:

reg [7:0] regfile [0:7][0:7];  // 8行,每行8位

// 访问第3行第5列
regfile[3][5] <= 8'hA5;

不过要注意,这种写法在有些综合工具里可能不支持。我建议你查一下目标工具的用户手册。我在一个项目中用过这种写法,结果换了个综合工具就报错了——嗯,从那以后我尽量用一维数组加地址计算来替代。

2.3.2 结构体(SystemVerilog)

如果你用的是 SystemVerilog(现在大多数新项目都是),那结构体是你的好伙伴。比如描述一个 AXI 总线的事务:

typedef struct packed {
    logic        valid;
    logic [31:0] addr;
    logic [2:0]  burst;
    logic [3:0]  len;
    logic [1:0]  size;
} axi_ar_chan_t;

axi_ar_chan_t ar_chan;

// 赋值
ar_chan.valid = 1'b1;
ar_chan.addr  = 32'h1000_0000;
ar_chan.burst = 3'b001;
ar_chan.len   = 4'd7;  // 8次传输
ar_chan.size  = 2'b10; // 4字节

结构体的好处是:你把相关的信号打包在一起,代码可读性瞬间提升一个档次。而且 packed 结构体可以直接作为寄存器或线网类型,综合工具也能处理。

我的建议: 在模块接口中尽量使用结构体。比如一个 AXI 从机接口,用结构体打包后,模块端口从几十个缩减到几个。代码清爽多了,而且不容易漏连信号。

2.4 本章知识体系

下面这张图总结了本章的核心知识点和它们之间的关系。你可以把它当作一个「思维导图」来用。

Verilog 进阶语法 generate 语句 generate for:批量例化 generate if/case:条件编译 function 与 task function:组合逻辑 task:仿真时序 多维数组与结构体 多维数组:存储器 结构体:打包接口 核心原则: 减少重复代码 · 提高可读性 · 注意可综合性

这张图把三个知识点串起来了。你看,generate 解决的是「重复例化」的问题,function/task 解决的是「重复逻辑」的问题,而多维数组和结构体解决的是「数据组织」的问题。三者合在一起,你的代码就能从「能跑」变成「优雅」。

好了,今天的内容就到这里。记住,这些语法不是用来炫技的,而是用来解决实际问题的。下次写代码时,多想想:这段代码能不能用 generate 简化?这个计算能不能写成 function?这些信号能不能打包成结构体?

养成这个习惯,你的 RTL 设计水平会上一个台阶。

课后练习: 尝试用 generate for 实现一个参数化的桶形移位器(barrel shifter),并用 function 计算移位后的结果。写完后对比两种方式的代码量和可读性。

可可精品资料库