Skip to content
VLSI Mentor

I²C · Module 21

The Master Driver — Driving an Open-Drain Bus from a Testbench

The component that turns an intent into edges, why a release is a request rather than a result, and a published tool that compares the driver's algorithm against the version simulated in Module 20 — which found two real defects in the code this chapter publishes.

The driver is the component with a reason to cheat. It knows what it is trying to achieve, it controls the clock, and almost every shortcut it could take produces the intended transfer anyway — against most targets, most of the time.

It is also the component in this module where "NOT EXECUTED" costs the most. A structural linter can tell you a driver calls item_done; it cannot tell you the bit timing is right. So this chapter does something more than publish code.

1. The Claim This Chapter Is Entitled to Make

2. What It Owns, and What It Must Not

the driver ownsthe driver must not own
the SCL waveform: when every bit movesany expected value
turning an intent into framing and bytesany verdict
obeying every protocol rule itselfany knowledge of the target's register map
reporting what it observed from its own positionwhether that was correct

The last row is the one that decays first. A driver that reads back an acknowledge already knows something interesting, and if (!ack) uvm_error(...) is a two-line change that looks obviously right.

It is wrong, because the driver's position is one view among several and it lacks the information to judge. A missing acknowledge can mean a broken target or an address the target was never meant to answer — Chapter 20.3's T8 is a transfer whose correct outcome is a NACK on every single byte. So the driver records observations and says nothing about them.

3. Releasing Is a Request; the Line Level Is the Result

