4. Gensys基础语法:模型定义、参数化、层次化设计、接口定义、事务级建模(TLM)
好,咱们进入正题。Gensys 这门语言,说白了就是用来搭硬件模型的。你把它想象成乐高积木——Gensys 就是那本说明书,告诉你每个积木长什么样、怎么拼、怎么跟别的积木通信。我个人习惯把这一章叫做「地基」,因为后面所有复杂的系统级验证,都建立在今天这几个概念之上。
4.1 模型定义:一切从 module 开始
在 Gensys 里,定义一个模型用 module 关键字。嗯,这里要注意,它跟 Verilog 的 module 有点像,但更偏向于事务级抽象。你不需要关心底层信号怎么跳变,只需要描述这个模块「做什么」。
// 一个简单的加法器模型
module adder {
// 端口声明
input int a;
input int b;
output int sum;
// 行为描述
void run() {
sum = a + b;
}
}
你看,这个模型只有三个端口和一个行为函数。我在项目中遇到过很多新手,一上来就想把时序细节写进去。其实没必要——Gensys 的模型定义阶段,你只需要定义清楚输入输出和核心功能。时序?那是后面仿真器的事。
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 做精细时序验证。