Deep Dive

UCDB Anatomy: What Your Coverage Database Actually Stores

Hiroshi Watanabe · 10 min read
UCDB schema structure diagram

Most DV engineers interact with UCDB through their simulator's GUI or a few urg and vcover command invocations. The database exists, coverage gets merged, reports get generated. The underlying file format stays opaque — which is fine until you want to do something more sophisticated with the data than read the default summary report.

When we built Photoniq's coverage analysis engine, we spent a significant amount of time inside UCDB files directly. What the format actually stores is richer — and more structurally interesting — than the summary report suggests. This walkthrough covers the schema from the coverage type hierarchy down to individual bin representation, with notes on what each layer means for AI-guided analysis.

UCDB Is a Hierarchical Database, Not a Flat Report

The first thing to understand is that UCDB stores coverage data in a tree structure that mirrors your design hierarchy. The root is the test plan or testbench top level. Below that, scope nodes represent design instances (module instances, hierarchical names in SystemVerilog) that contain coverage items. Coverage items are the leaves: toggle points, FSM objects, line/branch coverage markers, covergroup instances.

This hierarchy is not just organizational metadata. It carries the RTL context needed to understand what a coverage hole means. A toggle miss on a signal 6 levels deep in a datapath module means something different from the same miss on a top-level control signal — and the path through the design hierarchy is what tells you which one you're looking at.

The scope hierarchy in UCDB maps to SCOPE records. Each scope has:

  • A type identifier (module instance, generate block, function, task, etc.)
  • A source location reference (file path + line number)
  • A parent scope reference
  • Child scope list
  • Coverage item list

The source location is important: it ties the UCDB record back to the RTL line. When we do coverage gap analysis, the scope hierarchy + source location is how we map a coverage hole to the relevant RTL segment.

Toggle Coverage: What Actually Gets Recorded

Toggle coverage is the lowest-level structural metric. A toggle coverage item in UCDB tracks whether a signal has transitioned in both directions: 0→1 and 1→0. For multi-bit signals, toggles are tracked per bit.

Each toggle item has a bin count of 2: one bin for each transition direction. The count field records how many times each transition occurred during simulation. A bin with count 0 is an uncovered toggle — that signal never made that transition.

What toggle coverage does not tell you: whether those transitions happened at the right time, in the right relationship to other signals, or in the right context. A signal that toggled 10,000 times in a loop but never toggled during a specific protocol sequence still shows as 100% toggle covered. Toggle coverage is necessary but far from sufficient for functional correctness confidence.

For AI analysis purposes, toggle coverage holes are informative primarily as structural indicators. A toggle miss on a clock enable, a chip select, or a reset signal often points to an entire sub-circuit that never executed. A toggle miss on a data bus bit in the middle of an otherwise fully-covered datapath suggests a specific data value range was never exercised.

FSM State and Transition Coverage

FSM coverage items in UCDB represent finite state machines that the simulator has identified or that you've declared with coverage annotations. Each FSM item contains two sub-structures: a state table and a transition table.

The state table lists all legal states (identified by state encoding or label if you used state label annotations) with hit counts per state. The transition table lists all legal transitions between states with hit counts per transition.

The transition record is the more diagnostic of the two. A state that was never entered shows up in both tables — as a state miss and as misses on all transitions that target that state. But a transition miss where both the source and destination states are individually covered indicates a specific path between two visited states that was never taken. These are the most interesting FSM coverage holes for bug-escape analysis: the design visited both states but never transitioned between them via that path, which often represents a protocol sequence or error recovery path that was exercised in neither direction.

UCDB represents FSM transitions as directed edges: (source_state_id, destination_state_id, hit_count, first_hit_time). The first_hit_time is useful for campaign analysis — it shows you how late in the simulation run a particular state was first entered, which correlates with how much directed effort it took to reach.

Line and Branch Coverage Items

Line coverage in UCDB is stored at the statement level, not the source line level — a single RTL source line that contains multiple statements gets multiple line coverage items. Each item has a single hit count. Zero means the statement never executed.

Branch coverage is more structured. Each branch item represents a conditional: if/else, case statement, ternary operator, generate conditional. The item contains one bin per branch arm, each with its own hit count. A branch item where one arm has hit_count = 0 is a branch miss — the logic always evaluated to the other arm.

