Skip to content
VLSI Mentor

I²C · Module 10

Write-Then-Read — The I²C Register-Pointer Pattern

Almost every real I²C access is this one shape: write the register pointer, repeated START, read the data, without ever letting the bus go. This chapter derives it from the specification's own example and builds the transaction sequencer.

Module 8 built the write. Module 9 built the read. Neither of them, on its own, is how you talk to a real device.

What a real access looks like is both of them joined together: write the register pointer, reverse direction without releasing the bus, read the data. That shape has a name in the specification — the combined format — and it accounts for a large majority of the I²C traffic on any board you will ever debug.

This chapter builds it, and the one thing to carry through the whole module is where the seam goes:

The junction between the two phases is a repeated START. The bus is never released. An abort is a STOP, because the bus must be released. Getting those two backwards produces, respectively, an intermittent multi-master fault and a permanently hung bus.

1. Why a Pointer Phase Exists At All

Chapter 8.1 §2 made a point that now becomes structural: I²C's address byte addresses the device, never a location inside it. There is no address phase in the sense a memory bus has one. Seven bits pick a chip; the frame contains nothing else that identifies where in that chip you mean.

So a device with more than one readable register needs a convention, and the convention nearly all of them chose is a pointer: write one byte (or two) that the device latches as "the location I will read from next", then read. Two phases, because a write and a read are different directions and direction is fixed for the duration of an addressing.

The specification does not merely permit this. It gives it as the example of why the combined format exists.

Read note 1 carefully, because it is unusually prescriptive for this document. "The internal memory location must be written during the first data byte" — that is the pointer phase, named as the intended use of the format. And note 2 is the sentence that settles a question Chapter 8.3 §6 argued from experience: auto-increment is the device designer's decision, not the protocol's. The specification says so directly.

2. The Frame, Event by Event

Here is the canonical access: read three bytes from register 0x12 of an EEPROM at address 0x50.

A sequence diagram with two actors, master and slave. The master sends a START, then the address byte 0xA0 meaning device 0x50 with write direction; the slave acknowledges. The master sends the pointer byte 0x12 and the slave acknowledges. The master then sends a repeated START, not a STOP, and re-sends the address as 0xA1 with read direction; the slave acknowledges. The slave then sends three data bytes; the master acknowledges the first two and not-acknowledges the third. The master finally sends a STOP.Read three bytes from register 0x12 of device 0x50MasterSlave 0x50S — START0xA0 — addr 0x50, WACK0x12 — registerpointerACKSr — repeated START0xA1 — addr 0x50, RACK — from the slave3 data bytesACK, ACK, NACKP — STOP
A write-then-read. Two addressings joined by a repeated START, with the bus never released between them. The address byte appears twice, differing only in its direction bit.

Three observations, each of which the rest of the module depends on.

The address byte appears twice and differs by one bit. 0xA0 then 0xA1. The specification's "both repeated, but with the R/W bit reversed" is exactly this, and it is worth noticing that the re-addressing is what changes the direction — there is no other mechanism, which is why a direction change requires a new addressing and therefore a START.

The sixth event is Sr, and that is the whole subject of the module. A STOP there would work on a bus with one master and fail on a shared one. Chapter 10.2 measures what it costs and Chapter 10.3 says exactly what breaks.

The acknowledge changes owner once, and not where you would guess. The first three acknowledges are the slave's. So is the fourth — the one answering 0xA1 — because at that instant the slave is still the receiver, having just received an address. Only the data bytes' acknowledges are the master's. Chapter 9.1 §1 derived this from the specification's "this first acknowledge is still generated by the slave"; here it is visible as a row in the figure below.

3. The Same Frame at Byte Resolution

Write-then-read: pointer phase, Sr, then a three-byte read

9 cycles
Nine intervals at byte resolution. The first is a START consuming no SCL pulses. The second is the address byte 0xA0 with write direction, acknowledged by the slave. The third is the pointer byte 0x12, acknowledged by the slave. The fourth is a repeated START, consuming no pulses. The fifth is the address byte 0xA1 with read direction, acknowledged by the slave. The next three intervals are data bytes; the master acknowledges the first two and not-acknowledges the third. The last interval is a STOP consuming no pulses. Rows show the ninth bit, who drove it, and the pulse count per interval.phase 1: writephase 1: writere-address, Rre-addre…Rphase 2: readphase 2: readpointer latched by the devicepointer latched by thedeviceSr: bus NOT releasedSr: bus NOT releasedNACK ends the readNACK ends the readbyteS0xA00x12Sr0xA1d0d1d2P9th bit0AAAAAANNdriven by0SSSSMMMMpulses099099990t0t1t2t3t4t5t6t7t8
The same transaction, one interval per byte slot. Framing consumes no clock pulses, which is why the pulse row reads zero for S, Sr and P — and why the repeated START is free.

The figure carries no clock row deliberately: at byte resolution one interval is nine SCL pulses, so a clock row would draw one period where nine belong. The pulses row carries that in the right units, as in Chapter 8.3 §5.

And the pulses row makes the cost visible. Six byte slots at nine pulses each is 54 pulses to move three payload bytes — the n + 3 arithmetic Chapter 9.3 §2 derived, here as a picture. The three framing intervals cost zero pulses, which is why the repeated START is free: it buys atomicity and costs nothing in clock cycles. It is the cheapest correctness available on this bus.

4. Pointer Width Is a Device Property

The pointer is one byte on a small sensor and two on anything with more than 256 locations — a 64 kbit EEPROM needs sixteen address bits, so the pointer phase carries two bytes, most significant first.

device classpointer bytesbytes on the wire for an n-byte read
small sensor, ≤ 256 registers1n + 3
serial memory, > 256 locations2n + 4

Two things about this are worth stating because both are easy to get wrong.

Most significant first. The pointer follows the bus's own byte order (Chapter 7.1) and every two-byte serial memory's convention: high half, then low half. Sending them the other way round addresses a location up to 65280 away from the one you meant, and on a memory that wraps or ignores high bits the symptom is plausible data from the wrong place — the quietest failure mode there is.

It is not part of I²C. Note 2 says auto-increment is the designer's decision, and the same is true of the pointer's width, its endianness, whether it wraps at a page boundary, and whether a read even uses one. The design below takes req_ptr_bytes as a request input for exactly that reason: a sequencer that hard-coded one byte could not talk to a serial memory, and a serial memory is the device the specification's own note names.

5. Four Places It Can Fail, and Two Kinds of Ending

A plain write has three outcomes (Chapter 8.1 §4). A write-then-read has four, because it has two addressings.

#wherewhat it meanswhat the master should do
1phase-1 address NACKedno device at that addressabort; retry is futile
2the pointer byte NACKeddevice present, refused the location — condition 3abort; a different pointer may work
3phase-2 address NACKeddevice took the pointer, refused the readabort; usually means busy
4a data bytecannot happen — the master owns those acknowledges—

Row 3 is the one people do not expect, and it is the most diagnostic of the three. The device answered the write addressing a few microseconds earlier, so it is present, powered and listening. Refusing the read addressing therefore means something specific and temporary: on an EEPROM it is the classic signature of an internal write cycle still in progress, which is precisely why polling for the address to be acknowledged is the standard way to wait for a page write to finish.

Collapsing rows 1 and 3 into one "address NACK" throws that away. A driver that reports only "NACK" makes "the device is absent" and "the device is busy for another millisecond" indistinguishable, and those call for opposite responses — give up, versus try again shortly.

Row 4 is worth stating explicitly because its absence is structural: in the read phase, the acknowledges belong to the master (Chapter 9.2 §3), and a slave-transmitter has no way to refuse anything. So the read phase of a combined transaction cannot fail by NACK. It can return fill bytes, and nothing on the bus will say so.

6. The Combined Sequencer in Three Languages

Eight states, and the state list is the transaction:

