第四章:复杂场景建模
场景图的构建:把复杂关系画出来
做验证这么多年,我最大的体会就是——场景越复杂,越需要一张图。你想想看,一个SoC里几十个IP同时工作,各种中断、DMA传输、总线抢占搅在一起,光靠脑子想,迟早会漏掉某个边界情况。
场景图(Scenario Graph)说白了,就是把所有可能的激励序列画成一张有向图。每个节点代表一个状态或一个操作,每条边代表允许的转换。我在项目中遇到过最典型的例子:验证一个GPU的上下文切换。如果不画场景图,你根本不知道多少个上下文同时切换才会触发死锁。
场景图的核心三要素:
- 节点(Node):表示一个具体的操作或状态,比如“发起读请求”、“等待响应”、“超时处理”
- 边(Edge):表示节点间的合法转换,可以带条件(如“data_ready==1”)
- 权重(Weight):表示该路径被选中的概率,用于约束随机激励的分布
嗯,这里要注意:场景图不是状态机。状态机描述的是设计的行为,场景图描述的是验证者想施加的激励行为。说白了,状态机是DUT的,场景图是你的。
我习惯用YAML来描述场景图,然后自动生成UVM序列。举个例子:
# 一个简单的DMA读写场景图
scenario_graph:
name: "dma_rw_scenario"
nodes:
- id: 1
name: "IDLE"
action: "wait_cycle(10)"
- id: 2
name: "DMA_READ"
action: "send_dma_read_req()"
- id: 3
name: "DMA_WRITE"
action: "send_dma_write_req()"
- id: 4
name: "CHECK"
action: "check_completion()"
edges:
- from: 1
to: 2
condition: "rand_weight(70)"
- from: 1
to: 3
condition: "rand_weight(30)"
- from: 2
to: 4
condition: "dma_done==1"
- from: 3
to: 4
condition: "dma_done==1"
- from: 4
to: 1
condition: "always"
这样写的好处是,场景和代码分离。验证工程师只需要维护YAML,不需要改UVM代码。我曾经在一个项目里用这种方式,把场景覆盖率从65%提到了92%,而且只花了两周时间。
小技巧:场景图中的节点不要超过20个。超过20个,图就变得难以维护。这时候应该拆成子图,用层次化的方式组织。
状态机与序列:从单步到多步的进化
状态机(FSM)大家都很熟,但验证中的状态机和设计中的状态机是两码事。设计中的FSM描述的是硬件行为,验证中的FSM描述的是激励的推进逻辑。
我刚开始做验证时,总喜欢把序列写成线性的:先发A,再发B,再发C。后来发现,真实场景下,A和B可能同时发生,C可能在A完成之前就触发了。线性序列根本覆盖不到这些情况。
所以,我推荐用层次状态机(HSM)来建模序列。举个例子:
// 一个简单的HSM序列模型
class hsm_sequence extends uvm_sequence;
// 状态定义
typedef enum {IDLE, CONFIG, DATA_XFER, CHECK, DONE} state_t;
state_t current_state;
// 层次化子状态
typedef enum {SINGLE, BURST, STREAM} xfer_mode_t;
xfer_mode_t xfer_mode;
virtual task body();
current_state = IDLE;
while(current_state != DONE) begin
case(current_state)
IDLE: begin
`uvm_do(init_seq)
current_state = CONFIG;
end
CONFIG: begin
`uvm_do(cfg_seq)
current_state = DATA_XFER;
end
DATA_XFER: begin
// 根据模式选择子序列
case(xfer_mode)
SINGLE: `uvm_do(single_xfer_seq)
BURST: `uvm_do(burst_xfer_seq)
STREAM: `uvm_do(stream_xfer_seq)
endcase
current_state = CHECK;
end
CHECK: begin
`uvm_do(check_seq)
current_state = DONE;
end
endcase
end
endtask
endclass
为什么用HSM?因为真实场景中,状态转换往往不是线性的。比如在DATA_XFER状态下,如果检测到错误,可能需要回退到CONFIG状态重新配置。这种回退逻辑,用线性序列写起来非常痛苦,但用HSM就很简单——加一条回退边就行。
我曾经踩过的坑:在一个PCIe验证项目中,我用了三层嵌套的HSM,结果状态爆炸了——总共48个状态,200多条转换边。调试时根本搞不清当前在哪个状态。后来我强制规定:HSM不超过两层,每层不超过8个状态。超过就拆成独立的序列,用sequence仲裁来组合。
事务级建模(TLM):让激励更抽象
TLM(Transaction Level Modeling)这个概念,最早是SystemC里提出的。说白了,就是把底层的信号级操作,封装成更高层次的事务。比如,你不需要关心AXI总线上每个时钟周期的信号变化,你只需要关心“发一次读事务”和“收一次读响应”。
在UVM中,TLM的核心是事务对象(Transaction Object)和TLM端口(TLM Port)。我习惯把事务对象设计成可约束的,这样随机化时就能自动生成各种边界情况。
// 一个带约束的TLM事务对象
class dma_transaction extends uvm_sequence_item;
rand bit [31:0] src_addr;
rand bit [31:0] dst_addr;
rand int length; // 传输长度,单位字节
rand bit is_write; // 0:读, 1:写
rand bit [3:0] burst_type; // 0:固定, 1:增量, 2:回环
// 约束:地址对齐
constraint addr_align {
src_addr[1:0] == 2'b00;
dst_addr[1:0] == 2'b00;
}
// 约束:长度范围
constraint length_range {
length inside {[1:1024]};
}
// 约束:写操作时burst_type不能为回环
constraint write_burst {
if (is_write) burst_type != 2;
}
`uvm_object_utils_begin(dma_transaction)
`uvm_field_int(src_addr, UVM_ALL_ON)
`uvm_field_int(dst_addr, UVM_ALL_ON)
`uvm_field_int(length, UVM_ALL_ON)
`uvm_field_int(is_write, UVM_ALL_ON)
`uvm_field_int(burst_type, UVM_ALL_ON)
`uvm_object_utils_end
endclass
你想想看,如果没有TLM,你要怎么验证DMA的地址对齐错误?你得手动构造一个非对齐的地址,然后盯着波形看DUT有没有报错。有了TLM,你只需要在约束里加一条src_addr[1:0] != 2'b00,随机化时自然就会生成非对齐的地址。
TLM的另一个好处是可组合性。你可以把多个小事务组合成一个复杂场景。比如:
// 组合多个TLM事务形成复杂场景
class complex_scenario extends uvm_sequence;
`uvm_object_utils(complex_scenario)
virtual task body();
dma_transaction t1, t2, t3;
// 场景1:连续写,然后读回验证
repeat(10) begin
`uvm_do_with(t1, {is_write == 1; length == 64;})
end
`uvm_do_with(t2, {is_write == 0; src_addr == t1.dst_addr;})
// 场景2:写操作中插入中断
fork
begin
`uvm_do_with(t3, {is_write == 1; length == 256;})
end
begin
#100ns;
`uvm_do(interrupt_seq)
end
join
endtask
endclass
我个人习惯把TLM事务分成三类:
| 事务类型 | 粒度 | 典型例子 |
|---|---|---|
| 原子事务 | 最小操作单元 | 一次AXI读、一次寄存器写 |
| 复合事务 | 多个原子事务组合 | DMA传输(含配置+数据+中断) |
| 场景事务 | 多个复合事务组合 | 多通道DMA并发+中断嵌套 |
经验之谈:TLM的粒度不是越细越好。太细了,序列写起来像在拼乐高,一个场景要几十行代码。太粗了,又失去了灵活性。我一般遵循“一个事务对应一次协议交互”的原则。比如AXI,一次读事务就对应一次AR通道+一次R通道的交互。
本章知识体系总览
说了这么多,咱们用一张图来总结一下本章的核心逻辑:
这张图把本章的三个核心内容串起来了。你想想看,场景图是骨架,定义了所有可能的路径;状态机是肌肉,控制着路径的推进逻辑;TLM是血液,把具体的数据和操作注入到路径中。三者缺一不可。
本章核心要点:
- 场景图用节点和边描述激励空间,权重控制随机分布
- 层次状态机比线性序列更适合复杂场景,但层数不要超过两层
- TLM事务的粒度遵循“一次协议交互”原则,太细太粗都不好
- 三者结合:场景图→状态机→TLM,形成完整的激励生成链路
好了,这一章的内容就到这里。下一章我们会深入讨论如何用SystemVerilog的随机化约束来实现这些场景图,到时候我会分享一些我在实际项目中用过的“约束黑魔法”。