Skip to content
VLSI Mentor

I²C · Module 20

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.

The driver is the component with a reason to cheat. It knows what it is trying to achieve, it controls the clock, and almost every shortcut it could take produces the intended transfer anyway — most of the time, against most targets.

Which is why it is the component that most needs to be written as a participant rather than as a stimulus generator. Everything the environment checks of other devices applies to it, and when it violates a rule the failure appears somewhere else, attributed to something else.

1. What It Owns, and What It Must Not

The driver owns exactly one translation: from an intent — an address, a direction, a payload, a framing choice — into a sequence of pull-low operations on two wires. That is a genuinely difficult job and it is the only one it has.

the driver ownsthe driver must not own
the SCL waveform: when every bit movesany expected value
turning an intent into framing and bytesany verdict
obeying every protocol rule itselfany knowledge of the target's register map
reporting what it observed from its own positionthe decision about whether that was correct

The last row is the one that decays first. A driver that reads back an acknowledge already knows something interesting, and adding if (!ack) error("target did not respond") is a two-line change that seems obviously right. It is how a driver acquires a verdict, and the reason not to is that the driver's position is one view among several. It cannot distinguish a target that failed to respond from an address it was never meant to answer — Chapter 20.3's T8 is precisely a transfer where the correct behaviour is a NACK on every byte.

So the driver reports obs_addr_acked and obs_n_acked and says nothing about whether they are right. Observations, not conclusions.

2. It Must Obey the Rule It Tests

SDA may change only while SCL is low, except for framing. This is the rule the environment most wants to check of a target, and a driver that breaks it does not produce a failed check — it produces a different transfer from the one intended, because an SDA change while SCL is high is a START or a STOP by definition.

Look at where the obligation is discharged:

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_ctrl_bfm.sv — the data-valid rule, in the bit task itself
      // One bit out. SDA changes only while SCL is LOW -- the data-valid rule the BFM
      // is testing others against, and must therefore obey itself.
      task automatic put_bit (input bit_v);
         begin
            @(negedge clk); scl_drive_low = 1'b1; hp;
            @(negedge clk); sda_drive_low = ~bit_v; hp;
            scl_release_and_wait;
            @(negedge clk); scl_drive_low = 1'b1; hp;
         end
      endtask

The ordering is the enforcement. SDA is only ever assigned on the line after SCL has been pulled low, and there is exactly one task that moves a data bit — so the rule is obeyed by construction rather than by review. Framing gets its own tasks, pulse_start, pulse_restart and pulse_stop, which change SDA while SCL is high deliberately, and they are the only places that do.

3. Releasing Is Not the Same as Being High

This is the single most important line of code in the file, and it is a while loop.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_ctrl_bfm.sv — the whole of clock-stretch support
      task automatic scl_release_and_wait;
         begin
            @(negedge clk); scl_drive_low = 1'b0;
            w = 0;
            while (!scl_in && w < STRETCH_TIMEOUT) begin
               @(posedge clk); @(negedge clk);
               w = w + 1;
               if (w > 1) obs_stretched = 1'b1;
            end
            if (!scl_in) obs_timeout = 1'b1;
            hp;
         end
      endtask

On an open-drain bus, releasing a line is a request, not a result. The line rises when every participant has released it, and a target that is not ready holds SCL low precisely so that this does not happen. A driver that treats its own release as the line being high clocks the next bit into a device that has stopped listening, and the data is simply gone — no error anywhere, a byte that quietly differs from the one that was sent.

Three properties of that loop are each load-bearing.

It reads scl_in, the resolved line. Not its own drive intent. The whole point is that these differ.

It is bounded. STRETCH_TIMEOUT is a parameter and exceeding it sets obs_timeout. A target that never releases SCL is a real fault — 20.9 creates it deliberately — and an unbounded wait would hang the simulation on the environment's own stimulus rather than reporting anything.

It reports rather than decides. obs_stretched and obs_timeout are outputs. The driver does not know whether a stretch was legitimate; it knows the line stayed low, and how long for.

4. The Ninth Slot Belongs to Somebody Else

