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 owns | the driver must not own |
|---|---|
| the SCL waveform: when every bit moves | any expected value |
| turning an intent into framing and bytes | any verdict |
| obeying every protocol rule itself | any knowledge of the target's register map |
| reporting what it observed from its own position | whether 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.
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++;
endOn 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.
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();
endtaskSDA 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
// -----------------------------------------------------------------------------
// 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#!/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
// 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
// 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
Related tutorials
- Related topic
The Master Agent — Stimulus That Owns the Bus
The driver that turns an intent into edges, owns SCL, and must obey every rule it exists to test others against. Covers the difference between releasing a line and the line being high, why every wait needs a bound and two tests, and why a completion handshake must be a count rather than a pulse.
- 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.
