Skip to content
VLSI Mentor

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:

fieldwhat it decideswhy a controller cares
resp_ackanswer our address, or refuse ita controller must handle an absent or busy device
resp_nack_afteracknowledge N data bytes, then stopmid-transfer refusal is legal and common
resp_stretch_clkshold SCL after an acknowledgethe controller must wait rather than count
resp_read_basethe first byte of a read, incrementingread 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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 it

R_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.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_responder_driver.sv — one expression for two entry paths
            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

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_responder_driver.sv — framing before state, in every state
         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

A 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

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_responder_driver.sv — a state machine over edges
   // -----------------------------------------------------------------------------
   // 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

   endclass

Note 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.

A state diagram with six states. R_IDLE leads on a START to R_ADDR, which leads to R_ACK after eight bits. From R_ACK a write path goes up to R_WDATA and a read path goes down to R_RDATA. The link between R_ACK and R_WDATA is bidirectional because each written byte returns for its acknowledge. R_RDATA links bidirectionally to R_CACK, where the controller drives the slot. R_CACK returns to R_IDLE when the controller sends a NACK.R_WDATAdata inR_IDLEawaiting STARTR_ADDRaddress inR_ACKwe drive itR_RDATAdata outR_CACKcontroller drivesitSTART8 bitswritereadbyte outNACKed12
Figure 1 — the responder's states, with the two acknowledge slots separated. R_ACK is a slot the responder owns and drives; R_CACK is one the controller owns, which the responder must release and read, and where the controller's bit decides whether another byte follows. Both acknowledge links are drawn bidirectional because each byte returns to its slot. Collapsing the two states makes 'we are not driving the acknowledge' mean 'the transfer has ended'.

Every multi-byte read returns 0xFF after the first byte

Pitfall — one acknowledge state for two different owners
Buggy Code
// 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
Buggy Code
// 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