AMBA APB · Module 15
APB Functional Coverage
The APB functional-coverage model — a covergroup sampled per transaction across direction, address region, response, wait-count, and PSTRB, with the crosses (direction × response, region × response) that prove every interesting scenario was actually exercised. Coverage answers 'did we test it?'; assertions answer 'did it pass?' — and the wait-count coverpoint is the classic hole.
You have built an environment that checks APB — now you must prove it actually exercised the cases worth checking. Assertions fire when something is wrong; a scoreboard catches wrong data. But neither tells you whether the test ever drove a write that errored at the reserved region, or a transfer the responder held with two wait states. A passing, assertion-clean regression can be a regression that never reached the interesting corner at all. The single idea to carry: functional coverage is a covergroup, sampled once per transaction from the monitor's analysis port, that records which scenarios were exercised — direction, address region, response, wait-count, PSTRB pattern, and their crosses — so that closing it guarantees no meaningful case (especially wait-states and error-on-each-region) went un-tested. Coverage measures exercised; assertions and the scoreboard measure correct. Both are required, and they answer different questions.
1. Problem statement
The problem is measuring, per transaction, which interesting APB scenarios the verification actually exercised — so that "we ran a lot of tests and nothing failed" can be upgraded to "we provably hit every direction, region, response, and wait-state combination that matters."
A green regression is a weak claim on its own. It says nothing observed was wrong; it does not say everything worth observing was observed. A constrained-random test that, by luck of its seeds, only ever issued zero-wait reads to the CTRL register can pass forever while the held-bus paths, the error responses, and the partial-byte writes go completely un-driven. The bug lives in a path the stimulus never reached, so no assertion ever had a chance to fire on it. Functional coverage is the instrument that closes this gap, and it has three demands:
- It must be sampled from real observed transactions, not from the stimulus. Coverage is collected from the monitor's reconstructed transaction — what actually appeared on the bus — not from what the sequence intended. Sampling the intent measures your generator; sampling the observed transfer measures the DUT's exercised behaviour, which is the thing that matters.
- It must enumerate the scenarios that matter, with bins fine enough to distinguish them. Direction (read vs write), address region (each mapped register block), response (OKAY vs PSLVERR), wait-count (0, 1, 2..N — including the tail), and PSTRB byte-lane pattern. A coverpoint with too few bins reports a high percentage that means nothing — the classic failure where a single 0-wait bin reads 100% while every held-bus path is un-tested.
- It must capture the combinations, not just the marginals. Hitting "write" and "PSLVERR" separately does not prove you ever saw a write that errored. The cross — direction × response, region × response — is what forces the per-region error path and the per-direction wait path to be exercised.
So the job is not "did the tests pass?" — assertions and the scoreboard answer that. It is "did the tests go everywhere interesting?" — and only an explicit, per-transaction coverage model can answer it.
2. Why previous knowledge is insufficient
Everything earlier in this module checks correctness; none of it measures exercise. That is precisely the gap coverage fills.
- The monitor gives you the transaction; it does not tell you which transactions you saw. Module 15.3 built the monitor that reconstructs each APB transfer and broadcasts it on an analysis port. Coverage is the subscriber to that port — it consumes the same stream the scoreboard does, but where the scoreboard asks "is this transfer correct?", coverage asks "which kind of transfer is this, and have we seen it before?" Without coverage, the analysis port's transaction stream is checked but never catalogued.
- Assertions prove the bus never broke a rule — not that the rule's scenario ever occurred. Module 15.2 wrote SVA that fires on illegal behaviour. But an assertion that never fires proves only that nothing illegal happened — it cannot distinguish "the PSLVERR rule held because errors were handled correctly" from "the PSLVERR rule held because no error was ever driven." A passing error-response assertion on a test that never triggered an error is a green light over an un-tested path. Coverage is what demands the error actually be driven.
- Wait-state behaviour is the canonical un-exercised path. Wait-state verification is hard precisely because responders default to zero waits and random stimulus rarely stretches the tail. Knowing how wait states work does not make them occur in your regression — only a wait-count coverpoint with 0/1/N bins, backed by a wait-injecting responder, forces them. This is the single most common APB coverage hole.
Coverage is also what makes the later UVM APB agent self-reporting: the agent's coverage collector, subscribed to the monitor, is how a reusable agent proves its own thoroughness in any environment it is dropped into. So the model to add is the exercise ledger: a per-transaction record of which scenarios the regression actually reached, sitting beside — and distinct from — the correctness checks.
3. Mental model
The model: coverage is the attendance sheet of your verification; assertions and the scoreboard are the exams. The exam tells you whether each student who showed up answered correctly. The attendance sheet tells you which students showed up at all. A class where everyone present scored 100% looks perfect — until the attendance sheet reveals that half the students never came to class. A green regression is the perfect exam; coverage is the attendance check that asks "did the interesting scenarios even show up?"
Three refinements make it precise:
- One covergroup, sampled once per observed transaction. The covergroup instance lives in a coverage collector subscribed to the monitor's analysis port. Each time a transaction arrives, you call
cg.sample(item)— and the coverpoints (direction, region, response, wait-count, PSTRB) each drop the transaction into a bin. Coverage accumulates across the whole regression; closure is "every meaningful bin and cross-cell got at least one transaction." - Crosses are where the real scenarios live. A coverpoint hits its bins independently — you can reach 100% on
cp_dirand 100% oncp_respwhile never having seen a single write that errored. The crossdirection × responsehas a cell for exactly that, andregion × responsehas a cell for "did we see an error at each region?" — the per-region error path that marginal coverage silently misses. - A hole is an un-tested path, not a bug. An empty bin means "this scenario was never exercised" — it is a gap in what you tested, not evidence of a defect. You close it by driving stimulus until the bin fills (or by declaring it
illegal_bins/ignore_binsif it genuinely cannot occur). The discipline is: every meaningful empty bin must be explained — exercised, or formally excluded — before sign-off.
4. Real SoC implementation
In a real APB environment the covergroup lives in a coverage collector — a UVM subscriber on the monitor's analysis port — and is sampled with the reconstructed transaction. The wait-count and region fields are derived by the monitor (cycles PREADY was low; the address decoded against the register map) and carried on the transaction item. A realistic model:
// APB functional-coverage collector — subscribes to the monitor analysis port
// and samples ONE covergroup per observed transaction. Measures EXERCISE,
// not correctness (assertions + scoreboard own correctness).
class apb_coverage extends uvm_subscriber #(apb_seq_item);
`uvm_component_utils(apb_coverage)
apb_seq_item item; // current transaction, set in write()
covergroup apb_cg;
option.per_instance = 1;
// --- direction: read vs write ---
cp_dir : coverpoint item.pwrite {
bins rd = {0};
bins wr = {1};
}
// --- address region: one bin per mapped register block ---
cp_region : coverpoint item.region { // region enum from the monitor's decode
bins ctrl = {CTRL};
bins status = {STATUS};
bins data = {DATA};
bins reserved = {RESERVED}; // accesses to a hole must be exercised too
}
// --- response: OKAY vs slave error ---
cp_resp : coverpoint item.pslverr {
bins okay = {0};
bins err = {1};
}
// --- wait-state count: 0 / 1 / the tail. THE classic APB hole. ---
cp_wait : coverpoint item.wait_cycles { // PREADY-low cycles, counted by the monitor
bins zero = {0};
bins one = {1};
bins many = {[2:$]}; // 2..N — must NOT be left as a single 0 bin
}
// --- PSTRB byte-lane pattern (writes): all-lanes vs partial ---
cp_strb : coverpoint item.pstrb iff (item.pwrite) {
bins full = {'hF}; // every byte lane enabled
bins partial = default; // sparse / sub-word writes
}
// --- CROSSES: where the real scenarios live ---
// "did we see a write that errored?" and "an error at EACH region?"
x_dir_resp : cross cp_dir, cp_resp;
x_region_resp : cross cp_region, cp_resp; // forces error-per-region to be exercised
x_dir_wait : cross cp_dir, cp_wait; // forces held-bus reads AND writes
endgroup
function new(string name, uvm_component parent);
super.new(name, parent);
apb_cg = new(); // construct the covergroup
endfunction
// Called by the analysis port for every observed transaction.
function void write(apb_seq_item t);
item = t;
apb_cg.sample(); // one sample per transfer on the bus
endfunction
endclassTwo facts make this the right shape. First, it samples the observed transaction, not the stimulus — write() is invoked by the monitor's analysis port, so every bin reflects what genuinely appeared on the bus, including DUT-driven responses and wait counts the sequence never explicitly requested. Second, the crosses carry the model's real value: x_region_resp is what turns "we saw some errors" into "we saw an error at the STATUS register, at the DATA register, and at the RESERVED region" — the per-region error path that marginal cp_resp coverage would report as 100% while leaving most of those cells empty. (Accesses to the reserved/unmapped space have their own verification concerns that this region bin makes visible.) Correctness for all of these is decided by the scoreboard and the assertions — the covergroup only records that the scenario happened.
5. Engineering tradeoffs
A coverage model is a sequence of judgement calls about bin granularity, what to cross, and what to formally exclude. Each cell below names the coverpoint, its bins, why it matters, and the bug a missing bin lets hide.
| Coverpoint | Bins | Why it matters | Bug an un-covered bin hides |
|---|---|---|---|
| Direction | rd, wr | Read and write exercise different DUT paths (data drive vs capture) | A write-only or read-only test that never touches the other datapath |
| Region | one per mapped block (CTRL, STATUS, DATA, RESERVED) | Each register block decodes and responds differently | An un-accessed register whose decode/access logic is never tested |
| Response | okay, err | OKAY and PSLVERR drive different completion handling | Error-handling logic (and the PSLVERR assertion) never actually triggered |
| Wait-count | zero, one, many ([2:$]) | Held-bus paths only run when PREADY is held low — the classic hole | A wait-state bug: signals drift or the manager mis-handles a stretched ACCESS |
| PSTRB pattern | full, partial | Partial-byte writes exercise byte-lane merge/mask logic | A sub-word write corrupting adjacent bytes, never exercised |
| dir × resp | rd-okay, rd-err, wr-okay, wr-err | Proves a write that errored was seen, not just the marginals | An error path reachable only on writes, never driven |
| region × resp | err at each region | Proves an error at every region was exercised | A region whose error response is broken but never tested |
| dir × wait | held-bus read AND write | Proves both directions were stretched, not just one | A wait-state bug that only manifests on the un-stretched direction |
The throughline: bins must be fine enough to distinguish the scenarios that fail differently, and the crosses must cover the combinations that hide in the marginals. Over-binning is real cost too — a coverpoint with thousands of bins you never close becomes noise that masks the holes that matter, so cross only the axes whose combination is a distinct DUT path, and use ignore_bins for combinations that are legal-but-uninteresting and illegal_bins for ones that must never occur. A good model is small, every bin is justified, and every empty bin is a question someone must answer before sign-off.
6. Common RTL mistakes
7. Debugging scenario
The signature coverage failure is a false closure: a regression reports "coverage closed, all green," yet a bug ships down a path the coverage summary implied was tested but a too-coarse coverpoint never actually exercised.
- Observed symptom: the wait-state handling block reports a field-return defect — a peripheral occasionally commits stale data when the bus is held — yet the sign-off record shows the APB agent's functional coverage at 98% and the regression fully green. The summary number said the wait paths were covered.
- Waveform clue: pulling the regression traces, every observed transaction completes in a single ACCESS cycle —
PREADYis high on the first sample, every time. No transaction in the entire regression was ever held. The held-bus path the field bug lives on was never on any waveform, despite "98% coverage." - Root cause: the
cp_waitcoverpoint defined only a singlebins zero = {0};. Because the responder model never injected wait states, that one bin hit 100%, and a coverpoint that is 100% of one bin contributes a perfect score to the summary. The 1-wait and N-wait scenarios had no bin to report them as missing, so the hole was invisible — the report read 98% while the entire wait-state path read 0% exercise. The coverage model was complete relative to its bins, and the bins had a hole. - Correct RTL: the fix is in the coverage model and the stimulus together. Split the coverpoint into meaningful bins —
bins zero = {0}; bins one = {1}; bins many = {[2:$]};— so the un-driven tail now shows as 0%, and configure the responder to inject 0, 1, and N waits across the regression so closure forces the held-bus scenarios to run. Add thex_dir_waitcross so both directions get stretched. The DUT bug is then exercised by construction, and the assertions/scoreboard catch the stale-data commit. - Verification assertion: beyond the wait coverpoint, add a sign-off gate that no meaningful bin or cross-cell is empty without an explicit
ignore/illegaljustification — a coverage review, not a runtime SVA: everycp_waitbin and everyx_region_respcell must be hit or formally excluded before "closed" can be declared. Pair it with a wait-injection assertion in the responder (assert (waits_injected_this_regression > 0)) so a responder that silently stops stretching the bus fails loudly. - Debug habit: when a "coverage-closed" design ships a bug, never trust the summary percentage — open the bin-level report for the coverpoint on the failing path. A high overall number almost always hides a coverpoint with too few bins, where one trivially-hit bin masks an un-exercised tail. The question is not "what's the coverage number?" but "which bins are empty, and is the empty one explained?" Functional-coverage completeness is a bin property, not a percentage property.
8. Verification perspective
Functional coverage is the verification deliverable that turns "we ran tests" into a sign-off claim — and using it correctly means keeping it sharply distinct from the correctness checks.
- Coverage is a sign-off gate, but it gates exercise, not pass/fail. Closure — every meaningful bin and cross-cell hit or formally excluded — is a required gate before tape-out, alongside (never instead of) all-assertions-passing and a clean scoreboard. The two gates answer different questions: coverage says "we drove every interesting scenario," the checkers say "every scenario we drove behaved correctly." A design ships only when both are satisfied; either alone is a false sign-off. Reviewing coverage means reading the bin-level report, not the headline percentage, because the percentage hides exactly the holes that ship bugs.
- Distinguish exercised from passing, deliberately, in the model's structure. Keep coverpoints describing what happened on the bus (direction, region, response, wait, strobe) and let the scoreboard and assertions decide whether it was right. Crucially, the
errbin filling means "an error response was observed" — not "the error was handled correctly"; that the error was handled correctly is the scoreboard's job. Conflating the two (e.g. only sampling coverage when the scoreboard passes) corrupts the exercise ledger and hides un-tested paths. - Close the known holes explicitly — wait-states and error-per-region. These two are the APB coverage holes that ship bugs. The wait-count coverpoint needs
0/1/[2:$]bins backed by a wait-injecting responder; theregion × responsecross needs every region's error cell driven, which means stimulus that targets errors at each mapped block, not just one. Treat both as named closure items reviewed individually, because both are the kind of path random stimulus reaches last, if ever. - Use illegal and ignore bins to make exclusions honest. A scenario that must never occur (e.g. a response combination the protocol forbids) goes in
illegal_binsso that if it ever is observed, the simulation flags it — turning coverage into a lightweight checker for "this should be impossible." A scenario that legally cannot occur in this DUT (an unimplemented feature) goes inignore_binsso it doesn't sit as a permanent un-closeable hole. The rule: every empty meaningful bin is either driven to closure or formally excluded with a reason — there are no silently-ignored holes.
The point: coverage is the gate that proves thoroughness of exercise, it is structurally separate from the correctness gate, and its value at sign-off is in the bin-level review that surfaces the wait-state and error-per-region holes before they reach silicon.
9. Interview discussion
"How do you know your APB verification actually tested everything that matters?" is a senior-DV question, and the answer that signals real experience is "functional coverage — and I keep it strictly separate from the checkers."
Lead with the distinction: coverage measures whether a scenario was exercised; assertions and the scoreboard measure whether it was correct — a green, coverage-closed regression needs both, because each answers a different question, and a passing assertion on an un-driven path proves nothing. Then describe the model concretely: one covergroup, sampled once per transaction from the monitor's analysis port, with coverpoints for direction, address region (per mapped register), response, wait-count, and PSTRB pattern — sampled from the observed transaction, not the stimulus, so it reflects the bus and not the generator's intent. The depth that distinguishes a strong candidate is the crosses: region × response and direction × wait are where the real scenarios live — marginal closure on each coverpoint does not prove you ever saw a write that errored or an error at the reserved region, and the cross is what forces those combinations. Land the closing on the known holes: wait-count is the classic APB coverage hole — responders default to zero waits, so a single {0} bin reads 100% while every held-bus path is un-tested; you fix it with 0/1/[2:$] bins and a wait-injecting responder. Finishing with "I never trust the coverage percentage — I read the bin-level report, because a high number almost always hides a too-coarse coverpoint" shows you understand that coverage completeness is a bin property, not a percentage.
10. Practice
- Build the covergroup. Write an APB covergroup with coverpoints for direction, region (four mapped blocks), response, and wait-count (
0/1/[2:$]), sampled from a transaction item. State which port it subscribes to and why. - Add the crosses that matter. Add
direction × response,region × response, anddirection × wait. For each, name one specific scenario the cross forces that the marginals would miss. - Diagnose a false closure. Given a report showing 97% overall coverage but a field bug on a held bus, describe what you'd look at first and the most likely bin-level cause.
- Exercised vs correct. Explain what it means for the
errbin to be hit, and what it does not mean. Which component decides whether the error was handled correctly? - Illegal vs ignore. Give one APB scenario you'd put in
illegal_binsand one you'd put inignore_bins, and explain the difference in intent.
11. Q&A
12. Key takeaways
- Coverage measures exercised; assertions and the scoreboard measure correct. They answer different questions, a green regression needs both, and a passing check over an un-driven path proves nothing — exercised ≠ correct.
- The model is one covergroup, sampled once per transaction from the monitor's analysis port, with coverpoints for direction, address region (per mapped register), response, wait-count, and PSTRB pattern — sampled from the observed transaction, not the stimulus.
- Crosses are where the real scenarios live.
region × responseforces an error at each region;direction × waitforces both directions to be held — combinations the marginals report as "covered" while the cells stay empty. - Wait-count is the classic APB coverage hole. A single
{0}bin reads 100% while every held-bus path is un-tested; close it with0/1/[2:$]bins and a wait-injecting responder. - A hole is an un-exercised path, not a bug. Every meaningful empty bin must be driven to closure or formally excluded with
illegal_bins/ignore_binsand a reason — there are no silently-ignored holes. - Trust bins, not percentages. Coverage completeness is a bin property; a high summary number almost always hides a too-coarse coverpoint, so sign-off reviews the bin-level report, not the headline figure.