I²C · Module 21
Transaction Reconstruction in the Monitor
The edge-to-transaction state machine, why a repeated START both ends and begins a transfer in one cycle, and what to publish when the traffic is illegal. Includes two failure signatures worth memorising — values belonging to the next transfer, and a count exactly half of what it should be.
Chapter 21.5 established what the monitor is. This chapter is about the decision inside it: given a stream of edges, what objects come out, and — the question that actually separates a working monitor from one that works only on cooperative traffic — what comes out when the traffic is wrong.
1. Accumulate, Then Publish
A transaction cannot be published as it is discovered, because it is not complete until the framing event that ends it. So the monitor keeps a single accumulator object, fills it in as bytes arrive, and publishes it at a framing event.
i2c_txn acc;
acc = i2c_txn::type_id::create("acc");
forever begin
...
if (start_now) begin
if (active) begin // a PREVIOUS transfer is open: end it
acc.ended_by_restart = 1'b1;
acc.truncated = in_byte;
publish(acc);
end
acc = i2c_txn::type_id::create("acc"); // a NEW object, not a reset one
acc.began_with_restart = active;
...Two details there are load-bearing.
A new object, not a cleared one. The accumulator is published by handle. Clearing and reusing it would mutate the object a subscriber is still holding — and the scoreboard would compare the next transfer's fields while believing it had this one. The bug appears as a scoreboard reporting mismatches whose values belong to a transfer that had not happened yet, and it is a class of fault that does not exist in the struct-based version of 20.3 because a struct is copied on assignment.
acc.began_with_restart = active. Whether this transfer opened with a repeated START is precisely the question "was a transfer already open", which the monitor knows and nobody else does.
2. A Repeated START Is Two Events in One Cycle
This is the reconstruction's central awkwardness. A repeated START ends one transfer and begins another, simultaneously, and a monitor that treats framing as a set of mutually exclusive cases will lose one of them.
A consumer that assumed a STOP always precedes a START would lose the second phase entirely — and the write-pointer-then-read access that every register device uses is exactly two phases joined by a repeated START. So the most common real transaction shape is the one a naive reconstruction drops.
3. What to Emit When the Traffic Is Illegal
Three illegal or unusual situations, and the decision in each case is a design decision rather than an obvious consequence.
A transfer that ends mid-byte
Framing arrives with some bits accumulated. Those bits never completed, no acknowledge slot occurred, and no device received them — so they are not data.
The choice is between discarding the transfer silently and publishing it marked truncated. This monitor publishes it.
An SDA change while SCL is high, mid-byte
Not an error to be flagged, and not a data bit to be shrugged at. By the specification it is a START or a STOP, and every conforming device will treat it as one. So the monitor treats it the same way: the partial byte is dropped and a new phase opens.
This is the behaviour Chapter 20.9's error injector relies on to reframe a real target mid-transfer, and it is why the injected fault is indistinguishable from legal framing — because it is not a corrupted START, it is a START.
A byte with no acknowledge slot
Eight bits and then framing. The byte is complete as bits and incomplete as a protocol event: nobody was given the chance to accept it. Treated the same as any truncation — the transfer is published, the byte is not in the payload.
4. Per Phase, Not Per Transfer
After a repeated START the data count restarts, because the direction bit applies to the phase rather than to the bus occupancy.
A write-pointer-then-read sequence is one occupancy and two phases with different directions. A count spanning both would be meaningless: three bytes total, of which one was written and two were read, is not a fact about anything. Chapter 20.7's T6 exists specifically to check that the count restarts, because a monitor that accumulated across phases would produce plausible totals that no consumer could interpret.
5. The Reconstruction
// -----------------------------------------------------------------------------
// i2c_monitor.sv
// The passive component, and the one with the most ways to be quietly useless.
//
// NOT EXECUTED -- see i2c_if.sv. The reconstruction algorithm is a transcription of
// Chapter 20.7's `i2c_mon`, which was simulated against nine hand-built traces -- two of
// them deliberately illegal -- in SystemVerilog and VHDL, and survived eighteen
// mutations.
//
// THREE PROPERTIES, and each one is a decision a reviewer should be able to check:
//
// 1. IT DRIVES NOTHING. It connects through `mon_mp`, whose clocking block has no
// outputs, so there is nothing in its view of the interface to drive. Passivity is
// a property of the connection rather than of the author's intentions, which is
// what makes it survive a bring-up shortcut six months from now.
//
// 2. ITS FRAMING IS ITS OWN. Not the DUT's framing detector, and not the driver's.
// Two instances of one implementation given identical inputs agree ALWAYS,
// including when the implementation is wrong -- so a monitor that borrows the
// design's framing cannot detect a framing bug, which is a large part of what it
// exists for. The rules below are written from UM10204. This is the one place in
// this curriculum where duplicating verified logic is correct.
//
// 3. IT IS EDGE-DRIVEN. There is no bit period, no bus rate and no divider anywhere
// in this file, which is why clock stretching needs no handling: a stretched
// transfer is a transfer whose edges are further apart. A monitor that counted
// cycles would break on the one feature it most needs to watch, and its reports
// would stay plausible while being wrong -- which sends debugging to the DUT.
//
// IT REPORTS, IT DOES NOT JUDGE. A NACK is published as observed. It is frequently the
// correct behaviour -- a foreign address, a read-only register, a controller ending a
// read -- and deciding needs a contract the monitor does not have.
// -----------------------------------------------------------------------------
class i2c_monitor extends uvm_monitor;
`uvm_component_utils(i2c_monitor)
virtual i2c_if vif;
i2c_agent_config cfg;
// Published to anything that cares, and to nothing in particular. An analysis port
// is a broadcast: the monitor does not know whether a scoreboard, a coverage
// collector, both or neither is listening, which is what lets it be reused.
uvm_analysis_port #(i2c_txn) ap;
// Byte-level, for consumers that work below the transaction level.
uvm_analysis_port #(i2c_txn) byte_ap;
int unsigned n_txns, n_bytes, n_nacks;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
ap = new("ap", this);
byte_ap = new("byte_ap", this);
if (!uvm_config_db #(i2c_agent_config)::get(this, "", "cfg", cfg))
`uvm_fatal("NOCFG", "no i2c_agent_config for the monitor")
vif = cfg.vif;
if (vif == null)
`uvm_fatal("NOVIF", "i2c_agent_config.vif is null in the monitor")
endfunction
task run_phase(uvm_phase phase);
super.run_phase(phase);
collect();
endtask
// ---- reconstruction -----------------------------------------------------
task collect();
bit scl_d = 1'b1, sda_d = 1'b1;
bit scl_now, sda_now;
bit scl_rise, sda_fall, sda_rise;
bit start_now, stop_now;
bit active = 1'b0;
bit addr_seen = 1'b0;
bit in_byte = 1'b0;
bit [7:0] shreg = 8'h00;
int unsigned bitcnt = 0;
i2c_txn acc;
acc = i2c_txn::type_id::create("acc");
forever begin
@(vif.mon_cb);
scl_now = vif.mon_cb.scl;
sda_now = vif.mon_cb.sda;
scl_rise = scl_now && !scl_d;
sda_fall = !sda_now && sda_d;
sda_rise = sda_now && !sda_d;
// START and STOP are SDA transitions WHILE SCL IS HIGH. Both the current and
// the previous SCL are required high: an SDA transition in the same cycle as
// an SCL edge is a data bit changing at a bit boundary, not framing. Removing
// the previous-cycle term is a mutation that the VHDL twin's bench kills in
// twenty-one checks, which measures how much of the reconstruction rests on it.
start_now = sda_fall && scl_now && scl_d;
stop_now = sda_rise && scl_now && scl_d;
if (start_now) begin
// A repeated START both ENDS one transfer and BEGINS another, in the same
// cycle. A consumer that assumed a STOP always precedes a START would lose
// the second phase entirely.
if (active) begin
acc.ended_by_restart = 1'b1;
acc.ended_by_stop = 1'b0;
acc.truncated = in_byte;
publish(acc);
end
acc = i2c_txn::type_id::create("acc");
acc.began_with_restart = active;
active = 1'b1;
addr_seen = 1'b0;
in_byte = 1'b0;
bitcnt = 0;
shreg = 8'h00;
end
else if (stop_now) begin
if (active) begin
acc.ended_by_restart = 1'b0;
acc.ended_by_stop = 1'b1;
// Framing arrived mid-byte: those bits never completed, no acknowledge
// slot occurred, and no device received them. They are not data.
acc.truncated = in_byte;
publish(acc);
end
active = 1'b0;
in_byte = 1'b0;
end
else if (scl_rise && active) begin
if (bitcnt < 8) begin
shreg = {shreg[6:0], sda_now};
bitcnt++;
in_byte = 1'b1;
end else begin
// The NINTH rising edge, and the byte is published WITH its acknowledge.
// Eight bits are not yet a protocol fact: whether they were received is
// undetermined until this slot. Splitting them would make every consumer
// responsible for pairing a byte with a later acknowledge event, and
// every consumer would have to get that pairing right on its own.
n_bytes++;
if (sda_now) n_nacks++;
if (!addr_seen) begin
acc.addr = shreg[7:1];
acc.read = shreg[0];
acc.addr_acked = ~sda_now;
addr_seen = 1'b1;
end else begin
acc.data = new[acc.data.size() + 1](acc.data);
acc.acks = new[acc.acks.size() + 1](acc.acks);
acc.data[acc.data.size() - 1] = shreg;
acc.acks[acc.acks.size() - 1] = ~sda_now;
end
publish_byte(shreg, ~sda_now, !addr_seen);
bitcnt = 0;
shreg = 8'h00;
in_byte = 1'b0;
end
end
scl_d = scl_now;
sda_d = sda_now;
end
endtask
function void publish(i2c_txn t);
n_txns++;
ap.write(t);
endfunction
function void publish_byte(bit [7:0] d, bit acked, bit is_addr);
i2c_txn b;
b = i2c_txn::type_id::create("byte");
b.data = new[1];
b.acks = new[1];
b.data[0] = d;
b.acks[0] = acked;
byte_ap.write(b);
endfunction
endclassWhy the byte is published at the ninth edge and not the eighth
10 cyclesThe scoreboard that compared a transfer that had not happened yet
Pitfall — an accumulator cleared and reused instead of replaced
// Reconstruction with one accumulator object, cleared between transfers. It looks
// like an obvious efficiency: one allocation instead of thousands.
//
// i2c_txn acc;
// function void new_phase();
// acc.addr = 0; acc.read = 0; acc.addr_acked = 0;
// acc.data.delete(); acc.acks.delete();
// acc.began_with_restart = 0; acc.truncated = 0;
// endfunction
//
// if (stop_now) begin
// acc.ended_by_stop = 1'b1;
// ap.write(acc); // publishes the HANDLE
// new_phase(); // ... and immediately clears the object
// end
//
// The scoreboard holds that handle. Analysis ports are not queues -- write() is a
// function call, but a subscriber that stores the handle for later, or that is
// reached through a TLM FIFO, sees the object as it is WHEN IT LOOKS.
//
// So the scoreboard compares fields that have been cleared, or worse, fields the
// NEXT transfer has already filled in. It reports mismatches whose values belong
// to a transfer that had not happened when the error was raised.Pitfall — a repeated START treated as mutually exclusive with a START
// Framing handled as a case over three exclusive events:
//
// if (start_now) begin acc = new(); active = 1; end
// else if (restart_now) begin acc = new(); active = 1; end
// else if (stop_now) begin publish(acc); active = 0; end
//
// where restart_now is start_now && active.
//
// The open transfer is NEVER PUBLISHED. A repeated START creates a new
// accumulator and the previous one is dropped, so a write-pointer-then-read
// sequence -- the access every register device uses -- reports only the READ.
//
// The scoreboard therefore never sees the write that set the pointer, so its model
// of the pointer is wrong from the first transfer onward, and it reports a DATA
// mismatch on every subsequent read. Every value it prints is plausible and the
// error count is large, which makes it look like a DUT fault in the register file.6. What 21.6 Settled
Accumulate and publish at framing, with a fresh object each time. Publishing a handle transfers a reference, so reusing the accumulator lets a subscriber's view of a past transfer become the present one — a failure mode that appears only once transactions are class handles.
A repeated START is an ending and a beginning in the same cycle. Treating framing as exclusive cases drops the transfer it terminates, which is the most common real access shape, and makes every downstream comparison wrong.
Illegal traffic gets published, described rather than judged. Truncation is flagged, the partial byte is excluded from the payload, and an SDA change while SCL is high is treated as what the specification says it is — framing — because every conforming device will treat it that way too.
The data count is per phase. A count spanning a direction change is not a fact about anything.
Two failure signatures worth memorising. Reported values belonging to the next transfer means handle aliasing; a transaction count that is exactly half the expected number means a missed publication at a repeated START.
Next, the three components become one reusable unit with a single switch that decides which of them exist. Chapter 21.7 — Assembling the I²C Agent.
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
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.
- Related topic
Address Decoding Inside a Slave
The first piece of slave hardware in the curriculum, and it is assembled rather than written: byte capture, reserved-map classification, one equality test, and a decision whose ordering matters — because a prohibition has to beat an address match.
