4. Gensys基础语法:模型定义、参数化、层次化设计、接口定义、事务级建模(TLM)

好,咱们进入正题。Gensys 这门语言,说白了就是用来搭硬件模型的。你把它想象成乐高积木——Gensys 就是那本说明书,告诉你每个积木长什么样、怎么拼、怎么跟别的积木通信。我个人习惯把这一章叫做「地基」,因为后面所有复杂的系统级验证,都建立在今天这几个概念之上。

核心要点: Gensys 的模型定义是骨架,参数化是灵活性,层次化是组织能力,接口是通信管道,TLM 是效率灵魂。

4.1 模型定义:一切从 module 开始

在 Gensys 里,定义一个模型用 module 关键字。嗯,这里要注意,它跟 Verilog 的 module 有点像,但更偏向于事务级抽象。你不需要关心底层信号怎么跳变,只需要描述这个模块「做什么」。

// 一个简单的加法器模型
module adder {
    // 端口声明
    input  int a;
    input  int b;
    output int sum;

    // 行为描述
    void run() {
        sum = a + b;
    }
}

你看,这个模型只有三个端口和一个行为函数。我在项目中遇到过很多新手,一上来就想把时序细节写进去。其实没必要——Gensys 的模型定义阶段,你只需要定义清楚输入输出和核心功能。时序?那是后面仿真器的事。

我的小技巧: 模型定义时,尽量让每个 module 只做一件事。比如加法器就只做加法,不要在里面混入 FIFO 控制逻辑。这样后期复用起来特别爽。

4.2 参数化:让模型活起来

参数化是什么?说白了就是给模型留「旋钮」。你想想看,如果每个加法器都写死成 32 位,那遇到 64 位的场景就得重写一个。太蠢了。

Gensys 用 param 关键字来定义参数。我习惯把参数分为两类:结构参数(比如位宽、深度)和行为参数(比如延迟周期数)。

module adder #(
    param int WIDTH = 32
) {
    input  bit [WIDTH-1:0] a;
    input  bit [WIDTH-1:0] b;
    output bit [WIDTH-1:0] sum;

    void run() {
        sum = a + b;
    }
}

// 实例化时指定参数
adder #(16) my_adder_16bit;
adder #(64) my_adder_64bit;

我曾经犯过一个错误:把所有参数都塞到一个大结构体里。结果后来想改一个 FIFO 深度,得翻遍整个文件。现在我学乖了——参数要「就近定义」,跟它相关的模块放在一起。

避坑指南: 参数化虽然灵活,但别过度。我见过有人把加法器的进位链延迟也做成参数——说实话,这种细节在事务级建模里根本不需要关心。参数化要把握一个度:只参数化那些在系统集成时确实会变化的东西。

4.3 层次化设计:搭积木的艺术

层次化设计,说白了就是「大模块套小模块」。Gensys 支持任意深度的嵌套,你可以在一个 module 里实例化其他 module,就像在顶层文件里调用子模块一样。

module alu {
    // 子模块实例化
    adder #(32) add_unit;
    multiplier #(32) mul_unit;
    comparator #(32) cmp_unit;

    // 内部连线
    connect add_unit.a = alu_op_a;
    connect add_unit.b = alu_op_b;
    connect mul_unit.a = alu_op_a;
    connect mul_unit.b = alu_op_b;

    // 选择输出
    void run() {
        case (alu_opcode) {
            ADD: alu_result = add_unit.sum;
            MUL: alu_result = mul_unit.product;
            CMP: alu_result = cmp_unit.result;
        }
    }
}

这里有个关键点:connect 语句。它不像 Verilog 那样用 wire 连,而是用 connect 做逻辑绑定。我个人觉得这种方式更清晰——你一眼就能看出哪个信号连到哪个端口。

