Skip to content
VLSI Mentor

I²C · Module 8

The I²C Write Transaction End to End

Seven modules built the pieces. This one runs a write from START to STOP as a single continuous story, counts every bit on the wire, and builds the transaction-level sequencer that issues it — in SystemVerilog, Verilog and VHDL.

Every mechanism this course has built so far exists to serve two transactions. This chapter is the first of them.

A write is the simpler of the two, and it is simpler for one specific reason: the direction never changes. The master holds the transmitter role from the first bit of the address to the last bit of the payload, and the addressed slave holds the receiver role for the whole transfer. Nothing hands over. That single property is why a write is the right place to assemble the whole stack, and why Module 9's read — which does hand over, mid-frame, on a bus where both ends can pull low — is a harder chapter.

What is new here is not a mechanism. It is sequencing: the layer that decides which byte goes next, what to do with each answer, and when the transfer is over.

1. The Write, in One Sentence

The specification is unusually terse about the write, and the terseness is the point.

Three clauses, and each one is a design constraint rather than a description.

"Master-transmitter transmits to slave-receiver" fixes the roles for the whole transfer. Chapter 7.3 derived the general rule; for a write it collapses to something you can hold in your head — the master drives all eight data bits of every byte, and the slave drives every ninth bit.

"The transfer direction is not changed" is a prohibition, and it is the clause people misread. It does not say the direction cannot change on the bus; it says it cannot change within one addressing. To change direction you must re-address, which means a repeated START and a new address byte — exactly the mechanism Chapter 5.4 built. A write, as a frame, has one direction and one direction only.

"The slave receiver acknowledges each byte" is the clause that makes a write verifiable byte by byte. Every byte has an answer, so a master never has to guess whether a write landed. Chapter 7.4 established the asymmetry that follows: in a write, every acknowledge should be an ACK, and any NACK is information. In a read, the final NACK is normal. A write that ends with a NACK on its last byte did not complete.

2. The Frame, Event by Event

Here is a complete single-register write: device 0x48, register 0x10, value 0x2F. Nine events, and every one of them has a chapter behind it.

A sequence diagram with two actors, master and slave. The master sends a START condition, then the address byte 0x90, and the slave returns an acknowledge. The master sends the register pointer 0x10 and the slave acknowledges. The master sends the data byte 0x2F and the slave acknowledges. The master then sends a STOP condition. Every acknowledge travels from slave back to master and is drawn as a dashed return arrow.Write 0x2F to register 0x10 of device 0x48Master (transmitter)Slave 0x48 (receiver)S — START condition0x90 — address 0x48,WACK — pulls SDA low0x10 — registerpointerACK0x2F — data byteACKP — STOP condition
A complete single-register write. The master holds the transmitter role throughout; the slave answers every byte and drives nothing else.

Two things about that diagram are worth stating explicitly, because a sequence diagram hides them.

The dashed arrows are not messages. They are a single bit each, in the ninth clock slot of the byte above them, on the same two wires. Nothing separate happens. The diagram draws them as returns because that is what they mean, but on the wire the acknowledge is the ninth pulse of the byte it answers — there is no gap, no turnaround, no idle time.

The register pointer is not part of I²C. The address byte, the acknowledges and the framing are protocol. The byte 0x10 meaning "register 16" is a convention invented by that device's designer and written in its datasheet. I²C transports bytes; what the first payload byte means is entirely the device's business. This is the single most common source of confusion for engineers coming from a bus with an address phase — I²C's address byte addresses the device, never a location inside it.

3. Every Bit Accounted For

The chapter's title claims every bit is accounted for, so here is the accounting for the frame above.

segmentSCL pulsesdriven byestablished in
Snonemaster5.2
address byte 0x90 — 8 bits8master6.1
its acknowledge1slave 0x487.2
pointer byte 0x10 — 8 bits8master7.1
its acknowledge1slave 0x487.3
data byte 0x2F — 8 bits8master7.1
its acknowledge1slave 0x487.3
Pnonemaster5.3
total27

Twenty-seven clock pulses for two payload bytes. That number, and what it implies, is Chapter 8.3's entire subject.

Neither framing condition consumes a clock pulse. S and P are defined by SDA moving while SCL is high — they live in the gaps, not in the pulse count. This is why the pulse count of a frame is always an exact multiple of nine, and why a capture whose pulse count is not a multiple of nine contains either a truncated byte or a framing event you have not spotted.

Here is the first byte at bit resolution. Each interval is one bit, so each interval is one SCL pulse.

Address byte 0x90 — 1001 0000 — then the slave's ACK

9 cycles
Nine intervals, one per bit. SCL ticks once per interval. SDA carries one, zero, zero, one, zero, zero, zero, zero across the first eight intervals, most significant bit first, giving 0x90. The ninth interval is the acknowledge slot and SDA is low, driven by the slave.seven address bitsseven address bitsR/WR/WACKACKMSB of address 0x48MSB of address 0x48R/W = 0: writeR/W = 0: writeslave pulls SDA low: ACKslave pulls SDA low: ACKsclsda100100000drives SDAMMMMMMMMSt0t1t2t3t4t5t6t7t8
The address byte 0x90 and its acknowledge. Eight bits driven by the master, most significant first, then one bit driven by the slave.

The drives SDA row is the part worth staring at. It changes exactly once in the byte, at the boundary into the ninth interval, and it changes because of a rule that is not in the data — Chapter 7.2 established that the transmitter must release SDA before the ninth slot so the receiver can answer. Nothing in the byte's value says when to let go. It is the slot number.

4. Where a Write Can Fail

A write has exactly three outcomes that a sequencer must distinguish, and conflating any two of them produces a driver that reports the wrong fault.

outcomewhat the wire showswhat it meanswhat the master should do
completeevery byte ACKed, then Pthe whole payload landednothing
address NACKthe address byte NACKedno device at that address answeredthere is nothing to retry — abort
data NACKbyte k NACKed after the address ACKedthe device is there, and refused byte kretry is meaningful; bytes 0..k−1 landed

The distinction between the last two is the one that matters in the field, and it is the distinction a summary "write failed" return code destroys.

An address NACK is a wiring or configuration fault. Nobody claimed the address. Chapter 6.4 covered the causes: the device is unpowered, the strap pins select a different address, the address in the driver was not shifted, or the device is not on the bus at all. Retrying will produce the same result forever, which is why a sequencer that retries an address NACK turns a static fault into a busy loop.

A data NACK means the device is present and said no. Chapter 7.4 and §3.1.6 give the two reasons a receiver produces one: it got data it does not understand, or it cannot accept any more. Both are transfer-specific, so both may well succeed on a retry — after the device has serviced whatever occupied it, or with a payload it does understand.

And there is the number the caller actually needs, which is neither of the above:

How many bytes landed. A five-byte write that was NACKed on byte three wrote two bytes. Those two writes are not undone — I²C has no rollback and no transaction abort. Whatever registers they hit hold their new values, and a driver that reports only "failed" leaves the device in a state its caller believes it is not in. This is why the sequencer below exposes bytes_written as a first-class output rather than a debug counter.

5. Sequencing Is a Layer, Not a Detail

Everything in the previous four sections is already built. Chapter 5.5 emits framing, Chapter 7.1 transfers a byte, Chapter 7.2 owns the ninth slot. What no chapter has built is the thing that decides what happens next.

That layer has a clean contract, and defining it clean is most of the design:

  • it issues commands — send an S, send this byte, send a P — and never touches SDA or SCL;
  • it consumes completions — the S finished, the byte finished, here is the answer;
  • it holds the transaction state — which byte we are on, how many were accepted, what went wrong.

The payoff of that split is that the sequencer contains no timing at all. It has no idea what the SCL frequency is, no idea whether the slave stretched the clock, and no notion of a nanosecond. It is a pure transaction-level machine, and that is exactly the level a UVM sequence operates at — §9 makes that correspondence explicit.

6. The Write Sequencer in Three Languages

The design below is that layer. It is worth reading the state list before the code, because the state count is where the one non-obvious decision lives.

statewaiting forwhy it exists
WS_IDLEa request
WS_STARTframing_donethe S must complete before a byte may be sent
WS_ADDRbyte_donethe address byte is in flight; its answer decides everything
WS_DATAbyte_donea payload byte is in flight
WS_FETCHone clockthe payload source needs a cycle to present the next byte
WS_STOPframing_donethe P must complete before the transaction is done

