UVM验证实战:从环境搭建到高级调试与覆盖率收集
2026/8/2 8:33:09 网站建设 项目流程

1. 项目概述:从零散记录到体系化验证经验库

在芯片设计和验证领域,SystemVerilog (SV) 和 Universal Verification Methodology (UVM) 是构建高效、可重用验证环境的基石。很多工程师,包括我自己,在项目初期或学习阶段,都会有一个名为“使用记录”的文档或笔记,里面零零散散地记着各种语法片段、调试命令、报错信息和临时解决方案。这个项目,本质上就是将我个人这些零散的“SV/UVM 使用记录”进行系统化整理、深度解析和实战经验注入,形成一个可供同行直接参考、复现和避坑的实战指南。它解决的不仅仅是“某个函数怎么用”的问题,更是“为什么在这里用”、“用了会有什么坑”、“如何组合使用更高效”等一系列工程实践中的深层需求。

无论是刚接触 SV/UVM 的验证新人,还是有一定经验但在某些高级特性或调试技巧上需要查漏补缺的工程师,这份记录都旨在提供从基础到进阶、从理论到实操的连贯性指导。我们会围绕验证环境搭建、常用机制解析、高级调试技巧以及代码规范等核心场景展开,并结合网络热词中大家普遍关注的uvm_hdl_forceuvm_glob_to_re、寄存器读写、覆盖率收集等痛点进行重点剖析。

2. 验证环境构建的核心思路与组件选型

2.1 自顶向下的验证环境架构设计

一个健壮的 UVM 验证环境,其架构设计决定了后续的可扩展性、可重用性和调试便利性。我倾向于采用经典的自顶向下(Top-Down)设计方法。首先,在testbench顶层模块中,完成 DUT (Design Under Test) 的例化、时钟复位生成以及 UVM 测试的启动。

module tb_top; import uvm_pkg::*; `include “uvm_macros.svh” // 时钟和复位信号生成 bit clk; bit rst_n; initial begin clk = 0; forever #5 clk = ~clk; // 100MHz 时钟 end initial begin rst_n = 0; #100 rst_n = 1; end // DUT 例化 my_dut u_my_dut ( .clk (clk), .rst_n (rst_n), // ... 其他端口连接 ); // 启动 UVM 测试 initial begin run_test(“my_base_test”); end endmodule

这里的关键在于run_test(“my_base_test”)。这个语句会在仿真开始时创建一个指定的test类实例,并自动触发 UVM 的相位机制。my_base_test作为所有测试用例的基类,负责构建整个验证环境的结构。这种设计的优势在于,测试用例(test)与环境(env)解耦,通过不同的测试类来配置环境行为,实现极高的灵活性。

注意run_test的参数可以通过仿真命令行的+UVM_TESTNAME选项动态指定,这是实现回归测试的基础。例如,在仿真命令中加入+UVM_TESTNAME=my_smoke_test,则会覆盖代码中指定的my_base_test

2.2 关键组件的职责与交互关系

my_base_test中,我们构建环境的核心组件。一个典型的 UVM 环境包含以下核心部分:

  1. uvm_env:环境的容器,负责例化和连接所有子组件。
  2. uvm_agent:驱动(driver)、监视器(monitor)和序列器(sequencer)的封装。通常分为主动(is_active = UVM_ACTIVE)和被动(is_active = UVM_PASSIVE)模式。主动模式下,agent会驱动信号;被动模式下,仅用于监测总线。
  3. uvm_sequencer:调度和管理测试序列(sequence),是产生激励的“大脑”。
  4. uvm_driver:从sequencer获取事务(transaction),并将其转换为具体的时序信号,驱动到 DUT 接口。
  5. uvm_monitor:监视 DUT 的接口,将观测到的信号转换为事务,并发送给后续的分析组件(如scoreboard)。
  6. uvm_scoreboard:验证的核心,负责比较reference model的预期输出和monitor采集到的实际输出,判断测试是否通过。
  7. uvm_config_db:UVM 的配置数据库,用于在组件间传递配置参数、虚拟接口(virtual interface)等,是实现组件间灵活配置的关键机制。

构建环境的代码骨架通常如下:

class my_env extends uvm_env; `uvm_component_utils(my_env) my_agent i_agt; // 输入 agent my_agent o_agt; // 输出 agent (被动模式) my_scoreboard scb; my_virtual_sequencer v_sqr; // 虚拟序列器,用于协调多个 sequencer function new(string name = “my_env”, uvm_component parent = null); super.new(name, parent); endfunction virtual function void build_phase(uvm_phase phase); super.build_phase(phase); i_agt = my_agent::type_id::create(“i_agt”, this); o_agt = my_agent::type_id::create(“o_agt”, this); o_agt.is_active = UVM_PASSIVE; // 设置为被动模式 scb = my_scoreboard::type_id::create(“scb”, this); v_sqr = my_virtual_sequencer::type_id::create(“v_sqr”, this); endfunction virtual function void connect_phase(uvm_phase phase); super.connect_phase(phase); // 连接 monitor 到 scoreboard 的分析端口 i_agt.monitor.ap.connect(scb.exp_port.analysis_export); o_agt.monitor.ap.connect(scb.act_port.analysis_export); // 连接虚拟序列器与实际序列器 v_sqr.i_sqr = i_agt.sequencer; endfunction endclass

