Skip to content
VLSI Mentor

I²C · Module 8

Master-Transmitter and Slave-Receiver Roles

A write has two sides and they are not symmetrical. This chapter splits the responsibilities bit slot by bit slot, then builds the slave-receiver that owns the register pointer, the auto-increment and the two NACK conditions the specification grants a receiver.

Chapter 8.1 gave the slave one job: answer every byte. At the protocol level that is complete and correct. At the design level it is the part of a slave that takes the least code.

The rest of a slave-receiver is what it does with each byte — and that is where the two NACK conditions the specification grants a receiver come from, where the register pointer lives, and where the most common class of slave bug hides. This chapter is the slave's half of a write, built to the same standard as the master's.

The organising question is narrow and mechanical: for each of the nine bit slots in a byte, who is driving SDA, who is reading it, and who is doing neither?

1. The Roles Are Fixed by the Address Byte, Not by the Byte

Four role names appear in the specification and it is worth being precise about them, because two of them are properties of a device and two are properties of a transfer.

namefixed byfor how long
masterwhich device generated the START and drives SCLthe whole transfer
slaveevery other device on the busthe whole transfer
transmitterthe direction bit of the current addressinguntil the next S or Sr
receiverthe direction bit of the current addressinguntil the next S or Sr

Master and slave are about who owns the clock. Transmitter and receiver are about who owns the data. They are independent, which is why all four combinations exist and why the specification names the write "master-transmitter to slave-receiver" rather than just "a write".

For a write the pairing is master-transmitter and slave-receiver, and Chapter 8.1 §1 established it holds for the entire frame. What this chapter adds is that the slave side of that pairing is conditional on something the slave decided one byte earlier: whether it was addressed at all.

2. Nine Slots, Three Jobs

Here is the slot-by-slot split for one data byte of a write. Nothing in this table is new; assembling it in one place is the point.

slotmaster (transmitter)addressed slave (receiver)every other device
1–8drives SDA, changes it on SCL fallingsamples SDA on SCL risingignores everything
8 → 9 boundaryreleases SDAprepares its answer—
9samples SDA on SCL risingdrives SDA: low for ACK, released for NACKignores everything

Three observations, each of which is a design rule rather than a description.

Nobody ever drives SDA high. Chapter 2.3 established the open-drain rule and it applies to every cell of that table. "Drives SDA" always means pulls it low or lets go; the pull-up provides the high. A slave that drove SDA high to signal a NACK would fight the master on the very next byte.

The handover happens on a slot boundary, not on a value. The master releases SDA because the eighth bit is over, regardless of what that bit was. This is the rule that a transmitter cannot infer from the data, and Chapter 7.2 §4 showed the failure it produces when it is missed: a byte whose LSB is 0 leaves SDA already low, so a transmitter that forgot to release looks correct — the slot reads as an ACK. It only breaks when the LSB is 1. The mutation suite for this module tests both, for exactly that reason.

The two sides run on opposite clock edges, and neither may choose otherwise. The transmitter changes SDA on the falling edge; the receiver samples on the rising edge. Chapter 7.1 §3 derived this from the data-valid window, and the consequence for the slave-receiver is the one thing about its timing that matters: the acknowledge decision must be complete before the ninth slot's falling edge. There is no time inside the slot to decide.

Here is a data byte at slot resolution, with the drive ownership drawn as its own row.

Data byte 0x2F — 0010 1111 — then the slave's ACK

9 cycles
Nine intervals, one per bit. SCL ticks once per interval. SDA carries zero, zero, one, zero, one, one, one, one across the first eight intervals, most significant bit first, giving 0x2F. In the ninth interval SDA is low, driven by the slave as an acknowledge. Two further rows show ownership: the master drives SDA for the first eight intervals and the slave drives the ninth, while the sampling role is the exact mirror.master transmitsmaster transmitsslave answersslaveanswersMSBMSBLSB is 1: SDA is high hereLSB is 1: SDA is high hereslave drives ACKslave drives ACKsclsda001011110drives SDAMMMMMMMMSsamples SDASSSSSSSSMt0t1t2t3t4t5t6t7t8
One data byte of a write. The master drives eight bits, releases, and the addressed slave drives the ninth. Ownership changes exactly once, at a slot boundary.

The byte value 0x2F is chosen deliberately: its LSB is 1, so SDA is high at the end of the eighth slot. If the master fails to release, SDA stays high through the ninth slot and the master reads its own high as a NACK — the transfer aborts and the slave, which did pull low, sees a STOP it cannot explain. That is the diagnostic signature of a missing release, and a byte ending in 0 will never show it.

3. What the Slave Actually Decides

The acknowledge is one bit, and a slave-receiver produces it from a decision with real content. §3.1.6 of the specification lists five conditions under which a receiver returns a NACK; two of them belong to a slave-receiver during a write, and they are the two this design implements.

Both of those are abstract. Making them concrete is the design work, and the concrete forms for a register-file slave are:

Condition 3 is a pointer that names no register. A device with twelve registers that receives a pointer of 0x40 has been given a command it does not understand. Refusing is the honest answer, and the alternative — masking the pointer down into range — is the bug §7 mutates for.

Condition 4 is the auto-increment running off the end. A burst that starts at register 10 of 12 can accept two bytes. The third has nowhere to go. Refusing it tells the master exactly where the payload stopped being accepted; wrapping to register 0 would silently corrupt an unrelated register, and the master would be told everything was fine.

There is a third decision that is not a NACK condition at all, and it is the one that must be made first:

Is this transfer even ours? A slave that is not addressed must drive nothing whatsoever — not an ACK, not a NACK, nothing. Chapter 7.3 established that a NACK is released SDA, which means "not addressed" and "addressed and refusing" look identical on the wire. That equivalence is convenient on the bus and dangerous in a design, because it means a slave that wrongly answers a transfer it does not own produces a visible fault, while a slave that wrongly stays silent on one it does own produces the same waveform as an absent device.

4. The Slave-Receiver as a Pipeline

Before the code, the block structure. Every stage here except the rightmost was built in an earlier module; this chapter supplies the decision stage and the register port.

A pipeline of four columns. Framing events from Module 5 and the addressing verdict from Chapter 6.5 both feed a write-receiver decision stage, as does the byte stream from Chapter 7.1. The decision stage drives two outputs: the acknowledge decision, which goes to the acknowledge slot of Chapter 7.2, and a registered write port consisting of address, data and write-enable, which goes to the register file.Framing eventsS, Sr and P from Module 5Addressing verdictaddressed, and the latchedR/WByte streameight bits at a time, from7.1Write-receiverdecisionpointer, bounds,auto-incrementAcknowledge slotthe ninth bit, from 7.2Register fileregistered address, data,enable12
The slave-receiver's datapath. Framing and addressing arrive as decisions already made; this chapter's block supplies the pointer, the bound checks and the acknowledge.

Note what does not appear in that diagram: any connection to SDA or SCL. The decision block never touches the wires. It receives decisions and byte values and emits a decision and a write port, which is what makes it synthesisable on the system clock and testable with no bus model at all — the testbench in §5 drives bytes and framing pulses directly.

5. The Slave-Receiver in Three Languages

Three states, and the state names are the whole protocol:

statemeaning
SR_IDLEnot addressed, or this transfer is not ours, or we have refused a byte
SR_POINTERaddressed for a write; the next byte is the register pointer
SR_DATAsubsequent bytes are payload, written at the pointer and auto-incrementing

