第二章:验证环境总览——UVM验证平台架构、组件划分与目录结构
各位同学,大家好。今天我们来聊聊验证环境的整体框架。说实话,很多新人一上来就扎进代码细节里,结果写了半天,连自己写的组件该放哪个目录都搞不清楚。我个人习惯是,先搭好架子,再填内容。就像盖房子,你得先有图纸,对吧?
2.1 UVM验证平台架构——从宏观到微观
UVM验证平台,说白了就是一套标准化的“积木”。你想想看,我们做存储控制器验证,无非就是产生激励、驱动到DUT、监测响应、最后比对结果。UVM把这套流程抽象成了几个核心组件。
我画了一张图,帮你快速建立整体认知:
这张图里,从上到下依次是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