实操心得:在构建环境时,我习惯将virtual sequencer单独定义在env中,而不是test中。这样做的理由是,virtual sequencer本质上是环境内部多个sequencer的协调者,属于环境结构的一部分。将其放在envconnect_phase中进行连接,逻辑更清晰,也符合 UVM 组件的层次关系。

3. 核心机制深度解析与避坑指南

3.1uvm_config_db的灵活运用与常见陷阱

uvm_config_db是 UVM 的“神经系统”,负责信息传递。其基本用法是setget

// 在顶层 testbench 或 test 中设置虚拟接口 virtual my_if vif; initial begin uvm_config_db#(virtual my_if)::set(null, “uvm_test_top.env.i_agt.drv”, “vif”, vif); end // 在 driver 中获取虚拟接口 class my_driver extends uvm_driver #(my_transaction); virtual my_if vif; virtual function void build_phase(uvm_phase phase); super.build_phase(phase); if(!uvm_config_db#(virtual my_if)::get(this, “”, “vif”, vif)) `uvm_fatal(“CFGDB/NO_VIF”, “No virtual interface specified for this driver instance”) endfunction endclass

为什么必须用uvm_config_db而不是直接赋值?因为 UVM 组件是在仿真过程中动态创建的,其层次结构(contxt)在编译时并不确定。uvm_config_db通过字符串路径(如“uvm_test_top.env.i_agt.drv”)在运行时进行匹配,实现了松耦合。

常见陷阱1:路径错误。这是最常导致get失败的原因。务必使用get_full_name()打印出组件的完整路径,并与set时的路径严格匹配。null作为contxt表示从全局根目录开始搜索。

