I²C · Module 20
Monitor Architecture and Transaction Reconstruction
Rebuilding a transaction from raw edges without assuming the design is correct and without borrowing a line of its framing logic. Covers why an edge-driven monitor handles clock stretching for free, why the byte and its acknowledge are one fact, and the two illegal traces that make the framing rules testable at all.
The monitor is the hardest component in the environment to get right, and the reason is not that reconstruction is difficult. It is that every shortcut available to it is a shortcut that makes the environment agree with the design.
It has to answer, from two wires: where did a transfer start, which address was it, which direction, how many bytes, was each one acknowledged, and how did it end. All of that is derivable from SCL and SDA. It is also derivable from the design's own internal state, and from the driver's idea of what it sent, and both of those are much easier.
1. What It Must Know, and What It Must Not
| the monitor knows | the monitor must not know |
|---|---|
| the resolved level of SCL and SDA | what the driver intended to send |
| the specification's framing rules | the target's register map |
| how to assemble bits into bytes | any expected value |
| the direction bit's position | whether what it saw was correct |
Two inputs, and one of them is a clock for its own registers. That is the entire interface to the outside world, and it is enforced by the port list rather than by discipline — Chapter 20.4 argues that case.
The right-hand column has a consequence that is easy to state and easy to violate: the monitor reports a NACK, it does not report an error. A NACK is frequently the correct behaviour — a foreign address, a read-only register, the controller terminating a read. A monitor that flags it has made a judgement using knowledge it does not have, and the environment now reports failures on correct traffic.
2. Independence Costs a Reimplementation
Module 18's target contains a framing detector. It is correct, it is verified, and reusing it inside the monitor would be the normal engineering choice.
It would also destroy the monitor. Two instances of the same logic given the same inputs agree always — including when the logic is wrong. A monitor whose framing comes from the design's framing detector cannot detect a framing bug, which is a substantial fraction of what it exists for.
So the monitor implements framing again, from the specification:
wire scl_rise = scl & ~scl_d;
wire scl_fall = ~scl & scl_d;
wire sda_fall = ~sda & sda_d;
wire sda_rise = sda & ~sda_d;
// Framing is an SDA edge while SCL is HIGH -- the same PROTOCOL DEFINITION as
// Chapter 18.3, reached by a different implementation.
wire start_now = sda_fall & scl & scl_d;
wire stop_now = sda_rise & scl & scl_d;Note scl & scl_d rather than scl. An SDA transition in the same cycle as an SCL edge is not framing — it is a data bit changing at a boundary — and requiring SCL to have been high in the previous cycle as well is what makes the distinction. Mutation M10 removes the SCL condition entirely and the VHDL twin's bench kills it in twenty-one checks, which is a measure of how much of the reconstruction depends on framing being right.
3. Edge-Driven, Which Makes Stretching Free
The monitor has no notion of a bit period, a baud rate, or a clock divider. Everything it does is triggered by an edge on SCL or SDA.
That is not a stylistic choice; it is what makes clock stretching a non-issue. A monitor that counted cycles between bits would need to know the bus rate, and would break the moment a target held SCL low — the very feature the environment most needs to be able to watch. An edge-driven monitor sees a stretched transfer as a transfer whose edges are further apart, which is exactly what it is.
end else if (scl_rise && txn_active) begin
if (bitcnt < 4'd8) begin
shreg <= {shreg[6:0], sda};
bitcnt <= bitcnt + 4'd1;
in_byte <= 1'b1;
end else begin
// The ninth bit: the acknowledge. LOW means acknowledged, and it is
// whoever is RECEIVING that drives it -- the monitor does not care
// which device, only what the bus said.
byte_valid <= 1'b1;
byte_data <= shreg;
byte_acked <= ~sda;
byte_is_addr <= ~addr_seen;
n_bytes <= n_bytes + 16'd1;
if (sda) n_nacks <= n_nacks + 16'd1;
if (!addr_seen) beginSampling SDA at SCL's rising edge is not a convenience. It is the protocol's own guarantee: SDA may not change while SCL is high, so the rising edge is the instant at which the value is defined. A monitor that sampled anywhere else would be reading a line that is permitted to be in transition.
4. The Byte and Its Acknowledge Are One Fact
byte_valid fires at the ninth rising edge, not the eighth, and carries byte_data and byte_acked together.
The reason is that a byte without its acknowledge is not yet a protocol fact. Eight bits have appeared on the wire; whether they were received is undetermined until the ninth slot. A monitor that reported at the eighth edge would force every consumer to remember the byte and wait for a separate acknowledge event — and every consumer would have to get that pairing right independently.
5. The Cases That Decide Whether It Works
Four reconstruction cases separate a monitor that works from one that works on cooperative traffic.
A repeated START ends one transaction and begins another. Not "ends" and not "begins" — both, in the same cycle. The monitor reports txn_done with txn_ended_by_restart set while simultaneously opening the next phase, and a consumer that assumed a STOP always precedes a START would lose the second phase entirely.
A truncated byte transfers nothing. If framing arrives mid-byte, the bits accumulated so far are not data. They never completed, no acknowledge slot occurred, and no device received them. The monitor discards them, and n_bytes does not increase.
An illegal SDA change while SCL is high is framing. Not an error to be flagged, and not a data bit to be shrugged at — by the specification it is a START or STOP, and every conforming device will treat it as one. The monitor treats it the same way, which means a partial byte is dropped and a new phase opens. This is the behaviour the fault injector in 20.9 relies on to reframe a real target mid-transfer.
The data count is per phase, not per transaction. After a repeated START the count restarts, because the phase is what the direction bit applies to. A write-pointer-then-read sequence is one bus occupancy and two phases with different directions, and a count that spanned both would be meaningless.
6. The Monitor
// -----------------------------------------------------------------------------
// i2c_mon.sv
// A PASSIVE I²C monitor: resolved bus in, reconstructed transaction out.
//
// WHAT MAKES THIS A MONITOR AND NOT A SECOND DUT:
//
// 1. IT DRIVES NOTHING. There is no output that reaches the bus, no `drive_low`,
// no output enable. It has two inputs from the wire and everything else is an
// observation. A component that can affect what it measures is not a monitor.
//
// 2. IT READS THE RESOLVED BUS, not any participant's intent. Chapter 19.1 showed
// that on an open-drain bus a device's own drive intent and the line's actual
// level legitimately disagree -- a releasing device reads LOW while somebody
// else holds the line. A monitor built on intent reports a clean bus at the
// exact moment the bus is contended.
//
// 3. IT SHARES NO LOGIC WITH THE DUT. Module 18.3's framing detector and this
// block both implement "SDA edge while SCL is high", and they are SEPARATE
// IMPLEMENTATIONS on purpose. Reusing 18.3 here would make one defect appear
// identically in the design and in the checker, and the comparison would pass.
// Sharing a protocol DEFINITION is correct; sharing an implementation is
// common-mode failure. This module therefore samples levels rather than
// consuming one-cycle edge pulses, and counts bits with its own counter.
//
// WHAT IT RECONSTRUCTS, in the order the pipeline builds it:
//
// resolved SDA/SCL
// -> START / repeated START / STOP (SDA edge while SCL high)
// -> bit sampling (SDA at the SCL rising edge)
// -> byte + ninth-bit acknowledge (8 + 1)
// -> address byte -> 7-bit address, direction
// -> data bytes, in order
// -> transaction boundary (START .. STOP or repeated START)
//
// WHAT IT DELIBERATELY DOES NOT DO: decide whether the traffic was CORRECT. It
// reports what happened. Chapter 20.8's scoreboard decides whether that was right,
// using an independently produced expectation. A monitor that also judged would
// collapse observation and prediction into one component, and a single wrong
// assumption would then be invisible.
//
// BOUNDED BY CONSTRUCTION: every state transition is driven by a sampled level
// change, and the byte counter saturates. There is no wait, so there is nothing to
// hang -- a dead bus simply produces no events, which is itself an observation.
// -----------------------------------------------------------------------------
module i2c_mon #(
// Bytes of one transaction the monitor will retain. A real environment streams
// these to an analysis consumer; retaining a window keeps this module testable
// without a queue implementation, which Module 21 owns.
parameter int MAX_BYTES = 8
) (
input logic clk,
input logic rst_n,
// ---- the only inputs: the RESOLVED bus -----------------------------------
// Not a participant's drive intent. Not a DUT internal signal.
input logic scl,
input logic sda,
// ---- framing events, as one-cycle pulses ---------------------------------
output logic saw_start, // a START of either kind
output logic saw_restart, // ... and a transfer was already open
output logic saw_stop,
// ---- byte-level reconstruction -------------------------------------------
// One cycle, at the NINTH SCL RISING EDGE -- the instant the acknowledge bit is
// guaranteed stable. The byte and its acknowledge are reported together because
// a byte without its acknowledge is not yet a protocol fact.
output logic byte_valid,
output logic [7:0] byte_data,
output logic byte_acked, // 1 = the ninth bit was LOW (acknowledged)
output logic byte_is_addr, // this byte was the address byte
// ---- transaction-level reconstruction ------------------------------------
output logic txn_active,
output logic [6:0] txn_addr,
output logic txn_read, // direction latched from the address byte
output logic txn_addr_acked,
output logic [3:0] txn_n_data, // data bytes seen in this phase, saturating
// One cycle, when a transaction ends at a STOP or a repeated START.
output logic txn_done,
output logic txn_ended_by_restart,
// ---- counters, for a bench to assert against ------------------------------
output logic [15:0] n_txns,
output logic [15:0] n_bytes,
output logic [15:0] n_nacks
);
// ---- the monitor's own view of the two lines -----------------------------
// Its OWN registers, not Module 18's. One delay stage is all a monitor needs to
// see an edge in an already-synchronous simulation; a real environment sampling
// asynchronous pins would use Chapter 19.4's synchroniser first, and Section 6
// of the chapter says what that costs a monitor's timestamps.
logic scl_d, sda_d;
wire scl_rise = scl & ~scl_d;
wire scl_fall = ~scl & scl_d;
wire sda_fall = ~sda & sda_d;
wire sda_rise = sda & ~sda_d;
// Framing is an SDA edge while SCL is HIGH -- the same PROTOCOL DEFINITION as
// Chapter 18.3, reached by a different implementation.
wire start_now = sda_fall & scl & scl_d;
wire stop_now = sda_rise & scl & scl_d;
logic [3:0] bitcnt;
logic [7:0] shreg;
logic in_byte; // a byte is being assembled
logic addr_seen; // the address byte of this phase has completed
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
scl_d <= 1'b1; sda_d <= 1'b1;
saw_start <= 1'b0; saw_restart <= 1'b0; saw_stop <= 1'b0;
byte_valid <= 1'b0; byte_data <= 8'h00; byte_acked <= 1'b0;
byte_is_addr <= 1'b0;
txn_active <= 1'b0; txn_addr <= 7'h00; txn_read <= 1'b0;
txn_addr_acked <= 1'b0; txn_n_data <= 4'd0;
txn_done <= 1'b0; txn_ended_by_restart <= 1'b0;
bitcnt <= 4'd0; shreg <= 8'h00; in_byte <= 1'b0; addr_seen <= 1'b0;
n_txns <= 16'd0; n_bytes <= 16'd0; n_nacks <= 16'd0;
end else begin
scl_d <= scl; sda_d <= sda;
saw_start <= 1'b0; saw_restart <= 1'b0; saw_stop <= 1'b0;
byte_valid <= 1'b0; txn_done <= 1'b0; txn_ended_by_restart <= 1'b0;
// ---------------- framing ----------------------------------------
if (start_now) begin
saw_start <= 1'b1;
if (txn_active) begin
// A repeated START ends the previous phase and opens a new one. The
// PREVIOUS transaction is reported now, which is why `txn_done` and
// `saw_restart` can be asserted in the same cycle.
saw_restart <= 1'b1;
txn_done <= 1'b1;
txn_ended_by_restart <= 1'b1;
n_txns <= n_txns + 16'd1;
end
txn_active <= 1'b1;
addr_seen <= 1'b0;
txn_n_data <= 4'd0;
in_byte <= 1'b0;
bitcnt <= 4'd0;
shreg <= 8'h00;
end else if (stop_now) begin
saw_stop <= 1'b1;
if (txn_active) begin
txn_done <= 1'b1;
n_txns <= n_txns + 16'd1;
end
txn_active <= 1'b0;
addr_seen <= 1'b0;
in_byte <= 1'b0;
bitcnt <= 4'd0;
// ---------------- bit sampling ------------------------------------
// SDA is sampled at the SCL RISING edge, because that is the only instant
// the specification guarantees it is stable. Sampling on the falling edge
// reads a value the transmitter is entitled to be changing -- mutation
// M03 does exactly that.
end else if (scl_rise && txn_active) begin
if (bitcnt < 4'd8) begin
shreg <= {shreg[6:0], sda};
bitcnt <= bitcnt + 4'd1;
in_byte <= 1'b1;
end else begin
// The ninth bit: the acknowledge. LOW means acknowledged, and it is
// whoever is RECEIVING that drives it -- the monitor does not care
// which device, only what the bus said.
byte_valid <= 1'b1;
byte_data <= shreg;
byte_acked <= ~sda;
byte_is_addr <= ~addr_seen;
n_bytes <= n_bytes + 16'd1;
if (sda) n_nacks <= n_nacks + 16'd1;
if (!addr_seen) begin
// First byte of the phase is the address: seven bits then R/W.
txn_addr <= shreg[7:1];
txn_read <= shreg[0];
txn_addr_acked <= ~sda;
addr_seen <= 1'b1;
end else if (txn_n_data != 4'd15) begin
txn_n_data <= txn_n_data + 4'd1;
end
bitcnt <= 4'd0;
shreg <= 8'h00;
in_byte <= 1'b0;
end
end
end
end
endmoduleOne implementation note, because it looks like a shortcut and is not. The monitor keeps its own one-stage delay of SCL and SDA — scl_d and sda_d — rather than using anything from the design. One stage is all an edge detector needs in an already-synchronous simulation. A real environment sampling a physical bus would need the synchronisation of 19.4, and it is worth being explicit that the monitor here does not model metastability, because nothing in RTL simulation can.
7. The Bench: Traces Known by Construction
The monitor is verified against traces a bench drives directly onto the lines, with no design present. That is the only arrangement in which the expected reconstruction is known independently of anything being tested — the bench built the trace, so it knows what the answer is.
// -----------------------------------------------------------------------------
// i2c_mon_tb.sv
// The monitor is verification code, so it gets verified.
//
// THE BENCH DRIVES THE BUS DIRECTLY, not through any DUT. That is deliberate: the
// monitor's contract is "given this waveform, report this transaction", and the only
// way to test it is to produce waveforms whose correct interpretation is known by
// construction. Attaching a DUT here would mean a monitor failure and a DUT failure
// were indistinguishable -- Chapter 20.9 attaches the real DUT, once the monitor is
// trustworthy.
//
// THE TRACES INCLUDE MALFORMED ONES. A monitor exercised only on legal traffic has
// been shown to decode, not to discriminate. T7 and T8 feed it a truncated byte and
// an SDA change while SCL is high -- the two things the protocol forbids -- and
// require the reconstruction to differ from the legal case.
//
// Every wait is a fixed number of clocks; nothing waits on the monitor.
// -----------------------------------------------------------------------------
`timescale 1ns/1ps
module i2c_mon_tb;
localparam int HALF = 6; // clocks per SCL half-phase
logic clk = 1'b0, rst_n = 1'b0;
// The bench owns the bus outright here: two drive-intent signals resolved by a
// wired-AND, which is what Chapter 19.1's pad law reduces to for one participant.
// LOW or RELEASE only -- the bench never drives HIGH.
logic scl_low = 1'b0, sda_low = 1'b0;
wire scl = ~scl_low;
wire sda = ~sda_low;
logic saw_start, saw_restart, saw_stop;
logic byte_valid, byte_acked, byte_is_addr;
logic [7:0] byte_data;
logic txn_active, txn_read, txn_addr_acked, txn_done, txn_ended_by_restart;
logic [6:0] txn_addr;
logic [3:0] txn_n_data;
logic [15:0] n_txns, n_bytes, n_nacks;
integer errors = 0;
integer n, k;
i2c_mon #(.MAX_BYTES(8)) dut (
.clk(clk), .rst_n(rst_n), .scl(scl), .sda(sda),
.saw_start(saw_start), .saw_restart(saw_restart), .saw_stop(saw_stop),
.byte_valid(byte_valid), .byte_data(byte_data), .byte_acked(byte_acked),
.byte_is_addr(byte_is_addr),
.txn_active(txn_active), .txn_addr(txn_addr), .txn_read(txn_read),
.txn_addr_acked(txn_addr_acked), .txn_n_data(txn_n_data),
.txn_done(txn_done), .txn_ended_by_restart(txn_ended_by_restart),
.n_txns(n_txns), .n_bytes(n_bytes), .n_nacks(n_nacks));
// ---- an observer, so one-cycle reports can be checked after the fact --------
// The monitor emits `byte_valid` for exactly one cycle. A bench that only looked
// at it between transfers would never see it, so the reports are captured here --
// which is itself the pattern Chapter 20.4 calls an analysis consumer.
integer nb = 0;
logic [7:0] cap_data [0:15];
logic cap_ack [0:15];
logic cap_isaddr[0:15];
integer ntd = 0, nrs = 0;
always @(posedge clk) begin
if (rst_n) begin
if (byte_valid && nb < 16) begin
cap_data[nb] <= byte_data;
cap_ack[nb] <= byte_acked;
cap_isaddr[nb] <= byte_is_addr;
nb <= nb + 1;
end
if (txn_done) ntd <= ntd + 1;
if (saw_restart) nrs <= nrs + 1;
end
end
always #5 clk = ~clk;
task step; begin @(posedge clk); @(negedge clk); end endtask
task phase; begin for (n = 0; n < HALF; n = n + 1) step; end endtask
task do_reset;
begin
@(negedge clk); rst_n = 1'b0; scl_low = 1'b0; sda_low = 1'b0;
step; step;
@(negedge clk); rst_n = 1'b1;
phase;
nb = 0; ntd = 0; nrs = 0;
end
endtask
// ---- a controller, driving pins by hand ----------------------------------
// SDA changes only while SCL is LOW for data, and only while SCL is HIGH for
// framing. That distinction IS the protocol, and a bench that blurred it could
// not test a monitor that depends on it.
task t_start;
begin
@(negedge clk); sda_low = 1'b0; scl_low = 1'b0; phase; // both released
@(negedge clk); sda_low = 1'b1; phase; // SDA falls, SCL high
@(negedge clk); scl_low = 1'b1; phase;
end
endtask
task t_restart;
begin
@(negedge clk); scl_low = 1'b1; sda_low = 1'b0; phase;
@(negedge clk); scl_low = 1'b0; phase; // SCL high, SDA high
@(negedge clk); sda_low = 1'b1; phase; // SDA falls
@(negedge clk); scl_low = 1'b1; phase;
end
endtask
task t_stop;
begin
@(negedge clk); scl_low = 1'b1; sda_low = 1'b1; phase;
@(negedge clk); scl_low = 1'b0; phase; // SCL high, SDA low
@(negedge clk); sda_low = 1'b0; phase; // SDA rises
end
endtask
task t_bit (input b);
begin
@(negedge clk); scl_low = 1'b1; phase;
@(negedge clk); sda_low = ~b; phase;
@(negedge clk); scl_low = 1'b0; phase;
@(negedge clk); scl_low = 1'b1; phase;
end
endtask
// Eight bits MSB first, then a ninth slot in which the bench drives the
// acknowledge itself -- because here the bench is playing both participants.
task t_byte (input [7:0] d, input ack);
begin
for (k = 7; k >= 0; k = k - 1) t_bit(d[k]);
t_bit(~ack); // ack=1 -> drive LOW -> the ninth bit is 0
end
endtask
task ck (input [200*8:1] what, input integer g, input integer e);
begin
if (g !== e) begin
$display(" FAIL %0s: got %0d expected %0d", what, g, e);
errors = errors + 1;
end
end
endtask
task ck_idx (input [200*8:1] what, input integer idx,
input integer g, input integer e);
begin
if (g !== e) begin
$display(" FAIL %0s[%0d]: got %0d expected %0d", what, idx, g, e);
errors = errors + 1;
end
end
endtask
initial begin
$display("=== i2c_mon: a monitor is verification code, so it gets verified ===");
// ----------------------------------------------------------------
// T1. RESET REPORTS NOTHING. A monitor that came out of reset claiming a
// transaction was open would inject a phantom item into every downstream
// consumer, and the scoreboard would compare against traffic that never
// existed.
// ----------------------------------------------------------------
do_reset;
$display("T1 out of reset the monitor reports no transaction and no events");
ck("T1 no transaction open", txn_active, 0);
ck("T1 no transactions counted", n_txns, 0);
ck("T1 no bytes counted", n_bytes, 0);
ck("T1 no framing event", saw_start | saw_stop | saw_restart, 0);
// ----------------------------------------------------------------
// T2. A COMPLETE WRITE TRANSACTION IS RECONSTRUCTED. Address 0x50 write, two
// data bytes, all acknowledged, STOP. Everything the monitor claims is
// checked: the address, the direction, the byte VALUES, the byte ORDER,
// which byte was the address, and the acknowledge of each.
// ----------------------------------------------------------------
t_start;
ck("T2 a START opens a transaction", txn_active, 1);
t_byte(8'hA0, 1'b1); // 0x50 << 1 | write
t_byte(8'h11, 1'b1);
t_byte(8'h22, 1'b1);
t_stop;
$display("T2 a write transaction is reconstructed from the wire alone");
ck("T2 three bytes seen", nb, 3);
ck("T2 the address is 0x50", txn_addr, 7'h50);
ck("T2 the direction is write", txn_read, 0);
ck("T2 the address was acked", txn_addr_acked, 1);
ck("T2 first byte is the address", cap_isaddr[0], 1);
ck("T2 second byte is not", cap_isaddr[1], 0);
ck("T2 byte 0 value", cap_data[0], 8'hA0);
ck("T2 byte 1 value", cap_data[1], 8'h11);
ck("T2 byte 2 value", cap_data[2], 8'h22);
ck("T2 every byte acked", cap_ack[0] & cap_ack[1] & cap_ack[2], 1);
ck("T2 one transaction completed", ntd, 1);
ck("T2 no NACK was seen", n_nacks, 0);
ck("T2 and it is closed now", txn_active, 0);
// ----------------------------------------------------------------
// T3. DIRECTION IS DECODED FROM THE ADDRESS BYTE'S LOW BIT. A monitor that
// ignored it would report every read as a write and the scoreboard would
// then predict the wrong side of the transfer entirely.
// ----------------------------------------------------------------
do_reset;
t_start;
t_byte(8'hA1, 1'b1); // 0x50 << 1 | READ
t_byte(8'h5A, 1'b1); // the target's data, on the wire
t_stop;
$display("T3 the direction bit is decoded, so a read is not reported as a write");
ck("T3 the address is still 0x50", txn_addr, 7'h50);
ck("T3 the direction is READ", txn_read, 1);
ck("T3 the data byte was captured", cap_data[1], 8'h5A);
// ----------------------------------------------------------------
// T4. A NACK IS OBSERVED, AND IT IS A BUS FACT, NOT AN OPINION. The monitor
// reports the ninth bit it saw. Whether a NACK was CORRECT is Chapter
// 20.8's question -- a monitor that decided would be judging, and a
// judging monitor hides its own wrong assumptions.
// ----------------------------------------------------------------
do_reset;
t_start;
t_byte(8'hA0, 1'b1); // address, acked
t_byte(8'h33, 1'b0); // data, NACKed
t_stop;
$display("T4 a NACK on the wire is reported as observed, not judged");
ck("T4 the address was acked", cap_ack[0], 1);
ck("T4 the data byte was NACKed", cap_ack[1], 0);
ck("T4 one NACK counted", n_nacks, 1);
ck("T4 the byte value survived the NACK", cap_data[1], 8'h33);
// ----------------------------------------------------------------
// T5. A REPEATED START ENDS ONE TRANSACTION AND OPENS ANOTHER. This is the
// boundary a monitor most often gets wrong: the phase before the repeated
// START is a complete transaction and must be reported, and the address
// that follows belongs to a NEW one. A monitor that treated a repeated
// START as ordinary framing would merge a write and a read into one item,
// and no scoreboard could untangle them afterwards.
// ----------------------------------------------------------------
do_reset;
t_start;
t_byte(8'hA0, 1'b1); // phase 1: write 0x50
t_byte(8'h07, 1'b1); // a pointer byte
t_restart;
t_byte(8'hA1, 1'b1); // phase 2: read 0x50
t_byte(8'h99, 1'b1);
t_stop;
$display("T5 a repeated START closes one transaction and opens the next");
ck("T5 one repeated START seen", nrs, 1);
ck("T5 TWO transactions completed", ntd, 2);
ck("T5 the second phase is a read", txn_read, 1);
ck("T5 four bytes in total", nb, 4);
ck("T5 the second address byte is flagged as an address", cap_isaddr[2], 1);
ck("T5 and the byte after it is not", cap_isaddr[3], 0);
// ----------------------------------------------------------------
// T6. THE SECOND PHASE'S DATA COUNT RESTARTS. `txn_n_data` is per phase, not
// per bus session. A monitor that accumulated across a repeated START
// would report a two-byte read as a four-byte one.
// ----------------------------------------------------------------
ck("T6 the read phase carried one data byte", txn_n_data, 1);
// ----------------------------------------------------------------
// T7. A TRUNCATED BYTE IS NOT REPORTED AS A BYTE.
//
// The negative test. Five bits are clocked and then a STOP arrives. A
// monitor that reported a byte here would hand the scoreboard a value
// assembled from bits the transmitter never finished sending -- and the
// scoreboard would compare it, fail, and blame the DUT.
//
// A monitor exercised only on legal traffic cannot distinguish "decodes
// correctly" from "decodes anything it is given".
// ----------------------------------------------------------------
do_reset;
t_start;
t_byte(8'hA0, 1'b1); // a good address byte first
for (k = 0; k < 5; k = k + 1) t_bit(1'b1); // five bits, then nothing
t_stop;
$display("T7 a truncated byte is discarded, not reported as data");
ck("T7 only the address byte was reported", nb, 1);
ck("T7 and it was the address", cap_isaddr[0], 1);
ck("T7 the transaction still ended", ntd, 1);
ck("T7 no phantom data byte", txn_n_data, 0);
// ----------------------------------------------------------------
// T8. AN SDA CHANGE WHILE SCL IS HIGH IS FRAMING, NOT DATA.
//
// The second negative test, and the sharper one. Mid-byte, SDA is changed
// while SCL is HIGH -- which the specification forbids for data and
// defines as framing. The monitor must treat it as a START and abandon the
// partial byte, because that is what every conforming device on the bus
// will do. A monitor that quietly sampled it as a data bit would
// reconstruct a byte no device ever received.
// ----------------------------------------------------------------
do_reset;
t_start;
t_byte(8'hA0, 1'b1);
t_bit(1'b1); t_bit(1'b1); // two bits of a data byte
// now the illegal change: SDA falls while SCL is HIGH
@(negedge clk); scl_low = 1'b1; phase;
@(negedge clk); sda_low = 1'b0; phase; // SDA released
@(negedge clk); scl_low = 1'b0; phase; // SCL HIGH
@(negedge clk); sda_low = 1'b1; phase; // SDA falls: a START
@(negedge clk); scl_low = 1'b1; phase;
$display("T8 an SDA edge while SCL is high is framing: the partial byte is dropped");
ck("T8 it was reported as a START", nrs, 1);
ck("T8 the first phase was closed", ntd, 1);
ck("T8 only the address byte was ever reported", nb, 1);
ck("T8 a transaction is open again", txn_active, 1);
t_stop;
// ----------------------------------------------------------------
// T9. THE MONITOR NEVER DRIVES. Its port list has no output that reaches the
// bus, so this is structural rather than dynamic -- but the bench asserts
// it anyway, because the property is the reason the component is trusted.
// With the bench released, both lines must read HIGH: if the monitor were
// driving anything, they would not.
// ----------------------------------------------------------------
do_reset;
@(negedge clk); scl_low = 1'b0; sda_low = 1'b0;
for (n = 0; n < 20; n = n + 1) step;
$display("T9 with the bench released, the monitor is holding nothing");
ck("T9 SCL reads high", scl, 1);
ck("T9 SDA reads high", sda, 1);
if (errors == 0) $display("=== i2c_mon: ALL CHECKS PASSED ===");
else $display("=== i2c_mon: %0d CHECK(S) FAILED ===", errors);
$finish;
end
endmoduleWhy byte_valid fires at the ninth edge and not the eighth
10 cycles8. The VHDL Twin, and What Three Languages Actually Prove
The monitor exists in SystemVerilog and in VHDL, with the same nine traces and the same expected values.
Both files are published so that the claim is reproducible rather than merely described. The VHDL bench drives the same nine traces and asserts the same expected values; what differs is entirely language mechanics.
-- -----------------------------------------------------------------------------
-- i2c_mon.vhd
-- The passive monitor, in VHDL.
--
-- WHY THIS FILE EXISTS AT ALL, since Chapter 20.4 argues the architecture is
-- language-neutral: it is a SIMULATOR constraint, not an architectural one. Icarus
-- compiles a SystemVerilog environment against the SystemVerilog and the
-- Verilog-2001 DUT, so one environment already covers two implementations. The VHDL
-- DUT needs an analyser that reads VHDL, and NVC will not read the SystemVerilog
-- environment. So the environment is expressed twice, not three times, and the
-- duplication tracks the toolchain rather than the design.
--
-- WHAT MUST BE IDENTICAL is the verification INTENT: the same reconstruction rules,
-- the same reported fields, the same expected values, the same verdict. Chapter 20.4
-- states the contract; these two files are two encodings of it. If they disagreed
-- about what the bus meant, one of them would be wrong -- which is why the two
-- benches assert the same numbers and are compared in Chapter 20.7's parity table.
--
-- EVERYTHING ELSE IS AS THE SYSTEMVERILOG VERSION: passive, resolved-bus only, its
-- own framing implementation rather than Module 18.3's, and sampling SDA at the SCL
-- rising edge because that is the only instant the specification guarantees it stable.
-- -----------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_mon is
generic (
MAX_BYTES : positive := 8
);
port (
clk : in std_logic;
rst_n : in std_logic;
-- the only inputs: the RESOLVED bus
scl : in std_logic;
sda : in std_logic;
saw_start : out std_logic;
saw_restart : out std_logic;
saw_stop : out std_logic;
byte_valid : out std_logic;
byte_data : out std_logic_vector(7 downto 0);
byte_acked : out std_logic;
byte_is_addr : out std_logic;
txn_active : out std_logic;
txn_addr : out std_logic_vector(6 downto 0);
txn_read : out std_logic;
txn_addr_acked : out std_logic;
txn_n_data : out unsigned(3 downto 0);
txn_done : out std_logic;
txn_ended_by_restart : out std_logic;
n_txns : out unsigned(15 downto 0);
n_bytes : out unsigned(15 downto 0);
n_nacks : out unsigned(15 downto 0)
);
end entity i2c_mon;
architecture obs of i2c_mon is
signal scl_d, sda_d : std_logic := '1';
signal bitcnt : unsigned(3 downto 0) := (others => '0');
signal shreg : std_logic_vector(7 downto 0) := (others => '0');
signal addr_seen : std_logic := '0';
signal active_i : std_logic := '0';
begin
txn_active <= active_i;
process (clk, rst_n)
variable start_now, stop_now, scl_rise : boolean;
begin
if rst_n = '0' then
scl_d <= '1'; sda_d <= '1';
saw_start <= '0'; saw_restart <= '0'; saw_stop <= '0';
byte_valid <= '0'; byte_data <= (others => '0');
byte_acked <= '0'; byte_is_addr <= '0';
active_i <= '0'; txn_addr <= (others => '0'); txn_read <= '0';
txn_addr_acked <= '0'; txn_n_data <= (others => '0');
txn_done <= '0'; txn_ended_by_restart <= '0';
bitcnt <= (others => '0'); shreg <= (others => '0'); addr_seen <= '0';
n_txns <= (others => '0'); n_bytes <= (others => '0');
n_nacks <= (others => '0');
elsif rising_edge(clk) then
-- framing is an SDA edge while SCL is HIGH: the same PROTOCOL DEFINITION as
-- Chapter 18.3, a different implementation
start_now := (sda = '0' and sda_d = '1') and scl = '1' and scl_d = '1';
stop_now := (sda = '1' and sda_d = '0') and scl = '1' and scl_d = '1';
scl_rise := (scl = '1' and scl_d = '0');
scl_d <= scl; sda_d <= sda;
saw_start <= '0'; saw_restart <= '0'; saw_stop <= '0';
byte_valid <= '0'; txn_done <= '0'; txn_ended_by_restart <= '0';
if start_now then
saw_start <= '1';
if active_i = '1' then
saw_restart <= '1';
txn_done <= '1';
txn_ended_by_restart <= '1';
n_txns <= n_txns + 1;
end if;
active_i <= '1';
addr_seen <= '0';
txn_n_data <= (others => '0');
bitcnt <= (others => '0');
shreg <= (others => '0');
elsif stop_now then
saw_stop <= '1';
if active_i = '1' then
txn_done <= '1';
n_txns <= n_txns + 1;
end if;
active_i <= '0';
addr_seen <= '0';
bitcnt <= (others => '0');
elsif scl_rise and active_i = '1' then
if bitcnt < 8 then
shreg <= shreg(6 downto 0) & sda;
bitcnt <= bitcnt + 1;
else
byte_valid <= '1';
byte_data <= shreg;
if sda = '0' then byte_acked <= '1'; else byte_acked <= '0'; end if;
if addr_seen = '0' then byte_is_addr <= '1';
else byte_is_addr <= '0'; end if;
n_bytes <= n_bytes + 1;
if sda = '1' then n_nacks <= n_nacks + 1; end if;
if addr_seen = '0' then
txn_addr <= shreg(7 downto 1);
txn_read <= shreg(0);
if sda = '0' then txn_addr_acked <= '1';
else txn_addr_acked <= '0'; end if;
addr_seen <= '1';
elsif txn_n_data /= 15 then
txn_n_data <= txn_n_data + 1;
end if;
bitcnt <= (others => '0');
shreg <= (others => '0');
end if;
end if;
end if;
end process;
end architecture obs; -- -----------------------------------------------------------------------------
-- i2c_mon_tb.vhd
-- The monitor's self-test, in VHDL: the same nine traces, the same expected values.
--
-- THE SAME VERIFICATION INTENT AS THE SYSTEMVERILOG BENCH, deliberately. Same trace
-- set (legal write, read, NACK, repeated START, truncated byte, illegal SDA change),
-- same expected byte values, same expected counts. Chapter 20.7's parity table
-- compares the two, because two encodings of one contract that disagreed would mean
-- one of them is wrong -- and a monitor is the component whose correctness everything
-- downstream rests on.
--
-- The bench owns the bus outright and drives LOW or RELEASE only; it never drives HIGH.
-- -----------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_mon_tb is
end entity i2c_mon_tb;
architecture sim of i2c_mon_tb is
constant HALFP : integer := 6;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal scl_low, sda_low : std_logic := '0';
signal scl, sda : std_logic;
signal saw_start, saw_restart, saw_stop : std_logic;
signal byte_valid, byte_acked, byte_is_addr : std_logic;
signal byte_data : std_logic_vector(7 downto 0);
signal txn_active, txn_read, txn_addr_acked, txn_done, txn_rs : std_logic;
signal txn_addr : std_logic_vector(6 downto 0);
signal txn_n_data : unsigned(3 downto 0);
signal n_txns, n_bytes, n_nacks : unsigned(15 downto 0);
-- capture: the monitor reports for one cycle, so the reports are collected here
type byte_arr is array (0 to 15) of std_logic_vector(7 downto 0);
type bit_arr is array (0 to 15) of std_logic;
signal cap_data : byte_arr;
signal cap_ack : bit_arr;
signal cap_isaddr : bit_arr;
signal nb, ntd, nrs : integer := 0;
signal clr_obs : boolean := false;
signal halt : boolean := false;
begin
-- LOW or RELEASE only: the wired-AND of one participant
scl <= not scl_low;
sda <= not sda_low;
clkgen : process
begin
while not halt loop
clk <= '0'; wait for 5 ns;
clk <= '1'; wait for 5 ns;
end loop;
wait;
end process;
dut : entity work.i2c_mon
generic map (MAX_BYTES => 8)
port map (clk => clk, rst_n => rst_n, scl => scl, sda => sda,
saw_start => saw_start, saw_restart => saw_restart,
saw_stop => saw_stop,
byte_valid => byte_valid, byte_data => byte_data,
byte_acked => byte_acked, byte_is_addr => byte_is_addr,
txn_active => txn_active, txn_addr => txn_addr,
txn_read => txn_read, txn_addr_acked => txn_addr_acked,
txn_n_data => txn_n_data, txn_done => txn_done,
txn_ended_by_restart => txn_rs,
n_txns => n_txns, n_bytes => n_bytes, n_nacks => n_nacks);
-- One driver per signal: the clear arrives as a REQUEST, not as a second driver.
-- `std_logic`/integer signals driven from two processes resolve silently or error,
-- and Module 19 lost a run to exactly that.
obs : process (clk, clr_obs)
begin
if clr_obs then
nb <= 0; ntd <= 0; nrs <= 0;
elsif rising_edge(clk) then
if rst_n = '1' then
if byte_valid = '1' and nb < 16 then
cap_data(nb) <= byte_data;
cap_ack(nb) <= byte_acked;
cap_isaddr(nb) <= byte_is_addr;
nb <= nb + 1;
end if;
if txn_done = '1' then ntd <= ntd + 1; end if;
if saw_restart = '1' then nrs <= nrs + 1; end if;
end if;
end if;
end process;
stim : process
variable err : integer := 0;
function b2i (b : std_logic) return integer is
begin
if b = '1' then return 1; else return 0; end if;
end function;
procedure step is
begin
wait until rising_edge(clk);
wait until falling_edge(clk);
end procedure;
procedure phase is
begin
for i in 1 to HALFP loop step; end loop;
end procedure;
procedure clear_obs is
begin
clr_obs <= true; wait for 1 ns;
clr_obs <= false; wait for 1 ns;
end procedure;
procedure do_reset is
begin
wait until falling_edge(clk);
rst_n <= '0'; scl_low <= '0'; sda_low <= '0';
step; step;
wait until falling_edge(clk);
rst_n <= '1';
phase;
clear_obs;
end procedure;
procedure t_start is
begin
wait until falling_edge(clk); sda_low <= '0'; scl_low <= '0'; phase;
wait until falling_edge(clk); sda_low <= '1'; phase;
wait until falling_edge(clk); scl_low <= '1'; phase;
end procedure;
procedure t_restart is
begin
wait until falling_edge(clk); scl_low <= '1'; sda_low <= '0'; phase;
wait until falling_edge(clk); scl_low <= '0'; phase;
wait until falling_edge(clk); sda_low <= '1'; phase;
wait until falling_edge(clk); scl_low <= '1'; phase;
end procedure;
procedure t_stop is
begin
wait until falling_edge(clk); scl_low <= '1'; sda_low <= '1'; phase;
wait until falling_edge(clk); scl_low <= '0'; phase;
wait until falling_edge(clk); sda_low <= '0'; phase;
end procedure;
procedure t_bit (b : std_logic) is
begin
wait until falling_edge(clk); scl_low <= '1'; phase;
wait until falling_edge(clk); sda_low <= not b; phase;
wait until falling_edge(clk); scl_low <= '0'; phase;
wait until falling_edge(clk); scl_low <= '1'; phase;
end procedure;
procedure t_byte (d : std_logic_vector(7 downto 0); ack : std_logic) is
begin
for k in 7 downto 0 loop t_bit(d(k)); end loop;
t_bit(not ack);
end procedure;
procedure ck (what : string; g : integer; e : integer) is
begin
if g /= e then
report " FAIL " & what & ": got " & integer'image(g)
& " expected " & integer'image(e) severity note;
err := err + 1;
end if;
end procedure;
begin
report "=== i2c_mon: a monitor is verification code, so it gets verified ==="
severity note;
-- T1. Reset reports nothing. A monitor claiming an open transaction out of
-- reset would inject a phantom item into every downstream consumer.
do_reset;
report "T1 out of reset the monitor reports no transaction and no events"
severity note;
ck("T1 no transaction open", b2i(txn_active), 0);
ck("T1 no transactions counted", to_integer(n_txns), 0);
ck("T1 no bytes counted", to_integer(n_bytes), 0);
ck("T1 no framing event",
b2i(saw_start) + b2i(saw_stop) + b2i(saw_restart), 0);
-- T2. A complete write transaction is reconstructed: address, direction, byte
-- values, byte ORDER, which byte was the address, and each acknowledge.
t_start;
ck("T2 a START opens a transaction", b2i(txn_active), 1);
t_byte(x"A0", '1');
t_byte(x"11", '1');
t_byte(x"22", '1');
t_stop;
report "T2 a write transaction is reconstructed from the wire alone" severity note;
ck("T2 three bytes seen", nb, 3);
ck("T2 the address is 0x50", to_integer(unsigned(txn_addr)), 16#50#);
ck("T2 the direction is write", b2i(txn_read), 0);
ck("T2 the address was acked", b2i(txn_addr_acked), 1);
ck("T2 first byte is the address", b2i(cap_isaddr(0)), 1);
ck("T2 second byte is not", b2i(cap_isaddr(1)), 0);
ck("T2 byte 0 value", to_integer(unsigned(cap_data(0))), 16#A0#);
ck("T2 byte 1 value", to_integer(unsigned(cap_data(1))), 16#11#);
ck("T2 byte 2 value", to_integer(unsigned(cap_data(2))), 16#22#);
ck("T2 every byte acked",
b2i(cap_ack(0)) + b2i(cap_ack(1)) + b2i(cap_ack(2)), 3);
ck("T2 one transaction completed", ntd, 1);
ck("T2 no NACK was seen", to_integer(n_nacks), 0);
ck("T2 and it is closed now", b2i(txn_active), 0);
-- T3. Direction is decoded from the address byte's low bit.
do_reset;
t_start;
t_byte(x"A1", '1');
t_byte(x"5A", '1');
t_stop;
report "T3 the direction bit is decoded, so a read is not reported as a write"
severity note;
ck("T3 the address is still 0x50", to_integer(unsigned(txn_addr)), 16#50#);
ck("T3 the direction is READ", b2i(txn_read), 1);
ck("T3 the data byte was captured", to_integer(unsigned(cap_data(1))), 16#5A#);
-- T4. A NACK is observed as a bus fact, not judged.
do_reset;
t_start;
t_byte(x"A0", '1');
t_byte(x"33", '0');
t_stop;
report "T4 a NACK on the wire is reported as observed, not judged" severity note;
ck("T4 the address was acked", b2i(cap_ack(0)), 1);
ck("T4 the data byte was NACKed", b2i(cap_ack(1)), 0);
ck("T4 one NACK counted", to_integer(n_nacks), 1);
ck("T4 the byte value survived the NACK",
to_integer(unsigned(cap_data(1))), 16#33#);
-- T5. A repeated START closes one transaction and opens the next.
do_reset;
t_start;
t_byte(x"A0", '1');
t_byte(x"07", '1');
t_restart;
t_byte(x"A1", '1');
t_byte(x"99", '1');
t_stop;
report "T5 a repeated START closes one transaction and opens the next"
severity note;
ck("T5 one repeated START seen", nrs, 1);
ck("T5 TWO transactions completed", ntd, 2);
ck("T5 the second phase is a read", b2i(txn_read), 1);
ck("T5 four bytes in total", nb, 4);
ck("T5 the second address byte is flagged as an address",
b2i(cap_isaddr(2)), 1);
ck("T5 and the byte after it is not", b2i(cap_isaddr(3)), 0);
-- T6. The second phase's data count restarts.
ck("T6 the read phase carried one data byte", to_integer(txn_n_data), 1);
-- T7. A truncated byte is not reported as a byte. The negative test: five bits
-- then a STOP. A monitor that reported a byte here would hand the scoreboard
-- a value assembled from bits never finished.
do_reset;
t_start;
t_byte(x"A0", '1');
for k in 1 to 5 loop t_bit('1'); end loop;
t_stop;
report "T7 a truncated byte is discarded, not reported as data" severity note;
ck("T7 only the address byte was reported", nb, 1);
ck("T7 and it was the address", b2i(cap_isaddr(0)), 1);
ck("T7 the transaction still ended", ntd, 1);
ck("T7 no phantom data byte", to_integer(txn_n_data), 0);
-- T8. An SDA change while SCL is HIGH is framing, not data: the partial byte is
-- abandoned, because that is what every conforming device will do.
do_reset;
t_start;
t_byte(x"A0", '1');
t_bit('1'); t_bit('1');
wait until falling_edge(clk); scl_low <= '1'; phase;
wait until falling_edge(clk); sda_low <= '0'; phase;
wait until falling_edge(clk); scl_low <= '0'; phase;
wait until falling_edge(clk); sda_low <= '1'; phase;
wait until falling_edge(clk); scl_low <= '1'; phase;
report "T8 an SDA edge while SCL is high is framing: the partial byte is dropped"
severity note;
ck("T8 it was reported as a START", nrs, 1);
ck("T8 the first phase was closed", ntd, 1);
ck("T8 only the address byte was ever reported", nb, 1);
ck("T8 a transaction is open again", b2i(txn_active), 1);
t_stop;
-- T9. The monitor never drives: with the bench released, both lines read HIGH.
do_reset;
wait until falling_edge(clk); scl_low <= '0'; sda_low <= '0';
for i in 1 to 20 loop step; end loop;
report "T9 with the bench released, the monitor is holding nothing" severity note;
ck("T9 SCL reads high", b2i(scl), 1);
ck("T9 SDA reads high", b2i(sda), 1);
if err = 0 then
report "=== i2c_mon: ALL CHECKS PASSED ===" severity note;
else
report "=== i2c_mon: " & integer'image(err) & " CHECK(S) FAILED ==="
severity note;
end if;
halt <= true;
wait;
end process;
end architecture sim;The framing check that could never fail
Pitfall — a monitor that instantiated the design's framing detector
// The monitor reuses the target's verified framing module. This looks like good
// practice: one implementation, already tested.
//
// i2c_framing fr (.scl(scl), .sda(sda),
// .start(mon_start), .stop(mon_stop), .restart(mon_restart));
//
// The framing module has a bug. It treats any SDA fall as a START:
//
// assign start = sda_fall; // missing: & scl
//
// So a data bit falling at the wrong moment starts a transfer. The target does
// this. The MONITOR DOES EXACTLY THE SAME THING, because it is the same module,
// and agrees with the target on every trace.
//
// The environment's framing check compares mon_start against the DUT's internal
// start signal. They are identical, always, by construction. The check has never
// fired and cannot be made to fire -- break the framing module further and both
// break together.Pitfall — a monitor that counted cycles, and a target that stretched
// A monitor written against a 100 kHz bus with a known clock period:
//
// localparam CYCLES_PER_BIT = 100;
// always @(posedge clk) begin
// if (txn_active) begin
// tick <= tick + 1;
// if (tick == CYCLES_PER_BIT/2) begin // sample mid-bit
// shreg <= {shreg[6:0], sda};
// bitcnt <= bitcnt + 1;
// tick <= 0;
// end
// end
// end
//
// Works perfectly. Then a target stretches the clock, and the monitor's byte
// boundaries drift away from the real ones -- so bytes are reported with bits
// from two different bus bits, and the byte COUNT eventually diverges too.
//
// The reports are garbage, and they are garbage about a transfer that was
// entirely correct. Debugging starts at the target.9. What 20.7 Settled
A monitor's only inputs are the resolved lines, and its only outputs are observations. No expected values, no verdicts, no drive port. That is what lets the same file be the thing under test in one scenario and trusted infrastructure in another.
Independence requires reimplementation. Framing is written from the specification, not from the design, because two instances of one implementation agree always — including in error. The symptom of getting this wrong is a check that cannot be made to fail.
Edge-driven, not cycle-counting. No bus rate appears anywhere in the file, which makes clock stretching a non-event and makes the component impossible to misconfigure. Sampling at SCL's rising edge follows from the protocol's own stability guarantee.
A byte and its acknowledge are one fact, reported together at the ninth edge. Splitting them makes every consumer responsible for pairing them, and one-cycle phase errors against this interface appeared twice in this module — in the assembler and in the scoreboard — with identical symptoms: every value legal, every value one event out.
The negative traces are the ones that matter. A truncated byte and an illegal SDA change are conditions a correct design never produces, so a monitor verified only against a correct design has never had its framing tested in the direction that counts.
The monitor says what happened. Nothing so far says what should have happened — and 20.3 showed that the stimulus cannot answer it, because intent and observation agree perfectly on transfers the target refused entirely. The next chapter builds the component that knows, from the contract and never from the bus. Chapter 20.8 — Reference Model and Scoreboard Strategy.
Continue learning
Related tutorials
- Related topic
What Should an I²C Transaction Object Represent?
Why an intent and an observation must be different types, with the comparison that holds when the DUT is removed. Builds both types and a transaction assembler against Module 18's real target, and reaches a transfer the target refused completely on which intent and observation agree perfectly — the gap a type system cannot close.
- 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
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
Mice on USB
A relative report cannot be resent, so the accumulator must saturate rather than wrap and must be cleared by the act of being read — and a real signedness bug the testbench caught on its first check.