This is the most important loop in the file.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_master_driver.sv — the whole of clock-stretch support
      task scl_release_and_wait(ref i2c_seq_item rsp);
         int unsigned w = 0;
         vif.drv_cb.scl_drive_low[drv_idx] <= 1'b0;
         @(vif.drv_cb);
         while (vif.drv_cb.scl !== 1'b1 && w < cfg.stretch_timeout) begin
            @(vif.drv_cb);
            w++;
         end

On an open-drain bus the line rises when every participant has released it, and a target that is not ready holds SCL low precisely so that this does not happen. A driver that treats its own release as the line being high clocks the next bit into a device that never saw a rising edge, and the data is simply gone — no error anywhere, a byte that quietly differs from the one that was sent.

Three properties, each load-bearing:

It reads the resolved line, not its own drive intent. The whole point is that these differ.

It is bounded. A target that never releases SCL is a real fault, and an unbounded wait would hang the simulation on the environment's own stimulus rather than reporting anything.

The bound is a policy, not a protocol fact. A legal stretch and a stuck line are indistinguishable from the wire — in both cases SCL is low, released, and not rising. There is no condition to test, only a bound to choose, which is why it is configuration. Chapter 20.9 proves both halves in simulation: T5 holds SCL low forever and the timeout must fire, T5b has the real target stretch legitimately and the driver must wait it out. Either test alone establishes the opposite of what it appears to.

4. The Data-Valid Rule, Enforced Structurally

SDA may change only while SCL is low, except for framing. A driver that breaks it does not produce a failed check — it produces a different transfer, because an SDA change while SCL is high is a START or a STOP by definition.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_master_driver.sv — one task moves a data bit, and the order is the enforcement
      task put_bit(bit v, ref i2c_seq_item rsp);
         vif.drv_cb.scl_drive_low[drv_idx] <= 1'b1;
         half();
         vif.drv_cb.sda_drive_low[drv_idx] <= ~v;
         half();
         scl_release_and_wait(rsp);
         vif.drv_cb.scl_drive_low[drv_idx] <= 1'b1;
         half();
      endtask

SDA is assigned only after SCL has been pulled low, and there is exactly one task that moves a data bit — so the rule is obeyed by construction rather than by review. Framing has its own three tasks, which change SDA while SCL is high deliberately and are the only code that does.

5. The Driver, and the Tool That Checks It

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_master_driver.sv — an intent becomes edges
   // -----------------------------------------------------------------------------
   // i2c_master_driver.sv
   // An intent becomes edges. That is the whole job.
   //
   // NOT EXECUTED -- see i2c_if.sv. The ALGORITHM below is not new and is not unverified:
   // it is a transcription of Chapter 20.5's `i2c_ctrl_bfm`, which was simulated against a
   // real target in three HDLs and survived twenty mutations of its own. What Module 21
   // adds is the packaging -- phases, a sequencer handshake, configuration -- and the
   // packaging is what this file should be read for. `trace.py` checks the transcription
   // mechanically by comparing the ORDER of bus operations against the verified module.
   //
   // WHAT IT MUST NOT OWN. No expected values and no verdicts. A driver that reads back an
   // acknowledge already knows something interesting, and `if (!ack) uvm_error(...)` is a
   // two-line change that looks obviously right. It is wrong here, because the driver's
   // position is one view among several and it cannot distinguish a target that failed to
   // respond from an address the target was never meant to answer. Chapter 20.3's T8 is
   // exactly a transfer whose correct outcome is a NACK on every byte. So the driver
   // records observations into the response item and says nothing about whether they are
   // right.
   // -----------------------------------------------------------------------------

   class i2c_master_driver extends uvm_driver #(i2c_seq_item);

      `uvm_component_utils(i2c_master_driver)

      virtual i2c_if vif;
      i2c_agent_config cfg;

      // Which of the interface's pull bits are ours. Two participants sharing an index
      // would silently mask each other's releases.
      int unsigned drv_idx = 0;

      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 master driver")
         vif = cfg.vif;
         // A null virtual interface is the single most common UVM bring-up failure and it
         // presents as a null-handle dereference thousands of lines from its cause.
         if (vif == null)
            `uvm_fatal("NOVIF", "i2c_agent_config.vif is null in the master driver")
         drv_idx = cfg.drv_index;
      endfunction

      // ---- the sequencer handshake --------------------------------------------
      task run_phase(uvm_phase phase);
         super.run_phase(phase);
         release_lines();
         forever begin
            seq_item_port.get_next_item(req);
            drive_item(req);
            // item_done is not optional bookkeeping: without it the sequencer blocks
            // forever on the next item and the symptom is a hang in the SEQUENCE, which
            // sends debugging to the wrong component entirely.
            seq_item_port.item_done();
         end
      endtask

      // ---- releasing is a request; the line level is the result ---------------
      task release_lines();
         vif.drv_cb.scl_drive_low[drv_idx] <= 1'b0;
         vif.drv_cb.sda_drive_low[drv_idx] <= 1'b0;
      endtask

      // The whole of clock-stretch support on the controller side. Release SCL and WAIT
      // for the RESOLVED line to rise -- a target that is not ready holds it low, and a
      // driver that treats its own release as the line being high clocks the next bit
      // into a device that never saw an edge. The data is then simply gone, with no error
      // anywhere.
      //
      // BOUNDED, because the line may never rise. A legal stretch and a stuck line are
      // indistinguishable from the wire -- in both cases SCL is low, released, and not
      // rising -- so there is no condition to test, only a bound to choose. Chapter 20.9
      // proves both halves: T5 holds SCL low forever and the timeout must fire, T5b has a
      // real target stretch legally and the driver must wait it out.
      task scl_release_and_wait(ref i2c_seq_item rsp);
         int unsigned w = 0;
         vif.drv_cb.scl_drive_low[drv_idx] <= 1'b0;
         @(vif.drv_cb);
         while (vif.drv_cb.scl !== 1'b1 && w < cfg.stretch_timeout) begin
            @(vif.drv_cb);
            w++;
         end
         if (w > 1) stretch_seen = 1'b1;
         if (vif.drv_cb.scl !== 1'b1) begin
            timeout_seen = 1'b1;
            `uvm_warning("STRETCH", $sformatf(
               "SCL did not rise within %0d cycles -- treating the bus as stuck",
               cfg.stretch_timeout))
         end
         // The high period begins once the line has actually risen, so the half-period
         // wait belongs HERE and not in each caller. Omitting it shortens every bit in the
         // transfer by half a period, which is the kind of defect that produces an
         // intermittent, bus-rate-dependent failure -- and `trace.py` is what found it,
         // by comparing this task against the verified module it was transcribed from.
         half();
      endtask

      bit stretch_seen, timeout_seen;

      // ---- framing: the ONLY places SDA moves while SCL is high ---------------
      task pulse_start();
         vif.drv_cb.sda_drive_low[drv_idx] <= 1'b0;
         vif.drv_cb.scl_drive_low[drv_idx] <= 1'b0;
         half();
         vif.drv_cb.sda_drive_low[drv_idx] <= 1'b1;   // SDA falls, SCL high
         half();
         vif.drv_cb.scl_drive_low[drv_idx] <= 1'b1;
         half();
      endtask

      task pulse_restart(ref i2c_seq_item rsp);
         vif.drv_cb.scl_drive_low[drv_idx] <= 1'b1;
         vif.drv_cb.sda_drive_low[drv_idx] <= 1'b0;
         half();
         scl_release_and_wait(rsp);
         vif.drv_cb.sda_drive_low[drv_idx] <= 1'b1;   // SDA falls, SCL high
         half();
         vif.drv_cb.scl_drive_low[drv_idx] <= 1'b1;
         half();
      endtask

      task pulse_stop(ref i2c_seq_item rsp);
         vif.drv_cb.scl_drive_low[drv_idx] <= 1'b1;
         vif.drv_cb.sda_drive_low[drv_idx] <= 1'b1;
         half();
         scl_release_and_wait(rsp);
         vif.drv_cb.sda_drive_low[drv_idx] <= 1'b0;   // SDA rises, SCL high
         half();
      endtask

      task half();
         repeat (cfg.half_period) @(vif.drv_cb);
      endtask

      // ---- one data bit out ---------------------------------------------------
      // SDA is assigned ONLY after SCL has been pulled low. The ordering is the
      // enforcement of the data-valid rule, and there is exactly one task that moves a
      // data bit, so the rule is obeyed by construction rather than by review. A driver
      // that changes SDA while SCL is high has generated a START or a STOP whatever it
      // intended, and the resulting failure report blames the target.
      task put_bit(bit v, ref i2c_seq_item rsp);
         vif.drv_cb.scl_drive_low[drv_idx] <= 1'b1;
         half();
         vif.drv_cb.sda_drive_low[drv_idx] <= ~v;
         half();
         scl_release_and_wait(rsp);
         vif.drv_cb.scl_drive_low[drv_idx] <= 1'b1;
         half();
      endtask

      // ---- the ninth slot belongs to the receiver -----------------------------
      // RELEASE SDA, then sample. A driver that forgets to release reads back its own
      // last data bit, so every byte with bit 0 low is reported as acknowledged --
      // including transfers to addresses no device owns, which makes a NACK test pass.
      task ack_slot(output bit acked, ref i2c_seq_item rsp);
         vif.drv_cb.scl_drive_low[drv_idx] <= 1'b1;
         half();
         vif.drv_cb.sda_drive_low[drv_idx] <= 1'b0;      // RELEASE: not our slot
         half();
         scl_release_and_wait(rsp);
         // Walk to the MIDDLE of the high period before sampling, rather than reading the
         // line at the edge. The protocol guarantees SDA is stable throughout the high
         // period, so either instant is legal -- but the verified module samples at the
         // midpoint for margin, and a transcription that quietly moved the sampling point
         // is no longer a transcription.
         repeat (cfg.half_period - 2) @(vif.drv_cb);
         acked = (vif.drv_cb.sda === 1'b0);              // LOW on the wire = acknowledged
         @(vif.drv_cb);
         vif.drv_cb.scl_drive_low[drv_idx] <= 1'b1;
         half();
      endtask

      task put_byte(bit [7:0] d, output bit acked, ref i2c_seq_item rsp);
         for (int i = 7; i >= 0; i--) put_bit(d[i], rsp);
         ack_slot(acked, rsp);
      endtask

      // A transfer abandoned mid-byte. The bits sent so far never completed, no acknowledge
      // slot occurred, and no device received them -- so nothing was transferred, which is
      // what the monitor must conclude and what Chapter 20.7's T7 checks. Note that the
      // driver still frames a STOP: a controller that simply stopped clocking would leave
      // the bus held, and every later test would inherit it.
      task put_partial_byte(bit [7:0] d, int unsigned nbits, ref i2c_seq_item rsp);
         for (int i = 7; i >= 8 - nbits; i--) put_bit(d[i], rsp);
      endtask

      // One byte IN. SDA released for eight bits so the target sources them, then the
      // controller drives the ninth itself -- acknowledging every byte except the last,
      // which it NACKs to tell the target to stop sourcing. That is Chapter 18.8's
      // contract seen from this side, and it is a protocol DECISION rather than a rule.
      task get_byte(bit send_ack, output bit [7:0] d, ref i2c_seq_item rsp);
         d = 8'h00;
         for (int i = 7; i >= 0; i--) begin
            vif.drv_cb.scl_drive_low[drv_idx] <= 1'b1;
            half();
            vif.drv_cb.sda_drive_low[drv_idx] <= 1'b0;   // released: the target drives
            half();
            scl_release_and_wait(rsp);
            d[i] = vif.drv_cb.sda;
            half();
            vif.drv_cb.scl_drive_low[drv_idx] <= 1'b1;
            half();
         end
         vif.drv_cb.scl_drive_low[drv_idx] <= 1'b1;
         half();
         vif.drv_cb.sda_drive_low[drv_idx] <= send_ack;  // we own the ninth slot now
         half();
         scl_release_and_wait(rsp);
         vif.drv_cb.scl_drive_low[drv_idx] <= 1'b1;
         half();
         vif.drv_cb.sda_drive_low[drv_idx] <= 1'b0;
      endtask

      // ---- one transfer ------------------------------------------------------
      task drive_item(i2c_seq_item item);
         i2c_seq_item rsp;
         bit acked;
         bit [7:0] got;

         stretch_seen = 1'b0;
         timeout_seen = 1'b0;

         if (item.restart_first) pulse_restart(rsp);
         else                    pulse_start();

         put_byte({item.addr, item.read}, acked, rsp);
         addr_acked_obs = acked;

         if (item.abort_after_bits > 0) begin
            // Deliberately truncate, then frame a STOP and stop. Everything after this
            // point in a normal transfer is skipped.
            put_partial_byte(item.data[0], item.abort_after_bits, rsp);
            pulse_stop(rsp);
            return;
         end

         if (!item.read) begin
            for (int i = 0; i < item.n_bytes; i++) begin
               put_byte(item.data[i], acked, rsp);
               data_acked_obs[i] = acked;
            end
         end else begin
            read_back = new[item.n_bytes];
            for (int i = 0; i < item.n_bytes; i++) begin
               // acknowledge every byte except the last
               get_byte(i < item.n_bytes - 1, got, rsp);
               read_back[i] = got;
            end
         end

         // Declining to send the STOP leaves the bus held and the transfer open, to be
         // ended by whatever framing arrives next -- normally the next transfer's
         // repeated START. Without this a "repeated START" is a repeated START in name
         // only, because a STOP already ended the transfer and released the bus.
         if (!item.hold_bus) pulse_stop(rsp);
      endtask

      // ---- observations, not conclusions -------------------------------------
      bit        addr_acked_obs;
      bit        data_acked_obs [8];
      bit [7:0]  read_back [];

   endclass
Azvya Education Pvt. Ltd.VLSI Mentor
trace.py — is the published algorithm the one that was verified?
#!/usr/bin/env python3
"""
trace.py -- is the published UVM driver's algorithm the one that was VERIFIED?

Module 21's UVM source cannot be compiled here, so "this driver is correct" cannot be
established by running it. What CAN be established is that its algorithm is not new: the
bit-level behaviour is a transcription of Module 20's `i2c_ctrl_bfm`, which was simulated
against a real target in three HDLs and survived twenty mutations.

A transcription claim is worth exactly as much as the check behind it, so this reduces each
task -- in both files -- to the ORDERED SEQUENCE OF BUS OPERATIONS it performs, and compares
them. Comments, identifiers, clocking-block syntax and class structure all differ; the
operation sequence must not.

WHAT THIS DOES NOT SHOW: that either file compiles, that the timing is legal, or that the
UVM packaging around the algorithm is right. It shows one thing -- that the sequence of
pulls, releases, waits and samples is identical to the one that was verified -- and that is
the claim Module 21 is entitled to make about its driver.
"""
import io, os, re, sys

SP   = os.path.dirname(os.path.abspath(__file__))
M20  = os.path.join(os.path.dirname(SP), "m20", "src")
UVM  = os.path.join(SP, "src", "uvm")

# (label, UVM file, UVM task, Module 20 file, Module 20 task)
PAIRS = [
 ("release and wait for SCL", "i2c_master_driver.sv", "scl_release_and_wait",
  "master-agent-architecture/i2c_ctrl_bfm.sv", "scl_release_and_wait"),
 ("one data bit out",         "i2c_master_driver.sv", "put_bit",
  "master-agent-architecture/i2c_ctrl_bfm.sv", "put_bit"),
 ("the acknowledge slot",     "i2c_master_driver.sv", "ack_slot",
  "master-agent-architecture/i2c_ctrl_bfm.sv", "ack_slot"),
 ("START framing",            "i2c_master_driver.sv", "pulse_start",
  "master-agent-architecture/i2c_ctrl_bfm.sv", "pulse_start"),
 ("repeated START framing",   "i2c_master_driver.sv", "pulse_restart",
  "master-agent-architecture/i2c_ctrl_bfm.sv", "pulse_restart"),
 ("STOP framing",             "i2c_master_driver.sv", "pulse_stop",
  "master-agent-architecture/i2c_ctrl_bfm.sv", "pulse_stop"),
]

def strip_comments(s):
    s = re.sub(r'/\*.*?\*/', '', s, flags=re.S)
    return re.sub(r'//[^\n]*', '', s)

def task_body(path, name):
    src = strip_comments(io.open(path, encoding="utf-8").read())
    m = re.search(r'\btask\s+(?:automatic\s+)?' + re.escape(name) + r'\b[^;]*;', src)
    if not m:
        return None
    end = re.search(r'\bendtask\b', src[m.end():])
    return src[m.end(): m.end() + (end.start() if end else 0)]

def ops(body):
    """Reduce a task body to an ordered list of bus operations.

    TWO THINGS THIS GETS RIGHT and the first version did not:

    Tokens are emitted in TEXTUAL ORDER. The first version looped over signal names and
    so always reported SCL before SDA regardless of which the code touched first -- which
    made a matching pair look like an ordering difference. On this bus the order of two
    pulls is the entire difference between framing and a data bit, so an extractor that
    reorders them cannot be used to compare framing.

    Statements are found ANYWHERE on a line, not just at its start. Module 20's module
    style puts several statements on one line (`@(negedge clk); scl_drive_low = 1'b1; hp;`)
    and the UVM style puts each on its own, so an anchored pattern silently dropped every
    half-period wait from one side of the comparison.

    Bare cycle advances (`@(negedge clk)`, `@(vif.drv_cb)`) are deliberately NOT tokens.
    They are how each style spells "let time pass" -- procedural edge versus clocking
    block -- and including them would make this a comparison of clocking style rather than
    of bus behaviour. The half-period wait IS a token, because it is bit timing.
    """
    toks = []
    for line in body.split("\n"):
        t = line.strip()
        if not t:
            continue
        found = []
        if re.search(r'\bscl_release_and_wait\b', t):
            found.append((t.index("scl_release_and_wait"), "WAIT_SCL_RISE"))
        for sig, tag in (("scl_drive_low", "SCL"), ("sda_drive_low", "SDA")):
            for mm in re.finditer(re.escape(sig) + r'(?:\s*\[[^\]]*\])?\s*(?:<=|=)\s*([^;]+);', t):
                v = mm.group(1).strip()
                if v in ("1'b1", "1"):     found.append((mm.start(), "PULL_" + tag))
                elif v in ("1'b0", "0"):   found.append((mm.start(), "REL_" + tag))
                else:                      found.append((mm.start(), "DRIVE_" + tag))
        for mm in re.finditer(r'\b(?:hp|half\(\))\s*;', t):
            found.append((mm.start(), "HALF"))
        # The positioning loop that walks to the MIDDLE of the high period before
        # sampling. Without this token the comparison could not see WHERE in the high
        # period a sample lands -- so a transcription that sampled at the edge instead of
        # mid-period would have been reported as identical. A check that cannot see a
        # difference cannot be used to claim there is none.
        for mm in re.finditer(r'(?:for|repeat)\s*\(.*(?:HALF|half_period)\s*-\s*2', t):
            found.append((mm.start(), "SETTLE_TO_MID_HIGH"))
        for mm in re.finditer(r'=\s*~?\s*\(?\s*(?:vif\.drv_cb\.)?sda(?:_in)?\b', t):
            found.append((mm.start(), "SAMPLE_SDA"))
        for mm in re.finditer(r'while\s*\(.*scl(?:_in)?\b', t):
            found.append((mm.start(), "WAIT_LOOP_ON_RESOLVED_SCL"))
        for mm in re.finditer(r'(?:obs_timeout|timeout_seen)\s*=', t):
            found.append((mm.start(), "REPORT_TIMEOUT"))
        for mm in re.finditer(r'(?:obs_stretched|stretch_seen)\s*=', t):
            found.append((mm.start(), "REPORT_STRETCH"))
        for mm in re.finditer(r'for\s*\(.*\b[ik]\b\s*=\s*7', t):
            found.append((mm.start(), "LOOP_8_BITS_MSB_FIRST"))
        toks.extend(tok for _, tok in sorted(found, key=lambda x: x[0]))
    return toks

def main():
    print("%-26s %-9s %s" % ("ALGORITHM", "VERDICT", "OPERATION SEQUENCE"))
    print("%-26s %-9s %s" % ("-" * 26, "-" * 9, "-" * 42))
    bad = 0
    for label, uf, ut, mf, mt in PAIRS:
        ub = task_body(os.path.join(UVM, uf), ut)
        mb = task_body(os.path.join(M20, mf), mt)
        if ub is None or mb is None:
            print("%-26s %-9s task not found (%s / %s)" % (label, "ERROR", ut, mt)); bad += 1; continue
        uo, mo = ops(ub), ops(mb)
        if uo == mo:
            print("%-26s %-9s %s" % (label, "IDENTICAL", " ".join(uo)))
        else:
            bad += 1
            print("%-26s %-9s" % (label, "DIFFERS"))
            print("%-26s %-9s UVM : %s" % ("", "", " ".join(uo)))
            print("%-26s %-9s M20 : %s" % ("", "", " ".join(mo)))
    print()
    print("algorithms compared : %d" % len(PAIRS))
    print("identical           : %d" % (len(PAIRS) - bad))
    print("differing           : %d" % bad)
    return 1 if bad else 0

if __name__ == "__main__":
    sys.exit(main())

6. What the Traceability Check Found

It found two real defects in the code published above, and reporting them is more useful than the six green rows.

A third defect was found by a different check and is worth recording here because it is the one a reader is most likely to reproduce: a stray ) typed into this file passed all thirteen structural checks, because a structural checker has no opinion about punctuation. Chapter 21.11 adds two syntax-hygiene checks for precisely that hole, and they exist because of this file.

7. Two Interface Features, Both Added for a Found Reason

req_hold — decline to send the trailing STOP. Without it, restart_first is unreachable in any meaningful sense: a transfer that always ends in a STOP can only be followed by a plain START, and a "repeated START" after a STOP is indistinguishable from an ordinary one to every device on the bus. Chapter 20.3 found this by trying to write a framing test and being unable to. A controller that wants to change direction without releasing the bus has to be able to keep it, and that is a driver capability rather than a test trick.

abort_after_bits — abandon the transfer part way through a byte, then frame a STOP. This is legal controller behaviour producing an illegal-looking bus event: a driver whose software timed out, a master that lost arbitration. It belongs on the ordinary item rather than in a derived error class, and 21.10 draws the line between violations a controller can produce and violations that are another device misbehaving.

Note that it still frames a STOP. A controller that simply stopped clocking would leave the bus held, and every later test would inherit it.

The driver that worked against four targets and lost data on the fifth

Pitfall — a release treated as a result
Buggy Code
// A UVM master driver in use for two years against four different targets.
// It has never produced a wrong byte.
//
//    task put_bit(bit v);
//      vif.drv_cb.scl_drive_low <= 1'b1;   repeat (HALF) @(vif.drv_cb);
//      vif.drv_cb.sda_drive_low <= ~v;     repeat (HALF) @(vif.drv_cb);
//      vif.drv_cb.scl_drive_low <= 1'b0;   repeat (HALF) @(vif.drv_cb);  // release
//      vif.drv_cb.scl_drive_low <= 1'b1;   repeat (HALF) @(vif.drv_cb);  // pull again
//    endtask
//
// The fifth target stretches the clock. Reads come back with bits from the wrong
// positions, intermittently, depending on how busy the target is.
//
// The driver never reads vif.drv_cb.scl. It releases SCL, waits HALF, and pulls
// it low again -- so while the target holds SCL down, the driver completes an
// entire "bit period" in which SCL NEVER ROSE. No edge occurred, the target
// sampled nothing, and the driver moved on.
//
// The loss is silent: from the driver's point of view every bit was sent.
Pitfall — every NACK reported as an acknowledge, and a NACK test that passes
Buggy Code
// The acknowledge slot in a driver that has never failed a test:
//
//    task ack_slot(output bit acked);
//      vif.drv_cb.scl_drive_low[i] <= 1'b1;  half();
//      vif.drv_cb.scl_drive_low[i] <= 1'b0;  half();   // SCL released...
//      acked = (vif.drv_cb.sda === 1'b0);              // ...and SDA never released
//      vif.drv_cb.scl_drive_low[i] <= 1'b1;  half();
//    endtask
//
// SDA still carries bit 0 of the byte just sent. For any byte with bit 0 = 0 --
// half of all bytes, and every EVEN address -- the driver is pulling SDA low
// itself and reads back acked = 1.
//
// So "the target acknowledged" is reported for a target that is absent, held in
// reset, or a different device entirely. The environment's NACK test writes to an
// unused address, expects acked = 0, gets 1... and was written to expect 1,
// because that is what the driver has always returned. The test now DOCUMENTS
// the bug.

8. What 21.3 Settled

The driver translates intent into edges and owns nothing else. The same observation is correct behaviour in one test and a failure in another, which is exactly why the driver must not decide.

Protocol obligations are discharged structurally. One task moves a data bit, and in it SDA is assigned only after SCL is pulled low. Framing has its own tasks and they are the only code that moves SDA while SCL is high.

A release is a request; the resolved line is the result. The bounded wait for that line is the whole of clock-stretch support, and its bound is a policy rather than a derivable fact — so it needs a test on each side.

An unexecutable file can still be checked against an executed one. Six algorithms compared, six identical, five perturbations detected — and two real defects found in this chapter's own code, one of which required extending the tool before the fix meant anything.

A structural checker has no opinion about punctuation. A stray bracket in this file passed thirteen checks, which is why 21.11 has two more.

Next, the other active component — and the one whose structure is genuinely different, because a responder has no items to fetch. Chapter 21.4 — The Target Responder Driver.

Continue learning