常见陷阱2:类型参数不匹配setget时的类型参数(如#(virtual my_if))必须完全一致,包括virtual关键字。

常见陷阱3:设置时机晚于获取时机。UVM 的build_phase是自顶向下执行的。如果testbuild_phaseagentbuild_phase之后执行,那么在agentget配置就会失败。解决方案是确保set操作在最早的阶段执行,例如在testbuild_phase最开始,或者使用uvm_rootrun_test之前的初始块。

3.2 Sequence 机制:激励生成的艺术

Sequence 是 UVM 激励生成的核心。一个sequence通过start()方法挂载到某个sequencer上,然后其body()任务被自动执行。

class my_sequence extends uvm_sequence #(my_transaction); `uvm_object_utils(my_sequence) rand int num_trans = 10; // 随机化发送的事务数量 virtual task body(); if(starting_phase != null) starting_phase.raise_objection(this); // 提起异议,防止仿真提前结束 repeat(num_trans) begin `uvm_do(req) // 宏,自动完成 create, randomize, send 过程 // 也可以手动控制: // `uvm_create(req) // assert(req.randomize()); // `uvm_send(req) end #100; // 等待所有事务处理完 if(starting_phase != null) starting_phase.drop_objection(this); // 放下异议 endtask endclass

关键点解析:uvm_do。这个宏看似简单,背后完成了三件事:1) 调用create_item创建事务对象;2) 调用start_item等待sequencerdriver的授权;3) 随机化事务并调用finish_item将其发送给driver。对于简单的序列,用宏很方便。但对于需要精细控制(如事务间延迟、条件发送)的场景,建议使用手动方式(create,start_item,randomize,finish_item)。

关于starting_phaseobjection:在 UVM 1.2 之后,更推荐使用phase.raise_objection(this)的方式在sequence中控制仿真运行。这比在testmain_phase中提起objection更加精确和模块化。务必确保raisedrop成对出现,否则仿真可能挂起或提前结束。

3.3 TLM 通信:组件间的数据流桥梁

TLM (Transaction Level Modeling) 是 UVM 组件间通信的高级抽象。最常用的是uvm_analysis_portuvm_tlm_analysis_fifo

  • uvm_analysis_port: 一对多的广播端口。monitor用它来发送采集到的事务,任何感兴趣的组件(如scoreboard,coverage collector)都可以连接并接收。
  • uvm_tlm_analysis_fifo: 一个带有深度的 FIFO,内部实现了uvm_analysis_imp。常用于scoreboard中,临时存储来自不同monitor的事务,以便进行比对。

scoreboard中的典型连接和使用:

class my_scoreboard extends uvm_scoreboard; `uvm_component_utils(my_scoreboard) uvm_tlm_analysis_fifo #(my_transaction) exp_fifo; uvm_tlm_analysis_fifo #(my_transaction) act_fifo; my_transaction exp_q[$], act_q[$]; // 本地队列用于缓存 function new(string name, uvm_component parent); super.new(name, parent); exp_fifo = new(“exp_fifo”, this); act_fifo = new(“act_fifo”, this); endfunction virtual task run_phase(uvm_phase phase); fork this.collect_expected_trans(); this.collect_actual_trans(); this.compare_trans(); join endtask task collect_expected_trans(); my_transaction tr; forever begin exp_fifo.get(tr); // 阻塞直到从 fifo 中取到数据 exp_q.push_back(tr); end endtask // collect_actual_trans 类似... task compare_trans(); forever begin wait(exp_q.size() > 0 && act_q.size() > 0); // 从队列头部取出事务进行比较 // ... end endtask endclass

注意事项:使用uvm_tlm_analysis_fifo时,其get任务是阻塞的,这非常适合在run_phase中启动的永久循环任务。但要注意,如果数据生产端(monitor)和生产端(scoreboard的比较线程)速率不匹配,FIFO 可能会满(如果设置了深度)或者消耗大量内存。通常需要设计合理的比对策略,例如基于事务ID进行匹配,而不是严格按顺序。

4. 高级调试技巧与实用函数详解

4.1 信号强制与释放:uvm_hdl_forceuvm_hdl_release

这是调试和错误注入的利器。当需要强制 DUT 内部某个信号为特定值以模拟特定场景(如错误注入、跳过初始化序列)时,就会用到它们。

// 强制 DUT 实例 u_dut 内部的信号 int_signal 为 1‘b1 initial begin uvm_hdl_force(“tb_top.u_dut.int_signal”, 1‘b1); end // 在某个 sequence 中,根据条件强制信号 virtual task body(); // ... 一些正常激励 if(some_error_condition) begin `uvm_info(“DEBUG”, “Injecting fault by forcing signal X”, UVM_LOW) uvm_hdl_force(“tb_top.u_dut.fault_signal”, 1‘b0); #100ns; // 保持强制 100ns uvm_hdl_release(“tb_top.u_dut.fault_signal”); `uvm_info(“DEBUG”, “Fault signal released”, UVM_LOW) end // ... 后续激励 endtask