An eight-bit byte followed by an acknowledge means the driver has to hand a bit slot over, and handing over means releasing SDA — not driving it high, which is impossible, and not leaving it as it was.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_ctrl_bfm.sv — the acknowledge slot, released
      // The ninth slot with SDA RELEASED, so the target owns it. What comes back is the
      // resolved line -- not anything this BFM drove.
      task automatic ack_slot (output ackv);
         begin
            @(negedge clk); scl_drive_low = 1'b1; hp;
            @(negedge clk); sda_drive_low = 1'b0; hp;
            scl_release_and_wait;
            for (k = 0; k < HALF - 2; k = k + 1) begin @(posedge clk); @(negedge clk); end
            ackv = ~sda_in;               // LOW on the wire = acknowledged
            @(posedge clk); @(negedge clk);
            @(negedge clk); scl_drive_low = 1'b1; hp;
         end
      endtask

A driver that forgets to release SDA in the ninth slot reads back its own last data bit. If that bit was low, every transfer is reported as acknowledged — including transfers to addresses no device owns. Mutation C01 is exactly this change and the responder bench kills it, but notice what the symptom would be in an environment without that check: a NACK test that passes.

The read direction inverts the ownership. get_byte releases SDA for eight bits so the target can source them, then drives the ninth itself — and the ninth bit is meaningful: the controller acknowledges every byte except the last, which it NACKs to tell the target to stop sourcing. That is Module 18's Chapter 18.8 contract seen from the other side, and it is one of the few places where the driver has to encode a protocol decision rather than a protocol rule.

5. Handshaking With a Procedural Bench

The driver runs continuously; the bench is a sequence of statements. Getting the handshake between them wrong produces a symptom that points at the wrong component entirely.

