I²C · Module 21
The Target Responder Driver — Reacting Within Protocol Timing
Why a responder is a state machine over edges rather than a loop over items, why it is deliberately not connected to a sequencer, and why 'not acknowledging' and 'the transfer is over' must be different states — collapsing them returns 0xFF after the first byte of every read.
A master driver and a target responder look like the same component with the polarity reversed. They are not, and the difference is structural rather than a matter of detail.
A master driver fetches an item and then decides when everything happens. A responder has no items to fetch and decides nothing about timing: it can only recognise what has already happened on the bus and answer inside the window somebody else left open. Write it as a loop over items with a timed sequence of drives and it will miss its slot the first time the controller's timing differs from the one it assumed.
1. No Sequencer, and That Is Not an Omission
The responder in this VIP is a uvm_driver that never calls get_next_item, and Chapter 21.7's agent deliberately does not connect it to a sequencer.
That looks like a mistake against every UVM diagram ever drawn, so it is worth being explicit about why it is correct. A sequencer exists to arbitrate between sequences competing for a driver. A responder has nothing to arbitrate: the bus tells it what to do, one edge at a time. Connect it to a sequencer and the most likely outcome is a driver blocked in get_next_item waiting for stimulus that never arrives — which presents as a target that never acknowledges, on a bus that looks fine.
2. Thin Policy, Because a Faithful Model Shares the Bugs
The temptation is to make the responder a good model of Module 18's target: a register file, a pointer, a read-only mask, the six documented decisions. It would be more realistic and it would be a serious mistake.
So the policy is four configuration fields and none of them is a register map:
| field | what it decides | why a controller cares |
|---|---|---|
resp_ack | answer our address, or refuse it | a controller must handle an absent or busy device |
resp_nack_after | acknowledge N data bytes, then stop | mid-transfer refusal is legal and common |
resp_stretch_clks | hold SCL after an acknowledge | the controller must wait rather than count |
resp_read_base | the first byte of a read, incrementing | read data has to come from somewhere |
Whether those bytes are the right bytes is a question about a device contract, and it is answered by 21.8's predictor — a separate component with a separate job. The responder produces traffic; the predictor produces expectations. One component doing both would be predicting its own output.
3. Two Acknowledge States
The state machine has six states and two of them look redundant until you try to remove one.
R_IDLE waiting for a START
R_ADDR shifting in the address byte
R_ACK the ninth slot, and WE own it
R_WDATA shifting in a data byte
R_RDATA shifting OUT a data byte
R_CACK the ninth slot, and the CONTROLLER owns itR_ACK and R_CACK are both "the ninth bit slot". They are different states because the question who drives SDA here has different answers.
And a second defect hid behind the first. Once R_RDATA had two entry paths they needed different bit-counter starting values, and only one had been considered — so the second byte of every read arrived as 0x83 where 0xC1 was expected. A plausible-looking wrong value, which is the worst kind.
R_RDATA: begin
if (scl_fall && bitcnt < 8)
vif.drv_cb.sda_drive_low[drv_idx] <= ~tx[7 - bitcnt];Indexing the byte rather than shifting a register means one expression is correct from both paths. The per-path initialisation that was the opportunity to disagree no longer exists.
4. Framing Is Checked First, Unconditionally
if (sda_fall && scl_now && scl_d) begin
state = R_ADDR; bitcnt = 0; shreg = 8'h00; selected = 1'b0;
vif.drv_cb.sda_drive_low[drv_idx] <= 1'b0;
return;
endA START or STOP can arrive at any point in any state — the controller may abort, another master may intervene, an error injector may create one. A responder that only looks for framing when it expects framing is a responder that never recovers from an aborted transfer: it stays in the middle of a byte that will never finish, holding whatever it was holding, and every subsequent test inherits a wedged bus.
Note also that the framing branches release SDA. Recovering the state machine without releasing the line would leave the responder as a silent second puller.
5. The Responder
// -----------------------------------------------------------------------------
// i2c_responder_driver.sv
// Reacting inside somebody else's timing windows, which is a different problem.
//
// NOT EXECUTED -- see i2c_if.sv. The algorithm is a transcription of Chapter 20.6's
// `i2c_resp_bfm`, which was simulated and survived eleven mutations of its own.
//
// THE STRUCTURAL DIFFERENCE FROM THE MASTER DRIVER, and it is the reason this is a
// separate chapter rather than a paragraph: the master driver is a LOOP OVER ITEMS and
// this is a STATE MACHINE OVER EDGES. A responder has no items to fetch. It cannot
// decide when anything happens; it can only recognise what has happened and answer
// within the window the controller leaves open. A responder written as `get_next_item`
// plus a timed sequence of drives will miss its slot the first time the controller's
// timing differs from the one it assumed.
//
// TWO ACKNOWLEDGE STATES, and the second one looks redundant until you remove it.
// R_ACK is a slot WE own and drive. R_CACK is a slot the CONTROLLER owns, which we must
// release and then read. With a single state, "we are not driving the acknowledge"
// collapses into "the transfer has ended", every multi-byte read terminates after one
// byte, and the controller reads back the pull-up: 0xFF. The symptom points at the read
// datapath, which is correct.
// -----------------------------------------------------------------------------
class i2c_responder_driver extends uvm_driver #(i2c_seq_item);
`uvm_component_utils(i2c_responder_driver)
virtual i2c_if vif;
i2c_agent_config cfg;
int unsigned drv_idx = 1;
typedef enum { R_IDLE, R_ADDR, R_ACK, R_WDATA, R_RDATA, R_CACK } state_e;
state_e state = R_IDLE;
bit [7:0] shreg;
int unsigned bitcnt;
int unsigned nbytes;
bit selected, is_read;
bit [7:0] tx;
// Observations this responder exports. Counters rather than pulses: a one-cycle
// pulse is invisible to anything that polls.
int unsigned n_addr_seen, n_acked, n_nacked, n_stretches;
bit [7:0] last_write;
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 responder driver")
vif = cfg.vif;
if (vif == null)
`uvm_fatal("NOVIF", "i2c_agent_config.vif is null in the responder driver")
drv_idx = cfg.drv_index;
endfunction
// No item queue. The responder is driven by the bus, not by a sequence -- so it
// never calls get_next_item, and there is nothing for a sequencer to arbitrate.
task run_phase(uvm_phase phase);
super.run_phase(phase);
vif.drv_cb.scl_drive_low[drv_idx] <= 1'b0;
vif.drv_cb.sda_drive_low[drv_idx] <= 1'b0;
forever step();
endtask
// ---- one clock of the responder's state machine -------------------------
task step();
bit scl_d, sda_d, scl_now, sda_now;
bit scl_rise, scl_fall, sda_fall, sda_rise;
scl_d = vif.drv_cb.scl;
sda_d = vif.drv_cb.sda;
@(vif.drv_cb);
scl_now = vif.drv_cb.scl;
sda_now = vif.drv_cb.sda;
scl_rise = scl_now && !scl_d;
scl_fall = !scl_now && scl_d;
sda_fall = !sda_now && sda_d;
sda_rise = sda_now && !sda_d;
// Framing first, and unconditionally. A START or STOP can arrive at any point in
// any state, and a responder that only looks for framing when it expects framing
// is a responder that never recovers from an aborted transfer.
if (sda_fall && scl_now && scl_d) begin
state = R_ADDR; bitcnt = 0; shreg = 8'h00; selected = 1'b0;
vif.drv_cb.sda_drive_low[drv_idx] <= 1'b0;
return;
end
if (sda_rise && scl_now && scl_d) begin
state = R_IDLE; selected = 1'b0;
vif.drv_cb.sda_drive_low[drv_idx] <= 1'b0;
vif.drv_cb.scl_drive_low[drv_idx] <= 1'b0;
return;
end
case (state)
R_IDLE: ; // wait for a START
// Sample on the RISING edge, because that is the instant the protocol
// guarantees SDA is stable. Anywhere else reads a line permitted to move.
R_ADDR: if (scl_rise) begin
shreg = {shreg[6:0], sda_now};
bitcnt++;
if (bitcnt == 8) begin
n_addr_seen++;
is_read = shreg[0];
selected = cfg.resp_ack && (shreg[7:1] == cfg.resp_addr);
nbytes = 0;
state = R_ACK;
bitcnt = 0;
end
end
// WE own this slot. Drive it on the FALL, so SDA settles while SCL is low.
R_ACK: begin
if (scl_fall) begin
vif.drv_cb.sda_drive_low[drv_idx] <= selected;
if (selected) n_acked++; else n_nacked++;
end
if (scl_rise) begin
// A stretch, if policy asked for one. Held for a counted number of
// clocks and then released -- a responder that stretched forever would
// wedge the bus rather than exercise the controller's patience.
if (cfg.resp_stretch_clks > 0) begin
n_stretches++;
vif.drv_cb.scl_drive_low[drv_idx] <= 1'b1;
repeat (cfg.resp_stretch_clks) @(vif.drv_cb);
vif.drv_cb.scl_drive_low[drv_idx] <= 1'b0;
end
if (!selected) begin
state = R_IDLE;
vif.drv_cb.sda_drive_low[drv_idx] <= 1'b0;
end else if (is_read) begin
tx = cfg.resp_read_base + nbytes[7:0];
bitcnt = 1; // the fall that closed our slot drives bit 7
state = R_RDATA;
vif.drv_cb.sda_drive_low[drv_idx] <= ~tx[7];
end else begin
vif.drv_cb.sda_drive_low[drv_idx] <= 1'b0;
shreg = 8'h00;
bitcnt = 0;
state = R_WDATA;
end
end
end
R_WDATA: if (scl_rise) begin
shreg = {shreg[6:0], sda_now};
bitcnt++;
if (bitcnt == 8) begin
last_write = shreg;
nbytes++;
// Thin policy: refuse after N bytes if asked to. This is the only
// decision the responder makes about content, and it is deliberately not
// a register map.
selected = (cfg.resp_nack_after == 0) || (nbytes <= cfg.resp_nack_after);
state = R_ACK;
bitcnt = 0;
end
end
// ONE BIT INDEX, TWO ENTRY PATHS. `bitcnt` names the NEXT bit to drive, as
// tx[7 - bitcnt], and the byte is done when it reaches 8. From R_ACK the fall
// already drove bit 7 so bitcnt arrives as 1; from R_CACK nothing is driven
// yet so it arrives as 0. One expression is correct for both. Indexing the
// byte rather than shifting a register removes the per-path initialisation
// that is otherwise a place for the two paths to disagree -- and getting it
// wrong made the second read byte arrive as 0x83 instead of 0xC1.
R_RDATA: begin
if (scl_fall && bitcnt < 8)
vif.drv_cb.sda_drive_low[drv_idx] <= ~tx[7 - bitcnt];
if (scl_rise) begin
bitcnt++;
if (bitcnt == 8) begin
vif.drv_cb.sda_drive_low[drv_idx] <= 1'b0; // RELEASE: not our slot
state = R_CACK;
end
end
end
// The CONTROLLER owns this slot. We release and read it, and the controller's
// bit -- not our policy -- decides whether another byte follows.
R_CACK: if (scl_rise) begin
if (sda_now == 1'b0) begin // controller acknowledged: another byte
nbytes++;
tx = cfg.resp_read_base + nbytes[7:0];
bitcnt = 0;
state = R_RDATA;
end else begin // controller NACKed: it is finished
state = R_IDLE;
vif.drv_cb.sda_drive_low[drv_idx] <= 1'b0;
end
end
default: state = R_IDLE;
endcase
endtask
endclassNote that the responder samples on the rising edge and drives on the falling edge, and neither is arbitrary. The protocol forbids SDA from changing while SCL is high, so the rising edge is the instant a received value is defined — and the falling edge is the only time a transmitted value may be changed. A responder that drove on the rise would be violating the rule it is there to help test.
Every multi-byte read returns 0xFF after the first byte
Pitfall — one acknowledge state for two different owners
// A responder state machine with a single acknowledge state. Writes work
// perfectly. Single-byte reads work. Multi-byte reads return the first byte
// correctly and 0xFF for every byte after it.
//
// R_RDATA: if (bit_done) state <= R_ACK; // the ninth slot
//
// R_ACK: begin
// sda_drive_low[i] <= ack_this; // WE drive the acknowledge
// if (slot_done)
// state <= ack_this ? R_WDATA : R_IDLE; // not acking => finished
// end
//
// During a READ the CONTROLLER owns the ninth slot, not the responder. So the
// responder drives a slot it does not own -- and then, because "we are not
// acknowledging" is encoded as "the transfer is over", returns to R_IDLE.
//
// The controller keeps clocking. Nobody is driving SDA. It reads the pull-up:
// 0xFF, for every byte after the first.
//
// The symptom points at the read datapath, which is correct.Pitfall — a responder wired to a sequencer, waiting forever
// A responder built from the standard agent template, which connects every
// driver to a sequencer:
//
// function void connect_phase(uvm_phase phase);
// super.connect_phase(phase);
// driver.seq_item_port.connect(sequencer.seq_item_export);
// responder.seq_item_port.connect(sequencer.seq_item_export); // <-- copied
// endfunction
//
// // and the responder, also from the template:
// task run_phase(uvm_phase phase);
// forever begin
// seq_item_port.get_next_item(req); // blocks forever
// respond_to_bus();
// seq_item_port.item_done();
// end
// endtask
//
// Nothing ever starts a sequence on that sequencer, because a responder has no
// stimulus to be given. So get_next_item blocks on the first call and
// respond_to_bus() is NEVER ENTERED.
//
// The symptom: the target never acknowledges anything. The bus looks healthy,
// the master driver runs correctly, every transfer is NACKed. Debugging goes to
// the address comparison -- which is never executed.6. What 21.4 Settled
A responder is a state machine over edges, not a loop over items. It fetches nothing and is deliberately not connected to a sequencer — which is correct here and wrong for a reactive agent whose policy changes per transfer, and the difference is worth naming rather than copying either pattern blindly.
Its policy must be too thin to share the target's bugs. Four configuration fields and no register map, because a faithful model agrees with the target about its mistakes.
"I am not driving this slot" and "the transfer is over" are different states. Collapsing them returns 0xFF after the first byte of every read and points debugging at a correct datapath.
A state with two entry paths must not carry per-path initialisation. Indexing the byte instead of shifting a register removed the only place the two paths could disagree — after they had already disagreed once, producing a plausible wrong value.
Framing is handled before state, in every state, and it releases the lines. Otherwise an aborted transfer leaves the responder holding a line and every later test inherits it.
Next, the passive component — and the reason it must reimplement framing rather than borrow it. Chapter 21.5 — The Monitor.
Continue learning
Related tutorials
- Related topic
The Target Responder — Modeling a Real Device
A responder judged by the awkward legal behaviour it can produce on demand — refusal, mid-transfer NACK, clock stretching — with a policy deliberately too thin to share the target's bugs. Includes why 'not acknowledging' and 'the transfer is over' are different states, and a mutation that survived on a parameter value.
- 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.