WS_FETCH is the state that looks removable and is not. §6a defends it, and the mutation table in §8 shows exactly what disappears without it.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_write_sequencer.sv — the transaction-level master-transmitter sequencer
   // A master-transmitter's TRANSACTION-level sequencer. It issues framing and byte
   // commands to the lower layers built in Modules 5 and 7 and reacts to each
   // acknowledge; it contains no bit-level logic of its own.
   //
   // The specification's write format is one sentence: "Master-transmitter transmits
   // to slave-receiver. The transfer direction is not changed. The slave receiver
   // acknowledges each byte." This block is that sentence plus what to do when the
   // slave does not acknowledge.
   module i2c_write_sequencer #(
       parameter int LEN_W = 8
   )(
       input  logic clk,
       input  logic rst_n,

       // ---- transaction request ----
       input  logic             req,           // pulse: begin a write
       input  logic [6:0]       req_addr,      // seven-bit address, as the datasheet states it
       input  logic [LEN_W-1:0] req_len,       // data bytes; ZERO is a legal address-only probe

       // ---- commands to the framing sequencer (5.5) and byte engine (7.1) ----
       output logic             cmd_start,     // pulse: emit S
       output logic             cmd_stop,      // pulse: emit P
       output logic             cmd_byte,      // pulse: transmit one byte
       output logic [7:0]       cmd_byte_data,

       // ---- completions from those layers ----
       input  logic             framing_done,  // pulse: an S or P completed
       input  logic             byte_done,     // pulse: a byte AND its ninth slot completed
       input  logic             ack_received,  // the answer to the byte just sent

       // ---- payload source ----
       output logic [LEN_W-1:0] data_index,    // which payload byte is wanted
       input  logic [7:0]       data_in,

       // ---- status ----
       output logic             busy,
       output logic             done,          // pulse: the transaction has ended
       output logic             addr_nacked,   // nobody answered the address byte
       output logic             data_nacked,   // a slave refused a data byte
       output logic [LEN_W-1:0] bytes_written  // payload bytes actually acknowledged
   );
       typedef enum logic [2:0] {
           WS_IDLE,
           WS_START,     // waiting for the S to complete
           WS_ADDR,      // the address byte is in flight
           WS_DATA,      // a payload byte is in flight
           WS_FETCH,     // one cycle for the payload source to present the next byte
           WS_STOP       // waiting for the P to complete
       } state_e;

       state_e state;

       // The address byte is the seven-bit address with R/W = 0 appended -- one field
       // on the wire, exactly as Chapter 6.1 established.
       logic [7:0] addr_byte;
       assign addr_byte = {req_addr, 1'b0};

       always_ff @(posedge clk) begin
           if (!rst_n) begin
               state         <= WS_IDLE;
               cmd_start     <= 1'b0;
               cmd_stop      <= 1'b0;
               cmd_byte      <= 1'b0;
               cmd_byte_data <= 8'h00;
               data_index    <= '0;
               done          <= 1'b0;
               addr_nacked   <= 1'b0;
               data_nacked   <= 1'b0;
               bytes_written <= '0;
           end else begin
               // Commands are single-cycle pulses: the layers below count events.
               cmd_start <= 1'b0;
               cmd_stop  <= 1'b0;
               cmd_byte  <= 1'b0;
               done      <= 1'b0;

               case (state)
                   WS_IDLE:
                       if (req) begin
                           // A new transaction clears the previous one's verdict, so a
                           // consumer can never read a stale NACK as this transfer's.
                           addr_nacked   <= 1'b0;
                           data_nacked   <= 1'b0;
                           bytes_written <= '0;
                           data_index    <= '0;
                           cmd_start     <= 1'b1;
                           state         <= WS_START;
                       end

                   WS_START:
                       if (framing_done) begin
                           cmd_byte      <= 1'b1;
                           cmd_byte_data <= addr_byte;
                           state         <= WS_ADDR;
                       end

                   WS_ADDR:
                       if (byte_done) begin
                           if (!ack_received) begin
                               // Condition 1: nobody is there. Abort with a STOP -- the
                               // specification allows a STOP or a repeated START, and a
                               // write with no target has nothing to retry.
                               addr_nacked <= 1'b1;
                               cmd_stop    <= 1'b1;
                               state       <= WS_STOP;
                           end else if (req_len == '0) begin
                               // An ADDRESS-ONLY write. Not a degenerate case: this is
                               // exactly how an address scan probes for a device.
                               cmd_stop <= 1'b1;
                               state    <= WS_STOP;
                           end else begin
                               cmd_byte      <= 1'b1;
                               cmd_byte_data <= data_in;
                               state         <= WS_DATA;
                           end
                       end

                   WS_DATA:
                       if (byte_done) begin
                           if (!ack_received) begin
                               // Condition 3 or 4: the slave did not understand the byte
                               // or cannot take another. bytes_written deliberately does
                               // NOT count this one -- it was refused.
                               data_nacked <= 1'b1;
                               cmd_stop    <= 1'b1;
                               state       <= WS_STOP;
                           end else begin
                               bytes_written <= bytes_written + 1'b1;
                               if ((data_index + 1'b1) == req_len) begin
                                   cmd_stop <= 1'b1;
                                   state    <= WS_STOP;
                               end else begin
                                   // Advance the index and give the payload source ONE
                                   // CYCLE to present the new byte. data_in is a function
                                   // of data_index, so latching it in the same cycle as
                                   // the increment would re-send the byte just sent -- a
                                   // classic off-by-one that only a payload of DISTINCT
                                   // bytes can expose.
                                   data_index <= data_index + 1'b1;
                                   state      <= WS_FETCH;
                               end
                           end
                       end

                   WS_FETCH: begin
                       cmd_byte      <= 1'b1;
                       cmd_byte_data <= data_in;      // now indexed by the NEW data_index
                       state         <= WS_DATA;
                   end

                   WS_STOP:
                       if (framing_done) begin
                           done  <= 1'b1;
                           state <= WS_IDLE;
                       end

                   default: state <= WS_IDLE;
               endcase
           end
       end

       assign busy = (state != WS_IDLE);
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_write_sequencer_tb.sv — a full write, an address-only probe, and both NACK aborts
   module i2c_write_sequencer_tb;
       localparam int LEN_W = 8;

       logic clk = 1'b0, rst_n;
       logic req;
       logic [6:0] req_addr;
       logic [LEN_W-1:0] req_len;
       logic cmd_start, cmd_stop, cmd_byte;
       logic [7:0] cmd_byte_data;
       logic framing_done, byte_done, ack_received;
       logic [LEN_W-1:0] data_index;
       logic [7:0] data_in;
       logic busy, done, addr_nacked, data_nacked;
       logic [LEN_W-1:0] bytes_written;

       int errors = 0;

       i2c_write_sequencer #(.LEN_W(LEN_W)) dut (.*);

       always #5 clk = ~clk;
       initial begin #80000; $display("FAIL: watchdog expired"); $finish; end

       // ---- a payload the sequencer reads through data_index ----
       logic [7:0] payload [0:15];
       assign data_in = payload[data_index[3:0]];

       // ---- counts and a log of what was actually put on the wire ----
       int n_start = 0, n_stop = 0, n_byte = 0, n_done = 0;
       // Module scope: the completion wait below is count-based rather than level-based,
       // because `done` is still asserted at the negedge after it fires -- so a plain
       // `wait (done)` in the NEXT transaction returns immediately on the stale pulse.
       int base_done;
       logic [7:0] wire_log [0:31];
       int n_wire = 0;
       always @(posedge clk) if (rst_n) begin
           if (cmd_start) n_start++;
           if (cmd_stop)  n_stop++;
           if (done)      n_done++;
           if (cmd_byte) begin
               if (n_wire < 32) wire_log[n_wire] = cmd_byte_data;
               n_wire++;
               n_byte++;
           end
       end

       // ---- a model of the layers below: Module 5's framing sequencer and Module 7's
       //      byte engine. TWO independent processes, because a single always block with
       //      embedded waits can only service one command at a time and silently drops
       //      the next one -- which cost a debugging round during development.
       int      nack_on_byte;      // wire byte index the slave refuses (0 = the address)
       int      byte_seen;
       logic    model_en = 1'b0;
       // The model OWNS byte_seen and clears it on request. The stimulus could simply
       // assign it here, but VHDL permits only one driver per signal, so the VHDL
       // version must use a request -- and keeping all three testbenches structurally
       // identical is worth more than the one line it saves.
       logic    byte_rst = 1'b0;

       initial forever begin
           @(posedge clk);
           if (rst_n && model_en && (cmd_start || cmd_stop)) begin
               repeat (3) @(posedge clk);
               framing_done <= 1'b1; @(posedge clk); framing_done <= 1'b0;
           end
       end

       initial forever begin
           @(posedge clk);
           if (byte_rst) byte_seen = 0;
           else if (rst_n && model_en && cmd_byte) begin
               repeat (4) @(posedge clk);
               ack_received <= (byte_seen != nack_on_byte);
               byte_done    <= 1'b1; @(posedge clk); byte_done <= 1'b0;
               byte_seen++;
           end
       end

       task automatic run_write(input logic [6:0] a, input int len, input int nack_idx);
           nack_on_byte = nack_idx;
           byte_rst = 1'b1; @(negedge clk); byte_rst = 1'b0;
           n_wire   = 0;
           req_addr = a; req_len = len[LEN_W-1:0];
           base_done = n_done;
           @(negedge clk);              // let the request fields settle before asserting req
           req = 1'b1; @(negedge clk); req = 1'b0;
           wait (n_done == base_done + 1);
           @(negedge clk);
       endtask

       initial begin
           rst_n = 1'b0; req = 1'b0; req_addr = 7'h00; req_len = '0;
           framing_done = 1'b0; byte_done = 1'b0; ack_received = 1'b0;
           nack_on_byte = -1; byte_seen = 0;
           for (int i = 0; i < 16; i++) payload[i] = 8'hA0 + i[7:0];
           repeat (3) @(negedge clk);
           if (busy !== 1'b0) begin $display("FAIL: busy out of reset"); errors++; end
           rst_n = 1'b1; model_en = 1'b1; @(negedge clk);

           // ---- 1: a three-byte write, everything acknowledged.
           run_write(7'h48, 3, -1);
           if (n_start != 1 || n_stop != 1) begin
               $display("FAIL: expected one S and one P, saw %0d and %0d", n_start, n_stop); errors++; end
           if (n_byte != 4) begin
               $display("FAIL: a 3-byte write must put 4 bytes on the wire, saw %0d", n_byte); errors++; end
           if (wire_log[0] !== 8'h90) begin
               $display("FAIL: address byte was 0x%02h, expected 0x90 (0x48 << 1, write)", wire_log[0]); errors++; end
           if (wire_log[1] !== 8'hA0 || wire_log[2] !== 8'hA1 || wire_log[3] !== 8'hA2) begin
               $display("FAIL: payload was 0x%02h,0x%02h,0x%02h", wire_log[1], wire_log[2], wire_log[3]); errors++; end
           if (bytes_written !== 8'd3) begin
               $display("FAIL: bytes_written = %0d, expected 3", bytes_written); errors++; end
           if (addr_nacked !== 1'b0 || data_nacked !== 1'b0) begin
               $display("FAIL: spurious NACK report on a clean write"); errors++; end

           // ---- 2: the ADDRESS is NACKed. The transfer must abort immediately, with a
           //         STOP and NO payload bytes on the wire.
           n_start = 0; n_stop = 0; n_byte = 0;
           run_write(7'h7A, 3, 0);
           if (addr_nacked !== 1'b1) begin $display("FAIL: an unanswered address was not reported"); errors++; end
           if (data_nacked !== 1'b0) begin $display("FAIL: data_nacked set on an address NACK"); errors++; end
           if (n_byte != 1) begin
               $display("FAIL: after an address NACK, %0d bytes went out; expected only the address", n_byte);
               errors++; end
           if (bytes_written !== 8'd0) begin
               $display("FAIL: bytes_written = %0d after an address NACK", bytes_written); errors++; end
           if (n_stop != 1) begin $display("FAIL: an aborted write must still issue a STOP"); errors++; end

           // ---- 3: the SECOND payload byte is refused (wire byte index 2).
           n_start = 0; n_stop = 0; n_byte = 0;
           run_write(7'h48, 4, 2);
           if (data_nacked !== 1'b1) begin $display("FAIL: a refused data byte was not reported"); errors++; end
           if (addr_nacked !== 1'b0) begin $display("FAIL: addr_nacked set on a data NACK"); errors++; end
           // One payload byte was accepted before the refusal; the refused one is NOT counted.
           if (bytes_written !== 8'd1) begin
               $display("FAIL: bytes_written = %0d, expected 1 (the refused byte is not written)", bytes_written);
               errors++; end
           if (n_byte != 3) begin
               $display("FAIL: %0d bytes on the wire, expected 3 (addr + accepted + refused)", n_byte);
               errors++; end
           if (n_stop != 1) begin $display("FAIL: no STOP after a data NACK"); errors++; end

           // ---- 4: an ADDRESS-ONLY write. This is how an address scan probes, so it
           //         must be a first-class case rather than a degenerate one.
           n_start = 0; n_stop = 0; n_byte = 0;
           run_write(7'h50, 0, -1);
           if (n_byte != 1) begin
               $display("FAIL: an address-only write put %0d bytes on the wire", n_byte); errors++; end
           if (addr_nacked !== 1'b0) begin $display("FAIL: probe of a present device reported a NACK"); errors++; end
           if (bytes_written !== 8'd0) begin
               $display("FAIL: an address-only write wrote %0d payload bytes", bytes_written); errors++; end
           if (n_start != 1 || n_stop != 1) begin
               $display("FAIL: a probe must still be framed by one S and one P"); errors++; end

           // ---- 5: an address-only probe of an ABSENT device -- the scan's negative case.
           run_write(7'h7B, 0, 0);
           if (addr_nacked !== 1'b1) begin
               $display("FAIL: probe of an absent device did not report a NACK"); errors++; end

           // ---- 6: back-to-back transactions must not inherit a verdict. This follows
           //         a NACKed probe with a clean write.
           n_start = 0; n_stop = 0; n_byte = 0;
           run_write(7'h48, 2, -1);
           if (addr_nacked !== 1'b0 || data_nacked !== 1'b0) begin
               $display("FAIL: a clean write inherited the previous transaction's NACK"); errors++; end
           if (bytes_written !== 8'd2) begin
               $display("FAIL: bytes_written = %0d on the follow-up write", bytes_written); errors++; end

           if (n_done != 6) begin
               $display("FAIL: %0d done pulses over 6 transactions", n_done); errors++; end
           if (busy !== 1'b0) begin $display("FAIL: still busy after the last transaction"); errors++; end

           if (errors == 0)
               $display("PASS: address byte shifted, payload in order, NACK phase reported, probe and abort framed");
           else $display("FAIL: %0d error(s)", errors);
           $finish;
       end
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_write_sequencer.v — the same sequencer in Verilog-2001
   // A master-transmitter's TRANSACTION-level sequencer. It issues framing and byte
   // commands to the lower layers built in Modules 5 and 7 and reacts to each
   // acknowledge; it contains no bit-level logic of its own.
   //
   // The specification's write format is one sentence: "Master-transmitter transmits
   // to slave-receiver. The transfer direction is not changed. The slave receiver
   // acknowledges each byte." This block is that sentence plus what to do when the
   // slave does not acknowledge.
   module i2c_write_sequencer #(
       parameter integer LEN_W = 8
   )(
       input  wire clk,
       input  wire rst_n,

       // ---- transaction request ----
       input  wire              req,           // pulse: begin a write
       input  wire  [6:0]       req_addr,      // seven-bit address, as the datasheet states it
       input  wire  [LEN_W-1:0] req_len,       // data bytes; ZERO is a legal address-only probe

       // ---- commands to the framing sequencer (5.5) and byte engine (7.1) ----
       output reg               cmd_start,     // pulse: emit S
       output reg               cmd_stop,      // pulse: emit P
       output reg               cmd_byte,      // pulse: transmit one byte
       output reg   [7:0]       cmd_byte_data,

       // ---- completions from those layers ----
       input  wire              framing_done,  // pulse: an S or P completed
       input  wire              byte_done,     // pulse: a byte AND its ninth slot completed
       input  wire              ack_received,  // the answer to the byte just sent

       // ---- payload source ----
       output reg   [LEN_W-1:0] data_index,    // which payload byte is wanted
       input  wire  [7:0]       data_in,

       // ---- status ----
       output wire              busy,
       output reg               done,          // pulse: the transaction has ended
       output reg               addr_nacked,   // nobody answered the address byte
       output reg               data_nacked,   // a slave refused a data byte
       output reg   [LEN_W-1:0] bytes_written  // payload bytes actually acknowledged
   );
       localparam WS_IDLE  = 3'd0;
       localparam WS_START = 3'd1;   // waiting for the S to complete
       localparam WS_ADDR  = 3'd2;   // the address byte is in flight
       localparam WS_DATA  = 3'd3;   // a payload byte is in flight
       localparam WS_FETCH = 3'd4;   // one cycle for the payload source to present the next byte
       localparam WS_STOP  = 3'd5;   // waiting for the P to complete

       reg [2:0] state;

       // The address byte is the seven-bit address with R/W = 0 appended -- one field
       // on the wire, exactly as Chapter 6.1 established.
       wire [7:0] addr_byte = {req_addr, 1'b0};

       always @(posedge clk) begin
           if (!rst_n) begin
               state         <= WS_IDLE;
               cmd_start     <= 1'b0;
               cmd_stop      <= 1'b0;
               cmd_byte      <= 1'b0;
               cmd_byte_data <= 8'h00;
               data_index    <= {LEN_W{1'b0}};
               done          <= 1'b0;
               addr_nacked   <= 1'b0;
               data_nacked   <= 1'b0;
               bytes_written <= {LEN_W{1'b0}};
           end else begin
               // Commands are single-cycle pulses: the layers below count events.
               cmd_start <= 1'b0;
               cmd_stop  <= 1'b0;
               cmd_byte  <= 1'b0;
               done      <= 1'b0;

               case (state)
                   WS_IDLE:
                       if (req) begin
                           // A new transaction clears the previous one's verdict, so a
                           // consumer can never read a stale NACK as this transfer's.
                           addr_nacked   <= 1'b0;
                           data_nacked   <= 1'b0;
                           bytes_written <= {LEN_W{1'b0}};
                           data_index    <= {LEN_W{1'b0}};
                           cmd_start     <= 1'b1;
                           state         <= WS_START;
                       end

                   WS_START:
                       if (framing_done) begin
                           cmd_byte      <= 1'b1;
                           cmd_byte_data <= addr_byte;
                           state         <= WS_ADDR;
                       end

                   WS_ADDR:
                       if (byte_done) begin
                           if (!ack_received) begin
                               // Condition 1: nobody is there. Abort with a STOP -- the
                               // specification allows a STOP or a repeated START, and a
                               // write with no target has nothing to retry.
                               addr_nacked <= 1'b1;
                               cmd_stop    <= 1'b1;
                               state       <= WS_STOP;
                           end else if (req_len == {LEN_W{1'b0}}) begin
                               // An ADDRESS-ONLY write. Not a degenerate case: this is
                               // exactly how an address scan probes for a device.
                               cmd_stop <= 1'b1;
                               state    <= WS_STOP;
                           end else begin
                               cmd_byte      <= 1'b1;
                               cmd_byte_data <= data_in;
                               state         <= WS_DATA;
                           end
                       end

                   WS_DATA:
                       if (byte_done) begin
                           if (!ack_received) begin
                               // Condition 3 or 4: the slave did not understand the byte
                               // or cannot take another. bytes_written deliberately does
                               // NOT count this one -- it was refused.
                               data_nacked <= 1'b1;
                               cmd_stop    <= 1'b1;
                               state       <= WS_STOP;
                           end else begin
                               bytes_written <= bytes_written + 1'b1;
                               if ((data_index + 1'b1) == req_len) begin
                                   cmd_stop <= 1'b1;
                                   state    <= WS_STOP;
                               end else begin
                                   // Advance the index and give the payload source ONE
                                   // CYCLE to present the new byte. data_in is a function
                                   // of data_index, so latching it in the same cycle as
                                   // the increment would re-send the byte just sent -- a
                                   // classic off-by-one that only a payload of DISTINCT
                                   // bytes can expose.
                                   data_index <= data_index + 1'b1;
                                   state      <= WS_FETCH;
                               end
                           end
                       end

                   WS_FETCH: begin
                       cmd_byte      <= 1'b1;
                       cmd_byte_data <= data_in;      // now indexed by the NEW data_index
                       state         <= WS_DATA;
                   end

                   WS_STOP:
                       if (framing_done) begin
                           done  <= 1'b1;
                           state <= WS_IDLE;
                       end

                   default: state <= WS_IDLE;
               endcase
           end
       end

       assign busy = (state != WS_IDLE);
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_write_sequencer_tb.v — the Verilog testbench, structurally identical
   module i2c_write_sequencer_tb;
       localparam integer LEN_W = 8;

       reg clk, rst_n;
       reg req;
       reg [6:0] req_addr;
       reg [LEN_W-1:0] req_len;
       wire cmd_start, cmd_stop, cmd_byte;
       wire [7:0] cmd_byte_data;
       reg framing_done, byte_done, ack_received;
       wire [LEN_W-1:0] data_index;
       wire [7:0] data_in;
       wire busy, done, addr_nacked, data_nacked;
       wire [LEN_W-1:0] bytes_written;

       integer errors, n_start, n_stop, n_byte, n_done, n_wire, base_done;
       integer nack_on_byte, byte_seen, i;

       i2c_write_sequencer #(.LEN_W(LEN_W)) dut (.clk(clk), .rst_n(rst_n), .req(req),
           .req_addr(req_addr), .req_len(req_len), .cmd_start(cmd_start), .cmd_stop(cmd_stop),
           .cmd_byte(cmd_byte), .cmd_byte_data(cmd_byte_data), .framing_done(framing_done),
           .byte_done(byte_done), .ack_received(ack_received), .data_index(data_index),
           .data_in(data_in), .busy(busy), .done(done), .addr_nacked(addr_nacked),
           .data_nacked(data_nacked), .bytes_written(bytes_written));

       initial clk = 1'b0;
       always #5 clk = ~clk;
       initial begin #80000; $display("FAIL: watchdog expired"); $finish; end

       // ---- a payload the sequencer reads through data_index ----
       reg [7:0] payload [0:15];
       assign data_in = payload[data_index[3:0]];

       // ---- counts and a log of what was actually put on the wire ----
       // The completion wait below is count-based rather than level-based, because done
       // is still asserted at the negedge after it fires -- so a plain wait on the level
       // would return immediately on the previous transaction's stale pulse.
       reg [7:0] wire_log [0:31];
       always @(posedge clk) if (rst_n) begin
           if (cmd_start) n_start = n_start + 1;
           if (cmd_stop)  n_stop = n_stop + 1;
           if (done)      n_done = n_done + 1;
           if (cmd_byte) begin
               if (n_wire < 32) wire_log[n_wire] = cmd_byte_data;
               n_wire = n_wire + 1;
               n_byte = n_byte + 1;
           end
       end

       // ---- a model of the layers below: Module 5's framing sequencer and Module 7's
       //      byte engine. TWO independent processes, because a single always block with
       //      embedded waits can only service one command at a time and silently drops
       //      the next one -- which cost a debugging round during development.
       reg model_en;
       // The model OWNS byte_seen and clears it on request, matching the VHDL version,
       // where one driver per signal is a language rule rather than a style choice.
       reg byte_rst;

       initial forever begin
           @(posedge clk);
           if (rst_n && model_en && (cmd_start || cmd_stop)) begin
               repeat (3) @(posedge clk);
               framing_done <= 1'b1; @(posedge clk); framing_done <= 1'b0;
           end
       end

       initial forever begin
           @(posedge clk);
           if (byte_rst) byte_seen = 0;
           else if (rst_n && model_en && cmd_byte) begin
               repeat (4) @(posedge clk);
               ack_received <= (byte_seen != nack_on_byte);
               byte_done    <= 1'b1; @(posedge clk); byte_done <= 1'b0;
               byte_seen = byte_seen + 1;
           end
       end

       task run_write; input [6:0] a; input integer len; input integer nack_idx; begin
           nack_on_byte = nack_idx;
           byte_rst = 1'b1; @(negedge clk); byte_rst = 1'b0;
           n_wire   = 0;
           req_addr = a; req_len = len[LEN_W-1:0];
           base_done = n_done;
           @(negedge clk);              // let the request fields settle before asserting req
           req = 1'b1; @(negedge clk); req = 1'b0;
           wait (n_done == base_done + 1);
           @(negedge clk);
       end endtask

       initial begin
           errors = 0; n_start = 0; n_stop = 0; n_byte = 0; n_done = 0; n_wire = 0;
           nack_on_byte = -1; byte_seen = 0; model_en = 1'b0; byte_rst = 1'b0;
           rst_n = 1'b0; req = 1'b0; req_addr = 7'h00; req_len = {LEN_W{1'b0}};
           framing_done = 1'b0; byte_done = 1'b0; ack_received = 1'b0;
           nack_on_byte = -1; byte_seen = 0;
           for (i = 0; i < 16; i = i + 1) payload[i] = 8'hA0 + i[7:0];
           repeat (3) @(negedge clk);
           if (busy !== 1'b0) begin $display("FAIL: busy out of reset"); errors = errors + 1; end
           rst_n = 1'b1; model_en = 1'b1; @(negedge clk);

           // ---- 1: a three-byte write, everything acknowledged.
           run_write(7'h48, 3, -1);
           if (n_start != 1 || n_stop != 1) begin
               $display("FAIL: expected one S and one P, saw %0d and %0d", n_start, n_stop); errors = errors + 1; end
           if (n_byte != 4) begin
               $display("FAIL: a 3-byte write must put 4 bytes on the wire, saw %0d", n_byte); errors = errors + 1; end
           if (wire_log[0] !== 8'h90) begin
               $display("FAIL: address byte was 0x%02h, expected 0x90 (0x48 << 1, write)", wire_log[0]); errors = errors + 1; end
           if (wire_log[1] !== 8'hA0 || wire_log[2] !== 8'hA1 || wire_log[3] !== 8'hA2) begin
               $display("FAIL: payload was 0x%02h,0x%02h,0x%02h", wire_log[1], wire_log[2], wire_log[3]); errors = errors + 1; end
           if (bytes_written !== 8'd3) begin
               $display("FAIL: bytes_written = %0d, expected 3", bytes_written); errors = errors + 1; end
           if (addr_nacked !== 1'b0 || data_nacked !== 1'b0) begin
               $display("FAIL: spurious NACK report on a clean write"); errors = errors + 1; end

           // ---- 2: the ADDRESS is NACKed. The transfer must abort immediately, with a
           //         STOP and NO payload bytes on the wire.
           n_start = 0; n_stop = 0; n_byte = 0;
           run_write(7'h7A, 3, 0);
           if (addr_nacked !== 1'b1) begin $display("FAIL: an unanswered address was not reported"); errors = errors + 1; end
           if (data_nacked !== 1'b0) begin $display("FAIL: data_nacked set on an address NACK"); errors = errors + 1; end
           if (n_byte != 1) begin
               $display("FAIL: after an address NACK, %0d bytes went out; expected only the address", n_byte);
               errors = errors + 1; end
           if (bytes_written !== 8'd0) begin
               $display("FAIL: bytes_written = %0d after an address NACK", bytes_written); errors = errors + 1; end
           if (n_stop != 1) begin $display("FAIL: an aborted write must still issue a STOP"); errors = errors + 1; end

           // ---- 3: the SECOND payload byte is refused (wire byte index 2).
           n_start = 0; n_stop = 0; n_byte = 0;
           run_write(7'h48, 4, 2);
           if (data_nacked !== 1'b1) begin $display("FAIL: a refused data byte was not reported"); errors = errors + 1; end
           if (addr_nacked !== 1'b0) begin $display("FAIL: addr_nacked set on a data NACK"); errors = errors + 1; end
           // One payload byte was accepted before the refusal; the refused one is NOT counted.
           if (bytes_written !== 8'd1) begin
               $display("FAIL: bytes_written = %0d, expected 1 (the refused byte is not written)", bytes_written);
               errors = errors + 1; end
           if (n_byte != 3) begin
               $display("FAIL: %0d bytes on the wire, expected 3 (addr + accepted + refused)", n_byte);
               errors = errors + 1; end
           if (n_stop != 1) begin $display("FAIL: no STOP after a data NACK"); errors = errors + 1; end

           // ---- 4: an ADDRESS-ONLY write. This is how an address scan probes, so it
           //         must be a first-class case rather than a degenerate one.
           n_start = 0; n_stop = 0; n_byte = 0;
           run_write(7'h50, 0, -1);
           if (n_byte != 1) begin
               $display("FAIL: an address-only write put %0d bytes on the wire", n_byte); errors = errors + 1; end
           if (addr_nacked !== 1'b0) begin $display("FAIL: probe of a present device reported a NACK"); errors = errors + 1; end
           if (bytes_written !== 8'd0) begin
               $display("FAIL: an address-only write wrote %0d payload bytes", bytes_written); errors = errors + 1; end
           if (n_start != 1 || n_stop != 1) begin
               $display("FAIL: a probe must still be framed by one S and one P"); errors = errors + 1; end

           // ---- 5: an address-only probe of an ABSENT device -- the scan's negative case.
           run_write(7'h7B, 0, 0);
           if (addr_nacked !== 1'b1) begin
               $display("FAIL: probe of an absent device did not report a NACK"); errors = errors + 1; end

           // ---- 6: back-to-back transactions must not inherit a verdict. This follows
           //         a NACKed probe with a clean write.
           n_start = 0; n_stop = 0; n_byte = 0;
           run_write(7'h48, 2, -1);
           if (addr_nacked !== 1'b0 || data_nacked !== 1'b0) begin
               $display("FAIL: a clean write inherited the previous transaction's NACK"); errors = errors + 1; end
           if (bytes_written !== 8'd2) begin
               $display("FAIL: bytes_written = %0d on the follow-up write", bytes_written); errors = errors + 1; end

           if (n_done != 6) begin
               $display("FAIL: %0d done pulses over 6 transactions", n_done); errors = errors + 1; end
           if (busy !== 1'b0) begin $display("FAIL: still busy after the last transaction"); errors = errors + 1; end

           if (errors == 0)
               $display("PASS: address byte shifted, payload in order, NACK phase reported, probe and abort framed");
           else $display("FAIL: %0d error(s)", errors);
           $finish;
       end
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_write_sequencer.vhd — the same sequencer in VHDL
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   -- A master-transmitter's TRANSACTION-level sequencer. It issues framing and byte
   -- commands to the lower layers built in Modules 5 and 7 and reacts to each
   -- acknowledge; it contains no bit-level logic of its own.
   entity i2c_write_sequencer is
       generic (
           LEN_W : positive := 8
       );
       port (
           clk   : in std_logic;
           rst_n : in std_logic;

           -- transaction request
           req      : in std_logic;                              -- pulse: begin a write
           req_addr : in std_logic_vector(6 downto 0);           -- seven-bit address
           req_len  : in unsigned(LEN_W - 1 downto 0);           -- ZERO is a legal probe

           -- commands to the framing sequencer (5.5) and byte engine (7.1)
           cmd_start     : out std_logic;
           cmd_stop      : out std_logic;
           cmd_byte      : out std_logic;
           cmd_byte_data : out std_logic_vector(7 downto 0);

           -- completions from those layers
           framing_done : in std_logic;
           byte_done    : in std_logic;
           ack_received : in std_logic;

           -- payload source
           data_index : out unsigned(LEN_W - 1 downto 0);
           data_in    : in  std_logic_vector(7 downto 0);

           -- status
           busy          : out std_logic;
           done          : out std_logic;
           addr_nacked   : out std_logic;
           data_nacked   : out std_logic;
           bytes_written : out unsigned(LEN_W - 1 downto 0)
       );
   end entity;

   architecture rtl of i2c_write_sequencer is
       type state_t is (
           WS_IDLE,
           WS_START,     -- waiting for the S to complete
           WS_ADDR,      -- the address byte is in flight
           WS_DATA,      -- a payload byte is in flight
           WS_FETCH,     -- one cycle for the payload source to present the next byte
           WS_STOP       -- waiting for the P to complete
       );
       signal state : state_t := WS_IDLE;

       constant ZERO : unsigned(LEN_W - 1 downto 0) := (others => '0');
       signal idx    : unsigned(LEN_W - 1 downto 0) := (others => '0');
       signal nwr    : unsigned(LEN_W - 1 downto 0) := (others => '0');

       -- The address byte is the seven-bit address with R/W = 0 appended -- one field
       -- on the wire, exactly as Chapter 6.1 established.
       signal addr_byte : std_logic_vector(7 downto 0);
   begin
       addr_byte     <= req_addr & '0';
       data_index    <= idx;
       bytes_written <= nwr;
       busy          <= '0' when state = WS_IDLE else '1';

       process (clk)
       begin
           if rising_edge(clk) then
               if rst_n = '0' then
                   state         <= WS_IDLE;
                   cmd_start     <= '0';
                   cmd_stop      <= '0';
                   cmd_byte      <= '0';
                   cmd_byte_data <= (others => '0');
                   idx           <= (others => '0');
                   done          <= '0';
                   addr_nacked   <= '0';
                   data_nacked   <= '0';
                   nwr           <= (others => '0');
               else
                   -- Commands are single-cycle pulses: the layers below count events.
                   cmd_start <= '0';
                   cmd_stop  <= '0';
                   cmd_byte  <= '0';
                   done      <= '0';

                   case state is
                       when WS_IDLE =>
                           if req = '1' then
                               -- A new transaction clears the previous verdict.
                               addr_nacked <= '0';
                               data_nacked <= '0';
                               nwr         <= (others => '0');
                               idx         <= (others => '0');
                               cmd_start   <= '1';
                               state       <= WS_START;
                           end if;

                       when WS_START =>
                           if framing_done = '1' then
                               cmd_byte      <= '1';
                               cmd_byte_data <= addr_byte;
                               state         <= WS_ADDR;
                           end if;

                       when WS_ADDR =>
                           if byte_done = '1' then
                               if ack_received = '0' then
                                   -- Condition 1: nobody is there. Abort with a STOP.
                                   addr_nacked <= '1';
                                   cmd_stop    <= '1';
                                   state       <= WS_STOP;
                               elsif req_len = ZERO then
                                   -- An ADDRESS-ONLY write: how an address scan probes.
                                   cmd_stop <= '1';
                                   state    <= WS_STOP;
                               else
                                   cmd_byte      <= '1';
                                   cmd_byte_data <= data_in;
                                   state         <= WS_DATA;
                               end if;
                           end if;

                       when WS_DATA =>
                           if byte_done = '1' then
                               if ack_received = '0' then
                                   -- Condition 3 or 4. The refused byte is NOT counted.
                                   data_nacked <= '1';
                                   cmd_stop    <= '1';
                                   state       <= WS_STOP;
                               else
                                   nwr <= nwr + 1;
                                   if (idx + 1) = req_len then
                                       cmd_stop <= '1';
                                       state    <= WS_STOP;
                                   else
                                       -- Advance the index and give the payload source ONE
                                       -- CYCLE to present the new byte: data_in is a
                                       -- function of data_index, so latching it in the same
                                       -- cycle would re-send the byte just sent.
                                       idx   <= idx + 1;
                                       state <= WS_FETCH;
                                   end if;
                               end if;
                           end if;

                       when WS_FETCH =>
                           cmd_byte      <= '1';
                           cmd_byte_data <= data_in;    -- now indexed by the NEW idx
                           state         <= WS_DATA;

                       when WS_STOP =>
                           if framing_done = '1' then
                               done  <= '1';
                               state <= WS_IDLE;
                           end if;
                   end case;
               end if;
           end if;
       end process;
   end architecture;
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_write_sequencer_tb.vhd — the VHDL testbench, with a watchdog on the whole transaction
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   entity i2c_write_sequencer_tb is
   end entity;

   architecture sim of i2c_write_sequencer_tb is
       constant LEN_W : positive := 8;

       signal clk          : std_logic := '0';
       signal rst_n        : std_logic := '0';
       signal req          : std_logic := '0';
       signal req_addr     : std_logic_vector(6 downto 0) := (others => '0');
       signal req_len      : unsigned(LEN_W - 1 downto 0) := (others => '0');
       signal cmd_start, cmd_stop, cmd_byte : std_logic;
       signal cmd_byte_data : std_logic_vector(7 downto 0);
       signal framing_done : std_logic := '0';
       signal byte_done    : std_logic := '0';
       signal ack_received : std_logic := '0';
       signal data_index   : unsigned(LEN_W - 1 downto 0);
       signal data_in      : std_logic_vector(7 downto 0);
       signal busy, done, addr_nacked, data_nacked : std_logic;
       signal bytes_written : unsigned(LEN_W - 1 downto 0);

       -- a payload the sequencer reads through data_index
       type pay_t is array (0 to 15) of std_logic_vector(7 downto 0);
       signal payload : pay_t;

       -- counts and a log of what was actually put on the wire
       signal n_start, n_stop, n_byte, n_done, n_wire : natural := 0;
       type log_t is array (0 to 31) of std_logic_vector(7 downto 0);
       signal wire_log : log_t := (others => (others => '0'));

       signal nack_on_byte : integer := -1;
       -- VHDL permits ONE driver per signal, so the stimulus cannot clear a counter that
       -- a model process owns. It asks instead, with a request signal it alone drives,
       -- and the owning process does the clearing. The Verilog testbenches simply assign
       -- from both places; this is a real cross-language difference, not a translation
       -- artefact, and the VHDL form is the one where the rule is checked.
       signal byte_rst     : std_logic := '0';
       signal byte_seen    : natural := 0;
       signal model_en     : std_logic := '0';
       signal test_done    : std_logic := '0';
   begin
       dut : entity work.i2c_write_sequencer
           generic map (LEN_W => LEN_W)
           port map (clk => clk, rst_n => rst_n, req => req, req_addr => req_addr,
                     req_len => req_len, cmd_start => cmd_start, cmd_stop => cmd_stop,
                     cmd_byte => cmd_byte, cmd_byte_data => cmd_byte_data,
                     framing_done => framing_done, byte_done => byte_done,
                     ack_received => ack_received, data_index => data_index,
                     data_in => data_in, busy => busy, done => done,
                     addr_nacked => addr_nacked, data_nacked => data_nacked,
                     bytes_written => bytes_written);

       data_in <= payload(to_integer(data_index(3 downto 0)));

       clk <= not clk after 5 ns;

       watchdog : process
       begin
           wait for 80 us;
           if test_done = '0' then
               report "watchdog expired -- the design never reached the expected state"
                   severity failure;
           end if;
           wait;
       end process;

       counters : process (clk)
       begin
           if rising_edge(clk) then
               if rst_n = '1' then
                   if cmd_start = '1' then n_start <= n_start + 1; end if;
                   if cmd_stop  = '1' then n_stop  <= n_stop  + 1; end if;
                   if done      = '1' then n_done  <= n_done  + 1; end if;
                   if cmd_byte  = '1' then
                       if n_wire < 32 then wire_log(n_wire) <= cmd_byte_data; end if;
                       n_wire <= n_wire + 1;
                       n_byte <= n_byte + 1;
                   end if;
               end if;
           end if;
       end process;

       -- A model of the layers below: Module 5's framing sequencer and Module 7's byte
       -- engine. TWO independent processes, because one process with embedded waits can
       -- only service a single command at a time and silently drops the next.
       framing_model : process
       begin
           wait until rising_edge(clk);
           if rst_n = '1' and model_en = '1' and (cmd_start = '1' or cmd_stop = '1') then
               for i in 1 to 3 loop wait until rising_edge(clk); end loop;
               framing_done <= '1';
               wait until rising_edge(clk);
               framing_done <= '0';
           end if;
       end process;

       byte_model : process
       begin
           wait until rising_edge(clk);
           if byte_rst = '1' then
               byte_seen <= 0;
           elsif rst_n = '1' and model_en = '1' and cmd_byte = '1' then
               for i in 1 to 4 loop wait until rising_edge(clk); end loop;
               if byte_seen = nack_on_byte then ack_received <= '0';
               else                             ack_received <= '1'; end if;
               byte_done <= '1';
               wait until rising_edge(clk);
               byte_done <= '0';
               byte_seen <= byte_seen + 1;
           end if;
       end process;

       stim : process
           variable errs : natural := 0;
           variable base : natural := 0;
           variable b_st, b_sp, b_by, b_wire : natural := 0;

           procedure waitn (n : in positive) is
           begin
               for i in 1 to n loop wait until falling_edge(clk); end loop;
           end procedure;

           -- The completion wait is COUNT-based, not level-based: done is still asserted
           -- at the negedge after it fires, so waiting on the level would return at once
           -- on the previous transaction's stale pulse.
           procedure run_write (a : in std_logic_vector(6 downto 0);
                                len : in natural; nack_idx : in integer) is
           begin
               nack_on_byte <= nack_idx;
               byte_rst     <= '1'; waitn(1); byte_rst <= '0';
               req_addr <= a;
               req_len  <= to_unsigned(len, LEN_W);
               -- Snapshot every counter this transaction will be measured against,
               -- because the stimulus does not own them and cannot zero them.
               base   := n_done;
               b_st   := n_start;
               b_sp   := n_stop;
               b_by   := n_byte;
               b_wire := n_wire;
               waitn(1);
               req <= '1'; waitn(1); req <= '0';
               wait until n_done = base + 1;
               waitn(1);
           end procedure;
       begin
           waitn(3);
           if busy /= '0' then
               report "busy out of reset" severity error; errs := errs + 1; end if;
           for i in 0 to 15 loop
               payload(i) <= std_logic_vector(to_unsigned(160 + i, 8));
           end loop;
           rst_n <= '1'; model_en <= '1'; waitn(1);

           -- 1: a three-byte write, everything acknowledged.
           run_write("1001000", 3, -1);
           if (n_start - b_st) /= 1 or (n_stop - b_sp) /= 1 then
               report "expected one S and one P" severity error; errs := errs + 1; end if;
           if (n_byte - b_by) /= 4 then
               report "a 3-byte write must put 4 bytes on the wire" severity error; errs := errs + 1; end if;
           if wire_log(b_wire) /= x"90" then
               report "address byte wrong: expected 0x90 (0x48 shifted, write)" severity error;
               errs := errs + 1; end if;
           if wire_log(b_wire+1) /= x"A0" or wire_log(b_wire+2) /= x"A1"
              or wire_log(b_wire+3) /= x"A2" then
               report "payload bytes wrong or out of order" severity error; errs := errs + 1; end if;
           if bytes_written /= 3 then
               report "bytes_written wrong" severity error; errs := errs + 1; end if;
           if addr_nacked /= '0' or data_nacked /= '0' then
               report "spurious NACK report on a clean write" severity error; errs := errs + 1; end if;

           -- 2: the ADDRESS is NACKed -- abort at once, no payload on the wire.
           run_write("1111010", 3, 0);
           if addr_nacked /= '1' then
               report "an unanswered address was not reported" severity error; errs := errs + 1; end if;
           if data_nacked /= '0' then
               report "data_nacked set on an address NACK" severity error; errs := errs + 1; end if;
           if (n_byte - b_by) /= 1 then
               report "payload went out after an address NACK" severity error; errs := errs + 1; end if;
           if bytes_written /= 0 then
               report "bytes_written nonzero after an address NACK" severity error; errs := errs + 1; end if;
           if (n_stop - b_sp) /= 1 then
               report "an aborted write must still issue a STOP" severity error; errs := errs + 1; end if;

           -- 3: the SECOND payload byte is refused (wire byte index 2).
           run_write("1001000", 4, 2);
           if data_nacked /= '1' then
               report "a refused data byte was not reported" severity error; errs := errs + 1; end if;
           if addr_nacked /= '0' then
               report "addr_nacked set on a data NACK" severity error; errs := errs + 1; end if;
           if bytes_written /= 1 then
               report "the refused byte must not be counted as written" severity error;
               errs := errs + 1; end if;
           if (n_byte - b_by) /= 3 then
               report "wrong number of bytes on the wire after a data NACK" severity error;
               errs := errs + 1; end if;
           if (n_stop - b_sp) /= 1 then
               report "no STOP after a data NACK" severity error; errs := errs + 1; end if;

           -- 4: an ADDRESS-ONLY write -- how an address scan probes.
           run_write("1010000", 0, -1);
           if (n_byte - b_by) /= 1 then
               report "an address-only write put the wrong number of bytes out" severity error;
               errs := errs + 1; end if;
           if addr_nacked /= '0' then
               report "probe of a present device reported a NACK" severity error; errs := errs + 1; end if;
           if bytes_written /= 0 then
               report "an address-only write wrote payload bytes" severity error; errs := errs + 1; end if;
           if (n_start - b_st) /= 1 or (n_stop - b_sp) /= 1 then
               report "a probe must still be framed by one S and one P" severity error;
               errs := errs + 1; end if;

           -- 5: probing an ABSENT device -- the scan's negative case.
           run_write("1111011", 0, 0);
           if addr_nacked /= '1' then
               report "probe of an absent device did not report a NACK" severity error;
               errs := errs + 1; end if;

           -- 6: back-to-back transactions must not inherit a verdict.
           run_write("1001000", 2, -1);
           if addr_nacked /= '0' or data_nacked /= '0' then
               report "a clean write inherited the previous transaction's NACK" severity error;
               errs := errs + 1; end if;
           if bytes_written /= 2 then
               report "bytes_written wrong on the follow-up write" severity error; errs := errs + 1; end if;

           if n_done /= 6 then
               report "wrong number of done pulses over 6 transactions" severity error;
               errs := errs + 1; end if;
           if busy /= '0' then
               report "still busy after the last transaction" severity error; errs := errs + 1; end if;

           if errs = 0 then
               report "i2c_write_sequencer self-check complete: address byte shifted, payload in "
                    & "order, NACK phase reported, probe and abort framed" severity note;
           else
               report "i2c_write_sequencer self-check FAILED" severity error;
           end if;
           test_done <= '1';
           wait;
       end process;
   end architecture;

6a. Three Decisions Worth Defending

WS_FETCH exists because data_in is a function of data_index. The payload source is combinational on the index — a memory read, a register-file mux, a FIFO output. So on the cycle the sequencer increments data_index, data_in still shows the old byte; the new one appears a cycle later. Latching cmd_byte_data in the same cycle as the increment therefore re-sends the byte just sent. WS_FETCH spends one clock waiting for the new value, and one clock is free — the byte it precedes takes nine SCL periods, which at 100 kHz is ninety microseconds.

This is a bug worth recognising by shape rather than by memory: an index and the data it selects are never valid in the same cycle. It also hides from a lazy testbench, because a payload of identical bytes passes either way. The testbench sends 0xA0, 0xA1, 0xA2 for that reason, and the mutation that removes WS_FETCH reports 0xa0, 0xa0, 0xa1 — the first byte sent twice and the last never sent at all.

bytes_written counts acknowledged bytes, not attempted ones. The increment sits in the ACK branch, and the NACK branch deliberately leaves it alone. The output therefore answers the question a caller actually has — how much of my payload landed — rather than how far did you get, and those differ by exactly one byte in precisely the case where the answer matters. The mutation that moves the increment into the NACK branch is killed by a single check.

A zero-length request is a legal transaction, not an error. req_len = 0 sends S, the address byte, and P — nothing else. That is not a degenerate case to be rejected; it is exactly how an address scan works. Probing an address means asking whether anyone acknowledges it, and the cheapest way to ask is an address-only write. Chapter 6.4 built the scan; this is the transaction it issues, 127 times. A sequencer that treated req_len = 0 as invalid would make the standard discovery tool unimplementable on top of it.

6b. Verified Execution

All three implementations were compiled and run. The SystemVerilog and Verilog with Icarus Verilog 12.0, the VHDL with NVC 1.23.0.

Azvya Education Pvt. Ltd.VLSI Mentor
terminal — three simulators, one result
   $ iverilog -g2012 -o a0 i2c_write_sequencer.sv i2c_write_sequencer_tb.sv && ./a0
   PASS: address byte shifted, payload in order, NACK phase reported, probe and abort framed
   i2c_write_sequencer_tb.sv:174: $finish called at 1700 (1s)

   $ iverilog -g2005 -o a1 i2c_write_sequencer.v i2c_write_sequencer_tb.v && ./a1
   PASS: address byte shifted, payload in order, NACK phase reported, probe and abort framed
   i2c_write_sequencer_tb.v:176: $finish called at 1700 (1s)

   $ nvc -a i2c_write_sequencer.vhd i2c_write_sequencer_tb.vhd && nvc -e i2c_write_sequencer_tb
   $ nvc -r i2c_write_sequencer_tb --stop-time=200us
   ** Note: 1700ns+0: i2c_write_sequencer self-check complete: address byte shifted,
      payload in order, NACK phase reported, probe and abort framed

The three finish at the same simulated time, 1700 ns. That is not decoration. It means the three testbenches apply identical stimulus with identical cycle counts, so the three implementations are being compared rather than merely each passing its own private test. A divergence in that number is the first thing to investigate when porting between languages, and it caught a real discrepancy while this chapter was being written: the VHDL testbench needed a settling wait that the other two did not have, and the fix was to add the wait to all three rather than delete it from one.

7. What the Testbench Proves

The testbench models the two layers below the sequencer — a framing engine and a byte engine — as two independent processes. That structure is itself a finding, recorded in §11.

#stimuluswhat it establishes
1a 3-byte write to 0x48the address byte is 0x90: the address shifted left, R/W clear
2the same writethe payload appears in order — 0xA0, 0xA1, 0xA2, no repeats
3the same writeexactly one S and one P, and bytes_written is 3
4req_len = 0an address-only probe: one byte on the wire, correctly framed
5the address NACKedaddr_nacked set, data_nacked clear, and a P still emitted
6byte 2 of 3 NACKeddata_nacked set, bytes_written is 1, and the transfer aborted
7back-to-back transactionsdone is a pulse and the second transaction starts clean

Test 6 is the one that earns its place. It checks a count under a fault, and the count is the thing a caller needs and the thing that is easiest to get wrong — off by one in either direction, depending on whether the refused byte is counted and whether the abort happens before or after the increment.

Test 7 exists because of a defect found during development rather than by design: the testbench's own wait (done) returned on a stale pulse. done was still asserted at the following negative edge, so the next transaction's wait was satisfied immediately by the previous transaction's completion. The fix was to stop waiting on a level and start waiting on a count — wait (n_done == base_done + 1) — which is the general remedy whenever a testbench waits on something a design asserts for exactly one cycle.

8. Mutation Testing

Eight targeted defects were injected into the SystemVerilog sequencer, one at a time, and the testbench re-run against each.

#injected defectoutcome
A1the address byte's R/W bit says readkilled — address byte was 0x91, expected 0x90
A2an address NACK is ignored and the payload is sent anywaykilled
A3a zero-length request sends a byte anywaykilled — the probe put 257 bytes on the wire
A4the payload byte is latched in the same cycle as the index incrementkilled — payload was 0xa0, 0xa0, 0xa1
A5a refused byte is counted as writtenkilled — bytes_written 2, expected 1
A6the last-byte test is off by onekilled — a 3-byte write put 5 bytes on the wire
A7cmd_start and cmd_stop are held instead of pulsedkilled — saw 36 S's and 5 P's
A8no STOP after the final bytekilled — the watchdog expired

Eight injected, eight killed. Two of them are worth a second look.

A4 is the WS_FETCH mutation, and its failure message — 0xa0, 0xa0, 0xa1 — is the off-by-one made visible. Note what it takes to catch: a payload of distinct bytes. With 0xAA, 0xAA, 0xAA this mutation survives, the design ships, and the fault appears in the field as "the first register of a burst gets written twice and the last one never". Choosing distinct stimulus values is not fussiness.

A8 was killed by the watchdog, not by a check, and that is a legitimate kill worth naming as such. A sequencer that never emits its final STOP never asserts done, so no assertion about the result ever runs — the test simply stops making progress. A testbench without a watchdog would have hung, and a hung test in a regression is indistinguishable from an infrastructure problem. Every testbench in this module has one for that reason, and the VHDL watchdogs were verified to fire by deliberately stalling a design.

9. Verification Connection — A Write Is a Transaction Object

The sequencer's port list is, almost line for line, a UVM sequence_item. That is not a coincidence — it is what "transaction-level" means, and the correspondence is worth making explicit because it is the bridge between this course's RTL and its verification track.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_write_item.sv — the sequencer's request as a UVM sequence item
   class i2c_write_item extends uvm_sequence_item;
      `uvm_object_utils(i2c_write_item)

      // ---- the request: exactly the sequencer's req_* inputs ----
      rand bit [6:0] addr;
      rand bit [7:0] payload [];

      // ---- the response: exactly the sequencer's status outputs ----
      bit        addr_nacked;
      bit        data_nacked;
      int        bytes_written;

      // A zero-length payload is the address-only probe of section 6a, so the
      // constraint permits it deliberately -- a scan is a legal transaction and
      // a generator that excluded it could never produce one.
      constraint c_len { payload.size() inside {[0:16]}; }

      // Not every seven-bit value is a device address. Chapter 6.3's reserved
      // ranges must be excluded here rather than filtered in the driver, so that
      // an illegal address is UNGENERATABLE instead of merely unused.
      constraint c_addr {
         addr != 7'h00;                    // general call / START byte
         addr[6:3] != 4'b0000;             // 0x00-0x07 reserved
         addr[6:3] != 4'b1111;             // 0x78-0x7F reserved
      }

      function new(string name = "i2c_write_item");
         super.new(name);
      endfunction

      // The response side is what a scoreboard compares, so it prints separately
      // from the request -- a log line that mixes the two is unreadable when the
      // interesting case is a request that got an unexpected response.
      function string convert2string();
         return $sformatf("WRITE addr=0x%02h len=%0d -> written=%0d%s%s",
                          addr, payload.size(), bytes_written,
                          addr_nacked ? " ADDR-NACK" : "",
                          data_nacked ? " DATA-NACK" : "");
      endfunction
   endclass

Three things about that class are load-bearing, and each maps to something derived earlier in this chapter.

bytes_written is in the response, not derived from the request. A scoreboard that assumed a successful write moved payload.size() bytes could never detect the partial-write case of §4 — the very case that leaves hardware in an unintended state. The DUT must tell the environment how far it got.

The address constraint excludes reserved ranges structurally. Chapter 6.3 established which addresses a device may not own. Putting that in a constraint rather than in the driver means random generation cannot produce an illegal stimulus at all, which is the difference between a testbench that avoids a case and one that cannot express it.

A zero-length payload is explicitly legal. The constraint says [0:16], not [1:16]. If §6a is right that an address-only write is the scan primitive, then a random generator that never produces one leaves the single most common real transaction untested.

And the checker that belongs with it is the one property a write must satisfy under every stimulus:

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_write_props.sv — the write's one invariant, as an assertion
   // The direction bit is set once per addressing and never changes -- the
   // specification's "the transfer direction is not changed", made checkable.
   //
   // The property is written on the ADDRESS byte rather than on every byte,
   // because the direction is not re-transmitted: there is nothing to compare
   // on a data byte. What must be shown is that no data byte appears with a
   // direction other than the one the address byte established.
   property p_write_direction_fixed;
      @(posedge clk) disable iff (!rst_n)
      (addr_phase_done && !dir_is_read) |=> (!dir_is_read throughout (!frame_start[->1]));
   endproperty
   assert property (p_write_direction_fixed)
      else $error("the direction changed without a repeated START");

   // Every byte of a write gets exactly one acknowledge slot: nine SCL pulses
   // per byte, never eight and never ten. This is Chapter 7.1's byte format
   // expressed as a bus-level property rather than a block-level one.
   property p_nine_pulses_per_byte;
      @(posedge clk) disable iff (!rst_n)
      byte_done |-> (pulses_this_byte == 9);
   endproperty
   assert property (p_nine_pulses_per_byte)
      else $error("a byte took %0d SCL pulses, not 9", pulses_this_byte);

10. FPGA and ASIC Implications

The sequencer is small and its cost is dominated by one parameter.

Area is LEN_W-driven, not state-driven. Six states need three bits. The registers that matter are data_index and bytes_written, both LEN_W wide, plus the eight-bit cmd_byte_data. At LEN_W = 8 the whole block is roughly 30 flops — negligible on any FPGA, and small enough on an ASIC that the decision of what LEN_W should be is about the longest burst you intend to support, not about area.

There is no timing path to the bus. Every output is registered and every input is a completion pulse from a layer that owns the clocking. The sequencer can therefore be synthesised on the system clock with no relationship to the SCL frequency whatsoever, and its static timing is trivially met. This is the synthesis-level payoff of §5's layering.

The payload interface is the integration risk. data_index out, data_in back, combinationally. If the payload source is a block RAM, that path has a registered output and the one cycle WS_FETCH provides is exactly enough — but only just, and only for a one-cycle latency. A source with two-cycle latency needs a second fetch cycle or a handshake, and the failure mode if you forget is precisely A4's: bytes sent one position stale. On an ASIC with a multi-cycle SRAM, make the interface a valid/ready handshake rather than counting cycles.

Reset must leave the bus alone. The sequencer's reset clears cmd_start, cmd_stop and cmd_byte, which means a reset mid-transaction stops issuing commands — it does not generate a STOP. That is the correct choice for this layer and it has a consequence worth knowing: a master reset in the middle of a write leaves the bus with a slave that is still addressed and a transfer that was never ended. Chapter 5.5 covered the recovery, and the reason it is a separate mechanism is exactly this — a reset cannot be a bus operation, because the block that would perform it is the block being reset.

11. Debugging — The Testbench That Dropped Half Its Commands

Pitfall — one process cannot service two independent command streams
Buggy Code
// The FIRST version of this chapter's testbench modelled both lower layers --
// framing and the byte engine -- in a single clocked process:
//
//   always @(posedge clk) begin
//      if (cmd_start || cmd_stop) begin
//         repeat (FRAMING_CYCLES) @(posedge clk);
//         framing_done <= 1'b1; @(posedge clk); framing_done <= 1'b0;
//      end
//      if (cmd_byte) begin
//         repeat (BYTE_CYCLES) @(posedge clk);
//         byte_done <= 1'b1; ack_received <= slave_answer; @(posedge clk);
//         byte_done <= 1'b0;
//      end
//   end
//
// It looks like two independent responders. It is not. The embedded
// "repeat (...) @(posedge clk)" waits SUSPEND THE WHOLE PROCESS, so while the
// framing model is counting out its cycles, the process is not executing the
// second if-statement at all -- and any cmd_byte that arrives during that
// window is never seen.
Symptom

The first transaction completes perfectly. Everything after it is wrong in a way that looks like a design bug in the sequencer.

The 3-byte write puts two bytes on the wire instead of four. The address-only probe sometimes emits no byte at all. Test 7's back-to-back transactions hang. Every symptom points at the sequencer's state machine -- it looks exactly like a machine that loses track of its byte count, and that is a plausible bug in the code under test.

Time went into the sequencer's WS_DATA branch, which was correct. The clue that resolved it was that the NUMBER of dropped bytes depended on FRAMING_CYCLES, a constant in the testbench. Nothing in the design under test can possibly depend on that value -- it only changes how long the testbench's framing model takes. A design symptom that varies with a testbench-only parameter is a testbench bug, and that inference is worth reaching for early.

Root Cause

A single always block that contains blocking waits is a single thread of control, not a set of concurrent responders. Two command streams that can overlap in time need two processes.

The mental model that produced the bug is that an always block "checks its conditions every clock". It does not -- it executes its body, and a wait inside that body stops the block, conditions and all.

12. Common Misconceptions

"The address byte selects a register." It selects a device. The register pointer is a payload byte whose meaning is invented by that device's datasheet, and some devices have no such concept at all. This is the misconception that makes engineers look for an address phase that does not exist.

"A write that returns an error wrote nothing." It wrote every byte that was acknowledged before the refusal. There is no rollback in I²C, and §4 covers why that matters for devices whose registers must be updated together.

"A NACK means the device is broken." A NACK on the address means nobody answered. A NACK on a data byte means a device that is present and working refused that byte — most often because it is busy, which is a normal condition and frequently the documented behaviour during an internal write cycle. An EEPROM NACKs its address for milliseconds after a page write, deliberately, and polling for the ACK to return is the standard way to know it has finished.

"A zero-length write is meaningless." It is the address scan, and it is the most-issued I²C transaction in the world.

"The acknowledge is a separate transfer." It is the ninth clock pulse of the byte it answers. There is no gap and no turnaround; the only thing that happens is that the transmitter released SDA one bit early.

"The direction bit could change mid-transfer to save a repeated START." The specification forbids it, and the reason is mechanical rather than arbitrary: direction is established by the address byte and there is no other place to encode it. A device cannot be told to start transmitting except by being addressed again — which is what a repeated START is for.

13. Reason It Through

A capture shows S, then one byte, then P, and the byte was ACKed. What happened, and what are the two candidate explanations?

One byte after the S is the address byte, so this is an address-only write and the ACK means a device claimed that address. The two readings are: an address scan probing for devices, which is exactly this transaction; or a driver bug — a write whose payload length was computed as zero, which would send the address, get an ACK, and report success while transferring nothing. The two are indistinguishable from the capture alone, which is why the sequencer exposes bytes_written — a driver that logs it cannot silently succeed at writing nothing.

A five-byte write reports data_nacked with bytes_written = 5. Why is that report self-contradictory?

Because the abort happens instead of the increment. If five bytes were acknowledged, the transaction reached its length and finished normally — there was no byte left to refuse. data_nacked with a full count therefore cannot arise from the design in §6, and if you see it, either bytes_written is counting attempts rather than acknowledgements (mutation A5) or the two outputs are being sampled at different times. The second is the more common integration error: both are registered, and both must be read after done.

Why does the sequencer wait for framing_done after a STOP, when the transaction is over?

Because done must not be asserted while the bus is still being driven. A caller that sees done is entitled to issue the next transaction, and the next transaction begins with a START — which needs the bus idle and the bus-free interval observed. Asserting done at the moment the STOP was requested rather than when it completed would let a caller begin a START into a bus that had not finished stopping. This is the same "the completion is the completion of the mechanism, not of the request" reasoning that Chapter 5.5 applied to the bus-free timer.

Clock stretching adds 50 µs to byte three of a write. What in the sequencer changes?

Nothing at all. byte_done arrives 50 µs later; the sequencer was waiting for it and continues to wait. The value of the layering in §5 is that this question has a one-word answer.

14. Understanding Check

15. Summary

The write is the transfer where nothing hands over. The master transmits from the first address bit to the last payload bit, the addressed slave receives throughout, and the only thing the slave drives is the ninth bit of each byte. The specification's "the transfer direction is not changed" is a prohibition on changing direction within an addressing, and re-addressing via a repeated START is the only way to change it.

Nine pulses per byte, and no pulses for framing. A write of n payload bytes puts n+1 bytes on the wire and takes 9(n+1) SCL pulses. The framing conditions live in the gaps, which is why a frame's pulse count is always a multiple of nine.

A write has three outcomes, not two. Complete; address-NACKed, meaning nobody is there and a retry is futile; and data-NACKed, meaning a present device refused a specific byte and a retry may well work. The byte count is a fourth piece of information and the one a caller most needs, because a refused write has still performed every byte it was given an ACK for.

Sequencing is its own layer. Issue commands, consume completions, hold transaction state, touch no wires. That split is what makes clock stretching free, makes static timing trivial, and makes the block's port list read as a UVM sequence_item — because transaction-level is what it is.

WS_FETCH is the chapter's smallest lesson and its most transferable one. An index and the data it selects are never valid in the same cycle, and a payload of identical bytes will never tell you.

16. What Comes Next

Chapter 8.2 takes the other side of this transfer. §2's diagram gave the slave one job — answer every byte — and that is true at the protocol level and an oversimplification at the design level. A real slave-receiver decides what to do with each byte, maintains a register pointer, and owns the two NACK conditions §3.1.6 grants a receiver. It is where a write becomes a device rather than a transfer.

Chapter 8.3 takes §3's number seriously. Twenty-seven pulses for two bytes of payload is 59% efficiency, and the arithmetic of where the rest goes — and what bursting buys — is the difference between a driver that meets its throughput budget and one that does not.

Continue learning