工作原理:这两个函数通过 PLI/VPI 接口直接访问仿真内核,修改指定层次路径下信号的当前值。uvm_hdl_force会覆盖驱动源,uvm_hdl_release则解除强制,恢复信号由原有逻辑驱动。

必须注意的坑

  1. 路径字符串必须绝对正确。最可靠的方式是在仿真波形中选中该信号,查看其完整层次路径。路径中的实例名和信号名需与 RTL 完全一致。
  2. 作用范围是全局的。一旦强制,该信号在所有进程中的值都会改变,直到被释放。这可能会产生意想不到的副作用。
  3. 释放时机至关重要。如果强制后忘记释放,该信号将永远保持强制值,导致后续仿真行为异常。务必在post_shutdown_phase或通过final块确保所有强制被释放。
  4. 对多维数组和位选的支持。函数支持对信号的某一位或某个数组元素进行强制,语法如“tb_top.u_dut.reg_array[5]”“tb_top.u_dut.bus[31:16]”

4.2 正则表达式在配置中的应用:uvm_glob_to_re

这个函数较少被提及,但在处理复杂配置字符串匹配时非常有用。它将类 Shell 的通配符模式(glob)转换为 SystemVerilog 支持的正则表达式(RE)。

典型场景:你想通过+UVM_CONFIG_DB_SET命令行参数,为一系列名字有规律的组件设置配置,但不想为每一个单独写一行set

假设有多个agent,命名为agt_0,agt_1, ...,agt_7。你想为它们全部设置同一个虚拟接口。

// 在 test 的 build_phase 中 string pattern = “uvm_test_top.env.agt_*”; // glob 模式 uvm_regex re; string error_string; if (!uvm_glob_to_re(pattern, re, error_string)) begin `uvm_fatal(“CFG”, $sformatf(“Bad glob pattern ‘%s’: %s”, pattern, error_string)) end // 遍历 UVM 根(root)的所有子组件 uvm_root root = uvm_root::get(); foreach (root.find(“*”)) begin // 这是一个简化的示意,实际遍历需要递归 uvm_component comp; // ... 获取组件 comp if (re.match(comp.get_full_name())) begin uvm_config_db#(virtual my_if)::set(this, comp.get_full_name(), “vif”, shared_vif); `uvm_info(“CFG”, $sformatf(“Set vif for %s”, comp.get_full_name()), UVM_HIGH) end end

为什么需要它?因为 UVM 的配置数据库路径是字符串,直接使用正则表达式匹配更灵活,但glob模式(使用*,?等)对人类更友好。uvm_glob_to_re完成了这个转换。不过,在大多数简单场景下,直接使用明确的路径进行set更直观。这个函数更适用于框架开发者或需要高度动态配置的复杂环境。

4.3 寄存器模型(RAL)读写操作详解

寄存器模型是 UVM 中用于对 DUT 寄存器进行抽象、前门/后门访问和功能检查的强大工具。其核心是uvm_reguvm_reg_blockuvm_reg_map

前门访问 vs 后门访问

  • 前门访问:通过模拟总线协议(如 APB、AHB、AXI)的物理接口来读写寄存器。速度慢,但真实模拟了芯片行为。
  • 后门访问:通过 HDL 路径直接读写寄存器对应的信号。速度快,常用于测试初始化和快速检查。

基本读写操作

// 假设 ral_model 是已创建并配置好的寄存器模型块 task read_register; uvm_status_e status; uvm_reg_data_t value; // 前门读 ral_model.REG_NAME.read(status, value, .path(UVM_FRONTDOOR)); if (status == UVM_IS_OK) begin `uvm_info(“RAL”, $sformatf(“Read REG_NAME via frontdoor: 0x%0h”, value), UVM_LOW) end // 后门写 ral_model.REG_NAME.write(status, 32‘hdeadbeef, .path(UVM_BACKDOOR)); endtask

