4. Transaction与Sequence:掌握uvm_sequence_item的定义,以及sequence如何生成并发送transaction
好,咱们今天聊聊UVM验证平台里最核心的两个概念——Transaction和Sequence。说白了,整个验证平台就是在跟数据打交道,而Transaction就是数据的载体,Sequence就是数据的生产者。这两个搞明白了,UVM就算入门一半了。
4.1 什么是Transaction?为什么需要它?
Transaction,翻译过来叫“事务”或“数据包”。在UVM的世界里,它代表了一次完整的数据交换。比如一个AXI写操作、一个APB读操作、一个以太网帧,都可以抽象成一个Transaction。
我个人习惯把Transaction想象成一个“快递包裹”。包裹里装着地址、数据、控制信号等所有信息。driver拿到这个包裹,就知道该怎么驱动接口信号;monitor抓到接口上的信号,也能打包成一个Transaction送给reference model。
核心要点:Transaction是验证平台中数据流动的最小单元。所有组件之间的通信,本质上都是在传递Transaction。
4.2 定义自己的uvm_sequence_item
UVM里,所有的Transaction都必须从uvm_sequence_item派生。为什么?因为uvm_sequence_item提供了很多好用的机制,比如自动化的复制、比较、打印、记录等等。
来看一个简单的例子。假设我们要验证一个SPI接口的IP,先定义一个SPI的Transaction:
class spi_transaction extends uvm_sequence_item;
// 声明字段
rand bit [7:0] data;
rand bit [7:0] addr;
rand bit write_en;
rand int delay_cycles;
// 约束
constraint c_delay {
delay_cycles inside {[0:15]};
}
// 工厂注册
`uvm_object_utils_begin(spi_transaction)
`uvm_field_int(data, UVM_ALL_ON)
`uvm_field_int(addr, UVM_ALL_ON)
`uvm_field_int(write_en, UVM_ALL_ON)
`uvm_field_int(delay_cycles, UVM_ALL_ON)
`uvm_object_utils_end
// 构造函数
function new(string name = "spi_transaction");
super.new(name);
endfunction
endclass
嗯,这里要注意几个关键点:
- rand关键字:让字段可以随机化。这是验证的核心——用随机来覆盖各种场景。
- 约束块:控制随机值的范围。我在项目中遇到过,如果不加约束,delay_cycles可能随机出很大的值,导致仿真跑半天都跑不完一个transaction。
- 字段宏:
`uvm_field_*宏自动实现了copy、compare、print、record等方法。省去了手写这些函数的麻烦。
我的建议:刚开始学UVM时,很多人觉得字段宏可有可无。但等你需要调试、需要对比两个transaction是否相等时,就知道它的好了。我建议所有自定义的transaction都加上字段宏。
4.3 Sequence是什么?它怎么生成Transaction?
Sequence,直译是“序列”。它的任务就是生成一串Transaction,然后发给driver去执行。
你想想看,如果每次都要手动创建Transaction、设置字段、再发送,那得多累?Sequence就是帮我们自动化这个过程的。它可以从一个简单的循环开始,也可以包含复杂的随机策略和时序控制。
看一个最简单的sequence:
class spi_simple_seq extends uvm_sequence #(spi_transaction);
`uvm_object_utils(spi_simple_seq)
function new(string name = "spi_simple_seq");
super.new(name);
endfunction
virtual task body();
spi_transaction tr;
repeat(10) begin
tr = spi_transaction::type_id::create("tr");
start_item(tr);
if (!tr.randomize()) begin
`uvm_error("RAND_FAIL", "Randomization failed!")
end
finish_item(tr);
end
endtask
endclass
这段代码里,最关键的就是body()任务里的三步走:
- 创建:用factory创建transaction对象。
- start_item:告诉sequencer“我要开始发送一个transaction了”。sequencer会做仲裁,决定哪个sequence先发。
- randomize + finish_item:随机化transaction的字段,然后发送给driver。
我曾经踩过的坑:一开始我总忘记在start_item和finish_item之间做randomize。结果发出去的transaction全是默认值,仿真跑得飞快但啥也没测到。记住:randomize一定要放在start_item之后、finish_item之前。
4.4 更复杂的Sequence:带约束和时序控制
实际项目中,sequence不会这么简单。我们经常需要生成特定场景的transaction。比如,要测试SPI写操作,可以这样:
class spi_write_seq extends uvm_sequence #(spi_transaction);
`uvm_object_utils(spi_write_seq)
function new(string name = "spi_write_seq");
super.new(name);
endfunction
virtual task body();
spi_transaction tr;
tr = spi_transaction::type_id::create("tr");
start_item(tr);
// 约束写操作
tr.write_en.constraint_mode(1);
tr.write_en = 1;
tr.addr.rand_mode(1);
tr.data.rand_mode(1);
if (!tr.randomize()) begin
`uvm_error("RAND_FAIL", "Randomization failed!")
end
finish_item(tr);
endtask
endclass
这里我用了constraint_mode和rand_mode来控制哪些字段随机、哪些固定。说白了,就是让sequence可以灵活地组合出各种测试场景。
4.5 Sequence的层次化:嵌套与复用
UVM的sequence还有一个很强大的特性——可以嵌套。一个sequence可以启动另一个sequence。这在实际项目中非常有用。
举个例子,我们可以先定义一个“写一个寄存器”的sequence,再定义一个“配置所有寄存器”的sequence,后者循环调用前者:
class spi_cfg_all_regs_seq extends uvm_sequence #(spi_transaction);
`uvm_object_utils(spi_cfg_all_regs_seq)
virtual task body();
spi_write_seq write_seq;
// 配置寄存器0x00
`uvm_do_with(write_seq, {addr == 8'h00; data == 8'hA5;})
// 配置寄存器0x01
`uvm_do_with(write_seq, {addr == 8'h01; data == 8'h5A;})
// 配置寄存器0x02
`uvm_do_with(write_seq, {addr == 8'h02; data == 8'hFF;})
endtask
endclass
你看,用`uvm_do_with宏,一行代码就完成了创建、随机化、发送的全过程。而且可以内联约束,非常方便。
我的习惯:在项目中,我会把常用的操作封装成小的sequence,比如“写寄存器”、“读寄存器”、“等待中断”。然后上层sequence像搭积木一样组合它们。这样既提高了复用性,也让测试用例的代码更清晰。
4.6 总结一下关键点
| 概念 | 作用 | 关键方法/宏 |
|---|---|---|
| uvm_sequence_item | 定义数据包的结构和约束 | 字段宏、constraint、rand |
| uvm_sequence | 生成并发送transaction | body()、start_item()、finish_item() |
| 嵌套sequence | 组合复用已有sequence | `uvm_do、`uvm_do_with |
最后说一句,Transaction和Sequence是UVM验证平台的基石。你花时间把它们搞透,后面的driver、monitor、scoreboard学起来就会轻松很多。下一章,咱们就聊聊driver怎么接收sequence发来的transaction,并驱动到接口上。