I²C · Module 21
The Environment — Agents, Scoreboard, Coverage and Configuration
The one connection that decides whether an environment can detect anything, why the contract is copied from the datasheet rather than read from the DUT, and why a checker needs one negative test per comparison path — a mutation that disabled half a scoreboard survived a suite that already had one.
The environment is where the architecture becomes visible. Trace its arrows and you can tell, without reading a line of component code, whether it is capable of detecting anything.
1. The One Connection That Matters
if (cfg.have_scoreboard) begin
sb.pred = pred;
ctrl_agent.monitor.ap.connect(sb.txn_imp);
endObserved transactions go from the monitor to the scoreboard. Nothing connects a sequence or a driver to the comparison.
That absence is the design. Chapter 21.2 made the circular comparison a type error; this makes it a topology error as well. There is no path by which a sequence's own object reaches the checking components, so the comparison cannot close a loop that excludes the design.
The predictor's inputs come from the same monitor, and that is not circular either: the monitor supplies which bytes actually arrived, and the contract supplies what should have happened to them. Fed from the driver instead, the predictor would be predicting about traffic that may never have reached the bus.
2. The Contract Is Configuration, Not Introspection
pred = i2c_predictor::type_id::create("pred", this);
pred.my_addr = cfg.dut_addr;
pred.n_reg = cfg.dut_n_reg;
pred.ro_mask = cfg.dut_ro_mask;Those three values are a transcription of the datasheet, handed in by whoever instantiates the environment. They are deliberately not read out of the DUT, and the distinction is the difference between a reference model and a mirror: a model parameterised from the design agrees with the design by construction, including where the design is wrong.
The six decisions the predictor implements are Module 18's D1 to D6, and none of them is in any specification. Chapter 20.8 §1 lists them; the one worth repeating here is that D6 is not implied by D4. Refusing a write and not advancing the pointer are separate behaviours with separate failure modes, and a target can do the first correctly and the second wrongly.
// -----------------------------------------------------------------------------
// i2c_predictor.sv
// The device contract, as executable code. Six sentences and nothing else.
//
// NOT EXECUTED -- see i2c_if.sv. Transcribed from Chapter 20.8's `i2c_pred`, which was
// simulated against Module 18's real target in three HDLs and survived sixteen mutations.
//
// WHERE ITS INPUTS COME FROM, and it is the whole design: the MONITOR, never the driver.
// It has to predict about transfers that actually happened, and the monitor is what
// knows which bytes arrived. That is not circular -- the monitor supplies the STIMULUS to
// the model and the contract supplies the RESPONSE. Fed from the driver instead, it
// would predict about traffic that may never have reached the bus.
//
// It also never reads the target's registers or pointer. It keeps its own and evolves
// them by D1 to D6. A model that read the DUT's state would agree with the DUT by
// construction, which is the difference between a reference model and a mirror.
//
// D1 an accepted data byte writes the addressed register and ADVANCES the pointer
// D2 the pointer is taken MODULO the map size: the map WRAPS, it does not clamp
// D3 the pointer is APPLICATION state -- framing does not clear it
// D4 a write to a read-only register is REFUSED, and refusal means a NACK on it
// D5 after a byte is read out the pointer sits ONE PAST the byte just served
// D6 a REFUSED write does not advance the pointer
//
// None of that is in UM10204. Every line is a decision someone made about this part, and
// D6 is NOT implied by D4 -- refusing a write and not advancing the pointer are separate
// behaviours with separate failure modes, which is why a target can do the first
// correctly and the second wrongly.
// -----------------------------------------------------------------------------
class i2c_predictor extends uvm_component;
`uvm_component_utils(i2c_predictor)
// ---- the contract's parameters, from the datasheet ----------------------
bit [6:0] my_addr = 7'h50;
int unsigned n_reg = 8;
bit [7:0] ro_mask = 8'h04; // bit k set: register k is read-only
// ---- the model's OWN state ---------------------------------------------
bit [7:0] regs [];
int unsigned ptr = 0;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
regs = new[n_reg];
foreach (regs[i]) regs[i] = 8'h00;
endfunction
// D2: the map WRAPS. Written as a modulo because that is what the contract says. A
// mask would be equivalent only while n_reg is a power of two, and which of the two
// is correct is a statement about the device rather than about arithmetic.
function int unsigned in_range(int unsigned p);
return p % n_reg;
endfunction
// D4: read-only is a property of the ADDRESSED register.
function bit is_ro(int unsigned p);
return ((ro_mask >> in_range(p)) & 1) != 0;
endfunction
// ---- what SHOULD have happened -----------------------------------------
// A FUNCTION of the observed transfer and the state before it. The state evolves
// separately, below. Conflating the two is the defect that made Chapter 20.8's first
// scoreboard compare each acknowledge against the PREVIOUS byte's expectation --
// reporting a read-only NACK as an error and every acknowledged address as an error,
// against a correct design and a correct contract. Its signature is that every
// reported value is legal, because every value was legal for the adjacent item.
function void predict(i2c_txn t,
output bit exp_addr_ack,
output bit exp_acks [],
output bit [7:0] exp_read [] );
int unsigned p;
bit addressed;
exp_addr_ack = (t.addr == my_addr);
addressed = exp_addr_ack;
exp_acks = new[t.data.size()];
exp_read = new[t.data.size()];
p = ptr;
foreach (t.data[i]) begin
if (!addressed) begin
exp_acks[i] = 1'b0; // not ours: we answer nothing
exp_read[i] = 8'h00;
end else if (t.read) begin
exp_acks[i] = 1'b0; // the CONTROLLER owns a read's ack
exp_read[i] = regs[in_range(p)];
p++; // D5
end else if (i == 0) begin
exp_acks[i] = 1'b1; // the pointer byte is always accepted
exp_read[i] = 8'h00;
end else begin
exp_acks[i] = ~is_ro(p); // D4
exp_read[i] = 8'h00;
if (!is_ro(p)) p++; // D1, and D6 by omission
end
end
endfunction
// ---- how the state evolves ---------------------------------------------
// Called once per observed transfer, AFTER the prediction for it has been made.
// D3 is expressed by what this function does NOT do: nothing here clears `ptr` on
// framing, because the pointer is application state and framing does not touch it.
function void update(i2c_txn t);
if (t.addr != my_addr) return;
foreach (t.data[i]) begin
if (t.read) begin
ptr++; // D5
end else if (i == 0) begin
ptr = t.data[0]; // the pointer byte selects
end else if (!is_ro(ptr)) begin
regs[in_range(ptr)] = t.data[i]; // D1
ptr++;
end
// D6: a refused write falls through here and does NOT advance ptr.
end
endfunction
endclassThe structural rule inside that file is the one this curriculum has now needed three times:
The prediction is a function; the state evolution is a register.
What should happen to the current transfer is computed from that transfer and the state before it. The state advances separately, after the prediction has been made. Conflating them produces a comparison that runs one item out of phase, reporting a read-only NACK as an error and every acknowledged address as an error — against a correct design and a correct contract.
3. Two Counters, Not One Flag
// -----------------------------------------------------------------------------
// i2c_scoreboard.sv
// Compares, and holds no knowledge at all.
//
// NOT EXECUTED -- see i2c_if.sv. Transcribed from Chapter 20.8's `i2c_sb`.
//
// TWO COUNTERS, NOT ONE FLAG, and the reason is diagnostic rather than aesthetic. An
// acknowledge mismatch means the target's DECISION differed from the contract; a data
// mismatch means its CONTENT differed. Those are different bugs in different logic, and
// a single verdict bit makes them indistinguishable in the failure report -- so the
// first thing anyone does after a failure is guess.
//
// Chapter 20.9 produces the case that makes this concrete: a disturbance spanning a
// sampling edge turns 0xFF into 0x7F with EVERY BYTE STILL ACKNOWLEDGED. The protocol
// layer is flawless. Only the data comparison sees it.
//
// `uvm_analysis_imp` rather than a `uvm_subscriber`, because there are two streams to
// receive -- observed transactions and, for the layered checks, bytes -- and a subscriber
// gives you one `write` method. Two named imps make the connection explicit at the
// point of connection rather than encoded in a type parameter.
// -----------------------------------------------------------------------------
`uvm_analysis_imp_decl(_txn)
class i2c_scoreboard extends uvm_scoreboard;
`uvm_component_utils(i2c_scoreboard)
uvm_analysis_imp_txn #(i2c_txn, i2c_scoreboard) txn_imp;
i2c_predictor pred;
// The two verdicts, kept apart by layer.
int unsigned n_ack_mismatch;
int unsigned n_data_mismatch;
int unsigned n_checked;
// Armed by the test. A scoreboard that has never reported a mismatch has never been
// shown capable of reporting one, and that is the DEFAULT state of a new scoreboard
// -- indistinguishable from a working one. `corrupt_*` below exist so a test can
// falsify the comparison deliberately; they are not part of the architecture.
bit armed = 1'b1;
bit corrupt_pred = 1'b0;
bit corrupt_ack = 1'b0;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
txn_imp = new("txn_imp", this);
endfunction
// The scoreboard receives an OBSERVATION and asks the predictor what should have
// happened. It contains no protocol knowledge, no register map and no contract --
// which is precisely what keeps it from acquiring the DUT's bugs.
function void write_txn(i2c_txn t);
bit exp_addr_ack;
bit exp_acks [];
bit [7:0] exp_read [];
if (!armed || pred == null) return;
pred.predict(t, exp_addr_ack, exp_acks, exp_read);
if (corrupt_ack) exp_addr_ack = ~exp_addr_ack;
if (corrupt_pred && exp_read.size() > 0) exp_read[0] = ~exp_read[0];
n_checked++;
// ---- PROTOCOL layer: did the target decide as the contract requires? ----
if (t.addr_acked !== exp_addr_ack) begin
n_ack_mismatch++;
`uvm_error("ACKMM", $sformatf(
"address 0x%02h: observed ack=%0b, contract requires %0b",
t.addr, t.addr_acked, exp_addr_ack))
end
foreach (t.acks[i]) begin
// A read's acknowledge is sent by the CONTROLLER, so it is not the target's
// decision and is not compared here. Comparing it would report a mismatch on
// every correct read.
if (!t.read && t.acks[i] !== exp_acks[i]) begin
n_ack_mismatch++;
`uvm_error("ACKMM", $sformatf(
"byte %0d of a write to 0x%02h: observed ack=%0b, contract requires %0b",
i, t.addr, t.acks[i], exp_acks[i]))
end
end
// ---- APPLICATION layer: did the target return the right CONTENT? -------
// Each conjunct is a statement about what the comparison means. The predictor
// models THIS device, so its expected read data is meaningless for a transfer
// aimed elsewhere -- and a scoreboard without the `exp_addr_ack` guard would
// report a mismatch on every legal foreign read.
if (exp_addr_ack && t.read) begin
foreach (t.data[i]) begin
if (t.data[i] !== exp_read[i]) begin
n_data_mismatch++;
`uvm_error("DATAMM", $sformatf(
"read byte %0d from 0x%02h: observed 0x%02h, contract requires 0x%02h",
i, t.addr, t.data[i], exp_read[i]))
end
end
end
// The model advances only after the prediction has been made against the state
// that was current when the transfer occurred.
pred.update(t);
endfunction
function void report_phase(uvm_phase phase);
super.report_phase(phase);
`uvm_info("SB", $sformatf(
"%0d transfers checked, %0d acknowledge mismatches, %0d data mismatches",
n_checked, n_ack_mismatch, n_data_mismatch), UVM_LOW)
// A scoreboard that checked nothing is a passing test that proved nothing, and it
// must be an error rather than a silence.
if (n_checked == 0)
`uvm_error("SB", "the scoreboard received no transactions -- nothing was checked")
endfunction
endclassThe scoreboard maintains n_ack_mismatch and n_data_mismatch separately. That is diagnostic rather than aesthetic: an acknowledge mismatch means the target's decision differed from the contract, and a data mismatch means its content did. Different bugs, different logic, and a single verdict bit makes them indistinguishable in the failure report — so the first thing anyone does after a failure is guess.
Note also what the scoreboard does not compare: a read's acknowledge bits. Those are sent by the controller, not the target, so they are not the target's decision — and comparing them would report a mismatch on every correct read. Each guard in the comparison is a statement about what the comparison means, and mutation S05 removing the addressed guard survived the first test suite because no foreign read had ever been driven.
4. A Checker Needs One Negative Test Per Path
A scoreboard that has never reported a mismatch has never been shown capable of reporting one. That is the default state of a new scoreboard and it is indistinguishable from a working one.
That is why the scoreboard carries armed, corrupt_pred and corrupt_ack. They exist so a test can falsify the comparison deliberately, they are off in every other test, and the alternative is a scoreboard nobody has ever seen fail.
report_phase raises an error when zero transactions were checked. A scoreboard that checked nothing is a passing test that proved nothing, and that has to be loud rather than silent.
5. Coverage Answers a Different Question
// -----------------------------------------------------------------------------
// i2c_coverage.sv
// What was exercised -- which is a different question from what was checked.
//
// NOT EXECUTED -- see i2c_if.sv. Note also that the only Verilog simulator available
// here rejects covergroups outright, so this file has never been elaborated in any form.
//
// WHY IT IS DELIBERATELY SMALL. Coverage measures whether a situation OCCURRED. It says
// nothing about whether anything looked. Chapter 20.8 reports six mutations that
// survived a structurally correct environment, and coverage would have found none of
// them: every line involved was executed by the very tests that failed to kill them.
// So the bins here are chosen to answer questions the mutation campaign could not --
// did we ever see a repeated START, a stretched transfer, an out-of-range pointer -- and
// not to produce a percentage.
//
// Full coverage modelling, closure criteria and the argument about what a percentage is
// worth belong to Module 22. This is the subscriber the environment needs in order to
// have somewhere to put it.
// -----------------------------------------------------------------------------
class i2c_coverage extends uvm_subscriber #(i2c_txn);
`uvm_component_utils(i2c_coverage)
i2c_txn tr;
covergroup cg_txn;
option.per_instance = 1;
// Direction, and the fact that both were seen at all.
cp_dir : coverpoint tr.read { bins write = {0}; bins read = {1}; }
// Was the address ours, one bit away from ours, or unrelated? A one-bit near miss
// is the only case that demonstrates a specific address bit participates -- an
// address four bits away is rejected by a comparator missing any one of them.
cp_addr : coverpoint tr.addr {
bins mine = {7'h50};
bins near_miss = {7'h51, 7'h52, 7'h54, 7'h58, 7'h40, 7'h60, 7'h10};
bins other = default;
}
// How the transfer ENDED. A transfer ended by a repeated START exercises different
// state retention from one ended by a STOP, and a run with no repeated START has
// not tested Decision D3 at all.
cp_end : coverpoint tr.ended_by_restart { bins stop = {0}; bins restart = {1}; }
// Was the address acknowledged? Both outcomes are legal and a run containing only
// one of them has exercised half the addressing logic.
cp_acked : coverpoint tr.addr_acked { bins nacked = {0}; bins acked = {1}; }
// Byte count, with the boundary cases separated from the middle.
cp_len : coverpoint tr.data.size() {
bins none = {0};
bins one = {1};
bins few = {[2:3]};
bins many = {[4:8]};
}
// Did a transfer ever end mid-byte? A correct controller never produces this, so
// an empty bin here means the error sequences of Chapter 21.10 never ran.
cp_trunc : coverpoint tr.truncated { bins clean = {0}; bins truncated = {1}; }
// The cross that matters: direction against how the transfer ended. The
// write-pointer-then-read sequence is a read that BEGAN with a repeated START, and
// it is the access pattern every register device actually uses -- so an empty cell
// here means the most common real transaction shape was never driven.
x_dir_end : cross cp_dir, cp_end;
endgroup
function new(string name, uvm_component parent);
super.new(name, parent);
cg_txn = new();
endfunction
function void write(i2c_txn t);
tr = t;
cg_txn.sample();
endfunction
endclassCoverage measures whether a situation occurred. It says nothing about whether anything looked.
Full coverage modelling and closure criteria are Module 22's subject. This is the subscriber the environment needs so there is somewhere to put it.
6. Configurable Means Another Project Does Not Edit It
// -----------------------------------------------------------------------------
// i2c_env.sv
// The composition, and the place where "configurable" stops being a slogan.
//
// NOT EXECUTED -- see i2c_if.sv.
//
// WHAT IS CONFIGURABLE HERE AND WHY IT MATTERS FOR REUSE: the number of agents, which
// of them are active, which is the controller, the address the predictor models, the
// register map size and the read-only mask. Every one of those is a thing a different
// project would need to change, and every one of them hard-coded is a reason that
// project forks the VIP instead of adopting it. Chapter 21.11 is about that test.
//
// THE SCOREBOARD IS CONNECTED TO A MONITOR, NOT TO A DRIVER. That single connection is
// the whole of oracle independence at the topology level: there is no path in this
// environment by which a sequence's own object reaches the comparison. Trace the arrows
// and the DUT is between the two quantities being compared.
// -----------------------------------------------------------------------------
class i2c_env_config extends uvm_object;
`uvm_object_utils(i2c_env_config)
virtual i2c_if vif;
int unsigned n_agents = 1;
bit have_responder = 1'b0; // model a second device on the bus
bit have_scoreboard = 1'b1;
bit have_coverage = 1'b1;
// The contract the predictor will model. Not the DUT's parameters read out of the
// DUT -- a copy of the DATASHEET, supplied by whoever instantiates the environment.
bit [6:0] dut_addr = 7'h50;
int unsigned dut_n_reg = 8;
bit [7:0] dut_ro_mask = 8'h04;
int unsigned half_period = 16;
int unsigned stretch_timeout = 4000;
function new(string name = "i2c_env_config");
super.new(name);
endfunction
endclass
class i2c_env extends uvm_env;
`uvm_component_utils(i2c_env)
i2c_env_config cfg;
i2c_agent ctrl_agent;
i2c_agent resp_agent;
i2c_scoreboard sb;
i2c_predictor pred;
i2c_coverage cov;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
i2c_agent_config acfg;
super.build_phase(phase);
if (!uvm_config_db #(i2c_env_config)::get(this, "", "cfg", cfg))
`uvm_fatal("NOCFG", "no i2c_env_config for the environment")
if (cfg.vif == null)
`uvm_fatal("NOVIF", "i2c_env_config.vif is null")
// ---- the controller agent -------------------------------------------
acfg = i2c_agent_config::type_id::create("ctrl_cfg");
acfg.vif = cfg.vif;
acfg.drv_index = 0;
acfg.is_active = UVM_ACTIVE;
acfg.role = I2C_CONTROLLER;
acfg.half_period = cfg.half_period;
acfg.stretch_timeout = cfg.stretch_timeout;
uvm_config_db #(i2c_agent_config)::set(this, "ctrl_agent", "cfg", acfg);
ctrl_agent = i2c_agent::type_id::create("ctrl_agent", this);
// ---- an optional second device --------------------------------------
if (cfg.have_responder) begin
acfg = i2c_agent_config::type_id::create("resp_cfg");
acfg.vif = cfg.vif;
acfg.drv_index = 1; // its OWN pull index
acfg.is_active = UVM_ACTIVE;
acfg.role = I2C_TARGET;
acfg.half_period = cfg.half_period;
acfg.stretch_timeout = cfg.stretch_timeout;
uvm_config_db #(i2c_agent_config)::set(this, "resp_agent", "cfg", acfg);
resp_agent = i2c_agent::type_id::create("resp_agent", this);
end
if (cfg.have_scoreboard) begin
pred = i2c_predictor::type_id::create("pred", this);
pred.my_addr = cfg.dut_addr;
pred.n_reg = cfg.dut_n_reg;
pred.ro_mask = cfg.dut_ro_mask;
sb = i2c_scoreboard::type_id::create("sb", this);
end
if (cfg.have_coverage)
cov = i2c_coverage::type_id::create("cov", this);
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
// THE ONE CONNECTION THAT DEFINES THE ENVIRONMENT. Observed transactions go from
// the monitor to the scoreboard. Nothing connects a sequence or a driver to the
// comparison, and the absence of that arrow is what makes the comparison mean
// something.
if (cfg.have_scoreboard) begin
sb.pred = pred;
ctrl_agent.monitor.ap.connect(sb.txn_imp);
end
if (cfg.have_coverage)
ctrl_agent.monitor.ap.connect(cov.analysis_export);
// The byte-level stream exists for consumers below the transaction level. It is
// connected to coverage so that it is not a port nothing consumes -- an analysis
// port with no subscriber is silently dead, and its absence looks identical to a
// quiet bus.
ctrl_agent.monitor.byte_ap.connect(cov.analysis_export);
endfunction
// A topology report, printed once. It is the cheapest possible defence against the
// commonest environment fault: a component that was never built because a
// configuration field was mistyped, which produces no error and no activity.
function void end_of_elaboration_phase(uvm_phase phase);
super.end_of_elaboration_phase(phase);
`uvm_info("ENV", $sformatf(
"controller=%0s responder=%0s scoreboard=%0s coverage=%0s target=0x%02h regs=%0d ro=0x%02h",
(ctrl_agent != null) ? "yes" : "NO",
(resp_agent != null) ? "yes" : "no",
(sb != null) ? "yes" : "no",
(cov != null) ? "yes" : "no",
cfg.dut_addr, cfg.dut_n_reg, cfg.dut_ro_mask), UVM_LOW)
endfunction
endclassEvery field in i2c_env_config is there because a different project would need to change it: the target's address, register count and read-only mask; whether a second device is modelled; whether the scoreboard and coverage exist at all; the bit period and stretch patience.
Each of those hard-coded is a reason that project forks the VIP, and a forked VIP stops receiving fixes on the day it is forked. Chapter 21.11 makes that the explicit test.
The end_of_elaboration_phase report is the cheapest possible defence against the commonest environment fault:
`uvm_info("ENV", $sformatf(
"controller=%0s responder=%0s scoreboard=%0s coverage=%0s target=0x%02h regs=%0d ro=0x%02h",
(ctrl_agent != null) ? "yes" : "NO", ...A component that was never built because a configuration field was mistyped produces no error and no activity. One line naming what exists turns that into something a reader notices in the log.
Half the scoreboard was dead and every test was green
Pitfall — one negative test for two comparison paths
// A scoreboard with two independent comparisons:
//
// // PROTOCOL layer
// if (t.addr_acked !== exp_addr_ack) n_ack_mismatch++;
//
// // APPLICATION layer
// if (exp_addr_ack && t.read)
// foreach (t.data[i])
// if (t.data[i] !== exp_read[i]) n_data_mismatch++;
//
// The bench proves the scoreboard can fail:
//
// corrupt_pred = 1'b1; // corrupt the predicted DATA
// do_read();
// ck("a mismatch is reported", sb.n_data_mismatch > 0, 1); // PASSES
//
// Sign-off records "the scoreboard has a negative test".
//
// Then a mutation disables the ACKNOWLEDGE comparison completely:
//
// if (t.addr_acked !== exp_addr_ack) -> if (1'b0)
//
// It SURVIVES every test. Half the scoreboard is dead and the suite is green: the
// read-only NACK check, the address-match check and the foreign-address check are
// all reading a counter that can no longer increment.Pitfall — a reference model parameterised from the DUT
// The predictor is configured by reading the DUT's own parameters, which looks
// like the way to keep them in step:
//
// function void build_phase(uvm_phase phase);
// super.build_phase(phase);
// pred = i2c_predictor::type_id::create("pred", this);
// pred.my_addr = i2c_tb_top.dut.MY_ADDR; // hierarchical reference
// pred.n_reg = i2c_tb_top.dut.N_REG;
// pred.ro_mask = i2c_tb_top.dut.RO_MASK;
// endfunction
//
// Every address-match and read-only test now passes unconditionally, for every
// value of those parameters -- including wrong ones.
//
// The DUT is integrated with RO_MASK = 8'h00 by mistake, so no register is
// protected. The predictor reads 8'h00 too, expects every write to be accepted,
// and the target accepts every write. PERFECT AGREEMENT. The read-only feature
// is gone and the environment reports nothing, because both sides are wrong in
// exactly the same way.7. What 21.8 Settled
One connection decides everything. Observed from the monitor, expected from the contract, and no path at all from a sequence to the comparison.
The contract is copied in, not read out. A model parameterised from the design agrees with it for every value including wrong ones, and the deliberate duplication is what makes disagreement possible.
The prediction is a function; the state evolution is a register. Conflating them runs the comparison one item out of phase and reports errors against a correct design.
Two counters, because a protocol-perfect transfer can carry wrong data — which 20.9 demonstrates in simulation with every byte acknowledged.
One negative test per comparison path, each asserting the other counters stayed still, plus an error when nothing was checked at all.
Coverage answers a different question from mutation, and would have found none of the six survivors, because every line involved was already executed.
Next, what actually runs in this environment — and why a sequence that contains an expected value is a sequence that has to be edited when the device changes. Chapter 21.9 — Sequencer and Sequences.
Continue learning
Related tutorials
- 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
Assembling the I²C Agent
Why passive must mean not built rather than told to be quiet, why configuration is handed down rather than looked up, and why a participant plays one role. Includes a driver and a monitor bound to different buses with no error reported anywhere.
- Related topic
Reference Model and Scoreboard Strategy
Where an expected value legitimately comes from, why the prediction must be a function and only the state a register, and why a scoreboard needs one negative test per comparison path. Runs against Module 18's target in three languages, and works through six mutations that survived a structurally correct scoreboard.
- Related topic
Verification Completeness Review
Judging whether an I²C testbench has proven anything, by asking whether it is capable of failing. Two measured experiments: a scoreboard that performs seven comparisons and reports zero mismatches with the device physically absent, and a defect that survives at 100 % coverage until one extra transfer is added.
