第四章:复杂场景建模

场景图的构建:把复杂关系画出来

做验证这么多年,我最大的体会就是——场景越复杂,越需要一张图。你想想看,一个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是血液,把具体的数据和操作注入到路径中。三者缺一不可。

本章核心要点:

  • 场景图用节点和边描述激励空间,权重控制随机分布
  • 层次状态机比线性序列更适合复杂场景,但层数不要超过两层
  • TLM事务的粒度遵循“一次协议交互”原则,太细太粗都不好
  • 三者结合:场景图→状态机→TLM,形成完整的激励生成链路

好了,这一章的内容就到这里。下一章我们会深入讨论如何用SystemVerilog的随机化约束来实现这些场景图,到时候我会分享一些我在实际项目中用过的“约束黑魔法”。

可可精品资料库