首页 / 资讯中心 / 文章详情

Easier UVM 编码指南:用宏封装告别验证平台八股文

Easier UVM 编码指南:用宏封装告别验证平台八股文 ★ FEATURED ARTICLE
简介这份《Easier UVM编码指南》由Doulos出品面向具备SystemVerilog基础、正在使用或准备引入UVM框架的验证工程师与项目管理者旨在解决团队编码风格不统一、验证环境难以复用与维护的问题。文档围绕命名约定、通用编码规范、时钟与同步、事务与序列、组件结构、配置管理、工厂机制、测试编写、消息报告、寄存器层及功能覆盖率等主题给出具体指导并针对仿真加速器与仿真器场景提出拆分transactor等建议。资源包为1个PDF文件约1.12MB内容为可打印的完整指南便于团队直接采用或合并进公司自有规范。目前已有2169人学习读者可据此快速建立符合行业最佳实践的UVM验证环境提升代码一致性与从块级到系统级的复用能力并借助配套代码生成器与视频教程边学边练。1. 为什么你的 UVM 代码越写越像“八股文”写过几个 UVM 验证平台的人大概都有这种体会driver、monitor、agent、env、test 一层层搭起来代码量不小但真正跟 DUT 行为相关的逻辑可能只占三成剩下七成全是模板化的“规定动作”。更麻烦的是换一个项目这些代码几乎没法直接搬过去改名字、改接口、改 sequence 就得折腾好几天。Easier UVM 编码指南要解决的正是这个问题——它不是某个库也不是某个工具而是一套编码约定和宏封装思路目标是把 UVM 里那些重复度极高的样板代码压缩到最少同时让组件之间的连接关系更清晰、更容易复用。如果你正在搭 UVM 环境或者维护着一套越改越乱的验证平台这套思路值得花时间吃透。它适合已经写过至少一个完整 UVM 环境、对 phase 机制和 factory 有基本概念的工程师新手直接上手可能会觉得“省掉了太多东西反而心里没底”。2. Easier UVM 的核心思路把样板代码关进宏里2.1 传统 UVM 编码到底“重”在哪里先看一个最典型的 driver 写法。标准 UVM 里你要继承uvm_driver用uvm_component_utils注册实现build_phase拿 virtual interface实现run_phase做seq_item_port.get_next_item循环最后还要item_done。一个最简单的 driver 少说三四十行其中真正跟协议相关的可能只有驱动信号那几行。Agent 更夸张要实例化 driver、monitor、sequencer还要在connect_phase里把 sequencer 和 driver 连起来。Env 里又要实例化 agent、scoreboard、coverage再一层层连接。这些代码结构高度固定但每次新项目都得重写一遍写多了就容易出错比如忘了super.build_phase或者connect_phase里端口名写错。Easier UVM 的出发点很直接既然这些结构是固定的那就用宏把它固定下来。你只需要告诉宏“我要一个 driver它驱动哪个接口”剩下的注册、phase 骨架、端口连接都由宏展开完成。这样代码量能压到原来的三分之一甚至更少而且因为结构统一团队里不同人写的组件看起来风格一致维护成本大幅下降。2.2 用宏封装组件一个 driver 的极简写法下面是一个基于 Easier UVM 思路的 driver 写法。注意这里用到的宏不是 UVM 标准库自带的而是你需要在自己项目里定义的一套宏通常放在一个uvm_macros.svh之类的文件里。// 定义一个简单的 APB driver class apb_driver extends uvm_driver #(apb_transaction); uvm_component_utils(apb_driver) virtual apb_if vif; function new(string name, uvm_component parent); super.new(name, parent); endfunction // 用宏替代手写 build_phase 拿接口的样板 uvm_component_build_phase_begin if (!uvm_config_db#(virtual apb_if)::get(this, , vif, vif)) uvm_fatal(NOVIF, virtual interface must be set) uvm_component_build_phase_end // 用宏封装 run_phase 的 get_next_item/item_done 循环 uvm_driver_run_phase_begin (posedge vif.pclk); vif.psel 1b1; vif.paddr req.addr; vif.pwdata req.data; vif.pwrite req.write; (posedge vif.pclk); vif.penable 1b1; (posedge vif.pclk); vif.psel 1b0; vif.penable 1b0; uvm_driver_run_phase_end endclass这段代码里uvm_component_build_phase_begin/end和uvm_driver_run_phase_begin/end就是 Easier UVM 风格的核心。宏展开后build_phase会自动调用super.build_phaserun_phase会自动处理seq_item_port.get_next_item(req)和item_done()你只需要在中间写真正的驱动逻辑。参数方面宏本身不带参数但你可以通过定义不同的宏变体来支持带复位、带时钟等场景。比如有些项目会定义uvm_driver_run_phase_with_reset_begin在循环开始前先等复位释放。注意宏展开后的代码在仿真器里调试时行号可能指向宏定义处而不是调用处这是用宏换简洁的代价。建议在宏内部加上uvm_info打印组件名方便定位。2.3 Agent 和 Env 的自动化连接Agent 的样板代码更多。标准写法要声明 driver、monitor、sequencer 三个成员在build_phase里创建在connect_phase里把 sequencer 的seq_item_export连到 driver 的seq_item_port。Easier UVM 用一组宏把这三步合并成一步。class apb_agent extends uvm_agent; uvm_component_utils(apb_agent) apb_driver drv; apb_monitor mon; uvm_sequencer #(apb_transaction) sqr; function new(string name, uvm_component parent); super.new(name, parent); endfunction uvm_agent_build_phase_begin drv apb_driver::type_id::create(drv, this); mon apb_monitor::type_id::create(mon, this); sqr uvm_sequencer#(apb_transaction)::type_id::create(sqr, this); uvm_agent_build_phase_end uvm_agent_connect_phase_begin drv.seq_item_port.connect(sqr.seq_item_export); uvm_agent_connect_phase_end endclassuvm_agent_build_phase_begin/end宏内部会自动判断 agent 是 active 还是 passive 模式如果是 passive 就不创建 driver 和 sequencer。这个判断逻辑如果手写很容易漏掉或者写错条件。宏把它固定下来减少人为失误。Env 层面也是类似用uvm_env_build_phase_begin/end和uvm_env_connect_phase_begin/end把 agent、scoreboard、coverage 的创建和连接收拢。参数说明agent 的 active/passive 通过uvm_config_db#(uvm_active_passive_enum)::set来配置宏内部会读取这个值。如果你不设置默认是UVM_ACTIVE。这个默认值在大多数场景下没问题但如果你做模块级验证只想被动监测记得在 test 里设成UVM_PASSIVE否则会多出一个 driver 和 sequencer浪费仿真资源。3. 从零搭一个 Easier UVM 风格的验证环境3.1 目录结构和文件划分Easier UVM 不强制目录结构但根据我的经验下面这种划分方式最省心tb/ apb_if.sv // 接口定义 apb_transaction.sv // transaction apb_driver.sv // driver apb_monitor.sv // monitor apb_agent.sv // agent apb_scoreboard.sv // scoreboard apb_env.sv // env apb_test.sv // test uvm_macros.svh // 自定义宏关键是把宏定义单独放一个文件在编译时最先 include。这样所有组件都能用同一套宏改宏定义时全局生效。接口文件独立出来因为 driver 和 monitor 都要用 virtual interface放在同一个包里容易产生编译顺序问题。3.2 宏定义文件怎么写宏定义文件是整个方案的地基。下面给出几个核心宏的实现你可以直接抄到自己项目里改。// uvm_macros.svh ifndef UVM_MACROS_SVH define UVM_MACROS_SVH // build_phase 骨架自动调用 super.build_phase define uvm_component_build_phase_begin \ virtual function void build_phase(uvm_phase phase); \ super.build_phase(phase); define uvm_component_build_phase_end \ endfunction // driver run_phase 骨架自动处理 get_next_item 和 item_done define uvm_driver_run_phase_begin \ virtual task run_phase(uvm_phase phase); \ forever begin \ seq_item_port.get_next_item(req); define uvm_driver_run_phase_end \ seq_item_port.item_done(); \ end \ endtask // agent build_phase根据 active/passive 决定是否创建 driver 和 sequencer define uvm_agent_build_phase_begin \ virtual function void build_phase(uvm_phase phase); \ super.build_phase(phase); \ mon apb_monitor::type_id::create(mon, this); \ if (get_is_active() UVM_ACTIVE) begin define uvm_agent_build_phase_end \ end \ endfunction endif逻辑说明uvm_component_build_phase_begin展开后就是一个标准的build_phase函数头并且已经调用了super.build_phase(phase)。你只需要在 begin 和 end 之间写自己的配置获取代码。uvm_driver_run_phase_begin展开后是一个forever循环里面第一句就是get_next_item所以你写的驱动逻辑天然就在正确的循环位置。uvm_agent_build_phase_begin里用get_is_active()判断这个函数是uvm_agent自带的返回当前 agent 的 active/passive 状态。参数怎么改如果你用的 transaction 类型不是apb_transaction把宏里的类型名替换成你的类型即可。更好的做法是宏不写死类型而是用uvm_agent_build_phase_begin(TXN)带参数但这样宏定义会复杂一些。我一般建议初期先写死等项目稳定了再参数化。3.3 一个最小可跑的 test 和 sequence有了上面的组件test 和 sequence 反而简单了。Easier UVM 风格下test 只负责配置环境和启动 sequence。class apb_base_test extends uvm_test; uvm_component_utils(apb_base_test) apb_env env; function new(string name, uvm_component parent); super.new(name, parent); endfunction virtual function void build_phase(uvm_phase phase); super.build_phase(phase); env apb_env::type_id::create(env, this); uvm_config_db#(virtual apb_if)::set(this, env.agent.*, vif, apb_if_inst); endfunction virtual task run_phase(uvm_phase phase); apb_simple_seq seq; phase.raise_objection(this); seq apb_simple_seq::type_id::create(seq); seq.start(env.agent.sqr); phase.drop_objection(this); endtask endclass这里没有用宏因为 test 的build_phase和run_phase差异太大强行宏封装反而降低可读性。Easier UVM 的原则是结构高度一致的组件driver、monitor、agent用宏差异大的组件test、scoreboard手写。uvm_config_db的 set 路径env.agent.*表示对 env 下 agent 的所有子组件生效这样 driver 和 monitor 都能拿到同一个 virtual interface。sequence 的启动用seq.start(env.agent.sqr)注意这里 sqr 是 agent 里的 sequencer 实例名要跟 agent 里的声明一致。提示如果你在 agent 里把 sequencer 命名为sqr在 test 里就要用env.agent.sqr。命名不一致是新手最常见的翻车点之一建议团队统一用sqr、drv、mon这三个缩写。4. 避坑与常见问题排查4.1 宏展开后编译报错“undefined macro”现象编译时提示某个宏未定义比如uvm_driver_run_phase_begin找不到。原因通常是宏定义文件没有被正确 include或者 include 顺序在组件文件之后。解决在编译脚本里把uvm_macros.svh放在所有 UVM 组件文件之前并且确保include路径正确。如果你用的是 VCS可以在 filelist 里把宏文件放第一行。4.2 agent 的 active/passive 配置不生效现象明明在 test 里设了UVM_PASSIVE但仿真时 driver 还是在跑。原因可能是uvm_config_db的 set 路径写错了或者 agent 的build_phase里读取配置的时机不对。解决在 agent 的build_phase开头用uvm_config_db#(uvm_active_passive_enum)::get(this, , is_active, is_active)读取并且确保 test 里的 set 路径是env.agent而不是env.agent.*。另外注意get_is_active()返回的是uvm_active_passive_enum类型比较时用UVM_ACTIVE而不是1。4.3 driver 里 get_next_item 和 item_done 不配对现象仿真卡死sequence 发不出下一个 transaction。原因通常是 driver 的run_phase里get_next_item之后没有调用item_done或者item_done被放在了条件分支里导致某些路径漏掉。解决用 Easier UVM 的uvm_driver_run_phase_begin/end宏宏展开后item_done固定在循环末尾不会漏。如果你手写建议用try_get_next_item或者确保每个get_next_item后面都有item_done。4.4 virtual interface 在 driver 里是 null现象仿真时报uvm_fatal提示 virtual interface 没设置。原因通常是uvm_config_db的 set 路径和 get 路径不匹配。比如 test 里 set 的是env.agent.drv但 driver 里 get 的是vif路径对不上。解决统一用通配符env.agent.*来 set这样 agent 下所有组件都能拿到。另外注意 set 的第二个参数是相对路径第三个参数是接口名第四个参数是接口实例。四个参数任何一个写错都会导致 get 失败。4.5 宏里的类型名写死导致复用困难现象想把 APB 的 driver 宏用到 AXI 项目里发现宏里写死了apb_transaction。原因宏定义时没有参数化。解决把宏改成带参数的形式比如define uvm_driver_run_phase_begin(TXN)然后在展开时用TXN替换类型名。但这样宏的调用会变成uvm_driver_run_phase_begin(axi_transaction)稍微麻烦一点。我的习惯是项目初期用写死的宏快速跑通等项目稳定、需要跨项目复用时再参数化。5. 进阶用宏工厂和配置表把复用性再提一档前面讲的宏封装解决了“写”的问题但“复用”还有一层不同项目之间怎么快速迁移。我的做法是建一个宏工厂文件把常用组件的宏按协议分类再配一张配置表。// macro_factory.svh define DEFINE_APB_AGENT(NAME) \ class NAME extends uvm_agent; \ uvm_component_utils(NAME) \ apb_driver drv; \ apb_monitor mon; \ uvm_sequencer#(apb_transaction) sqr; \ uvm_agent_build_phase_begin \ drv apb_driver::type_id::create(drv, this); \ sqr uvm_sequencer#(apb_transaction)::type_id::create(sqr, this); \ uvm_agent_build_phase_end \ uvm_agent_connect_phase_begin \ drv.seq_item_port.connect(sqr.seq_item_export); \ uvm_agent_connect_phase_end \ endclass用的时候直接DEFINE_APB_AGENT(apb_agent)就定义了一个完整的 agent 类。配置表则是一个简单的 SystemVerilog 关联数组或者 UVM 的uvm_config_db封装把接口名、时钟频率、超时时间这些项目相关参数集中管理。这样换项目时只需要改配置表里的几行组件代码基本不动。验证方法上我一般会跑一个最小回归一个 sequence 发 10 个 transactionscoreboard 比对通过覆盖率收集到基本功能点。如果这个回归能在 5 分钟内跑完说明环境搭建没问题。进阶技巧是给宏加上uvm_info打印在build_phase和connect_phase结束时输出组件树这样一眼就能看出 agent 是 active 还是 passive、sequencer 有没有连上 driver。这个打印在调试连接问题时比看波形快得多。血泪经验我曾经在一个项目里为了省事把宏定义直接写在 driver 文件里结果另一个 driver 想用同一个宏时找不到定义编译报错查了半天。后来养成习惯所有宏必须放在独立的uvm_macros.svh里并且用ifndef保护。还有一次agent 的 passive 配置没生效原因是 test 里 set 的路径写成了env.agent但 agent 在 env 里的实例名是apb_agent路径应该是env.apb_agent。这种拼写错误没有编译器报错只能靠仿真行为反推所以我现在都会在 agent 的build_phase里加一句uvm_info打印get_is_active()的返回值一眼就能确认配置有没有传进来。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站