3. Verilog基础语法(下):结构化建模、任务与函数、编译预处理

好,咱们接着聊。上一节我们把Verilog的“词”和“句”讲清楚了,这一节要聊的是“段落”和“章节”——也就是怎么把零散的代码组织成有层次、可复用的硬件描述。

结构化建模,说白了就是三种描述硬件的“视角”。你想想看,描述一个加法器,你可以用一个个与门或门搭出来(门级),也可以用assign连续赋值写个表达式(数据流级),还可以用always块描述它的行为(行为级)。这三种方式,我在项目里都见过,也踩过坑。

核心观点:三种建模方式不是互斥的,而是互补的。一个成熟的RTL设计,往往是行为级控制逻辑 + 数据流级运算路径 + 门级关键路径优化的混合体。

3.1 结构化建模的三种层次

3.1.1 门级建模(Gate-Level)

门级建模是最底层的描述方式。你直接实例化标准单元库里的与门、或门、非门、缓冲器。嗯,这里要注意,门级建模在现在的ASIC流程里,基本是综合工具帮你做的。但为什么还要学?

我个人习惯在两种场景下用手写门级:一是做FPGA里的小型快速进位链,二是做后仿真的网表调试。你想想看,当综合后的网表有几十万行,你一眼看出某个与门的输入接错了,那感觉——比在行为级代码里找bug快多了。

// 门级建模示例:半加器
module half_adder (
    input  a, b,
    output sum, cout
);
    xor u_xor (sum, a, b);   // 异或门
    and u_and (cout, a, b);  // 与门
endmodule

这里每个门都是一个实例,u_xoru_and是实例名。我曾经在调试一个老项目时,发现设计者把异或门和与门的输入顺序写反了——虽然功能没错,但后仿时序收敛不了。所以,门级建模的连线顺序一定要和端口声明一致。

3.1.2 数据流级建模(Dataflow)

数据流级用assign连续赋值语句,描述信号之间的组合逻辑关系。说白了,就是写布尔表达式。

// 数据流级建模:半加器
module half_adder_df (
    input  a, b,
    output sum, cout
);
    assign sum  = a ^ b;
    assign cout = a & b;
endmodule

这种方式可读性比门级好太多了。我在项目中,80%的组合逻辑都用数据流级描述。但有个坑——assign语句的右边不能出现寄存器变量,否则综合会报错。我曾经有个同事,在assign里用了一个always块里的reg变量,查了半天才发现。

3.1.3 行为级建模(Behavioral)

行为级用always块,配合if-elsecase等语句,描述电路的行为。这是最接近软件思维的描述方式,也是新手最容易上手、也最容易出错的。

// 行为级建模:半加器(其实没必要,但为了演示)
module half_adder_bh (
    input  a, b,
    output reg sum, cout
);
    always @(*) begin
        case ({a, b})
            2'b00: begin sum = 0; cout = 0; end
            2'b01: begin sum = 1; cout = 0; end
            2'b10: begin sum = 1; cout = 0; end
            2'b11: begin sum = 0; cout = 1; end
        endcase
    end
endmodule

行为级建模最大的陷阱是——你以为在写软件,其实在画电路。比如always @(posedge clk)里漏了else分支,综合工具会给你生成一个锁存器。我刚开始做设计时,就因为这个被 mentor 骂过。

我的建议:组合逻辑用always @(*),时序逻辑用always @(posedge clk)。别混用,别偷懒。每个always块只描述一个功能,这样调试时能快速定位问题。

3.2 任务(Task)与函数(Function)

任务和函数,是Verilog里用来封装可重用代码的机制。很多初学者分不清它们,我当年也迷糊过。其实区别很简单:

特性 函数(function) 任务(task)
返回值 必须有,且只有一个 无返回值,通过output/input传递
时序控制 不能包含#、@、wait等 可以包含时序控制
调用方式 作为表达式的一部分 作为独立的语句
可综合性 纯组合逻辑可综合 通常不可综合(用于仿真)

函数适合做纯组合逻辑的运算,比如计算奇偶校验、数据编码。任务适合做仿真中的激励生成、波形打印等。

// 函数示例:计算奇偶校验位
function parity;
    input [7:0] data;
    begin
        parity = ^data;  // 异或归约,返回1表示奇数个1
    end
endfunction

// 调用
wire p = parity(8'b1010_1011);
// 任务示例:生成仿真激励
task gen_stimulus;
    input [7:0] addr;
    input [31:0] data;
    begin
        @(posedge clk);
        addr_bus <= addr;
        data_bus <= data;
        we <= 1;
        @(posedge clk);
        we <= 0;
    end
endtask

注意:函数里不能调用任务,但任务里可以调用函数。另外,函数默认是1位返回值,如果要返回多位,必须在声明时指定范围,比如function [7:0] my_func;

3.3 编译预处理

编译预处理指令,以反引号`开头。它们不是Verilog语法的一部分,而是编译器在编译前处理的文本替换。我常用的有这几个:

  • `define:宏定义,类似C语言的#define
  • `include:文件包含
  • `ifdef / `ifndef / `else / `endif:条件编译
  • `timescale:时间单位和精度
// 宏定义示例
`define DATA_WIDTH 32
`define SIM_MODE

reg [`DATA_WIDTH-1:0] data_bus;

`ifdef SIM_MODE
    initial $monitor("data = %h", data_bus);
`endif

我在项目中,经常用`ifdef来做仿真和综合的代码切换。比如仿真时打印调试信息,综合时把这些代码“剪掉”。但要注意——宏定义是全局的,不同文件之间可能会冲突。我建议把宏定义集中放在一个头文件里,用`include统一管理。

一个小技巧:`timescale 1ns/1ps定义仿真时间精度。1ns是时间单位,1ps是精度。精度不能比单位更粗,否则仿真器会报错。我习惯用1ns/1ps,既能满足大多数场景,又不会让仿真跑得太慢。

3.4 本章知识体系

为了让你更直观地理解这三部分内容的关系,我画了一张图:

Verilog基础语法(下)知识体系 结构化建模 • 门级建模 实例化标准单元 • 数据流级建模 assign连续赋值 • 行为级建模 always块 + 过程赋值 三种层次互补,混合使用 任务与函数 • 函数(function) 有返回值,纯组合 • 任务(task) 无返回值,可含时序 函数可综合,任务通常不可 任务可调用函数 编译预处理 • `define 宏定义 • `include 文件包含 • `ifdef 条件编译 • `timescale 时间精度 预处理在编译前完成 宏定义全局有效 三者结合:行为级控流程 + 数据流算逻辑 + 预处理做配置

这张图把本章的三个核心模块串起来了。结构化建模是“骨架”,任务与函数是“肌肉”,编译预处理是“皮肤”——三者结合,才能写出可维护、可复用的Verilog代码。

好了,这一节的内容就到这里。记住,写Verilog不是写C语言,你脑子里要时刻有一张电路图。下一节我们会聊到组合逻辑和时序逻辑的建模技巧,那才是真正的实战环节。


专注资料整理