statewaiting fornote
CS_IDLEa request
CS_STARTframing_donethe S
CS_ADDR_Wbyte_doneaddress + W; the slave answers
CS_PTRbyte_doneone or two pointer bytes; the slave answers each
CS_RESTARTframing_donethe junction — an Sr, never a P
CS_ADDR_Rbyte_doneaddress + R; the slave answers
CS_DATAbyte_donepayload; this master answers
CS_ENDframing_donethe final P, or an abort's P
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_combined_sequencer.sv — the write-then-read transaction sequencer
   // The WRITE-THEN-READ transaction: the shape almost every real device access takes.
   //
   //     S  addr+W  A  ptr[..]  A  Sr  addr+R  A  d0 A  d1 A ... dn N  P
   //        \___________________/      \_______________________________/
   //           phase 1: write            phase 2: read
   //                     the junction is Sr, NOT P then S
   //
   // This block composes Chapter 8.1's write sequencer and Chapter 9.1's read sequencer
   // into ONE transaction that never releases the bus between the phases. That is the
   // whole point of the format, and the specification describes it directly:
   //
   //   "Combined formats can be used, for example, to control a serial memory. The
   //    internal memory location must be written during the first data byte. After the
   //    START condition and slave address is repeated, data can be transferred."
   //
   //   "During a change of direction within a transfer, the START condition and the slave
   //    address are both repeated, but with the R/W bit reversed."
   //
   // Two asymmetries are load-bearing and are easy to get backwards:
   //
   //   the PHASE JUNCTION is a repeated START -- the bus is never released;
   //   an ABORT is a STOP           -- the bus MUST be released, or it stays held.
   //
   // A design that used a STOP at the junction would work on a single-master bus and
   // fail intermittently on a shared one (Chapter 10.3). A design that used a repeated
   // START to abort would hold the bus forever.
   module i2c_combined_sequencer #(
       parameter int LEN_W = 8
   )(
       input  logic clk,
       input  logic rst_n,

       // ---- transaction request ----
       input  logic             req,           // pulse: begin a write-then-read
       input  logic [6:0]       req_addr,      // seven-bit address, as the datasheet states it
       // Pointer width is a DEVICE property, not a bus one. A sensor with sixteen
       // registers takes one byte; a 64 kbit EEPROM takes two, most significant first.
       // Supporting only one byte would exclude most serial memories, which are the
       // devices the specification's own note names.
       input  logic [1:0]       req_ptr_bytes, // 1 or 2
       input  logic [15:0]      req_ptr,       // big-endian: [15:8] goes first
       input  logic [LEN_W-1:0] req_len,       // payload bytes to read; must be >= 1

       // ---- commands to the framing sequencer (5.5) and byte engine (7.1) ----
       output logic       cmd_start,           // pulse: emit S
       output logic       cmd_restart,         // pulse: emit Sr -- the phase junction
       output logic       cmd_stop,            // pulse: emit P  -- the END, or an abort
       output logic       cmd_byte,            // pulse: TRANSMIT one byte
       output logic [7:0] cmd_byte_data,
       output logic       cmd_recv,            // pulse: RECEIVE one byte
       output logic       cmd_ack,             // the answer for the byte being received

       // ---- completions from those layers ----
       input  logic       framing_done,        // pulse: an S, Sr or P completed
       input  logic       byte_done,           // pulse: a byte AND its ninth slot completed
       input  logic       ack_received,        // the SLAVE's answer to a transmitted byte
       input  logic [7:0] byte_in,

       // ---- payload sink ----
       output logic [LEN_W-1:0] data_index,
       output logic [7:0]       data_out,
       output logic             data_valid,

       // ---- status ----
       output logic             busy,
       output logic             done,           // pulse: the transaction has ended
       output logic [LEN_W-1:0] bytes_read,
       // Three DISTINCT failure points, because they call for three different responses.
       // Collapsing them into one flag is the defect Chapter 8.1 section 4 argues against,
       // and a combined transaction has one more place to fail than a plain read does.
       output logic             wr_addr_nacked, // phase 1 address: nobody is there
       output logic             ptr_nacked,     // the pointer byte was refused
       output logic             rd_addr_nacked, // phase 2 address NACKed -- see section 6a
       output logic             bus_held        // high from the first S until the final P
   );
       typedef enum logic [2:0] {
           CS_IDLE,
           CS_START,     // waiting for the S
           CS_ADDR_W,    // address + W in flight; the slave answers
           CS_PTR,       // a pointer byte in flight; the slave answers
           CS_RESTART,   // waiting for the Sr -- the junction, never a STOP
           CS_ADDR_R,    // address + R in flight; the slave answers
           CS_DATA,      // a payload byte arriving; THIS master answers
           CS_END        // waiting for the final P, or for an abort's P
       } state_e;

       state_e state;

       logic [1:0]       ptr_index;    // which pointer byte is next, 0 = first
       logic [LEN_W-1:0] rd_index;

       logic [7:0] addr_w, addr_r;
       assign addr_w = {req_addr, 1'b0};
       assign addr_r = {req_addr, 1'b1};

       // Pointer bytes go MOST SIGNIFICANT FIRST, matching the byte order of the bus
       // itself (Chapter 7.1) and the convention every two-byte serial memory uses. With
       // one pointer byte only the low half is sent, so a one-byte device is not a
       // special case in the datapath -- just a shorter loop.
       // Written as a continuous assignment rather than an always_comb block: a part-select
       // of a vector inside a procedural block makes the block sensitive to every bit of
       // that vector, which some tools warn about and none of them need here.
       logic [7:0] ptr_byte;
       assign ptr_byte = ((req_ptr_bytes == 2'd2) && (ptr_index == 2'd0)) ? req_ptr[15:8]
                                                                         : req_ptr[7:0];

       // The acknowledge policy of the READ phase, unchanged from Chapter 9.1: ACK a byte
       // if and only if another is wanted after it, and NACK the last.
       function automatic logic ack_for(input logic [LEN_W-1:0] idx);
           return ((idx + 1'b1) < req_len);
       endfunction

       always_ff @(posedge clk) begin
           if (!rst_n) begin
               state          <= CS_IDLE;
               cmd_start      <= 1'b0;
               cmd_restart    <= 1'b0;
               cmd_stop       <= 1'b0;
               cmd_byte       <= 1'b0;
               cmd_recv       <= 1'b0;
               cmd_ack        <= 1'b0;
               cmd_byte_data  <= 8'h00;
               ptr_index      <= 2'd0;
               rd_index       <= '0;
               data_index     <= '0;
               data_out       <= 8'h00;
               data_valid     <= 1'b0;
               busy           <= 1'b0;
               done           <= 1'b0;
               bytes_read     <= '0;
               wr_addr_nacked <= 1'b0;
               ptr_nacked     <= 1'b0;
               rd_addr_nacked <= 1'b0;
               bus_held       <= 1'b0;
           end else begin
               // Every command is a single-cycle pulse.
               cmd_start   <= 1'b0;
               cmd_restart <= 1'b0;
               cmd_stop    <= 1'b0;
               cmd_byte    <= 1'b0;
               cmd_recv    <= 1'b0;
               data_valid  <= 1'b0;
               done        <= 1'b0;

               case (state)
                   CS_IDLE:
                       if (req) begin
                           busy           <= 1'b1;
                           bus_held       <= 1'b1;   // from the first S to the final P
                           wr_addr_nacked <= 1'b0;
                           ptr_nacked     <= 1'b0;
                           rd_addr_nacked <= 1'b0;
                           bytes_read     <= '0;
                           rd_index       <= '0;
                           data_index     <= '0;
                           ptr_index      <= 2'd0;
                           cmd_start      <= 1'b1;
                           state          <= CS_START;
                       end

                   CS_START:
                       if (framing_done) begin
                           cmd_byte      <= 1'b1;
                           cmd_byte_data <= addr_w;
                           state         <= CS_ADDR_W;
                       end

                   CS_ADDR_W:
                       if (byte_done) begin
                           if (!ack_received) begin
                               // Nobody is at this address. An ABORT, so the bus must be
                               // RELEASED -- a repeated START here would hold it.
                               wr_addr_nacked <= 1'b1;
                               cmd_stop       <= 1'b1;
                               state          <= CS_END;
                           end else begin
                               cmd_byte      <= 1'b1;
                               cmd_byte_data <= ptr_byte;   // ptr_index is still 0
                               state         <= CS_PTR;
                           end
                       end

                   CS_PTR:
                       if (byte_done) begin
                           if (!ack_received) begin
                               // The device refused the pointer -- NACK condition 3,
                               // exactly as Chapter 8.2's slave-receiver produces it.
                               // Abort with a STOP; there is nothing to read.
                               ptr_nacked <= 1'b1;
                               cmd_stop   <= 1'b1;
                               state      <= CS_END;
                           end else if ((ptr_index + 2'd1) < req_ptr_bytes) begin
                               // A second pointer byte. ptr_byte is combinational on
                               // ptr_index, so the NEW value is presented in the same
                               // cycle -- safe here because it is a mux over a register,
                               // not a memory read. Chapter 9.1 section 6a covers why the
                               // write sequencer's payload needed a fetch cycle and this
                               // does not.
                               ptr_index     <= ptr_index + 2'd1;
                               cmd_byte      <= 1'b1;
                               cmd_byte_data <= req_ptr[7:0];   // the second byte is the low half
                               state         <= CS_PTR;
                           end else begin
                               // THE JUNCTION. A repeated START, never a STOP: the bus is
                               // not released, so no other master can interpose and change
                               // the pointer we just wrote.
                               cmd_restart <= 1'b1;
                               state       <= CS_RESTART;
                           end
                       end

                   CS_RESTART:
                       if (framing_done) begin
                           cmd_byte      <= 1'b1;
                           cmd_byte_data <= addr_r;
                           state         <= CS_ADDR_R;
                       end

                   CS_ADDR_R:
                       if (byte_done) begin
                           if (!ack_received) begin
                               // The device answered the write phase and then refused the
                               // read addressing. Reported SEPARATELY from the phase-1
                               // NACK because it means something completely different --
                               // section 6a.
                               rd_addr_nacked <= 1'b1;
                               cmd_stop       <= 1'b1;
                               state          <= CS_END;
                           end else begin
                               // The direction has changed. From here this master is the
                               // receiver and owns every ninth bit.
                               cmd_recv <= 1'b1;
                               cmd_ack  <= ack_for('0);
                               state    <= CS_DATA;
                           end
                       end

                   CS_DATA:
                       if (byte_done) begin
                           data_out   <= byte_in;
                           data_valid <= 1'b1;
                           bytes_read <= bytes_read + 1'b1;

                           if (!ack_for(rd_index)) begin
                               cmd_stop <= 1'b1;
                               state    <= CS_END;
                           end else begin
                               rd_index   <= rd_index + 1'b1;
                               data_index <= rd_index + 1'b1;
                               cmd_recv   <= 1'b1;
                               cmd_ack    <= ack_for(rd_index + 1'b1);
                               state      <= CS_DATA;
                           end
                       end

                   CS_END:
                       if (framing_done) begin
                           busy     <= 1'b0;
                           bus_held <= 1'b0;   // the P has completed; the bus is free
                           done     <= 1'b1;
                           state    <= CS_IDLE;
                       end

                   default: state <= CS_IDLE;
               endcase
           end
       end
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_combined_sequencer_tb.sv — seven transactions, including all three abort points
   `timescale 1ns/1ps
   // The two layers below the sequencer are modelled as two INDEPENDENT processes, one per
   // layer. Chapter 8.1 section 11 records why: a single clocked block containing blocking
   // waits is one thread of control and drops commands that arrive while it is suspended.
   module i2c_combined_sequencer_tb;
       localparam int LEN_W = 8;
       localparam int FRAMING_CYCLES = 4;
       localparam int BYTE_CYCLES    = 9;

       logic clk = 1'b0;
       always #5 clk = ~clk;

       logic rst_n = 1'b0;
       logic req = 1'b0;
       logic [6:0] req_addr = 7'h50;
       logic [1:0] req_ptr_bytes = 2'd1;
       logic [15:0] req_ptr = 16'h0000;
       logic [LEN_W-1:0] req_len = 8'd1;

       logic cmd_start, cmd_restart, cmd_stop, cmd_byte, cmd_recv, cmd_ack;
       logic [7:0] cmd_byte_data;
       logic framing_done = 1'b0, byte_done = 1'b0, ack_received = 1'b0;
       logic [7:0] byte_in = 8'h00;
       logic [LEN_W-1:0] data_index;
       logic [7:0] data_out;
       logic data_valid, busy, done;
       logic [LEN_W-1:0] bytes_read;
       logic wr_addr_nacked, ptr_nacked, rd_addr_nacked, bus_held;

       int errors = 0;

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

       initial begin #400000; $display("FAIL: watchdog expired"); $finish; end

       // ---- observation -------------------------------------------------------------
       int n_s, n_sr, n_p, n_tx, n_rx, n_done;
       logic [7:0] tx_log [0:15];      // every byte the master TRANSMITTED, in order
       logic       ack_log [0:15];     // the answer the master gave to each byte it READ
       logic [7:0] rx_log [0:15];
       int n_rxlog;

       // THE property of a combined transaction: a STOP must not appear between the first
       // START and the final one. This records WHEN each framing event happened relative to
       // the byte stream, so the test can assert on the ORDER and not merely on counts.
       // seq_log is the framing sequence as small codes -- 1 = S, 2 = Sr, 3 = P -- rather
       // than as characters, so the VHDL and Verilog testbenches can hold exactly the same
       // structure. Parity between the three is what makes the matching finish time mean
       // something, and a SystemVerilog-only string type would have broken it.
       localparam int SEQ_S = 1, SEQ_R = 2, SEQ_P = 3;
       int seq_log [0:15];
       int n_seq;

       logic obs_clear = 1'b0;
       always @(posedge clk) if (rst_n) begin
         if (obs_clear) begin
           n_s = 0; n_sr = 0; n_p = 0; n_tx = 0; n_rx = 0; n_rxlog = 0; n_seq = 0;
         end else begin
           if (cmd_start)   begin n_s++;  if (n_seq < 16) seq_log[n_seq] = SEQ_S; n_seq++; end
           if (cmd_restart) begin n_sr++; if (n_seq < 16) seq_log[n_seq] = SEQ_R; n_seq++; end
           if (cmd_stop)    begin n_p++;  if (n_seq < 16) seq_log[n_seq] = SEQ_P; n_seq++; end
           if (cmd_byte) begin if (n_tx < 16) tx_log[n_tx] = cmd_byte_data; n_tx++; end
           if (cmd_recv) begin if (n_rx < 16) ack_log[n_rx] = cmd_ack;      n_rx++; end
           if (data_valid) begin if (n_rxlog < 16) rx_log[n_rxlog] = data_out; n_rxlog++; end
         end
         if (done) n_done++;
       end

       // A CONTINUOUS check: bus_held must be asserted from the first S until the final P.
       // If it ever drops while the sequencer still has work to do, the bus was released
       // mid-transaction -- which is the fault this whole module exists to prevent.
       int held_violations = 0;
       always @(posedge clk) if (rst_n)
           if (busy && !bus_held) held_violations++;

       // ---- the framing layer, an independent responder -----------------------------
       initial forever begin
           wait (cmd_start || cmd_restart || cmd_stop);
           repeat (FRAMING_CYCLES) @(posedge clk);
           framing_done <= 1'b1; @(posedge clk); framing_done <= 1'b0;
       end

       // ---- the byte layer, an independent responder --------------------------------
       logic slave_answer = 1'b1;      // the slave's answer to a TRANSMITTED byte
       logic is_recv;
       logic [7:0] slave_seed = 8'h00;
       logic       seed_load  = 1'b0;
       logic       rx_taken   = 1'b0;
       logic [7:0] slave_data = 8'h00;
       always @(posedge clk) begin
           if (seed_load)     slave_data <= slave_seed;
           else if (rx_taken) slave_data <= slave_data + 8'h11;
       end

       initial forever begin
           wait (cmd_byte || cmd_recv);
           is_recv = cmd_recv;
           if (is_recv) byte_in <= slave_data;
           repeat (BYTE_CYCLES) @(posedge clk);
           byte_done <= 1'b1;
           // For a RECEIVED byte the ninth bit is the master's own, so there is no slave
           // answer and this model drives X rather than a plausible value. A design that
           // consulted ack_received on a received byte then propagates X.
           ack_received <= is_recv ? 1'bx : slave_answer;
           rx_taken     <= is_recv;
           @(posedge clk);
           byte_done <= 1'b0;
           rx_taken  <= 1'b0;
       end

       task automatic clear_obs();
           obs_clear = 1'b1; @(negedge clk); obs_clear = 1'b0; @(negedge clk);
       endtask

       // Check the framing SEQUENCE, not merely the counts. "S then Sr then P" and
       // "S then P then S" have identical counts of each event and are completely
       // different transactions -- the whole subject of this module -- so only the order
       // distinguishes them.
       task automatic check_seq(input int e0, input int e1, input int e2, input int n,
                                input string what);
           if (n_seq !== n || seq_log[0] !== e0
               || (n > 1 && seq_log[1] !== e1) || (n > 2 && seq_log[2] !== e2)) begin
               $display("FAIL: %s framing was %0d events [%0d %0d %0d], expected %0d [%0d %0d %0d]  (1=S 2=Sr 3=P)",
                        what, n_seq, seq_log[0], seq_log[1], seq_log[2], n, e0, e1, e2);
               errors++;
           end
       endtask

       task automatic start_txn(input logic [6:0] a, input logic [1:0] pb,
                                input logic [15:0 ] p, input logic [LEN_W-1:0] len,
                                input logic [7:0] first_data);
           int base;
           base = n_done;
           clear_obs();
           req_addr = a; req_ptr_bytes = pb; req_ptr = p; req_len = len;
           slave_seed = first_data;
           seed_load = 1'b1; @(negedge clk); seed_load = 1'b0; @(negedge clk);
           req = 1'b1; @(negedge clk); req = 1'b0;
           wait (n_done == base + 1);
           repeat (2) @(negedge clk);
       endtask

       initial begin
           repeat (3) @(negedge clk);
           if (busy !== 1'b0) begin $display("FAIL: busy out of reset"); errors++; end
           if (bus_held !== 1'b0) begin $display("FAIL: bus_held out of reset"); errors++; end
           rst_n = 1'b1; @(negedge clk);

           // ---- 1: the canonical access. One pointer byte, three bytes read, from an
           //      EEPROM at 0x50. This is the shape the whole module is about.
           slave_answer = 1'b1;
           start_txn(7'h50, 2'd1, 16'h0012, 8'd3, 8'hA0);
           // THE assertion of this module: S, then Sr, then P. No STOP in between.
           check_seq(SEQ_S, SEQ_R, SEQ_P, 3, "the canonical access");
           if (n_p !== 1 || n_sr !== 1 || n_s !== 1) begin
               $display("FAIL: framing counts S=%0d Sr=%0d P=%0d, expected 1,1,1", n_s, n_sr, n_p);
               errors++; end
           // three transmitted bytes: addr+W, the pointer, addr+R
           if (n_tx !== 3) begin
               $display("FAIL: transmitted %0d bytes, expected 3", n_tx); errors++; end
           if (tx_log[0] !== 8'hA0) begin
               $display("FAIL: phase-1 address was 0x%02h, expected 0xa0 (0x50 << 1 | W)",
                        tx_log[0]); errors++; end
           if (tx_log[1] !== 8'h12) begin
               $display("FAIL: pointer byte was 0x%02h, expected 0x12", tx_log[1]); errors++; end
           if (tx_log[2] !== 8'hA1) begin
               $display("FAIL: phase-2 address was 0x%02h, expected 0xa1 (0x50 << 1 | R)",
                        tx_log[2]); errors++; end
           // the read phase's acknowledge policy is unchanged from Chapter 9.1
           if (ack_log[0] !== 1'b1 || ack_log[1] !== 1'b1 || ack_log[2] !== 1'b0) begin
               $display("FAIL: ack pattern %b%b%b, expected 110 (ACK ACK NACK)",
                        ack_log[0], ack_log[1], ack_log[2]); errors++; end
           if (rx_log[0] !== 8'hA0 || rx_log[1] !== 8'hB1 || rx_log[2] !== 8'hC2) begin
               $display("FAIL: payload 0x%02h,0x%02h,0x%02h, expected 0xa0,0xb1,0xc2",
                        rx_log[0], rx_log[1], rx_log[2]); errors++; end
           if (bytes_read !== 8'd3) begin
               $display("FAIL: bytes_read = %0d, expected 3", bytes_read); errors++; end
           if (bus_held !== 1'b0) begin
               $display("FAIL: bus_held still asserted after the final P"); errors++; end

           // ---- 2: a TWO-BYTE pointer, as every larger serial memory uses. The bytes
           //      must go MOST SIGNIFICANT FIRST, and there must still be exactly one Sr.
           start_txn(7'h50, 2'd2, 16'h1234, 8'd2, 8'h5A);
           check_seq(SEQ_S, SEQ_R, SEQ_P, 3, "a 2-byte pointer");
           if (n_tx !== 4) begin
               $display("FAIL: transmitted %0d bytes, expected 4 (addr, 2 ptr, addr)", n_tx);
               errors++; end
           if (tx_log[1] !== 8'h12 || tx_log[2] !== 8'h34) begin
               $display("FAIL: pointer bytes 0x%02h,0x%02h -- expected 0x12,0x34 (MSB first)",
                        tx_log[1], tx_log[2]); errors++; end
           if (tx_log[3] !== 8'hA1) begin
               $display("FAIL: phase-2 address after a 2-byte pointer was 0x%02h", tx_log[3]);
               errors++; end
           if (bytes_read !== 8'd2) begin
               $display("FAIL: bytes_read = %0d, expected 2", bytes_read); errors++; end

           // ---- 3: a ONE-BYTE read. The read phase's only acknowledge is a NACK. This is
           //      the single most common real transaction: read one status register.
           start_txn(7'h48, 2'd1, 16'h0001, 8'd1, 8'h77);
           check_seq(SEQ_S, SEQ_R, SEQ_P, 3, "a 1-byte read");
           if (ack_log[0] !== 1'b0) begin
               $display("FAIL: the only byte of a 1-byte read was ACKed, not NACKed"); errors++; end
           if (rx_log[0] !== 8'h77 || bytes_read !== 8'd1) begin
               $display("FAIL: 1-byte read returned 0x%02h, count %0d", rx_log[0], bytes_read);
               errors++; end

           // ---- 4: the PHASE-1 ADDRESS is NACKed. Nobody is there. This is an ABORT, so
           //      the terminator must be a STOP -- the bus MUST be released. An Sr here
           //      would hold the bus with no transaction to continue.
           slave_answer = 1'b0;
           start_txn(7'h50, 2'd1, 16'h0012, 8'd3, 8'h00);
           check_seq(SEQ_S, SEQ_P, 0, 2, "an abort at the phase-1 address");
           if (n_sr !== 0) begin
               $display("FAIL: an abort emitted %0d repeated STARTs -- it must emit a STOP",
                        n_sr); errors++; end
           if (wr_addr_nacked !== 1'b1) begin
               $display("FAIL: wr_addr_nacked not reported"); errors++; end
           if (ptr_nacked !== 1'b0 || rd_addr_nacked !== 1'b0) begin
               $display("FAIL: a phase-1 address NACK set another failure flag too"); errors++; end
           if (n_tx !== 1) begin
               $display("FAIL: %0d bytes transmitted after an address NACK, expected 1", n_tx);
               errors++; end
           if (bytes_read !== 8'd0) begin
               $display("FAIL: bytes_read = %0d after an aborted transaction", bytes_read);
               errors++; end
           if (bus_held !== 1'b0) begin
               $display("FAIL: the bus was left HELD after an abort"); errors++; end

           // ---- 5: the POINTER byte is NACKed. The device is there and refused the
           //      pointer -- condition 3. Distinct from test 4, and it must be reported so.
           begin
               int base;
               base = n_done;
               clear_obs();
               slave_answer = 1'b1;
               req_addr = 7'h50; req_ptr_bytes = 2'd1; req_ptr = 16'h00FF; req_len = 8'd3;
               req = 1'b1; @(negedge clk); req = 1'b0;
               // let the address byte be ACKed, then refuse the pointer
               wait (n_tx == 1); wait (byte_done); @(negedge clk);
               slave_answer = 1'b0;
               wait (n_done == base + 1);
               repeat (2) @(negedge clk);
               slave_answer = 1'b1;
           end
           check_seq(SEQ_S, SEQ_P, 0, 2, "an abort on the pointer byte");
           if (ptr_nacked !== 1'b1) begin
               $display("FAIL: ptr_nacked not reported"); errors++; end
           if (wr_addr_nacked !== 1'b0) begin
               $display("FAIL: a pointer NACK was reported as an address NACK"); errors++; end
           if (n_rx !== 0) begin
               $display("FAIL: read %0d bytes after the pointer was refused", n_rx); errors++; end

           // ---- 6: the PHASE-2 address is NACKed. The device took the pointer and then
           //      refused the read addressing -- a real and distinct condition, which
           //      section 6a argues is the most diagnostic of the three.
           begin
               int base;
               base = n_done;
               clear_obs();
               slave_answer = 1'b1;
               req_addr = 7'h50; req_ptr_bytes = 2'd1; req_ptr = 16'h0020; req_len = 8'd2;
               req = 1'b1; @(negedge clk); req = 1'b0;
               wait (n_tx == 2); wait (byte_done); @(negedge clk);   // addr+W and ptr ACKed
               slave_answer = 1'b0;                                  // refuse addr+R
               wait (n_done == base + 1);
               repeat (2) @(negedge clk);
               slave_answer = 1'b1;
           end
           check_seq(SEQ_S, SEQ_R, SEQ_P, 3, "an abort at the phase-2 address");
           if (rd_addr_nacked !== 1'b1) begin
               $display("FAIL: rd_addr_nacked not reported"); errors++; end
           if (wr_addr_nacked !== 1'b0 || ptr_nacked !== 1'b0) begin
               $display("FAIL: a phase-2 NACK was attributed to phase 1"); errors++; end
           if (n_rx !== 0) begin
               $display("FAIL: read %0d bytes after the read addressing was refused", n_rx);
               errors++; end
           if (bus_held !== 1'b0) begin
               $display("FAIL: the bus was left held after a phase-2 abort"); errors++; end

           // ---- 7: back-to-back transactions, and the flags must CLEAR. A stale
           //      wr_addr_nacked from test 4 would make a healthy transaction look failed.
           start_txn(7'h50, 2'd1, 16'h0030, 8'd2, 8'h11);
           check_seq(SEQ_S, SEQ_R, SEQ_P, 3, "the transaction after an abort");
           if (wr_addr_nacked !== 1'b0 || ptr_nacked !== 1'b0 || rd_addr_nacked !== 1'b0) begin
               $display("FAIL: a failure flag survived into the next transaction"); errors++; end
           if (bytes_read !== 8'd2) begin
               $display("FAIL: bytes_read = %0d, expected 2", bytes_read); errors++; end
           if (n_done !== 7) begin
               $display("FAIL: %0d done pulses across 7 transactions", n_done); errors++; end

           // ---- the continuous check: the bus was never released while work remained.
           if (held_violations !== 0) begin
               $display("FAIL: bus_held dropped %0d times while the sequencer was busy",
                        held_violations); errors++; end

           if (errors == 0)
               $display("PASS: junction is Sr and abort is P, pointer bytes MSB first, all three NACK points distinguished, bus never released mid-transaction");
           else $display("FAIL: %0d error(s)", errors);
           $finish;
       end
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_combined_sequencer.v — the same sequencer in Verilog-2001
   // The WRITE-THEN-READ transaction.  (Verilog-2001)
   // the shape almost every real device access takes.
   //
   //     S  addr+W  A  ptr[..]  A  Sr  addr+R  A  d0 A  d1 A ... dn N  P
   //        \___________________/      \_______________________________/
   //           phase 1: write            phase 2: read
   //                     the junction is Sr, NOT P then S
   //
   // This block composes Chapter 8.1's write sequencer and Chapter 9.1's read sequencer
   // into ONE transaction that never releases the bus between the phases. That is the
   // whole point of the format, and the specification describes it directly:
   //
   //   "Combined formats can be used, for example, to control a serial memory. The
   //    internal memory location must be written during the first data byte. After the
   //    START condition and slave address is repeated, data can be transferred."
   //
   //   "During a change of direction within a transfer, the START condition and the slave
   //    address are both repeated, but with the R/W bit reversed."
   //
   // Two asymmetries are load-bearing and are easy to get backwards:
   //
   //   the PHASE JUNCTION is a repeated START -- the bus is never released;
   //   an ABORT is a STOP           -- the bus MUST be released, or it stays held.
   //
   // A design that used a STOP at the junction would work on a single-master bus and
   // fail intermittently on a shared one (Chapter 10.3). A design that used a repeated
   // START to abort would hold the bus forever.
   module i2c_combined_sequencer #(
       parameter LEN_W = 8
   )(
       input  wire  clk,
       input  wire  rst_n,

       // ---- transaction request ----
       input  wire              req,           // pulse: begin a write-then-read
       input  wire  [6:0]       req_addr,      // seven-bit address, as the datasheet states it
       // Pointer width is a DEVICE property, not a bus one. A sensor with sixteen
       // registers takes one byte; a 64 kbit EEPROM takes two, most significant first.
       // Supporting only one byte would exclude most serial memories, which are the
       // devices the specification's own note names.
       input  wire  [1:0]       req_ptr_bytes, // 1 or 2
       input  wire  [15:0]      req_ptr,       // big-endian: [15:8] goes first
       input  wire  [LEN_W-1:0] req_len,       // payload bytes to read; must be >= 1

       // ---- commands to the framing sequencer (5.5) and byte engine (7.1) ----
       output reg         cmd_start,           // pulse: emit S
       output reg         cmd_restart,         // pulse: emit Sr -- the phase junction
       output reg         cmd_stop,            // pulse: emit P  -- the END, or an abort
       output reg         cmd_byte,            // pulse: TRANSMIT one byte
       output reg   [7:0] cmd_byte_data,
       output reg         cmd_recv,            // pulse: RECEIVE one byte
       output reg         cmd_ack,             // the answer for the byte being received

       // ---- completions from those layers ----
       input  wire        framing_done,        // pulse: an S, Sr or P completed
       input  wire        byte_done,           // pulse: a byte AND its ninth slot completed
       input  wire        ack_received,        // the SLAVE's answer to a transmitted byte
       input  wire  [7:0] byte_in,

       // ---- payload sink ----
       output reg   [LEN_W-1:0] data_index,
       output reg   [7:0]       data_out,
       output reg               data_valid,

       // ---- status ----
       output reg               busy,
       output reg               done,           // pulse: the transaction has ended
       output reg   [LEN_W-1:0] bytes_read,
       // Three DISTINCT failure points, because they call for three different responses.
       // Collapsing them into one flag is the defect Chapter 8.1 section 4 argues against,
       // and a combined transaction has one more place to fail than a plain read does.
       output reg               wr_addr_nacked, // phase 1 address: nobody is there
       output reg               ptr_nacked,     // the pointer byte was refused
       output reg               rd_addr_nacked, // phase 2 address NACKed -- see section 6a
       output reg               bus_held        // high from the first S until the final P
   );
       localparam CS_IDLE    = 3'd0;
       localparam CS_START   = 3'd1;   // waiting for the S
       localparam CS_ADDR_W  = 3'd2;   // address + W in flight; the slave answers
       localparam CS_PTR     = 3'd3;   // a pointer byte in flight; the slave answers
       localparam CS_RESTART = 3'd4;   // waiting for the Sr -- the junction, never a STOP
       localparam CS_ADDR_R  = 3'd5;   // address + R in flight; the slave answers
       localparam CS_DATA    = 3'd6;   // a payload byte arriving; THIS master answers
       localparam CS_END     = 3'd7;   // waiting for the final P, or for an abort's P

       reg [2:0] state;

       reg [1:0]       ptr_index;    // which pointer byte is next, 0 = first
       reg [LEN_W-1:0] rd_index;

       wire [7:0] addr_w, addr_r;
       assign addr_w = {req_addr, 1'b0};
       assign addr_r = {req_addr, 1'b1};

       // Pointer bytes go MOST SIGNIFICANT FIRST, matching the byte order of the bus
       // itself (Chapter 7.1) and the convention every two-byte serial memory uses. With
       // one pointer byte only the low half is sent, so a one-byte device is not a
       // special case in the datapath -- just a shorter loop.
       // Written as a continuous assignment rather than an always_comb block: a part-select
       // of a vector inside a procedural block makes the block sensitive to every bit of
       // that vector, which some tools warn about and none of them need here.
       wire [7:0] ptr_byte;
       assign ptr_byte = ((req_ptr_bytes == 2'd2) && (ptr_index == 2'd0)) ? req_ptr[15:8]
                                                                         : req_ptr[7:0];

       // The acknowledge policy of the READ phase, unchanged from Chapter 9.1: ACK a byte
       // if and only if another is wanted after it, and NACK the last.
       function ack_for;
           input [LEN_W-1:0] idx;
           begin
               ack_for = ((idx + 1'b1) < req_len);
           end
       endfunction

       always @(posedge clk) begin
           if (!rst_n) begin
               state          <= CS_IDLE;
               cmd_start      <= 1'b0;
               cmd_restart    <= 1'b0;
               cmd_stop       <= 1'b0;
               cmd_byte       <= 1'b0;
               cmd_recv       <= 1'b0;
               cmd_ack        <= 1'b0;
               cmd_byte_data  <= 8'h00;
               ptr_index      <= 2'd0;
               rd_index       <= {LEN_W{1'b0}};
               data_index     <= {LEN_W{1'b0}};
               data_out       <= 8'h00;
               data_valid     <= 1'b0;
               busy           <= 1'b0;
               done           <= 1'b0;
               bytes_read     <= {LEN_W{1'b0}};
               wr_addr_nacked <= 1'b0;
               ptr_nacked     <= 1'b0;
               rd_addr_nacked <= 1'b0;
               bus_held       <= 1'b0;
           end else begin
               // Every command is a single-cycle pulse.
               cmd_start   <= 1'b0;
               cmd_restart <= 1'b0;
               cmd_stop    <= 1'b0;
               cmd_byte    <= 1'b0;
               cmd_recv    <= 1'b0;
               data_valid  <= 1'b0;
               done        <= 1'b0;

               case (state)
                   CS_IDLE:
                       if (req) begin
                           busy           <= 1'b1;
                           bus_held       <= 1'b1;   // from the first S to the final P
                           wr_addr_nacked <= 1'b0;
                           ptr_nacked     <= 1'b0;
                           rd_addr_nacked <= 1'b0;
                           bytes_read     <= {LEN_W{1'b0}};
                           rd_index       <= {LEN_W{1'b0}};
                           data_index     <= {LEN_W{1'b0}};
                           ptr_index      <= 2'd0;
                           cmd_start      <= 1'b1;
                           state          <= CS_START;
                       end

                   CS_START:
                       if (framing_done) begin
                           cmd_byte      <= 1'b1;
                           cmd_byte_data <= addr_w;
                           state         <= CS_ADDR_W;
                       end

                   CS_ADDR_W:
                       if (byte_done) begin
                           if (!ack_received) begin
                               // Nobody is at this address. An ABORT, so the bus must be
                               // RELEASED -- a repeated START here would hold it.
                               wr_addr_nacked <= 1'b1;
                               cmd_stop       <= 1'b1;
                               state          <= CS_END;
                           end else begin
                               cmd_byte      <= 1'b1;
                               cmd_byte_data <= ptr_byte;   // ptr_index is still 0
                               state         <= CS_PTR;
                           end
                       end

                   CS_PTR:
                       if (byte_done) begin
                           if (!ack_received) begin
                               // The device refused the pointer -- NACK condition 3,
                               // exactly as Chapter 8.2's slave-receiver produces it.
                               // Abort with a STOP; there is nothing to read.
                               ptr_nacked <= 1'b1;
                               cmd_stop   <= 1'b1;
                               state      <= CS_END;
                           end else if ((ptr_index + 2'd1) < req_ptr_bytes) begin
                               // A second pointer byte. ptr_byte is combinational on
                               // ptr_index, so the NEW value is presented in the same
                               // cycle -- safe here because it is a mux over a register,
                               // not a memory read. Chapter 9.1 section 6a covers why the
                               // write sequencer's payload needed a fetch cycle and this
                               // does not.
                               ptr_index     <= ptr_index + 2'd1;
                               cmd_byte      <= 1'b1;
                               cmd_byte_data <= req_ptr[7:0];   // the second byte is the low half
                               state         <= CS_PTR;
                           end else begin
                               // THE JUNCTION. A repeated START, never a STOP: the bus is
                               // not released, so no other master can interpose and change
                               // the pointer we just wrote.
                               cmd_restart <= 1'b1;
                               state       <= CS_RESTART;
                           end
                       end

                   CS_RESTART:
                       if (framing_done) begin
                           cmd_byte      <= 1'b1;
                           cmd_byte_data <= addr_r;
                           state         <= CS_ADDR_R;
                       end

                   CS_ADDR_R:
                       if (byte_done) begin
                           if (!ack_received) begin
                               // The device answered the write phase and then refused the
                               // read addressing. Reported SEPARATELY from the phase-1
                               // NACK because it means something completely different --
                               // section 6a.
                               rd_addr_nacked <= 1'b1;
                               cmd_stop       <= 1'b1;
                               state          <= CS_END;
                           end else begin
                               // The direction has changed. From here this master is the
                               // receiver and owns every ninth bit.
                               cmd_recv <= 1'b1;
                               cmd_ack  <= ack_for({LEN_W{1'b0}});
                               state    <= CS_DATA;
                           end
                       end

                   CS_DATA:
                       if (byte_done) begin
                           data_out   <= byte_in;
                           data_valid <= 1'b1;
                           bytes_read <= bytes_read + 1'b1;

                           if (!ack_for(rd_index)) begin
                               cmd_stop <= 1'b1;
                               state    <= CS_END;
                           end else begin
                               rd_index   <= rd_index + 1'b1;
                               data_index <= rd_index + 1'b1;
                               cmd_recv   <= 1'b1;
                               cmd_ack    <= ack_for(rd_index + 1'b1);
                               state      <= CS_DATA;
                           end
                       end

                   CS_END:
                       if (framing_done) begin
                           busy     <= 1'b0;
                           bus_held <= 1'b0;   // the P has completed; the bus is free
                           done     <= 1'b1;
                           state    <= CS_IDLE;
                       end

                   default: state <= CS_IDLE;
               endcase
           end
       end
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_combined_sequencer_tb.v — the Verilog testbench, structurally identical
   `timescale 1ns/1ps
   // The two layers below the sequencer are modelled as two INDEPENDENT processes, one per
   // layer. Chapter 8.1 section 11 records why: a single clocked block containing blocking
   // waits is one thread of control and drops commands that arrive while it is suspended.
   module i2c_combined_sequencer_tb;   // Verilog-2001
       localparam LEN_W = 8;
       localparam FRAMING_CYCLES = 4;
       localparam BYTE_CYCLES    = 9;

       reg clk = 1'b0;
       always #5 clk = ~clk;

       reg rst_n = 1'b0;
       reg req = 1'b0;
       reg [6:0] req_addr = 7'h50;
       reg [1:0] req_ptr_bytes = 2'd1;
       reg [15:0] req_ptr = 16'h0000;
       reg [LEN_W-1:0] req_len = 8'd1;

       wire cmd_start, cmd_restart, cmd_stop, cmd_byte, cmd_recv, cmd_ack;
       wire [7:0] cmd_byte_data;
       reg framing_done = 1'b0, byte_done = 1'b0, ack_received = 1'b0;
       reg [7:0] byte_in = 8'h00;
       wire [LEN_W-1:0] data_index;
       wire [7:0] data_out;
       wire data_valid, busy, done;
       wire [LEN_W-1:0] bytes_read;
       wire wr_addr_nacked, ptr_nacked, rd_addr_nacked, bus_held;

       integer errors = 0;
       integer base;

       i2c_combined_sequencer #(.LEN_W(LEN_W)) dut (
           .clk(clk), .rst_n(rst_n), .req(req), .req_addr(req_addr),
           .req_ptr_bytes(req_ptr_bytes), .req_ptr(req_ptr), .req_len(req_len),
           .cmd_start(cmd_start), .cmd_restart(cmd_restart), .cmd_stop(cmd_stop),
           .cmd_byte(cmd_byte), .cmd_byte_data(cmd_byte_data), .cmd_recv(cmd_recv),
           .cmd_ack(cmd_ack), .framing_done(framing_done), .byte_done(byte_done),
           .ack_received(ack_received), .byte_in(byte_in), .data_index(data_index),
           .data_out(data_out), .data_valid(data_valid), .busy(busy), .done(done),
           .bytes_read(bytes_read), .wr_addr_nacked(wr_addr_nacked),
           .ptr_nacked(ptr_nacked), .rd_addr_nacked(rd_addr_nacked), .bus_held(bus_held));

       initial begin #400000; $display("FAIL: watchdog expired"); $finish; end

       // ---- observation -------------------------------------------------------------
       integer n_s = 0, n_sr = 0, n_p = 0, n_tx = 0, n_rx = 0, n_done = 0;
       reg [7:0] tx_log [0:15];      // every byte the master TRANSMITTED, in order
       reg       ack_log [0:15];     // the answer the master gave to each byte it READ
       reg [7:0] rx_log [0:15];
       integer n_rxlog = 0;

       // THE property of a combined transaction: a STOP must not appear between the first
       // START and the final one. This records WHEN each framing event happened relative to
       // the byte stream, so the test can assert on the ORDER and not merely on counts.
       // seq_log is the framing sequence as small codes -- 1 = S, 2 = Sr, 3 = P -- rather
       // than as characters, so the VHDL and Verilog testbenches can hold exactly the same
       // structure. Parity between the three is what makes the matching finish time mean
       // something, and a SystemVerilog-only string type would have broken it.
       localparam SEQ_S = 1, SEQ_R = 2, SEQ_P = 3;
       integer seq_log [0:15];
       integer n_seq = 0;

       reg obs_clear = 1'b0;
       always @(posedge clk) if (rst_n) begin
         if (obs_clear) begin
           n_s = 0; n_sr = 0; n_p = 0; n_tx = 0; n_rx = 0; n_rxlog = 0; n_seq = 0;
         end else begin
           if (cmd_start)   begin n_s = n_s + 1;  if (n_seq < 16) seq_log[n_seq] = SEQ_S; n_seq = n_seq + 1; end
           if (cmd_restart) begin n_sr = n_sr + 1; if (n_seq < 16) seq_log[n_seq] = SEQ_R; n_seq = n_seq + 1; end
           if (cmd_stop)    begin n_p = n_p + 1;  if (n_seq < 16) seq_log[n_seq] = SEQ_P; n_seq = n_seq + 1; end
           if (cmd_byte) begin if (n_tx < 16) tx_log[n_tx] = cmd_byte_data; n_tx = n_tx + 1; end
           if (cmd_recv) begin if (n_rx < 16) ack_log[n_rx] = cmd_ack;      n_rx = n_rx + 1; end
           if (data_valid) begin if (n_rxlog < 16) rx_log[n_rxlog] = data_out; n_rxlog = n_rxlog + 1; end
         end
         if (done) n_done = n_done + 1;
       end

       // A CONTINUOUS check: bus_held must be asserted from the first S until the final P.
       // If it ever drops while the sequencer still has work to do, the bus was released
       // mid-transaction -- which is the fault this whole module exists to prevent.
       integer held_violations = 0;
       always @(posedge clk) if (rst_n)
           if (busy && !bus_held) held_violations = held_violations + 1;

       // ---- the framing layer, an independent responder -----------------------------
       initial forever begin
           wait (cmd_start || cmd_restart || cmd_stop);
           repeat (FRAMING_CYCLES) @(posedge clk);
           framing_done <= 1'b1; @(posedge clk); framing_done <= 1'b0;
       end

       // ---- the byte layer, an independent responder --------------------------------
       reg slave_answer = 1'b1;      // the slave's answer to a TRANSMITTED byte
       reg is_recv;
       reg [7:0] slave_seed = 8'h00;
       reg       seed_load  = 1'b0;
       reg       rx_taken   = 1'b0;
       reg [7:0] slave_data = 8'h00;
       always @(posedge clk) begin
           if (seed_load)     slave_data <= slave_seed;
           else if (rx_taken) slave_data <= slave_data + 8'h11;
       end

       initial forever begin
           wait (cmd_byte || cmd_recv);
           is_recv = cmd_recv;
           if (is_recv) byte_in <= slave_data;
           repeat (BYTE_CYCLES) @(posedge clk);
           byte_done <= 1'b1;
           // For a RECEIVED byte the ninth bit is the master's own, so there is no slave
           // answer and this model drives X rather than a plausible value. A design that
           // consulted ack_received on a received byte then propagates X.
           ack_received <= is_recv ? 1'bx : slave_answer;
           rx_taken     <= is_recv;
           @(posedge clk);
           byte_done <= 1'b0;
           rx_taken  <= 1'b0;
       end

       task clear_obs; begin
           obs_clear = 1'b1; @(negedge clk); obs_clear = 1'b0; @(negedge clk);
       end endtask

       // Check the framing SEQUENCE, not merely the counts. "S then Sr then P" and
       // "S then P then S" have identical counts of each event and are completely
       // different transactions -- the whole subject of this module -- so only the order
       // distinguishes them.
       task check_seq;
           input integer e0; input integer e1; input integer e2; input integer n;
           input [8*40:1] what;
           begin
           if (n_seq !== n || seq_log[0] !== e0
               || (n > 1 && seq_log[1] !== e1) || (n > 2 && seq_log[2] !== e2)) begin
               $display("FAIL: %0s framing was %0d events [%0d %0d %0d], expected %0d [%0d %0d %0d]  (1=S 2=Sr 3=P)",
                        what, n_seq, seq_log[0], seq_log[1], seq_log[2], n, e0, e1, e2);
               errors = errors + 1;
           end
           end
       endtask

       task start_txn;
           input [6:0] a; input [1:0] pb; input [15:0] p; input [LEN_W-1:0] len;
           input [7:0] first_data;
           begin
           base = n_done;
           clear_obs;
           req_addr = a; req_ptr_bytes = pb; req_ptr = p; req_len = len;
           slave_seed = first_data;
           seed_load = 1'b1; @(negedge clk); seed_load = 1'b0; @(negedge clk);
           req = 1'b1; @(negedge clk); req = 1'b0;
           wait (n_done == base + 1);
           repeat (2) @(negedge clk);
           end
       endtask

       initial begin
           repeat (3) @(negedge clk);
           if (busy !== 1'b0) begin $display("FAIL: busy out of reset"); errors = errors + 1; end
           if (bus_held !== 1'b0) begin $display("FAIL: bus_held out of reset"); errors = errors + 1; end
           rst_n = 1'b1; @(negedge clk);

           // ---- 1: the canonical access. One pointer byte, three bytes read, from an
           //      EEPROM at 0x50. This is the shape the whole module is about.
           slave_answer = 1'b1;
           start_txn(7'h50, 2'd1, 16'h0012, 8'd3, 8'hA0);
           // THE assertion of this module: S, then Sr, then P. No STOP in between.
           check_seq(SEQ_S, SEQ_R, SEQ_P, 3, "the canonical access");
           if (n_p !== 1 || n_sr !== 1 || n_s !== 1) begin
               $display("FAIL: framing counts S=%0d Sr=%0d P=%0d, expected 1,1,1", n_s, n_sr, n_p);
               errors = errors + 1; end
           // three transmitted bytes: addr+W, the pointer, addr+R
           if (n_tx !== 3) begin
               $display("FAIL: transmitted %0d bytes, expected 3", n_tx); errors = errors + 1; end
           if (tx_log[0] !== 8'hA0) begin
               $display("FAIL: phase-1 address was 0x%02h, expected 0xa0 (0x50 << 1 | W)",
                        tx_log[0]); errors = errors + 1; end
           if (tx_log[1] !== 8'h12) begin
               $display("FAIL: pointer byte was 0x%02h, expected 0x12", tx_log[1]); errors = errors + 1; end
           if (tx_log[2] !== 8'hA1) begin
               $display("FAIL: phase-2 address was 0x%02h, expected 0xa1 (0x50 << 1 | R)",
                        tx_log[2]); errors = errors + 1; end
           // the read phase's acknowledge policy is unchanged from Chapter 9.1
           if (ack_log[0] !== 1'b1 || ack_log[1] !== 1'b1 || ack_log[2] !== 1'b0) begin
               $display("FAIL: ack pattern %b%b%b, expected 110 (ACK ACK NACK)",
                        ack_log[0], ack_log[1], ack_log[2]); errors = errors + 1; end
           if (rx_log[0] !== 8'hA0 || rx_log[1] !== 8'hB1 || rx_log[2] !== 8'hC2) begin
               $display("FAIL: payload 0x%02h,0x%02h,0x%02h, expected 0xa0,0xb1,0xc2",
                        rx_log[0], rx_log[1], rx_log[2]); errors = errors + 1; end
           if (bytes_read !== 8'd3) begin
               $display("FAIL: bytes_read = %0d, expected 3", bytes_read); errors = errors + 1; end
           if (bus_held !== 1'b0) begin
               $display("FAIL: bus_held still asserted after the final P"); errors = errors + 1; end

           // ---- 2: a TWO-BYTE pointer, as every larger serial memory uses. The bytes
           //      must go MOST SIGNIFICANT FIRST, and there must still be exactly one Sr.
           start_txn(7'h50, 2'd2, 16'h1234, 8'd2, 8'h5A);
           check_seq(SEQ_S, SEQ_R, SEQ_P, 3, "a 2-byte pointer");
           if (n_tx !== 4) begin
               $display("FAIL: transmitted %0d bytes, expected 4 (addr, 2 ptr, addr)", n_tx);
               errors = errors + 1; end
           if (tx_log[1] !== 8'h12 || tx_log[2] !== 8'h34) begin
               $display("FAIL: pointer bytes 0x%02h,0x%02h -- expected 0x12,0x34 (MSB first)",
                        tx_log[1], tx_log[2]); errors = errors + 1; end
           if (tx_log[3] !== 8'hA1) begin
               $display("FAIL: phase-2 address after a 2-byte pointer was 0x%02h", tx_log[3]);
               errors = errors + 1; end
           if (bytes_read !== 8'd2) begin
               $display("FAIL: bytes_read = %0d, expected 2", bytes_read); errors = errors + 1; end

           // ---- 3: a ONE-BYTE read. The read phase's only acknowledge is a NACK. This is
           //      the single most common real transaction: read one status register.
           start_txn(7'h48, 2'd1, 16'h0001, 8'd1, 8'h77);
           check_seq(SEQ_S, SEQ_R, SEQ_P, 3, "a 1-byte read");
           if (ack_log[0] !== 1'b0) begin
               $display("FAIL: the only byte of a 1-byte read was ACKed, not NACKed"); errors = errors + 1; end
           if (rx_log[0] !== 8'h77 || bytes_read !== 8'd1) begin
               $display("FAIL: 1-byte read returned 0x%02h, count %0d", rx_log[0], bytes_read);
               errors = errors + 1; end

           // ---- 4: the PHASE-1 ADDRESS is NACKed. Nobody is there. This is an ABORT, so
           //      the terminator must be a STOP -- the bus MUST be released. An Sr here
           //      would hold the bus with no transaction to continue.
           slave_answer = 1'b0;
           start_txn(7'h50, 2'd1, 16'h0012, 8'd3, 8'h00);
           check_seq(SEQ_S, SEQ_P, 0, 2, "an abort at the phase-1 address");
           if (n_sr !== 0) begin
               $display("FAIL: an abort emitted %0d repeated STARTs -- it must emit a STOP",
                        n_sr); errors = errors + 1; end
           if (wr_addr_nacked !== 1'b1) begin
               $display("FAIL: wr_addr_nacked not reported"); errors = errors + 1; end
           if (ptr_nacked !== 1'b0 || rd_addr_nacked !== 1'b0) begin
               $display("FAIL: a phase-1 address NACK set another failure flag too"); errors = errors + 1; end
           if (n_tx !== 1) begin
               $display("FAIL: %0d bytes transmitted after an address NACK, expected 1", n_tx);
               errors = errors + 1; end
           if (bytes_read !== 8'd0) begin
               $display("FAIL: bytes_read = %0d after an aborted transaction", bytes_read);
               errors = errors + 1; end
           if (bus_held !== 1'b0) begin
               $display("FAIL: the bus was left HELD after an abort"); errors = errors + 1; end

           // ---- 5: the POINTER byte is NACKed. The device is there and refused the
           //      pointer -- condition 3. Distinct from test 4, and it must be reported so.
           begin
               base = n_done;
               clear_obs;
               slave_answer = 1'b1;
               req_addr = 7'h50; req_ptr_bytes = 2'd1; req_ptr = 16'h00FF; req_len = 8'd3;
               req = 1'b1; @(negedge clk); req = 1'b0;
               // let the address byte be ACKed, then refuse the pointer
               wait (n_tx == 1); wait (byte_done); @(negedge clk);
               slave_answer = 1'b0;
               wait (n_done == base + 1);
               repeat (2) @(negedge clk);
               slave_answer = 1'b1;
           end
           check_seq(SEQ_S, SEQ_P, 0, 2, "an abort on the pointer byte");
           if (ptr_nacked !== 1'b1) begin
               $display("FAIL: ptr_nacked not reported"); errors = errors + 1; end
           if (wr_addr_nacked !== 1'b0) begin
               $display("FAIL: a pointer NACK was reported as an address NACK"); errors = errors + 1; end
           if (n_rx !== 0) begin
               $display("FAIL: read %0d bytes after the pointer was refused", n_rx); errors = errors + 1; end

           // ---- 6: the PHASE-2 address is NACKed. The device took the pointer and then
           //      refused the read addressing -- a real and distinct condition, which
           //      section 6a argues is the most diagnostic of the three.
           begin
               base = n_done;
               clear_obs;
               slave_answer = 1'b1;
               req_addr = 7'h50; req_ptr_bytes = 2'd1; req_ptr = 16'h0020; req_len = 8'd2;
               req = 1'b1; @(negedge clk); req = 1'b0;
               wait (n_tx == 2); wait (byte_done); @(negedge clk);   // addr+W and ptr ACKed
               slave_answer = 1'b0;                                  // refuse addr+R
               wait (n_done == base + 1);
               repeat (2) @(negedge clk);
               slave_answer = 1'b1;
           end
           check_seq(SEQ_S, SEQ_R, SEQ_P, 3, "an abort at the phase-2 address");
           if (rd_addr_nacked !== 1'b1) begin
               $display("FAIL: rd_addr_nacked not reported"); errors = errors + 1; end
           if (wr_addr_nacked !== 1'b0 || ptr_nacked !== 1'b0) begin
               $display("FAIL: a phase-2 NACK was attributed to phase 1"); errors = errors + 1; end
           if (n_rx !== 0) begin
               $display("FAIL: read %0d bytes after the read addressing was refused", n_rx);
               errors = errors + 1; end
           if (bus_held !== 1'b0) begin
               $display("FAIL: the bus was left held after a phase-2 abort"); errors = errors + 1; end

           // ---- 7: back-to-back transactions, and the flags must CLEAR. A stale
           //      wr_addr_nacked from test 4 would make a healthy transaction look failed.
           start_txn(7'h50, 2'd1, 16'h0030, 8'd2, 8'h11);
           check_seq(SEQ_S, SEQ_R, SEQ_P, 3, "the transaction after an abort");
           if (wr_addr_nacked !== 1'b0 || ptr_nacked !== 1'b0 || rd_addr_nacked !== 1'b0) begin
               $display("FAIL: a failure flag survived into the next transaction"); errors = errors + 1; end
           if (bytes_read !== 8'd2) begin
               $display("FAIL: bytes_read = %0d, expected 2", bytes_read); errors = errors + 1; end
           if (n_done !== 7) begin
               $display("FAIL: %0d done pulses across 7 transactions", n_done); errors = errors + 1; end

           // ---- the continuous check: the bus was never released while work remained.
           if (held_violations !== 0) begin
               $display("FAIL: bus_held dropped %0d times while the sequencer was busy",
                        held_violations); errors = errors + 1; end

           if (errors == 0)
               $display("PASS: junction is Sr and abort is P, pointer bytes MSB first, all three NACK points distinguished, bus never released mid-transaction");
           else $display("FAIL: %0d error(s)", errors);
           $finish;
       end
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_combined_sequencer.vhd — the same sequencer in VHDL
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   -- The WRITE-THEN-READ transaction: the shape almost every real device access takes.
   --
   --     S  addr+W  A  ptr[..]  A  Sr  addr+R  A  d0 A  d1 A ... dn N  P
   --        \___________________/      \_______________________________/
   --           phase 1: write            phase 2: read
   --                     the junction is Sr, NOT P then S
   --
   -- This block composes Chapter 8.1's write sequencer and Chapter 9.1's read sequencer
   -- into ONE transaction that never releases the bus between the phases. That is the
   -- whole point of the format, and the specification describes it directly:
   --
   --   "Combined formats can be used, for example, to control a serial memory. The
   --    internal memory location must be written during the first data byte. After the
   --    START condition and slave address is repeated, data can be transferred."
   --
   --   "During a change of direction within a transfer, the START condition and the slave
   --    address are both repeated, but with the R/W bit reversed."
   --
   -- Two asymmetries are load-bearing and are easy to get backwards:
   --
   --   the PHASE JUNCTION is a repeated START -- the bus is never released;
   --   an ABORT is a STOP           -- the bus MUST be released, or it stays held.
   entity i2c_combined_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 the transaction
           req_addr : in std_logic_vector(6 downto 0);
           -- Pointer width is a DEVICE property, not a bus one. A sensor with sixteen
           -- registers takes one byte; a 64 kbit EEPROM takes two, most significant first.
           req_ptr_bytes : in unsigned(1 downto 0);            -- 1 or 2
           req_ptr       : in std_logic_vector(15 downto 0);   -- big-endian: [15:8] first
           req_len       : in unsigned(LEN_W - 1 downto 0);    -- payload bytes; >= 1

           -- commands to the framing sequencer (5.5) and byte engine (7.1)
           cmd_start     : out std_logic;   -- pulse: emit S
           cmd_restart   : out std_logic;   -- pulse: emit Sr -- the phase junction
           cmd_stop      : out std_logic;   -- pulse: emit P  -- the END, or an abort
           cmd_byte      : out std_logic;   -- pulse: TRANSMIT one byte
           cmd_byte_data : out std_logic_vector(7 downto 0);
           cmd_recv      : out std_logic;   -- pulse: RECEIVE one byte
           cmd_ack       : out std_logic;

           -- completions from those layers
           framing_done : in std_logic;
           byte_done    : in std_logic;
           ack_received : in std_logic;
           byte_in      : in std_logic_vector(7 downto 0);

           -- payload sink
           data_index : out unsigned(LEN_W - 1 downto 0);
           data_out   : out std_logic_vector(7 downto 0);
           data_valid : out std_logic;

           -- status
           busy       : out std_logic;
           done       : out std_logic;
           bytes_read : out unsigned(LEN_W - 1 downto 0);
           -- Three DISTINCT failure points, because they call for three different
           -- responses. A combined transaction has one more place to fail than a plain
           -- read does, and collapsing them loses exactly the information a caller needs.
           wr_addr_nacked : out std_logic;   -- phase 1 address: nobody is there
           ptr_nacked     : out std_logic;   -- the pointer byte was refused
           rd_addr_nacked : out std_logic;   -- phase 2 address NACKed
           bus_held       : out std_logic    -- high from the first S until the final P
       );
   end entity;

   architecture rtl of i2c_combined_sequencer is
       type state_t is (
           CS_IDLE,
           CS_START,     -- waiting for the S
           CS_ADDR_W,    -- address + W in flight; the slave answers
           CS_PTR,       -- a pointer byte in flight; the slave answers
           CS_RESTART,   -- waiting for the Sr -- the junction, never a STOP
           CS_ADDR_R,    -- address + R in flight; the slave answers
           CS_DATA,      -- a payload byte arriving; THIS master answers
           CS_END        -- waiting for the final P, or for an abort's P
       );
       signal state : state_t := CS_IDLE;

       constant ZERO_LEN : unsigned(LEN_W - 1 downto 0) := (others => '0');

       signal ptr_index : unsigned(1 downto 0) := (others => '0');
       signal rd_index  : unsigned(LEN_W - 1 downto 0) := (others => '0');
       signal idx_out   : unsigned(LEN_W - 1 downto 0) := (others => '0');

       signal addr_w, addr_r : std_logic_vector(7 downto 0);

       -- Pointer bytes go MOST SIGNIFICANT FIRST, matching the byte order of the bus
       -- itself (Chapter 7.1) and the convention every two-byte serial memory uses.
       signal ptr_byte : std_logic_vector(7 downto 0);

       -- The acknowledge policy of the READ phase, unchanged from Chapter 9.1: ACK a byte
       -- if and only if another is wanted after it, and NACK the last.
       function ack_for (i : unsigned; len : unsigned) return std_logic is
       begin
           if (i + 1) < len then return '1'; else return '0'; end if;
       end function;
   begin
       addr_w <= req_addr & '0';
       addr_r <= req_addr & '1';

       ptr_byte <= req_ptr(15 downto 8)
                   when (req_ptr_bytes = to_unsigned(2, 2) and ptr_index = to_unsigned(0, 2))
                   else req_ptr(7 downto 0);

       data_index <= idx_out;

       process (clk)
       begin
           if rising_edge(clk) then
               if rst_n = '0' then
                   state          <= CS_IDLE;
                   cmd_start      <= '0';
                   cmd_restart    <= '0';
                   cmd_stop       <= '0';
                   cmd_byte       <= '0';
                   cmd_recv       <= '0';
                   cmd_ack        <= '0';
                   cmd_byte_data  <= (others => '0');
                   ptr_index      <= (others => '0');
                   rd_index       <= (others => '0');
                   idx_out        <= (others => '0');
                   data_out       <= (others => '0');
                   data_valid     <= '0';
                   busy           <= '0';
                   done           <= '0';
                   bytes_read     <= (others => '0');
                   wr_addr_nacked <= '0';
                   ptr_nacked     <= '0';
                   rd_addr_nacked <= '0';
                   bus_held       <= '0';
               else
                   -- Every command is a single-cycle pulse.
                   cmd_start   <= '0';
                   cmd_restart <= '0';
                   cmd_stop    <= '0';
                   cmd_byte    <= '0';
                   cmd_recv    <= '0';
                   data_valid  <= '0';
                   done        <= '0';

                   case state is
                       when CS_IDLE =>
                           if req = '1' then
                               busy           <= '1';
                               bus_held       <= '1';   -- from the first S to the final P
                               wr_addr_nacked <= '0';
                               ptr_nacked     <= '0';
                               rd_addr_nacked <= '0';
                               bytes_read     <= (others => '0');
                               rd_index       <= (others => '0');
                               idx_out        <= (others => '0');
                               ptr_index      <= (others => '0');
                               cmd_start      <= '1';
                               state          <= CS_START;
                           end if;

                       when CS_START =>
                           if framing_done = '1' then
                               cmd_byte      <= '1';
                               cmd_byte_data <= addr_w;
                               state         <= CS_ADDR_W;
                           end if;

                       when CS_ADDR_W =>
                           if byte_done = '1' then
                               if ack_received /= '1' then
                                   -- Nobody is at this address. An ABORT, so the bus must
                                   -- be RELEASED -- a repeated START here would hold it.
                                   wr_addr_nacked <= '1';
                                   cmd_stop       <= '1';
                                   state          <= CS_END;
                               else
                                   cmd_byte      <= '1';
                                   cmd_byte_data <= ptr_byte;   -- ptr_index is still 0
                                   state         <= CS_PTR;
                               end if;
                           end if;

                       when CS_PTR =>
                           if byte_done = '1' then
                               if ack_received /= '1' then
                                   -- The device refused the pointer -- NACK condition 3,
                                   -- exactly as Chapter 8.2's slave-receiver produces it.
                                   ptr_nacked <= '1';
                                   cmd_stop   <= '1';
                                   state      <= CS_END;
                               elsif (ptr_index + 1) < req_ptr_bytes then
                                   -- A second pointer byte. ptr_byte is combinational on
                                   -- ptr_index, so the NEW value is presented in the same
                                   -- cycle -- safe because it is a mux over a register,
                                   -- not a memory read.
                                   ptr_index     <= ptr_index + 1;
                                   cmd_byte      <= '1';
                                   cmd_byte_data <= req_ptr(7 downto 0);
                                   state         <= CS_PTR;
                               else
                                   -- THE JUNCTION. A repeated START, never a STOP: the bus
                                   -- is not released, so no other master can interpose and
                                   -- change the pointer we just wrote.
                                   cmd_restart <= '1';
                                   state       <= CS_RESTART;
                               end if;
                           end if;

                       when CS_RESTART =>
                           if framing_done = '1' then
                               cmd_byte      <= '1';
                               cmd_byte_data <= addr_r;
                               state         <= CS_ADDR_R;
                           end if;

                       when CS_ADDR_R =>
                           if byte_done = '1' then
                               if ack_received /= '1' then
                                   -- The device answered the write phase and then refused
                                   -- the read addressing. Reported SEPARATELY because it
                                   -- means something completely different.
                                   rd_addr_nacked <= '1';
                                   cmd_stop       <= '1';
                                   state          <= CS_END;
                               else
                                   -- The direction has changed. From here this master is
                                   -- the receiver and owns every ninth bit.
                                   cmd_recv <= '1';
                                   cmd_ack  <= ack_for(ZERO_LEN, req_len);
                                   state    <= CS_DATA;
                               end if;
                           end if;

                       when CS_DATA =>
                           if byte_done = '1' then
                               data_out   <= byte_in;
                               data_valid <= '1';
                               bytes_read <= bytes_read + 1;

                               if ack_for(rd_index, req_len) = '0' then
                                   cmd_stop <= '1';
                                   state    <= CS_END;
                               else
                                   rd_index <= rd_index + 1;
                                   idx_out  <= rd_index + 1;
                                   cmd_recv <= '1';
                                   cmd_ack  <= ack_for(rd_index + 1, req_len);
                                   state    <= CS_DATA;
                               end if;
                           end if;

                       when CS_END =>
                           if framing_done = '1' then
                               busy     <= '0';
                               bus_held <= '0';   -- the P has completed; the bus is free
                               done     <= '1';
                               state    <= CS_IDLE;
                           end if;
                   end case;
               end if;
           end if;
       end process;
   end architecture;
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_combined_sequencer_tb.vhd — the VHDL testbench, single-writer throughout
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   -- The two layers below the sequencer are modelled as two INDEPENDENT processes, one per
   -- layer. Chapter 8.1 section 11 records why: a single process containing waits is one
   -- thread of control and drops commands that arrive while it is suspended.
   --
   -- Every signal here has exactly ONE driving process, because VHDL permits one driver per
   -- signal -- and the SystemVerilog and Verilog testbenches were written to the same
   -- discipline so that the matching finish time between the three means something.
   entity i2c_combined_sequencer_tb is
   end entity;

   architecture sim of i2c_combined_sequencer_tb is
       constant LEN_W          : positive := 8;
       constant FRAMING_CYCLES : positive := 4;
       constant BYTE_CYCLES    : positive := 9;

       -- The framing sequence is recorded as small codes rather than characters, so all
       -- three testbenches can hold exactly the same structure.
       constant SEQ_S : natural := 1;
       constant SEQ_R : natural := 2;
       constant SEQ_P : natural := 3;

       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) := "1010000";   -- 0x50
       signal req_ptr_bytes : unsigned(1 downto 0) := to_unsigned(1, 2);
       signal req_ptr       : std_logic_vector(15 downto 0) := (others => '0');
       signal req_len       : unsigned(LEN_W - 1 downto 0) := to_unsigned(1, LEN_W);

       signal cmd_start, cmd_restart, cmd_stop, cmd_byte, cmd_recv, cmd_ack : 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 byte_in      : std_logic_vector(7 downto 0) := (others => '0');
       signal data_index   : unsigned(LEN_W - 1 downto 0);
       signal data_out     : std_logic_vector(7 downto 0);
       signal data_valid, busy, done : std_logic;
       signal bytes_read   : unsigned(LEN_W - 1 downto 0);
       signal wr_addr_nacked, ptr_nacked, rd_addr_nacked, bus_held : std_logic;

       -- observation, owned solely by the observe process
       type byte_arr is array (0 to 15) of std_logic_vector(7 downto 0);
       type bit_arr  is array (0 to 15) of std_logic;
       type nat_arr  is array (0 to 15) of natural;
       signal n_s, n_sr, n_p, n_tx, n_rx, n_done, n_rxlog, n_seq : natural := 0;
       signal tx_log  : byte_arr := (others => (others => '0'));
       signal ack_log : bit_arr  := (others => '0');
       signal rx_log  : byte_arr := (others => (others => '0'));
       signal seq_log : nat_arr  := (others => 0);
       signal obs_clear : std_logic := '0';

       -- the continuous bus-held check, owned solely by its own process
       signal held_violations : natural := 0;

       -- the far end, owned solely by the byte process
       signal slave_answer : std_logic := '1';
       signal is_recv      : std_logic := '0';
       signal rx_taken     : std_logic := '0';

       -- the payload source: one owning process, fed by a request pulse from the stimulus
       signal slave_seed : std_logic_vector(7 downto 0) := (others => '0');
       signal seed_load  : std_logic := '0';
       signal slave_data : std_logic_vector(7 downto 0) := (others => '0');

       signal test_done : std_logic := '0';
   begin
       dut : entity work.i2c_combined_sequencer
           generic map (LEN_W => LEN_W)
           port map (clk => clk, rst_n => rst_n, req => req, req_addr => req_addr,
                     req_ptr_bytes => req_ptr_bytes, req_ptr => req_ptr, req_len => req_len,
                     cmd_start => cmd_start, cmd_restart => cmd_restart,
                     cmd_stop => cmd_stop, cmd_byte => cmd_byte,
                     cmd_byte_data => cmd_byte_data, cmd_recv => cmd_recv,
                     cmd_ack => cmd_ack, framing_done => framing_done,
                     byte_done => byte_done, ack_received => ack_received,
                     byte_in => byte_in, data_index => data_index, data_out => data_out,
                     data_valid => data_valid, busy => busy, done => done,
                     bytes_read => bytes_read, wr_addr_nacked => wr_addr_nacked,
                     ptr_nacked => ptr_nacked, rd_addr_nacked => rd_addr_nacked,
                     bus_held => bus_held);

       clk <= not clk after 5 ns;

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

       -- ---- observation --------------------------------------------------------------
       observe : process (clk)
       begin
           if rising_edge(clk) and rst_n = '1' then
             if obs_clear = '1' then
               n_s <= 0; n_sr <= 0; n_p <= 0; n_tx <= 0; n_rx <= 0; n_rxlog <= 0; n_seq <= 0;
             else
               if cmd_start = '1' then
                   n_s <= n_s + 1;
                   if n_seq < 16 then seq_log(n_seq) <= SEQ_S; end if;
                   n_seq <= n_seq + 1;
               elsif cmd_restart = '1' then
                   n_sr <= n_sr + 1;
                   if n_seq < 16 then seq_log(n_seq) <= SEQ_R; end if;
                   n_seq <= n_seq + 1;
               elsif cmd_stop = '1' then
                   n_p <= n_p + 1;
                   if n_seq < 16 then seq_log(n_seq) <= SEQ_P; end if;
                   n_seq <= n_seq + 1;
               end if;
               if cmd_byte = '1' then
                   if n_tx < 16 then tx_log(n_tx) <= cmd_byte_data; end if;
                   n_tx <= n_tx + 1;
               end if;
               if cmd_recv = '1' then
                   if n_rx < 16 then ack_log(n_rx) <= cmd_ack; end if;
                   n_rx <= n_rx + 1;
               end if;
               if data_valid = '1' then
                   if n_rxlog < 16 then rx_log(n_rxlog) <= data_out; end if;
                   n_rxlog <= n_rxlog + 1;
               end if;
             end if;
             if done = '1' then n_done <= n_done + 1; end if;
           end if;
       end process;

       -- A CONTINUOUS check: bus_held must be asserted from the first S until the final P.
       -- If it ever drops while the sequencer still has work to do, the bus was released
       -- mid-transaction -- the fault this whole module exists to prevent.
       heldcheck : process (clk)
       begin
           if rising_edge(clk) and rst_n = '1' then
               if busy = '1' and bus_held = '0' then
                   held_violations <= held_violations + 1;
               end if;
           end if;
       end process;

       -- ---- the framing layer, an independent responder -----------------------------
       framing : process
       begin
           wait until cmd_start = '1' or cmd_restart = '1' or cmd_stop = '1';
           for i in 1 to FRAMING_CYCLES loop wait until rising_edge(clk); end loop;
           framing_done <= '1';
           wait until rising_edge(clk);
           framing_done <= '0';
       end process;

       -- ---- the byte layer, an independent responder --------------------------------
       bytelayer : process
           variable recv : std_logic;
       begin
           wait until cmd_byte = '1' or cmd_recv = '1';
           recv    := cmd_recv;
           is_recv <= recv;
           if recv = '1' then byte_in <= slave_data; end if;
           for i in 1 to BYTE_CYCLES loop wait until rising_edge(clk); end loop;
           byte_done <= '1';
           -- For a RECEIVED byte the ninth bit is the master's own, so there is no slave
           -- answer and this model drives X rather than a plausible value.
           if recv = '1' then ack_received <= 'X'; else ack_received <= slave_answer; end if;
           rx_taken <= recv;
           wait until rising_edge(clk);
           byte_done <= '0';
           rx_taken  <= '0';
       end process;

       -- ---- the payload source, single owner ----------------------------------------
       payload : process (clk)
       begin
           if rising_edge(clk) then
               if seed_load = '1' then
                   slave_data <= slave_seed;
               elsif rx_taken = '1' then
                   slave_data <= std_logic_vector(unsigned(slave_data) + 16#11#);
               end if;
           end if;
       end process;

       -- ---- stimulus ----------------------------------------------------------------
       stim : process
           variable errs : natural := 0;
           variable base : natural;

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

           procedure clear_obs is
           begin
               obs_clear <= '1'; waitn(1); obs_clear <= '0'; waitn(1);
           end procedure;

           -- Check the framing SEQUENCE, not merely the counts. "S then Sr then P" and
           -- "S then P then S" have identical counts of each event and are completely
           -- different transactions -- the whole subject of this module -- so only the
           -- order distinguishes them.
           procedure check_seq (e0, e1, e2, n : in natural; what : in string) is
           begin
               if n_seq /= n or seq_log(0) /= e0
                  or (n > 1 and seq_log(1) /= e1) or (n > 2 and seq_log(2) /= e2) then
                   report what & " framing was " & integer'image(n_seq) & " events ["
                        & integer'image(seq_log(0)) & " " & integer'image(seq_log(1)) & " "
                        & integer'image(seq_log(2)) & "], expected " & integer'image(n)
                        & "  (1=S 2=Sr 3=P)" severity error;
                   errs := errs + 1;
               end if;
           end procedure;

           procedure start_txn (a : in natural; pb : in natural; p : in natural;
                                len : in natural; first_data : in std_logic_vector(7 downto 0)) is
           begin
               base := n_done;
               clear_obs;
               req_addr      <= std_logic_vector(to_unsigned(a, 7));
               req_ptr_bytes <= to_unsigned(pb, 2);
               req_ptr       <= std_logic_vector(to_unsigned(p, 16));
               req_len       <= to_unsigned(len, LEN_W);
               slave_seed    <= first_data;
               seed_load <= '1'; waitn(1); seed_load <= '0'; waitn(1);
               req <= '1'; waitn(1); req <= '0';
               wait until n_done = base + 1;
               waitn(2);
           end procedure;
       begin
           waitn(3);
           if busy /= '0' then
               report "busy out of reset" severity error; errs := errs + 1; end if;
           if bus_held /= '0' then
               report "bus_held out of reset" severity error; errs := errs + 1; end if;
           rst_n <= '1'; waitn(1);

           -- 1: the canonical access. One pointer byte, three bytes read, from an EEPROM at
           -- 0x50. This is the shape the whole module is about.
           slave_answer <= '1';
           start_txn(16#50#, 1, 16#0012#, 3, x"A0");
           -- THE assertion of this module: S, then Sr, then P. No STOP in between.
           check_seq(SEQ_S, SEQ_R, SEQ_P, 3, "the canonical access");
           if n_p /= 1 or n_sr /= 1 or n_s /= 1 then
               report "framing counts wrong -- expected one S, one Sr, one P" severity error;
               errs := errs + 1; end if;
           if n_tx /= 3 then
               report "transmitted the wrong number of bytes, expected 3" severity error;
               errs := errs + 1; end if;
           if tx_log(0) /= x"A0" then
               report "phase-1 address wrong, expected 0xa0" severity error; errs := errs + 1; end if;
           if tx_log(1) /= x"12" then
               report "pointer byte wrong, expected 0x12" severity error; errs := errs + 1; end if;
           if tx_log(2) /= x"A1" then
               report "phase-2 address wrong, expected 0xa1" severity error; errs := errs + 1; end if;
           if ack_log(0) /= '1' or ack_log(1) /= '1' or ack_log(2) /= '0' then
               report "read-phase ack pattern wrong -- expected ACK ACK NACK" severity error;
               errs := errs + 1; end if;
           if rx_log(0) /= x"A0" or rx_log(1) /= x"B1" or rx_log(2) /= x"C2" then
               report "payload wrong -- expected 0xa0, 0xb1, 0xc2" severity error;
               errs := errs + 1; end if;
           if bytes_read /= to_unsigned(3, LEN_W) then
               report "bytes_read wrong, expected 3" severity error; errs := errs + 1; end if;
           if bus_held /= '0' then
               report "bus_held still asserted after the final P" severity error;
               errs := errs + 1; end if;

           -- 2: a TWO-BYTE pointer, as every larger serial memory uses. The bytes must go
           -- MOST SIGNIFICANT FIRST, and there must still be exactly one Sr.
           start_txn(16#50#, 2, 16#1234#, 2, x"5A");
           check_seq(SEQ_S, SEQ_R, SEQ_P, 3, "a 2-byte pointer");
           if n_tx /= 4 then
               report "expected 4 transmitted bytes (addr, 2 ptr, addr)" severity error;
               errs := errs + 1; end if;
           if tx_log(1) /= x"12" or tx_log(2) /= x"34" then
               report "pointer bytes wrong -- expected 0x12 then 0x34, MSB first" severity error;
               errs := errs + 1; end if;
           if tx_log(3) /= x"A1" then
               report "phase-2 address after a 2-byte pointer wrong" severity error;
               errs := errs + 1; end if;
           if bytes_read /= to_unsigned(2, LEN_W) then
               report "bytes_read wrong, expected 2" severity error; errs := errs + 1; end if;

           -- 3: a ONE-BYTE read. The read phase's only acknowledge is a NACK. This is the
           -- single most common real transaction: read one status register.
           start_txn(16#48#, 1, 16#0001#, 1, x"77");
           check_seq(SEQ_S, SEQ_R, SEQ_P, 3, "a 1-byte read");
           if ack_log(0) /= '0' then
               report "the only byte of a 1-byte read was ACKed, not NACKed" severity error;
               errs := errs + 1; end if;
           if rx_log(0) /= x"77" or bytes_read /= to_unsigned(1, LEN_W) then
               report "1-byte read returned the wrong value or count" severity error;
               errs := errs + 1; end if;

           -- 4: the PHASE-1 ADDRESS is NACKed. Nobody is there. This is an ABORT, so the
           -- terminator must be a STOP -- the bus MUST be released. An Sr here would hold
           -- the bus with no transaction to continue.
           slave_answer <= '0';
           start_txn(16#50#, 1, 16#0012#, 3, x"00");
           check_seq(SEQ_S, SEQ_P, 0, 2, "an abort at the phase-1 address");
           if n_sr /= 0 then
               report "an abort emitted a repeated START -- it must emit a STOP" severity error;
               errs := errs + 1; end if;
           if wr_addr_nacked /= '1' then
               report "wr_addr_nacked not reported" severity error; errs := errs + 1; end if;
           if ptr_nacked /= '0' or rd_addr_nacked /= '0' then
               report "a phase-1 address NACK set another failure flag too" severity error;
               errs := errs + 1; end if;
           if n_tx /= 1 then
               report "too many bytes transmitted after an address NACK" severity error;
               errs := errs + 1; end if;
           if bytes_read /= to_unsigned(0, LEN_W) then
               report "bytes_read nonzero after an aborted transaction" severity error;
               errs := errs + 1; end if;
           if bus_held /= '0' then
               report "the bus was left HELD after an abort" severity error; errs := errs + 1; end if;

           -- 5: the POINTER byte is NACKed. The device is there and refused the pointer --
           -- condition 3. Distinct from test 4, and it must be reported so.
           base := n_done;
           clear_obs;
           slave_answer  <= '1';
           req_addr      <= std_logic_vector(to_unsigned(16#50#, 7));
           req_ptr_bytes <= to_unsigned(1, 2);
           req_ptr       <= std_logic_vector(to_unsigned(16#00FF#, 16));
           req_len       <= to_unsigned(3, LEN_W);
           req <= '1'; waitn(1); req <= '0';
           wait until n_tx = 1;
           wait until byte_done = '1';
           waitn(1);
           slave_answer <= '0';
           wait until n_done = base + 1;
           waitn(2);
           slave_answer <= '1';
           check_seq(SEQ_S, SEQ_P, 0, 2, "an abort on the pointer byte");
           if ptr_nacked /= '1' then
               report "ptr_nacked not reported" severity error; errs := errs + 1; end if;
           if wr_addr_nacked /= '0' then
               report "a pointer NACK was reported as an address NACK" severity error;
               errs := errs + 1; end if;
           if n_rx /= 0 then
               report "read bytes after the pointer was refused" severity error;
               errs := errs + 1; end if;

           -- 6: the PHASE-2 address is NACKed. The device took the pointer and then refused
           -- the read addressing -- a real and distinct condition.
           base := n_done;
           clear_obs;
           slave_answer  <= '1';
           req_addr      <= std_logic_vector(to_unsigned(16#50#, 7));
           req_ptr_bytes <= to_unsigned(1, 2);
           req_ptr       <= std_logic_vector(to_unsigned(16#0020#, 16));
           req_len       <= to_unsigned(2, LEN_W);
           req <= '1'; waitn(1); req <= '0';
           wait until n_tx = 2;
           wait until byte_done = '1';
           waitn(1);
           slave_answer <= '0';                 -- refuse addr+R
           wait until n_done = base + 1;
           waitn(2);
           slave_answer <= '1';
           check_seq(SEQ_S, SEQ_R, SEQ_P, 3, "an abort at the phase-2 address");
           if rd_addr_nacked /= '1' then
               report "rd_addr_nacked not reported" severity error; errs := errs + 1; end if;
           if wr_addr_nacked /= '0' or ptr_nacked /= '0' then
               report "a phase-2 NACK was attributed to phase 1" severity error;
               errs := errs + 1; end if;
           if n_rx /= 0 then
               report "read bytes after the read addressing was refused" severity error;
               errs := errs + 1; end if;
           if bus_held /= '0' then
               report "the bus was left held after a phase-2 abort" severity error;
               errs := errs + 1; end if;

           -- 7: back-to-back transactions, and the flags must CLEAR. A stale wr_addr_nacked
           -- from test 4 would make a healthy transaction look failed.
           start_txn(16#50#, 1, 16#0030#, 2, x"11");
           check_seq(SEQ_S, SEQ_R, SEQ_P, 3, "the transaction after an abort");
           if wr_addr_nacked /= '0' or ptr_nacked /= '0' or rd_addr_nacked /= '0' then
               report "a failure flag survived into the next transaction" severity error;
               errs := errs + 1; end if;
           if bytes_read /= to_unsigned(2, LEN_W) then
               report "bytes_read wrong, expected 2" severity error; errs := errs + 1; end if;
           if n_done /= 7 then
               report "wrong number of done pulses across seven transactions" severity error;
               errs := errs + 1; end if;

           -- the continuous check: the bus was never released while work remained.
           if held_violations /= 0 then
               report "bus_held dropped while the sequencer was busy" severity error;
               errs := errs + 1; end if;

           if errs = 0 then
               report "i2c_combined_sequencer self-check complete: junction is Sr and abort "
                    & "is P, pointer bytes MSB first, all three NACK points distinguished, "
                    & "bus never released mid-transaction" severity note;
           else
               report "i2c_combined_sequencer self-check FAILED" severity error;
           end if;
           test_done <= '1';
           wait;
       end process;
   end architecture;

6a. Five Decisions Worth Defending

cmd_restart and cmd_stop are separate outputs. They could have been one "terminate this phase" command with a modifier, and separating them makes the wrong one impossible to issue by accident: the junction path names cmd_restart and every abort path names cmd_stop, so a reviewer can check the asymmetry of §5 by reading the two branch names rather than by tracing a modifier. This is a case where two outputs are cheaper than one, because the cost of confusing them is a hung bus.

bus_held exists purely to be checked. It is asserted when the transaction begins and deasserted only when the final framing completes, and nothing in the design consumes it. Its entire job is to let a testbench — and Chapter 10.3's tracker — assert continuously that the bus was never released while work remained. A design property that is easy to state and easy to violate deserves an output whose only purpose is to make the violation observable.

Three separate NACK flags, not one. §5's table is three rows and they call for three different responses. wr_addr_nacked, ptr_nacked and rd_addr_nacked cost three flip-flops between them. Mutation A7 merges two of them and is killed by a single check — but the argument is not the mutation, it is that a driver cannot distinguish "absent" from "busy" if the hardware never did.

req_ptr_bytes is a request field, not a parameter. A parameter would fix the pointer width at synthesis, so one master could not serve both a one-byte sensor and a two-byte EEPROM on the same bus — which is an extremely ordinary board. Making it part of the request costs a two-bit input and a comparison.

The second pointer byte is presented in the same cycle as the index increment, and that is safe here. ptr_byte is combinational on ptr_index, so the new value appears immediately. Compare Chapter 8.1 §6a, where the identical-looking move was a bug requiring a whole extra state: there the payload came from a memory whose output lagged its index. Here it is a mux over a register that is already present. The difference is the data source, not the care taken — and recognising which situation you are in is what stops you from either shipping the bug or adding a state you do not need.

6b. Verified Execution

Azvya Education Pvt. Ltd.VLSI Mentor
terminal — three simulators, one result, one finish time
   $ iverilog -g2012 -o a0 i2c_combined_sequencer.sv i2c_combined_sequencer_tb.sv && ./a0
   PASS: junction is Sr and abort is P, pointer bytes MSB first, all three NACK points
   distinguished, bus never released mid-transaction
   i2c_combined_sequencer_tb.sv:299: $finish called at 4140 (1ps)

   $ iverilog -g2005 -o a1 i2c_combined_sequencer.v i2c_combined_sequencer_tb.v && ./a1
   PASS: junction is Sr and abort is P, pointer bytes MSB first, all three NACK points
   distinguished, bus never released mid-transaction
   i2c_combined_sequencer_tb.v:311: $finish called at 4140 (1ps)

   $ nvc -a i2c_combined_sequencer.vhd i2c_combined_sequencer_tb.vhd
   $ nvc -e i2c_combined_sequencer_tb && nvc -r i2c_combined_sequencer_tb --stop-time=900us
   ** Note: 4140ns+0: i2c_combined_sequencer self-check complete: junction is Sr and abort
      is P, pointer bytes MSB first, all three NACK points distinguished, bus never
      released mid-transaction

All three at 4140 ns.

7. What the Testbench Proves

The testbench records the framing sequence as an ordered list, not as counts, and that choice is the point. S Sr P and S P S contain one of each event in the first case and two S's in the second — but a test that only counted STOPs would pass a design that put its STOP in the wrong place, because the totals can be made to match. Only the order distinguishes a combined transaction from two adjacent ones.

#stimuluswhat it establishes
1read 3 bytes from pointer 0x12framing is S Sr P; addresses are 0xA0 then 0xA1; the read phase's pattern is ACK ACK NACK
2a two-byte pointerfour transmitted bytes, pointer MSB first, still exactly one Sr
3a one-byte readthe read phase's only acknowledge is a NACK
4phase-1 address NACKedframing is S P — an abort releases the bus; only wr_addr_nacked set
5the pointer NACKedframing S P; ptr_nacked set and the address flag clear
6phase-2 address NACKedframing S Sr P; rd_addr_nacked set, phase-1 flags clear, no bytes read
7back-to-backevery failure flag clears; seven transactions, seven done pulses
—every cyclebus_held never drops while the sequencer is busy

Tests 4, 5 and 6 are three aborts at three different points and they are not interchangeable. Notice in particular that test 6's framing is S Sr P — the junction did happen, because the pointer was accepted, and the abort comes after it. So the presence of a repeated START does not by itself mean the transaction succeeded, and a monitor that inferred success from "we saw an Sr" would be wrong on every busy-EEPROM retry.

The continuous bus_held check earns its place because it is a property rather than a step. It holds at every cycle of every scenario including ones added later, and it is exactly the property mutation A9 violates — dropping bus_held at the junction, which no end-of-test assertion would notice.

8. Mutation Testing

Ten defects injected into the SystemVerilog sequencer.

#injected defectoutcome
A1the junction is a STOP instead of a repeated STARTkilled — framing was S P P, expected S Sr P
A2an abort uses a repeated START, leaving the bus heldkilled — framing had no STOP
A3the phase-2 address says writekilled — 0xA0, expected 0xA1
A4the phase-1 address says readkilled — 0xA1, expected 0xA0
A5pointer bytes sent LSB firstkilled — pointer byte was 0x00, expected 0x12
A6only one pointer byte is ever sentkilled — 3 bytes transmitted, expected 4
A7a phase-2 NACK is reported as a phase-1 NACKkilled — rd_addr_nacked not reported
A8the read phase ACKs its final bytekilled — ack pattern 111, expected 110
A9bus_held is dropped at the junctionkilled — dropped 180 times while busy
A10the failure flags are not cleared at the startkilled — a pointer NACK reported as an address NACK

Ten injected, ten killed, no survivors and no invalid mutants.

A1 and A2 are the module's two signature defects and their messages are worth reading side by side. A1 turns the junction into a STOP: the framing sequence becomes S P P, because the design then emits its own final STOP as well. A2 turns the abort into a repeated START: the sequence becomes S Sr with no STOP at all, which is a held bus. One mutation releases the bus when it should not; the other fails to release it when it must. The failure messages name the sequence, so the diagnosis is in the message rather than in a waveform.

A5's message is more informative than it looks. Sending the pointer LSB-first for req_ptr = 0x0012 transmits 0x00 — the high half — and the test reports pointer byte was 0x00, expected 0x12. The pointer value was chosen so that the two halves differ, which is the same discipline Chapter 9.2 §7 applies to register contents: a value whose bytes happen to be equal would let a byte-order bug pass silently. 0x1234 in test 2 does the same job for the two-byte case.

A9 was killed only by the continuous check. No end-of-transaction assertion looks at bus_held mid-transaction, because by the end it has legitimately returned to zero. A property that is momentarily violated and then self-corrects is invisible to any check placed at the end, which is the general argument for always-on assertions over step assertions.

9. Verification Connection — One Item, Two Phases

A combined transaction is one sequence_item, not two. Modelling it as two would let a sequence interleave something between them, which is precisely the fault the format exists to prevent — the testbench would be able to generate stimulus the protocol forbids.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_combined_item.sv — the register access as ONE transaction object
   class i2c_combined_item extends uvm_sequence_item;
      `uvm_object_utils(i2c_combined_item)

      // ---- the request: both phases, in ONE item ----
      rand bit [6:0]  addr;
      rand int        ptr_bytes;     // 1 or 2 -- a DEVICE property (note 2)
      rand bit [15:0] ptr;
      rand int        len;           // payload bytes to read

      // ---- the response: four outcomes, not two ----
      bit       wr_addr_nacked;
      bit       ptr_nacked;
      bit       rd_addr_nacked;
      int       bytes_read;
      bit [7:0] payload [];

      constraint c_ptr_bytes { ptr_bytes inside {1, 2}; }
      constraint c_len       { len inside {[1:16]}; }

      // A pointer that fits its width. Generating ptr = 0x1234 with ptr_bytes = 1 would
      // produce a request the sequencer cannot express, and the driver would have to
      // silently truncate it -- a testbench that quietly changes its own stimulus is
      // worse than one that cannot produce it.
      constraint c_ptr_fits { (ptr_bytes == 1) -> (ptr <= 16'h00FF); }

      // Chapter 6.3's reserved ranges, excluded structurally so an illegal address is
      // ungeneratable rather than merely unused.
      constraint c_addr {
         addr != 7'h00;
         addr[6:3] != 4'b0000;
         addr[6:3] != 4'b1111;
      }

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

      // The three NACK points print separately, because "it NACKed" is the report that
      // makes a busy EEPROM indistinguishable from an absent device.
      function string convert2string();
         return $sformatf("RD addr=0x%02h ptr=0x%0*h len=%0d -> got=%0d%s%s%s",
                          addr, ptr_bytes * 2, ptr, len, bytes_read,
                          wr_addr_nacked ? " WR-ADDR-NACK" : "",
                          ptr_nacked     ? " PTR-NACK"     : "",
                          rd_addr_nacked ? " RD-ADDR-NACK" : "");
      endfunction
   endclass

And the property that makes the format's guarantee checkable rather than aspirational:

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_combined_props.sv — the junction must not release the bus
   // THE property of this module. Between the first START of a combined transaction and
   // its final STOP, no STOP may appear. Written with `throughout` so it constrains
   // EVERY cycle of the interval rather than only its endpoints -- a STOP that appeared
   // and was followed by a fresh START would satisfy any endpoint-only formulation.
   property p_no_stop_between_phases;
      @(posedge clk) disable iff (!rst_n)
      (txn_begin && !txn_is_single_phase)
        |=> (!stop_det throughout (txn_last_phase[->1]));
   endproperty
   assert property (p_no_stop_between_phases)
      else $error("the bus was RELEASED between the phases of a combined transaction");

   // A direction change requires a re-addressing. There is no other mechanism, so a
   // data direction that flips without an intervening START or repeated START means the
   // monitor has lost track of the frame -- which is itself worth knowing.
   property p_direction_changes_only_at_addressing;
      @(posedge clk) disable iff (!rst_n)
      $changed(dir_is_read) |-> $past(start_det || restart_det, 1);
   endproperty
   assert property (p_direction_changes_only_at_addressing)
      else $error("the transfer direction changed without a re-addressing");

   // An abort must RELEASE the bus. The mirror of the first property, and the one whose
   // violation is permanent rather than intermittent: a master that aborts with a
   // repeated START holds a shared bus having already given up on it.
   property p_abort_releases_the_bus;
      @(posedge clk) disable iff (!rst_n)
      (wr_addr_nacked || ptr_nacked || rd_addr_nacked) |-> ##[1:$] stop_det;
   endproperty
   assert property (p_abort_releases_the_bus)
      else $error("an aborted transaction did not release the bus");

10. FPGA and ASIC Implications

The block is small and its cost is LEN_W plus a handful of state. Eight states need three bits; the registers that matter are rd_index, data_index and bytes_read, all LEN_W wide, plus a two-bit pointer index and the eight-bit byte register. At LEN_W = 8 this is roughly 40 flops — about the same as Chapter 8.1's write sequencer plus one extra phase.

There is no timing path to the bus and no relationship to the SCL rate. Every output is registered and every input is a completion pulse from the layer that owns the clocking. Clock stretching, bus speed and every setup and hold parameter are absorbed before they reach this block, so a stretched byte and a fast byte take the same code path. This is the same layering payoff Chapter 8.1 §5 argued for, and it is why a two-phase transaction is no harder to synthesise than a one-phase one.

The pointer register is the one place a real design usually wants more. req_ptr is sixteen bits here because two pointer bytes is the practical maximum on I²C. Devices exist with three-byte pointers (large serial memories addressed as pages) and the extension is mechanical — widen req_ptr and req_ptr_bytes, and the CS_PTR loop is unchanged because it already counts. What is not mechanical is the endianness question of §4: a three-byte pointer has three plausible orderings and only the datasheet settles it.

Reset mid-transaction is the hazard this block cannot fix, and it is worse here than for a single-phase transfer. Resetting clears the command outputs, so the sequencer stops issuing — it does not emit a STOP. A reset between the phases therefore leaves a slave that has latched a pointer, believes it is mid-transaction, and is waiting for a re-addressing that will never come; the bus is left held with no master driving it. Chapter 5.5 built the recovery mechanism and this is the strongest reason to have one: a combined transaction has a longer window in which a reset does lasting damage than any single-phase transfer does.

bus_held is worth routing out of the block even in silicon. It costs one flop and one pin of a debug register, and it converts "the bus hung and we do not know why" into a signal that says whether this master thought it was mid-transaction at the time. Chapter 10.3 builds the block that consumes it.

11. Debugging — The Sensor Read That Returned the Wrong Register

Pitfall — two-byte pointer bytes sent in the wrong order
Buggy Code
// A driver reads a calibration block from a 64 kbit EEPROM at address 0x50. The
// device takes a TWO-BYTE pointer, most significant first. The driver builds it
// from a uint16_t and passes it to a helper that writes bytes from memory:
//
//     uint16_t addr = 0x0120;              // the calibration block
//     i2c_write_then_read(0x50, (uint8_t *)&addr, 2, buf, 8);
//
// On a little-endian processor -- which is to say, on almost every processor -- the
// two bytes in memory are 0x20 then 0x01. So the wire carries:
//
//     S  0xA0  A  0x20  A  0x01  A  Sr  0xA1  A  d0 A ... d7 N  P
//                  ^^^^     ^^^^
//                  low half first: the device reads this as pointer 0x2001
Symptom

Eight bytes come back. Every byte is acknowledged. The transaction reports success, the framing is textbook -- one S, one Sr, one P -- and the data is completely wrong.

What made this expensive is that the data was not obviously wrong. The calibration block holds small signed integers, and location 0x2001 in this part holds a different calibration block, for a different channel. So the values were plausible: right magnitude, right sign pattern, right general shape. They simply belonged to another channel.

The investigation therefore went to the SENSOR, on the theory that it was mis-calibrated at the factory. Then to the conversion maths, which was correct. Then to a suspicion that the part was a different die revision with a shifted memory map -- a theory that survived a surprisingly long time because it explained everything.

A capture resolved it in about a minute, and the clue was not the data. It was the two pointer bytes: 0x20 then 0x01, for a request the source said was 0x0120. Two bytes, present, acknowledged, in the wrong order. The device did exactly what it was told.

Root Cause

A 16-bit pointer was passed as a byte pointer into memory, so its byte order was the PROCESSOR's rather than the BUS's. I2C is most-significant-byte-first; the processor is little-endian; nothing in between converted.

The reason this class of bug survives review is that the code looks correct at every level. The address value is right. The length is right. The helper writes exactly the bytes it was given. The defect is in an assumption that was never written down: that the bytes in memory are in wire order.

12. Common Misconceptions

"The address byte selects the register." It selects the device. The register pointer is a payload byte in a first phase, and its existence, width and endianness are all the device designer's decisions — note 2 says so.

"A write-then-read is two transactions." It is one, joined by a repeated START, with the bus never released. If it were two, another master could take the bus in between and change the pointer — which is Chapter 10.3 in one sentence.

"You can use a STOP instead of a repeated START; it just takes longer." It also releases the bus. On a single-master bus the difference is only time; on a shared one it is correctness, and the failure is intermittent. Chapter 10.2 measures the window.

"An abort should also use a repeated START, for symmetry." An abort must release the bus, because there is no transaction left to continue. Holding a shared bus after giving up on it locks out every other device.

"A repeated START costs clock pulses." No framing condition does. The pulses row in §3 reads zero for S, Sr and P alike — which is why atomicity here is free.

"If the read phase's address is NACKed, the device is absent." It answered the write phase moments earlier, so it is present. A phase-2 NACK usually means busy — on an EEPROM, an internal write cycle still running — and that is the case where retrying shortly is exactly right.

"The read phase can report a bad byte with a NACK." The master owns those acknowledges, and a slave-transmitter has no way to refuse (Chapter 9.2 §3). The read phase cannot fail by NACK at all.

13. Reason It Through

A capture shows S 0xA0 A 0x12 A P then, 30 µs later, S 0xA1 A d0 A d1 N P. The driver's source performs a register read. What happened, and what is the risk?

The junction was a STOP rather than a repeated START, so the bus was released between the phases. On a single-master bus the data is still correct and the only cost is time. On a shared bus, any other master could have taken the free bus during those 30 µs and written its own pointer to the same device — after which the read phase returns data from that location, with every byte acknowledged and nothing reporting a problem. The risk is a rare, data-dependent wrong answer that is essentially impossible to reproduce on demand. Chapter 10.2 measures the window; Chapter 10.3 classifies it.

Why is a phase-2 address NACK more informative than a phase-1 one?

Because phase 1 already succeeded. The device acknowledged its address microseconds earlier, so it is present, powered and decoding — which rules out wiring, address straps and power. A refusal of the read addressing is therefore about the device's internal state, and on a memory it most often means an internal write cycle is still running. Phase 1 NACKing narrows the cause to "not there"; phase 2 NACKing narrows it to "there and busy", and the correct response differs.

A two-byte pointer read returns plausible data from the wrong location, with every byte acknowledged. What is the first thing to check, and why not the data path?

The order of the two pointer bytes on the wire. Plausible-but-wrong data means the transfer worked and went somewhere else; a corrupted data path produces noise, not well-formed values. The capture shows both pointer bytes in the order they were sent, so a transposition is visible immediately — §11 is that bug, and the underlying cause is a processor's byte order leaking onto a bus that specifies its own.

Why is the repeated START free while the pointer phase is not?

Because framing conditions consume no clock pulses — they are defined by SDA moving while SCL is high, in the gaps between pulses. The pointer phase is a byte, so it costs a full nine pulses and carries no payload. That is why a one-byte register read is only 22% efficient (Chapter 9.3 §2): four byte slots on the wire to move one payload byte, and none of the overhead is the Sr.

Could the sequencer use one cmd_terminate output with a modifier bit instead of separate cmd_restart and cmd_stop?

It could, and it would be worse. The asymmetry of §5 — junction is Sr, abort is P — is the single most important invariant in the module, and with two named outputs a reviewer verifies it by reading which output each branch names. With one output and a modifier, the same check requires tracing a data value through the branches. Two outputs cost one extra pin and make the invariant locally checkable, which is the right trade when the cost of confusing them is a hung bus.

14. Understanding Check

15. Summary

Almost every real I²C access is a write-then-read. Write the register pointer, repeated START, read the data — and the specification names this as the reason the combined format exists, because the address byte addresses a device and nothing inside it.

The junction is an Sr and an abort is a P, and the asymmetry is the module. The junction continues the transaction so the bus must not be released; an abort has nothing to continue so the bus must be. Swapping them produces an intermittent multi-master fault and a permanently held bus respectively.

Framing is free; the pointer phase is not. No framing condition consumes a clock pulse, so atomicity costs nothing — while the pointer byte costs a full nine pulses and carries no payload, which is why a one-byte register read is the least efficient common transaction on the bus.

Pointer width, endianness and auto-increment are all device properties. Note 2 settles it: these are the designer's decisions. A sequencer that hard-codes one pointer byte cannot talk to a serial memory, which is the device the specification's own note names.

Four failure points, three worth distinguishing. Absent device, refused pointer, and busy device are three different diagnoses with three different responses. The read phase cannot fail by NACK at all, because the master owns those acknowledges.

Check the sequence, not the counts. S Sr P versus S P S is the difference between one transaction and two, and the totals can be made to match. The same reasoning makes the no-release property need throughout rather than an endpoint check.

16. What Comes Next

Chapter 10.2 takes the comparison this chapter asserted and measures it. Both sequences are built from identical primitives, put on the wire byte for byte the same, and distinguished by a passive monitor using nothing but the framing rule — SDA moving while SCL is high. What the monitor produces is a number: how many cycles the bus was actually free, which is the window another master could have used. It also runs into a limit worth knowing about, because the wire records events and not intentions.

Chapter 10.3 closes the module by defining atomicity precisely and enumerating what can break it. Three things end bus ownership — a release, an arbitration loss, and an abandonment — and one thing that looks like it should does not: clock stretching, where a slave holding SCL low is the bus being owned rather than lost. The chapter builds the tracker that tells them apart, and it needs one input the wire cannot supply.

Continue learning