基于地址的读写:这是网络热词中特别关注的点。有时我们可能只知道寄存器的地址,而不是其模型句柄。

task read_by_address(uvm_reg_addr_t addr); uvm_status_e status; uvm_reg_data_t value; uvm_reg target_reg; // 通过地址获取寄存器对象 target_reg = ral_model.default_map.get_reg_by_offset(addr); if (target_reg == null) begin `uvm_error(“RAL”, $sformatf(“No register found at address 0x%0h”, addr)) return; end target_reg.read(status, value, .path(UVM_FRONTDOOR)); // ... 处理读出的值 endtask task write_by_address(uvm_reg_addr_t addr, uvm_reg_data_t data); uvm_status_e status; uvm_reg target_reg; target_reg = ral_model.default_map.get_reg_by_offset(addr); if (target_reg == null) return; target_reg.write(status, data, .path(UVM_FRONTDOOR)); endtask

关键点get_reg_by_offsetuvm_reg_map的方法。寄存器模型在add_map时会建立地址偏移量到寄存器对象的映射关系。确保你的地址是相对于该map基地址的偏移量。

常见问题

  1. 地址映射错误get_reg_by_offset返回null。检查寄存器模型的地址映射(add_map时的基地址和偏移量计算)是否正确,以及传入的addr是否是绝对地址还是偏移地址。
  2. 后门路径未设置:后门访问需要为每个uvm_reg设置 HDL 路径。这通常在寄存器模型定义时通过add_hdl_path完成,并在顶层通过ral_model.set_hdl_path_root(“tb_top.u_dut”)设置根路径。如果路径不对,后门操作会失败。
  3. 镜像值与期望值:寄存器模型内部维护一个期望值(desired)和镜像值(mirrored)。write操作会更新期望值和镜像值(如果成功)。read操作会更新镜像值。可以使用ral_model.REG_NAME.get()获取镜像值,ral_model.REG_NAME.get_mirrored_value()获取上次读回的镜像值,用ral_model.REG_NAME.compare()来比较镜像值与期望值是否一致,这是寄存器自检的基础。

5. 覆盖率收集与代码规范实践

5.1 功能覆盖率模型设计与采样策略

功能覆盖率是衡量验证完备性的关键指标。在 UVM 中,通常使用uvm_subscriber或直接在scoreboard/monitor中定义covergroup

class my_coverage extends uvm_subscriber #(my_transaction); `uvm_component_utils(my_coverage) my_transaction cov_trans; covergroup cg_bus_trans; // 地址覆盖点:划分为多个区间(bins) cp_addr: coverpoint cov_trans.addr { bins low = {[0:16‘hff]}; bins mid = {[16‘h100:16‘hfff]}; bins high = {[16‘h1000:16‘hffff]}; illegal_bins illegal = {16‘hdead}; // 非法值检查 } // 数据覆盖点:关注特定值 cp_data: coverpoint cov_trans.data { bins zero = {0}; bins max = {32‘hffff_ffff}; bins others = default; // 其他所有值归为一类 } // 交叉覆盖:地址与读写的组合 cross cp_addr, cov_trans.rw { ignore_bins read_high = binsof(cp_addr.high) && binsof(cov_trans.rw.read); } endgroup function new(string name, uvm_component parent); super.new(name, parent); cg_bus_trans = new(); endfunction virtual function void write(my_transaction t); cov_trans = t; cg_bus_trans.sample(); // 采样 endfunction // 在报告阶段打印覆盖率 virtual function void report_phase(uvm_phase phase); super.report_phase(phase); `uvm_info(“COV”, $sformatf(“Coverage: %.2f%%”, cg_bus_trans.get_coverage()), UVM_MEDIUM) endfunction endclass