The transition into SR_IDLE from a refusal is deliberate and is discussed in §5a: once this device has said no, it says nothing further until re-addressed.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_slave_write_receiver.sv — the slave-receiver: pointer, auto-increment and both NACK conditions
   // The slave-receiver half of a write. It consumes the address decision from Chapter
   // 6.5 and the byte stream from Chapter 7.1, and produces the one thing a
   // slave-receiver owns: the acknowledge decision for every byte.
   //
   // "Master-transmitter transmits to slave-receiver. The transfer direction is not
   // changed. The slave receiver acknowledges each byte." -- so this block must have an
   // answer ready for EVERY byte, and two of the specification's five NACK conditions
   // are the ones it can legitimately produce:
   //   3. the receiver gets data or commands it does not understand  -> a bad pointer
   //   4. the receiver cannot receive any more data bytes            -> past the end
   module i2c_slave_write_receiver #(
       parameter int REG_ADDR_W = 4,                 // pointer width
       parameter int REG_COUNT  = 12                 // registers that actually exist
   )(
       input  logic clk,
       input  logic rst_n,

       // ---- framing and addressing, from Modules 5 and 6 ----
       input  logic frame_start,        // pulse: S or Sr
       input  logic frame_stop,         // pulse: P
       input  logic addressed,          // this transfer is for us (6.5)
       input  logic dir_is_read,        // the latched R/W (6.5)

       // ---- the byte stream, from Chapter 7.1 ----
       input  logic       byte_valid,   // pulse: eight bits have arrived
       input  logic [7:0] byte_in,

       // ---- the acknowledge decision, consumed by Chapter 7.2's slot ----
       output logic       send_ack,

       // ---- the register file side ----
       output logic [REG_ADDR_W-1:0] reg_addr,
       output logic                  reg_we,
       output logic [7:0]            reg_wdata,

       // ---- status ----
       output logic       pointer_valid,   // a pointer has been latched this transfer
       output logic [7:0] bytes_accepted,  // payload bytes written this transfer
       output logic       nack_bad_pointer,// condition 3: the pointer names no register
       output logic       nack_full        // condition 4: the pointer ran past the end
   );
       typedef enum logic [1:0] {
           SR_IDLE,       // not addressed, or the transfer is not ours
           SR_POINTER,    // the next byte is the register pointer
           SR_DATA        // subsequent bytes are payload
       } state_e;

       state_e state;
       logic [REG_ADDR_W-1:0] ptr;

       // reg_addr is REGISTERED alongside reg_we and reg_wdata, not derived from `ptr`.
       // `ptr` auto-increments on the same edge that raises reg_we, so a combinational
       // reg_addr would present the ALREADY-ADVANCED pointer and every byte would land
       // one register too high. Capturing the address with the data is the standard fix
       // and the reason a write port has three registered fields rather than two.

       always_ff @(posedge clk) begin
           if (!rst_n) begin
               state            <= SR_IDLE;
               ptr              <= '0;
               send_ack         <= 1'b0;
               reg_we           <= 1'b0;
               reg_addr         <= '0;
               reg_wdata        <= 8'h00;
               pointer_valid    <= 1'b0;
               bytes_accepted   <= 8'h00;
               nack_bad_pointer <= 1'b0;
               nack_full        <= 1'b0;
           end else begin
               reg_we <= 1'b0;

               if (frame_stop) begin
                   // A STOP ends the transfer. send_ack is dropped so this block cannot
                   // answer a byte belonging to somebody else's transfer.
                   state    <= SR_IDLE;
                   send_ack <= 1'b0;
               end else if (frame_start) begin
                   // S or Sr: a fresh addressing. The verdicts clear so a consumer never
                   // reads the previous transfer's NACK as this one's.
                   state            <= SR_IDLE;
                   send_ack         <= 1'b0;
                   pointer_valid    <= 1'b0;
                   bytes_accepted   <= 8'h00;
                   nack_bad_pointer <= 1'b0;
                   nack_full        <= 1'b0;
                   ptr              <= '0;
               end else if (addressed && !dir_is_read && state == SR_IDLE) begin
                   // Addressed for a write: the next byte is the pointer.
                   state <= SR_POINTER;
               end else if (byte_valid) begin
                   case (state)
                       SR_POINTER: begin
                           // The decision must be REGISTERED here, on the eighth bit,
                           // because the acknowledge slot's falling edge is next. A slave
                           // that decided during the slot would be moving SDA while SCL
                           // is high -- framing, not a late answer (Chapter 7.2, §2).
                           if (byte_in >= REG_COUNT[7:0]) begin
                               // Condition 3: a pointer naming no register is a command
                               // this device does not understand.
                               nack_bad_pointer <= 1'b1;
                               send_ack         <= 1'b0;
                               state            <= SR_IDLE;
                           end else begin
                               ptr           <= byte_in[REG_ADDR_W-1:0];
                               pointer_valid <= 1'b1;
                               send_ack      <= 1'b1;
                               state         <= SR_DATA;
                           end
                       end

                       SR_DATA: begin
                           if (ptr >= REG_COUNT[REG_ADDR_W-1:0]) begin
                               // Condition 4: the auto-increment has run past the end, so
                               // there is nowhere to put this byte. Refusing is the only
                               // honest answer -- wrapping would corrupt register 0.
                               nack_full <= 1'b1;
                               send_ack  <= 1'b0;
                               state     <= SR_IDLE;
                           end else begin
                               reg_we         <= 1'b1;
                               reg_addr       <= ptr;            // the pointer BEFORE the increment
                               reg_wdata      <= byte_in;
                               bytes_accepted <= bytes_accepted + 8'd1;
                               ptr            <= ptr + 1'b1;   // auto-increment
                               send_ack       <= 1'b1;
                           end
                       end

                       default: send_ack <= 1'b0;
                   endcase
               end
           end
       end
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_slave_write_receiver_tb.sv — eight scenarios, including a read and somebody else's transfer
   module i2c_slave_write_receiver_tb;
       localparam int REG_ADDR_W = 4;
       localparam int REG_COUNT  = 12;      // registers 0..11 exist; 12..15 do not

       logic clk = 1'b0, rst_n;
       logic frame_start, frame_stop, addressed, dir_is_read;
       logic byte_valid;
       logic [7:0] byte_in;
       logic send_ack;
       logic [REG_ADDR_W-1:0] reg_addr;
       logic reg_we;
       logic [7:0] reg_wdata;
       logic pointer_valid;
       logic [7:0] bytes_accepted;
       logic nack_bad_pointer, nack_full;

       int errors = 0;

       i2c_slave_write_receiver #(.REG_ADDR_W(REG_ADDR_W), .REG_COUNT(REG_COUNT)) dut (.*);

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

       // A register file behind the receiver, so writes can be checked where they land.
       logic [7:0] regs [0:15];
       always @(posedge clk) if (rst_n && reg_we) regs[reg_addr] <= reg_wdata;

       // Record the acknowledge decision for each byte, so the whole pattern is checked.
       logic ack_log [0:15];
       int   n_logged;

       task automatic send_byte(input logic [7:0] v);
           byte_in = v; byte_valid = 1'b1; @(negedge clk); byte_valid = 1'b0;
           // The decision is registered on the byte, so it is readable the next cycle --
           // which is what the acknowledge slot will consume.
           @(negedge clk);
           if (n_logged < 16) ack_log[n_logged] = send_ack;
           n_logged++;
       endtask

       task automatic pulse_start(); frame_start = 1'b1; @(negedge clk); frame_start = 1'b0; @(negedge clk); endtask
       task automatic pulse_stop();  frame_stop  = 1'b1; @(negedge clk); frame_stop  = 1'b0; @(negedge clk); endtask

       // Address this device for a write: framing, then the decoder's verdict.
       task automatic begin_write();
           pulse_start();
           addressed = 1'b1; dir_is_read = 1'b0;
           repeat (2) @(negedge clk);
           n_logged = 0;
       endtask

       initial begin
           rst_n = 1'b0; frame_start = 1'b0; frame_stop = 1'b0;
           addressed = 1'b0; dir_is_read = 1'b0; byte_valid = 1'b0; byte_in = 8'h00;
           n_logged = 0;
           for (int i = 0; i < 16; i++) regs[i] = 8'h00;
           repeat (3) @(negedge clk);
           if (send_ack !== 1'b0) begin $display("FAIL: send_ack asserted out of reset"); errors++; end
           rst_n = 1'b1; @(negedge clk);

           // ---- 1: a normal write. Pointer 2, then three payload bytes, which must land
           //      in registers 2, 3 and 4 by auto-increment.
           begin_write();
           send_byte(8'd2);
           if (ack_log[0] !== 1'b1) begin $display("FAIL: a valid pointer was not acknowledged"); errors++; end
           if (pointer_valid !== 1'b1) begin $display("FAIL: pointer_valid not set"); errors++; end
           send_byte(8'hAA);
           send_byte(8'hBB);
           send_byte(8'hCC);
           if (ack_log[1] !== 1'b1 || ack_log[2] !== 1'b1 || ack_log[3] !== 1'b1) begin
               $display("FAIL: payload bytes were not all acknowledged"); errors++; end
           if (regs[2] !== 8'hAA || regs[3] !== 8'hBB || regs[4] !== 8'hCC) begin
               $display("FAIL: auto-increment landed wrong: r2=0x%02h r3=0x%02h r4=0x%02h",
                        regs[2], regs[3], regs[4]); errors++; end
           if (bytes_accepted !== 8'd3) begin
               $display("FAIL: bytes_accepted = %0d, expected 3", bytes_accepted); errors++; end
           pulse_stop();
           addressed = 1'b0;

           // ---- 2: a pointer that names no register. Condition 3 -- the device does not
           //      understand the command -- so the POINTER byte itself is NACKed.
           begin_write();
           send_byte(8'd12);                       // REG_COUNT is 12, so 12 does not exist
           if (ack_log[0] !== 1'b0) begin
               $display("FAIL: an out-of-range pointer was acknowledged"); errors++; end
           if (nack_bad_pointer !== 1'b1) begin
               $display("FAIL: nack_bad_pointer not reported"); errors++; end
           if (pointer_valid !== 1'b0) begin
               $display("FAIL: pointer_valid set on a rejected pointer"); errors++; end
           pulse_stop();
           addressed = 1'b0;

           // ---- 3: THE BOUNDARY. Pointer 11 is the LAST valid register, so it must be
           //      accepted -- REG_COUNT is a count, not a maximum index.
           begin_write();
           send_byte(8'd11);
           if (ack_log[0] !== 1'b1) begin
               $display("FAIL: pointer 11 rejected, but registers 0..11 exist"); errors++; end
           send_byte(8'hEE);
           if (ack_log[1] !== 1'b1) begin
               $display("FAIL: a write to the last register was refused"); errors++; end
           if (regs[11] !== 8'hEE) begin
               $display("FAIL: the last register did not take the byte"); errors++; end

           // ---- 4: and the NEXT byte runs past the end. Condition 4 -- the receiver
           //      cannot take any more -- so it NACKs rather than wrapping to register 0.
           send_byte(8'hFF);
           if (ack_log[2] !== 1'b0) begin
               $display("FAIL: a byte past the last register was acknowledged"); errors++; end
           if (nack_full !== 1'b1) begin $display("FAIL: nack_full not reported"); errors++; end
           if (regs[0] !== 8'h00) begin
               $display("FAIL: the overflowing byte WRAPPED into register 0 (0x%02h)", regs[0]); errors++; end
           if (bytes_accepted !== 8'd1) begin
               $display("FAIL: bytes_accepted = %0d, expected 1", bytes_accepted); errors++; end
           pulse_stop();
           addressed = 1'b0;

           // ---- 5: a transfer addressed to SOMEBODY ELSE. This device must answer
           //      nothing at all, however many bytes go past.
           pulse_start();
           addressed = 1'b0; dir_is_read = 1'b0;
           repeat (2) @(negedge clk);
           n_logged = 0;
           // Register 7 is chosen because no earlier step touches it -- checking a
           // register an earlier step legitimately wrote would fail for the wrong reason.
           send_byte(8'd7); send_byte(8'h11);
           if (ack_log[0] !== 1'b0 || ack_log[1] !== 1'b0) begin
               $display("FAIL: answered a transfer addressed to another device"); errors++; end
           if (regs[7] !== 8'h00) begin
               $display("FAIL: wrote a register during somebody else's transfer"); errors++; end
           pulse_stop();

           // ---- 6: addressed for a READ. This block is the WRITE receiver, so it must
           //      not answer -- Chapter 7.3's table gives the read's data bytes to the
           //      master, not to the slave.
           pulse_start();
           addressed = 1'b1; dir_is_read = 1'b1;
           repeat (2) @(negedge clk);
           n_logged = 0;
           // The byte is a VALID pointer (register 6 exists and no earlier step touches
           // it). That matters: an out-of-range byte like 0x55 would be NACKed as
           // condition 3 even by a block that ignored dir_is_read, so it would pass for
           // entirely the wrong reason and hide the bug.
           send_byte(8'd6); send_byte(8'hee);
           if (ack_log[0] !== 1'b0 || ack_log[1] !== 1'b0) begin
               $display("FAIL: the write receiver answered a read's bytes"); errors++; end
           // Silence must mean NO VERDICT, not a rejection: a read is simply not this
           // block's transfer, so neither the pointer nor a NACK reason may be produced.
           if (pointer_valid !== 1'b0) begin
               $display("FAIL: a read latched a pointer"); errors++; end
           if (nack_bad_pointer !== 1'b0 || nack_full !== 1'b0) begin
               $display("FAIL: a read produced a NACK verdict this block does not own"); errors++; end
           if (regs[6] !== 8'h00) begin
               $display("FAIL: a read transfer wrote a register"); errors++; end
           pulse_stop();
           addressed = 1'b0;

           // ---- 7: a repeated START mid-transfer clears the verdict and the pointer.
           begin_write();
           send_byte(8'd5); send_byte(8'h77);
           if (bytes_accepted !== 8'd1) begin $display("FAIL: setup for step 7 wrong"); errors++; end
           begin_write();                              // Sr, re-addressed
           if (bytes_accepted !== 8'd0) begin
               $display("FAIL: a repeated START did not clear bytes_accepted"); errors++; end
           if (pointer_valid !== 1'b0) begin
               $display("FAIL: a repeated START did not clear pointer_valid"); errors++; end
           send_byte(8'd0); send_byte(8'h99);
           if (regs[0] !== 8'h99) begin
               $display("FAIL: the post-Sr write did not land in register 0"); errors++; end
           pulse_stop();
           addressed = 1'b0;

           // ---- 8: the acknowledge decision must not SURVIVE the STOP. A block still
           //      holding "yes" after the transfer ended is a block that can pull SDA
           //      low during the very window where SDA must rise to form the P.
           begin_write();
           send_byte(8'd1); send_byte(8'h22);
           if (send_ack !== 1'b1) begin
               $display("FAIL: setup for step 8 -- the byte should have been acknowledged"); errors++; end
           pulse_stop();
           if (send_ack !== 1'b0) begin
               $display("FAIL: send_ack survived the STOP"); errors++; end
           addressed = 1'b0;

           if (errors == 0)
               $display("PASS: pointer validated, auto-increment correct, both NACK conditions produced, silent when not ours");
           else $display("FAIL: %0d error(s)", errors);
           $finish;
       end
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_slave_write_receiver.v — the same slave-receiver in Verilog-2001
   // The slave-receiver half of a write. It consumes the address decision from Chapter
   // 6.5 and the byte stream from Chapter 7.1, and produces the one thing a
   // slave-receiver owns: the acknowledge decision for every byte.
   //
   // "Master-transmitter transmits to slave-receiver. The transfer direction is not
   // changed. The slave receiver acknowledges each byte." -- so this block must have an
   // answer ready for EVERY byte, and two of the specification's five NACK conditions
   // are the ones it can legitimately produce:
   //   3. the receiver gets data or commands it does not understand  -> a bad pointer
   //   4. the receiver cannot receive any more data bytes            -> past the end
   module i2c_slave_write_receiver #(
       parameter integer REG_ADDR_W = 4,                 // pointer width
       parameter integer REG_COUNT  = 12                 // registers that actually exist
   )(
       input  wire clk,
       input  wire rst_n,

       // ---- framing and addressing, from Modules 5 and 6 ----
       input  wire frame_start,        // pulse: S or Sr
       input  wire frame_stop,         // pulse: P
       input  wire addressed,          // this transfer is for us (6.5)
       input  wire dir_is_read,        // the latched R/W (6.5)

       // ---- the byte stream, from Chapter 7.1 ----
       input  wire        byte_valid,   // pulse: eight bits have arrived
       input  wire  [7:0] byte_in,

       // ---- the acknowledge decision, consumed by Chapter 7.2's slot ----
       output reg         send_ack,

       // ---- the register file side ----
       output reg   [REG_ADDR_W-1:0] reg_addr,
       output reg                    reg_we,
       output reg   [7:0]            reg_wdata,

       // ---- status ----
       output reg         pointer_valid,   // a pointer has been latched this transfer
       output reg   [7:0] bytes_accepted,  // payload bytes written this transfer
       output reg         nack_bad_pointer,// condition 3: the pointer names no register
       output reg         nack_full        // condition 4: the pointer ran past the end
   );
       localparam SR_IDLE    = 2'd0;   // not addressed, or the transfer is not ours
       localparam SR_POINTER = 2'd1;   // the next byte is the register pointer
       localparam SR_DATA    = 2'd2;   // subsequent bytes are payload

       reg [1:0] state;
       reg [REG_ADDR_W-1:0] ptr;

       // reg_addr is REGISTERED alongside reg_we and reg_wdata, not derived from `ptr`.
       // `ptr` auto-increments on the same edge that raises reg_we, so a combinational
       // reg_addr would present the ALREADY-ADVANCED pointer and every byte would land
       // one register too high. Capturing the address with the data is the standard fix
       // and the reason a write port has three registered fields rather than two.

       always @(posedge clk) begin
           if (!rst_n) begin
               state            <= SR_IDLE;
               ptr              <= {REG_ADDR_W{1'b0}};
               send_ack         <= 1'b0;
               reg_we           <= 1'b0;
               reg_addr         <= {REG_ADDR_W{1'b0}};
               reg_wdata        <= 8'h00;
               pointer_valid    <= 1'b0;
               bytes_accepted   <= 8'h00;
               nack_bad_pointer <= 1'b0;
               nack_full        <= 1'b0;
           end else begin
               reg_we <= 1'b0;

               if (frame_stop) begin
                   // A STOP ends the transfer. send_ack is dropped so this block cannot
                   // answer a byte belonging to somebody else's transfer.
                   state    <= SR_IDLE;
                   send_ack <= 1'b0;
               end else if (frame_start) begin
                   // S or Sr: a fresh addressing. The verdicts clear so a consumer never
                   // reads the previous transfer's NACK as this one's.
                   state            <= SR_IDLE;
                   send_ack         <= 1'b0;
                   pointer_valid    <= 1'b0;
                   bytes_accepted   <= 8'h00;
                   nack_bad_pointer <= 1'b0;
                   nack_full        <= 1'b0;
                   ptr              <= {REG_ADDR_W{1'b0}};
               end else if (addressed && !dir_is_read && state == SR_IDLE) begin
                   // Addressed for a write: the next byte is the pointer.
                   state <= SR_POINTER;
               end else if (byte_valid) begin
                   case (state)
                       SR_POINTER: begin
                           // The decision must be REGISTERED here, on the eighth bit,
                           // because the acknowledge slot's falling edge is next. A slave
                           // that decided during the slot would be moving SDA while SCL
                           // is high -- framing, not a late answer (Chapter 7.2, §2).
                           if (byte_in >= REG_COUNT[7:0]) begin
                               // Condition 3: a pointer naming no register is a command
                               // this device does not understand.
                               nack_bad_pointer <= 1'b1;
                               send_ack         <= 1'b0;
                               state            <= SR_IDLE;
                           end else begin
                               ptr           <= byte_in[REG_ADDR_W-1:0];
                               pointer_valid <= 1'b1;
                               send_ack      <= 1'b1;
                               state         <= SR_DATA;
                           end
                       end

                       SR_DATA: begin
                           if (ptr >= REG_COUNT[REG_ADDR_W-1:0]) begin
                               // Condition 4: the auto-increment has run past the end, so
                               // there is nowhere to put this byte. Refusing is the only
                               // honest answer -- wrapping would corrupt register 0.
                               nack_full <= 1'b1;
                               send_ack  <= 1'b0;
                               state     <= SR_IDLE;
                           end else begin
                               reg_we         <= 1'b1;
                               reg_addr       <= ptr;            // the pointer BEFORE the increment
                               reg_wdata      <= byte_in;
                               bytes_accepted <= bytes_accepted + 8'd1;
                               ptr            <= ptr + 1'b1;   // auto-increment
                               send_ack       <= 1'b1;
                           end
                       end

                       default: send_ack <= 1'b0;
                   endcase
               end
           end
       end
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_slave_write_receiver_tb.v — the Verilog testbench, structurally identical
   module i2c_slave_write_receiver_tb;
       localparam integer REG_ADDR_W = 4;
       localparam integer REG_COUNT  = 12;      // registers 0..11 exist; 12..15 do not

       reg clk, rst_n;
       reg frame_start, frame_stop, addressed, dir_is_read;
       reg byte_valid;
       reg [7:0] byte_in;
       wire send_ack;
       wire [REG_ADDR_W-1:0] reg_addr;
       wire reg_we;
       wire [7:0] reg_wdata;
       wire pointer_valid;
       wire [7:0] bytes_accepted;
       wire nack_bad_pointer, nack_full;

       integer errors, n_logged, i;

       i2c_slave_write_receiver #(.REG_ADDR_W(REG_ADDR_W), .REG_COUNT(REG_COUNT)) dut (
           .clk(clk), .rst_n(rst_n), .frame_start(frame_start), .frame_stop(frame_stop),
           .addressed(addressed), .dir_is_read(dir_is_read), .byte_valid(byte_valid),
           .byte_in(byte_in), .send_ack(send_ack), .reg_addr(reg_addr), .reg_we(reg_we),
           .reg_wdata(reg_wdata), .pointer_valid(pointer_valid),
           .bytes_accepted(bytes_accepted), .nack_bad_pointer(nack_bad_pointer),
           .nack_full(nack_full));

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

       // A register file behind the receiver, so writes can be checked where they land.
       reg [7:0] regs [0:15];
       always @(posedge clk) if (rst_n && reg_we) regs[reg_addr] <= reg_wdata;

       // Record the acknowledge decision for each byte, so the whole pattern is checked.
       reg ack_log [0:15];

       task send_byte; input [7:0] v; begin
           byte_in = v; byte_valid = 1'b1; @(negedge clk); byte_valid = 1'b0;
           // The decision is registered on the byte, so it is readable the next cycle --
           // which is what the acknowledge slot will consume.
           @(negedge clk);
           if (n_logged < 16) ack_log[n_logged] = send_ack;
           n_logged = n_logged + 1;
       end endtask

       task pulse_start; begin frame_start = 1'b1; @(negedge clk); frame_start = 1'b0; @(negedge clk); end endtask
       task pulse_stop;  begin frame_stop  = 1'b1; @(negedge clk); frame_stop  = 1'b0; @(negedge clk); end endtask

       // Address this device for a write: framing, then the decoder's verdict.
       task begin_write; begin
           pulse_start();
           addressed = 1'b1; dir_is_read = 1'b0;
           repeat (2) @(negedge clk);
           n_logged = 0;
       end endtask

       initial begin
           errors = 0; n_logged = 0;
           rst_n = 1'b0; frame_start = 1'b0; frame_stop = 1'b0;
           addressed = 1'b0; dir_is_read = 1'b0; byte_valid = 1'b0; byte_in = 8'h00;
           n_logged = 0;
           for (i = 0; i < 16; i = i + 1) regs[i] = 8'h00;
           repeat (3) @(negedge clk);
           if (send_ack !== 1'b0) begin $display("FAIL: send_ack asserted out of reset"); errors = errors + 1; end
           rst_n = 1'b1; @(negedge clk);

           // ---- 1: a normal write. Pointer 2, then three payload bytes, which must land
           //      in registers 2, 3 and 4 by auto-increment.
           begin_write();
           send_byte(8'd2);
           if (ack_log[0] !== 1'b1) begin $display("FAIL: a valid pointer was not acknowledged"); errors = errors + 1; end
           if (pointer_valid !== 1'b1) begin $display("FAIL: pointer_valid not set"); errors = errors + 1; end
           send_byte(8'hAA);
           send_byte(8'hBB);
           send_byte(8'hCC);
           if (ack_log[1] !== 1'b1 || ack_log[2] !== 1'b1 || ack_log[3] !== 1'b1) begin
               $display("FAIL: payload bytes were not all acknowledged"); errors = errors + 1; end
           if (regs[2] !== 8'hAA || regs[3] !== 8'hBB || regs[4] !== 8'hCC) begin
               $display("FAIL: auto-increment landed wrong: r2=0x%02h r3=0x%02h r4=0x%02h",
                        regs[2], regs[3], regs[4]); errors = errors + 1; end
           if (bytes_accepted !== 8'd3) begin
               $display("FAIL: bytes_accepted = %0d, expected 3", bytes_accepted); errors = errors + 1; end
           pulse_stop();
           addressed = 1'b0;

           // ---- 2: a pointer that names no register. Condition 3 -- the device does not
           //      understand the command -- so the POINTER byte itself is NACKed.
           begin_write();
           send_byte(8'd12);                       // REG_COUNT is 12, so 12 does not exist
           if (ack_log[0] !== 1'b0) begin
               $display("FAIL: an out-of-range pointer was acknowledged"); errors = errors + 1; end
           if (nack_bad_pointer !== 1'b1) begin
               $display("FAIL: nack_bad_pointer not reported"); errors = errors + 1; end
           if (pointer_valid !== 1'b0) begin
               $display("FAIL: pointer_valid set on a rejected pointer"); errors = errors + 1; end
           pulse_stop();
           addressed = 1'b0;

           // ---- 3: THE BOUNDARY. Pointer 11 is the LAST valid register, so it must be
           //      accepted -- REG_COUNT is a count, not a maximum index.
           begin_write();
           send_byte(8'd11);
           if (ack_log[0] !== 1'b1) begin
               $display("FAIL: pointer 11 rejected, but registers 0..11 exist"); errors = errors + 1; end
           send_byte(8'hEE);
           if (ack_log[1] !== 1'b1) begin
               $display("FAIL: a write to the last register was refused"); errors = errors + 1; end
           if (regs[11] !== 8'hEE) begin
               $display("FAIL: the last register did not take the byte"); errors = errors + 1; end

           // ---- 4: and the NEXT byte runs past the end. Condition 4 -- the receiver
           //      cannot take any more -- so it NACKs rather than wrapping to register 0.
           send_byte(8'hFF);
           if (ack_log[2] !== 1'b0) begin
               $display("FAIL: a byte past the last register was acknowledged"); errors = errors + 1; end
           if (nack_full !== 1'b1) begin $display("FAIL: nack_full not reported"); errors = errors + 1; end
           if (regs[0] !== 8'h00) begin
               $display("FAIL: the overflowing byte WRAPPED into register 0 (0x%02h)", regs[0]); errors = errors + 1; end
           if (bytes_accepted !== 8'd1) begin
               $display("FAIL: bytes_accepted = %0d, expected 1", bytes_accepted); errors = errors + 1; end
           pulse_stop();
           addressed = 1'b0;

           // ---- 5: a transfer addressed to SOMEBODY ELSE. This device must answer
           //      nothing at all, however many bytes go past.
           pulse_start();
           addressed = 1'b0; dir_is_read = 1'b0;
           repeat (2) @(negedge clk);
           n_logged = 0;
           // Register 7 is chosen because no earlier step touches it -- checking a
           // register an earlier step legitimately wrote would fail for the wrong reason.
           send_byte(8'd7); send_byte(8'h11);
           if (ack_log[0] !== 1'b0 || ack_log[1] !== 1'b0) begin
               $display("FAIL: answered a transfer addressed to another device"); errors = errors + 1; end
           if (regs[7] !== 8'h00) begin
               $display("FAIL: wrote a register during somebody else's transfer"); errors = errors + 1; end
           pulse_stop();

           // ---- 6: addressed for a READ. This block is the WRITE receiver, so it must
           //      not answer -- Chapter 7.3's table gives the read's data bytes to the
           //      master, not to the slave.
           pulse_start();
           addressed = 1'b1; dir_is_read = 1'b1;
           repeat (2) @(negedge clk);
           n_logged = 0;
           // The byte is a VALID pointer (register 6 exists and no earlier step touches
           // it). That matters: an out-of-range byte like 0x55 would be NACKed as
           // condition 3 even by a block that ignored dir_is_read, so it would pass for
           // entirely the wrong reason and hide the bug.
           send_byte(8'd6); send_byte(8'hee);
           if (ack_log[0] !== 1'b0 || ack_log[1] !== 1'b0) begin
               $display("FAIL: the write receiver answered a read's bytes"); errors = errors + 1; end
           // Silence must mean NO VERDICT, not a rejection: a read is simply not this
           // block's transfer, so neither the pointer nor a NACK reason may be produced.
           if (pointer_valid !== 1'b0) begin
               $display("FAIL: a read latched a pointer"); errors = errors + 1; end
           if (nack_bad_pointer !== 1'b0 || nack_full !== 1'b0) begin
               $display("FAIL: a read produced a NACK verdict this block does not own"); errors = errors + 1; end
           if (regs[6] !== 8'h00) begin
               $display("FAIL: a read transfer wrote a register"); errors = errors + 1; end
           pulse_stop();
           addressed = 1'b0;

           // ---- 7: a repeated START mid-transfer clears the verdict and the pointer.
           begin_write();
           send_byte(8'd5); send_byte(8'h77);
           if (bytes_accepted !== 8'd1) begin $display("FAIL: setup for step 7 wrong"); errors = errors + 1; end
           begin_write();                              // Sr, re-addressed
           if (bytes_accepted !== 8'd0) begin
               $display("FAIL: a repeated START did not clear bytes_accepted"); errors = errors + 1; end
           if (pointer_valid !== 1'b0) begin
               $display("FAIL: a repeated START did not clear pointer_valid"); errors = errors + 1; end
           send_byte(8'd0); send_byte(8'h99);
           if (regs[0] !== 8'h99) begin
               $display("FAIL: the post-Sr write did not land in register 0"); errors = errors + 1; end
           pulse_stop();
           addressed = 1'b0;

           // ---- 8: the acknowledge decision must not SURVIVE the STOP. A block still
           //      holding "yes" after the transfer ended is a block that can pull SDA
           //      low during the very window where SDA must rise to form the P.
           begin_write;
           send_byte(8'd1); send_byte(8'h22);
           if (send_ack !== 1'b1) begin
               $display("FAIL: setup for step 8 -- the byte should have been acknowledged"); errors = errors + 1; end
           pulse_stop;
           if (send_ack !== 1'b0) begin
               $display("FAIL: send_ack survived the STOP"); errors = errors + 1; end
           addressed = 1'b0;

           if (errors == 0)
               $display("PASS: pointer validated, auto-increment correct, both NACK conditions produced, silent when not ours");
           else $display("FAIL: %0d error(s)", errors);
           $finish;
       end
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_slave_write_receiver.vhd — the same slave-receiver in VHDL
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   -- The slave-receiver half of a write. It consumes the address decision from Chapter
   -- 6.5 and the byte stream from Chapter 7.1, and produces the one thing a
   -- slave-receiver owns: the acknowledge decision for every byte.
   --
   -- Two of the specification's five NACK conditions are the ones it can legitimately
   -- produce:
   --   3. the receiver gets data or commands it does not understand  -> a bad pointer
   --   4. the receiver cannot receive any more data bytes            -> past the end
   entity i2c_slave_write_receiver is
       generic (
           REG_ADDR_W : positive := 4;                -- pointer width
           REG_COUNT  : positive := 12                -- registers that actually exist
       );
       port (
           clk   : in std_logic;
           rst_n : in std_logic;

           -- framing and addressing, from Modules 5 and 6
           frame_start : in std_logic;
           frame_stop  : in std_logic;
           addressed   : in std_logic;
           dir_is_read : in std_logic;

           -- the byte stream, from Chapter 7.1
           byte_valid : in std_logic;
           byte_in    : in std_logic_vector(7 downto 0);

           -- the acknowledge decision, consumed by Chapter 7.2's slot
           send_ack : out std_logic;

           -- the register file side
           reg_addr  : out unsigned(REG_ADDR_W - 1 downto 0);
           reg_we    : out std_logic;
           reg_wdata : out std_logic_vector(7 downto 0);

           -- status
           pointer_valid    : out std_logic;
           bytes_accepted   : out unsigned(7 downto 0);
           nack_bad_pointer : out std_logic;
           nack_full        : out std_logic
       );
   end entity;

   architecture rtl of i2c_slave_write_receiver is
       type state_t is (
           SR_IDLE,       -- not addressed, or the transfer is not ours
           SR_POINTER,    -- the next byte is the register pointer
           SR_DATA        -- subsequent bytes are payload
       );
       signal state : state_t := SR_IDLE;

       -- reg_addr is REGISTERED alongside reg_we and reg_wdata, not derived from `ptr`.
       -- `ptr` auto-increments on the same edge that raises reg_we, so a combinational
       -- reg_addr would present the already-advanced pointer and every byte would land
       -- one register too high.
       signal ptr : unsigned(REG_ADDR_W - 1 downto 0) := (others => '0');
       signal cnt : unsigned(7 downto 0) := (others => '0');
   begin
       bytes_accepted <= cnt;

       process (clk)
       begin
           if rising_edge(clk) then
               if rst_n = '0' then
                   state            <= SR_IDLE;
                   ptr              <= (others => '0');
                   send_ack         <= '0';
                   reg_we           <= '0';
                   reg_addr         <= (others => '0');
                   reg_wdata        <= (others => '0');
                   pointer_valid    <= '0';
                   cnt              <= (others => '0');
                   nack_bad_pointer <= '0';
                   nack_full        <= '0';
               else
                   reg_we <= '0';

                   if frame_stop = '1' then
                       -- A STOP ends the transfer, and send_ack drops so this block
                       -- cannot answer a byte belonging to another transfer.
                       state    <= SR_IDLE;
                       send_ack <= '0';
                   elsif frame_start = '1' then
                       -- S or Sr: a fresh addressing, so the verdicts clear.
                       state            <= SR_IDLE;
                       send_ack         <= '0';
                       pointer_valid    <= '0';
                       cnt              <= (others => '0');
                       nack_bad_pointer <= '0';
                       nack_full        <= '0';
                       ptr              <= (others => '0');
                   elsif addressed = '1' and dir_is_read = '0' and state = SR_IDLE then
                       state <= SR_POINTER;
                   elsif byte_valid = '1' then
                       case state is
                           when SR_POINTER =>
                               -- The decision must be REGISTERED here, on the eighth bit:
                               -- the acknowledge slot's falling edge is next, and a slave
                               -- that decided during the slot would move SDA while SCL is
                               -- high -- framing, not a late answer.
                               if unsigned(byte_in) >= to_unsigned(REG_COUNT, 8) then
                                   -- Condition 3: a pointer naming no register.
                                   nack_bad_pointer <= '1';
                                   send_ack         <= '0';
                                   state            <= SR_IDLE;
                               else
                                   ptr           <= unsigned(byte_in(REG_ADDR_W - 1 downto 0));
                                   pointer_valid <= '1';
                                   send_ack      <= '1';
                                   state         <= SR_DATA;
                               end if;

                           when SR_DATA =>
                               if ptr >= to_unsigned(REG_COUNT, REG_ADDR_W) then
                                   -- Condition 4: the auto-increment has run past the end.
                                   -- Refusing is the only honest answer -- wrapping would
                                   -- corrupt register 0.
                                   nack_full <= '1';
                                   send_ack  <= '0';
                                   state     <= SR_IDLE;
                               else
                                   reg_we    <= '1';
                                   reg_addr  <= ptr;             -- BEFORE the increment
                                   reg_wdata <= byte_in;
                                   cnt       <= cnt + 1;
                                   ptr       <= ptr + 1;         -- auto-increment
                                   send_ack  <= '1';
                               end if;

                           when others =>
                               send_ack <= '0';
                       end case;
                   end if;
               end if;
           end if;
       end process;
   end architecture;
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_slave_write_receiver_tb.vhd — the VHDL testbench, with a single-driver register model
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   entity i2c_slave_write_receiver_tb is
   end entity;

   architecture sim of i2c_slave_write_receiver_tb is
       constant REG_ADDR_W : positive := 4;
       constant REG_COUNT  : positive := 12;    -- registers 0..11 exist; 12..15 do not

       signal clk         : std_logic := '0';
       signal rst_n       : std_logic := '0';
       signal frame_start : std_logic := '0';
       signal frame_stop  : std_logic := '0';
       signal addressed   : std_logic := '0';
       signal dir_is_read : std_logic := '0';
       signal byte_valid  : std_logic := '0';
       signal byte_in     : std_logic_vector(7 downto 0) := (others => '0');
       signal send_ack    : std_logic;
       signal reg_addr    : unsigned(REG_ADDR_W - 1 downto 0);
       signal reg_we      : std_logic;
       signal reg_wdata   : std_logic_vector(7 downto 0);
       signal pointer_valid : std_logic;
       signal bytes_accepted : unsigned(7 downto 0);
       signal nack_bad_pointer, nack_full : std_logic;

       -- A register file behind the receiver. Initialised at DECLARATION, because the
       -- writing process is its only driver and the stimulus may not also assign it.
       type regs_t is array (0 to 15) of std_logic_vector(7 downto 0);
       signal regs : regs_t := (others => (others => '0'));

       -- The acknowledge log is owned by the stimulus, so it is a variable there rather
       -- than a signal two processes would both want to write.
       signal test_done : std_logic := '0';
   begin
       dut : entity work.i2c_slave_write_receiver
           generic map (REG_ADDR_W => REG_ADDR_W, REG_COUNT => REG_COUNT)
           port map (clk => clk, rst_n => rst_n, frame_start => frame_start,
                     frame_stop => frame_stop, addressed => addressed,
                     dir_is_read => dir_is_read, byte_valid => byte_valid,
                     byte_in => byte_in, send_ack => send_ack, reg_addr => reg_addr,
                     reg_we => reg_we, reg_wdata => reg_wdata,
                     pointer_valid => pointer_valid, bytes_accepted => bytes_accepted,
                     nack_bad_pointer => nack_bad_pointer, nack_full => nack_full);

       clk <= not clk after 5 ns;

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

       regfile : process (clk)
       begin
           if rising_edge(clk) then
               if rst_n = '1' and reg_we = '1' then
                   regs(to_integer(reg_addr)) <= reg_wdata;
               end if;
           end if;
       end process;

       stim : process
           variable errs : natural := 0;
           type ack_log_t is array (0 to 15) of std_logic;
           variable ack_log  : ack_log_t := (others => '0');
           variable n_logged : natural := 0;

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

           procedure send_byte (v : in std_logic_vector(7 downto 0)) is
           begin
               byte_in <= v; byte_valid <= '1'; waitn(1); byte_valid <= '0';
               -- The decision is registered on the byte, so it is readable the next
               -- cycle -- which is what the acknowledge slot will consume.
               waitn(1);
               if n_logged < 16 then ack_log(n_logged) := send_ack; end if;
               n_logged := n_logged + 1;
           end procedure;

           procedure pulse_start is
           begin
               frame_start <= '1'; waitn(1); frame_start <= '0'; waitn(1);
           end procedure;

           procedure pulse_stop is
           begin
               frame_stop <= '1'; waitn(1); frame_stop <= '0'; waitn(1);
           end procedure;

           procedure begin_write is
           begin
               pulse_start;
               addressed <= '1'; dir_is_read <= '0';
               waitn(2);
               n_logged := 0;
           end procedure;
       begin
           waitn(3);
           if send_ack /= '0' then
               report "send_ack asserted out of reset" severity error; errs := errs + 1; end if;
           rst_n <= '1'; waitn(1);

           -- 1: a normal write. Pointer 2, then three payload bytes -> registers 2, 3, 4.
           begin_write;
           send_byte(x"02");
           if ack_log(0) /= '1' then
               report "a valid pointer was not acknowledged" severity error; errs := errs + 1; end if;
           if pointer_valid /= '1' then
               report "pointer_valid not set" severity error; errs := errs + 1; end if;
           send_byte(x"AA"); send_byte(x"BB"); send_byte(x"CC");
           if ack_log(1) /= '1' or ack_log(2) /= '1' or ack_log(3) /= '1' then
               report "payload bytes were not all acknowledged" severity error; errs := errs + 1; end if;
           if regs(2) /= x"AA" or regs(3) /= x"BB" or regs(4) /= x"CC" then
               report "auto-increment landed in the wrong registers" severity error;
               errs := errs + 1; end if;
           if bytes_accepted /= 3 then
               report "bytes_accepted wrong" severity error; errs := errs + 1; end if;
           pulse_stop;
           addressed <= '0';

           -- 2: a pointer that names no register -- condition 3, so the POINTER is NACKed.
           begin_write;
           send_byte(x"0C");                       -- 12, and REG_COUNT is 12
           if ack_log(0) /= '0' then
               report "an out-of-range pointer was acknowledged" severity error; errs := errs + 1; end if;
           if nack_bad_pointer /= '1' then
               report "nack_bad_pointer not reported" severity error; errs := errs + 1; end if;
           if pointer_valid /= '0' then
               report "pointer_valid set on a rejected pointer" severity error; errs := errs + 1; end if;
           pulse_stop;
           addressed <= '0';

           -- 3: THE BOUNDARY. Pointer 11 is the LAST valid register and must be accepted --
           --    REG_COUNT is a count, not a maximum index.
           begin_write;
           send_byte(x"0B");
           if ack_log(0) /= '1' then
               report "pointer 11 rejected, but registers 0..11 exist" severity error;
               errs := errs + 1; end if;
           send_byte(x"EE");
           if ack_log(1) /= '1' then
               report "a write to the last register was refused" severity error; errs := errs + 1; end if;
           if regs(11) /= x"EE" then
               report "the last register did not take the byte" severity error; errs := errs + 1; end if;

           -- 4: and the NEXT byte runs past the end -- condition 4, and it must NOT wrap.
           send_byte(x"FF");
           if ack_log(2) /= '0' then
               report "a byte past the last register was acknowledged" severity error;
               errs := errs + 1; end if;
           if nack_full /= '1' then
               report "nack_full not reported" severity error; errs := errs + 1; end if;
           if regs(0) /= x"00" then
               report "the overflowing byte WRAPPED into register 0" severity error;
               errs := errs + 1; end if;
           if bytes_accepted /= 1 then
               report "bytes_accepted wrong after the overflow" severity error; errs := errs + 1; end if;
           pulse_stop;
           addressed <= '0';

           -- 5: a transfer addressed to SOMEBODY ELSE -- answer nothing at all.
           --    Register 7 is chosen because no earlier step touches it.
           pulse_start;
           addressed <= '0'; dir_is_read <= '0';
           waitn(2);
           n_logged := 0;
           send_byte(x"07"); send_byte(x"11");
           if ack_log(0) /= '0' or ack_log(1) /= '0' then
               report "answered a transfer addressed to another device" severity error;
               errs := errs + 1; end if;
           if regs(7) /= x"00" then
               report "wrote a register during somebody else's transfer" severity error;
               errs := errs + 1; end if;
           pulse_stop;

           -- 6: addressed for a READ -- the write receiver must not answer.
           pulse_start;
           addressed <= '1'; dir_is_read <= '1';
           waitn(2);
           n_logged := 0;
           -- The byte is a VALID pointer (register 6 exists and no earlier step touches
           -- it). That matters: an out-of-range byte like 0x55 would be NACKed as
           -- condition 3 even by a block that ignored dir_is_read, so it would pass for
           -- entirely the wrong reason and hide the bug.
           send_byte(x"06"); send_byte(x"EE");
           if ack_log(0) /= '0' or ack_log(1) /= '0' then
               report "the write receiver answered a read's bytes" severity error;
               errs := errs + 1; end if;
           -- Silence must mean NO VERDICT, not a rejection: a read is simply not this
           -- block's transfer, so neither the pointer nor a NACK reason may be produced.
           if pointer_valid /= '0' then
               report "a read latched a pointer" severity error; errs := errs + 1; end if;
           if nack_bad_pointer /= '0' or nack_full /= '0' then
               report "a read produced a NACK verdict this block does not own" severity error;
               errs := errs + 1; end if;
           if regs(6) /= x"00" then
               report "a read transfer wrote a register" severity error; errs := errs + 1; end if;
           pulse_stop;
           addressed <= '0';

           -- 7: a repeated START clears the verdict and the pointer.
           begin_write;
           send_byte(x"05"); send_byte(x"77");
           if bytes_accepted /= 1 then
               report "setup for the repeated-START step is wrong" severity error; errs := errs + 1; end if;
           begin_write;                              -- Sr, re-addressed
           if bytes_accepted /= 0 then
               report "a repeated START did not clear bytes_accepted" severity error;
               errs := errs + 1; end if;
           if pointer_valid /= '0' then
               report "a repeated START did not clear pointer_valid" severity error;
               errs := errs + 1; end if;
           send_byte(x"00"); send_byte(x"99");
           if regs(0) /= x"99" then
               report "the post-Sr write did not land in register 0" severity error;
               errs := errs + 1; end if;
           pulse_stop;
           addressed <= '0';

           -- 8: the acknowledge decision must not SURVIVE the STOP. A block still holding
           -- "yes" after the transfer ended is a block that can pull SDA low during the
           -- very window where SDA must rise to form the P.
           begin_write;
           send_byte(x"01"); send_byte(x"22");
           if send_ack /= '1' then
               report "setup for step 8 -- the byte should have been acknowledged" severity error;
               errs := errs + 1; end if;
           pulse_stop;
           if send_ack /= '0' then
               report "send_ack survived the STOP" severity error; errs := errs + 1; end if;
           addressed <= '0';

           if errs = 0 then
               report "i2c_slave_write_receiver self-check complete: pointer validated, "
                    & "auto-increment correct, both NACK conditions produced, silent when "
                    & "not ours" severity note;
           else
               report "i2c_slave_write_receiver self-check FAILED" severity error;
           end if;
           test_done <= '1';
           wait;
       end process;
   end architecture;

5a. Four Decisions Worth Defending

reg_addr is registered with the data, not derived from the pointer. This is the chapter's subtlest bug and it was a real one during development. ptr auto-increments on the same clock edge that raises reg_we. A combinational reg_addr = ptr therefore presents the already-advanced pointer to the register file, and every byte lands one register too high — the first symptom was r3 = 0xAA where r2 = 0xAA was intended. Capturing the address alongside the data is the standard fix, and it is the reason a write port has three registered fields rather than two.

The general shape is worth keeping: whenever a pointer both selects and advances on the same edge, the value that was selected must be captured, not recomputed.

The decision is registered on the eighth bit, not during the ninth slot. §2 established that the acknowledge slot's falling edge comes immediately after the byte. A slave that began deciding when the slot opened would be moving SDA while SCL is high — which Chapter 5.1 established is reserved for framing. So the fault is not a late answer; it is a START or STOP appearing inside a byte. The decision must be a registered value already sitting on send_ack when the slot begins.

A refusal returns to SR_IDLE rather than staying in SR_DATA. Once this device has answered a byte with a NACK, §3.1.6 says the master will issue either a STOP or a repeated START. Both re-enter this block through frame_stop or frame_start. Remaining in SR_DATA would mean acknowledging a byte after having refused one, which tells the master the refusal was transient when it was not. Silence until re-addressed is the only consistent behaviour.

The verdict outputs clear on frame_start, not on frame_stop. They must survive the STOP, because the STOP is when a consumer reads them — the same reasoning Chapter 8.3's metrics block applies to its counters. Clearing them at the START of the next transfer is what stops a reader from seeing the previous transfer's NACK as this one's, and it is the only point at which that can be done without destroying the result.

5b. Verified Execution

Azvya Education Pvt. Ltd.VLSI Mentor
terminal — three simulators, identical stimulus, identical finish time
   $ iverilog -g2012 -o b0 i2c_slave_write_receiver.sv i2c_slave_write_receiver_tb.sv && ./b0
   PASS: pointer validated, auto-increment correct, both NACK conditions produced, silent when not ours
   i2c_slave_write_receiver_tb.sv:188: $finish called at 860 (1s)

   $ iverilog -g2005 -o b2 i2c_slave_write_receiver.v i2c_slave_write_receiver_tb.v && ./b2
   PASS: pointer validated, auto-increment correct, both NACK conditions produced, silent when not ours
   i2c_slave_write_receiver_tb.v:195: $finish called at 860 (1s)

   $ nvc -a i2c_slave_write_receiver.vhd i2c_slave_write_receiver_tb.vhd
   $ nvc -e i2c_slave_write_receiver_tb && nvc -r i2c_slave_write_receiver_tb --stop-time=200us
   ** Note: 860ns+0: i2c_slave_write_receiver self-check complete: pointer validated,
      auto-increment correct, both NACK conditions produced, silent when not ours

6. What the Testbench Proves

#stimuluswhat it establishes
1pointer 2, then 0xAA 0xBB 0xCCthe pointer is latched and the auto-increment lands bytes in r2, r3, r4 — not r3, r4, r5
2pointer 0x40 on a 12-register devicecondition 3: nack_bad_pointer, not acknowledged, no pointer latched
3pointer 11 — the last valid registerthe boundary is inclusive: 11 is accepted, and the byte lands
4one more byte after register 11condition 4: nack_full, and register 0 is not corrupted
5a transfer addressed to another deviceno acknowledge and no register write, however many bytes pass
6addressed for a readno acknowledge, no pointer latched, and no NACK verdict either
7a repeated START mid-transferthe verdicts, the byte count and the pointer all reset
8a STOP after an acknowledged bytesend_ack does not survive the STOP

Tests 3 and 4 are a pair and neither is meaningful alone. Test 3 alone would pass on a design whose bound was off by one in the permissive direction; test 4 alone would pass on a design that rejected register 11 as well. Testing the last valid index and the first invalid one is the general form of a boundary test, and it is worth writing both even when the second seems redundant.

Test 6 was strengthened after a mutation survived, and the reason is worth recording. The original version sent 0x55 as the read transfer's first byte and checked it was not acknowledged. But 0x55 is 85, far beyond the twelve registers that exist — so a mutant that ignored dir_is_read entirely would enter SR_POINTER, reject 0x55 as condition 3, and pass the check for completely the wrong reason. The fix was to send a byte that is a valid pointer, so that a mutant which ignores the direction bit has to acknowledge it, and to add the stronger assertion: a read must produce no verdict at all, not a rejection. Silence and refusal are different states even though they are the same waveform.

7. Mutation Testing

Eight defects injected into the SystemVerilog design, one at a time.

#injected defectoutcome
B1the pointer is never bounds-checked — condition 3 lostkilled — an out-of-range pointer was acknowledged
B2the pointer wraps instead of NACKing — condition 4 lostkilled — a byte past the last register was acknowledged
B3reg_addr uses the already-advanced pointerkilled — r2=0x00 r3=0xaa r4=0xbb
B4a read transfer is treated as a writekilled — the receiver answered a read's bytes
B5the first byte is treated as payload, not as the pointerkilled — pointer_valid not set
B6the STOP leaves send_ack assertedkilled — send_ack survived the STOP
B7the pointer does not auto-incrementkilled — r2=0xcc r3=0x00 r4=0x00
B8the pointer byte is counted as accepted payloadkilled — bytes_accepted 4, expected 3

Eight injected, eight killed — but three of those kills required strengthening the testbench first, and that is the honest account of this run.

B4 survived the original test 6, for the reason given in §6: the stimulus byte was out of range, so the mutant was caught by the wrong check and the direction bit was never actually tested. This is the second time in this course that a test passed because its stimulus value made two different mechanisms indistinguishable — Chapter 7.2 had the same shape with a byte whose LSB was 0. The lesson generalises: when a check passes, ask which mechanism made it pass. If two could have, the test does not distinguish them.

B6 survived because nothing looked at send_ack in the window after a STOP. The scenario matters: a block still holding "yes" after the transfer has ended is a block that can pull SDA low during the precise window in which SDA must rise to form the P. Test 8 was added and is two lines long.

B7's kill message is worth reading carefully. r2=0xcc r3=0x00 r4=0x00 — without the auto-increment all three bytes land in register 2, so the last one wins and the other two are silently overwritten. Compare it to B3's r2=0x00 r3=0xaa r4=0xbb, where the whole burst is shifted by one. Both are "the auto-increment is wrong"; the failure messages tell you which way, and a test that reported only "mismatch" would leave you to work that out from the waveform.

8. Verification Connection — The Slave Is a Model, and the Model Is the Check

A master is verified by driving it. A slave is verified by comparing it against a model of what it should have become — and for a register-file slave, that model is small enough to write in the scoreboard itself.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_slave_scoreboard.sv — a reference model, not a transaction log
   class i2c_slave_scoreboard extends uvm_component;
      `uvm_component_utils(i2c_slave_scoreboard)

      // THE model: what the register file should contain. A scoreboard that only
      // logged transactions could tell you a write happened; it could not tell you
      // the auto-increment put it in the right place -- which is mutation B3, the
      // one that shifts an entire burst by one register.
      bit [7:0] expect_regs [int];
      int       expect_ptr;
      bit       expect_ptr_valid;

      uvm_analysis_imp #(i2c_byte_item, i2c_slave_scoreboard) byte_ap;

      // Reproduce the DUT's decision INDEPENDENTLY, from the protocol rules, and
      // compare. The point of a reference model is that it is written from the
      // specification rather than from the design, so a shared misreading of the
      // specification is the only fault it cannot catch.
      function void write(i2c_byte_item t);
         bit expect_ack;

         if (t.is_first_payload_byte) begin
            // Condition 3: a pointer naming no register.
            expect_ack       = (t.data < REG_COUNT);
            expect_ptr       = t.data;
            expect_ptr_valid = expect_ack;
         end
         else begin
            // Condition 4: the auto-increment has run past the end. Note that the
            // model must NOT wrap here either -- a model that wrapped would agree
            // with a wrapping DUT and report a pass.
            expect_ack = expect_ptr_valid && (expect_ptr < REG_COUNT);
            if (expect_ack) begin
               expect_regs[expect_ptr] = t.data;
               expect_ptr++;
            end
         end

         if (t.observed_ack !== expect_ack)
            `uvm_error("ACK", $sformatf("byte 0x%02h: DUT %s, model %s",
                       t.data, t.observed_ack ? "ACK" : "NACK",
                       expect_ack ? "ACK" : "NACK"))
      endfunction

      // Called at the STOP, because that is when the transfer's effect is complete
      // and the register file is stable. Checking mid-transfer would race the write.
      function void check_registers(bit [7:0] actual [int]);
         foreach (expect_regs[i])
            if (actual[i] !== expect_regs[i])
               `uvm_error("REG", $sformatf("r%0d: expected 0x%02h, got 0x%02h",
                          i, expect_regs[i], actual[i]))
      endfunction
   endclass

The structural point is in the comment on expect_regs: a scoreboard that logs transactions cannot catch a placement bug. Mutation B3 writes every byte, acknowledges every byte, and produces a transaction log identical to a correct run. Only a model of the resulting state separates them, and that is the argument for reference-model scoreboards over transaction-matching ones in any design that has memory.

And the assertion that belongs on the slave side is the one §3 identified as the dangerous asymmetry:

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_slave_props.sv — a slave must never answer a transfer it does not own
   // An unaddressed device drives NOTHING. Not a NACK -- nothing. Releasing SDA
   // and being absent are the same waveform, so this property is the only place
   // the distinction can be enforced.
   property p_silent_when_not_addressed;
      @(posedge clk) disable iff (!rst_n)
      (!addressed) |-> (!send_ack && !reg_we);
   endproperty
   assert property (p_silent_when_not_addressed)
      else $error("answered or wrote while not addressed");

   // The acknowledge decision must be STABLE for the whole slot -- it is sampled
   // on the ninth rising edge, and a decision that moved during the slot would be
   // an SDA edge while SCL is high: framing, not a late answer.
   property p_ack_stable_in_slot;
      @(posedge clk) disable iff (!rst_n)
      ack_slot |-> $stable(send_ack);
   endproperty
   assert property (p_ack_stable_in_slot)
      else $error("the acknowledge decision changed inside the slot");

   // The pointer never exceeds the register count. This is the wrapping bug of
   // section 3 as an invariant: if it can never be out of range, it can never
   // have wrapped into range.
   property p_pointer_in_range;
      @(posedge clk) disable iff (!rst_n)
      reg_we |-> (reg_addr < REG_COUNT);
   endproperty
   assert property (p_pointer_in_range)
      else $error("wrote to register %0d, outside the file", reg_addr);

9. FPGA and ASIC Implications

The block is tiny and the register file is not. Three states, a four-bit pointer, an eight-bit count and four flag bits — under twenty flops. What it drives is the cost: the register file's write port. On an FPGA a small register file is distributed RAM or flops; past a few dozen registers it belongs in a block RAM, and then the registered reg_addr/reg_wdata/reg_we triple in §5 is already exactly the interface a block RAM wants. That was not an accident of style — a registered write port is the portable shape.

The bound check is a comparator against a parameter, so it costs nothing when REG_COUNT is a power of two — the synthesiser reduces it to the high bits of the pointer being zero — and costs a small comparator otherwise. Do not be tempted to round REG_COUNT up to a power of two to save that comparator: the registers between the real count and the rounded one do not exist, and accepting writes to them is precisely mutation B1.

Clock stretching is the slave's tool and it is not in this block. Module 12 derives clock stretching, by which a slave holds SCL low to buy time. This design never needs to, because every decision it makes is a comparison available in one cycle. A slave whose acknowledge depends on something slower — an ECC check, an external memory, a microcontroller's interrupt handler — must stretch, and the place to do it is the byte-level layer that owns SCL, not here. The layering is what makes that a local change.

The reset state must be silent. send_ack resets to 0 and reg_we to 0, which means a slave coming out of reset mid-transfer answers nothing and writes nothing. That is the only safe default on a shared bus: a slave that reset into "acknowledge" would pull SDA low during somebody else's transfer, and a bus-wide fault caused by one device's reset is very hard to attribute.

10. Debugging — The Burst That Landed One Register Too High

Pitfall — a pointer that both selects and advances on the same clock edge
Buggy Code
// The first version of the register write port. It looks right, and the
// auto-increment is in the correct place.
//
//   assign reg_addr = ptr;                 // COMBINATIONAL from the pointer
//
//   always_ff @(posedge clk)
//      if (byte_valid && state == SR_DATA) begin
//         reg_we    <= 1'b1;
//         reg_wdata <= byte_in;
//         ptr       <= ptr + 1'b1;         // ... and ptr advances on the SAME edge
//      end
//
// reg_we and reg_wdata are registered, so they present themselves to the register
// file one cycle after the byte. But reg_addr is combinational on ptr, and ptr has
// ALREADY advanced by then. So the write port shows the new pointer with the old
// data, and every byte lands one register too high.
Symptom

A three-byte burst to pointer 2 produced r2=0x00, r3=0xAA, r4=0xBB -- and 0xCC vanished entirely, into r5, which the test did not check.

What made this expensive is how reasonable the wrong answer looks. The bytes are in the right ORDER. The auto-increment is clearly working -- three consecutive registers were written with three consecutive payload bytes. Nothing is corrupt, nothing is missing from the data, and the acknowledge pattern is perfect. The only thing wrong is the starting point, which reads like a pointer-latching bug.

So the investigation went to SR_POINTER and to the byte that carries the pointer, and both were correct: the testbench showed ptr taking the value 2 from the pointer byte, exactly as intended. The pointer was right when it was latched and right when it was used. It was simply not right at the same TIME as the data.

The clue was that the offset was always exactly one, in the same direction, regardless of the starting pointer or the burst length. A data-dependent bug varies with the data; a constant offset of one register is a cycle-alignment bug.

Root Cause

ptr auto-increments on the same clock edge that raises reg_we. A combinational reg_addr therefore presents the ALREADY-ADVANCED pointer alongside data that belongs to the previous value.

The mental model that produced it is that "reg_addr = ptr" means "the address is the pointer". It does not -- it means "the address is whatever the pointer is RIGHT NOW", and right now is one increment too late.

11. Common Misconceptions

"A slave acknowledges because it is a slave." It acknowledges because it is the receiver, and it is the receiver because the address byte's direction bit was 0. The same device addressed for a read must not acknowledge the data bytes — it is driving them.

"An unaddressed slave sends a NACK." It sends nothing. A NACK is released SDA, and an unaddressed device has never driven SDA in the first place, so the two are the same waveform. The distinction matters in the design, not on the bus — which is why §8's first assertion exists.

"The register pointer is part of I²C." It is a convention of the device, described in its datasheet. I²C has no concept of a location within a device; the first payload byte is a payload byte, and what it means is the designer's choice. Plenty of devices have no pointer at all.

"Running past the last register is harmless because the pointer wraps." Wrapping writes over register 0, which on many devices is control or configuration. A three-byte overrun can reconfigure the part while reporting complete success. §3's callout ranks the three possible behaviours and wrapping is the worst of them.

"The slave can decide its answer during the acknowledge slot." The slot begins with SCL falling and the answer is sampled on the next rising edge. A decision made inside the slot moves SDA while SCL is high, which is a framing event — so the fault is not a late acknowledge, it is a spurious START or STOP inside a byte.

"A slave that refuses a byte should keep listening in case the next one is fine." After a NACK the master issues a STOP or a repeated START; both re-address the block. Acknowledging a byte after refusing one tells the master the refusal was transient when nothing has changed.

12. Reason It Through

A slave has 12 registers. The master writes pointer 10 followed by four data bytes. What does the wire show, and what is in the device afterwards?

Pointer 10 is valid, so it is ACKed. Registers 10 and 11 accept the first two bytes and both are ACKed. The pointer is now 12, which is past the end, so the third data byte is NACKed — condition 4 — and the fourth is never sent, because the master aborts on the NACK. The wire shows S, addr+W, A, 0x0A, A, d0, A, d1, A, d2, N, P. The device holds the first two bytes in r10 and r11, and bytes_accepted is 2. Nothing was corrupted and the master knows exactly where acceptance stopped.

The same slave receives pointer 12 as its first payload byte. How does the wire differ?

The pointer itself is NACKed — condition 3 — so the frame is S, addr+W, A, 0x0C, N, P. No data byte is ever sent. The distinction from the previous case is diagnostic: a NACK on the first payload byte means the device rejected the command, and a NACK on a later one means it accepted the command and then ran out of room. A driver that reports only "NACK" cannot tell a caller which of those happened.

Why can a slave-receiver not use the eighth bit's value to decide when to prepare its answer?

Because the release and the answer are keyed to the slot number, not to the data. Chapter 7.2 showed that a byte whose LSB is 0 leaves SDA already low, so a design that keyed off the value happens to look correct for half of all bytes. It fails on the other half, which makes it a data-dependent bug — the worst kind to reproduce, because it depends on payload content rather than on anything structural.

A slave is addressed, the master sends a valid pointer, and the slave NACKs it. Something is wrong with the design. What are the two candidates?

Either the bound check is too strict — the classic off-by-one, rejecting the last valid register, which test 3 exists to catch — or dir_is_read was latched wrong, so the block believes this is a read and is refusing on those grounds. Both produce the identical waveform, and the two are distinguished by nack_bad_pointer: condition 3 sets it, and a direction mix-up produces no verdict at all. This is the design-level payoff of §6's insistence that silence and refusal be distinguishable internally even though they are not on the wire.

13. Understanding Check

14. Summary

Master/slave and transmitter/receiver are independent axes. The first is about who owns the clock, the second about who owns the data for the current addressing. A write pairs master-transmitter with slave-receiver, and the slave's role is conditional on a direction bit it latched one byte earlier.

The handover inside a byte happens on a slot boundary, not on a value. The master releases SDA because the eighth bit is over, whatever that bit was — and a byte ending in 0 will hide a missing release, because SDA is already low.

The slave's real work is deciding, not answering. The acknowledge is one bit; the content behind it is a pointer bound check (condition 3) and an end-of-file check (condition 4). Wrapping instead of refusing is the most damaging of the three possible overrun behaviours, because it succeeds visibly while corrupting register 0.

Silence and refusal are the same waveform and different states. An unaddressed device and a refusing device both leave SDA released, so the distinction cannot be enforced on the bus. It has to be enforced in the design, with an assertion and with verdict outputs that separate "not mine" from "mine, and no".

A pointer that selects and advances on the same edge must have its value captured. That is the chapter's most transferable bug: a constant one-register offset, with the bytes in the right order, produced by a combinational address on an auto-incrementing pointer. A write port has three registered fields for this reason.

15. What Comes Next

Chapter 8.3 takes the cost of all this seriously. Both sides of a write are now built, and a three-byte write costs 36 SCL pulses to move 24 bits of payload. The arithmetic of where the other twelve pulses go, what bursting buys, and how to read an annotated burst capture is the last chapter of this module — and it includes a passive hardware instrument that measures the cost rather than asserting it.

Module 9 then takes the direction bit the other way. Every role in §1's table flips, the handover moves from a slot boundary inside a byte to a byte boundary inside a frame, and the final NACK that Chapter 7.4 introduced becomes load-bearing rather than informational.

Continue learning