Skip to content
VLSI Mentor

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.

violationwho can produce itwhere it belongs
an address in a reserved rangea controller, triviallya sequence
a transfer abandoned mid-bytea controller whose software timed outa sequence
a bus held past the end of a transfera controller keeping it for a repeated STARTa sequence
back-to-back repeated STARTsa controller, legallya sequence
SDA moving while SCL is high, mid-byteno controllerthe error injector
a line held low indefinitelyno controllerthe error injector
a narrow disturbance on SDAno device at allthe 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:

Azvya Education Pvt. Ltd.VLSI Mentor
What this proves, and what it does not
   // "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.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_error_injector.sv — faults as bus events, not flags
   // -----------------------------------------------------------------------------
   // 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

   endclass

3. 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

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_error_sequences.sv — violations a controller can actually produce
   // -----------------------------------------------------------------------------
   // 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

   endclass

One 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
Buggy Code
// 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
Buggy Code
// 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