6. The Driver

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_ctrl_bfm.sv — the controller driver: intent to edges, and nothing else
   // -----------------------------------------------------------------------------
   // i2c_ctrl_bfm.sv
   // The controller BFM: turn transaction INTENT into legal bus activity.
   //
   // WHAT THIS COMPONENT OWNS:
   //   - translating a request into a legal sequence of framing and bit activity
   //   - obeying every protocol rule it is testing others against
   //   - honouring clock stretching: releasing SCL and WAITING until the line rises
   //   - releasing SDA in the ninth slot so the target owns the acknowledge
   //   - reporting what it OBSERVED, distinct from what it requested
   //
   // WHAT IT DOES NOT OWN, and this is the architectural point of Chapter 20.5:
   //   - the verdict. It reports `obs_acked`; it never says "pass" or "fail".
   //   - passive reconstruction. Chapter 20.7's monitor does that, independently.
   //   - the expected register state. Chapter 20.8's predictor does that.
   //
   // A driver that judged itself would be the circular architecture Chapter 20.4
   // rejects: it would compare what it sent against what it believes it sent.
   //
   // WHY THE RESULT IS REPORTED AT ALL, if the driver does not judge: because a
   // controller genuinely needs it. `obs_acked` is how a real controller learns its write
   // was refused, and a driver that discarded it could not implement acknowledge polling
   // (Chapter 16.4). It is an OBSERVATION the driver made, and it is offered to the
   // environment -- not used to decide anything here.
   //
   // OPEN-DRAIN DISCIPLINE: the two outputs are drive-low intents. There is no way for
   // this module to drive either line HIGH -- Chapter 19.1's contract, and the reason a
   // conforming BFM cannot short the bus even when it is wrong.
   //
   // BOUNDED: the stretch wait has an explicit timeout. A target that never releases SCL
   // makes this BFM report `obs_timeout` and give up, rather than hanging the run.
   // -----------------------------------------------------------------------------

   module i2c_ctrl_bfm #(
      // Clocks per SCL half-phase. The BFM owns SCL, so it owns the bus rate.
      parameter int HALF = 20,
      // Clocks to wait for a stretched SCL to rise before declaring a timeout. Must
      // exceed the longest legal stretch the environment intends to exercise.
      parameter int STRETCH_TIMEOUT = 20000
   ) (
      input  logic clk,
      input  logic rst_n,

      // ---- the bus: drive intent out, resolved level in ------------------------
      output logic scl_drive_low,
      output logic sda_drive_low,
      input  logic scl_in,          // RESOLVED, so a stretch is visible
      input  logic sda_in,          // RESOLVED, so the target's acknowledge is visible

      // ---- the request: transaction INTENT ------------------------------------
      // A one-cycle pulse starts a transfer. Held stable by the caller until `done`.
      input  logic       req,
      input  logic [6:0] req_addr,
      input  logic       req_read,
      input  logic [2:0] req_len,        // data bytes to transfer
      input  logic [7:0] req_d0,         // up to three write bytes
      input  logic [7:0] req_d1,
      input  logic [7:0] req_d2,
      input  logic       req_restart,    // issue a repeated START instead of a START first
      // KEEP THE BUS. Without this, `req_restart` above is unreachable in any meaningful
      // sense: a transfer that always ends in a STOP can only ever be followed by a plain
      // START, so a "repeated START" issued after it is a repeated START in name only. A
      // controller that wants to change direction without releasing the bus has to be able
      // to decline to send the STOP, and that is a driver capability, not a test trick.
      input  logic       req_hold,

      // ---- the result: what the driver OBSERVED, never a verdict --------------
      // A COMPLETION COUNT, not a one-cycle pulse. A pulse can be missed by a caller
      // polling on a clock edge, and the first version of this BFM was polled that way
      // -- the transfer completed correctly and the bench reported a deadlock. A
      // monotonic count is race-free: the caller records it and waits for it to change.
      output logic [7:0] done_count,
      output logic       obs_addr_acked,
      output logic [2:0] obs_n_acked,    // data bytes that were acknowledged
      output logic [7:0] obs_r0,         // bytes read back, if this was a read
      output logic [7:0] obs_r1,
      output logic [7:0] obs_r2,
      output logic       obs_stretched,  // the target held SCL at least once
      output logic       obs_timeout,    // a stretch outlived STRETCH_TIMEOUT
      output logic       busy
   );

      integer i, k, w;
      logic [7:0] b;
      logic       a;

      // ---- primitives, all bounded --------------------------------------------
      task automatic hp;                      // one half-phase
         begin for (i = 0; i < HALF; i = i + 1) begin @(posedge clk); @(negedge clk); end end
      endtask

      // Release SCL and WAIT for it to actually rise. This is the whole of clock-stretch
      // handling on the controller side: releasing is not the same as the line being
      // high, and a driver that assumed otherwise clocks the next bit into a target that
      // is not ready. Chapter 19.1's T8 is the same lesson one layer down.
      task automatic scl_release_and_wait;
         begin
            @(negedge clk); scl_drive_low = 1'b0;
            w = 0;
            while (!scl_in && w < STRETCH_TIMEOUT) begin
               @(posedge clk); @(negedge clk);
               w = w + 1;
               if (w > 1) obs_stretched = 1'b1;
            end
            if (!scl_in) obs_timeout = 1'b1;
            hp;
         end
      endtask

      task automatic pulse_start;
         begin
            @(negedge clk); sda_drive_low = 1'b0; scl_drive_low = 1'b0; hp;
            @(negedge clk); sda_drive_low = 1'b1; hp;          // SDA falls, SCL high
            @(negedge clk); scl_drive_low = 1'b1; hp;
         end
      endtask

      task automatic pulse_restart;
         begin
            @(negedge clk); scl_drive_low = 1'b1; sda_drive_low = 1'b0; hp;
            scl_release_and_wait;
            @(negedge clk); sda_drive_low = 1'b1; hp;          // SDA falls, SCL high
            @(negedge clk); scl_drive_low = 1'b1; hp;
         end
      endtask

      task automatic pulse_stop;
         begin
            @(negedge clk); scl_drive_low = 1'b1; sda_drive_low = 1'b1; hp;
            scl_release_and_wait;
            @(negedge clk); sda_drive_low = 1'b0; hp;          // SDA rises, SCL high
         end
      endtask

      // One bit out. SDA changes only while SCL is LOW -- the data-valid rule the BFM
      // is testing others against, and must therefore obey itself.
      task automatic put_bit (input bit_v);
         begin
            @(negedge clk); scl_drive_low = 1'b1; hp;
            @(negedge clk); sda_drive_low = ~bit_v; hp;
            scl_release_and_wait;
            @(negedge clk); scl_drive_low = 1'b1; hp;
         end
      endtask

      // The ninth slot with SDA RELEASED, so the target owns it. What comes back is the
      // resolved line -- not anything this BFM drove.
      task automatic ack_slot (output ackv);
         begin
            @(negedge clk); scl_drive_low = 1'b1; hp;
            @(negedge clk); sda_drive_low = 1'b0; hp;
            scl_release_and_wait;
            for (k = 0; k < HALF - 2; k = k + 1) begin @(posedge clk); @(negedge clk); end
            ackv = ~sda_in;               // LOW on the wire = acknowledged
            @(posedge clk); @(negedge clk);
            @(negedge clk); scl_drive_low = 1'b1; hp;
         end
      endtask

      task automatic put_byte (input [7:0] d, output ackv);
         begin
            for (k = 7; k >= 0; k = k - 1) put_bit(d[k]);
            ack_slot(ackv);
         end
      endtask

      // One byte in: SDA released for eight bits so the target drives, then the
      // controller drives the ninth itself. `ackv` here is what the CONTROLLER sends.
      task automatic get_byte (input ackv, output [7:0] d);
         begin
            d = 8'h00;
            for (k = 7; k >= 0; k = k - 1) begin
               @(negedge clk); scl_drive_low = 1'b1; hp;
               @(negedge clk); sda_drive_low = 1'b0; hp;
               scl_release_and_wait;
               for (w = 0; w < HALF - 2; w = w + 1) begin @(posedge clk); @(negedge clk); end
               d[k] = sda_in;
               @(posedge clk); @(negedge clk);
               @(negedge clk); scl_drive_low = 1'b1; hp;
            end
            @(negedge clk); scl_drive_low = 1'b1; hp;
            @(negedge clk); sda_drive_low = ackv; hp;
            scl_release_and_wait;
            @(negedge clk); scl_drive_low = 1'b1; hp;
            @(negedge clk); sda_drive_low = 1'b0; hp;
         end
      endtask

      // ---- the request loop ---------------------------------------------------
      initial begin
         scl_drive_low = 1'b0; sda_drive_low = 1'b0;
         done_count = 8'h00; busy = 1'b0;
         obs_addr_acked = 1'b0; obs_n_acked = 3'd0;
         obs_r0 = 8'h00; obs_r1 = 8'h00; obs_r2 = 8'h00;
         obs_stretched = 1'b0; obs_timeout = 1'b0;
      end

      always @(posedge clk) begin
         if (!rst_n) begin
            scl_drive_low <= 1'b0; sda_drive_low <= 1'b0;
            done_count <= 8'h00; busy <= 1'b0;
         end
      end

      always @(posedge req) begin
         busy = 1'b1;
         obs_addr_acked = 1'b0; obs_n_acked = 3'd0;
         obs_stretched = 1'b0; obs_timeout = 1'b0;

         if (req_restart) pulse_restart; else pulse_start;
         put_byte({req_addr, req_read}, a);
         obs_addr_acked = a;

         if (!req_read) begin
            if (req_len > 0) begin put_byte(req_d0, a); if (a) obs_n_acked = obs_n_acked + 3'd1; end
            if (req_len > 1) begin put_byte(req_d1, a); if (a) obs_n_acked = obs_n_acked + 3'd1; end
            if (req_len > 2) begin put_byte(req_d2, a); if (a) obs_n_acked = obs_n_acked + 3'd1; end
         end else begin
            // The controller acknowledges every byte except the last, which it NACKs to
            // tell the target to stop sourcing. Chapter 18.8's contract, from this side.
            if (req_len > 0) begin get_byte(req_len > 1, b); obs_r0 = b; end
            if (req_len > 1) begin get_byte(req_len > 2, b); obs_r1 = b; end
            if (req_len > 2) begin get_byte(1'b0,        b); obs_r2 = b; end
         end

         // The STOP is what ENDS a transfer. Declining to send it leaves the bus held and
         // the transfer open, to be ended by whatever framing event comes next -- normally
         // the repeated START of the following transfer.
         if (!req_hold) pulse_stop;
         busy = 1'b0;
         done_count = done_count + 8'd1;
      end

   endmodule

Two features of the interface are worth pointing at, because both were added for reasons found while building other chapters.

req_hold — decline to send the trailing STOP. Without it, req_restart was unreachable in any meaningful sense: a transfer that always ends in a STOP can only be followed by a plain START, and a "repeated START" after a STOP is indistinguishable from an ordinary one to every device on the bus. Chapter 20.3 found this while trying to write a test for framing that spans transfers. A controller that wants to change direction without releasing the bus has to be able to keep it, and that is a driver capability rather than a test trick.

STRETCH_TIMEOUT as a parameter — so that a bench can state how patient it intends to be, and so that the same driver can be used both where a long stretch is expected and where one is a fault.

7. Evidence

The driver that worked against every target except the slow one

Pitfall — SCL released, and the next bit clocked out regardless
Buggy Code
// A controller driver that works. It has been used for two years against four
// different targets and has never produced a wrong byte.
//
//    task put_bit(input bit_v);
//      scl_drive_low = 1'b1;  #HALF;
//      sda_drive_low = ~bit_v; #HALF;
//      scl_drive_low = 1'b0;  #HALF;     // release SCL
//      scl_drive_low = 1'b1;  #HALF;     // ... and pull it low again
//    endtask
//
// The fifth target stretches. Reads come back with bits from the wrong
// positions, intermittently, depending on how busy the target is.
//
// The driver never looks at scl_in. It releases SCL, waits HALF, and pulls it
// low again -- so while the target is holding SCL down, the driver completes a
// whole "bit period" during which SCL never actually rose. No edge occurred, so
// the target sampled nothing, and the driver moved on to the next bit.
//
// The data loss is silent. There is no error to report: from the driver's point
// of view every bit was sent.
Pitfall — every NACK reported as an acknowledge, and a passing NACK test
Buggy Code
// The acknowledge slot in a driver that has never failed a test:
//
//    task put_byte(input [7:0] d, output ackv);
//      for (k = 7; k >= 0; k = k - 1) put_bit(d[k]);
//      // ninth slot
//      scl_drive_low = 1'b1;  #HALF;
//      scl_drive_low = 1'b0;  #HALF;      // SCL released...
//      ackv = ~sda_in;                    // ... and SDA never released
//      scl_drive_low = 1'b1;  #HALF;
//    endtask
//
// SDA still carries bit 0 of the byte just sent. For any byte with bit 0 = 0 --
// half of all bytes, and every even address -- the driver is pulling SDA low
// itself and reads back ackv = 1.
//
// So: "the target acknowledged" is reported for a target that is absent, in
// reset, or a different device entirely. The environment's NACK test writes to
// an unused address, expects ackv = 0, gets 1... and was written to expect 1,
// because that is what the driver has always returned. The test documents the
// bug.

8. What 20.5 Settled

The driver translates intent into edges and owns nothing else. No expected values, no verdicts. obs_addr_acked is an observation from one position, and the same observation is correct behaviour in one test and a failure in another — which is exactly why the driver must not decide.

Protocol obligations are discharged structurally. One task moves a data bit, and in it SDA is assigned only after SCL is pulled low. Framing has its own tasks, which are the only code that changes SDA while SCL is high.

A release is a request; the line level is the result. The while loop that waits for the resolved SCL to rise is the whole of clock-stretch support, and it is bounded because a line that never rises is a real fault.

A bound needs a test on each side. A timeout alone demonstrates that the driver gives up. A legal stretch alone demonstrates that it waits. Only the pair demonstrates stretch support.

Handshake with a count, not a pulse. A missed pulse presents as a deadlock in the component that was working.

The next chapter builds the other active component — the one that has to behave like a real part, which means it has to be able to be unhelpful: refuse an address it could answer, stretch when nothing requires it, and NACK in the middle of a transfer. Chapter 20.6 — The Target Responder.

Continue learning

Related tutorials