设计要点

  1. 目标导向:覆盖率点应直接对应验证计划中的功能点,而不是盲目覆盖所有信号。例如,关注地址映射区域、关键控制状态、错误注入条件等。
  2. bin 的划分:合理的bins划分能有效指导随机测试。避免使用过多的auto_bins,这会导致覆盖率空洞难以分析。应为关键值(如0,最大值,边界值)创建独立的bins
  3. 交叉覆盖的谨慎使用:交叉覆盖会指数级增加覆盖空间。只对确实存在功能关联的覆盖点进行交叉。使用ignore_binsillegal_bins来排除不关心或非法的组合。
  4. 采样时机:确保在事务数据稳定且有效时采样。通常在monitor检测到一个完整事务后,通过analysis_port广播,由coverage collector采样。

5.2 UVM 代码规范与可维护性实践

良好的代码规范是团队协作和项目长期维护的保障。以下是一些关键实践:

1. 文件组织

  • 一个类一个文件,文件名与类名一致(如my_agent.sv)。
  • 将所有的packageinclude文件放在一个单独的目录(如sv_pkg)中,在编译时统一指定。
  • 测试用例可以按功能分类放在不同的目录。

2. 命名约定

  • 类名:使用lower_case_with_underscores后跟_type的形式,如my_driverapb_sequence。避免使用CamelCase
  • 实例名:在组件内,使用有意义的实例名前缀,如i_agt(输入 agent)、o_mon(输出 monitor)、p_sequencer(指向parent_sequencer的指针)。
  • :全部大写,用下划线分隔,如UVM_INFOMY_MAX_LEN。自定义宏需格外小心,避免与 UVM 或仿真器宏冲突。
  • 信号/变量:采用小写加下划线,对于寄存器等,可以加入_o(输出)、_n(低有效)等后缀。

3. 代码结构

  • 在每个类的开头,紧随uvm_component_utilsuvm_object_utils之后,定义所有的成员变量。
  • 按照 UVM 相位顺序组织函数:new->build_phase->connect_phase->end_of_elaboration_phase->start_of_simulation_phase->run_phase(以及其中的子任务) ->extract_phase->check_phase->report_phase
  • build_phase中创建子组件;在connect_phase中连接 TLM 端口和导出。

4. 打印信息控制

  • 合理使用UVM_INFO的冗余度(verbosity)。UVM_LOW用于关键流程信息,UVM_MEDIUM用于一般调试,UVM_HIGH用于最详细的追踪。在命令行使用+UVM_VERBOSITY=HIGH等控制全局输出。
  • 使用UVM_ERROR报告检查到的设计错误或严重环境错误。使用UVM_FATAL报告导致验证无法继续的致命错误(如配置失败、关键组件创建失败)。
  • 为每条信息字符串添加有意义的ID,便于在日志中过滤和搜索。

5. 可配置性

  • 将可能变化的参数(如总线宽度、地址范围、超时时间)定义为uvm_config_db可配置的变量。
  • agent提供is_active配置,使其能在主动和被动模式间切换。
  • 考虑使用factory重载来替换整个组件或事务类型,以实现不同的测试场景,而不是修改原有代码。

6. 典型问题排查与调试实录

在实际项目中,总会遇到各种奇怪的问题。这里记录几个让我印象深刻的案例和排查思路。

