SystemVerilog verification environment for an I2C master

A layered, class-based SystemVerilog verification environment for an OpenCores I2C multi-bus master controller with a Wishbone host interface, driven to functional coverage closure against a written verification plan.

The device under test sits between two protocols at once. A host programs it over Wishbone by writing command and data registers; the controller then drives the I2C bus on the other side. Verifying it means observing both worlds and checking that what happened on the wire matches what the host asked for.

Environment structure

Component Role
Wishbone agent Driver and monitor for the host-side register interface
I2C agent Driver and monitor for the serial bus, including a responding slave model
Generator Layered stimulus, organized into seven directed phases plus a randomized burn-in
Predictor Decodes host-side register writes into the I2C transactions they should produce
Scoreboard Records observed bus-level transfers
Coverage subscriber Samples monitored traffic into the functional coverage model

The environment is built on a class-based framework using configuration lookup for virtual interfaces and analysis-style connections between monitors and subscribers. It is not UVM, though it follows the same architectural pattern — agents wrapping drivers and monitors, stimulus separated from checking, coverage collected passively from monitored traffic rather than from the stimulus generator.

Coverage model

Four covergroups spanning 18 coverpoints and crosses across 87 bins, each mapped to an item in a 22-entry verification plan:

  • Register access — address and read/write direction, crossed, plus data-value ranges on the data port
  • Control and status — the core enable and interrupt enable bits, and all four combinations of the two
  • Command activity — every command opcode, the response codes returned by polling, their cross, legal command-to-command transitions, and bus selection
  • Bus transfers — read against write, slave address ranges, transfer sizes, data values, and the crosses that matter

Writing the plan first and mapping covergroups onto it one-for-one is what made closure measurable. Coverage that is not tied to a plan tells you how much you sampled, not how much you verified.

Results

Metric Result
Register-access coverage 100%
Control and status coverage 100%
Command coverage 96.7%
DUT toggle coverage 81%
Wishbone transactions driven 3,214

Run under Questa, merging functional and code coverage into a unified database and reporting against the test plan.

Notes for revisiting this

Why layering matters

Each layer has one job. The driver knows how to wiggle pins and nothing about intent. The monitor knows how to reconstruct transactions from pins and nothing about correctness. The scoreboard knows about correctness and nothing about pins. Collapsing any two together produces a testbench that cannot be reused when the protocol changes, and one that can hide bugs by checking against its own stimulus rather than against the specification.

Monitor versus driver, and why coverage samples the monitor

The driver causes activity; the monitor observes it. Coverage must be sampled from the monitor, because sampling from the driver records what you intended rather than what the design actually did. If the DUT drops a transaction, driver-side coverage still counts it and reports a hole that does not exist.

Predictor and scoreboard

The predictor models what the design should do — decode a host-side register write into the I2C transaction it ought to produce. The scoreboard compares that expectation against what the monitor observed. This is the same reference-model idea as CPU verification, one abstraction level down.

Why the verification plan comes first

Coverage without a plan measures how much you happened to exercise, not how much you meant to. Mapping four covergroups onto 22 planned items means a coverage report answers “which of my intended checks are unproven” rather than “here is a number.” That mapping is why 96.7% command coverage is actionable — you can name the two bins still open.

Functional coverage versus code coverage

Code coverage (81% toggle here) says which parts of the DUT your stimulus reached. Functional coverage says which scenarios you verified. Both are necessary and neither is sufficient: 100% code coverage with no functional coverage means you executed every line without checking whether the behaviour was correct, and 100% functional coverage with poor code coverage means your plan missed whole regions of the design.

The I2C protocol, quickly

Two open-drain wires, SCL and SDA, with pull-ups. A start is SDA falling while SCL is high; a stop is SDA rising while SCL is high — the only two times SDA may change while SCL is high, which is what makes them unambiguous framing. Data bytes are eight bits, most-significant first, each followed by an ACK from the receiver pulling SDA low. A master reading multiple bytes ACKs each one and NACKs the last, which is how it signals “stop sending.”

Why open-drain matters

Devices can only pull the line low, never drive it high; pull-up resistors restore the high state. That is what allows multi-master arbitration without contention — a master that intends to drive high but observes low has lost arbitration and withdraws — and it enables clock stretching, where a slow slave holds SCL low to delay the master.

Questions worth being ready for

Why is this not UVM? It is built on a course framework with the same architecture — agents, configuration lookup, analysis-style connections — but not the UVM base classes. Worth saying plainly rather than letting someone discover it.

How would you convert it to UVM? Transactions become uvm_sequence_item, the generator’s phases become sequences on a sequencer, components extend uvm_component with build_phase and connect_phase, monitor output moves onto uvm_analysis_port, and the config lookup becomes uvm_config_db. The structure survives; the base classes change.

Why is command coverage only 96.7%? Two bins in the command-to-response cross remain unhit. Knowing precisely which holes remain, and why, is the point of mapping to a plan.

Why randomize at all if directed tests hit the plan? Directed tests only find bugs you thought of. Randomized burn-in explores orderings and value combinations nobody enumerated, which is where protocol bugs actually live.

What this project does not cover

No repeated-start sequences, no 10-bit addressing, no clock stretching, no multi-master arbitration, and no fast-mode timing — the DUT supports 100 kHz standard mode here. Assertion-based checking is also absent; the checking is scoreboard-based.

Context

Built for ECE 745 (ASIC Verification) at NC State University.