Skip to content
VLSI Mentor

I²C · Module 18

The Receive Datapath — Capturing Written Bytes

The subject is the hand-off, not the value. A byte delivered twice is as bad as one dropped and both are invisible to a test that only compares bytes — and the acknowledge pulse is not a data bit, so a receive path left enabled through it displaces every byte after the first.

The target is addressed, the direction is write, and it has acknowledged (18.5). Bytes are now arriving.

1. What Makes This Different From the Address Block

Both shift eight bits in. The difference is what completion produces.

18.4 address blockthis block
completion producesa decision — match or notdata something else must consume
the byte itself isof diagnostic interest onlythe entire point
the failure mode isthe wrong decisiona byte dropped, or delivered twice

So the contract is deliberately narrow:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
rx_valid is ONE cycle, exactly once per received byte
rx_byte  is stable when rx_valid is high

2. Why a Pulse Rather Than a Flag

A level-based "byte available" flag would need a consumer acknowledgement to clear — and then this block has two masters:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
the bus       which never waits
the consumer  which might

A one-cycle pulse plus a register downstream is the right shape for everything else, and Chapter 18.10 is where a target that genuinely cannot keep up gets to stretch the clock instead. Those are the two honest options; hoping is not one.

Mutation M5 turns the pulse into a level and fails fourteen checks.

3. Sampled at the Rising Edge, Enabled From Above

The sampling instant is scl_rise, for the same reason as every other received bit: §3.1.2 makes SDA stable only while SCL is HIGH. This block has no timing of its own.

And it is enabled, not self-starting:

Mutation M6 drops the gate and fails seven checks; M7 samples on any cycle rather than the rising edge and fails twenty-nine.

4. The Acknowledge Pulse Is Not a Data Bit

This is the boundary that bites hardest, and it surfaced as a testbench bug before it could surface as a design one.

A byte is nine clock pulses (17.7): eight data and one acknowledge. The acknowledge pulse clocks SDA exactly like a data bit does — so a receive path left enabled through it shifts it in.

So rx_enable must be dropped for the ninth pulse. That is Chapter 18.10's job, and this chapter's benches model it — the gen_ack_slot task drops the enable, because a bench that left it high would reproduce the defect in its own stimulus and then blame the design.

Mutation M3 makes the block consume nine bits itself and fails twenty-two checks.

5. Framing Discards a Partial Byte

A repeated START or a STOP part-way through a byte means the byte will never complete.

The block counts abandonments separately (n_partial), because a target that silently discards partial bytes and one that never receives any look identical from outside. Mutation M8 removes the discard and fails four checks.

6. The Hand-Off, and the Displacement It Prevents

A block diagram. A sampling instant and an SDA level from chapter 18.2 feed a shift register, gated by an enable from the transaction layer. The shift register feeds a bit counter and a byte output. On the eighth bit the counter produces a one-cycle valid pulse and a byte-done pulse, the first going to the register interface and the second to the acknowledge block of chapter 18.5. Separately, framing events from chapter 18.3 feed a discard path that clears the shift register and counter, and increments a partial-byte counter.scl_rise + sda_q18.2rx_enable18.10 — not the ACK slotShift registerMSB firstrx_byte + rx_validto 18.9Bit counter 0..7eight, never ninebyte_donearms 18.5Framing18.3Discard partialcount it, drop it12
Figure 1 — the receive path and its two gates. The enable keeps the address byte and the acknowledge pulse out of the shift register; framing discards a partial. Only the completion pulse leaves the block.

What an ungated receiver delivers: correct, then progressively displaced

7 cycles
Seven intervals at byte resolution. The wire carries an address byte, an acknowledge, a first data byte 0x11, an acknowledge, a second data byte 0x22, an acknowledge, and a third data byte 0x33. A correct receiver row shows 0x11, 0x22 and 0x33 delivered in the three data slots. An ungated receiver row shows 0x11 correct, then values displaced by one and two bits in the later slots.agreeagreediverge, and growdiverge, and growfirst byte: always correctfirst byte: always correctACK consumed as bit 0ACK consumed as bit 0on the wireADDRACK0x11ACK0x22ACK0x33gated (right)000x110x110x220x220x33ungated (wrong)000x110x11shift 1shift 1shift 2t0t1t2t3t4t5t6
Figure 2 — the displacement that an ungated receive path produces, at byte resolution. The first byte is correct and every later one is shifted, which is why the symptom points away from the receiver. Conceptual figure: one interval per byte slot.

