简介:这份资源是面向芯片验证工程师与IC设计学习者的UVM验证环境自动生成工具,旨在解决手工搭建SystemVerilog验证平台耗时且易出错的问题。工具提供图形界面与Excel配置两种方式,用户无需编写大量代码即可设定验证环境参数,对非编程背景或刚接触UVM的团队成员尤为友好。压缩包共78个文件,约5.74MB,包含14个sv源码文件、4个xlsx配置模板、1个py主程序及1个pdf说明文档,另有58张jpg截图辅助理解,覆盖agent、driver、monitor、scoreboard、sequence等核心组件的生成逻辑与接口声明示例。已有1032人学习下载,读者可借此快速生成可复用的UVM测试平台骨架,理解组件连接与验证流程定义方式,并参考Excel模板完成参数化管理,从而将精力集中于验证场景设计与覆盖率提升。
1. 芯片验证UVM环境自动生成工具:从三天搭环境到三十分钟跑通冒烟
如果你做过芯片验证,大概率经历过这种开局:项目启动会上,验证负责人说“下周一把UVM环境搭起来,先跑个冒烟用例”。然后你打开编辑器,开始复制上一个项目的目录结构,改agent、改sequencer、改virtual sequence、改config_db的路径字符串,改到第三天发现某个uvm_config_db#(virtual my_if)的set和get层级对不上,波形里driver一动不动,只能对着log一行行翻。芯片验证UVM环境自动生成工具要解决的,就是这段重复度极高、创造性极低、但出错代价极大的“环境脚手架”工作。它读取一份结构化的接口描述或寄存器描述,自动产出UVM验证平台里那些模板化程度最高的组件骨架——driver、monitor、sequencer、agent、env、scoreboard的壳、virtual sequence、以及配套的编译脚本和仿真makefile。适合谁用?适合已经懂UVM基本机制、但不想再把生命浪费在复制粘贴和改字符串上的验证工程师;也适合验证负责人,用来统一团队的环境目录规范和命名风格。它不替代验证思路,只替代体力活。
2. 自动生成工具到底生成什么:UVM组件的模板边界与选型逻辑
2.1 先划清边界:哪些能自动生成,哪些必须手写
自动生成工具的价值上限,取决于你对“模板化”三个字的理解深度。UVM环境里,真正能稳定自动生成的部分,是那些结构固定、只随接口信号和事务字段变化的代码。典型可生成清单包括:
- 接口相关的
interface定义与clocking block骨架 uvm_sequence_item的字段声明、uvm_field_*宏注册、convert2stringdriver的run_phase主循环骨架,含seq_item_port.get_next_item与item_donemonitor的采样骨架,含analysis_port声明与write调用位置sequencer的typedef与uvm_sequencer参数化agent的build_phase中driver/monitor/sequencer的创建与连接env的build_phase中agent实例化与uvm_config_db的setvirtual sequence与virtual sequencer的骨架base_test中env创建、config_db设置、超时控制- 编译脚本与仿真脚本的模板
必须手写、工具不该碰的部分:参考模型的算法逻辑、scoreboard的比较策略、覆盖率模型的功能覆盖点定义、以及任何与具体协议时序强相关的采样时机判断。把这些也塞进自动生成,产出的代码会变成“看起来能跑、实际全是坑”的黑匣子。
提示:判断一个组件该不该自动生成,标准是“它的代码结构是否只由接口信号列表和事务字段列表决定”。如果是,生成;如果还依赖协议状态机或验证意图,手写。
2.2 输入描述文件怎么设计:YAML还是JSON
自动生成工具的输入,通常是一份描述接口和事务的结构化文件。常见做法是用YAML,因为验证工程师写注释方便,层级直观。下面是一份最小可用的接口描述示例:
# if_spec.yaml interface: name: "apb_if" signals: - name: "pclk" dir: "input" width: 1 - name: "paddr" dir: "input" width: 32 - name: "pwdata" dir: "input" width: 32 - name: "prdata" dir: "output" width: 32 - name: "psel" dir: "input" width: 1 - name: "penable" dir: "input" width: 1 - name: "pwrite" dir: "input" width: 1 - name: "pready" dir: "input" width: 1 transaction: name: "apb_trans" fields: - name: "addr" type: "bit [31:0]" - name: "data" type: "bit [31:0]" - name: "write" type: "bit" - name: "resp" type: "bit [1:0]"这份文件里,interface段决定interface和monitor的采样信号,transaction段决定sequence_item的字段和driver的驱动对象。工具读取后,按内置模板渲染出SystemVerilog文件。
参数说明:dir字段只接受input/output/inout,生成clocking block时input对应input #1step,output对应output #1step。width为1时生成bit,大于1时生成bit [width-1:0]。type字段直接透传到sequence_item,所以写的时候要符合SV语法。
2.3 模板引擎选型:Jinja2还是纯字符串拼接
如果你用Python写生成器,模板引擎选Jinja2是最省事的。原因有三:第一,UVM代码里大量begin/end和缩进,Jinja2的空白控制能让你把模板写得像正常SV代码;第二,条件分支和循环写起来干净,比如agent里要不要生成uvm_analysis_port,一个{% if %}就解决;第三,模板文件可以独立存放,改模板不用动生成逻辑。
纯字符串拼接不是不能用,但一旦模板超过200行,维护成本会指数上升。我见过用f-string拼driver的写法,改一个信号名要动七八处,最后没人敢改。Jinja2的模板长这样:
{# templates/driver.sv.j2 #} class {{ agent_name }}_driver extends uvm_driver #({{ trans_name }}); `uvm_component_utils({{ agent_name }}_driver) virtual {{ if_name }} vif; function new(string name, uvm_component parent); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); if (!uvm_config_db#(virtual {{ if_name }})::get(this, "", "vif", vif)) `uvm_fatal(get_type_name(), "vif not set") endfunction task run_phase(uvm_phase phase); {{ trans_name }} req; forever begin seq_item_port.get_next_item(req); drive_one(req); seq_item_port.item_done(); end endtask task drive_one({{ trans_name }} tr); @(posedge vif.pclk); // TODO: 按协议填充驱动逻辑 vif.psel <= 1'b1; vif.penable <= 1'b0; vif.paddr <= tr.addr; vif.pwdata <= tr.data; vif.pwrite <= tr.write; @(posedge vif.pclk); vif.penable <= 1'b1; wait(vif.pready == 1'b1); @(posedge vif.pclk); vif.psel <= 1'b0; vif.penable <= 1'b0; endtask endclass逻辑说明:模板里{{ }}是变量占位,{# #}是注释。drive_one里保留了协议时序的TODO,因为自动生成工具不该猜你的协议握手细节。参数说明:agent_name、trans_name、if_name由生成器从YAML读取后注入。uvm_config_db的get路径写死为""和"vif",这是约定,生成env时set也要用同样的字符串,否则就是经典的血泪翻车现场。
2.4 生成器主流程:从YAML到文件树的四步
生成器本身的结构不复杂,四步走:
第一步,解析YAML,做合法性校验。检查信号名是否重复、width是否为正整数、transaction字段类型是否为空。
第二步,构建渲染上下文。把agent名、env名、文件名前缀按命名规范拼出来。常见规范是<模块名>_<组件类型>,比如apb_agent、apb_driver。
第三步,遍历模板清单,逐个渲染。模板清单可以写死在代码里,也可以放一个manifest.yaml。
第四步,写文件并生成目录结构。目录结构建议固定为tb/agent/、tb/env/、tb/seq/、tb/test/、sim/。
# gen_uvm.py import yaml from jinja2 import Environment, FileSystemLoader from pathlib import Path def load_spec(path): with open(path) as f: spec = yaml.safe_load(f) # 校验信号名唯一 names = [s["name"] for s in spec["interface"]["signals"]] assert len(names) == len(set(names)), "duplicate signal name" return spec def build_context(spec): if_name = spec["interface"]["name"] trans_name = spec["transaction"]["name"] agent_name = if_name.replace("_if", "") return { "if_name": if_name, "trans_name": trans_name, "agent_name": agent_name, "signals": spec["interface"]["signals"], "fields": spec["transaction"]["fields"], } def render_all(ctx, out_dir): env = Environment( loader=FileSystemLoader("templates"), trim_blocks=True, lstrip_blocks=True, ) out = Path(out_dir) out.mkdir(parents=True, exist_ok=True) mapping = { "interface.sv.j2": f"{ctx['if_name']}.sv", "transaction.sv.j2": f"{ctx['trans_name']}.sv", "driver.sv.j2": f"{ctx['agent_name']}_driver.sv", "monitor.sv.j2": f"{ctx['agent_name']}_monitor.sv", "sequencer.sv.j2": f"{ctx['agent_name']}_sequencer.sv", "agent.sv.j2": f"{ctx['agent_name']}_agent.sv", "env.sv.j2": f"{ctx['agent_name']}_env.sv", "base_test.sv.j2": f"{ctx['agent_name']}_base_test.sv", } for tpl, fname in mapping.items(): content = env.get_template(tpl).render(**ctx) (out / fname).write_text(content) print(f"generated {fname}") if __name__ == "__main__": spec = load_spec("if_spec.yaml") ctx = build_context(spec) render_all(ctx, "output")逻辑说明:load_spec做最小校验,防止重复信号名导致interface编译报错。build_context把YAML里的原始字段映射成模板变量,agent_name从接口名去掉_if后缀得到,这是约定。render_all里trim_blocks和lstrip_blocks让模板里的控制语句不产生多余空行。参数说明:out_dir建议按项目名分目录,避免覆盖。mapping字典决定生成哪些文件,想加scoreboard就加一条。
3. 把生成器跑起来:从零到冒烟用例的完整操作链
3.1 环境准备与依赖安装
生成器本身只需要Python 3.8以上和Jinja2、PyYAML两个包。仿真侧需要一套能跑UVM的仿真器,常见的是商业仿真器或开源的Verilator配合UVM支持库。这里不展开仿真器安装,假设你已经有一个能跑uvm_hello_world的环境。
# 创建虚拟环境,避免污染系统Python python3 -m venv venv source venv/bin/activate # 安装依赖,版本号写死避免CI上行为漂移 pip install jinja2==3.1.2 pyyaml==6.0.1 # 验证安装 python -c "import jinja2, yaml; print('ok')"参数说明:venv目录建议加到.gitignore。版本号写死是因为Jinja2 3.0到3.1的空白控制行为有细微变化,团队协作时统一版本能省掉“在我机器上生成的文件多一行空行”的扯皮。
3.2 编写第一份接口描述并生成代码
把2.2节的if_spec.yaml保存到项目根目录,模板文件放到templates/下。模板至少需要interface.sv.j2、transaction.sv.j2、driver.sv.j2、monitor.sv.j2、sequencer.sv.j2、agent.sv.j2、env.sv.j2、base_test.sv.j2八个。这里给一个agent.sv.j2的示例,展示组件之间的连接关系:
{# templates/agent.sv.j2 #} class {{ agent_name }}_agent extends uvm_agent; `uvm_component_utils({{ agent_name }}_agent) {{ agent_name }}_driver drv; {{ agent_name }}_monitor mon; {{ agent_name }}_sequencer sqr; function new(string name, uvm_component parent); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); mon = {{ agent_name }}_monitor::type_id::create("mon", this); if (get_is_active() == UVM_ACTIVE) begin drv = {{ agent_name }}_driver::type_id::create("drv", this); sqr = {{ agent_name }}_sequencer::type_id::create("sqr", this); end endfunction function void connect_phase(uvm_phase phase); super.connect_phase(phase); if (get_is_active() == UVM_ACTIVE) drv.seq_item_port.connect(sqr.seq_item_export); endfunction endclass逻辑说明:get_is_active()判断agent是active还是passive,passive时只创建monitor,这是UVM标准做法。connect_phase里只连driver和sequencer,monitor的analysis_port在env层连到scoreboard。参数说明:agent_name注入后,类名和变量名自动一致,避免手写时把apb_driver写成apb_drv的低级错误。
生成命令:
python gen_uvm.py预期输出:
generated apb_if.sv generated apb_trans.sv generated apb_driver.sv generated apb_monitor.sv generated apb_sequencer.sv generated apb_agent.sv generated apb_env.sv generated apb_base_test.sv3.3 编译脚本与仿真脚本的自动生成
生成器还应该产出sim/Makefile或sim/run.sh,把编译顺序和仿真参数固化下来。常见做法是生成一个文件列表filelist.f和一个Makefile:
# sim/Makefile 由生成器产出 UVM_HOME ?= /path/to/uvm SIM ?= vsim TOP ?= apb_base_test FILELIST = filelist.f LOG = sim.log all: compile run compile: $(SIM) -c -sv -uvm -work work -f $(FILELIST) +incdir+$(UVM_HOME)/src run: $(SIM) -c -sv -work work -do "run -all; quit" +UVM_TESTNAME=$(TOP) -l $(LOG) clean: rm -rf work $(LOG) transcript参数说明:UVM_HOME指向UVM库源码目录,SIM是仿真器命令,TOP是默认测试名。filelist.f里按依赖顺序列出生成的文件,interface在最前,test在最后。生成器写filelist.f时按固定顺序追加,避免手写时漏文件。
3.4 跑通第一个冒烟用例并看波形
生成完代码后,先别急着写复杂sequence。在base_test里加一个最简单的sequence,只发一个transaction,看driver能不能驱动、monitor能不能采样。
// 在apb_base_test.sv的run_phase里加 task run_phase(uvm_phase phase); apb_smoke_seq seq; phase.raise_objection(this); seq = apb_smoke_seq::type_id::create("seq"); seq.start(env.sqr); phase.drop_objection(this); endtaskapb_smoke_seq可以手写,也可以让生成器生成一个空壳。跑仿真后看log里有没有UVM_FATAL,看波形里psel、penable、pready的握手是否正常。如果driver的vif是null,log里会报vif not set,说明env里uvm_config_db的set路径和driver的get路径不一致。这是自动生成工具最常被骂的地方,但根因是模板里路径字符串没统一,改模板即可。
注意:第一次跑通冒烟用例后,把生成的代码提交到版本库,作为后续手写逻辑的基线。不要直接在生成产物上大改,否则下次重新生成会覆盖。正确做法是改模板或改YAML,重新生成。
4. 避坑与排查:自动生成UVM环境时最容易翻车的五件事
4.1 现象:driver里vif为null,仿真报UVM_FATAL
原因:uvm_config_db#(virtual apb_if)::set的路径和get的路径不匹配。常见情况是env里set时用了"*",driver里get时用了"",或者set的变量名是"vif"而get的是"vif0"。
解决:在生成器里把set和get的路径字符串抽成同一个变量,模板里统一引用。比如定义vif_path = "vif",env模板和driver模板都用{{ vif_path }}。改完重新生成,不要手动改生成产物。
4.2 现象:monitor采样到的transaction字段全是0
原因:monitor的采样时机不对,在clocking block的驱动沿采样,采到的是驱动前的旧值。或者monitor里忘了调用analysis_port.write(tr)。
解决:monitor采样用@(posedge vif.pclk)后加#1step或直接用clocking block的input #1step。生成器模板里把采样点固定在clocking block的input skew上,不要用裸@(posedge clk)。另外检查monitor模板里write调用是否在if (vif.psel && vif.penable)条件内。
4.3 现象:重新生成后手写的scoreboard逻辑被覆盖
原因:生成器输出目录和手写代码目录混在一起,重新生成时直接覆盖。
解决:生成器输出到gen/目录,手写代码放user/目录,编译时两个目录都加入filelist。生成器只负责gen/,永不碰user/。这是血泪经验,早期版本我直接把生成产物和手写逻辑放一起,某次改YAML重新生成,一个下午的scoreboard调试白干。
4.4 现象:agent的is_active配置不生效,passive agent里driver仍被创建
原因:get_is_active()读的是agent自己的active字段,但env里没有通过config_db设置。或者设置时路径写错,设到了env上而不是agent上。
解决:在base_test里用uvm_config_db#(uvm_active_passive_enum)::set(this, "env.agent", "is_active", UVM_PASSIVE)。生成器模板里agent的build_phase用get_is_active(),不要硬编码UVM_ACTIVE。如果项目里agent默认passive,就在模板里把默认值改成UVM_PASSIVE。
4.5 现象:生成的文件编译报错,提示类名未定义
原因:filelist.f里文件顺序不对,agent在driver之前编译,或者transaction在driver之后编译。
解决:生成器写filelist.f时按固定依赖顺序:interface → transaction → sequencer → driver → monitor → agent → env → test。不要按文件名字母序,字母序会把agent排在driver前面。如果用了include,确保include顺序也一致。
5. 进阶:让生成器支持多协议与回归验证
5.1 从单协议到多协议:YAML里加一个protocol字段
当项目从APB扩展到APB+AXI时,生成器需要区分协议模板。做法是在YAML顶层加protocol: "apb",生成器根据这个字段选择模板子目录templates/apb/或templates/axi/。公共模板放templates/common/,协议特有模板放各自目录。这样新增协议只需加一套模板和一份YAML,生成逻辑不动。
5.2 生成器的自验证:用生成产物跑一个最小回归
生成器本身也需要验证。常见做法是准备三份YAML:一份最小接口、一份多字段事务、一份passive agent配置。每次改生成器后,跑一遍生成,然后用仿真器编译生成产物,跑一个空sequence。如果编译通过且冒烟用例无UVM_FATAL,说明生成器没坏。这个自验证可以写成脚本,挂在CI上。
#!/bin/bash # self_check.sh set -e for spec in tests/*.yaml; do python gen_uvm.py --spec $spec --out /tmp/gen_check cd /tmp/gen_check make compile make run TOP=smoke_test cd - done echo "all specs passed"参数说明:--spec和--out是生成器的命令行参数,用argparse加。set -e让任何一步失败就退出。这个脚本跑一遍大约几分钟,但能挡住90%的模板改坏问题。
5.3 一个具体技巧:用生成器产出覆盖率模型骨架
功能覆盖率模型里,covergroup的coverpoint定义也高度模板化。可以在YAML的transaction字段里加coverage: true标记,生成器为标记字段生成coverpoint骨架:
covergroup apb_trans_cg; addr_cp: coverpoint tr.addr { bins low = {[0:32'h0000_00FF]}; bins mid = {[32'h0000_0100:32'h0000_FF00]}; bins high = {[32'h0000_FF01:32'hFFFF_FFFF]}; } write_cp: coverpoint tr.write; endgroup生成器只生成bins的默认划分,具体边界值由验证工程师手改。这样覆盖率模型的框架不用手敲,但覆盖意图仍然由人决定。我一般会在生成后把covergroup实例化到monitor或scoreboard里,用sample()在采样点调用。
5.4 值不值得做:一个粗略的投入产出判断
如果你只做一个小模块,接口信号不超过20个,手写环境可能比搭生成器快。但如果项目里有3个以上相似接口,或者团队里每个人搭环境的风格都不一样,生成器的价值就出来了。我的习惯是:新项目启动第一天,先花半天把生成器跑通,后面每加一个接口就改YAML重新生成,省下的时间足够把scoreboard写扎实。生成器不是银弹,它只是把“改字符串”这件事从手工变成自动,但验证的核心——激励设计和结果检查——仍然需要你一行行想清楚。希望帮到你。
本文还有配套的精品资源,点击获取