AMBA APB · Module 15
APB Negative Testing
Deliberately injecting protocol violations and illegal stimulus to prove an APB slave degrades gracefully — returns PSLVERR or stays consistent rather than hanging or corrupting state. Covers the injection taxonomy, the error-injection driver, and the central subtlety: scoping the protocol assertion on the injected signal while keeping the slave-response check active.
Positive testing proves the slave is correct under legal stimulus; negative testing proves it is robust under illegal stimulus — and its defining subtlety is that your own injected violation trips the protocol assertion, so you must scope which checks apply. The single idea to carry: negative testing deliberately breaks the rules catalogue on the bus, or sends a functionally-illegal-but-protocol-legal request, to confirm the slave fails safe — returns PSLVERR, stays consistent, never hangs or silently corrupts state — and because the protocol checker and the error injector are in direct tension on the violated signal, you must scope (disable or expect) the protocol assertion on the injected signal while keeping the slave-response check active. Positive and negative testing are complementary, not alternatives: one shows the design does the right thing when asked legally, the other shows it does no wrong thing when asked illegally. Get the scoping right and the negative test verifies the slave's robustness; get it wrong and either a false fail buries the result or a global waiver masks a real bug.
1. Problem statement
The problem is deliberately driving an APB slave with illegal or out-of-spec stimulus and verifying it degrades gracefully — responding with an error, staying internally consistent, and never hanging or corrupting state — rather than only ever exercising it with legal, well-formed transfers.
Directed and constrained-random positive testing answers "does the slave do the right thing when I ask correctly?" It is necessary but not sufficient. Silicon meets stimulus its designer never intended: a buggy master, a glitching fabric, a reset that lands mid-transfer, software writing a read-only register, an address decode that lets an unmapped access through. A slave that is correct under legal traffic can still hang the bus or silently corrupt a register under illegal traffic — and that failure mode never appears in a positive test plan, because the positive plan never sends the illegal stimulus. So a second, deliberately adversarial campaign is required, and it has hard requirements:
- The illegality must be targeted, not random. Negative testing is not "throw random noise at it." It is a catalogue of specific violations — each chosen to probe a specific failure mode — injected on purpose so you know exactly what was broken and exactly what graceful response to expect.
- The expected outcome is a safe failure, not acceptance. The slave must not silently swallow illegal input as if it were legal. It must reject gracefully: assert
PSLVERRfor an illegal access, hold or restore consistent state, and crucially never deadlock the bus. "Graceful" has a precise, checkable meaning per injection. - Injecting a bus-protocol violation collides with the protocol checker. The moment you force
PADDRto change mid-transfer, the protocol assertion that says "PADDRis stable across a transfer" fires — on your stimulus. So you must decide, per test, which assertions apply: scope off (or mark expected) the protocol assertion on the injected signal, while keeping the slave-response check live.
So the job is to build a negative-test plan that enumerates the illegal-stimulus catalogue, injects each violation deliberately, scopes the assertions correctly, and checks the slave's graceful response — turning "we never saw it break" into "we proved it fails safe."
2. Why previous knowledge is insufficient
By now you can prove an APB slave correct under legal stimulus, but that is exactly the gap: every check you have built assumes a legal bus, and negative testing makes the bus illegal on purpose.
- The protocol assertions check that the bus is LEGAL — negative testing makes it ILLEGAL. The SVA assertions in Module 15.2 encode each rule from the protocol-rules catalogue (15.1) and fail the instant a rule breaks. That is a positive contract: "the bus obeys the catalogue." Negative testing deliberately violates the catalogue, so those very assertions will fire on your injected illegality. This is the central tension of the chapter: the protocol checker and the error injector are pointed at the same signal with opposite intent, and the resolution is scoping — disabling or expecting the protocol assertion on the injected signal so the real check (the slave's response) is the one that decides pass or fail. You are not re-deriving the rules; you are turning each one into something to break on purpose.
- Illegal injection is not a legal-but-extreme corner case. Corner cases (15.8) are legal stimulus pushed to its limits — reset landing mid-transfer, back-to-back transfers with zero idle, maximum wait states. Those are sequences the protocol permits but rarely exercises; the protocol assertions should stay green throughout. Negative testing is the opposite: the stimulus is illegal, the protocol assertion on the violated signal is expected to fire, and the question is whether the slave survives. Confusing the two leads to scoping a corner-case test (where you should not) or leaving an assertion global in a negative test (where you must scope it).
- The graceful-response contract builds on error behaviour you already know. What "graceful" means for an illegal address or a read-only write — assert
PSLVERR, do not commit the write, stay consistent — is thePSLVERRand illegal-address behaviour already established for APB. Negative testing does not re-derive that contract; it provokes it deliberately and checks the slave honours it, then confirms the slave does not hang and does not corrupt neighbouring state.
So the model to add is the deliberate-violation discipline: an injection catalogue, an injection mechanism, the expected graceful-response check per injection, and — the load-bearing piece — per-test assertion scoping that resolves the checker-versus-injector tension.
3. Mental model
The model: a negative test is a controlled demolition with a blast radius you must fence off. The injector deliberately breaks one rule — it forces PADDR to change mid-transfer, drives an unmapped address, or pushes an X onto a line. The protocol assertion on that exact signal is a tripwire that fires on any break, including yours. And the slave-response check is the thing you actually care about: did the slave fail safe? The demolition only tells you something if you fence off the tripwire on the signal you are deliberately breaking (so it does not raise a false alarm or get waived globally) while leaving the slave-response check fully armed to record the real result.
Three refinements make it precise and interview-ready:
- There are three injection classes, and they differ in where the illegality lives. A protocol-rule violation is illegal on the bus itself —
PENABLEhigh withoutPSEL,PADDR/PWDATAchanging mid-ACCESS,PENABLEdeasserting beforePREADY, a non-one-hotPSEL. A functionally-illegal stimulus is protocol-legal — the handshake is textbook — but the request is out of spec: a write to a read-only register, an access to an unmapped or out-of-range or unaligned address. An X/unknown injection drivesX/Zonto a control or data line to confirm no unknown propagates into state,PRDATA, orPSLVERR. The first class trips a protocol assertion (so it needs scoping); the second is bus-legal (so the protocol assertions stay green and only the slave-response check matters). - "Graceful" is a checkable, per-injection contract — not a vibe. For a functionally-illegal access the contract is
PSLVERRasserted at completion and the write not committed. For a malformed transfer the contract is no hang — completion within a bounded number of cycles — and no corruption of unrelated registers. For anXinjection it is no X leak into observable state. Each injection names its own expected outcome, and the three failure modes you are hunting are precisely hang, silent corruption, and wrong/absent error response. - Scoping is the resolution of the checker-versus-injector tension, and it must be narrow and temporary. When you inject a protocol violation, scope only the assertion on that signal, only for the duration of that injection —
$assertoffthe specific property, inject, then$assertonit again — never a global waiver that silences the checker everywhere. The expected assertion firing during a negative test is the pass; a global waiver to quieten it is how a real violation in another test gets masked.
4. Real SoC implementation
In a real environment the injection lives in an error-injection driver (or a knob on the normal driver) that deliberately violates a chosen catalogue rule via force/release on the interface, paired with a slave-response check and — the load-bearing piece — per-test assertion scoping so the protocol assertion on the injected signal does not turn your own stimulus into a false fail. A representative negative-test sequence injecting a stability violation (PADDR changing mid-ACCESS) and an unmapped-address access:
// apb_negative_seq.sv — deliberate illegal stimulus + graceful-response check.
// Two injections: (1) a PROTOCOL violation (PADDR drift mid-ACCESS) that trips
// the stability assertion, so we SCOPE that one property; (2) a FUNCTIONALLY-
// illegal unmapped access that is protocol-legal, so NO scoping is needed.
class apb_negative_seq extends uvm_sequence #(apb_txn);
`uvm_object_utils(apb_negative_seq)
// hierarchical handle to the bound protocol checker's stability property
// (the bind instance from Module 15.2). Used to scope just this one rule.
// e.g. tb.dut.u_apb_chk.a_request_stable
function new(string name = "apb_negative_seq");
super.new(name);
endfunction
task body();
apb_txn t;
// ============================================================
// INJECTION 1 — PROTOCOL-RULE VIOLATION: PADDR drifts mid-ACCESS.
// This BREAKS catalogue rule STAB-1, so the protocol assertion
// a_request_stable WILL fire on our own stimulus. Scope it off for
// exactly this injection, then re-arm it immediately afterwards.
// ============================================================
// SCOPE: disable ONLY the stability property, ONLY for this injection.
// Narrow + temporary — never a global waiver (that would mask real bugs).
`uvm_info("NEG", "scoping off a_request_stable for PADDR-drift injection", UVM_LOW)
$assertoff(0, tb.dut.u_apb_chk.a_request_stable);
// drive a legal SETUP/ACCESS, then FORCE PADDR to change during ACCESS
// while the slave has not yet asserted PREADY (a held transfer).
t = apb_txn::type_id::create("t");
start_item(t);
assert (t.randomize() with { write == 1; addr == 32'h0000_0010; });
finish_item(t);
// mid-ACCESS interface violation via force (the deliberate illegality)
force tb.apb_if.paddr = 32'hDEAD_BEEF; // illegal: PADDR must stay stable
repeat (2) @(posedge tb.apb_if.pclk); // hold the illegal value a beat
release tb.apb_if.paddr;
// SLAVE-RESPONSE CHECK stays ACTIVE — this is the real pass/fail:
// the slave must NOT hang. Bound liveness: PREADY within N cycles.
fork begin : watchdog
int unsigned n = 0;
while (!tb.apb_if.pready) begin
@(posedge tb.apb_if.pclk);
if (++n > 16)
`uvm_error("NEG_HANG",
"slave hung after PADDR-drift injection — no PREADY in 16 cycles")
end
end join
// RE-ARM the protocol assertion the instant the injection is over,
// so STAB-1 keeps guarding every other beat and every other test.
$asserton(0, tb.dut.u_apb_chk.a_request_stable);
`uvm_info("NEG", "re-armed a_request_stable", UVM_LOW)
// ============================================================
// INJECTION 2 — FUNCTIONALLY-ILLEGAL but PROTOCOL-LEGAL: access an
// UNMAPPED address. The handshake is textbook, so NO protocol assertion
// fires and NO scoping is needed. The graceful contract is: PSLVERR=1.
// ============================================================
t = apb_txn::type_id::create("t_bad");
start_item(t);
assert (t.randomize() with { write == 0; addr == 32'hFFFF_FF00; }); // unmapped
finish_item(t);
// SLAVE-RESPONSE CHECK: an unmapped access MUST return PSLVERR, and the
// read data must not be treated as valid. (See the scoreboard's
// check_error_only path in Module 15.4 for the data-side equivalent.)
if (t.slverr !== 1'b1)
`uvm_error("NEG_NO_ERR",
$sformatf("unmapped read @0x%0h returned no PSLVERR — slave did not fail safe",
t.addr))
else
`uvm_info("NEG_OK",
$sformatf("unmapped read @0x%0h correctly returned PSLVERR", t.addr), UVM_LOW)
endtask
endclassTwo facts make this the right shape. First, the scoping is bracketed tightly around the protocol injection only — $assertoff the specific a_request_stable property immediately before the force, $asserton it the instant the injection ends — so the protocol checker is blind to your deliberate violation for exactly one beat and stays fully armed everywhere else. A global $assertoff (or a permanent waiver) would silence the stability rule for the whole test and mask any real drift elsewhere. Second, the slave-response check is the actual verdict — the watchdog that fails on a hang and the PSLVERR check on the unmapped access are what negative testing proves; the scoped protocol assertion is not the test goal, it is just noise to be fenced off. Injection 2 needs no scoping at all because it is protocol-legal, which is exactly why distinguishing the two classes matters.
5. Engineering tradeoffs
The injection taxonomy below is the heart of the negative-test plan: for each violation class it names what you inject, the graceful response you check, and which protocol assertion must be scoped.
| Violation class | What is injected | Expected slave response (the real check) | Protocol assertion to scope / disable |
|---|---|---|---|
| Protocol-rule violation (illegal on the bus) | PADDR/PWDATA change mid-ACCESS; PENABLE high without PSEL; PENABLE drops before PREADY; non-one-hot PSEL | Slave stays consistent and completes — no hang, no corruption of unrelated state; bus recovers on the next legal transfer | Yes — scope only the property on the injected signal (e.g. a_request_stable, a_enable_implies_sel) for the duration of the injection, then re-arm |
| Functionally-illegal (protocol-legal request) | Write to a read-only register; access to an unmapped / out-of-range / unaligned address; reserved-field write | PSLVERR asserted at completion; write not committed; read data not treated as valid; state unchanged | No — the bus is legal, so all protocol assertions stay green; only the slave-response (PSLVERR) check applies |
| X / unknown injection | X/Z driven onto PADDR, PWDATA, PSEL, PWRITE, or PENABLE | No X leak: no unknown propagates into registers, PRDATA, or PSLVERR; slave makes a defined (often error) response | Partly — scope the protocol property on the X-driven signal if it trips it; keep an $isunknown check on outputs active |
The throughline: the violation class dictates both the graceful contract you check and whether you scope. A protocol-rule violation is illegal on the bus, so it trips a protocol assertion and you must scope that one property, narrowly and temporarily, while checking the slave does not hang or corrupt. A functionally-illegal request is bus-legal, so the protocol assertions stay green and the only obligation is PSLVERR with no commit. An X injection is hybrid: scope the input-side property if it fires, but keep the output-side never-X check active, because the whole point is that no X escapes. Pick the scoping from the class, not from habit — and never reach for a global waiver to quieten the noise.
6. Common RTL mistakes
7. Debugging scenario
The signature negative-testing failure is the assertion-scoping bug: an injected protocol violation trips the global protocol assertion, the team reads it as a fail (or waives it globally to quieten the noise), and the real slave-response check never runs.
- Observed symptom: a brand-new negative test — "inject
PADDRdrift mid-ACCESS, confirm the slave does not hang" — reportsFAILon its very first run, citinga_request_stable(the STAB-1 stability assertion). The verification engineer, under schedule pressure, "fixes" it the fast way: adds a global waiver ofa_request_stableto the regression. The test now goes green, the negative-test box is ticked, and the campaign moves on. - Waveform clue: on the trace,
PADDRchanges during ACCESS exactly as injected (theforceis doing its job), anda_request_stablefires at that edge. But look at what else is now silent: across the entire regression — every positive test, every other negative test —a_request_stableis disabled by the global waiver. And the negative test's own watchdog (theno-hangcheck) shows it never actually evaluated: the test ended at the assertion fail before the slave-response check ran. - Root cause: two coupled mistakes. First, the protocol assertion on the injected signal was left global during the injection, so it fired on the tester's own deliberate illegality — a false fail, not a DUT bug. Second, the "fix" was a global waiver instead of a narrow, temporary scope: switching
a_request_stableoff everywhere means a realPADDR-drift bug in any other test (or a later regression) now sails through silently. The negative test never proved the slave does not hang, and the protocol checker was blinded across the whole suite. The checker and the injector were in direct tension onPADDR, and the global waiver resolved it by disarming the checker entirely. - Correct RTL (corrected scoping): there is no DUT bug here — the fix is in the test. Scope only the stability property, only around the injection, and keep the slave-response check as the verdict:
// FIX: scope NARROWLY and TEMPORARILY — only this property, only this injection.
$assertoff(0, tb.dut.u_apb_chk.a_request_stable); // not a global waiver
force tb.apb_if.paddr = 32'hDEAD_BEEF; // the deliberate violation
repeat (2) @(posedge tb.apb_if.pclk);
release tb.apb_if.paddr;
// THE REAL CHECK: slave must not hang — PREADY within a bounded window.
fork begin
int unsigned n = 0;
while (!tb.apb_if.pready) begin
@(posedge tb.apb_if.pclk);
if (++n > 16) `uvm_error("NEG_HANG", "slave hung after PADDR drift");
end
end join
$asserton(0, tb.dut.u_apb_chk.a_request_stable); // re-arm immediately — STAB-1
// guards every other beat/test- Verification assertion: make the scope auditable. Require every negative test to (a) name the exact property it scopes and the signal it injects on, (b) bracket the
$assertoff/$assertonaround the injection only, and (c) carry an active slave-response check (PSLVERR or no-hang) that produces the verdict. A sign-off gate flags any$assertoffthat is not re-armed in the same test, and any global assertion waiver as a regression smell — because a permanently-off protocol assertion is a hole, not a clean result. - Debug habit: when a negative test fails on the protocol assertion of the very signal you are injecting on, do not treat it as a DUT bug and do not reach for a global waiver. Ask first: is this the expected firing of the protocol check on my own injection? If so, scope that one property narrowly and temporarily, re-arm it after, and make sure the slave-response check is the thing deciding pass/fail. The expected assertion firing during negative testing is the pass; the only real fail is the slave hanging, corrupting state, or failing to return the error.
8. Verification perspective
A negative-test campaign is itself a verification deliverable, and its quality is measured not by "did anything fail" but by whether every injection class was hit, whether expected-fail was distinguished from real-fail, and whether the slave's response was actually checked.
- Negative-scenario coverage: every injection class must be hit. A negative-test plan is only complete when each class — protocol-rule violation, functionally-illegal request, and X-injection — is exercised, and within each, each specific violation (mid-ACCESS
PADDRdrift,PENABLE-without-PSEL, unmapped access, read-only write, unaligned address, X on each line) has a directed test that fired. Cover the injections the same way functional coverage (15.5) covers legal scenarios: acovergroupover "which illegality was injected" with a bin per violation, so a negative test that silently failed to inject (e.g. aforcethat never took) shows as an uncovered bin rather than a false green. - Distinguish expected-fail from real-fail — this is the discipline that defines negative testing. During a protocol-violation injection, the protocol assertion on the injected signal is expected to fire; that firing is the pass. The real fail is the slave hanging, corrupting state, or failing to return
PSLVERR. The campaign must encode this explicitly: scope the expected-firing assertion, and make the slave-response check the verdict. A test that treats the expected assertion firing as a fail will never go green; a test that waives it globally will go green for the wrong reason. Both are failures of the plan, not the DUT. - Scope assertions per-test, never globally. Scoping must be narrow (only the property on the injected signal) and temporary (only for the duration of the injection, re-armed immediately after). A global waiver to silence the noise switches the protocol checker off across the whole regression, masking real violations in every other test — the single most common way a negative-test campaign degrades the rest of the suite. Audit every
$assertofffor a matching$assertonin the same test, and treat any standing global waiver of a protocol property as a coverage hole. - Check the error response, not just the absence of a crash. "The simulation did not hang" is necessary but not sufficient. The slave-response check must positively confirm the defined graceful behaviour:
PSLVERRasserted at completion for an illegal access, the write not committed (confirm with a follow-up read that the register is unchanged — the scoreboard's job), bounded completion for a malformed transfer, and noXleaking into observable outputs. A negative test that only checks "no hang" passes a slave that silently corrupts state on illegal input.
The point: verify the campaign by negative-scenario coverage across every injection class, by rigorously separating the expected assertion firing (pass) from a real slave failure, by scoping per-test rather than globally, and by checking the positive graceful response — because a negative test that never injected, or that waived the checker to go green, proves nothing while reading as covered.
9. Interview discussion
"How would you negative-test an APB slave, and what is the hardest part?" separates engineers who have run a negative-test campaign from those who have only heard the phrase.
Lead with the philosophy: positive testing proves the slave is correct under legal stimulus; negative testing proves it is robust under illegal stimulus — they are complementary, and both are required. Then give the taxonomy: three injection classes — a protocol-rule violation that is illegal on the bus (PADDR drift mid-ACCESS, PENABLE without PSEL), a functionally-illegal but protocol-legal request (write to a read-only register, access to an unmapped address), and an X-injection — each with a defined graceful response: PSLVERR for an illegal access, no hang, no silent corruption, no X leak. Show you can name the injection mechanism: an error-injection driver or config knob using force/release on the interface to break a chosen catalogue rule. The depth flourish — and the answer the interviewer is listening for — is the assertion-scoping tension: when I inject a bus-protocol violation, the protocol assertion on that signal fires on my own stimulus, so I scope it — $assertoff that one property, narrowly and temporarily, around the injection, then $asserton it immediately — while keeping the slave-response check active as the verdict. Close with the discipline that proves seniority: "during negative testing the expected firing of the protocol assertion is the pass, not the fail; the only real fail is the slave hanging, corrupting state, or not returning PSLVERR — and I scope per-test, never with a global waiver, because a global waiver silences the checker across the whole regression and masks real bugs elsewhere." That distinction — expected-fail versus real-fail, and narrow scoping versus global waiver — is the mark of someone who has actually debugged a negative-test campaign.
10. Practice
- Classify the injections. For each, state the class (protocol-rule / functionally-illegal / X) and whether it needs assertion scoping: (a)
PADDRchanges during ACCESS; (b) a write to a read-onlySTATUSregister; (c)PWDATAdriven toX; (d) access to an unmapped address; (e)PENABLEasserted withPSELlow. - Write the graceful contract. For an access to an unmapped address and for a
PADDR-drift injection, write the exact slave-response check you would code (what signal, what value, within how many cycles), and say which is aPSLVERRcheck and which is a no-hang watchdog. - Scope it correctly. Write the
$assertoff/$assertonbracket for a mid-ACCESSPADDR-drift injection that scopes only the stability property and re-arms it, and explain in one sentence why a global waiver of that property is wrong. - Expected-fail vs real-fail. During a
PENABLE-without-PSELinjection, thea_enable_implies_selassertion fires. Is this a pass or a fail? State what the real verdict for this test should be and how you would check it. - Spot the masking bug. A teammate's negative test goes green, but you notice the regression has a standing global waiver of
a_request_stable. Explain what risk this creates across the rest of the suite and the one-line change that fixes it.
11. Q&A
12. Key takeaways
- Positive testing proves correctness under legal stimulus; negative testing proves robustness under illegal stimulus — they are complementary and both required, because a slave can be perfect on legal traffic and still hang or corrupt state on a malformed or out-of-spec transfer.
- Negative testing is targeted illegality, not random noise — a catalogue of specific injections across three classes: protocol-rule violations (illegal on the bus), functionally-illegal-but-protocol-legal requests, and X/unknown injection, each probing a defined failure mode.
- The expected graceful response is a checkable, per-injection contract —
PSLVERR(with the write not committed) for an illegal access, no hang (bounded completion) for a malformed transfer, and no silent corruption or X leak — versus the three failure modes of hang, corruption, and wrong/absent error response. - The central subtlety is assertion scoping — a bus-protocol violation trips the protocol assertion (15.2) on the injected signal, so you scope (
$assertoff/$asserton) that one property, narrowly and temporarily, while keeping the slave-response check active as the verdict; a functionally-illegal injection is bus-legal and needs no scoping. - During negative testing the expected assertion firing is the pass, not the fail — the only real fail is the slave hanging, corrupting state, or not returning the error; confusing the two yields a perpetual false fail, and a global waiver to silence the noise masks real bugs across the whole regression.
- Verify the campaign, not just the DUT — cover every injection class, distinguish expected-fail from real-fail, scope per-test rather than globally, and positively check the error response — and keep negative (illegal) injection distinct from the legal-but-extreme corner cases (15.8).