Branch misses are particularly informative for functional bug prediction. An if-else where the else branch was never executed means the error handling or edge-case path in that conditional was never tested. In synthesizable RTL, those else branches often correspond to: reset handling, overflow protection, arbitration tie-breaking, or protocol violation recovery. These are exactly the kinds of logic that cause field failures when they contain bugs.

We're not claiming that every uncovered else branch contains a bug. Most don't. But the structural property of "logic that runs only under exceptional conditions" correlates with higher defect risk than logic on the nominal execution path, and that correlation is what makes branch coverage a useful signal for test prioritization.

Functional Coverage: Covergroups, Coverpoints, and Bins

Functional coverage is the richest and most application-specific layer in UCDB. Unlike structural metrics (toggle, line, branch) which are derived automatically from the RTL, functional coverage is explicitly declared in the testbench via SystemVerilog covergroups.

In UCDB, a covergroup instance appears as a scope node containing coverpoint items. Each coverpoint item represents one coverage variable. The coverpoint contains a bin list — the set of value ranges or transitions you've declared you care about — with hit counts per bin.

The bin structure is where most of the interesting information lives. A coverpoint on a 5-bit field with the default auto-binning produces 32 bins, one per value. A coverpoint with explicit bins might have a handful of manually-declared ranges, plus a default bin for everything else. Crossing two coverpoints produces a cross item whose bins are the Cartesian product of the source coverpoint bins.

Cross bins are the heaviest items in a typical UCDB. A 2-way cross between a 4-bit field and a 3-bit field produces 128 bins. A 3-way cross can produce thousands. Most of those bins fill quickly during constrained-random simulation. The ones that don't are disproportionately the ones worth analyzing, because their emptiness usually means a specific combination of conditions was never tested.

One UCDB subtlety that matters for external analysis tools: the at_least attribute on a coverpoint bin specifies the minimum hit count required for that bin to be considered covered. The default is 1, but some testbenches set higher values for statistical confidence. When you read a UCDB and see a bin with hit_count > 0 marked as uncovered, it means the hit count is below the declared at_least threshold — the combination occurred, just not enough times to reach the required statistical confidence level.

How Photoniq Reads UCDB for Coverage Gap Analysis

When we ingest a UCDB file, we parse the full scope hierarchy and build an internal graph that links coverage items back to their source RTL. Toggle misses get mapped to the signals they represent. FSM transition misses get mapped to the RTL state machine implementation. Functional coverage bin misses get tagged with the covergroup definition and the semantic meaning of the bin (derived from its label, its value range, and the covergroup context).

This graph is the input to our coverage gap analysis model. The model isn't just counting uncovered bins — it's reasoning about which uncovered bins are structurally related (missing FSM state → missing toggle on the output signal that drives the FSM → missing branch on the condition that guards the transition), which uncovered regions carry higher defect risk based on the type and position of the RTL logic, and what stimulus characteristics are likely to exercise each uncovered region.

The UCDB structure matters here because the hierarchy tells us context. A functional coverage bin miss in a covergroup that lives inside the testbench's protocol layer tells us something different from a bin miss in a covergroup that directly monitors a hardware counter. The scope path from the UCDB is what preserves that context through the analysis.

Practical Notes on UCDB Export and Merging

A few UCDB handling details that affect analysis quality:

Merge semantics. When you merge UCDB files from multiple simulation runs with vcover merge or equivalent, bin hit counts are summed. This is correct for coverage accumulation purposes. But the merged database loses per-run timing information — you can no longer tell which run first hit a particular bin. If you want per-run analysis (to understand which tests contributed to which coverage), keep the per-run UCDB files alongside the merged one.

Scope filtering. Most simulators let you scope coverage collection to specific modules or exclude modules (typically VIP instances, memory models, third-party IP). The exclusion list is stored in the UCDB. When we read a UCDB, excluded scopes are treated as intentionally not measured, not as uncovered — they don't appear in our analysis output as coverage gaps.

Simulator-specific extensions. VCS, Questa, and Xcelium all write standard UCDB, but they differ in how they represent certain items — particularly in FSM detection heuristics, in how they handle generate loops, and in what metadata they include for cross bins. We'll cover these differences in a separate article on simulator compatibility. For now, the key point is that a UCDB exported from Questa may have subtly different structural properties than the same design exported from VCS, and parsing code that assumes one simulator's dialect will misread items from another.

Understanding the UCDB structure is prerequisite knowledge for anyone building tooling on top of simulator coverage data. It's also prerequisite knowledge for understanding why coverage reports that look identical at the summary level can represent meaningfully different verification states underneath.