Skip to content
VLSI Mentor

I²C · Module 18

Deriving the Slave FSM, Reset Behavior and Error Recovery

The whole target as one module. Owns the acknowledge policy, the SDA arbitration, what reset means for a device on a bus it does not own, and the one failure a target can inflict on every other device — plus the timeout that ends it.

Nine modules exist, each verified in three languages. What does not exist is one thing you can instantiate — and wire the blocks together is not a chapter.

1. What a Top Level Actually Adds

Four things, and none of them live in any block below:

why it can only be decided here
the acknowledge policy18.5 drives whatever it is told; what deserves an acknowledgement depends on the register file and the byte's role
the SDA arbitrationtwo blocks can pull SDA down, and only the level that instantiates both can say they must never do it together
reset on a shared busa block can release its own drive; only the top can guarantee that every drive is released
error recoverythe failure a target can cause is visible only as a relationship between the framing state and the clock

Everything else in this file is wiring. So this chapter is about those four, and about the two defects the end-to-end testbench found in them — both of which every block below was innocent of.

A block diagram of the assembled slave. The sampling front end from chapter 18.2 feeds framing, address, receive, transmit and master-acknowledge blocks. A transaction layer produces enables for them. Three top-level decisions are highlighted: an acknowledge policy that combines the address match, the pointer flag and the register file's permission; an SDA arbitration that ORs two pull-downs and counts any overlap; and a give-up timeout that watches for an open transfer with SCL held high and synthesises a STOP. The register file sits apart with no framing input.18.2 samplingtwo lines to events18.3 framingSTART / STOP18.10 transactionphase, index, stretch18.9 registersno framing input18.4 addressmatch + direction18.6 / 18.7receive and transmitAcknowledge policyNEW — three casesSDA arbitrationNEW — counts overlapGive-up timeoutNEW — synthesises a STOPscl / sda drivepull down or release12
Figure 1 — the assembled target. Only the three shaded decisions are new; everything else is a connection between blocks already built and verified.

2. The Acknowledge Policy Has Three Cases

This is the defect the bench found on its first run, and it is severe.

The obvious policy has two cases — the address byte is acknowledged because it matched, and a data byte is acknowledged if 18.9 will take it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ack_en = (addr_done && match) ? 1 : wr_accept;      // WRONG

A read-only bit restricts writes to a register. It must not restrict the act of choosing a register, and the two are only distinguishable here — 18.9 sees a byte and a flag, 18.5 sees a permission, and neither can tell a pointer byte from a data byte on its own.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
the address byte     ->  acknowledge: it matched
the POINTER byte     ->  ALWAYS acknowledge: it writes no register
a written data byte  ->  acknowledge only if 18.9 will take it

Mutation M1 restores the two-case version and fails eight checks. Mutation M3 drops the address case as well, and fails eight — a slave that refuses its own address once the pointer lands badly.

3. The SDA Arbitration, and Why an OR Gate Hides a Bug

Two blocks can pull SDA down: 18.5's acknowledge and 18.7's transmitter. 18.7 §12 said this file owns keeping them apart.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
assign sda_drive_low = ack_sda_low || tx_sda_low;

4. What Reset Means for a Device on a Bus It Does Not Own

Every drive released. Not "the state machine returns to idle" — that is a block-level concern, and every block below already does it.

5. The One Failure a Target Can Inflict on Everybody

A slave cannot corrupt another device's transfer and cannot generate clocks. There is exactly one thing it can do to the whole bus, and it does it by being interrupted at the wrong moment.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
the master is reset, or crashes, mid-byte
   ... at a moment when the slave is holding SDA low for a zero bit
   ... so SDA stays low for ever
   ... and a START needs a FALLING SDA, which needs SDA to be high first
   ... so no transfer can ever begin again

The signature of a master that is gone

A transfer is open (18.3 says bus_active) and SCL has been continuously high for IDLE_CYCLES clocks. A master that is still there does not leave SCL high in the middle of a transfer — it is either clocking or it has finished with a STOP.

A flowchart. From a start, a decision asks whether a transfer is open. If not, the counter is cleared and the flow returns. If it is open, a second decision asks whether SCL is high. If not, the counter is cleared and the flow returns. If it is, the counter increments and a third decision asks whether the limit is reached. If not, the flow returns. If it is, a step synthesises a STOP pulse, which every block abandons through, and the counter is cleared.yesnoyesnoyesnoEvery clockTransferopen?SCL high?Count upLimitreached?Synthesise a STOPClear the counterWait
Figure 2 — the give-up timeout. Any falling edge resets the counter, so a slow master is never mistaken for a dead one.

The recovery synthesises the STOP the master failed to send

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
wire stop_eff = stop_pulse || abort;