问题一:Sequence 产生的激励,Driver 没有收到。

  • 现象:仿真开始,sequence 的body任务在运行,uvm_do宏也执行了,但 driver 中的get_next_item一直阻塞。
  • 排查步骤
    1. 检查连接:首先确认sequencerdriveragentconnect_phase是否正确连接:driver.seq_item_port.connect(sequencer.seq_item_export);
    2. 检查 sequencer 的 arbitration 设置:默认情况下,sequencer 可以处理多个 sequence。如果同时启动了多个 sequence 且没有设置合适的仲裁机制,可能导致阻塞。检查sequencerset_arbitration方法。
    3. 检查 driver 的驱动逻辑:确保 driver 在get_next_item后,处理完事务,一定要调用item_done()。这个调用会通知 sequencer 当前事务处理完毕,sequencer 才会释放对 sequence 的锁定,允许其发送下一个事务。这是最常见的疏忽。
    4. 使用 UVM 调试命令:在仿真命令行中加入+UVM_PHASE_TRACE+UVM_OBJECTION_TRACE,观察相位和 objection 的状态。有时是因为run_phase提前结束了(objection 被过早 drop)。
  • 解决方案:上述案例中,根本原因是 driver 在异常路径下(如遇到错误信号)直接returnbreak,而没有调用item_done()。修复方法是确保在所有退出路径上都调用item_done()

问题二:寄存器后门读写成功,但前门读写失败。

  • 现象:使用ral_model.REG.write(..., UVM_BACKDOOR)可以正确修改 DUT 寄存器值,但使用UVM_FRONTDOOR时,总线波形异常或超时。
  • 排查步骤
    1. 检查适配器:前门访问依赖于uvm_reg_adapter。检查adapterreg2busbus2reg函数是否正确实现了总线协议(如 APB 的psel,penable,pwrite信号生成和采样)。
    2. 检查预测模式:寄存器模型的map设置了预测模式吗?set_auto_predict(1)是自动预测(仅基于前门操作更新镜像),而adapterpredictor组件用于根据总线活动自动更新镜像。如果设置不当,镜像值可能不会更新,导致后续compare失败。
    3. 检查总线监视器连接:如果使用predictor,确保总线monitoranalysis_port正确连接到了predictorbus_in
    4. 查看波形:这是最直接的。找到前门读写时的总线信号,检查时序是否符合协议。重点看地址、数据、读写控制信号是否正确,以及 DUT 的响应(如readyresp)是否被正确采样。
  • 解决方案:多数情况下是adapterbus2reg函数没有正确解析总线响应。例如,对于 APB 协议,需要在penable为高且pready为高的时钟沿采样数据。仔细对照协议规范修改adapter

问题三:覆盖率报告为 0%,但仿真明明运行了相关场景。

  • 现象:仿真日志显示事务在运行,但最后的覆盖率报告显示关键覆盖点为 0%。
  • 排查步骤
    1. 确认采样是否发生:在covergroupsample方法前后添加$displayUVM_INFO,确认采样函数被调用,并且传入的事务数据是预期的。
    2. 检查覆盖点条件coverpoint可能带有iff条件。例如cp_data: coverpoint tr.data iff (tr.valid)。如果tr.valid在采样时不为真,则不会采样。检查所有条件。
    3. 检查 bin 定义:是否定义了过于狭窄的bins?例如,数据是 32 位,但你只定义了bins special = {32‘h12345678},而随机数据几乎不可能命中这个精确值。考虑使用范围binswildcard bins
    4. 检查交叉覆盖的 ignore_bins/illegal_bins:可能你关心的组合被意外地ignore或标记为illegal了。
    5. 合并多个仿真的覆盖率数据库:如果是分布式仿真,需要确保所有子运行的覆盖率数据库(.ucd 文件)被正确合并。检查仿真脚本中的覆盖率收集和合并命令。
  • 解决方案:使用仿真工具(如 VCS 的urg、Questa 的vcover)提供的详细覆盖率报告功能,查看每个覆盖点的命中详情。通常会发现是bin定义不合理或采样条件不满足。调整bin划分策略,或者移除不必要的iff条件。

这些记录只是冰山一角,每个项目、每个设计都会带来新的挑战。核心的调试思路是:从现象出发,沿着数据流和控制流,逐层排查,并善用仿真工具提供的调试功能(波形、日志、调试命令)。养成给关键步骤添加可控制的调试信息(通过verbosity)的习惯,能在问题出现时快速定位。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询