7. The Receive Datapath, in Three Languages

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_slave_rx.sv — eight observed bits into one byte, handed off exactly once
   // -----------------------------------------------------------------------------
   // i2c_slave_rx.sv
   // The receive datapath: eight observed bits into one byte, handed off exactly once.
   //
   // WHAT MAKES THIS DIFFERENT FROM THE ADDRESS BLOCK, which also shifts eight bits in.
   // The address block's completion produces a DECISION -- match or not -- and the byte
   // itself is only of diagnostic interest. This block's completion produces DATA that
   // something else must consume, and the hand-off is the part that goes wrong: a byte
   // delivered twice is as bad as a byte dropped, and both are invisible to a test that
   // only checks the value.
   //
   // So the contract is deliberately narrow:
   //
   //   rx_valid is ONE cycle, exactly once per received byte
   //   rx_byte  is stable when rx_valid is high
   //
   // A level-based "byte available" flag would need a consumer acknowledgement to clear,
   // and then the block has two masters: the bus, which never waits, and the consumer,
   // which might. Chapter 18.10 is where a slave that genuinely cannot keep up gets to
   // stretch the clock; a one-cycle pulse plus a register downstream is the right shape for
   // everything else.
   //
   // SAMPLED AT THE RISING EDGE, for the same reason as every other received bit: §3.1.2
   // makes SDA stable only while SCL is HIGH, so `scl_rise` is the one instant a received
   // bit is guaranteed valid. This block has no timing of its own.
   //
   // ENABLED, NOT SELF-STARTING. It shifts only when the layer above says a data byte is
   // expected -- selected, write direction, address phase over. Without that gate it would
   // shift the address byte as data, and the first data byte of every write would be the
   // address. The gate is the whole reason the address block and this one can share a bus
   // without sharing a bit counter.
   //
   // AND FRAMING RESETS IT MID-BYTE. A repeated START or a STOP arriving part-way through a
   // byte means the byte will never complete. The partial contents must be discarded, not
   // held over to be completed by the NEXT transfer's first three bits -- which is what a
   // design that only resets its counter on completion would do.
   // -----------------------------------------------------------------------------

   module i2c_slave_rx #(
      parameter int CNT_W = 16
   ) (
      input  logic clk,
      input  logic rst_n,

      // From Chapter 18.2.
      input  logic scl_rise,
      input  logic sda_q,

      // From Chapter 18.3 -- framing discards a partial byte.
      input  logic start_pulse,
      input  logic stop_pulse,

      // From the layer above (18.10/18.11): a data byte is expected now.
      input  logic rx_enable,

      output logic       receiving,    // a byte is in progress -- the `mid_byte` report
      output logic [3:0] bit_index,    // 0..7
      output logic [7:0] rx_byte,
      output logic       rx_valid,     // ONE cycle, exactly once per byte
      output logic       byte_done,    // the same instant, for the acknowledge block

      output logic [CNT_W-1:0] n_bytes,
      output logic [CNT_W-1:0] n_partial   // bytes abandoned to framing
   );

      logic [7:0] shreg;
      wire  [7:0] shreg_next = {shreg[6:0], sda_q};

      always @(posedge clk or negedge rst_n) begin
         if (!rst_n) begin
            receiving <= 1'b0;
            bit_index <= 4'd0;
            rx_byte   <= 8'h00;
            rx_valid  <= 1'b0;
            byte_done <= 1'b0;
            shreg     <= 8'h00;
            n_bytes   <= {CNT_W{1'b0}};
            n_partial <= {CNT_W{1'b0}};
         end else begin
            rx_valid  <= 1'b0;
            byte_done <= 1'b0;

            if (start_pulse || stop_pulse) begin
               // Discard a partial byte. Holding it would let the next transfer's first
               // bits complete this one, producing a byte that never existed on the wire.
               if (receiving) n_partial <= n_partial + 1'b1;
               receiving <= 1'b0;
               bit_index <= 4'd0;
               shreg     <= 8'h00;
            end else if (rx_enable && scl_rise) begin
               shreg     <= shreg_next;
               receiving <= 1'b1;
               if (bit_index == 4'd7) begin
                  // Complete. One pulse, one byte, and the counter returns to zero so the
                  // ninth (acknowledge) pulse is not mistaken for a data bit.
                  receiving <= 1'b0;
                  bit_index <= 4'd0;
                  rx_byte   <= shreg_next;
                  rx_valid  <= 1'b1;
                  byte_done <= 1'b1;
                  n_bytes   <= n_bytes + 1'b1;
               end else begin
                  bit_index <= bit_index + 4'd1;
               end
            end
         end
      end

   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_slave_rx.v — the same design in Verilog-2001
   // -----------------------------------------------------------------------------
   // i2c_slave_rx.v
   // The receive datapath: eight observed bits into one byte, handed off exactly once.
   //
   // WHAT MAKES THIS DIFFERENT FROM THE ADDRESS BLOCK, which also shifts eight bits in.
   // The address block's completion produces a DECISION -- match or not -- and the byte
   // itself is only of diagnostic interest. This block's completion produces DATA that
   // something else must consume, and the hand-off is the part that goes wrong: a byte
   // delivered twice is as bad as a byte dropped, and both are invisible to a test that
   // only checks the value.
   //
   // So the contract is deliberately narrow:
   //
   //   rx_valid is ONE cycle, exactly once per received byte
   //   rx_byte  is stable when rx_valid is high
   //
   // A level-based "byte available" flag would need a consumer acknowledgement to clear,
   // and then the block has two masters: the bus, which never waits, and the consumer,
   // which might. Chapter 18.10 is where a slave that genuinely cannot keep up gets to
   // stretch the clock; a one-cycle pulse plus a register downstream is the right shape for
   // everything else.
   //
   // SAMPLED AT THE RISING EDGE, for the same reason as every other received bit: §3.1.2
   // makes SDA stable only while SCL is HIGH, so `scl_rise` is the one instant a received
   // bit is guaranteed valid. This block has no timing of its own.
   //
   // ENABLED, NOT SELF-STARTING. It shifts only when the layer above says a data byte is
   // expected -- selected, write direction, address phase over. Without that gate it would
   // shift the address byte as data, and the first data byte of every write would be the
   // address. The gate is the whole reason the address block and this one can share a bus
   // without sharing a bit counter.
   //
   // AND FRAMING RESETS IT MID-BYTE. A repeated START or a STOP arriving part-way through a
   // byte means the byte will never complete. The partial contents must be discarded, not
   // held over to be completed by the NEXT transfer's first three bits -- which is what a
   // design that only resets its counter on completion would do.
   // (Verilog-2001 -- structurally identical to the SystemVerilog above.)
   // -----------------------------------------------------------------------------

   module i2c_slave_rx #(
      parameter integer CNT_W = 16
   ) (
      input  wire  clk,
      input  wire  rst_n,

      // From Chapter 18.2.
      input  wire  scl_rise,
      input  wire  sda_q,

      // From Chapter 18.3 -- framing discards a partial byte.
      input  wire  start_pulse,
      input  wire  stop_pulse,

      // From the layer above (18.10/18.11): a data byte is expected now.
      input  wire  rx_enable,

      output reg         receiving,    // a byte is in progress -- the `mid_byte` report
      output reg   [3:0] bit_index,    // 0..7
      output reg   [7:0] rx_byte,
      output reg         rx_valid,     // ONE cycle, exactly once per byte
      output reg         byte_done,    // the same instant, for the acknowledge block

      output reg   [CNT_W-1:0] n_bytes,
      output reg   [CNT_W-1:0] n_partial   // bytes abandoned to framing
   );

      reg [7:0] shreg;
      wire [7:0] shreg_next = {shreg[6:0], sda_q};

      always @(posedge clk or negedge rst_n) begin
         if (!rst_n) begin
            receiving <= 1'b0;
            bit_index <= 4'd0;
            rx_byte   <= 8'h00;
            rx_valid  <= 1'b0;
            byte_done <= 1'b0;
            shreg     <= 8'h00;
            n_bytes   <= {CNT_W{1'b0}};
            n_partial <= {CNT_W{1'b0}};
         end else begin
            rx_valid  <= 1'b0;
            byte_done <= 1'b0;

            if (start_pulse || stop_pulse) begin
               // Discard a partial byte. Holding it would let the next transfer's first
               // bits complete this one, producing a byte that never existed on the wire.
               if (receiving) n_partial <= n_partial + 1'b1;
               receiving <= 1'b0;
               bit_index <= 4'd0;
               shreg     <= 8'h00;
            end else if (rx_enable && scl_rise) begin
               shreg     <= shreg_next;
               receiving <= 1'b1;
               if (bit_index == 4'd7) begin
                  // Complete. One pulse, one byte, and the counter returns to zero so the
                  // ninth (acknowledge) pulse is not mistaken for a data bit.
                  receiving <= 1'b0;
                  bit_index <= 4'd0;
                  rx_byte   <= shreg_next;
                  rx_valid  <= 1'b1;
                  byte_done <= 1'b1;
                  n_bytes   <= n_bytes + 1'b1;
               end else begin
                  bit_index <= bit_index + 4'd1;
               end
            end
         end
      end

   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_slave_rx.vhd — the same design in VHDL
   -- -----------------------------------------------------------------------------
   -- i2c_slave_rx.vhd
   -- The receive datapath: eight observed bits into one byte, handed off exactly once.
   -- Same ports, generic, reset values and pulse timing as the SystemVerilog and Verilog.
   -- -----------------------------------------------------------------------------
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   entity i2c_slave_rx is
      generic (
         CNT_W : positive := 16
      );
      port (
         clk   : in  std_logic;
         rst_n : in  std_logic;

         -- From Chapter 18.2.
         scl_rise : in  std_logic;
         sda_q    : in  std_logic;

         -- From Chapter 18.3 -- framing discards a partial byte.
         start_pulse : in  std_logic;
         stop_pulse  : in  std_logic;

         -- From the layer above: a data byte is expected now.
         rx_enable : in  std_logic;

         receiving : out std_logic;
         bit_index : out unsigned(3 downto 0);
         rx_byte   : out std_logic_vector(7 downto 0);
         rx_valid  : out std_logic;
         byte_done : out std_logic;

         n_bytes   : out unsigned(CNT_W-1 downto 0);
         n_partial : out unsigned(CNT_W-1 downto 0)
      );
   end entity i2c_slave_rx;

   architecture rtl of i2c_slave_rx is
      signal shreg      : std_logic_vector(7 downto 0) := (others => '0');
      signal shreg_next : std_logic_vector(7 downto 0);

      signal r_rcv, r_valid, r_done : std_logic := '0';
      signal r_idx  : unsigned(3 downto 0) := (others => '0');
      signal r_byte : std_logic_vector(7 downto 0) := (others => '0');
      signal c_b, c_p : unsigned(CNT_W-1 downto 0) := (others => '0');
   begin

      shreg_next <= shreg(6 downto 0) & sda_q;

      process (clk, rst_n)
      begin
         if rst_n = '0' then
            r_rcv   <= '0';
            r_idx   <= (others => '0');
            r_byte  <= (others => '0');
            r_valid <= '0';
            r_done  <= '0';
            shreg   <= (others => '0');
            c_b     <= (others => '0');
            c_p     <= (others => '0');
         elsif rising_edge(clk) then
            r_valid <= '0';
            r_done  <= '0';

            if start_pulse = '1' or stop_pulse = '1' then
               -- Discard a partial byte. Holding it would let the next transfer's first bits
               -- complete this one, producing a byte that never existed on the wire.
               if r_rcv = '1' then c_p <= c_p + 1; end if;
               r_rcv <= '0';
               r_idx <= (others => '0');
               shreg <= (others => '0');
            elsif rx_enable = '1' and scl_rise = '1' then
               shreg <= shreg_next;
               r_rcv <= '1';
               if r_idx = 7 then
                  -- Complete. One pulse, one byte, and the index returns to zero so the
                  -- ninth (acknowledge) pulse is not mistaken for a data bit.
                  r_rcv   <= '0';
                  r_idx   <= (others => '0');
                  r_byte  <= shreg_next;
                  r_valid <= '1';
                  r_done  <= '1';
                  c_b     <= c_b + 1;
               else
                  r_idx <= r_idx + 1;
               end if;
            end if;
         end if;
      end process;

      receiving <= r_rcv;
      bit_index <= r_idx;
      rx_byte   <= r_byte;
      rx_valid  <= r_valid;
      byte_done <= r_done;
      n_bytes   <= c_b;
      n_partial <= c_p;

   end architecture rtl;

7a. The testbenches

Thirteen tests. The observer is the interesting part: it logs every delivery, catches the pulse being high two cycles running, and — added after mutation testing — watches byte_done separately.

#TestProperty
T1reset has delivered nothing
T2disabled means deafwhich is what keeps the address byte out
T3one byte in, one delivery outand the pulse is one cycle
T4MSB first, with 0x80 then 0x01the two values that distinguish the orders
T5all zeros and all ones
T6four bytes, in order, none dropped or duplicated
T7exactly eight bits — and the ninth pulse delivers nothing
T8a STOP mid-byte discards the partialand the next byte is its own
T9a repeated START mid-byte does the same
T10the byte is stable in the cycle the pulse is asserted
T11disabling mid-byte stops the shift and delivers nothing
T12reset mid-byte clears the partial and the counters
T13byte_done fires once per byte, alongside rx_validadded after M10; see §8

T8 does something worth copying: after the abandoned partial it starts a fresh transfer and checks the next byte arrives complete and correct. Asserting only that nothing was delivered would pass a design that held the partial and completed it later from the next transfer's bits.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_slave_rx_tb.sv — the self-checking testbench
   // -----------------------------------------------------------------------------
   // i2c_slave_rx_tb.sv
   // Independent oracle for i2c_slave_rx. The hand-off is what is under test, not the value.
   // -----------------------------------------------------------------------------
   `timescale 1ns/1ps

   module i2c_slave_rx_tb;

      localparam integer HALF = 8;

      logic clk = 1'b0, rst_n = 1'b0;
      logic m_scl = 1'b1, m_sda = 1'b1;
      logic rx_enable = 1'b1;
      wire  scl_pin = m_scl, sda_pin = m_sda;

      logic scl_q, sda_q, scl_rise, scl_fall, sda_rise, sda_fall;
      logic start_pulse, restart_pulse, stop_pulse, bus_active, fmid;
      logic [15:0] n_sta, n_rs, n_sto;
      logic receiving, rx_valid, byte_done;
      logic [3:0] bit_index;
      logic [7:0] rx_byte;
      logic [15:0] n_bytes, n_partial;

      integer errors = 0, n, k;

      // ---- the hand-off observer. A byte delivered twice is as bad as one dropped, and
      // both are invisible to a test that only compares the value.
      integer n_valid_obs = 0, wide = 0;
      reg [7:0] log [0:15];
      integer   n_log = 0;
      logic v_d = 1'b0, bd_d = 1'b0;
      // `byte_done` gets its own observer. It is the pulse Chapter 18.5's acknowledge block
      // ARMS on, and this bench does not instantiate that block -- so without a check here
      // a receiver whose bytes are all correct and whose byte_done never fires would pass
      // everything while leaving the target permanently unable to acknowledge.
      integer n_done_obs = 0, done_wide = 0, done_mismatch = 0;
      always @(posedge clk) begin
         if (rst_n) begin
            if (rx_valid) begin
               n_valid_obs <= n_valid_obs + 1;
               log[n_log[3:0]] = rx_byte; n_log = n_log + 1;
            end
            if (rx_valid & v_d) wide <= wide + 1;
            if (byte_done) n_done_obs <= n_done_obs + 1;
            if (byte_done & bd_d) done_wide <= done_wide + 1;
            // The two pulses must be simultaneous: the acknowledge decision is taken from
            // the byte, so a byte_done that did not coincide with its rx_valid would arm a
            // slot for data the consumer has not been handed.
            if (byte_done !== rx_valid) done_mismatch <= done_mismatch + 1;
         end
         v_d <= rx_valid; bd_d <= byte_done;
      end

      i2c_slave_sync #(.SYNC_DEPTH(2)) u_sync (
         .clk(clk), .rst_n(rst_n), .scl_pin(scl_pin), .sda_pin(sda_pin),
         .scl_q(scl_q), .sda_q(sda_q), .scl_rise(scl_rise), .scl_fall(scl_fall),
         .sda_rise(sda_rise), .sda_fall(sda_fall));

      i2c_slave_framing #(.CNT_W(16)) u_frm (
         .clk(clk), .rst_n(rst_n), .scl_q(scl_q),
         .sda_rise(sda_rise), .sda_fall(sda_fall),
         .start_pulse(start_pulse), .restart_pulse(restart_pulse), .stop_pulse(stop_pulse),
         .bus_active(bus_active), .mid_byte(receiving), .framing_midbyte(fmid),
         .n_starts(n_sta), .n_restarts(n_rs), .n_stops(n_sto));

      i2c_slave_rx #(.CNT_W(16)) dut (
         .clk(clk), .rst_n(rst_n), .scl_rise(scl_rise), .sda_q(sda_q),
         .start_pulse(start_pulse), .stop_pulse(stop_pulse), .rx_enable(rx_enable),
         .receiving(receiving), .bit_index(bit_index), .rx_byte(rx_byte),
         .rx_valid(rx_valid), .byte_done(byte_done),
         .n_bytes(n_bytes), .n_partial(n_partial));

      always #5 clk = ~clk;
      task step;  begin @(posedge clk); @(negedge clk); end endtask
      task phase; begin for (n = 0; n < HALF; n = n + 1) step; end endtask

      task do_reset;
         begin
            @(negedge clk); rst_n = 1'b0; m_scl = 1'b1; m_sda = 1'b1; rx_enable = 1'b1;
            n_valid_obs = 0; wide = 0; n_log = 0;
            n_done_obs = 0; done_wide = 0; done_mismatch = 0;
            step; step; @(negedge clk); rst_n = 1'b1; phase;
         end
      endtask

      task gen_start; begin
         @(negedge clk); m_sda = 1'b1; m_scl = 1'b1; phase;
         @(negedge clk); m_sda = 1'b0; phase;
         @(negedge clk); m_scl = 1'b0; phase; end
      endtask
      task gen_stop; begin
         @(negedge clk); m_scl = 1'b0; m_sda = 1'b0; phase;
         @(negedge clk); m_scl = 1'b1; phase;
         @(negedge clk); m_sda = 1'b1; phase; end
      endtask
      task gen_restart; begin
         @(negedge clk); m_scl = 1'b0; m_sda = 1'b1; phase;
         @(negedge clk); m_scl = 1'b1; phase;
         @(negedge clk); m_sda = 1'b0; phase;
         @(negedge clk); m_scl = 1'b0; phase; end
      endtask
      task gen_bit (input b); begin
         @(negedge clk); m_scl = 1'b0; phase;
         @(negedge clk); m_sda = b;    phase;
         @(negedge clk); m_scl = 1'b1; phase;
         @(negedge clk); m_scl = 1'b0; phase; end
      endtask
      task gen_byte (input [7:0] d);
         begin for (k = 7; k >= 0; k = k - 1) gen_bit(d[k]); end
      endtask
      // The ninth pulse. `rx_enable` is dropped for it, which is what the transaction layer
      // of Chapter 18.10 does: the acknowledge pulse is not a data bit, and a receive path
      // left enabled through it shifts it in -- so every byte after the first is displaced
      // by one bit. The gate is the contract, and this task models it.
      task gen_ack_slot; begin
         @(negedge clk); m_scl = 1'b0; rx_enable = 1'b0; phase;
         @(negedge clk); m_sda = 1'b1; phase;
         @(negedge clk); m_scl = 1'b1; phase;
         @(negedge clk); m_scl = 1'b0; phase;
         @(negedge clk); rx_enable = 1'b1; end
      endtask

      task ck_bit (input [200*8:1] w, input g, input e);
         begin if (g !== e) begin
            $display("  FAIL %0s: got %0b expected %0b", w, g, e); errors = errors + 1; end end
      endtask
      task ck_int (input [200*8:1] w, input integer g, input integer e);
         begin if (g !== e) begin
            $display("  FAIL %0s: got %0d expected %0d", w, g, e); errors = errors + 1; end end
      endtask

      initial begin
         $display("=== i2c_slave_rx: eight observed bits, handed off exactly once ===");

         // T1. Reset delivers nothing.
         do_reset;
         $display("T1  a reset receiver has delivered nothing");
         ck_int("T1 no bytes", n_valid_obs, 0);
         ck_bit("T1 not receiving", receiving, 1'b0);

         // T2. DISABLED MEANS DEAF. Without the gate this block would shift the address
         //     byte as data, and the first data byte of every write would be the address.
         do_reset;
         @(negedge clk); rx_enable = 1'b0;
         gen_start; gen_byte(8'hA5);
         $display("T2  disabled, it shifts nothing -- which is what keeps the address out");
         ck_int("T2 no byte delivered", n_valid_obs, 0);
         ck_bit("T2 and it never started receiving", receiving, 1'b0);

         // T3. One byte, exactly once.
         do_reset;
         gen_start; gen_byte(8'hA5);
         $display("T3  one byte in, one delivery out");
         ck_int("T3 exactly one delivery", n_valid_obs, 1);
         ck_int("T3 the value is right", log[0], 8'hA5);
         ck_int("T3 the pulse was one cycle", wide, 0);
         ck_int("T3 the counter agrees", n_bytes, 1);

         // T4. MSB first, proved with an asymmetric byte. 0x80 and 0x01 are the two values
         //     whose transmission distinguishes a correct shift direction from a reversed
         //     one; a symmetric byte passes under either.
         do_reset;
         gen_start; gen_byte(8'h80); gen_ack_slot; gen_byte(8'h01);
         $display("T4  MSB first, proved with 0x80 then 0x01");
         ck_int("T4 first byte", log[0], 8'h80);
         ck_int("T4 second byte", log[1], 8'h01);

         // T5. The extremes.
         do_reset;
         gen_start; gen_byte(8'h00); gen_ack_slot; gen_byte(8'hFF);
         $display("T5  all zeros and all ones");
         ck_int("T5 zeros", log[0], 8'h00);
         ck_int("T5 ones", log[1], 8'hFF);

         // T6. Four bytes in a row, in order, none dropped and none duplicated.
         do_reset;
         gen_start;
         gen_byte(8'h11); gen_ack_slot;
         gen_byte(8'h22); gen_ack_slot;
         gen_byte(8'h33); gen_ack_slot;
         gen_byte(8'h44); gen_ack_slot;
         $display("T6  four bytes, in order, none dropped or duplicated");
         ck_int("T6 four deliveries", n_valid_obs, 4);
         ck_int("T6 b0", log[0], 8'h11);
         ck_int("T6 b1", log[1], 8'h22);
         ck_int("T6 b2", log[2], 8'h33);
         ck_int("T6 b3", log[3], 8'h44);

         // T7. EXACTLY EIGHT BITS. A ninth shift would consume the acknowledge slot as data.
         do_reset;
         gen_start;
         for (k = 0; k < 7; k = k + 1) gen_bit(1'b1);
         ck_bit("T7 still receiving after seven", receiving, 1'b1);
         ck_int("T7 index reached seven", bit_index, 7);
         gen_bit(1'b1);
         ck_bit("T7 done after exactly eight", receiving, 1'b0);
         ck_int("T7 one delivery", n_valid_obs, 1);
         gen_ack_slot;
         ck_int("T7 and the ninth pulse delivered nothing", n_valid_obs, 1);

         // T8. A STOP MID-BYTE DISCARDS THE PARTIAL. Holding it would let the NEXT
         //     transfer's first bits complete this one, producing a byte that was never
         //     on the wire.
         do_reset;
         gen_start;
         gen_bit(1'b1); gen_bit(1'b0); gen_bit(1'b1);   // three bits only
         ck_bit("T8 a byte is in progress", receiving, 1'b1);
         gen_stop;
         $display("T8  a STOP mid-byte discards the partial rather than holding it");
         ck_int("T8 nothing was delivered", n_valid_obs, 0);
         ck_int("T8 and the abandonment was counted", n_partial, 1);
         // Now a fresh transfer: its first byte must be its own, not five bits of it.
         gen_start; gen_byte(8'h5A);
         ck_int("T8 the next byte is complete and correct", log[0], 8'h5A);
         ck_int("T8 delivered once", n_valid_obs, 1);

         // T9. A REPEATED START MID-BYTE does the same.
         do_reset;
         gen_start;
         gen_bit(1'b1); gen_bit(1'b1);
         gen_restart;
         $display("T9  a repeated START mid-byte also discards the partial");
         ck_int("T9 nothing delivered", n_valid_obs, 0);
         ck_int("T9 counted as abandoned", n_partial, 1);

         // T10. The value is stable when the pulse is high -- a consumer that registers
         //      rx_byte on rx_valid must get the right byte.
         do_reset;
         gen_start; gen_byte(8'h96);
         $display("T10 the byte is stable in the cycle the pulse is asserted");
         ck_int("T10 the logged value matches", log[0], 8'h96);
         ck_int("T10 and the output still holds it", rx_byte, 8'h96);

         // T11. Disabling mid-byte stops the shift where it is, and does not deliver.
         do_reset;
         gen_start;
         gen_bit(1'b1); gen_bit(1'b0);
         @(negedge clk); rx_enable = 1'b0;
         gen_bit(1'b1); gen_bit(1'b1); gen_bit(1'b1); gen_bit(1'b1);
         gen_bit(1'b1); gen_bit(1'b1); gen_bit(1'b1);
         $display("T11 disabling mid-byte stops the shift and delivers nothing");
         ck_int("T11 no delivery", n_valid_obs, 0);

         // T12. Reset mid-byte clears everything.
         do_reset;
         gen_start; gen_bit(1'b1); gen_bit(1'b0);
         @(negedge clk); rst_n = 1'b0; step; step; @(negedge clk); rst_n = 1'b1; phase;
         $display("T12 reset mid-byte clears the partial and the counters");
         ck_bit("T12 not receiving", receiving, 1'b0);
         ck_int("T12 counters cleared", n_bytes, 0);

         // ----------------------------------------------------------------
         // T13. byte_done IS THE INTERFACE TO THE ACKNOWLEDGE BLOCK. One pulse, one cycle,
         //      simultaneous with rx_valid, once per byte. Chapter 18.5 arms its slot on
         //      this signal and nothing else, so a receiver with perfect bytes and no
         //      byte_done leaves the target unable to acknowledge anything at all.
         // ----------------------------------------------------------------
         do_reset;
         gen_start;
         gen_byte(8'h3C); gen_ack_slot;
         gen_byte(8'hC3); gen_ack_slot;
         $display("T13 byte_done fires once per byte, one cycle, alongside rx_valid");
         ck_int("T13 two byte_done pulses", n_done_obs, 2);
         ck_int("T13 and two deliveries", n_valid_obs, 2);
         ck_int("T13 neither pulse was wide", done_wide, 0);
         ck_int("T13 and they were always simultaneous", done_mismatch, 0);

         if (errors == 0) $display("=== i2c_slave_rx: ALL CHECKS PASSED ===");
         else             $display("=== i2c_slave_rx: %0d CHECK(S) FAILED ===", errors);
         $finish;
      end

   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_slave_rx_tb.v — the same tests in Verilog-2001
   // -----------------------------------------------------------------------------
   // i2c_slave_rx_tb.v
   // Independent oracle for i2c_slave_rx. The hand-off is what is under test, not the value.
   // -----------------------------------------------------------------------------
   `timescale 1ns/1ps

   module i2c_slave_rx_tb;

      localparam integer HALF = 8;

      reg clk = 1'b0, rst_n = 1'b0;
      reg m_scl = 1'b1, m_sda = 1'b1;
      reg rx_enable = 1'b1;
      wire  scl_pin = m_scl, sda_pin = m_sda;

      wire scl_q, sda_q, scl_rise, scl_fall, sda_rise, sda_fall;
      wire start_pulse, restart_pulse, stop_pulse, bus_active, fmid;
      wire [15:0] n_sta, n_rs, n_sto;
      wire receiving, rx_valid, byte_done;
      wire [3:0] bit_index;
      wire [7:0] rx_byte;
      wire [15:0] n_bytes, n_partial;

      integer errors = 0, n, k;

      // ---- the hand-off observer. A byte delivered twice is as bad as one dropped, and
      // both are invisible to a test that only compares the value.
      integer n_valid_obs = 0, wide = 0;
      reg [7:0] log [0:15];
      integer   n_log = 0;
      reg v_d = 1'b0, bd_d = 1'b0;
      // `byte_done` gets its own observer. It is the pulse Chapter 18.5's acknowledge block
      // ARMS on, and this bench does not instantiate that block -- so without a check here
      // a receiver whose bytes are all correct and whose byte_done never fires would pass
      // everything while leaving the target permanently unable to acknowledge.
      integer n_done_obs = 0, done_wide = 0, done_mismatch = 0;
      always @(posedge clk) begin
         if (rst_n) begin
            if (rx_valid) begin
               n_valid_obs <= n_valid_obs + 1;
               log[n_log[3:0]] = rx_byte; n_log = n_log + 1;
            end
            if (rx_valid & v_d) wide <= wide + 1;
            if (byte_done) n_done_obs <= n_done_obs + 1;
            if (byte_done & bd_d) done_wide <= done_wide + 1;
            // The two pulses must be simultaneous: the acknowledge decision is taken from
            // the byte, so a byte_done that did not coincide with its rx_valid would arm a
            // slot for data the consumer has not been handed.
            if (byte_done !== rx_valid) done_mismatch <= done_mismatch + 1;
         end
         v_d <= rx_valid; bd_d <= byte_done;
      end

      i2c_slave_sync #(.SYNC_DEPTH(2)) u_sync (
         .clk(clk), .rst_n(rst_n), .scl_pin(scl_pin), .sda_pin(sda_pin),
         .scl_q(scl_q), .sda_q(sda_q), .scl_rise(scl_rise), .scl_fall(scl_fall),
         .sda_rise(sda_rise), .sda_fall(sda_fall));

      i2c_slave_framing #(.CNT_W(16)) u_frm (
         .clk(clk), .rst_n(rst_n), .scl_q(scl_q),
         .sda_rise(sda_rise), .sda_fall(sda_fall),
         .start_pulse(start_pulse), .restart_pulse(restart_pulse), .stop_pulse(stop_pulse),
         .bus_active(bus_active), .mid_byte(receiving), .framing_midbyte(fmid),
         .n_starts(n_sta), .n_restarts(n_rs), .n_stops(n_sto));

      i2c_slave_rx #(.CNT_W(16)) dut (
         .clk(clk), .rst_n(rst_n), .scl_rise(scl_rise), .sda_q(sda_q),
         .start_pulse(start_pulse), .stop_pulse(stop_pulse), .rx_enable(rx_enable),
         .receiving(receiving), .bit_index(bit_index), .rx_byte(rx_byte),
         .rx_valid(rx_valid), .byte_done(byte_done),
         .n_bytes(n_bytes), .n_partial(n_partial));

      always #5 clk = ~clk;
      task step;  begin @(posedge clk); @(negedge clk); end endtask
      task phase; begin for (n = 0; n < HALF; n = n + 1) step; end endtask

      task do_reset;
         begin
            @(negedge clk); rst_n = 1'b0; m_scl = 1'b1; m_sda = 1'b1; rx_enable = 1'b1;
            n_valid_obs = 0; wide = 0; n_log = 0;
            n_done_obs = 0; done_wide = 0; done_mismatch = 0;
            step; step; @(negedge clk); rst_n = 1'b1; phase;
         end
      endtask

      task gen_start; begin
         @(negedge clk); m_sda = 1'b1; m_scl = 1'b1; phase;
         @(negedge clk); m_sda = 1'b0; phase;
         @(negedge clk); m_scl = 1'b0; phase; end
      endtask
      task gen_stop; begin
         @(negedge clk); m_scl = 1'b0; m_sda = 1'b0; phase;
         @(negedge clk); m_scl = 1'b1; phase;
         @(negedge clk); m_sda = 1'b1; phase; end
      endtask
      task gen_restart; begin
         @(negedge clk); m_scl = 1'b0; m_sda = 1'b1; phase;
         @(negedge clk); m_scl = 1'b1; phase;
         @(negedge clk); m_sda = 1'b0; phase;
         @(negedge clk); m_scl = 1'b0; phase; end
      endtask
      task gen_bit (input b); begin
         @(negedge clk); m_scl = 1'b0; phase;
         @(negedge clk); m_sda = b;    phase;
         @(negedge clk); m_scl = 1'b1; phase;
         @(negedge clk); m_scl = 1'b0; phase; end
      endtask
      task gen_byte (input [7:0] d);
         begin for (k = 7; k >= 0; k = k - 1) gen_bit(d[k]); end
      endtask
      // The ninth pulse. `rx_enable` is dropped for it, which is what the transaction layer
      // of Chapter 18.10 does: the acknowledge pulse is not a data bit, and a receive path
      // left enabled through it shifts it in -- so every byte after the first is displaced
      // by one bit. The gate is the contract, and this task models it.
      task gen_ack_slot; begin
         @(negedge clk); m_scl = 1'b0; rx_enable = 1'b0; phase;
         @(negedge clk); m_sda = 1'b1; phase;
         @(negedge clk); m_scl = 1'b1; phase;
         @(negedge clk); m_scl = 1'b0; phase;
         @(negedge clk); rx_enable = 1'b1; end
      endtask

      task ck_bit (input [200*8:1] w, input g, input e);
         begin if (g !== e) begin
            $display("  FAIL %0s: got %0b expected %0b", w, g, e); errors = errors + 1; end end
      endtask
      task ck_int (input [200*8:1] w, input integer g, input integer e);
         begin if (g !== e) begin
            $display("  FAIL %0s: got %0d expected %0d", w, g, e); errors = errors + 1; end end
      endtask

      initial begin
         $display("=== i2c_slave_rx: eight observed bits, handed off exactly once ===");

         // T1. Reset delivers nothing.
         do_reset;
         $display("T1  a reset receiver has delivered nothing");
         ck_int("T1 no bytes", n_valid_obs, 0);
         ck_bit("T1 not receiving", receiving, 1'b0);

         // T2. DISABLED MEANS DEAF. Without the gate this block would shift the address
         //     byte as data, and the first data byte of every write would be the address.
         do_reset;
         @(negedge clk); rx_enable = 1'b0;
         gen_start; gen_byte(8'hA5);
         $display("T2  disabled, it shifts nothing -- which is what keeps the address out");
         ck_int("T2 no byte delivered", n_valid_obs, 0);
         ck_bit("T2 and it never started receiving", receiving, 1'b0);

         // T3. One byte, exactly once.
         do_reset;
         gen_start; gen_byte(8'hA5);
         $display("T3  one byte in, one delivery out");
         ck_int("T3 exactly one delivery", n_valid_obs, 1);
         ck_int("T3 the value is right", log[0], 8'hA5);
         ck_int("T3 the pulse was one cycle", wide, 0);
         ck_int("T3 the counter agrees", n_bytes, 1);

         // T4. MSB first, proved with an asymmetric byte. 0x80 and 0x01 are the two values
         //     whose transmission distinguishes a correct shift direction from a reversed
         //     one; a symmetric byte passes under either.
         do_reset;
         gen_start; gen_byte(8'h80); gen_ack_slot; gen_byte(8'h01);
         $display("T4  MSB first, proved with 0x80 then 0x01");
         ck_int("T4 first byte", log[0], 8'h80);
         ck_int("T4 second byte", log[1], 8'h01);

         // T5. The extremes.
         do_reset;
         gen_start; gen_byte(8'h00); gen_ack_slot; gen_byte(8'hFF);
         $display("T5  all zeros and all ones");
         ck_int("T5 zeros", log[0], 8'h00);
         ck_int("T5 ones", log[1], 8'hFF);

         // T6. Four bytes in a row, in order, none dropped and none duplicated.
         do_reset;
         gen_start;
         gen_byte(8'h11); gen_ack_slot;
         gen_byte(8'h22); gen_ack_slot;
         gen_byte(8'h33); gen_ack_slot;
         gen_byte(8'h44); gen_ack_slot;
         $display("T6  four bytes, in order, none dropped or duplicated");
         ck_int("T6 four deliveries", n_valid_obs, 4);
         ck_int("T6 b0", log[0], 8'h11);
         ck_int("T6 b1", log[1], 8'h22);
         ck_int("T6 b2", log[2], 8'h33);
         ck_int("T6 b3", log[3], 8'h44);

         // T7. EXACTLY EIGHT BITS. A ninth shift would consume the acknowledge slot as data.
         do_reset;
         gen_start;
         for (k = 0; k < 7; k = k + 1) gen_bit(1'b1);
         ck_bit("T7 still receiving after seven", receiving, 1'b1);
         ck_int("T7 index reached seven", bit_index, 7);
         gen_bit(1'b1);
         ck_bit("T7 done after exactly eight", receiving, 1'b0);
         ck_int("T7 one delivery", n_valid_obs, 1);
         gen_ack_slot;
         ck_int("T7 and the ninth pulse delivered nothing", n_valid_obs, 1);

         // T8. A STOP MID-BYTE DISCARDS THE PARTIAL. Holding it would let the NEXT
         //     transfer's first bits complete this one, producing a byte that was never
         //     on the wire.
         do_reset;
         gen_start;
         gen_bit(1'b1); gen_bit(1'b0); gen_bit(1'b1);   // three bits only
         ck_bit("T8 a byte is in progress", receiving, 1'b1);
         gen_stop;
         $display("T8  a STOP mid-byte discards the partial rather than holding it");
         ck_int("T8 nothing was delivered", n_valid_obs, 0);
         ck_int("T8 and the abandonment was counted", n_partial, 1);
         // Now a fresh transfer: its first byte must be its own, not five bits of it.
         gen_start; gen_byte(8'h5A);
         ck_int("T8 the next byte is complete and correct", log[0], 8'h5A);
         ck_int("T8 delivered once", n_valid_obs, 1);

         // T9. A REPEATED START MID-BYTE does the same.
         do_reset;
         gen_start;
         gen_bit(1'b1); gen_bit(1'b1);
         gen_restart;
         $display("T9  a repeated START mid-byte also discards the partial");
         ck_int("T9 nothing delivered", n_valid_obs, 0);
         ck_int("T9 counted as abandoned", n_partial, 1);

         // T10. The value is stable when the pulse is high -- a consumer that registers
         //      rx_byte on rx_valid must get the right byte.
         do_reset;
         gen_start; gen_byte(8'h96);
         $display("T10 the byte is stable in the cycle the pulse is asserted");
         ck_int("T10 the logged value matches", log[0], 8'h96);
         ck_int("T10 and the output still holds it", rx_byte, 8'h96);

         // T11. Disabling mid-byte stops the shift where it is, and does not deliver.
         do_reset;
         gen_start;
         gen_bit(1'b1); gen_bit(1'b0);
         @(negedge clk); rx_enable = 1'b0;
         gen_bit(1'b1); gen_bit(1'b1); gen_bit(1'b1); gen_bit(1'b1);
         gen_bit(1'b1); gen_bit(1'b1); gen_bit(1'b1);
         $display("T11 disabling mid-byte stops the shift and delivers nothing");
         ck_int("T11 no delivery", n_valid_obs, 0);

         // T12. Reset mid-byte clears everything.
         do_reset;
         gen_start; gen_bit(1'b1); gen_bit(1'b0);
         @(negedge clk); rst_n = 1'b0; step; step; @(negedge clk); rst_n = 1'b1; phase;
         $display("T12 reset mid-byte clears the partial and the counters");
         ck_bit("T12 not receiving", receiving, 1'b0);
         ck_int("T12 counters cleared", n_bytes, 0);

         // ----------------------------------------------------------------
         // T13. byte_done IS THE INTERFACE TO THE ACKNOWLEDGE BLOCK. One pulse, one cycle,
         //      simultaneous with rx_valid, once per byte. Chapter 18.5 arms its slot on
         //      this signal and nothing else, so a receiver with perfect bytes and no
         //      byte_done leaves the target unable to acknowledge anything at all.
         // ----------------------------------------------------------------
         do_reset;
         gen_start;
         gen_byte(8'h3C); gen_ack_slot;
         gen_byte(8'hC3); gen_ack_slot;
         $display("T13 byte_done fires once per byte, one cycle, alongside rx_valid");
         ck_int("T13 two byte_done pulses", n_done_obs, 2);
         ck_int("T13 and two deliveries", n_valid_obs, 2);
         ck_int("T13 neither pulse was wide", done_wide, 0);
         ck_int("T13 and they were always simultaneous", done_mismatch, 0);

         if (errors == 0) $display("=== i2c_slave_rx: ALL CHECKS PASSED ===");
         else             $display("=== i2c_slave_rx: %0d CHECK(S) FAILED ===", errors);
         $finish;
      end

   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_slave_rx_tb.vhd — the same tests in VHDL
   -- -----------------------------------------------------------------------------
   -- i2c_slave_rx_tb.vhd
   -- Independent oracle for i2c_slave_rx. Behavioural twin of the SystemVerilog and Verilog
   -- benches. The HAND-OFF is what is under test, not the value: a byte delivered twice is
   -- as bad as one dropped, and both are invisible to a test that only compares bytes.
   -- -----------------------------------------------------------------------------
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   entity i2c_slave_rx_tb is
   end entity i2c_slave_rx_tb;

   architecture sim of i2c_slave_rx_tb is

      constant HALF : positive := 8;

      signal clk   : std_logic := '0';
      signal rst_n : std_logic := '0';
      signal m_scl : std_logic := '1';
      signal m_sda : std_logic := '1';
      signal rx_enable : std_logic := '1';

      signal scl_q, sda_q : std_logic;
      signal scl_rise, scl_fall, sda_rise, sda_fall : std_logic;
      signal start_pulse, restart_pulse, stop_pulse, bus_active, fmid : std_logic;
      signal n_sta, n_rs, n_sto : unsigned(15 downto 0);
      signal receiving, rx_valid, byte_done : std_logic;
      signal bit_index : unsigned(3 downto 0);
      signal rx_byte : std_logic_vector(7 downto 0);
      signal n_bytes, n_partial : unsigned(15 downto 0);

      signal halt : boolean := false;

      type log_t is array (0 to 15) of std_logic_vector(7 downto 0);
      signal logmem : log_t := (others => (others => '0'));
      signal n_log  : integer := 0;
      signal n_valid_obs, wide : integer := 0;
      -- byte_done gets its own observer: it is the pulse Chapter 18.5's acknowledge
      -- block ARMS on, and this bench does not instantiate that block.
      signal n_done_obs, done_wide, done_mismatch : integer := 0;
      signal clr : boolean := false;

   begin

      u_sync : entity work.i2c_slave_sync
         generic map (SYNC_DEPTH => 2)
         port map (clk => clk, rst_n => rst_n, scl_pin => m_scl, sda_pin => m_sda,
            scl_q => scl_q, sda_q => sda_q, scl_rise => scl_rise, scl_fall => scl_fall,
            sda_rise => sda_rise, sda_fall => sda_fall);

      u_frm : entity work.i2c_slave_framing
         generic map (CNT_W => 16)
         port map (clk => clk, rst_n => rst_n, scl_q => scl_q,
            sda_rise => sda_rise, sda_fall => sda_fall,
            start_pulse => start_pulse, restart_pulse => restart_pulse,
            stop_pulse => stop_pulse, bus_active => bus_active,
            mid_byte => receiving, framing_midbyte => fmid,
            n_starts => n_sta, n_restarts => n_rs, n_stops => n_sto);

      dut : entity work.i2c_slave_rx
         generic map (CNT_W => 16)
         port map (clk => clk, rst_n => rst_n, scl_rise => scl_rise, sda_q => sda_q,
            start_pulse => start_pulse, stop_pulse => stop_pulse, rx_enable => rx_enable,
            receiving => receiving, bit_index => bit_index, rx_byte => rx_byte,
            rx_valid => rx_valid, byte_done => byte_done,
            n_bytes => n_bytes, n_partial => n_partial);

      clkgen : process
      begin
         while not halt loop
            clk <= '0'; wait for 5 ns;
            clk <= '1'; wait for 5 ns;
         end loop;
         wait;
      end process;

      obs : process (clk, clr)
         variable v_d, bd_d : std_logic := '0';
      begin
         if clr then
            n_valid_obs <= 0; wide <= 0; n_log <= 0;
            n_done_obs <= 0; done_wide <= 0; done_mismatch <= 0;
         elsif rising_edge(clk) then
            if rst_n = '1' then
               if rx_valid = '1' then
                  n_valid_obs <= n_valid_obs + 1;
                  logmem(n_log mod 16) <= rx_byte;
                  n_log <= n_log + 1;
               end if;
               if rx_valid = '1' and v_d = '1' then wide <= wide + 1; end if;
               if byte_done = '1' then n_done_obs <= n_done_obs + 1; end if;
               if byte_done = '1' and bd_d = '1' then done_wide <= done_wide + 1; end if;
               -- The two pulses must be simultaneous.
               if byte_done /= rx_valid then done_mismatch <= done_mismatch + 1; end if;
            end if;
            v_d := rx_valid; bd_d := byte_done;
         end if;
      end process;

      stim : process
         variable err : integer := 0;
         variable k : integer;

         procedure step is
         begin
            wait until rising_edge(clk); wait until falling_edge(clk);
         end procedure;

         procedure phase is
         begin
            for i in 1 to HALF loop step; end loop;
         end procedure;

         procedure do_reset is
         begin
            wait until falling_edge(clk);
            rst_n <= '0'; m_scl <= '1'; m_sda <= '1'; rx_enable <= '1';
            clr <= true; wait for 1 ns; clr <= false;
            step; step;
            wait until falling_edge(clk); rst_n <= '1';
            phase;
         end procedure;

         procedure gen_start is
         begin
            wait until falling_edge(clk); m_sda <= '1'; m_scl <= '1'; phase;
            wait until falling_edge(clk); m_sda <= '0'; phase;
            wait until falling_edge(clk); m_scl <= '0'; phase;
         end procedure;

         procedure gen_restart is
         begin
            wait until falling_edge(clk); m_scl <= '0'; m_sda <= '1'; phase;
            wait until falling_edge(clk); m_scl <= '1'; phase;
            wait until falling_edge(clk); m_sda <= '0'; phase;
            wait until falling_edge(clk); m_scl <= '0'; phase;
         end procedure;

         procedure gen_stop is
         begin
            wait until falling_edge(clk); m_scl <= '0'; m_sda <= '0'; phase;
            wait until falling_edge(clk); m_scl <= '1'; phase;
            wait until falling_edge(clk); m_sda <= '1'; phase;
         end procedure;

         procedure gen_bit (b : std_logic) is
         begin
            wait until falling_edge(clk); m_scl <= '0'; phase;
            wait until falling_edge(clk); m_sda <= b;   phase;
            wait until falling_edge(clk); m_scl <= '1'; phase;
            wait until falling_edge(clk); m_scl <= '0'; phase;
         end procedure;

         procedure gen_byte (d : std_logic_vector(7 downto 0)) is
         begin
            for i in 7 downto 0 loop gen_bit(d(i)); end loop;
         end procedure;

         -- The ninth pulse. rx_enable is dropped for it, which is what the transaction layer
         -- of Chapter 18.10 does: the acknowledge pulse is not a data bit, and a receive path
         -- left enabled through it shifts it in -- so every byte after the first is displaced.
         procedure gen_ack_slot is
         begin
            wait until falling_edge(clk); m_scl <= '0'; rx_enable <= '0'; phase;
            wait until falling_edge(clk); m_sda <= '1'; phase;
            wait until falling_edge(clk); m_scl <= '1'; phase;
            wait until falling_edge(clk); m_scl <= '0'; phase;
            wait until falling_edge(clk); rx_enable <= '1';
         end procedure;

         procedure ck_bit (what : string; g : std_logic; e : std_logic) is
         begin
            if g /= e then
               report "  FAIL " & what severity note;
               err := err + 1;
            end if;
         end procedure;

         procedure ck_int (what : string; g : integer; e : integer) is
         begin
            if g /= e then
               report "  FAIL " & what & ": got " & integer'image(g)
                      & " expected " & integer'image(e) severity note;
               err := err + 1;
            end if;
         end procedure;

      begin
         report "=== i2c_slave_rx: eight observed bits, handed off exactly once ==="
                severity note;

         -- T1. Reset delivers nothing.
         do_reset;
         report "T1  a reset receiver has delivered nothing" severity note;
         ck_int("T1 no bytes", n_valid_obs, 0);
         ck_bit("T1 not receiving", receiving, '0');

         -- T2. Disabled means deaf.
         do_reset;
         wait until falling_edge(clk); rx_enable <= '0';
         gen_start; gen_byte(x"A5");
         report "T2  disabled, it shifts nothing -- which is what keeps the address out"
                severity note;
         ck_int("T2 no byte delivered", n_valid_obs, 0);
         ck_bit("T2 and it never started receiving", receiving, '0');

         -- T3. One byte, exactly once.
         do_reset;
         gen_start; gen_byte(x"A5");
         report "T3  one byte in, one delivery out" severity note;
         ck_int("T3 exactly one delivery", n_valid_obs, 1);
         ck_int("T3 the value is right", to_integer(unsigned(logmem(0))), 16#A5#);
         ck_int("T3 the pulse was one cycle", wide, 0);
         ck_int("T3 the counter agrees", to_integer(n_bytes), 1);

         -- T4. MSB first, proved with 0x80 then 0x01.
         do_reset;
         gen_start; gen_byte(x"80"); gen_ack_slot; gen_byte(x"01");
         report "T4  MSB first, proved with 0x80 then 0x01" severity note;
         ck_int("T4 first byte", to_integer(unsigned(logmem(0))), 16#80#);
         ck_int("T4 second byte", to_integer(unsigned(logmem(1))), 16#01#);

         -- T5. The extremes.
         do_reset;
         gen_start; gen_byte(x"00"); gen_ack_slot; gen_byte(x"FF");
         report "T5  all zeros and all ones" severity note;
         ck_int("T5 zeros", to_integer(unsigned(logmem(0))), 0);
         ck_int("T5 ones", to_integer(unsigned(logmem(1))), 255);

         -- T6. Four bytes in order.
         do_reset;
         gen_start;
         gen_byte(x"11"); gen_ack_slot;
         gen_byte(x"22"); gen_ack_slot;
         gen_byte(x"33"); gen_ack_slot;
         gen_byte(x"44"); gen_ack_slot;
         report "T6  four bytes, in order, none dropped or duplicated" severity note;
         ck_int("T6 four deliveries", n_valid_obs, 4);
         ck_int("T6 b0", to_integer(unsigned(logmem(0))), 16#11#);
         ck_int("T6 b1", to_integer(unsigned(logmem(1))), 16#22#);
         ck_int("T6 b2", to_integer(unsigned(logmem(2))), 16#33#);
         ck_int("T6 b3", to_integer(unsigned(logmem(3))), 16#44#);

         -- T7. Exactly eight bits.
         do_reset;
         gen_start;
         for k in 0 to 6 loop gen_bit('1'); end loop;
         ck_bit("T7 still receiving after seven", receiving, '1');
         ck_int("T7 index reached seven", to_integer(bit_index), 7);
         gen_bit('1');
         ck_bit("T7 done after exactly eight", receiving, '0');
         ck_int("T7 one delivery", n_valid_obs, 1);
         gen_ack_slot;
         ck_int("T7 and the ninth pulse delivered nothing", n_valid_obs, 1);

         -- T8. A STOP mid-byte discards the partial.
         do_reset;
         gen_start;
         gen_bit('1'); gen_bit('0'); gen_bit('1');
         ck_bit("T8 a byte is in progress", receiving, '1');
         gen_stop;
         report "T8  a STOP mid-byte discards the partial rather than holding it"
                severity note;
         ck_int("T8 nothing was delivered", n_valid_obs, 0);
         ck_int("T8 and the abandonment was counted", to_integer(n_partial), 1);
         gen_start; gen_byte(x"5A");
         ck_int("T8 the next byte is complete and correct",
                to_integer(unsigned(logmem(0))), 16#5A#);
         ck_int("T8 delivered once", n_valid_obs, 1);

         -- T9. A repeated START mid-byte does the same.
         do_reset;
         gen_start;
         gen_bit('1'); gen_bit('1');
         gen_restart;
         report "T9  a repeated START mid-byte also discards the partial" severity note;
         ck_int("T9 nothing delivered", n_valid_obs, 0);
         ck_int("T9 counted as abandoned", to_integer(n_partial), 1);

         -- T10. The value is stable while the pulse is asserted.
         do_reset;
         gen_start; gen_byte(x"96");
         report "T10 the byte is stable in the cycle the pulse is asserted" severity note;
         ck_int("T10 the logged value matches", to_integer(unsigned(logmem(0))), 16#96#);
         ck_int("T10 and the output still holds it", to_integer(unsigned(rx_byte)), 16#96#);

         -- T11. Disabling mid-byte stops the shift and delivers nothing.
         do_reset;
         gen_start;
         gen_bit('1'); gen_bit('0');
         wait until falling_edge(clk); rx_enable <= '0';
         for k in 0 to 6 loop gen_bit('1'); end loop;
         report "T11 disabling mid-byte stops the shift and delivers nothing" severity note;
         ck_int("T11 no delivery", n_valid_obs, 0);

         -- T12. Reset mid-byte clears everything.
         do_reset;
         gen_start; gen_bit('1'); gen_bit('0');
         wait until falling_edge(clk); rst_n <= '0'; step; step;
         wait until falling_edge(clk); rst_n <= '1'; phase;
         report "T12 reset mid-byte clears the partial and the counters" severity note;
         ck_bit("T12 not receiving", receiving, '0');
         ck_int("T12 counters cleared", to_integer(n_bytes), 0);

         -- T13. byte_done is the interface to the acknowledge block.
         do_reset;
         gen_start;
         gen_byte(x"3C"); gen_ack_slot;
         gen_byte(x"C3"); gen_ack_slot;
         report "T13 byte_done fires once per byte, one cycle, alongside rx_valid"
                severity note;
         ck_int("T13 two byte_done pulses", n_done_obs, 2);
         ck_int("T13 and two deliveries", n_valid_obs, 2);
         ck_int("T13 neither pulse was wide", done_wide, 0);
         ck_int("T13 and they were always simultaneous", done_mismatch, 0);

         if err = 0 then
            report "=== i2c_slave_rx: ALL CHECKS PASSED ===" severity note;
         else
            report "=== i2c_slave_rx: " & integer'image(err) & " CHECK(S) FAILED ==="
                   severity note;
         end if;
         halt <= true;
         wait;
      end process;

   end architecture sim;

7b. Execution

DesignSystemVerilogVerilog-2001VHDLFinish
i2c_slave_rxPASS 13/13PASS 13/13PASS 13/1358130 ns, all three

8. Mutation Testing

Ten defects.

#Injected defectExpected detectionResult
M1LSB firstT4KILLED (9)
M2seven bits per byteT7KILLED (15)
M3nine bits — the acknowledge slot shifted in as dataT7KILLED (22)
M4the delivered byte loses its last bitT3, T4KILLED (12)
M5rx_valid becomes a levelT3KILLED (14)
M6the enable gate dropped — the address is received as dataT2KILLED (7)
M7sampled on any cycle, not at the rising edgeT3–T6KILLED (29)
M8framing does not discard a partial byteT8KILLED (4)
M9the bit index not cleared on completionT6KILLED (10)
M10byte_done never emittedT13 newKILLED (3)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
baseline: PASS   (verified before injecting anything)
valid mutants: 10   killed: 10   survived: 0   equivalent: 0   invalid: 0
restored: PASS

M10 survived, and it is the fourth instance of one pattern

Silencing byte_done left every byte correct, every delivery counted, every order right — and the suite passed. rx_valid was checked exhaustively; byte_done was not checked at all.

T13 adds three checks on it: one pulse per byte, never two cycles wide, and always simultaneous with rx_valid. The last is the one that matters most — the acknowledge decision is taken from the byte, so a byte_done that did not coincide with its rx_valid would arm a slot for data the consumer has not been handed.

This is the same shape as Module 17.6 §7's bit_done, and the fourth survivor in this module to be an unexamined output or the untested half of a property:

ChapterSurvivorWhat was unexamined
18.2M4, M5line independence in one direction; the level, not the pulse
18.3M10the START branch of the mid-byte report
18.5M8framing inputs tied to zero
18.6M10byte_done, with no consumer instantiated

9. Verification Connection — The Predictor's Ordering Problem

Azvya Education Pvt. Ltd.VLSI Mentor
rx_scoreboard.sv — why byte ORDER is the property, not byte VALUE
   // A receive-path scoreboard is where a target environment first has to care about
   // ORDER rather than content, and the distinction is sharper than it looks.
   //
   //   a monitor sees   : bytes 0x11, 0x22, 0x33 on the wire, in that order
   //   the DUT delivers : 0x11, 0x22, 0x33 -- but delivered HOW MANY TIMES, and WHEN?
   //
   // A scoreboard written as an unordered multiset comparison passes a DUT that delivers
   // 0x11 twice and 0x22 never, if the payload happens to contain a repeat. A scoreboard
   // written as a QUEUE does not -- which is why the bench here logs into an indexed array
   // and compares position by position rather than checking membership.
   //
   // THE HARDER CASE IS A PARTIAL BYTE. When framing abandons a byte mid-flight the
   // predictor must NOT expect a delivery -- and it must also not expect the partial bits
   // to appear prepended to the next byte. Both are wrong in different directions:
   //
   //     wrong 1: predict a byte    -> the DUT correctly delivered nothing, FAIL
   //     wrong 2: predict the next byte as (partial | next bits) -> models the BUG
   //
   // So the predictor needs the framing events, not only the data bits, which means the
   // pin-level monitor has to emit framing as first-class items rather than only bytes.
   // A byte-only monitor cannot describe an aborted transfer at all.
   //
   // AND THE ENABLE IS ENVIRONMENT KNOWLEDGE. Whether a given ninth pulse is an
   // acknowledge or a data bit is not on the wire -- it depends on byte position, which
   // the monitor tracks because it followed the frame from the START. That is the same
   // fact Chapter 17.7 §8 established for the master's byte monitor, and it is why a
   // monitor is a state machine rather than a sampler.
   //
   // COVERAGE:
   //
   //   cover: a single-byte write
   //   cover: a multi-byte write (>= 4 bytes)      catches index-clear bugs (M9)
   //   cover: 0x00 and 0xFF                        the extremes
   //   cover: 0x80 and 0x01                        the order-distinguishing pair
   //   cover: a byte abandoned by a STOP           T8
   //   cover: a byte abandoned by a repeated START T9
   //   illegal_bin: two deliveries in consecutive cycles

10. FPGA and ASIC Implications

On an FPGA this is nine flops and a comparator — nothing. The interesting property is the interface: rx_valid is one cycle, so the consumer must register it. That is a deliberate cost shifted downstream, and Chapter 18.9 is where it is paid: the register file latches on the pulse.

The alternative — a level plus a consumer handshake — would put a bus-paced producer and a logic-paced consumer in the same flow-control loop, and the bus does not wait. A target that needs the consumer to be slow must stretch instead (18.10), which is a protocol mechanism rather than a datapath one.

On an ASIC, the byte-per-SCL-period rate makes this the slowest datapath in the design: at 400 kHz a byte arrives every ~22 µs, so there is no throughput consideration at any plausible system clock. What does matter is that the pulse is one cycle at the system clock, not one SCL period — so a consumer clocked from a different domain cannot sample it reliably and needs either the same clock or its own handshake. This module keeps everything in one domain deliberately; Module 19.6 owns the case where that is not true.

Reset clears the shift register and both counters, and leaves nothing pending. There is no bus-facing output here at all, which is why this block cannot damage a bus even when it is wrong — Chapter 18.1 §5's observation about where the risk lives.

11. Debugging — The Register Writes That Landed One Bit Out

Symptom

A target with a register file is written four bytes at a time. The first register of every burst receives the correct value. The second, third and fourth receive values that are recognisably related to the intended ones but wrong -- each looking like the intended byte shifted left, with a bit borrowed from somewhere. Single-byte writes are always correct. Reads are always correct.

Root Cause

The receive path was enabled through the acknowledge pulse, so it shifted the acknowledge bit in as though it were data. A byte is nine clock pulses and only eight of them are data; an ungated receiver therefore consumes nine bits per byte and every byte after the first is displaced by one additional bit. Nothing was wrong with the register file, the pointer, the acknowledge generator or the master. The first byte was correct because nothing precedes it, which is exactly the pattern that makes the symptom look like a consumer bug rather than a receiver one.

Fix
Drop rx_enable for the ninth pulse -- the acknowledge slot is not a data bit, and the transaction layer of Chapter 18.10 owns that gating. Note that the same defect can live in the BENCH: a testbench whose acknowledge-slot task leaves the enable asserted reproduces the displacement in its own stimulus and then reports it as a design failure, which is how this boundary was first encountered here. The regression that pins it is T7, which drives eight bits, checks the delivery, then drives the ninth pulse and asserts that nothing further was delivered.

Three generalisations.

A displacement that grows by one per byte names its own cause. One extra bit per byte boundary, and there is exactly one extra pulse per byte. Recognising the arithmetic goes straight to the gate.

The first byte being correct is what misdirects. Any defect that accumulates at byte boundaries leaves the first instance clean, and a clean first byte argues strongly that the receiver works.

The same bug can live in the bench. A bench that leaves the enable asserted through its own acknowledge-slot task produces the identical displacement — and then blames the design. Stimulus that models the gating is part of the contract, not a convenience.

12. Common Misconceptions

"Receiving a byte is the same job as receiving an address." Completion produces data a consumer must take, not a decision. The hand-off is the part that fails. §1.

"If the byte value is right, the receive path is right." A byte delivered twice has the right value both times. Value tests cannot see duplication or loss. §1.

"A byte_available flag is friendlier than a pulse." It needs a consumer acknowledgement to clear, which puts a bus-paced producer in a flow-control loop with a logic-paced consumer. The bus does not wait. §2.

"A byte is eight clock pulses." It is nine. The ninth is the acknowledge, and a receiver enabled through it shifts it in as data. §4.

"An ungated receiver corrupts everything." It gets the first byte right and displaces each later one by one more bit — which is why the symptom points at the consumer. §4, §11.

"A partial byte can just stay in the register." Then the next transfer's first bits complete it, producing a byte assembled from two transactions. §5.

"Counting abandoned partials is telemetry." A target that silently discards partials and one that never receives any look identical from outside. §5.

"An output with no consumer in the bench is still tested." It is not tested at all. byte_done was correct in every test and the suite could not see it being silenced. §8.

"A multiset comparison is a fine scoreboard." It passes a DUT that delivers one byte twice and another never, if the payload repeats. Compare as a queue. §9.

13. Reason It Through

Why is the hand-off harder to verify than the value?

Because a duplicated delivery has the correct value, and a test that compares bytes cannot distinguish one delivery from two. Only counting and position can. §1.

Why a one-cycle pulse rather than a level with a handshake?

A handshake would make the consumer's pace part of the bus's flow control, and the bus never waits. A target that genuinely needs time must stretch, which is a protocol mechanism. §2.

Nine pulses per byte, eight of them data. What happens to the ninth in an ungated receiver, and what is the visible pattern?

It is shifted in as a data bit, so each byte after the first is displaced by one additional bit — a displacement that grows by one per byte, with the first byte correct. §4, §11.

Why must a partial byte be discarded rather than left in the register?

Because the next transfer's first bits would complete it, delivering a byte that never existed on the wire and was assembled from two different transactions. §5.

Mutation M10 silenced byte_done and everything passed. Why, and what is the general rule?

Because this bench instantiates no consumer for it, so nothing observed it. An output with no consumer in the bench is an output nobody is checking, and it needs an explicit observer. §8.

Why must byte_done be simultaneous with rx_valid rather than merely near it?

Because 18.5 latches its acknowledge decision when the slot arms, and the decision is derived from the byte. A byte_done out of step would arm a slot for data the consumer has not been handed. §8.

Why can a byte-only monitor not describe an aborted transfer?

Because there is no byte to report — the abandonment is a framing event, and a predictor that sees only bytes cannot know to expect nothing. §9.

14. Understanding Check

15. Summary

The subject is the hand-off, not the value. A byte delivered twice is as bad as one dropped, and both are invisible to a test that compares bytes.

So the contract is narrow: rx_valid is one cycle, exactly once per byte, with rx_byte stable while it is high.

A pulse rather than a flag, because a level with a consumer handshake would put a bus-paced producer in a flow-control loop with a logic-paced consumer — and the bus never waits. A target that needs time must stretch.

Sampled at the rising edge and enabled from above. Without the gate, the address byte is received as data and every later byte is displaced.

The acknowledge pulse is not a data bit. A byte is nine pulses and eight are data; a receiver enabled through the ninth consumes it and each byte after the first is displaced by one more bit — with the first byte always correct, which is what misdirects the diagnosis.

Framing discards a partial byte, or the next transfer's bits complete it and deliver a byte assembled from two transactions.

Ten mutants, ten killed — after M10 silenced byte_done and the entire suite passed.

An output with no consumer in the bench is an output nobody is checking. byte_done now has its own observer: one pulse per byte, never two cycles wide, and always simultaneous with rx_valid.

That is the fourth survivor in this module of one family — an unexamined output or the untested half of a property. Line independence in one direction, the level rather than the pulse, the START branch of a report, tied-off framing inputs, and now an output with nowhere to go.

16. What Comes Next

The target can take bytes in. Chapter 18.7 makes it give them out, and that is the harder direction.

On a write the target receives and has a whole bit slot to think. On a read the master begins clocking immediately after the acknowledge, and the target must already have the first bit on SDA before the first rising edge — because §3.1.2 forbids changing SDA once SCL is high. The byte has to be fetched before it is needed, which is a structural consequence of not owning the clock.

It is also where a transmitted one stops being an abstraction: sending 0xFF means driving nothing at all.

Continue learning