层次化设计的好处是什么?我举个例子。有一次我们做 CPU 验证,需要替换 ALU 的实现。因为用了层次化,我只需要在顶层把 alu 模块换成新版本,所有连线自动生效。如果当初把所有逻辑写在一个大模块里……嗯,那画面太美我不敢看。

4.4 接口定义:通信的契约

接口,就是模块之间通信的「合同」。Gensys 用 interface 关键字定义接口,它把一组相关的信号打包在一起。这样做的好处是:你不需要在模块端口里列出一大堆信号,只需要声明一个接口类型。

interface bus_if {
    bit [31:0] address;
    bit [31:0] data;
    bit        read_en;
    bit        write_en;
    bit        ready;
}

module memory {
    port bus_if bus;  // 使用接口作为端口

    void run() {
        if (bus.read_en && bus.ready) {
            bus.data = mem[bus.address];
        }
        if (bus.write_en && bus.ready) {
            mem[bus.address] = bus.data;
        }
    }
}

我在项目中遇到过最头疼的事:接口定义不统一。A 模块用 valid/ready 握手,B 模块用 req/ack,结果集成的时候全是胶水逻辑。所以我建议:项目一开始就定好接口规范,所有模块统一使用同一套接口定义。

经验之谈: 接口定义时,尽量包含握手信号。比如上面的 ready 信号,它让模块可以告诉对方「我现在忙,等会儿再试」。没有握手信号的接口,在事务级建模里很容易死锁。

4.5 事务级建模(TLM):从信号到事务

TLM 是 Gensys 最核心的概念。传统 RTL 仿真里,你关心每个时钟沿信号怎么跳变。但在 TLM 里,你关心的是「事务」——比如一次读操作、一次写操作、一个数据包。事务是一个完整的操作,它包含地址、数据、控制信息等。

Gensys 用 transaction 关键字定义事务类型,用 put/get 操作在模块之间传递事务。

// 定义事务类型
transaction mem_req_t {
    bit [31:0] addr;
    bit [31:0] data;
    bit        is_write;
}

// 发起端
module cpu {
    port put_port<mem_req_t> mem_port;

    void run() {
        mem_req_t req;
        req.addr = 0x1000;
        req.data = 0xDEAD;
        req.is_write = 1;
        mem_port.put(req);  // 发送事务
    }
}

// 接收端
module memory {
    port get_port<mem_req_t> mem_port;

    void run() {
        mem_req_t req;
        mem_port.get(req);  // 接收事务
        if (req.is_write) {
            store(req.addr, req.data);
        }
    }
}

为什么要用 TLM?说白了就是快。传统 RTL 仿真里,一次总线写操作可能要模拟几十个时钟周期。但在 TLM 里,一次 put 调用就搞定了。我做过对比:同样的 SoC 验证场景,TLM 仿真比 RTL 仿真快 100 倍以上。

不过要注意,TLM 不是万能的。它适合做功能验证和性能评估,但不适合做时序验证。你想想看,TLM 里根本没有时钟的概念,怎么可能检查建立时间?所以我的做法是:先用 TLM 做快速功能验证,等基本功能没问题了,再切到 RTL 做精细时序验证。

本章小结: Gensys 的这五个基础语法,构成了一个完整的建模体系。模型定义给你骨架,参数化给你灵活性,层次化给你组织能力,接口定义给你通信规范,TLM 给你仿真效率。把这五个点吃透了,后面学系统级建模就会轻松很多。
Gensys 基础语法体系 模型定义 module / 端口 / 行为 参数化 param / 位宽 / 深度 层次化设计 实例化 / connect 接口定义 interface / 握手 事务级建模 transaction / put/get 五个基础语法构成完整的建模体系 骨架 → 灵活性 → 组织 → 通信 → 效率
我的建议: 学这章的时候,别光看代码。打开 Gensys 环境,亲手敲一遍。把加法器改成减法器,把参数从 32 改成 64,把接口从单向改成双向。只有亲手改过,你才能真正理解这些语法为什么这么设计。
专注资料整理