6. The Target, in Three Languages

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_slave.sv — the assembled target
   // -----------------------------------------------------------------------------
   // i2c_slave.sv
   // The whole target, as one module you can instantiate.
   //
   // WHAT IS ACTUALLY NEW HERE, because "wire the blocks together" is not a chapter. Nine
   // modules already exist and are each verified in three languages. This file adds the four
   // things that only exist at the top:
   //
   //   1. THE SDA ARBITRATION. Two blocks can pull SDA down -- 18.5's acknowledge and 18.7's
   //      transmitter -- and they must never do it at the same time. Chapter 18.7 §12 said
   //      this file owns that, and owning it means more than an OR gate: it means a counter
   //      that proves the overlap never happens.
   //
   //   2. THE ACKNOWLEDGE POLICY. 18.5 drives whatever `ack_en` says and has no opinion about
   //      what should be acknowledged. Deciding that is a top-level decision: the address is
   //      acknowledged when it matched; a written byte is acknowledged only if the register
   //      file will take it. That is where 18.9's `wr_accept` becomes an ACK or a NACK on the
   //      wire, and it is the only place in the design where a refusal becomes visible.
   //
   //   3. WHAT RESET MEANS FOR A DEVICE ON A BUS IT DOES NOT OWN. Every drive is released.
   //      A target that pulls either line down while held in reset takes the whole bus with
   //      it, and no other device can lift it.
   //
   //   4. ERROR RECOVERY FROM THE ONE FAILURE A TARGET CAN CAUSE. If the master abandons a
   //      transfer without a STOP -- it was reset, or it crashed -- and the slave was holding
   //      SDA low at that moment, the slave holds SDA low FOR EVER. No START is then possible,
   //      because a START needs a falling SDA, and every device on the bus is locked out by a
   //      device that is itself working perfectly.
   //
   //      §3.1.16 is the master-side recovery for exactly this, and Chapter 17.11 implemented
   //      it. This is the slave-side half: a timeout that gives up.
   //
   // HOW THE TIMEOUT WORKS, and why it is implemented the way it is. A transfer is open (18.3
   // says `bus_active`) and SCL has been continuously HIGH for IDLE_CYCLES clocks. A master
   // that is still there does not leave SCL high in the middle of a transfer -- it is either
   // clocking or it has finished with a STOP. So this is the signature of a master that is
   // gone.
   //
   // The recovery then SYNTHESISES THE STOP THE MASTER FAILED TO SEND: `abort` is ORed into
   // every block's `stop_pulse`, so every block abandons through the path it already has and
   // no block needs new logic. Releasing the lines is then automatic, because releasing on a
   // STOP is what they all already do.
   // -----------------------------------------------------------------------------

   module i2c_slave #(
      parameter [6:0] MY_ADDR     = 7'h50,
      parameter int   N_REG       = 8,
      parameter int   RO_MASK     = 0,
      // Clocks of continuous SCL-high with a transfer open before the slave gives up. Must be
      // longer than the slowest legal HIGH time at the slowest clock, or the slave will abandon
      // live transfers -- which is a far worse failure than the one it is trying to prevent.
      parameter int   IDLE_CYCLES = 512,
      parameter int   SYNC_DEPTH  = 2,
      parameter int   CNT_W       = 16
   ) (
      input  logic clk,
      input  logic rst_n,

      // ---- the bus ------------------------------------------------------------
      // Read back, never driven high. Chapter 19 turns these into a pad.
      input  logic scl_pin,
      input  logic sda_pin,
      output logic scl_drive_low,
      output logic sda_drive_low,

      // ---- the application ----------------------------------------------------
      input  logic stall_req,                    // "I need more time"
      output logic [8*N_REG-1:0] reg_flat,       // register i is reg_flat[8*i +: 8]
      output logic [7:0] pointer,

      // ---- diagnostics --------------------------------------------------------
      output logic selected,
      output logic stretching,
      output logic [CNT_W-1:0] n_phases,
      output logic [CNT_W-1:0] n_restarts,
      output logic [CNT_W-1:0] n_writes,
      output logic [CNT_W-1:0] n_refused,
      output logic [CNT_W-1:0] n_reads,
      output logic [CNT_W-1:0] n_aborts,
      // MUST STAY ZERO. Cycles in which both SDA drivers were asserted. It is an output rather
      // than an assertion so that a synthesised design can report it too -- a formal property
      // proves it cannot happen, and this proves it did not.
      output logic [CNT_W-1:0] n_sda_conflict
   );

      // ---- 18.2: the sampling front end ---------------------------------------
      logic scl_q, sda_q, scl_rise, scl_fall, sda_rise, sda_fall;

      i2c_slave_sync #(.SYNC_DEPTH(SYNC_DEPTH)) 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)
      );

      // ---- framing signals, declared here because the abort below uses them -----
      logic start_pulse, restart_pulse, stop_pulse, framing_midbyte, bus_active;
      logic [CNT_W-1:0] n_starts, n_stops, n_restarts_frm;
      logic acquiring, receiving;
      wire  mid_byte = acquiring || receiving;

      // ---- the abort, and the effective STOP every block sees ------------------
      logic abort;
      wire  stop_eff = stop_pulse || abort;

      // A PLAIN INTEGER, matching the VHDL version's `integer range 0 to IDLE_CYCLES`. An
      // earlier draft sized this with a width-computing system function and cleared it with a
      // bit-width query -- and the second of those is SystemVerilog only, so the file labelled
      // Verilog-2001 was not Verilog-2001. Synthesis prunes an integer to the bits its range
      // needs; a tool that does not should be given an explicit width, rather than the design
      // reaching for a system function only one of the three languages has.
      integer idle_cnt;

      always @(posedge clk or negedge rst_n) begin
         if (!rst_n) begin
            idle_cnt <= 0;
            abort    <= 1'b0;
            n_aborts <= {CNT_W{1'b0}};
         end else begin
            abort <= 1'b0;
            // The counter runs only while a transfer is open AND SCL is high. Any fall resets
            // it, so a clocking master never reaches the limit however slowly it clocks.
            if (!bus_active || !scl_q) begin
               idle_cnt <= 0;
            end else if (idle_cnt == IDLE_CYCLES) begin
               // One cycle, and then the counter is cleared by the abandon it causes.
               abort    <= 1'b1;
               idle_cnt <= 0;
               n_aborts <= n_aborts + 1'b1;
            end else begin
               idle_cnt <= idle_cnt + 1'b1;
            end
         end
      end

      // ---- 18.3: framing ------------------------------------------------------
      i2c_slave_framing #(.CNT_W(CNT_W)) 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(mid_byte), .framing_midbyte(framing_midbyte),
         .n_starts(n_starts), .n_restarts(n_restarts_frm), .n_stops(n_stops)
      );

      // ---- 18.4: the address ---------------------------------------------------
      logic addr_done, match, dir_read;
      logic [3:0] a_bit_index;
      logic [7:0] addr_byte;
      logic [CNT_W-1:0] n_match, n_miss;

      i2c_slave_addr #(.MY_ADDR(MY_ADDR), .CNT_W(CNT_W)) u_addr (
         .clk(clk), .rst_n(rst_n), .scl_rise(scl_rise), .sda_q(sda_q),
         .start_pulse(start_pulse), .stop_pulse(stop_eff),
         .acquiring(acquiring), .bit_index(a_bit_index), .addr_done(addr_done),
         .addr_byte(addr_byte), .match(match), .selected(selected),
         .dir_read(dir_read), .n_match(n_match), .n_miss(n_miss)
      );

      // ---- 18.10: the transaction layer ---------------------------------------
      logic in_phase, dir_q, rx_is_pointer, rx_enable, tx_start, tx_continue;
      logic [3:0] data_index;
      logic ack_active, ack_armed;
      logic rx_valid, rx_byte_done, byte_sent, mack_valid, mack_ack;
      logic [CNT_W-1:0] n_stretch;

      // 18.7's nets, declared here rather than beside the instance. Every internal net of this
      // module is declared before the first instance, so nothing depends on instance order --
      // and a monitor can be checked by an injection that references any of them.
      logic tx_req, tx_sda_low, driving;
      logic [3:0] t_bit_index;
      logic [7:0] rd_data;
      logic [CNT_W-1:0] n_bytes_tx, n_bits_tx;

      i2c_slave_txn #(.CNT_W(CNT_W)) u_txn (
         .clk(clk), .rst_n(rst_n), .scl_q(scl_q), .scl_fall(scl_fall),
         .start_pulse(start_pulse), .restart_pulse(restart_pulse), .stop_pulse(stop_eff),
         .addr_done(addr_done), .match(match), .dir_read(dir_read),
         .ack_active(ack_active), .rx_valid(rx_valid), .byte_sent(byte_sent),
         .mack_valid(mack_valid), .mack_ack(mack_ack), .stall_req(stall_req),
         .in_phase(in_phase), .dir_q(dir_q), .rx_is_pointer(rx_is_pointer),
         .rx_enable(rx_enable), .tx_start(tx_start), .tx_continue(tx_continue),
         .data_index(data_index), .scl_drive_low(scl_drive_low), .stretching(stretching),
         // THE REPORTED RESTART COUNT COMES FROM THE TRANSACTION LAYER, not from 18.3, and the
         // reason is not that the numbers differ -- they are equal by construction, since both
         // count the same pulse. It is that a count taken from 18.3 leaves this connection with
         // no observable effect at all: a mutation tying `restart_pulse` to zero here survived a
         // full pass, because the only thing it fed was an output nobody read.
         //
         // Wiring the diagnostic to the layer whose STATE a repeated START destroys makes the
         // connection load-bearing, and the mutant then dies on the restart count.
         .n_phases(n_phases), .n_restarts(n_restarts), .n_stretch(n_stretch)
      );

      // ---- the acknowledge policy, which is a TOP-LEVEL decision --------------
      // 18.5 drives whatever it is told and has no opinion about what deserves an
      // acknowledgement. Deciding that is this file's job, and there are THREE cases, not two:
      //
      //   the address byte    -> acknowledge, because it matched (if it had not, 18.4 would not
      //                          have raised `match` and this slot would not be armed at all)
      //   the POINTER byte    -> ALWAYS acknowledge. It writes no register, so no register's
      //                          access rules apply to it.
      //   a written data byte -> acknowledge only if 18.9 will take it
      //
      // THE MIDDLE CASE IS THE ONE THIS DESIGN GOT WRONG FIRST, and the bug was severe. With
      // `ack_en` written as a two-way choice, the pointer byte was answered with `wr_accept` --
      // which reflects whatever the pointer happened to be ALREADY pointing at. So if the
      // pointer was resting on a read-only register, the slave NACKed the very byte that would
      // have moved it somewhere writable, and the device became permanently unreachable: every
      // attempt to repoint was refused because of where the pointer already was.
      //
      // A read-only bit restricts writes TO A REGISTER. It must not restrict the act of
      // choosing a register, and the two are only distinguishable at this level -- 18.9 sees a
      // byte and a flag, and 18.5 sees a permission.
      logic wr_accept;
      wire  byte_done_any = rx_byte_done || (addr_done && match);
      wire  ack_en        = (addr_done && match) ? 1'b1 :
                            rx_is_pointer        ? 1'b1 : wr_accept;

      logic ack_sda_low;
      logic [CNT_W-1:0] n_acks, n_nacks;

      i2c_slave_ack #(.CNT_W(CNT_W)) u_ack (
         .clk(clk), .rst_n(rst_n), .scl_fall(scl_fall), .byte_done(byte_done_any),
         .ack_en(ack_en), .start_pulse(start_pulse), .stop_pulse(stop_eff),
         .sda_drive_low(ack_sda_low), .ack_active(ack_active), .ack_armed(ack_armed),
         .n_acks(n_acks), .n_nacks(n_nacks)
      );

      // ---- 18.6: receive -------------------------------------------------------
      logic [3:0] r_bit_index;
      logic [7:0] rx_byte;
      logic [CNT_W-1:0] n_bytes_rx, n_partial;

      i2c_slave_rx #(.CNT_W(CNT_W)) u_rx (
         .clk(clk), .rst_n(rst_n), .scl_rise(scl_rise), .sda_q(sda_q),
         .start_pulse(start_pulse), .stop_pulse(stop_eff), .rx_enable(rx_enable),
         .receiving(receiving), .bit_index(r_bit_index), .rx_byte(rx_byte),
         .rx_valid(rx_valid), .byte_done(rx_byte_done),
         .n_bytes(n_bytes_rx), .n_partial(n_partial)
      );

      // ---- 18.7: transmit ------------------------------------------------------
      i2c_slave_tx #(.CNT_W(CNT_W)) u_tx (
         .clk(clk), .rst_n(rst_n), .scl_fall(scl_fall),
         .start_pulse(start_pulse), .stop_pulse(stop_eff),
         .tx_start(tx_start), .tx_continue(tx_continue),
         .tx_req(tx_req), .tx_byte(rd_data), .sda_drive_low(tx_sda_low),
         .driving(driving), .bit_index(t_bit_index), .byte_sent(byte_sent),
         .n_bytes(n_bytes_tx), .n_bits(n_bits_tx)
      );

      // ---- 18.8: the master's answer on a read --------------------------------
      logic awaiting, keep_sourcing;
      logic [CNT_W-1:0] n_m_ack, n_m_nack;

      i2c_slave_mack #(.CNT_W(CNT_W)) u_mack (
         .clk(clk), .rst_n(rst_n), .scl_rise(scl_rise), .sda_q(sda_q),
         .start_pulse(start_pulse), .stop_pulse(stop_eff), .byte_sent(byte_sent),
         .awaiting(awaiting), .mack_valid(mack_valid), .mack_ack(mack_ack),
         .keep_sourcing(keep_sourcing), .n_ack(n_m_ack), .n_nack(n_m_nack)
      );

      // ---- 18.9: the register file, which sees no framing at all --------------
      // NOTE the absent connections: this instance has no START or STOP input, not even the
      // synthesised abort. The pointer survives everything, which is Chapter 18.9 §2.
      i2c_slave_regs #(.N_REG(N_REG), .RO_MASK(RO_MASK), .CNT_W(CNT_W)) u_regs (
         .clk(clk), .rst_n(rst_n),
         .rx_valid(rx_valid), .rx_byte(rx_byte), .rx_is_pointer(rx_is_pointer),
         .wr_accept(wr_accept), .rd_data(rd_data), .rd_taken(tx_req),
         .reg_flat(reg_flat), .pointer(pointer),
         .n_writes(n_writes), .n_refused(n_refused), .n_reads(n_reads)
      );

      // ---- the SDA arbitration -------------------------------------------------
      // Two drivers, one line, and a wired-AND means an OR of the pull-downs is electrically
      // correct however wrong the control logic is -- which is exactly why the overlap has to
      // be counted rather than assumed. An OR gate hides a direction bug perfectly: the line
      // goes low, the master reads a zero, and nothing anywhere reports a problem.
      assign sda_drive_low = ack_sda_low || tx_sda_low;

      always @(posedge clk or negedge rst_n) begin
         if (!rst_n)                        n_sda_conflict <= {CNT_W{1'b0}};
         else if (ack_sda_low && tx_sda_low) n_sda_conflict <= n_sda_conflict + 1'b1;
      end

   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_slave.v — the same design in Verilog-2001
   // -----------------------------------------------------------------------------
   // i2c_slave.v
   // The whole target, as one module you can instantiate.
   //
   // WHAT IS ACTUALLY NEW HERE, because "wire the blocks together" is not a chapter. Nine
   // modules already exist and are each verified in three languages. This file adds the four
   // things that only exist at the top:
   //
   //   1. THE SDA ARBITRATION. Two blocks can pull SDA down -- 18.5's acknowledge and 18.7's
   //      transmitter -- and they must never do it at the same time. Chapter 18.7 §12 said
   //      this file owns that, and owning it means more than an OR gate: it means a counter
   //      that proves the overlap never happens.
   //
   //   2. THE ACKNOWLEDGE POLICY. 18.5 drives whatever `ack_en` says and has no opinion about
   //      what should be acknowledged. Deciding that is a top-level decision: the address is
   //      acknowledged when it matched; a written byte is acknowledged only if the register
   //      file will take it. That is where 18.9's `wr_accept` becomes an ACK or a NACK on the
   //      wire, and it is the only place in the design where a refusal becomes visible.
   //
   //   3. WHAT RESET MEANS FOR A DEVICE ON A BUS IT DOES NOT OWN. Every drive is released.
   //      A target that pulls either line down while held in reset takes the whole bus with
   //      it, and no other device can lift it.
   //
   //   4. ERROR RECOVERY FROM THE ONE FAILURE A TARGET CAN CAUSE. If the master abandons a
   //      transfer without a STOP -- it was reset, or it crashed -- and the slave was holding
   //      SDA low at that moment, the slave holds SDA low FOR EVER. No START is then possible,
   //      because a START needs a falling SDA, and every device on the bus is locked out by a
   //      device that is itself working perfectly.
   //
   //      §3.1.16 is the master-side recovery for exactly this, and Chapter 17.11 implemented
   //      it. This is the slave-side half: a timeout that gives up.
   //
   // HOW THE TIMEOUT WORKS, and why it is implemented the way it is. A transfer is open (18.3
   // says `bus_active`) and SCL has been continuously HIGH for IDLE_CYCLES clocks. A master
   // that is still there does not leave SCL high in the middle of a transfer -- it is either
   // clocking or it has finished with a STOP. So this is the signature of a master that is
   // gone.
   //
   // The recovery then SYNTHESISES THE STOP THE MASTER FAILED TO SEND: `abort` is ORed into
   // every block's `stop_pulse`, so every block abandons through the path it already has and
   // no block needs new logic. Releasing the lines is then automatic, because releasing on a
   // STOP is what they all already do.
   // (Verilog-2001 -- structurally identical to the SystemVerilog above.)
   // -----------------------------------------------------------------------------

   module i2c_slave #(
      parameter [6:0] MY_ADDR     = 7'h50,
      parameter integer   N_REG       = 8,
      parameter integer   RO_MASK     = 0,
      // Clocks of continuous SCL-high with a transfer open before the slave gives up. Must be
      // longer than the slowest legal HIGH time at the slowest clock, or the slave will abandon
      // live transfers -- which is a far worse failure than the one it is trying to prevent.
      parameter integer   IDLE_CYCLES = 512,
      parameter integer   SYNC_DEPTH  = 2,
      parameter integer   CNT_W       = 16
   ) (
      input  wire  clk,
      input  wire  rst_n,

      // ---- the bus ------------------------------------------------------------
      // Read back, never driven high. Chapter 19 turns these into a pad.
      input  wire  scl_pin,
      input  wire  sda_pin,
      output wire scl_drive_low,
      output wire sda_drive_low,

      // ---- the application ----------------------------------------------------
      input  wire  stall_req,                    // "I need more time"
      output wire [8*N_REG-1:0] reg_flat,       // register i is reg_flat[8*i +: 8]
      output wire [7:0] pointer,

      // ---- diagnostics --------------------------------------------------------
      output wire selected,
      output wire stretching,
      output wire [CNT_W-1:0] n_phases,
      output wire [CNT_W-1:0] n_restarts,
      output wire [CNT_W-1:0] n_writes,
      output wire [CNT_W-1:0] n_refused,
      output wire [CNT_W-1:0] n_reads,
      output reg   [CNT_W-1:0] n_aborts,
      // MUST STAY ZERO. Cycles in which both SDA drivers were asserted. It is an output rather
      // than an assertion so that a synthesised design can report it too -- a formal property
      // proves it cannot happen, and this proves it did not.
      output reg   [CNT_W-1:0] n_sda_conflict
   );

      // ---- 18.2: the sampling front end ---------------------------------------
      wire scl_q, sda_q, scl_rise, scl_fall, sda_rise, sda_fall;

      i2c_slave_sync #(.SYNC_DEPTH(SYNC_DEPTH)) 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)
      );

      // ---- framing signals, declared here because the abort below uses them -----
      wire start_pulse, restart_pulse, stop_pulse, framing_midbyte, bus_active;
      wire [CNT_W-1:0] n_starts, n_stops, n_restarts_frm;
      wire acquiring, receiving;
      wire  mid_byte = acquiring || receiving;

      // ---- the abort, and the effective STOP every block sees ------------------
      reg abort;
      wire  stop_eff = stop_pulse || abort;

      // A PLAIN INTEGER, matching the VHDL version's `integer range 0 to IDLE_CYCLES`. An
      // earlier draft sized this with a width-computing system function and cleared it with a
      // bit-width query -- and the second of those is SystemVerilog only, so the file labelled
      // Verilog-2001 was not Verilog-2001. Synthesis prunes an integer to the bits its range
      // needs; a tool that does not should be given an explicit width, rather than the design
      // reaching for a system function only one of the three languages has.
      integer idle_cnt;

      always @(posedge clk or negedge rst_n) begin
         if (!rst_n) begin
            idle_cnt <= 0;
            abort    <= 1'b0;
            n_aborts <= {CNT_W{1'b0}};
         end else begin
            abort <= 1'b0;
            // The counter runs only while a transfer is open AND SCL is high. Any fall resets
            // it, so a clocking master never reaches the limit however slowly it clocks.
            if (!bus_active || !scl_q) begin
               idle_cnt <= 0;
            end else if (idle_cnt == IDLE_CYCLES) begin
               // One cycle, and then the counter is cleared by the abandon it causes.
               abort    <= 1'b1;
               idle_cnt <= 0;
               n_aborts <= n_aborts + 1'b1;
            end else begin
               idle_cnt <= idle_cnt + 1'b1;
            end
         end
      end

      // ---- 18.3: framing ------------------------------------------------------
      i2c_slave_framing #(.CNT_W(CNT_W)) 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(mid_byte), .framing_midbyte(framing_midbyte),
         .n_starts(n_starts), .n_restarts(n_restarts_frm), .n_stops(n_stops)
      );

      // ---- 18.4: the address ---------------------------------------------------
      wire addr_done, match, dir_read;
      wire [3:0] a_bit_index;
      wire [7:0] addr_byte;
      wire [CNT_W-1:0] n_match, n_miss;

      i2c_slave_addr #(.MY_ADDR(MY_ADDR), .CNT_W(CNT_W)) u_addr (
         .clk(clk), .rst_n(rst_n), .scl_rise(scl_rise), .sda_q(sda_q),
         .start_pulse(start_pulse), .stop_pulse(stop_eff),
         .acquiring(acquiring), .bit_index(a_bit_index), .addr_done(addr_done),
         .addr_byte(addr_byte), .match(match), .selected(selected),
         .dir_read(dir_read), .n_match(n_match), .n_miss(n_miss)
      );

      // ---- 18.10: the transaction layer ---------------------------------------
      wire in_phase, dir_q, rx_is_pointer, rx_enable, tx_start, tx_continue;
      wire [3:0] data_index;
      wire ack_active, ack_armed;
      wire rx_valid, rx_byte_done, byte_sent, mack_valid, mack_ack;
      wire [CNT_W-1:0] n_stretch;

      // 18.7's nets, declared here rather than beside the instance. Every internal net of this
      // module is declared before the first instance, so nothing depends on instance order --
      // and a monitor can be checked by an injection that references any of them.
      wire tx_req, tx_sda_low, driving;
      wire [3:0] t_bit_index;
      wire [7:0] rd_data;
      wire [CNT_W-1:0] n_bytes_tx, n_bits_tx;

      i2c_slave_txn #(.CNT_W(CNT_W)) u_txn (
         .clk(clk), .rst_n(rst_n), .scl_q(scl_q), .scl_fall(scl_fall),
         .start_pulse(start_pulse), .restart_pulse(restart_pulse), .stop_pulse(stop_eff),
         .addr_done(addr_done), .match(match), .dir_read(dir_read),
         .ack_active(ack_active), .rx_valid(rx_valid), .byte_sent(byte_sent),
         .mack_valid(mack_valid), .mack_ack(mack_ack), .stall_req(stall_req),
         .in_phase(in_phase), .dir_q(dir_q), .rx_is_pointer(rx_is_pointer),
         .rx_enable(rx_enable), .tx_start(tx_start), .tx_continue(tx_continue),
         .data_index(data_index), .scl_drive_low(scl_drive_low), .stretching(stretching),
         // THE REPORTED RESTART COUNT COMES FROM THE TRANSACTION LAYER, not from 18.3, and the
         // reason is not that the numbers differ -- they are equal by construction, since both
         // count the same pulse. It is that a count taken from 18.3 leaves this connection with
         // no observable effect at all: a mutation tying `restart_pulse` to zero here survived a
         // full pass, because the only thing it fed was an output nobody read.
         //
         // Wiring the diagnostic to the layer whose STATE a repeated START destroys makes the
         // connection load-bearing, and the mutant then dies on the restart count.
         .n_phases(n_phases), .n_restarts(n_restarts), .n_stretch(n_stretch)
      );

      // ---- the acknowledge policy, which is a TOP-LEVEL decision --------------
      // 18.5 drives whatever it is told and has no opinion about what deserves an
      // acknowledgement. Deciding that is this file's job, and there are THREE cases, not two:
      //
      //   the address byte    -> acknowledge, because it matched (if it had not, 18.4 would not
      //                          have raised `match` and this slot would not be armed at all)
      //   the POINTER byte    -> ALWAYS acknowledge. It writes no register, so no register's
      //                          access rules apply to it.
      //   a written data byte -> acknowledge only if 18.9 will take it
      //
      // THE MIDDLE CASE IS THE ONE THIS DESIGN GOT WRONG FIRST, and the bug was severe. With
      // `ack_en` written as a two-way choice, the pointer byte was answered with `wr_accept` --
      // which reflects whatever the pointer happened to be ALREADY pointing at. So if the
      // pointer was resting on a read-only register, the slave NACKed the very byte that would
      // have moved it somewhere writable, and the device became permanently unreachable: every
      // attempt to repoint was refused because of where the pointer already was.
      //
      // A read-only bit restricts writes TO A REGISTER. It must not restrict the act of
      // choosing a register, and the two are only distinguishable at this level -- 18.9 sees a
      // byte and a flag, and 18.5 sees a permission.
      wire wr_accept;
      wire  byte_done_any = rx_byte_done || (addr_done && match);
      wire  ack_en        = (addr_done && match) ? 1'b1 :
                            rx_is_pointer        ? 1'b1 : wr_accept;

      wire ack_sda_low;
      wire [CNT_W-1:0] n_acks, n_nacks;

      i2c_slave_ack #(.CNT_W(CNT_W)) u_ack (
         .clk(clk), .rst_n(rst_n), .scl_fall(scl_fall), .byte_done(byte_done_any),
         .ack_en(ack_en), .start_pulse(start_pulse), .stop_pulse(stop_eff),
         .sda_drive_low(ack_sda_low), .ack_active(ack_active), .ack_armed(ack_armed),
         .n_acks(n_acks), .n_nacks(n_nacks)
      );

      // ---- 18.6: receive -------------------------------------------------------
      wire [3:0] r_bit_index;
      wire [7:0] rx_byte;
      wire [CNT_W-1:0] n_bytes_rx, n_partial;

      i2c_slave_rx #(.CNT_W(CNT_W)) u_rx (
         .clk(clk), .rst_n(rst_n), .scl_rise(scl_rise), .sda_q(sda_q),
         .start_pulse(start_pulse), .stop_pulse(stop_eff), .rx_enable(rx_enable),
         .receiving(receiving), .bit_index(r_bit_index), .rx_byte(rx_byte),
         .rx_valid(rx_valid), .byte_done(rx_byte_done),
         .n_bytes(n_bytes_rx), .n_partial(n_partial)
      );

      // ---- 18.7: transmit ------------------------------------------------------
      i2c_slave_tx #(.CNT_W(CNT_W)) u_tx (
         .clk(clk), .rst_n(rst_n), .scl_fall(scl_fall),
         .start_pulse(start_pulse), .stop_pulse(stop_eff),
         .tx_start(tx_start), .tx_continue(tx_continue),
         .tx_req(tx_req), .tx_byte(rd_data), .sda_drive_low(tx_sda_low),
         .driving(driving), .bit_index(t_bit_index), .byte_sent(byte_sent),
         .n_bytes(n_bytes_tx), .n_bits(n_bits_tx)
      );

      // ---- 18.8: the master's answer on a read --------------------------------
      wire awaiting, keep_sourcing;
      wire [CNT_W-1:0] n_m_ack, n_m_nack;

      i2c_slave_mack #(.CNT_W(CNT_W)) u_mack (
         .clk(clk), .rst_n(rst_n), .scl_rise(scl_rise), .sda_q(sda_q),
         .start_pulse(start_pulse), .stop_pulse(stop_eff), .byte_sent(byte_sent),
         .awaiting(awaiting), .mack_valid(mack_valid), .mack_ack(mack_ack),
         .keep_sourcing(keep_sourcing), .n_ack(n_m_ack), .n_nack(n_m_nack)
      );

      // ---- 18.9: the register file, which sees no framing at all --------------
      // NOTE the absent connections: this instance has no START or STOP input, not even the
      // synthesised abort. The pointer survives everything, which is Chapter 18.9 §2.
      i2c_slave_regs #(.N_REG(N_REG), .RO_MASK(RO_MASK), .CNT_W(CNT_W)) u_regs (
         .clk(clk), .rst_n(rst_n),
         .rx_valid(rx_valid), .rx_byte(rx_byte), .rx_is_pointer(rx_is_pointer),
         .wr_accept(wr_accept), .rd_data(rd_data), .rd_taken(tx_req),
         .reg_flat(reg_flat), .pointer(pointer),
         .n_writes(n_writes), .n_refused(n_refused), .n_reads(n_reads)
      );

      // ---- the SDA arbitration -------------------------------------------------
      // Two drivers, one line, and a wired-AND means an OR of the pull-downs is electrically
      // correct however wrong the control logic is -- which is exactly why the overlap has to
      // be counted rather than assumed. An OR gate hides a direction bug perfectly: the line
      // goes low, the master reads a zero, and nothing anywhere reports a problem.
      assign sda_drive_low = ack_sda_low || tx_sda_low;

      always @(posedge clk or negedge rst_n) begin
         if (!rst_n)                        n_sda_conflict <= {CNT_W{1'b0}};
         else if (ack_sda_low && tx_sda_low) n_sda_conflict <= n_sda_conflict + 1'b1;
      end

   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_slave.vhd — the same design in VHDL
   -- -----------------------------------------------------------------------------
   -- i2c_slave.vhd
   -- The whole target, as one entity -- the same design in VHDL.
   --
   -- The four things this level adds, unchanged across the three languages:
   --
   --   1. THE SDA ARBITRATION, with a counter that proves the two drivers never overlap. A
   --      wired-AND makes an OR of the pull-downs electrically correct however wrong the
   --      control logic is, which is exactly why the overlap must be counted rather than
   --      assumed.
   --
   --   2. THE ACKNOWLEDGE POLICY, which has THREE cases: the address byte, the pointer byte,
   --      and a written data byte. Only the third consults the register file's permission --
   --      answering the pointer byte with it makes the device unreachable as soon as the
   --      pointer comes to rest on a read-only address.
   --
   --   3. WHAT RESET MEANS FOR A DEVICE ON A BUS IT DOES NOT OWN: every drive released.
   --
   --   4. ERROR RECOVERY FROM THE ONE FAILURE A TARGET CAN CAUSE. A master that abandons a
   --      transfer without a STOP while the slave holds SDA low leaves SDA low for ever, and
   --      no START is then possible. The recovery is a timeout that SYNTHESISES THE STOP the
   --      master failed to send, so every block abandons through the path it already has.
   -- -----------------------------------------------------------------------------
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   entity i2c_slave is
      generic (
         MY_ADDR     : std_logic_vector(6 downto 0) := "1010000";   -- 0x50
         N_REG       : positive := 8;
         RO_MASK     : natural  := 0;
         -- Clocks of continuous SCL-high with a transfer open before the slave gives up. Must
         -- exceed the slowest legal HIGH time, or the slave abandons live transfers.
         IDLE_CYCLES : positive := 512;
         SYNC_DEPTH  : positive := 2;
         CNT_W       : positive := 16
      );
      port (
         clk            : in  std_logic;
         rst_n          : in  std_logic;

         scl_pin        : in  std_logic;
         sda_pin        : in  std_logic;
         scl_drive_low  : out std_logic;
         sda_drive_low  : out std_logic;

         stall_req      : in  std_logic;
         reg_flat       : out std_logic_vector(8*N_REG-1 downto 0);
         pointer        : out std_logic_vector(7 downto 0);

         selected       : out std_logic;
         stretching     : out std_logic;
         n_phases       : out unsigned(CNT_W-1 downto 0);
         n_restarts     : out unsigned(CNT_W-1 downto 0);
         n_writes       : out unsigned(CNT_W-1 downto 0);
         n_refused      : out unsigned(CNT_W-1 downto 0);
         n_reads        : out unsigned(CNT_W-1 downto 0);
         n_aborts       : out unsigned(CNT_W-1 downto 0);
         -- MUST STAY ZERO: cycles in which both SDA drivers were asserted.
         n_sda_conflict : out unsigned(CNT_W-1 downto 0)
      );
   end entity i2c_slave;

   architecture rtl of i2c_slave is

      -- 18.2
      signal scl_q, sda_q, scl_rise, scl_fall, sda_rise, sda_fall : std_logic;

      -- 18.3
      signal start_pulse, restart_pulse, stop_pulse : std_logic;
      signal bus_active, framing_midbyte : std_logic;
      signal n_starts, n_stops, n_restarts_frm : unsigned(CNT_W-1 downto 0);
      signal acquiring, receiving, mid_byte : std_logic;

      -- the abort and the effective STOP
      signal abort    : std_logic := '0';
      signal stop_eff : std_logic;
      signal idle_cnt : integer range 0 to IDLE_CYCLES := 0;
      signal nab      : unsigned(CNT_W-1 downto 0) := (others => '0');
      signal ncf      : unsigned(CNT_W-1 downto 0) := (others => '0');

      -- 18.4
      signal addr_done, match, dir_read : std_logic;
      signal a_bit_index : unsigned(3 downto 0);
      signal addr_byte : std_logic_vector(7 downto 0);
      signal n_match, n_miss : unsigned(CNT_W-1 downto 0);

      -- 18.10
      signal in_phase, dir_q, rx_is_pointer, rx_enable, tx_start, tx_continue : std_logic;
      signal data_index : unsigned(3 downto 0);
      signal n_stretch : unsigned(CNT_W-1 downto 0);

      -- 18.5
      signal ack_active, ack_armed, ack_sda_low : std_logic;
      signal byte_done_any, ack_en : std_logic;
      signal n_acks, n_nacks : unsigned(CNT_W-1 downto 0);

      -- 18.6
      signal rx_valid, rx_byte_done : std_logic;
      signal r_bit_index : unsigned(3 downto 0);
      signal rx_byte : std_logic_vector(7 downto 0);
      signal n_bytes_rx, n_partial : unsigned(CNT_W-1 downto 0);

      -- 18.7
      signal tx_req, tx_sda_low, driving, byte_sent : std_logic;
      signal t_bit_index : unsigned(3 downto 0);
      signal rd_data : std_logic_vector(7 downto 0);
      signal n_bytes_tx, n_bits_tx : unsigned(CNT_W-1 downto 0);

      -- 18.8
      signal awaiting, mack_valid, mack_ack, keep_sourcing : std_logic;
      signal n_m_ack, n_m_nack : unsigned(CNT_W-1 downto 0);

      -- 18.9
      signal wr_accept : std_logic;

   begin

      mid_byte <= acquiring or receiving;
      stop_eff <= stop_pulse or abort;

      n_aborts       <= nab;
      n_sda_conflict <= ncf;

      -- ---- the give-up timeout ------------------------------------------------
      -- The counter runs only while a transfer is open AND SCL is high. Any fall resets it, so
      -- a clocking master never reaches the limit however slowly it clocks.
      process (clk, rst_n)
      begin
         if rst_n = '0' then
            idle_cnt <= 0;
            abort    <= '0';
            nab      <= (others => '0');
         elsif rising_edge(clk) then
            abort <= '0';
            if bus_active = '0' or scl_q = '0' then
               idle_cnt <= 0;
            elsif idle_cnt = IDLE_CYCLES then
               abort    <= '1';
               idle_cnt <= 0;
               nab      <= nab + 1;
            else
               idle_cnt <= idle_cnt + 1;
            end if;
         end if;
      end process;

      u_sync : entity work.i2c_slave_sync
         generic map (SYNC_DEPTH => SYNC_DEPTH)
         port map (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);

      u_frm : entity work.i2c_slave_framing
         generic map (CNT_W => CNT_W)
         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 => mid_byte, framing_midbyte => framing_midbyte,
            n_starts => n_starts, n_restarts => n_restarts_frm, n_stops => n_stops);

      u_addr : entity work.i2c_slave_addr
         generic map (MY_ADDR => MY_ADDR, CNT_W => CNT_W)
         port map (clk => clk, rst_n => rst_n, scl_rise => scl_rise, sda_q => sda_q,
            start_pulse => start_pulse, stop_pulse => stop_eff,
            acquiring => acquiring, bit_index => a_bit_index, addr_done => addr_done,
            addr_byte => addr_byte, match => match, selected => selected,
            dir_read => dir_read, n_match => n_match, n_miss => n_miss);

      u_txn : entity work.i2c_slave_txn
         generic map (CNT_W => CNT_W)
         port map (clk => clk, rst_n => rst_n, scl_q => scl_q, scl_fall => scl_fall,
            start_pulse => start_pulse, restart_pulse => restart_pulse,
            stop_pulse => stop_eff, addr_done => addr_done, match => match,
            dir_read => dir_read, ack_active => ack_active, rx_valid => rx_valid,
            byte_sent => byte_sent, mack_valid => mack_valid, mack_ack => mack_ack,
            stall_req => stall_req,
            in_phase => in_phase, dir_q => dir_q, rx_is_pointer => rx_is_pointer,
            rx_enable => rx_enable, tx_start => tx_start, tx_continue => tx_continue,
            data_index => data_index, scl_drive_low => scl_drive_low,
            stretching => stretching, n_phases => n_phases,
            -- The reported restart count comes from the transaction layer, not from 18.3.
            -- The numbers are equal by construction; wiring it this way makes the
            -- `restart_pulse` connection load-bearing, and a mutation tying it off then dies.
            n_restarts => n_restarts, n_stretch => n_stretch);

      -- ---- the acknowledge policy: THREE cases, not two -----------------------
      byte_done_any <= rx_byte_done or (addr_done and match);

      ack_en <= '1' when (addr_done = '1' and match = '1') else   -- the address, which matched
                '1' when rx_is_pointer = '1'                else  -- the pointer: writes nothing
                wr_accept;                                        -- a data byte: ask 18.9

      u_ack : entity work.i2c_slave_ack
         generic map (CNT_W => CNT_W)
         port map (clk => clk, rst_n => rst_n, scl_fall => scl_fall,
            byte_done => byte_done_any, ack_en => ack_en,
            start_pulse => start_pulse, stop_pulse => stop_eff,
            sda_drive_low => ack_sda_low, ack_active => ack_active,
            ack_armed => ack_armed, n_acks => n_acks, n_nacks => n_nacks);

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

      u_tx : entity work.i2c_slave_tx
         generic map (CNT_W => CNT_W)
         port map (clk => clk, rst_n => rst_n, scl_fall => scl_fall,
            start_pulse => start_pulse, stop_pulse => stop_eff,
            tx_start => tx_start, tx_continue => tx_continue,
            tx_req => tx_req, tx_byte => rd_data, sda_drive_low => tx_sda_low,
            driving => driving, bit_index => t_bit_index, byte_sent => byte_sent,
            n_bytes => n_bytes_tx, n_bits => n_bits_tx);

      u_mack : entity work.i2c_slave_mack
         generic map (CNT_W => CNT_W)
         port map (clk => clk, rst_n => rst_n, scl_rise => scl_rise, sda_q => sda_q,
            start_pulse => start_pulse, stop_pulse => stop_eff, byte_sent => byte_sent,
            awaiting => awaiting, mack_valid => mack_valid, mack_ack => mack_ack,
            keep_sourcing => keep_sourcing, n_ack => n_m_ack, n_nack => n_m_nack);

      -- NOTE the absent connections: this instance has no START or STOP input, not even the
      -- synthesised abort. The pointer survives everything -- Chapter 18.9 §2.
      u_regs : entity work.i2c_slave_regs
         generic map (N_REG => N_REG, RO_MASK => RO_MASK, CNT_W => CNT_W)
         port map (clk => clk, rst_n => rst_n,
            rx_valid => rx_valid, rx_byte => rx_byte, rx_is_pointer => rx_is_pointer,
            wr_accept => wr_accept, rd_data => rd_data, rd_taken => tx_req,
            reg_flat => reg_flat, pointer => pointer,
            n_writes => n_writes, n_refused => n_refused, n_reads => n_reads);

      -- ---- the SDA arbitration -----------------------------------------------
      sda_drive_low <= ack_sda_low or tx_sda_low;

      process (clk, rst_n)
      begin
         if rst_n = '0' then
            ncf <= (others => '0');
         elsif rising_edge(clk) then
            if ack_sda_low = '1' and tx_sda_low = '1' then
               ncf <= ncf + 1;
            end if;
         end if;
      end process;

   end architecture rtl;

7. The End-to-End Testbench

This bench is not for proving the blocks are connected. A test that reads back a register it just wrote would pass on a design whose acknowledge was inverted, whose read-only mask was ignored, and whose reset held the bus down — because none of those appear in a value comparison.

So every check is a bus-level check:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
the master reads SDA back in the high phase of every ninth pulse
   -> an acknowledge is OBSERVED, not inferred from a DUT signal
the master reconstructs every read byte from the wire
   -> the shift register cannot testify about the line
the master releases SCL and waits for it to rise
   -> a stretch is something that HAPPENS to it
testwhat it establishes
T1a target held in reset releases both lines and the bus idles high
T2a three-byte write, every byte answered on the wire, one NACK at the read-only register
T3write, repeated START, read: the byte comes back off the wire
T4a two-byte read walks the pointer forward
T5another device's address is met with silence — not a NACK the slave drove
T6a stretch delays the master and the transfer completes correctly
T7the master vanishes mid-byte with SCL high, and the slave gives up — then the bus works again
T8a repeated START mid-byte discards the partial byte and reframes
T9SCL was only ever pulled to stretch, and never while it was high
T10through a three-byte read, the acknowledge never fought the transmitter
T11a read-only register is readable: the restriction is one-directional
T12back-to-back transactions with no idle time between them
T13an idle bus for six timeout periods produces no abort
T14twelve pseudo-random transactions agree with a behavioural model

T3 is the module's thesis in one test. Write a byte, repeated START, read it back — and the value has to travel out through 18.6, into 18.9, survive the restart because 18.10 cleared the right state and 18.9 cleared none, then come back through 18.7 and onto the wire in time. A single wrong decision anywhere in that path changes the byte.

T7 is the recovery, end to end. The master stops clocking mid-byte with SCL high while the slave holds SDA low, the timeout fires, SDA is released, and then a completely ordinary transaction is run to prove the bus is usable again. A recovery that leaves the bus in a state where nothing works is not a recovery.

T14 is a scoreboard, because the directed tests check the cases I thought of. Twelve transactions with pointers that sometimes run past the end of the map and byte counts that sometimes cross the read-only register, compared against a behavioural model after every one.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_slave_tb.sv — the self-checking testbench
   // -----------------------------------------------------------------------------
   // i2c_slave_tb.sv
   // The end-to-end oracle for the whole target.
   //
   // WHAT THIS BENCH IS NOT FOR. It is not for proving the blocks are connected. A wiring
   // diagram that compiles is not a slave, and a test that reads back a register it just wrote
   // would pass on a design whose acknowledge was inverted, whose read-only mask was ignored,
   // and whose reset held the bus down -- because none of those show up in a value comparison.
   //
   // SO EVERY CHECK HERE IS A BUS-LEVEL CHECK. The master model reads SDA back in the high
   // phase of every ninth pulse, so an acknowledge is OBSERVED rather than inferred; it reads
   // every data bit of a read out of the wire rather than out of the DUT; and it releases SCL
   // and waits, so a stretch is a thing that happens to it.
   //
   // AND THE LAST TEST IS A SCOREBOARD. Twelve pseudo-random transactions against a
   // behavioural model of the register map, comparing after each one -- because the directed
   // tests check the cases I thought of, and the model checks the ones I did not.
   //
   // The sequence comes from an eight-bit LFSR with a fixed seed, not from $urandom, so that
   // all three language versions run the SAME stimulus and any failure is reproducible.
   // -----------------------------------------------------------------------------
   `timescale 1ns/1ps

   module i2c_slave_tb;

      localparam int    HALF    = 8;          // clocks per SCL half-phase
      localparam [6:0]  ADDR    = 7'h50;
      localparam [6:0]  OTHER   = 7'h21;
      localparam [6:0]  NEAR    = ADDR ^ 7'h40;   // 0x10: ours with only the top bit flipped
      localparam int    N_REG   = 8;
      localparam int    RO_MASK = 8'h04;      // register 2 is read-only
      localparam int    IDLE_C  = 64;         // short, so the abort test is quick

      logic clk = 1'b0;
      logic rst_n = 1'b0;

      // ---- the bus: device 0 is the master model, device 1 is the slave --------
      logic m_scl_low = 1'b0, m_sda_low = 1'b0;
      logic s_scl_low, s_sda_low;
      logic [1:0] scl_dl, sda_dl;
      logic scl, sda;
      logic [1:0] scl_in, sda_in, scl_rbl, sda_rbl;
      logic [7:0] scl_holders, sda_holders;

      assign scl_dl = {s_scl_low, m_scl_low};
      assign sda_dl = {s_sda_low, m_sda_low};

      i2c_line_model #(.N_DEV(2)) bus (
         .scl_drive_low(scl_dl), .sda_drive_low(sda_dl),
         .scl(scl), .sda(sda), .scl_in(scl_in), .sda_in(sda_in),
         .scl_released_but_low(scl_rbl), .sda_released_but_low(sda_rbl),
         .scl_holders(scl_holders), .sda_holders(sda_holders)
      );

      // ---- the application's stall, as a countdown ----------------------------
      localparam int STALL_N = 250;
      logic stall_load = 1'b0;
      int   stall_hold = 0;
      wire  stall_req  = (stall_hold > 0);

      always @(posedge clk) begin
         if (!rst_n)              stall_hold <= 0;
         else if (stall_load)     stall_hold <= STALL_N;
         else if (stall_hold > 0) stall_hold <= stall_hold - 1;
      end

      // ---- the DUT ------------------------------------------------------------
      logic [8*N_REG-1:0] reg_flat;
      logic [7:0] pointer;
      logic selected, stretching;
      logic [15:0] n_phases, n_restarts, n_writes, n_refused, n_reads,
                   n_aborts, n_sda_conflict;

      i2c_slave #(
         .MY_ADDR(ADDR), .N_REG(N_REG), .RO_MASK(RO_MASK),
         .IDLE_CYCLES(IDLE_C), .SYNC_DEPTH(2), .CNT_W(16)
      ) dut (
         .clk(clk), .rst_n(rst_n),
         .scl_pin(scl), .sda_pin(sda),
         .scl_drive_low(s_scl_low), .sda_drive_low(s_sda_low),
         .stall_req(stall_req), .reg_flat(reg_flat), .pointer(pointer),
         .selected(selected), .stretching(stretching),
         .n_phases(n_phases), .n_restarts(n_restarts), .n_writes(n_writes),
         .n_refused(n_refused), .n_reads(n_reads), .n_aborts(n_aborts),
         .n_sda_conflict(n_sda_conflict)
      );

      always #5 clk = ~clk;

      int errors = 0;
      int k, j;

      // Scratch, at module scope. Icarus does not support overriding a variable's lifetime
      // inside a block, so a bench that wants named temporaries declares them here.
      bit         nk, q;
      int         t;      // wr_txn's own loop variable -- see the note above
      logic [7:0] b, p8, d8;
      int         cnt, mism;

      // ---- a portable pseudo-random source ------------------------------------
      // NOT $urandom. The scoreboard sequence has to be IDENTICAL in all three languages, and
      // VHDL has no $random at all while Verilog-2001's is signed and differently seeded. An
      // eight-bit LFSR with a fixed seed is the same sequence everywhere, which makes the three
      // benches comparable and every failure reproducible.
      logic [7:0] lfsr;

      function automatic [7:0] lfsr_step (input [7:0] x);
         lfsr_step = {x[6:0], x[7] ^ x[5] ^ x[4] ^ x[3]};
      endfunction

      // ---- the behavioural model of the map ----------------------------------
      logic [7:0] model [0:N_REG-1];
      logic [7:0] mptr = 8'h00;

      function automatic bit ro (input int idx);
         ro = ((RO_MASK >> idx) & 1) != 0;
      endfunction

      task automatic model_reset;
         begin
            for (k = 0; k < N_REG; k++) model[k] = 8'h00;
            mptr = 8'h00;
         end
      endtask

      // ---- observers ----------------------------------------------------------
      int drive_while_high = 0;      // SCL pulled down while the line was high
      int scl_held_in_reset = 0;
      int sda_held_in_reset = 0;
      int scl_driven_unstretched = 0; // SCL pulled for any reason other than a stretch
      int stretch_waits = 0;
      logic scl_low_d = 1'b0;

      always @(posedge clk) begin
         if (!rst_n) begin
            if (s_scl_low) scl_held_in_reset++;
            if (s_sda_low) sda_held_in_reset++;
         end else begin
            if (s_scl_low && !scl_low_d && scl === 1'b1) drive_while_high++;
            if (s_scl_low && !stretching) scl_driven_unstretched++;
            scl_low_d <= s_scl_low;
         end
      end

      initial begin
         repeat (900000) @(posedge clk);
         $display("  FAIL watchdog: the bench did not finish");
         $fatal(1);
      end

      // ---- the master model ---------------------------------------------------
      task automatic hp;
         begin repeat (HALF) @(posedge clk); end
      endtask

      task automatic m_scl_release;
         int guard;
         begin
            @(negedge clk); m_scl_low = 1'b0; #1;
            guard = 0;
            if (scl !== 1'b1) begin
               stretch_waits++;
               while (scl !== 1'b1 && guard < 40000) begin @(posedge clk); guard++; end
               if (guard >= 40000) begin
                  $display("  FAIL the slave never released SCL"); errors++;
               end
            end
            hp();
         end
      endtask

      task automatic m_scl_pull;
         begin @(negedge clk); m_scl_low = 1'b1; hp(); end
      endtask

      task automatic m_start;
         begin
            @(negedge clk); m_scl_low = 1'b0; m_sda_low = 1'b0; hp();
            @(negedge clk); m_sda_low = 1'b1; hp();
            m_scl_pull();
         end
      endtask

      task automatic m_restart;
         begin
            @(negedge clk); m_sda_low = 1'b0; hp();
            m_scl_release();
            @(negedge clk); m_sda_low = 1'b1; hp();
            m_scl_pull();
         end
      endtask

      task automatic m_stop;
         begin
            @(negedge clk); m_sda_low = 1'b1; hp();
            m_scl_release();
            @(negedge clk); m_sda_low = 1'b0; hp();
         end
      endtask

      // One bit. The master drives `sda_low` and SAMPLES the line in the high phase, so what
      // comes back is what was on the wire -- not what the DUT thinks it sent.
      logic sampled;
      task automatic m_bit (input bit sda_low);
         begin
            @(negedge clk); m_sda_low = sda_low; hp();
            m_scl_release();
            sampled = sda;
            m_scl_pull();
         end
      endtask

      // A byte the master writes. Returns the ninth bit it OBSERVED: 0 means the slave
      // acknowledged, 1 means it did not.
      task automatic m_put (input [7:0] d, output bit nack);
         begin
            for (k = 7; k >= 0; k--) m_bit(~d[k]);
            m_bit(1'b0);                 // release: the slave answers
            nack = sampled;
         end
      endtask

      // A byte the master reads, reconstructed from the wire, followed by the master's answer.
      task automatic m_get (input bit ack, output [7:0] d);
         begin
            d = 8'h00;
            for (k = 7; k >= 0; k--) begin
               m_bit(1'b0);              // release: the slave drives
               d[k] = sampled;
            end
            m_bit(ack);                  // a pull-down is an ACK
         end
      endtask

      task automatic do_reset;
         begin
            @(negedge clk);
            rst_n = 1'b0; m_scl_low = 1'b0; m_sda_low = 1'b0; stall_load = 1'b0;
            drive_while_high = 0; scl_held_in_reset = 0; sda_held_in_reset = 0;
            scl_driven_unstretched = 0; stretch_waits = 0;
            repeat (4) @(posedge clk);
            @(negedge clk); rst_n = 1'b1;
            hp(); #1;
            model_reset();
         end
      endtask

      task automatic ck (input string what, input int got, input int exp);
         begin
            if (got !== exp) begin
               $display("  FAIL %0s: got %0d expected %0d", what, got, exp);
               errors++;
            end
         end
      endtask

      // ---- transaction-level helpers, mirrored into the model -----------------
      // A write transaction: START, address, pointer, then `n` data bytes, STOP.
      task automatic wr_txn (input [7:0] ptr, input int n, input [7:0] d0,
                             output int nacks);
         begin
            nacks = 0;
            m_start();
            m_put({ADDR, 1'b0}, nk); if (nk) begin $display("  FAIL address NACKed"); errors++; end
            m_put(ptr, nk);
            // A pointer byte must ALWAYS be acknowledged: it writes no register, so no
            // register's access rules apply to it. The first version of the top level answered
            // it with the register file's permission, which made the device unreachable as soon
            // as the pointer came to rest on a read-only address.
            if (nk) begin $display("  FAIL pointer byte NACKed"); errors++; end
            mptr = ptr;
            for (t = 0; t < n; t++) begin
               m_put(d0 + t[7:0], nk);
               if (nk) nacks++;
               // the model: a refused byte neither lands nor advances
               if (!ro(mptr % N_REG)) begin
                  model[mptr % N_REG] = d0 + t[7:0];
                  mptr = mptr + 8'd1;
               end
            end
            m_stop();
         end
      endtask

      logic [7:0] rd [0:7];

      // ---- tests --------------------------------------------------------------
      initial begin
         $display("=== i2c_slave: the whole target, checked from the wire ===");

         // ---- T1. A device held in reset holds nothing.
         @(negedge clk); rst_n = 1'b0; m_scl_low = 1'b0; m_sda_low = 1'b0;
         scl_held_in_reset = 0; sda_held_in_reset = 0;
         repeat (30) @(posedge clk);
         $display("T1  a target held in reset releases both lines");
         ck("T1 SCL never pulled in reset", scl_held_in_reset, 0);
         ck("T1 SDA never pulled in reset", sda_held_in_reset, 0);
         ck("T1 SCL idles high", scl, 1);
         ck("T1 SDA idles high", sda, 1);

         // ---- T2. A whole write transaction, acknowledged on the wire.
         do_reset();
         wr_txn(8'h00, 3, 8'h11, nk);
         $display("T2  a three-byte write: every byte acknowledged on the wire");
         ck("T2 reg 0", reg_flat[0*8 +: 8], 8'h11);
         ck("T2 reg 1", reg_flat[1*8 +: 8], 8'h12);
         ck("T2 reg 2 is read-only", reg_flat[2*8 +: 8], 8'h00);
         ck("T2 exactly one NACK, at the read-only register", nk, 1);
         ck("T2 two writes accepted", n_writes, 2);
         ck("T2 one refusal", n_refused, 1);

         // ---- T3. THE END-TO-END PROOF: a combined transfer reads back what was written.
         do_reset();
         wr_txn(8'h05, 1, 8'hA7, nk);
         ck("T3 the write was accepted", nk, 0);
         m_start();
         m_put({ADDR, 1'b0}, q);
         m_put(8'h05, q);          // repoint
         m_restart();
         m_put({ADDR, 1'b1}, q);
         m_get(1'b0, b);
         $display("T3  write, repeated START, read: the byte comes back off the wire");
         ck("T3 the byte read equals the byte written", b, 8'hA7);
         ck("T3 and the two SDA drivers never overlapped", n_sda_conflict, 0);
         m_stop();

         // ---- T4. Auto-increment across a multi-byte read.
         do_reset();
         wr_txn(8'h00, 2, 8'h31, nk);           // reg0=0x31, reg1=0x32
         m_start();
         m_put({ADDR, 1'b0}, q); m_put(8'h00, q);
         m_restart();
         m_put({ADDR, 1'b1}, q);
         m_get(1'b1, rd[0]);
         m_get(1'b0, rd[1]);
         m_stop();
         $display("T4  a two-byte read walks the pointer forward");
         ck("T4 first byte", rd[0], 8'h31);
         ck("T4 second byte", rd[1], 8'h32);
         ck("T4 two reads counted", n_reads, 2);
         ck("T4 and the two SDA drivers never overlapped", n_sda_conflict, 0);

         // ---- T5. Somebody else's address: total silence for the whole transaction.
         do_reset();
         m_start();
         m_put({OTHER, 1'b0}, nk);
         $display("T5  another device's address is met with silence, not a NACK we drove");
         ck("T5 the ninth bit was left high", nk, 1);
         ck("T5 we were never selected", selected, 0);
         ck("T5 no phase opened", n_phases, 0);
         m_put(8'h77, nk);
         ck("T5 and a data byte is ignored too", nk, 1);
         ck("T5 nothing was written", reg_flat[0*8 +: 8], 8'h00);
         m_stop();

         // AND AGAIN ONE BIT AWAY. 0x21 differs from 0x50 in four bits, so a decoder that
         // had stopped comparing any ONE of them would still reject it -- the bits it does
         // still compare are enough on their own. A decoder that had lost the top address
         // bit answers to 0x10, and only an address exactly one bit from ours can show
         // that. The claim this test makes is not "some wrong address is refused" but
         // "the address comparison is what gates the acknowledge, the phase and the
         // register file" -- which needs the one address that isolates a single bit.
         // The seven-bit sweep itself belongs to Chapter 18.4; here it is composition.
         do_reset();
         m_start();
         m_put({NEAR, 1'b0}, nk);
         $display("T5  a one-bit near miss is refused the same way, and changes nothing");
         ck("T5 the near miss was not acknowledged", nk, 1);
         ck("T5 it never selected us", selected, 0);
         ck("T5 no phase opened on it", n_phases, 0);
         m_put(8'h88, nk);
         ck("T5 its data byte was ignored too", nk, 1);
         ck("T5 and the register file is untouched", reg_flat[0*8 +: 8], 8'h00);
         ck("T5 the two SDA drivers never overlapped", n_sda_conflict, 0);
         m_stop();

         // ---- T6. Clock stretching, end to end, with the master blocking.
         do_reset();
         m_start();
         @(negedge clk); stall_load = 1'b1;
         @(posedge clk);
         @(negedge clk); stall_load = 1'b0;
         m_put({ADDR, 1'b0}, q);
         ck("T6 the address was still acknowledged", q, 0);
         // The stretch engages at the fall closing that acknowledge slot.
         ck("T6 the slave is stretching", stretching, 1);
         m_put(8'h03, q);                       // BLOCKS until the slave lets go
         ck("T6 the master had to wait", stretch_waits, 1);
         m_put(8'h5E, q);
         m_stop();
         $display("T6  a stretch delays the master and the transfer completes correctly");
         ck("T6 the byte landed", reg_flat[3*8 +: 8], 8'h5E);
         ck("T6 one stretch happened", n_aborts, 0);

         // ---- T7. THE SLAVE-SIDE WEDGE, AND THE RECOVERY.
         // The master abandons mid-byte, leaving SCL high, at a moment when the slave is
         // holding SDA low. Without the timeout the bus is dead for ever.
         do_reset();
         m_start();
         m_put({ADDR, 1'b0}, q);                // the address, acknowledged
         // Now send a byte of 0x00 but stop clocking in the middle, leaving SCL HIGH while the
         // slave is holding SDA low for a zero bit.
         for (j = 0; j < 4; j++) m_bit(1'b1);   // four zero bits: SDA pulled by the master
         @(negedge clk); m_sda_low = 1'b1; hp();
         m_scl_release();                       // SCL high, and there it stays
         $display("T7  the master vanishes mid-byte with SCL high: the slave must give up");
         ck("T7 a transfer is still open", selected, 1);
         // Wait for the timeout plus margin.
         repeat (IDLE_C * 3) @(posedge clk);
         ck("T7 the slave aborted", n_aborts > 0, 1);
         ck("T7 and released SDA", s_sda_low, 0);
         ck("T7 and is not selected any more", selected, 0);
         // The bus must now be usable. Release the master's SDA and run a clean transaction.
         @(negedge clk); m_sda_low = 1'b0; hp();
         ck("T7 the bus is free", sda, 1);
         wr_txn(8'h01, 1, 8'h6B, nk);
         ck("T7 a new transaction was accepted", nk, 0);
         ck("T7 and it landed", reg_flat[1*8 +: 8], 8'h6B);

         // ---- T8. A repeated START mid-byte: the partial byte is discarded.
         do_reset();
         m_start();
         m_put({ADDR, 1'b0}, q);
         m_put(8'h04, q);                       // pointer = 4
         for (j = 0; j < 3; j++) m_bit(1'b1);   // three bits of a byte that never finishes
         m_restart();
         m_put({ADDR, 1'b0}, q);
         m_put(8'h04, q);                       // a fresh pointer byte
         m_put(8'h9D, q);
         m_stop();
         $display("T8  a repeated START mid-byte discards the partial byte and reframes");
         ck("T8 the byte landed at 4", reg_flat[4*8 +: 8], 8'h9D);
         ck("T8 only one write happened", n_writes, 1);
         ck("T8 one restart counted", n_restarts, 1);

         // ---- T9. The slave never drives SCL except to stretch.
         $display("T9  across every test so far, SCL was only ever pulled to stretch");
         ck("T9 no unstretched SCL drive", scl_driven_unstretched, 0);
         ck("T9 no drive while SCL was high", drive_while_high, 0);

         // ---- T10. The two SDA drivers never overlap -- checked WITH A READ IN FLIGHT.
         //
         // `n_sda_conflict` is a counter inside the DUT, so every `do_reset` clears it. An
         // earlier version of this test checked it once at the end and passed a mutant that
         // made the acknowledge block fight the transmitter on every multi-byte read -- because
         // the only reads had happened before an intervening reset, and the evidence was gone.
         //
         // A cumulative counter is only as good as the window you read it over. This test owns
         // its window: it performs the multi-byte read that creates the overlap opportunity and
         // checks immediately afterwards.
         do_reset();
         wr_txn(8'h00, 2, 8'h61, nk);                // reg0=0x61, reg1=0x62
         m_start();
         m_put({ADDR, 1'b0}, q); m_put(8'h00, q);
         m_restart();
         m_put({ADDR, 1'b1}, q);
         m_get(1'b1, rd[0]);                         // ACK: another byte follows
         m_get(1'b1, rd[1]);                         // ACK again
         m_get(1'b0, rd[2]);                         // NACK: done
         m_stop();
         $display("T10 through a three-byte read, the acknowledge never fought the transmitter");
         ck("T10 no SDA conflict", n_sda_conflict, 0);
         ck("T10 first byte", rd[0], 8'h61);
         ck("T10 second byte", rd[1], 8'h62);
         ck("T10 three bytes served", n_reads, 3);

         // ---- T11. A read of a read-only register still works.
         // Read-only means "the master may not write it", not "the master may not read it".
         do_reset();
         m_start();
         m_put({ADDR, 1'b0}, q);
         m_put(8'h02, q);                       // the read-only register
         m_restart();
         m_put({ADDR, 1'b1}, q);
         m_get(1'b0, b);
         $display("T11 a read-only register is readable: the restriction is one-directional");
         ck("T11 read back its value", b, 8'h00);
         ck("T11 and the two SDA drivers never overlapped", n_sda_conflict, 0);
         m_stop();

         // ---- T12. Back-to-back transactions with no idle between them.
         do_reset();
         wr_txn(8'h06, 1, 8'h41, nk);
         wr_txn(8'h07, 1, 8'h42, nk);
         $display("T12 back-to-back transactions with no idle time between them");
         ck("T12 reg 6", reg_flat[6*8 +: 8], 8'h41);
         ck("T12 reg 7", reg_flat[7*8 +: 8], 8'h42);
         ck("T12 two phases per transaction is not what happened", n_phases, 2);

         // ---- T13. AN IDLE BUS IS NOT AN ABANDONED TRANSFER.
         // The give-up timeout must only run while a transfer is OPEN. A counter that also ran
         // on an idle bus would fire every IDLE_CYCLES for ever -- harmless to behaviour,
         // because every block is already idle, but it makes `n_aborts` mean nothing. A
         // non-zero abort count on a shipped device is supposed to say a master died.
         do_reset();
         wr_txn(8'h00, 1, 8'h13, nk);
         repeat (IDLE_C * 6) @(posedge clk);          // six timeout periods of nothing at all
         $display("T13 an idle bus for six timeout periods produces no abort");
         ck("T13 no aborts while idle", n_aborts, 0);
         ck("T13 and the map is untouched", reg_flat[0*8 +: 8], 8'h13);
         begin
            wr_txn(8'h01, 1, 8'h14, nk);
            ck("T13 and the bus still works afterwards", reg_flat[1*8 +: 8], 8'h14);
         end

         // ---- T14. THE SCOREBOARD. Twelve random transactions against the model.
         do_reset();
         begin
            mism = 0;
            lfsr = 8'h5A;
            for (j = 0; j < 12; j++) begin
               lfsr = lfsr_step(lfsr); p8  = lfsr % 12;   // sometimes past the end of the map
               lfsr = lfsr_step(lfsr); d8  = lfsr;
               lfsr = lfsr_step(lfsr); cnt = (lfsr % 3) + 1;
               wr_txn(p8, cnt, d8, nk);
               for (k = 0; k < N_REG; k++)
                  if (reg_flat[k*8 +: 8] !== model[k]) begin
                     $display("  FAIL T14 txn %0d reg %0d: got %02x expected %02x",
                              j, k, reg_flat[k*8 +: 8], model[k]);
                     mism++; errors++;
                  end
            end
            $display("T14 twelve pseudo-random write transactions agree with the model");
            ck("T14 no mismatches", mism, 0);
            ck("T14 and still no SDA conflict", n_sda_conflict, 0);
            ck("T14 and no spurious aborts", n_aborts, 0);
         end

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

   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_slave_tb.v — the same tests in Verilog-2001
   // -----------------------------------------------------------------------------
   // i2c_slave_tb.v
   // The end-to-end oracle for the whole target.
   //
   // WHAT THIS BENCH IS NOT FOR. It is not for proving the blocks are connected. A wiring
   // diagram that compiles is not a slave, and a test that reads back a register it just wrote
   // would pass on a design whose acknowledge was inverted, whose read-only mask was ignored,
   // and whose reset held the bus down -- because none of those show up in a value comparison.
   //
   // SO EVERY CHECK HERE IS A BUS-LEVEL CHECK. The master model reads SDA back in the high
   // phase of every ninth pulse, so an acknowledge is OBSERVED rather than inferred; it reads
   // every data bit of a read out of the wire rather than out of the DUT; and it releases SCL
   // and waits, so a stretch is a thing that happens to it.
   //
   // AND THE LAST TEST IS A SCOREBOARD. Twelve pseudo-random transactions against a
   // behavioural model of the register map, comparing after each one -- because the directed
   // tests check the cases I thought of, and the model checks the ones I did not.
   //
   // The sequence comes from an eight-bit LFSR with a fixed seed, not from $urandom, so that
   // all three language versions run the SAME stimulus and any failure is reproducible.
   // -----------------------------------------------------------------------------
   `timescale 1ns/1ps

   module i2c_slave_tb;

      localparam integer    HALF    = 8;          // clocks per SCL half-phase
      localparam [6:0]  ADDR    = 7'h50;
      localparam [6:0]  OTHER   = 7'h21;
      localparam [6:0]  NEAR    = ADDR ^ 7'h40;   // 0x10: ours with only the top bit flipped
      localparam integer    N_REG   = 8;
      localparam integer    RO_MASK = 8'h04;      // register 2 is read-only
      localparam integer    IDLE_C  = 64;         // short, so the abort test is quick

      reg  clk = 1'b0;
      reg  rst_n = 1'b0;

      // ---- the bus: device 0 is the master model, device 1 is the slave --------
      reg  m_scl_low = 1'b0, m_sda_low = 1'b0;
      wire s_scl_low, s_sda_low;
      wire [1:0] scl_dl, sda_dl;
      wire scl, sda;
      wire [1:0] scl_in, sda_in, scl_rbl, sda_rbl;
      wire [7:0] scl_holders, sda_holders;

      assign scl_dl = {s_scl_low, m_scl_low};
      assign sda_dl = {s_sda_low, m_sda_low};

      i2c_line_model #(.N_DEV(2)) bus (
         .scl_drive_low(scl_dl), .sda_drive_low(sda_dl),
         .scl(scl), .sda(sda), .scl_in(scl_in), .sda_in(sda_in),
         .scl_released_but_low(scl_rbl), .sda_released_but_low(sda_rbl),
         .scl_holders(scl_holders), .sda_holders(sda_holders)
      );

      // ---- the application's stall, as a countdown ----------------------------
      localparam integer STALL_N = 250;
      reg  stall_load = 1'b0;
      integer stall_hold = 0;
      wire  stall_req  = (stall_hold > 0);

      always @(posedge clk) begin
         if (!rst_n)              stall_hold <= 0;
         else if (stall_load)     stall_hold <= STALL_N;
         else if (stall_hold > 0) stall_hold <= stall_hold - 1;
      end

      // ---- the DUT ------------------------------------------------------------
      wire [8*N_REG-1:0] reg_flat;
      wire [7:0] pointer;
      wire selected, stretching;
      wire [15:0] n_phases, n_restarts, n_writes, n_refused, n_reads,
                   n_aborts, n_sda_conflict;

      i2c_slave #(
         .MY_ADDR(ADDR), .N_REG(N_REG), .RO_MASK(RO_MASK),
         .IDLE_CYCLES(IDLE_C), .SYNC_DEPTH(2), .CNT_W(16)
      ) dut (
         .clk(clk), .rst_n(rst_n),
         .scl_pin(scl), .sda_pin(sda),
         .scl_drive_low(s_scl_low), .sda_drive_low(s_sda_low),
         .stall_req(stall_req), .reg_flat(reg_flat), .pointer(pointer),
         .selected(selected), .stretching(stretching),
         .n_phases(n_phases), .n_restarts(n_restarts), .n_writes(n_writes),
         .n_refused(n_refused), .n_reads(n_reads), .n_aborts(n_aborts),
         .n_sda_conflict(n_sda_conflict)
      );

      always #5 clk = ~clk;

      integer errors = 0;
      integer k, j;

      // Scratch, at module scope. Icarus does not support overriding a variable's lifetime
      // inside a block, so a bench that wants named temporaries declares them here.
      reg         nk, q;
      integer t;      // wr_txn's own loop variable -- see the note above
      reg  [7:0] b, p8, d8;
      integer cnt, mism;

      // ---- a portable pseudo-random source ------------------------------------
      // NOT $urandom. The scoreboard sequence has to be IDENTICAL in all three languages, and
      // VHDL has no $random at all while Verilog-2001's is signed and differently seeded. An
      // eight-bit LFSR with a fixed seed is the same sequence everywhere, which makes the three
      // benches comparable and every failure reproducible.
      reg  [7:0] lfsr;

      function [7:0] lfsr_step (input [7:0] x);
         lfsr_step = {x[6:0], x[7] ^ x[5] ^ x[4] ^ x[3]};
      endfunction

      // ---- the behavioural model of the map ----------------------------------
      reg  [7:0] model [0:N_REG-1];
      reg  [7:0] mptr = 8'h00;

      function ro (input integer idx);
         ro = ((RO_MASK >> idx) & 1) != 0;
      endfunction

      task model_reset;
         begin
            for (k = 0; k < N_REG; k = k + 1) model[k] = 8'h00;
            mptr = 8'h00;
         end
      endtask

      // ---- observers ----------------------------------------------------------
      integer drive_while_high = 0;      // SCL pulled down while the line was high
      integer scl_held_in_reset = 0;
      integer sda_held_in_reset = 0;
      integer scl_driven_unstretched = 0; // SCL pulled for any reason other than a stretch
      integer stretch_waits = 0;
      reg  scl_low_d = 1'b0;

      always @(posedge clk) begin
         if (!rst_n) begin
            if (s_scl_low) scl_held_in_reset = scl_held_in_reset + 1;
            if (s_sda_low) sda_held_in_reset = sda_held_in_reset + 1;
         end else begin
            if (s_scl_low && !scl_low_d && scl === 1'b1) drive_while_high = drive_while_high + 1;
            if (s_scl_low && !stretching) scl_driven_unstretched = scl_driven_unstretched + 1;
            scl_low_d <= s_scl_low;
         end
      end

      initial begin
         repeat (900000) @(posedge clk);
         $display("  FAIL watchdog: the bench did not finish");
         $fatal(1);
      end

      // ---- the master model ---------------------------------------------------
      task hp;
         begin repeat (HALF) @(posedge clk); end
      endtask

      task m_scl_release;
         integer guard;
         begin
            @(negedge clk); m_scl_low = 1'b0; #1;
            guard = 0;
            if (scl !== 1'b1) begin
               stretch_waits = stretch_waits + 1;
               while (scl !== 1'b1 && guard < 40000) begin @(posedge clk); guard = guard + 1; end
               if (guard >= 40000) begin
                  $display("  FAIL the slave never released SCL"); errors = errors + 1;
               end
            end
            hp();
         end
      endtask

      task m_scl_pull;
         begin @(negedge clk); m_scl_low = 1'b1; hp(); end
      endtask

      task m_start;
         begin
            @(negedge clk); m_scl_low = 1'b0; m_sda_low = 1'b0; hp();
            @(negedge clk); m_sda_low = 1'b1; hp();
            m_scl_pull();
         end
      endtask

      task m_restart;
         begin
            @(negedge clk); m_sda_low = 1'b0; hp();
            m_scl_release();
            @(negedge clk); m_sda_low = 1'b1; hp();
            m_scl_pull();
         end
      endtask

      task m_stop;
         begin
            @(negedge clk); m_sda_low = 1'b1; hp();
            m_scl_release();
            @(negedge clk); m_sda_low = 1'b0; hp();
         end
      endtask

      // One bit. The master drives `sda_low` and SAMPLES the line in the high phase, so what
      // comes back is what was on the wire -- not what the DUT thinks it sent.
      reg  sampled;
      task m_bit (input sda_low);
         begin
            @(negedge clk); m_sda_low = sda_low; hp();
            m_scl_release();
            sampled = sda;
            m_scl_pull();
         end
      endtask

      // A byte the master writes. Returns the ninth bit it OBSERVED: 0 means the slave
      // acknowledged, 1 means it did not.
      task m_put (input [7:0] d, output nack);
         begin
            for (k = 7; k >= 0; k = k - 1) m_bit(~d[k]);
            m_bit(1'b0);                 // release: the slave answers
            nack = sampled;
         end
      endtask

      // A byte the master reads, reconstructed from the wire, followed by the master's answer.
      task m_get (input ack, output [7:0] d);
         begin
            d = 8'h00;
            for (k = 7; k >= 0; k = k - 1) begin
               m_bit(1'b0);              // release: the slave drives
               d[k] = sampled;
            end
            m_bit(ack);                  // a pull-down is an ACK
         end
      endtask

      task do_reset;
         begin
            @(negedge clk);
            rst_n = 1'b0; m_scl_low = 1'b0; m_sda_low = 1'b0; stall_load = 1'b0;
            drive_while_high = 0; scl_held_in_reset = 0; sda_held_in_reset = 0;
            scl_driven_unstretched = 0; stretch_waits = 0;
            repeat (4) @(posedge clk);
            @(negedge clk); rst_n = 1'b1;
            hp(); #1;
            model_reset();
         end
      endtask

      task ck (input [200*8:1] what, input integer got, input integer exp);
         begin
            if (got !== exp) begin
               $display("  FAIL %0s: got %0d expected %0d", what, got, exp);
               errors = errors + 1;
            end
         end
      endtask

      // ---- transaction-level helpers, mirrored into the model -----------------
      // A write transaction: START, address, pointer, then `n` data bytes, STOP.
      task wr_txn (input [7:0] ptr, input integer n, input [7:0] d0,
                             output integer nacks);
         begin
            nacks = 0;
            m_start();
            m_put({ADDR, 1'b0}, nk); if (nk) begin $display("  FAIL address NACKed"); errors = errors + 1; end
            m_put(ptr, nk);
            // A pointer byte must ALWAYS be acknowledged: it writes no register, so no
            // register's access rules apply to it. The first version of the top level answered
            // it with the register file's permission, which made the device unreachable as soon
            // as the pointer came to rest on a read-only address.
            if (nk) begin $display("  FAIL pointer byte NACKed"); errors = errors + 1; end
            mptr = ptr;
            for (t = 0; t < n; t = t + 1) begin
               m_put(d0 + t[7:0], nk);
               if (nk) nacks = nacks + 1;
               // the model: a refused byte neither lands nor advances
               if (!ro(mptr % N_REG)) begin
                  model[mptr % N_REG] = d0 + t[7:0];
                  mptr = mptr + 8'd1;
               end
            end
            m_stop();
         end
      endtask

      reg  [7:0] rd [0:7];

      // ---- tests --------------------------------------------------------------
      initial begin
         $display("=== i2c_slave: the whole target, checked from the wire ===");

         // ---- T1. A device held in reset holds nothing.
         @(negedge clk); rst_n = 1'b0; m_scl_low = 1'b0; m_sda_low = 1'b0;
         scl_held_in_reset = 0; sda_held_in_reset = 0;
         repeat (30) @(posedge clk);
         $display("T1  a target held in reset releases both lines");
         ck("T1 SCL never pulled in reset", scl_held_in_reset, 0);
         ck("T1 SDA never pulled in reset", sda_held_in_reset, 0);
         ck("T1 SCL idles high", scl, 1);
         ck("T1 SDA idles high", sda, 1);

         // ---- T2. A whole write transaction, acknowledged on the wire.
         do_reset();
         wr_txn(8'h00, 3, 8'h11, nk);
         $display("T2  a three-byte write: every byte acknowledged on the wire");
         ck("T2 reg 0", reg_flat[0*8 +: 8], 8'h11);
         ck("T2 reg 1", reg_flat[1*8 +: 8], 8'h12);
         ck("T2 reg 2 is read-only", reg_flat[2*8 +: 8], 8'h00);
         ck("T2 exactly one NACK, at the read-only register", nk, 1);
         ck("T2 two writes accepted", n_writes, 2);
         ck("T2 one refusal", n_refused, 1);

         // ---- T3. THE END-TO-END PROOF: a combined transfer reads back what was written.
         do_reset();
         wr_txn(8'h05, 1, 8'hA7, nk);
         ck("T3 the write was accepted", nk, 0);
         m_start();
         m_put({ADDR, 1'b0}, q);
         m_put(8'h05, q);          // repoint
         m_restart();
         m_put({ADDR, 1'b1}, q);
         m_get(1'b0, b);
         $display("T3  write, repeated START, read: the byte comes back off the wire");
         ck("T3 the byte read equals the byte written", b, 8'hA7);
         ck("T3 and the two SDA drivers never overlapped", n_sda_conflict, 0);
         m_stop();

         // ---- T4. Auto-increment across a multi-byte read.
         do_reset();
         wr_txn(8'h00, 2, 8'h31, nk);           // reg0=0x31, reg1=0x32
         m_start();
         m_put({ADDR, 1'b0}, q); m_put(8'h00, q);
         m_restart();
         m_put({ADDR, 1'b1}, q);
         m_get(1'b1, rd[0]);
         m_get(1'b0, rd[1]);
         m_stop();
         $display("T4  a two-byte read walks the pointer forward");
         ck("T4 first byte", rd[0], 8'h31);
         ck("T4 second byte", rd[1], 8'h32);
         ck("T4 two reads counted", n_reads, 2);
         ck("T4 and the two SDA drivers never overlapped", n_sda_conflict, 0);

         // ---- T5. Somebody else's address: total silence for the whole transaction.
         do_reset();
         m_start();
         m_put({OTHER, 1'b0}, nk);
         $display("T5  another device's address is met with silence, not a NACK we drove");
         ck("T5 the ninth bit was left high", nk, 1);
         ck("T5 we were never selected", selected, 0);
         ck("T5 no phase opened", n_phases, 0);
         m_put(8'h77, nk);
         ck("T5 and a data byte is ignored too", nk, 1);
         ck("T5 nothing was written", reg_flat[0*8 +: 8], 8'h00);
         m_stop();

         // AND AGAIN ONE BIT AWAY. 0x21 differs from 0x50 in four bits, so a decoder that
         // had stopped comparing any ONE of them would still reject it -- the bits it does
         // still compare are enough on their own. A decoder that had lost the top address
         // bit answers to 0x10, and only an address exactly one bit from ours can show
         // that. The claim this test makes is not "some wrong address is refused" but
         // "the address comparison is what gates the acknowledge, the phase and the
         // register file" -- which needs the one address that isolates a single bit.
         // The seven-bit sweep itself belongs to Chapter 18.4; here it is composition.
         do_reset();
         m_start();
         m_put({NEAR, 1'b0}, nk);
         $display("T5  a one-bit near miss is refused the same way, and changes nothing");
         ck("T5 the near miss was not acknowledged", nk, 1);
         ck("T5 it never selected us", selected, 0);
         ck("T5 no phase opened on it", n_phases, 0);
         m_put(8'h88, nk);
         ck("T5 its data byte was ignored too", nk, 1);
         ck("T5 and the register file is untouched", reg_flat[0*8 +: 8], 8'h00);
         ck("T5 the two SDA drivers never overlapped", n_sda_conflict, 0);
         m_stop();

         // ---- T6. Clock stretching, end to end, with the master blocking.
         do_reset();
         m_start();
         @(negedge clk); stall_load = 1'b1;
         @(posedge clk);
         @(negedge clk); stall_load = 1'b0;
         m_put({ADDR, 1'b0}, q);
         ck("T6 the address was still acknowledged", q, 0);
         // The stretch engages at the fall closing that acknowledge slot.
         ck("T6 the slave is stretching", stretching, 1);
         m_put(8'h03, q);                       // BLOCKS until the slave lets go
         ck("T6 the master had to wait", stretch_waits, 1);
         m_put(8'h5E, q);
         m_stop();
         $display("T6  a stretch delays the master and the transfer completes correctly");
         ck("T6 the byte landed", reg_flat[3*8 +: 8], 8'h5E);
         ck("T6 one stretch happened", n_aborts, 0);

         // ---- T7. THE SLAVE-SIDE WEDGE, AND THE RECOVERY.
         // The master abandons mid-byte, leaving SCL high, at a moment when the slave is
         // holding SDA low. Without the timeout the bus is dead for ever.
         do_reset();
         m_start();
         m_put({ADDR, 1'b0}, q);                // the address, acknowledged
         // Now send a byte of 0x00 but stop clocking in the middle, leaving SCL HIGH while the
         // slave is holding SDA low for a zero bit.
         for (j = 0; j < 4; j = j + 1) m_bit(1'b1);   // four zero bits: SDA pulled by the master
         @(negedge clk); m_sda_low = 1'b1; hp();
         m_scl_release();                       // SCL high, and there it stays
         $display("T7  the master vanishes mid-byte with SCL high: the slave must give up");
         ck("T7 a transfer is still open", selected, 1);
         // Wait for the timeout plus margin.
         repeat (IDLE_C * 3) @(posedge clk);
         ck("T7 the slave aborted", n_aborts > 0, 1);
         ck("T7 and released SDA", s_sda_low, 0);
         ck("T7 and is not selected any more", selected, 0);
         // The bus must now be usable. Release the master's SDA and run a clean transaction.
         @(negedge clk); m_sda_low = 1'b0; hp();
         ck("T7 the bus is free", sda, 1);
         wr_txn(8'h01, 1, 8'h6B, nk);
         ck("T7 a new transaction was accepted", nk, 0);
         ck("T7 and it landed", reg_flat[1*8 +: 8], 8'h6B);

         // ---- T8. A repeated START mid-byte: the partial byte is discarded.
         do_reset();
         m_start();
         m_put({ADDR, 1'b0}, q);
         m_put(8'h04, q);                       // pointer = 4
         for (j = 0; j < 3; j = j + 1) m_bit(1'b1);   // three bits of a byte that never finishes
         m_restart();
         m_put({ADDR, 1'b0}, q);
         m_put(8'h04, q);                       // a fresh pointer byte
         m_put(8'h9D, q);
         m_stop();
         $display("T8  a repeated START mid-byte discards the partial byte and reframes");
         ck("T8 the byte landed at 4", reg_flat[4*8 +: 8], 8'h9D);
         ck("T8 only one write happened", n_writes, 1);
         ck("T8 one restart counted", n_restarts, 1);

         // ---- T9. The slave never drives SCL except to stretch.
         $display("T9  across every test so far, SCL was only ever pulled to stretch");
         ck("T9 no unstretched SCL drive", scl_driven_unstretched, 0);
         ck("T9 no drive while SCL was high", drive_while_high, 0);

         // ---- T10. The two SDA drivers never overlap -- checked WITH A READ IN FLIGHT.
         //
         // `n_sda_conflict` is a counter inside the DUT, so every `do_reset` clears it. An
         // earlier version of this test checked it once at the end and passed a mutant that
         // made the acknowledge block fight the transmitter on every multi-byte read -- because
         // the only reads had happened before an intervening reset, and the evidence was gone.
         //
         // A cumulative counter is only as good as the window you read it over. This test owns
         // its window: it performs the multi-byte read that creates the overlap opportunity and
         // checks immediately afterwards.
         do_reset();
         wr_txn(8'h00, 2, 8'h61, nk);                // reg0=0x61, reg1=0x62
         m_start();
         m_put({ADDR, 1'b0}, q); m_put(8'h00, q);
         m_restart();
         m_put({ADDR, 1'b1}, q);
         m_get(1'b1, rd[0]);                         // ACK: another byte follows
         m_get(1'b1, rd[1]);                         // ACK again
         m_get(1'b0, rd[2]);                         // NACK: done
         m_stop();
         $display("T10 through a three-byte read, the acknowledge never fought the transmitter");
         ck("T10 no SDA conflict", n_sda_conflict, 0);
         ck("T10 first byte", rd[0], 8'h61);
         ck("T10 second byte", rd[1], 8'h62);
         ck("T10 three bytes served", n_reads, 3);

         // ---- T11. A read of a read-only register still works.
         // Read-only means "the master may not write it", not "the master may not read it".
         do_reset();
         m_start();
         m_put({ADDR, 1'b0}, q);
         m_put(8'h02, q);                       // the read-only register
         m_restart();
         m_put({ADDR, 1'b1}, q);
         m_get(1'b0, b);
         $display("T11 a read-only register is readable: the restriction is one-directional");
         ck("T11 read back its value", b, 8'h00);
         ck("T11 and the two SDA drivers never overlapped", n_sda_conflict, 0);
         m_stop();

         // ---- T12. Back-to-back transactions with no idle between them.
         do_reset();
         wr_txn(8'h06, 1, 8'h41, nk);
         wr_txn(8'h07, 1, 8'h42, nk);
         $display("T12 back-to-back transactions with no idle time between them");
         ck("T12 reg 6", reg_flat[6*8 +: 8], 8'h41);
         ck("T12 reg 7", reg_flat[7*8 +: 8], 8'h42);
         ck("T12 two phases per transaction is not what happened", n_phases, 2);

         // ---- T13. AN IDLE BUS IS NOT AN ABANDONED TRANSFER.
         // The give-up timeout must only run while a transfer is OPEN. A counter that also ran
         // on an idle bus would fire every IDLE_CYCLES for ever -- harmless to behaviour,
         // because every block is already idle, but it makes `n_aborts` mean nothing. A
         // non-zero abort count on a shipped device is supposed to say a master died.
         do_reset();
         wr_txn(8'h00, 1, 8'h13, nk);
         repeat (IDLE_C * 6) @(posedge clk);          // six timeout periods of nothing at all
         $display("T13 an idle bus for six timeout periods produces no abort");
         ck("T13 no aborts while idle", n_aborts, 0);
         ck("T13 and the map is untouched", reg_flat[0*8 +: 8], 8'h13);
         begin
            wr_txn(8'h01, 1, 8'h14, nk);
            ck("T13 and the bus still works afterwards", reg_flat[1*8 +: 8], 8'h14);
         end

         // ---- T14. THE SCOREBOARD. Twelve random transactions against the model.
         do_reset();
         begin
            mism = 0;
            lfsr = 8'h5A;
            for (j = 0; j < 12; j = j + 1) begin
               lfsr = lfsr_step(lfsr); p8  = lfsr % 12;   // sometimes past the end of the map
               lfsr = lfsr_step(lfsr); d8  = lfsr;
               lfsr = lfsr_step(lfsr); cnt = (lfsr % 3) + 1;
               wr_txn(p8, cnt, d8, nk);
               for (k = 0; k < N_REG; k = k + 1)
                  if (reg_flat[k*8 +: 8] !== model[k]) begin
                     $display("  FAIL T14 txn %0d reg %0d: got %02x expected %02x",
                              j, k, reg_flat[k*8 +: 8], model[k]);
                     mism = mism + 1; errors = errors + 1;
                  end
            end
            $display("T14 twelve pseudo-random write transactions agree with the model");
            ck("T14 no mismatches", mism, 0);
            ck("T14 and still no SDA conflict", n_sda_conflict, 0);
            ck("T14 and no spurious aborts", n_aborts, 0);
         end

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

   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_slave_tb.vhd — the same tests in VHDL
   -- -----------------------------------------------------------------------------
   -- i2c_slave_tb.vhd
   -- The end-to-end oracle for the whole target -- the same tests in VHDL.
   --
   -- Every check is a BUS-LEVEL check. The master model reads SDA back in the high phase of
   -- every ninth pulse, so an acknowledge is OBSERVED rather than inferred; it reconstructs
   -- every read byte from the wire; and it releases SCL and waits, so a stretch is a thing that
   -- happens to it.
   --
   -- The scoreboard sequence comes from an eight-bit LFSR with a fixed seed, so it is the same
   -- stimulus as the SystemVerilog and Verilog benches run.
   -- -----------------------------------------------------------------------------
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   entity i2c_slave_tb is
   end entity i2c_slave_tb;

   architecture sim of i2c_slave_tb is

      constant HALF    : positive := 8;
      constant ADDR    : std_logic_vector(6 downto 0) := "1010000";   -- 0x50
      constant OTHERA  : std_logic_vector(6 downto 0) := "0100001";   -- 0x21
      -- 0x10: ours with only the top address bit flipped.
      constant NEARA   : std_logic_vector(6 downto 0) := "0010000";   -- 0x10
      constant N_REG   : positive := 8;
      constant RO_MASK : natural  := 4;                               -- register 2 read-only
      constant IDLE_C  : positive := 64;
      constant STALL_N : positive := 250;

      signal clk   : std_logic := '0';
      signal rst_n : std_logic := '0';

      signal m_scl_low, m_sda_low : std_logic := '0';
      signal s_scl_low, s_sda_low : std_logic;
      signal scl_dl, sda_dl : std_logic_vector(1 downto 0);
      signal scl, sda : std_logic;
      signal scl_in, sda_in, scl_rbl, sda_rbl : std_logic_vector(1 downto 0);
      signal scl_holders, sda_holders : unsigned(7 downto 0);

      signal stall_load : std_logic := '0';
      signal stall_hold : integer := 0;
      signal stall_req  : std_logic;

      signal reg_flat : std_logic_vector(8*N_REG-1 downto 0);
      signal pointer  : std_logic_vector(7 downto 0);
      signal selected, stretching : std_logic;
      signal n_phases, n_restarts, n_writes, n_refused, n_reads,
             n_aborts, n_sda_conflict : unsigned(15 downto 0);

      signal halt : boolean := false;

      -- observers
      signal drive_while_high       : integer := 0;
      signal scl_held_in_reset      : integer := 0;
      signal sda_held_in_reset      : integer := 0;
      signal scl_driven_unstretched : integer := 0;
      signal stretch_waits          : integer := 0;
      signal clr_obs                : boolean := false;

      -- the sampled line, written by the bit procedure and read by the byte procedures
      signal sampled : std_logic := '1';

   begin

      scl_dl <= s_scl_low & m_scl_low;
      sda_dl <= s_sda_low & m_sda_low;

      stall_req <= '1' when stall_hold > 0 else '0';

      stall_ctr : process (clk)
      begin
         if rising_edge(clk) then
            if rst_n = '0' then           stall_hold <= 0;
            elsif stall_load = '1' then   stall_hold <= STALL_N;
            elsif stall_hold > 0 then     stall_hold <= stall_hold - 1;
            end if;
         end if;
      end process;

      bus_model : entity work.i2c_line_model
         generic map (N_DEV => 2)
         port map (scl_drive_low => scl_dl, sda_drive_low => sda_dl,
            scl => scl, sda => sda, scl_in => scl_in, sda_in => sda_in,
            scl_released_but_low => scl_rbl, sda_released_but_low => sda_rbl,
            scl_holders => scl_holders, sda_holders => sda_holders);

      dut : entity work.i2c_slave
         generic map (MY_ADDR => ADDR, N_REG => N_REG, RO_MASK => RO_MASK,
                      IDLE_CYCLES => IDLE_C, SYNC_DEPTH => 2, CNT_W => 16)
         port map (clk => clk, rst_n => rst_n,
            scl_pin => scl, sda_pin => sda,
            scl_drive_low => s_scl_low, sda_drive_low => s_sda_low,
            stall_req => stall_req, reg_flat => reg_flat, pointer => pointer,
            selected => selected, stretching => stretching,
            n_phases => n_phases, n_restarts => n_restarts, n_writes => n_writes,
            n_refused => n_refused, n_reads => n_reads, n_aborts => n_aborts,
            n_sda_conflict => n_sda_conflict);

      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_obs)
         variable d : std_logic := '0';
      begin
         if clr_obs then
            drive_while_high <= 0; scl_held_in_reset <= 0; sda_held_in_reset <= 0;
            scl_driven_unstretched <= 0;
         elsif rising_edge(clk) then
            if rst_n = '0' then
               if s_scl_low = '1' then
                  scl_held_in_reset <= scl_held_in_reset + 1;
               end if;
               if s_sda_low = '1' then
                  sda_held_in_reset <= sda_held_in_reset + 1;
               end if;
            else
               if s_scl_low = '1' and d = '0' and scl = '1' then
                  drive_while_high <= drive_while_high + 1;
               end if;
               if s_scl_low = '1' and stretching = '0' then
                  scl_driven_unstretched <= scl_driven_unstretched + 1;
               end if;
               d := s_scl_low;
            end if;
         end if;
      end process;

      stim : process
         variable err  : integer := 0;
         variable nk   : std_logic;
         variable bb   : std_logic_vector(7 downto 0);
         variable rd0, rd1 : std_logic_vector(7 downto 0);
         variable lfsr : std_logic_vector(7 downto 0);
         variable p8, d8 : std_logic_vector(7 downto 0);
         variable cnt, mism, nacks : integer;
         variable mptr : integer;
         type mem_t is array (0 to N_REG-1) of std_logic_vector(7 downto 0);
         variable model : mem_t;

         function b2i (b : std_logic) return integer is
         begin
            if b = '1' then return 1; else return 0; end if;
         end function;

         function slice8 (f : std_logic_vector; idx : integer) return integer is
         begin
            return to_integer(unsigned(f(8*idx+7 downto 8*idx)));
         end function;

         function is_ro (idx : integer) return boolean is
         begin
            return ((RO_MASK / (2 ** idx)) mod 2) = 1;
         end function;

         function lfsr_step (x : std_logic_vector(7 downto 0))
            return std_logic_vector is
         begin
            return x(6 downto 0) & (x(7) xor x(5) xor x(4) xor x(3));
         end function;

         procedure ck (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;

         procedure hp is
         begin
            for i in 1 to HALF loop wait until rising_edge(clk); end loop;
         end procedure;

         procedure m_scl_release is
            variable guard : integer := 0;
         begin
            wait until falling_edge(clk);
            m_scl_low <= '0';
            wait for 1 ns;
            if scl /= '1' then
               stretch_waits <= stretch_waits + 1;
               while scl /= '1' and guard < 40000 loop
                  wait until rising_edge(clk); guard := guard + 1;
               end loop;
               if guard >= 40000 then
                  report "  FAIL the slave never released SCL" severity note;
                  err := err + 1;
               end if;
            end if;
            hp;
         end procedure;

         procedure m_scl_pull is
         begin
            wait until falling_edge(clk); m_scl_low <= '1'; hp;
         end procedure;

         procedure m_start is
         begin
            wait until falling_edge(clk); m_scl_low <= '0'; m_sda_low <= '0'; hp;
            wait until falling_edge(clk); m_sda_low <= '1'; hp;
            m_scl_pull;
         end procedure;

         procedure m_restart is
         begin
            wait until falling_edge(clk); m_sda_low <= '0'; hp;
            m_scl_release;
            wait until falling_edge(clk); m_sda_low <= '1'; hp;
            m_scl_pull;
         end procedure;

         procedure m_stop is
         begin
            wait until falling_edge(clk); m_sda_low <= '1'; hp;
            m_scl_release;
            wait until falling_edge(clk); m_sda_low <= '0'; hp;
         end procedure;

         -- One bit: the master drives, then SAMPLES the line in the high phase.
         procedure m_bit (sda_low : std_logic) is
         begin
            wait until falling_edge(clk); m_sda_low <= sda_low; hp;
            m_scl_release;
            sampled <= sda;
            wait for 1 ns;
            m_scl_pull;
         end procedure;

         -- A byte the master writes; `nack` is the ninth bit it observed.
         procedure m_put (d : std_logic_vector(7 downto 0); nack : out std_logic) is
         begin
            for i in 7 downto 0 loop m_bit(not d(i)); end loop;
            m_bit('0');
            nack := sampled;
         end procedure;

         -- A byte the master reads, reconstructed from the wire, then its answer.
         procedure m_get (ack : std_logic; d : out std_logic_vector(7 downto 0)) is
            variable v : std_logic_vector(7 downto 0) := (others => '0');
         begin
            for i in 7 downto 0 loop
               m_bit('0');
               v(i) := sampled;
            end loop;
            m_bit(ack);
            d := v;
         end procedure;

         procedure do_reset is
         begin
            wait until falling_edge(clk);
            rst_n <= '0'; m_scl_low <= '0'; m_sda_low <= '0'; stall_load <= '0';
            clr_obs <= true; stretch_waits <= 0; wait for 1 ns; clr_obs <= false;
            for i in 1 to 4 loop wait until rising_edge(clk); end loop;
            wait until falling_edge(clk); rst_n <= '1';
            hp; wait for 1 ns;
            model := (others => (others => '0'));
            mptr  := 0;
         end procedure;

         -- A write transaction: START, address, pointer, n data bytes, STOP.
         procedure wr_txn (ptr : std_logic_vector(7 downto 0); n : integer;
                           d0 : std_logic_vector(7 downto 0); nacks_o : out integer) is
            variable q : std_logic;
            variable v : std_logic_vector(7 downto 0);
         begin
            nacks_o := 0;
            m_start;
            m_put(ADDR & '0', q);
            if q = '1' then
               report "  FAIL address NACKed" severity note; err := err + 1;
            end if;
            -- A pointer byte must ALWAYS be acknowledged: it writes no register, so no
            -- register's access rules apply to it.
            m_put(ptr, q);
            if q = '1' then
               report "  FAIL pointer byte NACKed" severity note; err := err + 1;
            end if;
            mptr := to_integer(unsigned(ptr));
            for i in 0 to n-1 loop
               v := std_logic_vector(unsigned(d0) + to_unsigned(i, 8));
               m_put(v, q);
               if q = '1' then nacks_o := nacks_o + 1; end if;
               if not is_ro(mptr mod N_REG) then
                  model(mptr mod N_REG) := v;
                  mptr := (mptr + 1) mod 256;
               end if;
            end loop;
            m_stop;
         end procedure;

      begin
         report "=== i2c_slave: the whole target, checked from the wire ===" severity note;

         -- T1.
         wait until falling_edge(clk);
         rst_n <= '0'; m_scl_low <= '0'; m_sda_low <= '0';
         clr_obs <= true; wait for 1 ns; clr_obs <= false;
         for i in 1 to 30 loop wait until rising_edge(clk); end loop;
         report "T1  a target held in reset releases both lines" severity note;
         ck("T1 SCL never pulled in reset", scl_held_in_reset, 0);
         ck("T1 SDA never pulled in reset", sda_held_in_reset, 0);
         ck("T1 SCL idles high", b2i(scl), 1);
         ck("T1 SDA idles high", b2i(sda), 1);

         -- T2.
         do_reset;
         wr_txn(x"00", 3, x"11", nacks);
         report "T2  a three-byte write: every byte acknowledged on the wire" severity note;
         ck("T2 reg 0", slice8(reg_flat, 0), 16#11#);
         ck("T2 reg 1", slice8(reg_flat, 1), 16#12#);
         ck("T2 reg 2 is read-only", slice8(reg_flat, 2), 0);
         ck("T2 exactly one NACK, at the read-only register", nacks, 1);
         ck("T2 two writes accepted", to_integer(n_writes), 2);
         ck("T2 one refusal", to_integer(n_refused), 1);

         -- T3. THE END-TO-END PROOF.
         do_reset;
         wr_txn(x"05", 1, x"A7", nacks);
         ck("T3 the write was accepted", nacks, 0);
         m_start;
         m_put(ADDR & '0', nk);
         m_put(x"05", nk);
         m_restart;
         m_put(ADDR & '1', nk);
         m_get('0', bb);
         report "T3  write, repeated START, read: the byte comes back off the wire"
                severity note;
         ck("T3 the byte read equals the byte written", to_integer(unsigned(bb)), 16#A7#);
         ck("T3 and the two SDA drivers never overlapped", to_integer(n_sda_conflict), 0);
         m_stop;

         -- T4.
         do_reset;
         wr_txn(x"00", 2, x"31", nacks);
         m_start;
         m_put(ADDR & '0', nk); m_put(x"00", nk);
         m_restart;
         m_put(ADDR & '1', nk);
         m_get('1', rd0);
         m_get('0', rd1);
         m_stop;
         report "T4  a two-byte read walks the pointer forward" severity note;
         ck("T4 first byte", to_integer(unsigned(rd0)), 16#31#);
         ck("T4 second byte", to_integer(unsigned(rd1)), 16#32#);
         ck("T4 two reads counted", to_integer(n_reads), 2);
         ck("T4 and the two SDA drivers never overlapped", to_integer(n_sda_conflict), 0);

         -- T5.
         do_reset;
         m_start;
         m_put(OTHERA & '0', nk);
         report "T5  another device's address is met with silence, not a NACK we drove"
                severity note;
         ck("T5 the ninth bit was left high", b2i(nk), 1);
         ck("T5 we were never selected", b2i(selected), 0);
         ck("T5 no phase opened", to_integer(n_phases), 0);
         m_put(x"77", nk);
         ck("T5 and a data byte is ignored too", b2i(nk), 1);
         ck("T5 nothing was written", slice8(reg_flat, 0), 0);
         m_stop;

         -- And again one bit away. 0x21 differs from 0x50 in four bits, so a decoder that
         -- had stopped comparing any ONE of them would still reject it -- the bits it does
         -- still compare are enough on their own. A decoder that had lost the top address
         -- bit answers to 0x10, and only an address exactly one bit from ours can show
         -- that. The claim this test makes is not "some wrong address is refused" but
         -- "the address comparison is what gates the acknowledge, the phase and the
         -- register file" -- which needs the one address that isolates a single bit.
         -- The seven-bit sweep itself belongs to Chapter 18.4; here it is composition.
         do_reset;
         m_start;
         m_put(NEARA & '0', nk);
         report "T5  a one-bit near miss is refused the same way, and changes nothing"
                severity note;
         ck("T5 the near miss was not acknowledged", b2i(nk), 1);
         ck("T5 it never selected us", b2i(selected), 0);
         ck("T5 no phase opened on it", to_integer(n_phases), 0);
         m_put(x"88", nk);
         ck("T5 its data byte was ignored too", b2i(nk), 1);
         ck("T5 and the register file is untouched", slice8(reg_flat, 0), 0);
         ck("T5 the two SDA drivers never overlapped", to_integer(n_sda_conflict), 0);
         m_stop;

         -- T6.
         do_reset;
         m_start;
         wait until falling_edge(clk); stall_load <= '1';
         wait until rising_edge(clk);
         wait until falling_edge(clk); stall_load <= '0';
         m_put(ADDR & '0', nk);
         ck("T6 the address was still acknowledged", b2i(nk), 0);
         ck("T6 the slave is stretching", b2i(stretching), 1);
         m_put(x"03", nk);
         ck("T6 the master had to wait", stretch_waits, 1);
         m_put(x"5E", nk);
         m_stop;
         report "T6  a stretch delays the master and the transfer completes correctly"
                severity note;
         ck("T6 the byte landed", slice8(reg_flat, 3), 16#5E#);
         ck("T6 no abort happened", to_integer(n_aborts), 0);

         -- T7. THE SLAVE-SIDE WEDGE, AND THE RECOVERY.
         do_reset;
         m_start;
         m_put(ADDR & '0', nk);
         for i in 0 to 3 loop m_bit('1'); end loop;
         wait until falling_edge(clk); m_sda_low <= '1'; hp;
         m_scl_release;
         report "T7  the master vanishes mid-byte with SCL high: the slave must give up"
                severity note;
         ck("T7 a transfer is still open", b2i(selected), 1);
         for i in 1 to IDLE_C * 3 loop wait until rising_edge(clk); end loop;
         if n_aborts > 0 then ck("T7 the slave aborted", 1, 1);
         else                 ck("T7 the slave aborted", 0, 1); end if;
         ck("T7 and released SDA", b2i(s_sda_low), 0);
         ck("T7 and is not selected any more", b2i(selected), 0);
         wait until falling_edge(clk); m_sda_low <= '0'; hp;
         ck("T7 the bus is free", b2i(sda), 1);
         wr_txn(x"01", 1, x"6B", nacks);
         ck("T7 a new transaction was accepted", nacks, 0);
         ck("T7 and it landed", slice8(reg_flat, 1), 16#6B#);

         -- T8.
         do_reset;
         m_start;
         m_put(ADDR & '0', nk);
         m_put(x"04", nk);
         for i in 0 to 2 loop m_bit('1'); end loop;
         m_restart;
         m_put(ADDR & '0', nk);
         m_put(x"04", nk);
         m_put(x"9D", nk);
         m_stop;
         report "T8  a repeated START mid-byte discards the partial byte and reframes"
                severity note;
         ck("T8 the byte landed at 4", slice8(reg_flat, 4), 16#9D#);
         ck("T8 only one write happened", to_integer(n_writes), 1);
         ck("T8 one restart counted", to_integer(n_restarts), 1);

         -- T9.
         report "T9  across every test so far, SCL was only ever pulled to stretch"
                severity note;
         ck("T9 no unstretched SCL drive", scl_driven_unstretched, 0);
         ck("T9 no drive while SCL was high", drive_while_high, 0);

         -- T10. Checked WITH A READ IN FLIGHT. `n_sda_conflict` is a counter inside the DUT, so
         -- every reset clears it; an earlier version checked it once at the end and passed a
         -- mutant that made the acknowledge block fight the transmitter on every multi-byte
         -- read, because the only reads had happened before an intervening reset.
         do_reset;
         wr_txn(x"00", 2, x"61", nacks);
         m_start;
         m_put(ADDR & '0', nk); m_put(x"00", nk);
         m_restart;
         m_put(ADDR & '1', nk);
         m_get('1', rd0);
         m_get('1', rd1);
         m_get('0', bb);
         m_stop;
         report "T10 through a three-byte read, the acknowledge never fought the transmitter"
                severity note;
         ck("T10 no SDA conflict", to_integer(n_sda_conflict), 0);
         ck("T10 first byte", to_integer(unsigned(rd0)), 16#61#);
         ck("T10 second byte", to_integer(unsigned(rd1)), 16#62#);
         ck("T10 three bytes served", to_integer(n_reads), 3);

         -- T11.
         do_reset;
         m_start;
         m_put(ADDR & '0', nk);
         m_put(x"02", nk);
         m_restart;
         m_put(ADDR & '1', nk);
         m_get('0', bb);
         report "T11 a read-only register is readable: the restriction is one-directional"
                severity note;
         ck("T11 read back its value", to_integer(unsigned(bb)), 0);
         ck("T11 and the two SDA drivers never overlapped", to_integer(n_sda_conflict), 0);
         m_stop;

         -- T12.
         do_reset;
         wr_txn(x"06", 1, x"41", nacks);
         wr_txn(x"07", 1, x"42", nacks);
         report "T12 back-to-back transactions with no idle time between them" severity note;
         ck("T12 reg 6", slice8(reg_flat, 6), 16#41#);
         ck("T12 reg 7", slice8(reg_flat, 7), 16#42#);
         ck("T12 two phases per transaction is not what happened",
            to_integer(n_phases), 2);

         -- T13. AN IDLE BUS IS NOT AN ABANDONED TRANSFER. The give-up timeout must only run
         -- while a transfer is OPEN; a counter that also ran on an idle bus would fire every
         -- IDLE_CYCLES for ever and make `n_aborts` mean nothing.
         do_reset;
         wr_txn(x"00", 1, x"13", nacks);
         for i in 1 to IDLE_C * 6 loop wait until rising_edge(clk); end loop;
         report "T13 an idle bus for six timeout periods produces no abort" severity note;
         ck("T13 no aborts while idle", to_integer(n_aborts), 0);
         ck("T13 and the map is untouched", slice8(reg_flat, 0), 16#13#);
         wr_txn(x"01", 1, x"14", nacks);
         ck("T13 and the bus still works afterwards", slice8(reg_flat, 1), 16#14#);

         -- T14. THE SCOREBOARD.
         do_reset;
         mism := 0;
         lfsr := x"5A";
         for i in 0 to 11 loop
            lfsr := lfsr_step(lfsr);
            p8   := std_logic_vector(to_unsigned(to_integer(unsigned(lfsr)) mod 12, 8));
            lfsr := lfsr_step(lfsr);
            d8   := lfsr;
            lfsr := lfsr_step(lfsr);
            cnt  := (to_integer(unsigned(lfsr)) mod 3) + 1;
            wr_txn(p8, cnt, d8, nacks);
            for r in 0 to N_REG-1 loop
               if slice8(reg_flat, r) /= to_integer(unsigned(model(r))) then
                  report "  FAIL T14 reg " & integer'image(r) & " mismatch" severity note;
                  mism := mism + 1; err := err + 1;
               end if;
            end loop;
         end loop;
         report "T14 twelve pseudo-random write transactions agree with the model"
                severity note;
         ck("T14 no mismatches", mism, 0);
         ck("T14 and still no SDA conflict", to_integer(n_sda_conflict), 0);
         ck("T14 and no spurious aborts", to_integer(n_aborts), 0);

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

   end architecture sim;

All three languages finish at the same instant:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
i2c_slave_tb.sv    ALL CHECKS PASSED    $finish at 253955000
i2c_slave_tb.v     ALL CHECKS PASSED    $finish at 253955000
i2c_slave_tb.vhd   ALL CHECKS PASSED    stopped at 253955 ns

8. Mutation Testing

Fourteen valid mutants, fourteen killed, one discarded as equivalent — and three of the fourteen only died after a change, two to the design and one to the bench.

#Injected defectExpected detectionResult
M1the pointer byte answered with the register file's permissionT2, T14KILLED (8)
M2every byte acknowledged, so a read-only write is silently discardedT2KILLED (2)
M3the address byte answered with the register file's permission tooT2, T14KILLED (8)
M4the give-up timeout never firesT7KILLED (3)
M5the abort is generated but never reaches the blocksT7KILLED (2)
M6the idle counter is not reset by a falling SCLeverythingKILLED (117)
M7the idle counter runs with no transfer openT13 newKILLED (2)
M8the transmitter is disconnected from the padT3, T4KILLED (7)
M9the acknowledge is disconnected from the padT2 onwardKILLED (46)
M10the acknowledge slot armed mid-read: the two drivers fightT10 rewrittenKILLED (5)
M11the register file is told the pointer byte is dataT2 onwardKILLED (62)
M12the register file is given the synthesised abortT7, T14KILLED (64)
M13the transmitter is fed the pointer instead of the dataT3KILLED (7)
M14the transaction layer is denied the restart pulseT8, after a rewireKILLED (2)
—reset asserts the abort—EQUIVALENT — discarded
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
baseline: PASS   (verified before injecting anything)
valid mutants: 14   killed: 14   survived: 0   equivalent: 1   invalid: 1
restored: PASS

The equivalent: an abort at reset changes nothing

abort's only consumer is stop_eff, which feeds blocks that are themselves held in reset — and abort self-clears on the first clock after release. The submodules do see one cycle of a synthesised STOP with reset released, and they respond by setting state they already hold to the values it already has. No counter moves, because the only framing-driven counters increment on an abandoned byte and there is none. Inert, so discarded rather than counted.

M10 survived first, and the testbench was the problem

The conflict counter lives inside the DUT, so every do_reset clears it. The first version of T10 checked it once, near the end — by which time the only multi-byte reads had happened several resets earlier, and the evidence was gone.

M14 survived because a connection had no observable effect

restart_pulse reached 18.10 and fed exactly one thing: a counter that this file did not read, because it reported 18.3's identical count instead. Tying the connection to zero therefore changed nothing anybody could see.

The two numbers are equal by construction — both count the same pulse — so the choice of which to expose was arbitrary until the mutant made it matter:

And the numbers worth reading are the small ones

M6's 117 and M12's 64 say the injection broke everything downstream. M5's 2, M7's 2 and M14's 2 are the informative ones: exactly the checks whose job it was, and no collateral noise.

9. Verification Connection — What an Integration Bench Must Assert

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Not synthesisable. Icarus rejects SVA, so these state the properties the bench
// checks procedurally -- and every one is a TOP-LEVEL property that no block below
// could possibly assert about itself.

// The two SDA drivers are mutually exclusive.
property one_sda_driver;
   @(posedge clk) disable iff (!rst_n) !(ack_sda_low && tx_sda_low);
endproperty

// A device in reset holds nothing.
property reset_releases_everything;
   @(posedge clk) !rst_n |-> (!scl_drive_low && !sda_drive_low);
endproperty

// A pointer byte is always acknowledged.
property pointer_always_acked;
   @(posedge clk) disable iff (!rst_n)
      (byte_done_any && rx_is_pointer) |-> ack_en;
endproperty

// The give-up timeout only ever runs during an open transfer.
property timeout_needs_a_transfer;
   @(posedge clk) disable iff (!rst_n) (idle_cnt != 0) |-> bus_active;
endproperty

The first is the one to carry into a real project, and it is worth writing as a formal property rather than a counter: n_sda_conflict proves the overlap did not happen in the runs you did, and a proof shows it cannot happen in the runs you did not.

10. FPGA and ASIC Implications

Two pull-down enables leave this module, and nothing else. There is no tri-state, no drive-high, no bidirectional signal — scl_pin and sda_pin come in, scl_drive_low and sda_drive_low go out. Turning those four into two actual pads is Module 19's subject, and keeping the boundary this narrow is what makes that a separable job.

Register the drives at the boundary. Both outputs are already flop outputs in every path except the SDA OR gate, which is one level of logic between two flops. A glitch there would be a spurious pull-down on a shared bus, so if the pad is far away, add the register rather than trusting the OR.

IDLE_CYCLES is a system-level number, not a preference. It must exceed the longest single SCL HIGH phase the device must tolerate, which at 100 kHz and a 50 MHz clock is around 250 cycles for a nominal half-period — so the default of 512 gives roughly a 2× margin at Standard-mode. A device that must also work behind a very slow bit-banging master needs more, and the cost of getting it wrong is asymmetric: too large merely delays a recovery, too small breaks working transfers.

The counters are the debug interface. n_refused says a master is writing where it should not; n_aborts says a master died mid-transfer; n_sda_conflict must be zero and its being non-zero is an RTL bug, not a bus event. CNT_W exists so a production build can shrink or remove all of them — but n_sda_conflict is the one to keep, because nothing else in the system can report it.

stall_req is the only application input, and it shares this module's clock. An application on another clock needs a synchroniser there, and it is the one place in a slave where a missing synchroniser produces a bus-level failure rather than a local one.

11. Debugging — The Device That Answers Its Address and Nothing Else

Symptom. A device works in bring-up and then, on one board, stops accepting writes. It still acknowledges its address — a bus scan finds it — but every byte after the address is NACKed. Power-cycling fixes it. It comes back after a few hours, or after a particular sequence of operations, and never in the lab.

What it is not. Not the address decode, which is proven by the scan. Not the register file, whose contents are correct when read. Not a bus fault, because other devices are fine.

What it is. The acknowledge policy answered the pointer byte with the register file's write permission, and the pointer had come to rest on a read-only register. From that moment every pointer byte is refused, so the pointer can never be moved off the read-only address, so every pointer byte is refused.

Why it is intermittent, and why power-cycling fixes it. The pointer only lands on the read-only address for particular access patterns — a sequential write that ends exactly there, or a read that auto-increments onto it. Reset clears the pointer to zero, which is writable, so the device recovers and the evidence is destroyed.

The fix is the third case in §2. The test is T14's scoreboard, whose pointers deliberately run past the end of the map and across the read-only register.

12. Common Misconceptions

"A top level is just wiring, so it needs no verification of its own." It carries the acknowledge policy, the arbitration, the reset guarantee and the recovery. Two genuine defects in this one were invisible to every block below.

"A read-only register should refuse any byte that addresses it." It should refuse a write to it. Refusing the pointer byte that names it makes the device unreachable. §2.

"An OR gate is the correct way to merge two open-drain drivers, so nothing can go wrong." It is correct electrically and hides a direction bug perfectly. The overlap has to be counted. §3.

"Reset means the state machine goes to idle." For a device on a shared bus it means every drive is released. A target that holds a line down in reset takes the bus with it. §4.

"A slave cannot break the bus." It can hold SDA low for ever if a master dies mid-byte, and then no START is possible. §5.

"A zero from a monitor is good news." A zero from an unproven monitor is no news. One mutation in this set exists purely to make the conflict counter increment. §3, §8.

13. Reason It Through

Why can the acknowledge policy not live in 18.5 or 18.9?

Because it needs the address match, the byte's role in the transaction, and the register file's permission — three facts that only coexist at the top. §1, §2.

A device answers its address and NACKs everything after it, intermittently, and a power cycle fixes it. What is the mechanism?

The pointer byte is answered with the register file's write permission and the pointer is resting on a read-only register, so it can never be moved. Reset clears the pointer and destroys the evidence. §11.

Why is an OR of two pull-downs dangerous even though it is electrically right?

Because a direction bug produces a low line and a plausible-looking zero, with no error anywhere. The overlap must be counted, and the counter must be proven to count. §3.

What single thing can a well-behaved target do to lock out every device on the bus?

Hold SDA low when a master dies mid-byte. A START needs a falling SDA, so no transfer can begin. §5.

Why does the recovery synthesise a STOP rather than add a recovery path?

Because every block already abandons cleanly on a STOP. The recovery supplies a reason to take a path that exists and is already tested. §5.

One mutant survived because a connection had no observable effect. Why was the fix a rewire rather than a deletion?

Because the signal does matter one level down — it feeds 18.10's own counter. Exposing that counter instead of the identical one from 18.3 makes the wire load-bearing. §8.

14. Understanding Check

15. Summary

A top level is not wiring. It owns the acknowledge policy, the SDA arbitration, the reset guarantee and the error recovery — four decisions that no block below can make, and the source of both defects this chapter's bench found.

The acknowledge policy has three cases. Answering the pointer byte with the register file's write permission makes the device permanently unreachable the moment the pointer rests on a read-only register, because the byte that would move it is the byte that gets refused.

An OR of two pull-downs is electrically correct and hides a direction bug perfectly, so the overlap is counted — and one mutation exists purely to prove the counter counts.

Reset means every drive released, not every state machine idle. A target holding a line down in reset takes the whole bus with it, and no device can lift it.

The one failure a target can inflict on everybody is holding SDA low when a master dies mid-byte: a START needs a falling SDA, so nothing can begin again. The slave-side recovery is a timeout, and it needs no other master to be alive.

The recovery synthesises the STOP the master failed to send, so every block abandons through the path it already has — except the register file, which receives no framing at all and keeps its pointer.

The bench checks the bus, not the design's opinion of itself. The master samples SDA in every ninth pulse, reconstructs every read byte from the wire, and blocks when the slave stretches.

Fourteen valid mutants, fourteen killed, one equivalent discarded — and three died only after a change: two to the design, one to the bench.

A cumulative counter is only as good as the window you read it over. The stimulus for M10 existed and the counter recorded it; an intervening reset erased it before anyone looked.

And if no test can tell a connection's presence from its absence, either the connection or the test is wrong — deletion in 18.10, a rewire here, one rule.

16. Module 18 Complete

Eleven chapters and ten designs — 18.1 is the one with no RTL in it. Sixty HDL files: thirty designs and thirty testbenches, ten of each in SystemVerilog, Verilog-2001 and VHDL, every trio finishing at the same instant.

What the module established, in the order it became true:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
18.1   a target reacts to edges it does not generate and cannot postpone except by stretching
18.2   two asynchronous lines become synchronous events, and framing is an SDA edge
       qualified by an SCL LEVEL
18.3   START and STOP, and the partial byte that framing destroys
18.4   the address, the direction, and the silence that a mismatch requires
18.5   the ninth slot on a write, which the slave owns
18.6   received bytes, sampled on the rising edge
18.7   transmitted bytes, placed on the falling edge, with a one sent by releasing
18.8   the ninth slot on a read, which the master owns
18.9   the register map, and a pointer that survives a repeated START
18.10  protocol state versus application state, and the wait state
18.11  the four decisions that exist only at the top

And a verification standard that produced a finding in eight of the eleven chapters. The exceptions are 18.1, which has no RTL, and 18.4 and 18.8, which killed every mutant on the first pass.

The survivor taxonomy grew from three shapes to six. A tied-off input; an output with no consumer; the untested half of a symmetric property; two features never tested together; a construct defending against an unreachable case; and a cumulative counter read over the wrong window.

Two of the six are findable mechanically — grep a bench for constant port connections, and grep for outputs that appear only in a port map. The other four are not greppable, and the module says so each time it names one: they are questions to ask (is there a test where both features are active?) and obligations to discharge (state the condition under which this line changes an outcome, then show it cannot hold). A checklist of four questions is worth having; pretending they are tools is not.

17. What Comes Next

Everything built so far reads scl_pin and sda_pin and drives scl_drive_low and sda_drive_low. Those four signals are not pads — they are an abstraction that has been carefully maintained for eleven chapters precisely so that this next question could be asked on its own.

Module 19 answers it: how an open-drain pad is actually coded so a synthesis tool infers a bidirectional buffer rather than a latch, what a pull-up has to be for a given bus capacitance, how deep a synchroniser needs to be, and why a glitch filter is not optional on a line that arrives from outside the chip.

None of it changes a single decision made in this module. That is the point of the boundary.

Continue learning