第二章:验证环境总览——UVM验证平台架构、组件划分与目录结构

各位同学,大家好。今天我们来聊聊验证环境的整体框架。说实话,很多新人一上来就扎进代码细节里,结果写了半天,连自己写的组件该放哪个目录都搞不清楚。我个人习惯是,先搭好架子,再填内容。就像盖房子,你得先有图纸,对吧?

2.1 UVM验证平台架构——从宏观到微观

UVM验证平台,说白了就是一套标准化的“积木”。你想想看,我们做存储控制器验证,无非就是产生激励、驱动到DUT、监测响应、最后比对结果。UVM把这套流程抽象成了几个核心组件。

我画了一张图,帮你快速建立整体认知:

UVM验证平台架构总览 test(测试用例层) base_test / nvme_test / ddr_test ... env(验证环境层) 包含:scoreboard、coverage collector、reference model 以及多个agent实例 agent(代理层) 每个接口对应一个agent,如:axi_agent、nvme_agent sequencer driver monitor interface(接口层) virtual interface连接DUT与driver/monitor DUT(待测设计) test层 env层 agent层 interface层 DUT层

这张图里,从上到下依次是test、env、agent、interface和DUT。每一层各司其职。test负责“做什么”,env负责“怎么组织”,agent负责“怎么驱动和监测”,interface负责“怎么连到DUT”。

核心要点:UVM验证平台的核心思想就是“分层解耦”。每一层只关心自己的事,层与层之间通过TLM端口通信。这样做的好处是——换一个DUT接口,你只需要换对应的agent,其他组件基本不用动。

2.2 验证环境组件划分——每个组件都是“螺丝钉”

好,架构看完了。我们来拆解一下每个组件具体干什么活。我在项目中遇到过很多新人,把driver和monitor写成了“双胞胎”,代码几乎一模一样。其实没必要,它们职责完全不同。

组件名称 主要职责 我在项目中的经验
test 配置验证环境、启动sequence、控制仿真流程 我习惯把test写得尽量“薄”,只做配置和启动。具体的激励生成交给sequence。
env 实例化并连接所有子组件(agent、scoreboard等) env是“包工头”,它不干活,但知道谁该干什么。我一般会在env里做config_db的配置。
agent 封装sequencer、driver、monitor,提供统一接口 agent分active和passive两种模式。active模式驱动激励,passive模式只监测。这个设计很巧妙。
sequencer 管理sequence的调度,向driver发送transaction sequencer说白了就是个“调度器”。我见过有人把sequence逻辑写进sequencer里,这是不对的。
driver 将transaction转换为接口时序,驱动到DUT driver是“翻译官”,把高层的事务翻译成底层的信号跳变。注意时序收敛。
monitor 监测接口信号,收集transaction并发送给scoreboard monitor只“看”不“动”。我曾经犯过一个错,在monitor里加了驱动逻辑,结果仿真波形乱成一团。
scoreboard 比对预期结果与实际结果 scoreboard是“裁判”。我建议用reference model生成预期值,而不是手动计算。
coverage collector 收集功能覆盖率信息 覆盖率是验证的“进度条”。没有覆盖率,你都不知道自己测了百分之多少。

小提示:agent的active/passive模式切换,我一般通过config_db传一个参数is_active来实现。这样同一个agent代码,既能当master用,也能当slave用,复用性很高。

2.3 验证环境的目录结构——好记性不如烂笔头,好项目不如好目录

嗯,这里要重点说一下。很多团队的项目,代码写得很漂亮,但目录结构一塌糊涂。你想想看,一个存储控制器项目,动辄几十万行代码,没有清晰的目录结构,找文件就像大海捞针。

我个人习惯的目录结构是这样的:

project_root/
├── sim/                    # 仿真运行目录
│   ├── run.f               # 文件列表
│   ├── Makefile            # 编译脚本
│   └── waves/              # 波形文件
├── rtl/                    # RTL代码(DUT)
│   ├── nvme_ctrl.sv
│   └── ddr_ctrl.sv
├── tb/                     # 验证环境
│   ├── test/               # 测试用例
│   │   ├── base_test.sv
│   │   ├── nvme_basic_test.sv
│   │   └── ddr_stress_test.sv
│   ├── env/                # 环境组件
│   │   ├── nvme_env.sv
│   │   ├── nvme_scoreboard.sv
│   │   └── nvme_coverage.sv
│   ├── agent/              # 代理组件
│   │   ├── nvme_agent.sv
│   │   ├── nvme_driver.sv
│   │   ├── nvme_monitor.sv
│   │   └── nvme_sequencer.sv
│   ├── seq/                # 激励序列
│   │   ├── nvme_base_seq.sv
│   │   └── nvme_random_seq.sv
│   └── model/              # 参考模型
│       └── nvme_ref_model.sv
├── ip/                     # 第三方IP
│   └── vip/                # 商用VIP
└── scripts/                # 脚本工具
    ├── run_sim.py
    └── gen_coverage.py

避坑指南:我曾经见过一个项目,把所有.sv文件都放在同一个目录下,结果编译时经常出现“文件依赖顺序错误”。后来我强制要求按组件分目录,每个目录下只放该组件的文件,编译顺序用run.f文件显式指定。从此再没出过类似问题。

这个结构有几个好处:

  • 职责清晰:test目录只放测试用例,agent目录只放代理组件。新人来了,一看目录就知道该往哪里加文件。
  • 便于复用:如果下一个项目要用到同样的agent,直接把agent目录拷过去就行。
  • 编译可控:run.f文件里按顺序列出所有文件,不会出现“先编译了子类再编译父类”的尴尬。

2.4 组件间的连接与通信——它们是怎么“说话”的?

组件划分好了,目录也搭起来了。那这些组件之间怎么通信呢?UVM提供了TLM(Transaction Level Modeling)机制。说白了,就是组件之间通过“端口”和“管道”来传递transaction。

常见的连接方式:

  • monitor → scoreboard:通过analysis_port发送监测到的transaction。scoreboard通过analysis_imp接收。
  • sequencer → driver:通过seq_item_port/seq_item_export连接。driver从sequencer“取”transaction。
  • env → agent:通过config_db传递配置参数。比如设置agent的工作模式、接口句柄等。

举个例子,monitor发送transaction的代码片段:

class nvme_monitor extends uvm_monitor;
  uvm_analysis_port #(nvme_transaction) ap;
  
  virtual function void write_to_scoreboard(nvme_transaction tr);
    ap.write(tr);  // 通过analysis_port发送
  endfunction
endclass

class nvme_scoreboard extends uvm_scoreboard;
  uvm_analysis_imp #(nvme_transaction, nvme_scoreboard) imp;
  
  virtual function void write(nvme_transaction tr);
    // 在这里比对transaction
    compare(tr);
  endfunction
endclass

这段代码里,monitor只管“写”,scoreboard只管“收”。两者之间没有直接耦合。这就是TLM的魅力——你换一个monitor,只要它发的transaction类型不变,scoreboard完全不用改。

总结一下:验证环境的总览,说白了就是三件事——架构分层、组件划分、目录组织。架构分层决定了“怎么搭积木”,组件划分决定了“每块积木长什么样”,目录组织决定了“积木放哪里”。这三件事做好了,后面的用例开发就是水到渠成的事。

好了,这一章的内容就到这里。下一章我们会深入agent内部,看看driver和monitor到底是怎么写的。到时候我会分享一些我在存储控制器项目中踩过的坑,保证让你少走弯路。


公众号:蓝海资料掘金营,微信deep3321