I²C · Module 21
Negative and Error Sequences
The line between a violation a controller can produce and one that is another device misbehaving, why configuration is not injection, and why every error sequence must hand the bus back — one held line turns a deliberate fault into a run of failures whose head is innocent.
Negative testing has a reputation for being the easy part — drive something illegal and check that nothing explodes. It is where two mistakes cluster instead: putting the wrong violations in sequences, and accepting a request as evidence that a fault happened.
1. The Dividing Line
A violation a real controller can produce is stimulus. A violation it cannot produce is another device misbehaving.
| violation | who can produce it | where it belongs |
|---|---|---|
| an address in a reserved range | a controller, trivially | a sequence |
| a transfer abandoned mid-byte | a controller whose software timed out | a sequence |
| a bus held past the end of a transfer | a controller keeping it for a repeated START | a sequence |
| back-to-back repeated STARTs | a controller, legally | a sequence |
| SDA moving while SCL is high, mid-byte | no controller | the error injector |
| a line held low indefinitely | no controller | the error injector |
| a narrow disturbance on SDA | no device at all | the error injector |
Blurring that line produces a driver with an illegal mode — and then every transfer in the environment is conditional on a flag that is usually off and occasionally is not. A driver is hard enough to get right without a second personality.
The abort_after_bits field on the ordinary sequence item is the interesting case. It looks like a violation and it is legal controller behaviour: a master whose software timed out, or one that lost arbitration, stops mid-byte. So it lives on the ordinary item, and Chapter 21.3 §7 records that the driver still frames a STOP afterwards — a controller that simply stopped clocking would leave the bus held, and every later test would inherit it.
2. Configuration Is Not Injection
The antipattern, and it is almost universal in a first error-injection suite:
// "test that the target handles a framing error"
dut.force_framing_error = 1'b1;
@(posedge clk);
if (!dut.err_framing) `uvm_error("INJ", "the target did not flag the error")That passes, and it establishes one fact: the target reacts to its own error input.
It proves nothing about whether the target would notice the corresponding real condition, because the real condition never occurred. No illegal edge appeared on the bus, and the entire path between "a bad thing happened on the wire" and "the design noticed" was bypassed. That path is the part most likely to be wrong.
So the injector is a third participant on the bus with its own drive index, and it only ever pulls low or releases. An injector that drove a line high would be modelling a fault open-drain hardware cannot produce.
// -----------------------------------------------------------------------------
// i2c_error_injector.sv
// Faults as bus events. A third participant, not a hook inside the design.
//
// NOT EXECUTED -- see i2c_if.sv. Transcribed from Chapter 20.9's `i2c_fault_inj`, which
// was simulated against Module 18's real target and survived fourteen mutations.
//
// THE ANTIPATTERN THIS EXISTS TO REPLACE:
//
// dut.force_framing_error = 1'b1;
// ck(dut.err_framing == 1); // passes
//
// That establishes that the target reacts to its own error input. It proves nothing
// about whether the target would notice the corresponding REAL condition, because the
// real condition never occurred -- no illegal edge appeared on the bus and the entire
// detection path was bypassed. That path is the part most likely to be wrong.
//
// So this component pulls lines low exactly as every other device does, and the design
// cannot tell it apart from a misbehaving neighbour. It only ever pulls LOW or releases:
// an injector that drove a line high would be modelling a fault open-drain hardware
// cannot produce.
//
// CONFIGURATION IS NOT INJECTION. `arm` being set is a request. The evidence that a
// fault happened is a CONSEQUENCE -- `n_injected` rising, the resolved line actually
// going low, and best of all an independent monitor reporting the framing event. The
// counters below exist so a test can check the consequence rather than its own intent.
// -----------------------------------------------------------------------------
typedef enum {
I2C_FAULT_NONE,
I2C_FAULT_SDA_STUCK, // a device that crashed holding SDA: the bus cannot be framed
I2C_FAULT_SCL_STUCK, // a device that crashed holding SCL: tests the driver's timeout
I2C_FAULT_SDA_GLITCH, // a narrow disturbance: the sampling-window question
I2C_FAULT_EXTRA_START // framing where none belongs, and it IS a START
} i2c_fault_e;
class i2c_error_injector extends uvm_component;
`uvm_component_utils(i2c_error_injector)
virtual i2c_if vif;
i2c_agent_config cfg;
int unsigned drv_idx = 2; // its OWN pull index
// ---- what to inject, and whether to ------------------------------------
i2c_fault_e fault = I2C_FAULT_NONE;
bit arm = 1'b0;
int unsigned glitch_clks = 2;
// ---- evidence the fault reached the wire -------------------------------
int unsigned n_injected;
bit injecting;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db #(i2c_agent_config)::get(this, "", "cfg", cfg))
`uvm_fatal("NOCFG", "no i2c_agent_config for the error injector")
vif = cfg.vif;
if (vif == null)
`uvm_fatal("NOVIF", "i2c_agent_config.vif is null in the error injector")
endfunction
task run_phase(uvm_phase phase);
super.run_phase(phase);
forever begin
@(vif.drv_cb);
if (!arm) begin
// Disarmed: release both lines and forget everything. An injector that kept
// holding a line after being disarmed would corrupt every later test in the
// run, and the failure would be attributed to whatever ran next.
vif.drv_cb.scl_drive_low[drv_idx] <= 1'b0;
vif.drv_cb.sda_drive_low[drv_idx] <= 1'b0;
injecting = 1'b0;
fired = 1'b0;
cnt = 0;
end else begin
inject_step();
end
end
endtask
bit fired;
int unsigned cnt;
task inject_step();
case (fault)
I2C_FAULT_SDA_STUCK: begin
if (!injecting) n_injected++;
vif.drv_cb.sda_drive_low[drv_idx] <= 1'b1;
injecting = 1'b1;
end
I2C_FAULT_SCL_STUCK: begin
if (!injecting) n_injected++;
vif.drv_cb.scl_drive_low[drv_idx] <= 1'b1;
injecting = 1'b1;
end
I2C_FAULT_SDA_GLITCH: begin
// One shot, `glitch_clks` wide. Whether it matters depends entirely on
// whether it spans a sampling edge -- Chapter 20.9 measures both answers at
// the same position, and a 2-clock and a 40-clock glitch give opposite
// results. WIDTH is a parameter and POSITION is the test's business: a fault
// whose position is uncontrolled is not a controlled experiment.
if (!fired) begin
if (cnt == 0) begin
n_injected++;
vif.drv_cb.sda_drive_low[drv_idx] <= 1'b1;
injecting = 1'b1;
cnt = 1;
end else if (cnt < glitch_clks) begin
cnt++;
end else begin
vif.drv_cb.sda_drive_low[drv_idx] <= 1'b0;
injecting = 1'b0;
fired = 1'b1;
end
end
end
I2C_FAULT_EXTRA_START: begin
// Timed against the RESOLVED bus rather than a clock count: pulling SDA low
// is only a START if SCL is HIGH at that moment. Waiting for the real
// condition is what makes this indistinguishable from a genuine START --
// because it IS one, and a conforming target reframing is correct behaviour.
if (!fired && vif.drv_cb.scl === 1'b1 && vif.drv_cb.sda === 1'b1) begin
n_injected++;
vif.drv_cb.sda_drive_low[drv_idx] <= 1'b1;
injecting = 1'b1;
cnt = 1;
end else if (injecting) begin
if (cnt < glitch_clks) cnt++;
else begin
vif.drv_cb.sda_drive_low[drv_idx] <= 1'b0;
injecting = 1'b0;
fired = 1'b1;
end
end
end
default: begin
vif.drv_cb.scl_drive_low[drv_idx] <= 1'b0;
vif.drv_cb.sda_drive_low[drv_idx] <= 1'b0;
injecting = 1'b0;
end
endcase
endtask
endclass3. Position Is Part of the Stimulus
The most instructive failure in Chapter 20.9 was a glitch test that passed for a reason unrelated to its conclusion.
The injector was armed at the first observed byte, which put the disturbance in the acknowledge slot — where the target is pulling SDA low itself and is not sampling. Pulling an already-low line changes nothing, so the glitch was harmless for a reason that has nothing to do with its width.
4. Leaving the Bus Usable Is the Hard Part
Every sequence in this file must leave the bus idle and framed, because the next test inherits whatever it leaves behind — and a held line produces a cascade of failures none of which is the real one.
Three specific obligations:
A truncated transfer still frames a STOP. Otherwise the bus stays held.
The injector releases on disarm. Its run_phase clears both lines and resets its one-shot state whenever arm is low, so a fault cannot outlive the test that requested it.
A hold_bus sequence must be followed by something that releases. i2c_restart_storm_seq sets hold_bus on every transfer except the last, which is the only correct shape: the storm keeps the bus and the final transfer gives it back.
5. The Error Sequences
// -----------------------------------------------------------------------------
// i2c_error_sequences.sv
// Stimulus that deliberately breaks the rules, and an environment that survives it.
//
// NOT EXECUTED -- see i2c_if.sv.
//
// THE DIVIDING LINE, and it is the reason this file is short. A violation a real
// CONTROLLER can produce is a sequence: an address in a reserved range, a transfer
// abandoned because software timed out, a bus held past the end of a transfer. A
// violation a controller CANNOT produce -- SDA moving while SCL is high mid-byte, a line
// held down forever -- is not stimulus at all. It is another device misbehaving, and it
// belongs to `i2c_error_injector`.
//
// Blurring that line produces a driver with an `illegal` mode, and then every transfer in
// the environment is conditional on a flag that is usually off and occasionally is not.
//
// KEEPING THE ENVIRONMENT STABLE IS THE HARD PART. Each sequence below must leave the
// bus idle and framed, because the next test inherits whatever it leaves behind, and a
// held line produces a cascade of failures none of which is the real one.
// -----------------------------------------------------------------------------
// ---- an address nobody is allowed to use ------------------------------------
class i2c_reserved_addr_seq extends i2c_base_seq;
`uvm_object_utils(i2c_reserved_addr_seq)
function new(string name = "i2c_reserved_addr_seq");
super.new(name);
endfunction
task body();
i2c_seq_item it;
bit [6:0] reserved [] = '{7'h00, 7'h01, 7'h04, 7'h78, 7'h7F};
foreach (reserved[i]) begin
it = i2c_seq_item::type_id::create("it");
start_item(it);
// The item's own constraint excludes these, so it has to be DISABLED by name.
// That is the point of writing the legal range as a named constraint rather
// than hard-coding it: an error sequence turns it off deliberately and
// visibly, and nothing else in the library can turn it off by accident.
it.c_addr_legal.constraint_mode(0);
if (!it.randomize() with {
addr == reserved[i];
read == 1'b0;
n_bytes == 1;
restart_first == 1'b0;
hold_bus == 1'b0;
abort_after_bits == 0;
})
`uvm_fatal("RAND", "i2c_reserved_addr_seq could not randomize")
finish_item(it);
end
endtask
endclass
// ---- a transfer abandoned part way through a byte ---------------------------
class i2c_truncated_seq extends i2c_base_seq;
`uvm_object_utils(i2c_truncated_seq)
rand bit [6:0] addr = 7'h50;
rand int unsigned nbits = 5;
// Strictly inside a byte. Zero would not be a truncation and eight would be a
// complete byte followed by a missing acknowledge slot, which is a different fault
// and deserves its own test rather than being conflated with this one.
constraint c_bits { nbits inside {[1:7]}; }
function new(string name = "i2c_truncated_seq");
super.new(name);
endfunction
task body();
i2c_seq_item it;
it = i2c_seq_item::type_id::create("it");
start_item(it);
if (!it.randomize() with {
addr == local::addr;
read == 1'b0;
n_bytes == 1;
restart_first == 1'b0;
hold_bus == 1'b0;
abort_after_bits == local::nbits;
})
`uvm_fatal("RAND", "i2c_truncated_seq could not randomize")
finish_item(it);
endtask
endclass
// ---- back-to-back repeated STARTs with no payload ---------------------------
// Legal, and a shape almost nothing is tested against: the target must reframe each
// time and must not accumulate state across them.
class i2c_restart_storm_seq extends i2c_base_seq;
`uvm_object_utils(i2c_restart_storm_seq)
rand bit [6:0] addr = 7'h50;
rand int unsigned n = 4;
constraint c_n { n inside {[2:8]}; }
function new(string name = "i2c_restart_storm_seq");
super.new(name);
endfunction
task body();
i2c_seq_item it;
for (int i = 0; i < n; i++) begin
it = i2c_seq_item::type_id::create("it");
start_item(it);
if (!it.randomize() with {
addr == local::addr;
read == 1'b0;
n_bytes == 1;
restart_first == (i != 0); // the first opens with a START
hold_bus == (i != local::n - 1); // the last releases the bus
abort_after_bits == 0;
})
`uvm_fatal("RAND", "i2c_restart_storm_seq could not randomize")
finish_item(it);
end
endtask
endclass
// ---- a write to a register the datasheet says is read-only ------------------
// Entirely legal traffic whose correct outcome is a NACK. It is in this file because it
// is where people look for it, and it is worth being explicit that it is NOT an error
// sequence: nothing about it violates the protocol, and a scoreboard that reported a
// failure here would be wrong. It is the case that distinguishes "the bus carried what
// I asked" from "the target accepted it".
class i2c_write_readonly_seq extends i2c_base_seq;
`uvm_object_utils(i2c_write_readonly_seq)
rand bit [6:0] addr = 7'h50;
rand bit [7:0] ro_reg = 8'h02;
function new(string name = "i2c_write_readonly_seq");
super.new(name);
endfunction
task body();
i2c_seq_item it;
it = i2c_seq_item::type_id::create("it");
start_item(it);
if (!it.randomize() with {
addr == local::addr;
read == 1'b0;
n_bytes == 2;
restart_first == 1'b0;
hold_bus == 1'b0;
abort_after_bits == 0;
})
`uvm_fatal("RAND", "i2c_write_readonly_seq could not randomize")
it.data[0] = ro_reg; // the pointer
it.data[1] = 8'h99; // the byte that must be refused
finish_item(it);
endtask
endclassOne sequence in that file is deliberately not an error sequence, and it is in the file because that is where people look for it.
i2c_write_readonly_seq writes to a register the datasheet declares read-only. Nothing about it violates the protocol; the traffic is entirely legal and its correct outcome is a NACK on the data byte. A scoreboard that reported a failure here would be wrong.
It is the case that distinguishes "the bus carried what I asked" from "the target accepted it" — and Chapter 20.3's T1b shows that an intent-to-observation comparison reports perfect agreement on it, because every byte asked for really was framed and really was on the wire. Refusal is not a fact about the bus.
Twelve green error-injection tests that never injected anything
Pitfall — forcing an internal flag and calling it injection
// An error-injection suite. Twelve tests, all passing.
//
// task test_framing_error();
// dut.inject_framing_err = 1'b1;
// @(posedge clk);
// if (!dut.err_framing) uvm_error("INJ", "no error flagged");
// if (dut.state != S_IDLE) uvm_error("INJ", "did not abort");
// endtask
//
// Every test has this shape. What they establish: the target reacts to its own
// error inputs, and its state machine responds to its own error flags.
//
// What they do NOT establish: whether an illegal edge on SDA would ever SET that
// flag. The detection logic -- the part between "something bad happened on the
// wire" and "the design noticed" -- is never executed.
//
// The part ships. In the field a noisy bus produces framing violations the target
// does not notice, because its detector requires SCL to be high AND low in the
// same cycle. That code was never reachable through the force path.Pitfall — an error sequence that left the bus wedged
// A truncation sequence that stops driving part way through a byte:
//
// class i2c_truncated_seq extends i2c_base_seq;
// task body();
// i2c_seq_item it;
// it = i2c_seq_item::type_id::create("it");
// start_item(it);
// void'(it.randomize() with { abort_after_bits == 5; hold_bus == 1'b1; });
// finish_item(it);
// endtask
// endclass
//
// hold_bus is 1, so the driver sends no STOP -- and abort_after_bits means it
// stopped mid-byte. The bus is left with SCL pulled low by the driver.
//
// The truncation test itself PASSES: the monitor reports a truncated transfer and
// nothing was written.
//
// Then the next four tests in the regression fail. Every one reports that the
// target never acknowledges, because SCL never rises, because a test that finished
// twenty minutes ago is still holding it. The failures name the wrong tests, and
// the first one that gets debugged is the one after the real culprit.6. What 21.10 Settled
The dividing line is who can produce the violation. A controller's own misbehaviour is stimulus; another device's is an injector. Merging them gives the driver a second personality that every transfer then depends on.
Configuration is not injection. A forced internal flag exercises the response and skips the detection, and the detection is what fails in the field. Evidence is a consequence, and the best consequence is one reported by a witness with no connection to the injector.
Position is part of the stimulus. The same fault in an acknowledge slot and in a data bit are different experiments, and a stimulus parameter that can be changed thirty-fold without moving a result is not reaching the mechanism.
Every error sequence must hand the bus back. A truncated transfer still frames a STOP, the injector releases on disarm, and a held-bus sequence ends with one that releases — otherwise one deliberate fault becomes a run of failures whose head is innocent.
A one-shot fault must be proven one-shot while still armed, because disarming hides the failure.
Not everything in the error file is an error. A write to a read-only register is legal traffic whose correct outcome is a refusal, and it is the case that separates "the bus carried it" from "the target accepted it".
One chapter remains, and it is the only real test of everything above: can another project use this without editing it. Chapter 21.11 — Building a Reusable I²C VIP.
Continue learning
Related tutorials
- Related topic
Protocol Checking and Error-Injection Strategy
Where each protocol rule belongs, why forcing a flag inside the design proves nothing about detection, and why a fault's position is as much stimulus as its size. Ends with a simulation in which 0xFF arrives as 0x7F with every byte acknowledged — silent corruption under a completely clean protocol verdict.
- Related topic
Repeated START — Holding the Bus Between Phases
A repeated START is not a new waveform. It is the START edge again, and what makes it a different event is that the bus was already busy. That single fact is why a classifier needs state and why a monitor that joins late cannot classify what it sees.
- Related topic
START/STOP Timing and Malformed Framing
Three framing margins, each with two anchor events, all of them minimums: the hold after a START, the setup before a repeated START, and the setup before a STOP. Build a sequencer that generates all three and refuses an illegal configuration, then catalogue the malformed framing the margins exist to prevent.
- Related topic
The Address Byte — Seven Address Bits and the R/W Bit
The first byte after a START is not an address followed by a direction bit. It is one eight-bit field that the bus, the slave and the datasheet all treat as a unit — and treating it as two things is the single most common source of I²C address confusion.
