Organizing a UVM project means placing every class in a SystemVerilog package, layering the packages so each one imports only the layers below it, giving each agent and environment its own directory, and following a small set of coding guidelines consistently. The usual layers are agent and register packages at the bottom, an environment package and sequence packages above them, a test package above those, and the top-level module importing the tests.
This article describes a layout that is common across the industry. The details vary between teams, but the principles, one package per reusable unit and no upward imports, appear almost everywhere.
8 min read | Intermediate
What You'll Learn
- Group testbench classes into layered packages
- Lay out directories so agents and environments can be reused
- Avoid the compile problems that come from including files in the wrong place
- Apply a set of practical UVM coding guidelines
Why Packages
A SystemVerilog package is a named scope that holds class definitions, types and functions. Code elsewhere uses them with import. How packages work in general is covered in SystemVerilog Package. For UVM there are three practical reasons to put every class in one:
- One definition per class. If the same class file is
`included in two different scopes, SystemVerilog creates two different, incompatible types with the same name. Defining each class once, inside a package, avoids this entirely. - Factory registration. A class registers with the UVM factory when its package is elaborated. A test package that the top module never imports is never registered, and
run_test()cannot find the test. - Separate compilation. Each package can be compiled on its own, in dependency order, which speeds up builds and gives clearer error messages.
The Package Layers
Each layer imports uvm_pkg and the layers below it, never above:
- Agent packages, one per interface: the sequence item, the agent configuration object, driver, monitor, sequencer typedef, agent and any low-level sequences for that interface.
- Utility packages at the same level: the register model, shared types, test parameters.
- The environment package: imports the agent and utility packages, and contains the environment, its configuration object, scoreboards, predictors and coverage.
- Sequence packages: import the agent packages and the register model, and contain the test-level and virtual sequences.
- The test package: imports everything below and contains the base test and every derived test.
- The top module: imports
uvm_pkgand the test package, instantiates the DUT and interfaces, and callsrun_test().
Because nothing imports upwards, there are no circular dependencies, and a lower layer, such as an agent package, can be reused in another project without dragging the rest of the testbench along.
An Agent Package
An agent package is a short file that imports UVM and includes the agent's class files in dependency order. Each included file holds exactly one class:
// apb_agent_pkg.sv
package apb_agent_pkg;
import uvm_pkg::*;
`include "uvm_macros.svh"
`include "apb_item.svh" // transaction first: everything else uses it
`include "apb_agent_cfg.svh"
`include "apb_driver.svh"
`include "apb_monitor.svh"
typedef uvm_sequencer #(apb_item) apb_sequencer;
`include "apb_agent.svh"
`include "apb_base_seq.svh" // simple sequences that come with the agent
endpackage : apb_agent_pkg
The included .svh files contain only the class itself, with no import or `include lines of their own. Everything they need is already in scope inside the package. Order matters, because SystemVerilog needs a class defined before another class refers to it. If two classes refer to each other, a forward declaration such as typedef class apb_agent; breaks the cycle.
Directory Layout
The directory structure mirrors the packages. Each reusable unit gets its own directory, so it can be copied or shared as a whole:
dma_verif/
agents/
apb_agent/ apb_agent_pkg.sv apb_item.svh apb_driver.svh apb_monitor.svh ...
axi_agent/ axi_agent_pkg.sv ...
reg_model/ dma_reg_pkg.sv dma_reg_block.svh
env/ dma_env_pkg.sv dma_env.svh dma_env_cfg.svh dma_scoreboard.svh dma_coverage.svh
sequences/ dma_seq_pkg.sv dma_base_vseq.svh dma_init_vseq.svh ...
tests/ dma_test_pkg.sv dma_base_test.svh dma_smoke_test.svh ...
tb/ tb_top.sv dma_if.sv
sim/ file lists, scripts, run directories
Interfaces are not classes, so they stay outside packages in their own .sv files, usually next to the agent that uses them or in the tb directory. Compile order then follows the layers: uvm_pkg, interfaces, agent and register packages, the environment and sequence packages, the test package, and finally the top module.
Quick Check
Q: apb_item.svh is included in both apb_agent_pkg and dma_env_pkg. The environment's scoreboard is connected to the agent's monitor. What goes wrong?
Show Answer
There are now two different types called apb_item, one in each package. The monitor's analysis port carries apb_agent_pkg::apb_item, while the scoreboard expects dma_env_pkg::apb_item, so the connection fails to compile with a type mismatch. The environment package should import apb_agent_pkg instead of including the file again.
Coding Guidelines
The following guidelines are widely used, and most of them prevent a specific, real problem:
- One class per file, named after the class.
apb_driver.svhholdsapb_driver, and nothing else. Classes are then easy to find, and version control history stays per class. - Package names end in
_pkg. It makes clear at a glance what animportrefers to. - Register every class with the factory and create every object with
type_id::create(). Otherwise tests cannot override it later. - Constructors take only
nameand, for components,parent. The factory passes nothing else, so extra arguments always keep their defaults. The constructor should callsuper.new(), create covergroups and nothing more; components build their children inbuild_phase. - Give each agent a configuration object holding its virtual interface, active or passive mode and options, passed through
uvm_config_db. - Use the message macros such as
`uvm_infoand`uvm_errorrather than$display, so verbosity and severity controls work. - Raise objections per test or sequence, not per transaction. Raising and dropping an objection for every item costs simulation time for no benefit.
Some published guideline sets go further and recommend avoiding field macros, the `uvm_do family, callbacks or custom phases. These are trade-offs rather than rules: field macros and `uvm_do save typing but cost some performance and debug visibility, and the tutorial pages on UVM Field Macros and How to use `uvm_do sequence macros explain both sides. What matters most is that a team picks one style and applies it consistently.
Naming conventions vary between teams, for example a m_ prefix for class members or _t for typedefs. Which one you choose matters much less than everyone on the project using the same one.
Common Mistakes
Mistake #1: Including a class file in more than one place
Each `include creates a separate type. Include every class file in exactly one package and import that package wherever the class is needed.
Mistake #2: Imports inside class files
An import or `include inside an .svh class file is really added to whichever package includes it, which hides dependencies and can import the same package repeatedly. Keep imports in the package file only.
Mistake #3: Not importing the test package in the top module
If the top module never imports the test package, the tests are never registered with the factory, and run_test() fails with a test-not-found error at time zero.
Mistake #4: Upward imports between layers
An agent package that imports the environment package ties the agent to one environment and can create circular dependencies. Lower layers must never import higher ones.
Summary
In this article, you learned:
- Every UVM class belongs in exactly one package; including a file twice creates two incompatible types
- Packages are layered: agents and register models, then environment and sequences, then tests, then the top module
- Each agent and environment gets its own directory and package, so it can be reused on its own
- Package files hold the imports; class files hold one class each and nothing else
- Guidelines such as factory creation, name-and-parent constructors and per-agent configuration objects prevent common bugs, while style choices like field macros are trade-offs