Skip to content
VLSI Mentor

I²C · Module 17

Deriving the Master FSM — The State Machine Designed Last

There is no state machine in the finished master's top level. Every state already exists inside a block that needed it, and integration is wiring plus three decisions. Shows what integration verification catches that no block bench can, and proves one mutant equivalent by demonstrating the contention it changes never occurs.

Chapter 17.1 claimed the FSM should be designed last, and that a master written FSM-first gets rewritten. This chapter is where that claim is settled.

1. The Claim: There Is No State Machine in This File

Every state in the finished master already exists inside a block that needed it. The top level is wiring plus three decisions.

That is not an accident of this particular design. It is what happens when the decomposition follows the protocol's four time bases rather than its vocabulary:

Time baseAdvances onLives in
TIMEthe divider ticki2c_scl_gen's phase
BITthe SCL edge, read backi2c_bit_engine
BYTEthe byte boundaryi2c_byte_engine's slot index
TRANSACTIONthe host commandi2c_txn_ctrl's phase

Those four change on unrelated events. A single FSM holding all four must take the product of their states and gets four sets of transitions out of each one — and the counters that inevitably get bolted on to make it tractable are the other three machines, admitted late and without their invariants.

2. The Three Decisions This File Actually Makes

Decision 1 — who owns SDA

Four blocks want it: the framer, the bit engine, the byte engine's acknowledge (which drives through the bit engine), and recovery. Chapter 17.4's arbiter resolves them by priority and reports any overlap as the design error it is.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
owner 0  the framer      -- highest: a START must pre-empt a data bit
owner 1  the bit engine
owner 2  (unused here -- the byte engine drives through the bit engine)
owner 3  recovery

Decision 2 — who owns SCL

Three contributors, wired-AND locally exactly as the bus would do it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
scl_drive_low = f_scl_low | g_scl_low | rec_scl_low

The handover overlaps — Chapter 17.8's controller asserts scl_yield and the generator's enable in the same cycle — so the line never rises in between. Chapter 17.8 §3 explained why the direction of that overlap matters: if SCL rises between owners, the framer's next act is to pull SDA low, and that is a START rather than the intended STOP.

Decision 3 — when recovery may run

Never during a transfer. Chapter 17.11 §10 debugged exactly this: both lines are low most of the time while clocking, so a diagnosis taken mid-transfer reads a transmitted zero as a stuck line and clocks nine pulses into a live byte.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
.start_recovery(start_recovery && !in_transfer)

One AND gate, and it is the difference between a recovery feature and a corruption source.

And the pins are three signals per line

drive_low out, the line in, and the pad's output enable derived from the first. An inout in synthesizable RTL can be neither synthesized nor simulated against a second driver — Chapter 17.1 §4's three-signal form, now at the top level where it meets the pad.

3. The Whole Master

A block diagram of the complete master. On the left a host register interface feeds a two-phase sequencer, which feeds a transaction controller. The transaction controller drives a byte engine, which drives a bit engine, and also drives a framing sequencer directly. The bit engine and framer both request SDA through an arbiter in the middle right, which produces the single SDA drive-low output to the pad. The framer, the SCL generator and recovery all contribute to an OR gate producing the SCL drive-low output. At the bottom a bus feedback block reads both lines back and returns stretch information to the generator and arbitration loss to the transaction controller, and an error manager receives the reports and can request recovery, gated by an AND with the not-in-transfer condition.Host registers17.2Two-phase seq17.9Transaction ctrl17.8Byte engine17.7Bit engine17.6Framer17.5SCL generator17.3SDA arbiterdecision 1 — 17.4SCL wired-ANDdecision 2Padsdrive_low + line_inBus feedback17.10Error manager17.11ANDnot-in-transferdecision 312
Figure 1 — the integrated master, with the three integration decisions marked. Every box is a chapter. The only logic at this level is the SDA arbiter, the SCL wired-AND, and the one AND gate that gates recovery on the transfer state.

4. What Stays Outside

Naming what is not at this level is what prevents state explosion:

Stays inside a blockBecause
timing countersthe generator owns every Table 10 number
the shift register and slot indexthe byte engine owns bit position
the synchronisersthey belong at the pad boundary
host data storagethe register file owns the buffers
the pad's output enablederived from drive_low, not a separate decision

A top level that reached into any of these would be re-implementing a block it already instantiates.

5. The Master, in Three Languages

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_master.sv — the whole master, and the FSM designed last
   // -----------------------------------------------------------------------------
   // i2c_master.sv
   // The whole master, and the FSM that was designed LAST.
   //
   // THE CLAIM OF THIS CHAPTER. There is no state machine in this file. Every state in the
   // finished master already exists inside a block that needed it, and the top level is
   // wiring plus three decisions. That is not an accident of this particular design: it is
   // what happens when the decomposition follows the protocol's four TIME BASES rather than
   // its vocabulary.
   //
   //   the divider tick          -> i2c_scl_gen's phase        (Chapter 17.3)
   //   the SCL edge, read back   -> i2c_bit_engine             (Chapter 17.6)
   //   the byte boundary         -> i2c_byte_engine's slot     (Chapter 17.7)
   //   the host command          -> i2c_txn_ctrl's phase       (Chapter 17.8)
   //
   // Those four change on unrelated events. A single FSM holding all four has to take the
   // product of their states and gets four sets of transitions out of each one, and the
   // counters that inevitably get bolted on to make it tractable ARE the other three
   // machines, admitted late and without their invariants.
   //
   // THE THREE DECISIONS THIS FILE ACTUALLY MAKES:
   //
   //   1. WHO OWNS SDA. Four blocks want it -- the framer, the bit engine, the byte
   //      engine's acknowledge, and recovery -- and Chapter 17.4's arbiter resolves them by
   //      priority while reporting any overlap as the design error it is.
   //
   //   2. WHO OWNS SCL. The framer and the generator, handed over with a one-cycle overlap
   //      in both directions so the line never rises in between. Chapter 17.8's controller
   //      sequences the handover because it is the only block that knows which is needed.
   //
   //   3. WHEN RECOVERY MAY RUN. Never during a transfer, because Chapter 17.11's stuck-line
   //      detector cannot distinguish a held line from a clocked one while a transfer is in
   //      progress -- both lines are low most of the time.
   //
   // AND THE PINS ARE THREE SIGNALS PER LINE, not one. `drive_low` out, the line in, and the
   // pad's output enable derived from the first. An `inout` in synthesizable RTL cannot be
   // driven by two blocks in simulation without resolution hazards and cannot be synthesized
   // at all; Chapter 17.4 §I makes the argument.
   // -----------------------------------------------------------------------------

   module i2c_master #(
      parameter int N_LOW    = 8,
      parameter int N_HIGH   = 4,
      parameter int N_SU     = 2,
      parameter int N_SAMP   = 2,
      parameter int N_HD_STA = 3,
      parameter int N_SU_STA = 3,
      parameter int N_SU_STO = 3,
      parameter int N_BUF    = 3,
      parameter int STRETCH_LIMIT = 0,
      parameter int N_PULSES = 9,
      parameter int N_BYTES  = 8,
      parameter int CNT_W    = 16
   ) (
      input  logic            clk,
      input  logic            rst_n,

      // ---- the host register bus ----------------------------------------------
      input  logic [3:0]      reg_addr,
      input  logic [7:0]      reg_wdata,
      input  logic            reg_we,
      input  logic            reg_re,
      output logic [7:0]      reg_rdata,

      // ---- the bus pins: three signals per line -------------------------------
      output logic           scl_drive_low,
      input  logic            scl_in,
      output logic           sda_drive_low,
      input  logic            sda_in,

      // ---- recovery, which the host must ask for explicitly -------------------
      input  logic            start_recovery,
      output logic           recovering,
      output logic           recovered,
      output logic           escalate,

      // ---- observability ------------------------------------------------------
      output logic           txn_busy,
      output logic           txn_done,
      output logic           txn_ok,
      output logic [5:0]      err,
      output logic           arb_lost,
      output logic           stretch_seen,
      output logic [CNT_W-1:0] transactions,
      output logic [CNT_W-1:0] stretch_cycles,
      output logic [CNT_W-1:0] arb_losses,
      output logic [CNT_W-1:0] sda_conflicts,
      output logic [3:0]      txn_state
   );

      // ---- Chapter 17.3: the clock -------------------------------------------
      logic g_scl_low, drive_point, sample_point, g_rise, g_fall, g_stretch;
      logic [CNT_W-1:0] g_scyc, g_bits;
      logic [1:0] g_phase;
      logic gen_enable, gen_idle_low;

      i2c_scl_gen #(.N_LOW(N_LOW), .N_HIGH(N_HIGH), .N_SU(N_SU), .N_SAMP(N_SAMP),
                    .CNT_W(CNT_W)) u_scl (
         .clk(clk), .rst_n(rst_n), .enable(gen_enable), .idle_low(gen_idle_low),
         .scl_in(scl_in), .scl_drive_low(g_scl_low),
         .drive_point(drive_point), .sample_point(sample_point),
         .scl_rising(g_rise), .scl_falling(g_fall),
         .stretching(g_stretch), .stretch_cycles(g_scyc),
         .bits_generated(g_bits), .phase(g_phase));

      // ---- Chapter 17.5: framing ---------------------------------------------
      logic f_sda_req, f_sda_bit, f_scl_low, f_busy, f_done, f_bus_free, f_started, f_sw;
      logic [CNT_W-1:0] n_sta, n_rs, n_sto;
      logic [3:0] f_state;
      logic do_start, do_restart, do_stop, scl_yield;

      i2c_framer #(.N_HD_STA(N_HD_STA), .N_SU_STA(N_SU_STA), .N_SU_STO(N_SU_STO),
                   .N_BUF(N_BUF), .N_SU_DAT(N_SU), .CNT_W(CNT_W)) u_fr (
         .clk(clk), .rst_n(rst_n),
         .do_start(do_start), .do_restart(do_restart), .do_stop(do_stop),
         .scl_in(scl_in), .sda_in(sda_in), .scl_yield(scl_yield),
         .sda_req(f_sda_req), .sda_bit(f_sda_bit), .scl_drive_low(f_scl_low),
         .busy(f_busy), .done(f_done), .bus_free(f_bus_free), .started(f_started),
         .stretch_wait(f_sw), .starts(n_sta), .restarts(n_rs), .stops(n_sto),
         .state(f_state));

      // ---- Chapters 17.6 and 17.7: the datapath ------------------------------
      logic b_sda_req, b_sda_bit, b_driving, b_busy, b_ack, b_ackv, b_done;
      logic [7:0] b_rx;
      logic [3:0] b_bidx;
      logic [CNT_W-1:0] b_bytes, b_acks, b_nacks;
      logic byte_go, byte_dir_write, byte_ack_send;
      logic [7:0] byte_tx;

      i2c_byte_engine #(.CNT_W(CNT_W)) u_by (
         .clk(clk), .rst_n(rst_n),
         .drive_point(drive_point), .sample_point(sample_point),
         .go(byte_go), .dir_write(byte_dir_write), .tx_byte(byte_tx),
         .ack_to_send(byte_ack_send),
         .sda_in(sda_in), .scl_high(scl_in), .abort(arb_lost),
         .sda_req(b_sda_req), .sda_bit(b_sda_bit),
         .rx_byte(b_rx), .ack(b_ack), .ack_valid(b_ackv), .byte_done(b_done),
         .busy(b_busy), .bit_index(b_bidx), .driving(b_driving),
         .bytes_done(b_bytes), .acks(b_acks), .nacks(b_nacks));

      // ---- Chapter 17.10: feedback. Declared here, ahead of the error manager that
      //      consumes `stretch_timeout`, because Verilog binds ports at elaboration and a
      //      wire used before its declaration is an elaboration error rather than a warning.
      logic scl_held, stretch_to;
      logic [CNT_W-1:0] fb_ev, fb_long;

      // ---- Chapter 17.11: recovery, which also wants the pins ----------------
      logic rec_scl_low, rec_sda_req, rec_sda_bit;
      logic err_valid;
      logic [CNT_W-1:0] rec_pulses;
      logic [2:0] rec_state;
      logic addr_nack, data_nack;

      // DECISION 3: recovery may not run during a transfer. The stuck-line detector cannot
      // tell a held line from a clocked one while a transfer is open, because both lines are
      // low most of the time.
      wire in_transfer = f_started || b_busy || txn_busy;

      i2c_err_mgr #(.N_PULSES(N_PULSES), .N_HALF(N_LOW), .CNT_W(CNT_W)) u_err (
         .clk(clk), .rst_n(rst_n),
         .addr_nack(addr_nack), .data_nack(data_nack), .arb_lost(arb_lost),
         .stretch_timeout(stretch_to), .in_transfer(in_transfer),
         .scl_in(scl_in), .sda_in(sda_in),
         .start_recovery(start_recovery && !in_transfer), .clear(txn_done),
         .recovering(recovering), .rec_scl_low(rec_scl_low),
         .rec_sda_req(rec_sda_req), .rec_sda_bit(rec_sda_bit),
         .err(err), .err_valid(err_valid), .pulses_issued(rec_pulses),
         .recovered(recovered), .escalate(escalate), .state(rec_state));

      // ---- DECISION 1: who owns SDA ------------------------------------------
      // Owner 0 the framer, 1 the bit engine, 2 unused here (the byte engine drives through
      // the bit engine), 3 recovery. Lowest index wins, and any overlap is reported.
      wire [3:0] sda_req_v = {rec_sda_req, 1'b0, b_sda_req, f_sda_req};
      wire [3:0] sda_bit_v = {rec_sda_bit, 1'b0, b_sda_bit, f_sda_bit};
      logic [3:0] grant;
      logic sda_owned, sda_tx, arb_now, arb_lost_w;

      i2c_sda_ctrl #(.N_OWNER(4), .CNT_W(CNT_W)) u_sda (
         .clk(clk), .rst_n(rst_n), .req(sda_req_v), .bit_val(sda_bit_v),
         .sda_in(sda_in), .scl_in(scl_in), .tx_active(b_driving),
         .sda_drive_low(sda_drive_low),
         .grant(grant), .owned(sda_owned), .tx_bit(sda_tx),
         .owner_conflict(), .conflicts(sda_conflicts),
         .arb_loss_now(arb_now), .arb_lost(arb_lost_w), .arb_losses(arb_losses),
         .arb_clear(txn_done));

      assign arb_lost = arb_lost_w;

      // ---- DECISION 2: who owns SCL ------------------------------------------
      // Three contributors, wired-AND locally exactly as the bus would. The handover
      // OVERLAPS -- Chapter 17.8's controller asserts `scl_yield` and the generator's enable
      // in the same cycle -- so the line never rises between owners.
      assign scl_drive_low = f_scl_low | g_scl_low | rec_scl_low;

      i2c_bus_feedback #(.STRETCH_LIMIT(STRETCH_LIMIT), .CNT_W(CNT_W)) u_fb (
         .clk(clk), .rst_n(rst_n),
         .scl_release(~scl_drive_low), .sda_release(~sda_drive_low),
         .tx_active(b_driving),
         .scl_in(scl_in), .sda_in(sda_in),
         .scl_held(scl_held), .stretch_seen(stretch_seen), .stretch_cycles(stretch_cycles),
         .stretch_events(fb_ev), .longest_stretch(fb_long), .stretch_timeout(stretch_to),
         .arb_loss_now(), .arb_lost(), .arb_losses(), .clear(txn_done));

      // ---- Chapter 17.2: the host interface ----------------------------------
      logic cmd_valid, cmd_read, cmd_stop;
      logic [6:0] cmd_addr;
      logic [3:0] cmd_len, tx_index, rx_index;
      logic [7:0] tx_data, rx_data;
      logic rx_we;
      logic [3:0] txn_bytes;
      logic [CNT_W-1:0] n_cmds;

      i2c_cmd_regs #(.N_BUF(N_BYTES), .CNT_W(CNT_W)) u_reg (
         .clk(clk), .rst_n(rst_n),
         .reg_addr(reg_addr), .reg_wdata(reg_wdata), .reg_we(reg_we), .reg_re(reg_re),
         .reg_rdata(reg_rdata),
         .cmd_valid(cmd_valid), .cmd_addr(cmd_addr), .cmd_read(cmd_read),
         .cmd_len(cmd_len), .cmd_stop(cmd_stop),
         .tx_data(tx_data), .tx_index(tx_index),
         .rx_data(rx_data), .rx_index(rx_index), .rx_we(rx_we),
         .txn_done(txn_done), .txn_ok(txn_ok), .txn_err(err),
         .txn_bytes(txn_bytes), .txn_busy(txn_busy),
         .commands_issued(n_cmds));

      // ---- Chapter 17.8: the phase sequencer --------------------------------
      i2c_txn_ctrl #(.CNT_W(CNT_W)) u_txn (
         .clk(clk), .rst_n(rst_n),
         .cmd_valid(cmd_valid), .cmd_addr(cmd_addr), .cmd_read(cmd_read),
         .cmd_len(cmd_len), .cmd_stop(cmd_stop),
         .do_start(do_start), .do_restart(do_restart), .do_stop(do_stop),
         .scl_yield(scl_yield), .frame_done(f_done), .frame_busy(f_busy),
         .bus_free(f_bus_free), .frame_started(f_started),
         .gen_enable(gen_enable), .gen_idle_low(gen_idle_low),
         .byte_go(byte_go), .byte_dir_write(byte_dir_write), .byte_tx(byte_tx),
         .byte_ack_send(byte_ack_send), .byte_done(b_done), .byte_busy(b_busy),
         .byte_ack(b_ack), .byte_rx(b_rx),
         .tx_data(tx_data), .tx_index(tx_index),
         .rx_data(rx_data), .rx_index(rx_index), .rx_we(rx_we),
         .arb_lost(arb_lost),
         .txn_done(txn_done), .txn_ok(txn_ok), .txn_err(),
         .txn_bytes(txn_bytes), .txn_busy(txn_busy),
         .addr_nack(addr_nack), .data_nack(data_nack),
         .state(txn_state), .transactions(transactions));

   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_master.v — the same design in Verilog-2001
   // -----------------------------------------------------------------------------
   // i2c_master.sv
   // The whole master, and the FSM that was designed LAST.
   //
   // THE CLAIM OF THIS CHAPTER. There is no state machine in this file. Every state in the
   // finished master already exists inside a block that needed it, and the top level is
   // wiring plus three decisions. That is not an accident of this particular design: it is
   // what happens when the decomposition follows the protocol's four TIME BASES rather than
   // its vocabulary.
   //
   //   the divider tick          -> i2c_scl_gen's phase        (Chapter 17.3)
   //   the SCL edge, read back   -> i2c_bit_engine             (Chapter 17.6)
   //   the byte boundary         -> i2c_byte_engine's slot     (Chapter 17.7)
   //   the host command          -> i2c_txn_ctrl's phase       (Chapter 17.8)
   //
   // Those four change on unrelated events. A single FSM holding all four has to take the
   // product of their states and gets four sets of transitions out of each one, and the
   // counters that inevitably get bolted on to make it tractable ARE the other three
   // machines, admitted late and without their invariants.
   //
   // THE THREE DECISIONS THIS FILE ACTUALLY MAKES:
   //
   //   1. WHO OWNS SDA. Four blocks want it -- the framer, the bit engine, the byte
   //      engine's acknowledge, and recovery -- and Chapter 17.4's arbiter resolves them by
   //      priority while reporting any overlap as the design error it is.
   //
   //   2. WHO OWNS SCL. The framer and the generator, handed over with a one-cycle overlap
   //      in both directions so the line never rises in between. Chapter 17.8's controller
   //      sequences the handover because it is the only block that knows which is needed.
   //
   //   3. WHEN RECOVERY MAY RUN. Never during a transfer, because Chapter 17.11's stuck-line
   //      detector cannot distinguish a held line from a clocked one while a transfer is in
   //      progress -- both lines are low most of the time.
   //
   // AND THE PINS ARE THREE SIGNALS PER LINE, not one. `drive_low` out, the line in, and the
   // pad's output enable derived from the first. An `inout` in synthesizable RTL cannot be
   // driven by two blocks in simulation without resolution hazards and cannot be synthesized
   // at all; Chapter 17.4 §I makes the argument.
   // -----------------------------------------------------------------------------

   // (Verilog-2001 -- structurally identical to the SystemVerilog above.)
   module i2c_master #(
      parameter N_LOW    = 8,
      parameter N_HIGH   = 4,
      parameter N_SU     = 2,
      parameter N_SAMP   = 2,
      parameter N_HD_STA = 3,
      parameter N_SU_STA = 3,
      parameter N_SU_STO = 3,
      parameter N_BUF    = 3,
      parameter STRETCH_LIMIT = 0,
      parameter N_PULSES = 9,
      parameter N_BYTES  = 8,
      parameter CNT_W    = 16
   ) (
      input  wire            clk,
      input  wire            rst_n,

      // ---- the host register bus ----------------------------------------------
      input  wire [3:0]      reg_addr,
      input  wire [7:0]      reg_wdata,
      input  wire            reg_we,
      input  wire            reg_re,
      output wire [7:0]      reg_rdata,

      // ---- the bus pins: three signals per line -------------------------------
      output wire            scl_drive_low,
      input  wire            scl_in,
      output wire            sda_drive_low,
      input  wire            sda_in,

      // ---- recovery, which the host must ask for explicitly -------------------
      input  wire            start_recovery,
      output wire            recovering,
      output wire            recovered,
      output wire            escalate,

      // ---- observability ------------------------------------------------------
      output wire            txn_busy,
      output wire            txn_done,
      output wire            txn_ok,
      output wire [5:0]      err,
      output wire            arb_lost,
      output wire            stretch_seen,
      output wire [CNT_W-1:0] transactions,
      output wire [CNT_W-1:0] stretch_cycles,
      output wire [CNT_W-1:0] arb_losses,
      output wire [CNT_W-1:0] sda_conflicts,
      output wire [3:0]      txn_state
   );

      // ---- Chapter 17.3: the clock -------------------------------------------
      wire g_scl_low, drive_point, sample_point, g_rise, g_fall, g_stretch;
      wire [CNT_W-1:0] g_scyc, g_bits;
      wire [1:0] g_phase;
      wire gen_enable, gen_idle_low;

      i2c_scl_gen #(.N_LOW(N_LOW), .N_HIGH(N_HIGH), .N_SU(N_SU), .N_SAMP(N_SAMP),
                    .CNT_W(CNT_W)) u_scl (
         .clk(clk), .rst_n(rst_n), .enable(gen_enable), .idle_low(gen_idle_low),
         .scl_in(scl_in), .scl_drive_low(g_scl_low),
         .drive_point(drive_point), .sample_point(sample_point),
         .scl_rising(g_rise), .scl_falling(g_fall),
         .stretching(g_stretch), .stretch_cycles(g_scyc),
         .bits_generated(g_bits), .phase(g_phase));

      // ---- Chapter 17.5: framing ---------------------------------------------
      wire f_sda_req, f_sda_bit, f_scl_low, f_busy, f_done, f_bus_free, f_started, f_sw;
      wire [CNT_W-1:0] n_sta, n_rs, n_sto;
      wire [3:0] f_state;
      wire do_start, do_restart, do_stop, scl_yield;

      i2c_framer #(.N_HD_STA(N_HD_STA), .N_SU_STA(N_SU_STA), .N_SU_STO(N_SU_STO),
                   .N_BUF(N_BUF), .N_SU_DAT(N_SU), .CNT_W(CNT_W)) u_fr (
         .clk(clk), .rst_n(rst_n),
         .do_start(do_start), .do_restart(do_restart), .do_stop(do_stop),
         .scl_in(scl_in), .sda_in(sda_in), .scl_yield(scl_yield),
         .sda_req(f_sda_req), .sda_bit(f_sda_bit), .scl_drive_low(f_scl_low),
         .busy(f_busy), .done(f_done), .bus_free(f_bus_free), .started(f_started),
         .stretch_wait(f_sw), .starts(n_sta), .restarts(n_rs), .stops(n_sto),
         .state(f_state));

      // ---- Chapters 17.6 and 17.7: the datapath ------------------------------
      wire b_sda_req, b_sda_bit, b_driving, b_busy, b_ack, b_ackv, b_done;
      wire [7:0] b_rx;
      wire [3:0] b_bidx;
      wire [CNT_W-1:0] b_bytes, b_acks, b_nacks;
      wire byte_go, byte_dir_write, byte_ack_send;
      wire [7:0] byte_tx;

      i2c_byte_engine #(.CNT_W(CNT_W)) u_by (
         .clk(clk), .rst_n(rst_n),
         .drive_point(drive_point), .sample_point(sample_point),
         .go(byte_go), .dir_write(byte_dir_write), .tx_byte(byte_tx),
         .ack_to_send(byte_ack_send),
         .sda_in(sda_in), .scl_high(scl_in), .abort(arb_lost),
         .sda_req(b_sda_req), .sda_bit(b_sda_bit),
         .rx_byte(b_rx), .ack(b_ack), .ack_valid(b_ackv), .byte_done(b_done),
         .busy(b_busy), .bit_index(b_bidx), .driving(b_driving),
         .bytes_done(b_bytes), .acks(b_acks), .nacks(b_nacks));

      // ---- Chapter 17.10: feedback. Declared here, ahead of the error manager that
      //      consumes `stretch_timeout`, because Verilog binds ports at elaboration and a
      //      wire used before its declaration is an elaboration error rather than a warning.
      wire scl_held, stretch_to;
      wire [CNT_W-1:0] fb_ev, fb_long;

      // ---- Chapter 17.11: recovery, which also wants the pins ----------------
      wire rec_scl_low, rec_sda_req, rec_sda_bit;
      wire err_valid;
      wire [CNT_W-1:0] rec_pulses;
      wire [2:0] rec_state;
      wire addr_nack, data_nack;

      // DECISION 3: recovery may not run during a transfer. The stuck-line detector cannot
      // tell a held line from a clocked one while a transfer is open, because both lines are
      // low most of the time.
      wire in_transfer = f_started || b_busy || txn_busy;

      i2c_err_mgr #(.N_PULSES(N_PULSES), .N_HALF(N_LOW), .CNT_W(CNT_W)) u_err (
         .clk(clk), .rst_n(rst_n),
         .addr_nack(addr_nack), .data_nack(data_nack), .arb_lost(arb_lost),
         .stretch_timeout(stretch_to), .in_transfer(in_transfer),
         .scl_in(scl_in), .sda_in(sda_in),
         .start_recovery(start_recovery && !in_transfer), .clear(txn_done),
         .recovering(recovering), .rec_scl_low(rec_scl_low),
         .rec_sda_req(rec_sda_req), .rec_sda_bit(rec_sda_bit),
         .err(err), .err_valid(err_valid), .pulses_issued(rec_pulses),
         .recovered(recovered), .escalate(escalate), .state(rec_state));

      // ---- DECISION 1: who owns SDA ------------------------------------------
      // Owner 0 the framer, 1 the bit engine, 2 unused here (the byte engine drives through
      // the bit engine), 3 recovery. Lowest index wins, and any overlap is reported.
      wire [3:0] sda_req_v = {rec_sda_req, 1'b0, b_sda_req, f_sda_req};
      wire [3:0] sda_bit_v = {rec_sda_bit, 1'b0, b_sda_bit, f_sda_bit};
      wire [3:0] grant;
      wire sda_owned, sda_tx, arb_now, arb_lost_w;

      i2c_sda_ctrl #(.N_OWNER(4), .CNT_W(CNT_W)) u_sda (
         .clk(clk), .rst_n(rst_n), .req(sda_req_v), .bit_val(sda_bit_v),
         .sda_in(sda_in), .scl_in(scl_in), .tx_active(b_driving),
         .sda_drive_low(sda_drive_low),
         .grant(grant), .owned(sda_owned), .tx_bit(sda_tx),
         .owner_conflict(), .conflicts(sda_conflicts),
         .arb_loss_now(arb_now), .arb_lost(arb_lost_w), .arb_losses(arb_losses),
         .arb_clear(txn_done));

      assign arb_lost = arb_lost_w;

      // ---- DECISION 2: who owns SCL ------------------------------------------
      // Three contributors, wired-AND locally exactly as the bus would. The handover
      // OVERLAPS -- Chapter 17.8's controller asserts `scl_yield` and the generator's enable
      // in the same cycle -- so the line never rises between owners.
      assign scl_drive_low = f_scl_low | g_scl_low | rec_scl_low;

      i2c_bus_feedback #(.STRETCH_LIMIT(STRETCH_LIMIT), .CNT_W(CNT_W)) u_fb (
         .clk(clk), .rst_n(rst_n),
         .scl_release(~scl_drive_low), .sda_release(~sda_drive_low),
         .tx_active(b_driving),
         .scl_in(scl_in), .sda_in(sda_in),
         .scl_held(scl_held), .stretch_seen(stretch_seen), .stretch_cycles(stretch_cycles),
         .stretch_events(fb_ev), .longest_stretch(fb_long), .stretch_timeout(stretch_to),
         .arb_loss_now(), .arb_lost(), .arb_losses(), .clear(txn_done));

      // ---- Chapter 17.2: the host interface ----------------------------------
      wire cmd_valid, cmd_read, cmd_stop;
      wire [6:0] cmd_addr;
      wire [3:0] cmd_len, tx_index, rx_index;
      wire [7:0] tx_data, rx_data;
      wire rx_we;
      wire [3:0] txn_bytes;
      wire [CNT_W-1:0] n_cmds;

      i2c_cmd_regs #(.N_BUF(N_BYTES), .CNT_W(CNT_W)) u_reg (
         .clk(clk), .rst_n(rst_n),
         .reg_addr(reg_addr), .reg_wdata(reg_wdata), .reg_we(reg_we), .reg_re(reg_re),
         .reg_rdata(reg_rdata),
         .cmd_valid(cmd_valid), .cmd_addr(cmd_addr), .cmd_read(cmd_read),
         .cmd_len(cmd_len), .cmd_stop(cmd_stop),
         .tx_data(tx_data), .tx_index(tx_index),
         .rx_data(rx_data), .rx_index(rx_index), .rx_we(rx_we),
         .txn_done(txn_done), .txn_ok(txn_ok), .txn_err(err),
         .txn_bytes(txn_bytes), .txn_busy(txn_busy),
         .commands_issued(n_cmds));

      // ---- Chapter 17.8: the phase sequencer --------------------------------
      i2c_txn_ctrl #(.CNT_W(CNT_W)) u_txn (
         .clk(clk), .rst_n(rst_n),
         .cmd_valid(cmd_valid), .cmd_addr(cmd_addr), .cmd_read(cmd_read),
         .cmd_len(cmd_len), .cmd_stop(cmd_stop),
         .do_start(do_start), .do_restart(do_restart), .do_stop(do_stop),
         .scl_yield(scl_yield), .frame_done(f_done), .frame_busy(f_busy),
         .bus_free(f_bus_free), .frame_started(f_started),
         .gen_enable(gen_enable), .gen_idle_low(gen_idle_low),
         .byte_go(byte_go), .byte_dir_write(byte_dir_write), .byte_tx(byte_tx),
         .byte_ack_send(byte_ack_send), .byte_done(b_done), .byte_busy(b_busy),
         .byte_ack(b_ack), .byte_rx(b_rx),
         .tx_data(tx_data), .tx_index(tx_index),
         .rx_data(rx_data), .rx_index(rx_index), .rx_we(rx_we),
         .arb_lost(arb_lost),
         .txn_done(txn_done), .txn_ok(txn_ok), .txn_err(),
         .txn_bytes(txn_bytes), .txn_busy(txn_busy),
         .addr_nack(addr_nack), .data_nack(data_nack),
         .state(txn_state), .transactions(transactions));

   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_master.vhd — the same design in VHDL
   -- ---------------------------------------------------------------------------
   -- i2c_master.vhd
   -- The whole master, and the FSM that was designed LAST.
   -- Behavioural twin of i2c_master.sv / .v.
   --
   -- THE CLAIM OF THIS CHAPTER. There is no state machine in this file. Every state in the
   -- finished master already exists inside a block that needed it, and the top level is wiring
   -- plus three decisions. That is what happens when the decomposition follows the protocol's four
   -- TIME BASES rather than its vocabulary:
   --
   --   the divider tick          -> i2c_scl_gen's phase        (Chapter 17.3)
   --   the SCL edge, read back   -> i2c_bit_engine             (Chapter 17.6)
   --   the byte boundary         -> i2c_byte_engine's slot     (Chapter 17.7)
   --   the host command          -> i2c_txn_ctrl's phase       (Chapter 17.8)
   --
   -- Those four change on unrelated events. A single FSM holding all four takes the product of
   -- their states and gets four sets of transitions out of each one, and the counters that
   -- inevitably get bolted on to make it tractable ARE the other three machines, admitted late and
   -- without their invariants.
   --
   -- THE THREE DECISIONS THIS FILE ACTUALLY MAKES:
   --   1. WHO OWNS SDA -- four blocks want it, resolved by priority with overlap reported.
   --   2. WHO OWNS SCL -- the framer and the generator, handed over with a one-cycle overlap in
   --      both directions so the line never rises in between.
   --   3. WHEN RECOVERY MAY RUN -- never during a transfer, because the stuck-line detector
   --      cannot distinguish a held line from a clocked one while a transfer is in progress.
   --
   -- AND THE PINS ARE THREE SIGNALS PER LINE, not one. In VHDL an `inout std_logic` would work in
   -- simulation because std_logic is resolved -- and that is exactly the trap: it would also
   -- resolve two accidental drivers to 'X' with no error, and it does not synthesise. The
   -- drive-low / line-in / pad form says what the hardware is.
   -- ---------------------------------------------------------------------------

   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   entity i2c_master is
      generic (
         N_LOW         : integer := 8;
         N_HIGH        : integer := 4;
         N_SU          : integer := 2;
         N_SAMP        : integer := 2;
         N_HD_STA      : integer := 3;
         N_SU_STA      : integer := 3;
         N_SU_STO      : integer := 3;
         N_BUF         : integer := 3;
         STRETCH_LIMIT : integer := 0;
         N_PULSES      : integer := 9;
         N_BYTES       : integer := 8;
         CNT_W         : integer := 16
      );
      port (
         clk   : in std_logic;
         rst_n : in std_logic;

         reg_addr  : in  std_logic_vector(3 downto 0);
         reg_wdata : in  std_logic_vector(7 downto 0);
         reg_we    : in  std_logic;
         reg_re    : in  std_logic;
         reg_rdata : out std_logic_vector(7 downto 0);

         scl_drive_low : out std_logic;
         scl_in        : in  std_logic;
         sda_drive_low : out std_logic;
         sda_in        : in  std_logic;

         start_recovery : in  std_logic;
         recovering     : out std_logic;
         recovered      : out std_logic;
         escalate       : out std_logic;

         txn_busy       : out std_logic;
         txn_done       : out std_logic;
         txn_ok         : out std_logic;
         err            : out std_logic_vector(5 downto 0);
         arb_lost       : out std_logic;
         stretch_seen   : out std_logic;
         transactions   : out unsigned(CNT_W-1 downto 0);
         stretch_cycles : out unsigned(CNT_W-1 downto 0);
         arb_losses     : out unsigned(CNT_W-1 downto 0);
         sda_conflicts  : out unsigned(CNT_W-1 downto 0);
         txn_state      : out unsigned(3 downto 0)
      );
   end entity i2c_master;

   architecture rtl of i2c_master is

      -- Chapter 17.3
      signal g_scl_low, drive_point, sample_point, g_rise, g_fall, g_stretch : std_logic;
      signal g_scyc, g_bits : unsigned(CNT_W-1 downto 0);
      signal g_phase : unsigned(1 downto 0);
      signal gen_enable, gen_idle_low : std_logic;

      -- Chapter 17.5
      signal f_sda_req, f_sda_bit, f_scl_low : std_logic;
      signal f_busy, f_done, f_bus_free, f_started, f_sw : std_logic;
      signal n_sta, n_rs, n_sto : unsigned(CNT_W-1 downto 0);
      signal f_state : unsigned(3 downto 0);
      signal do_start, do_restart, do_stop, scl_yield : std_logic;

      -- Chapters 17.6 and 17.7
      signal b_sda_req, b_sda_bit, b_driving, b_busy, b_ack, b_ackv, b_done : std_logic;
      signal b_rx : std_logic_vector(7 downto 0);
      signal b_bidx : unsigned(3 downto 0);
      signal b_bytes, b_acks, b_nacks : unsigned(CNT_W-1 downto 0);
      signal byte_go, byte_dir_write, byte_ack_send : std_logic;
      signal byte_tx : std_logic_vector(7 downto 0);

      -- Chapter 17.10, declared ahead of the error manager that consumes stretch_to
      signal scl_held, stretch_to : std_logic;
      signal fb_ev, fb_long : unsigned(CNT_W-1 downto 0);

      -- Chapter 17.11
      signal rec_scl_low, rec_sda_req, rec_sda_bit, err_valid : std_logic;
      signal rec_pulses : unsigned(CNT_W-1 downto 0);
      signal rec_state : unsigned(2 downto 0);
      signal addr_nack, data_nack : std_logic;
      signal in_transfer : std_logic;

      -- the SDA arbiter
      signal sda_req_v, sda_bit_v, grant : std_logic_vector(3 downto 0);
      signal sda_owned, sda_tx, arb_now, arb_lost_w : std_logic;

      -- Chapter 17.2
      signal cmd_valid, cmd_read, cmd_stop : std_logic;
      signal cmd_addr : std_logic_vector(6 downto 0);
      signal cmd_len, tx_index, rx_index, txn_bytes : unsigned(3 downto 0);
      signal tx_data, rx_data : std_logic_vector(7 downto 0);
      signal rx_we : std_logic;
      signal n_cmds : unsigned(CNT_W-1 downto 0);

      signal busy_i, done_i, ok_i : std_logic;
      signal err_i : std_logic_vector(5 downto 0);

   begin

      txn_busy <= busy_i;
      txn_done <= done_i;
      txn_ok   <= ok_i;
      err      <= err_i;
      arb_lost <= arb_lost_w;

      -- DECISION 3: recovery may not run during a transfer. The stuck-line detector cannot tell a
      -- held line from a clocked one while a transfer is open, because both lines are low most of
      -- the time.
      in_transfer <= f_started or b_busy or busy_i;

      u_scl : entity work.i2c_scl_gen
         generic map (N_LOW => N_LOW, N_HIGH => N_HIGH, N_SU => N_SU, N_SAMP => N_SAMP,
                      CNT_W => CNT_W)
         port map (clk => clk, rst_n => rst_n, enable => gen_enable, idle_low => gen_idle_low,
            scl_in => scl_in, scl_drive_low => g_scl_low,
            drive_point => drive_point, sample_point => sample_point,
            scl_rising => g_rise, scl_falling => g_fall,
            stretching => g_stretch, stretch_cycles => g_scyc,
            bits_generated => g_bits, phase => g_phase);

      u_fr : entity work.i2c_framer
         generic map (N_HD_STA => N_HD_STA, N_SU_STA => N_SU_STA, N_SU_STO => N_SU_STO,
                      N_BUF => N_BUF, N_SU_DAT => N_SU, CNT_W => CNT_W)
         port map (clk => clk, rst_n => rst_n,
            do_start => do_start, do_restart => do_restart, do_stop => do_stop,
            scl_in => scl_in, sda_in => sda_in, scl_yield => scl_yield,
            sda_req => f_sda_req, sda_bit => f_sda_bit, scl_drive_low => f_scl_low,
            busy => f_busy, done => f_done, bus_free => f_bus_free, started => f_started,
            stretch_wait => f_sw, starts => n_sta, restarts => n_rs, stops => n_sto,
            state => f_state);

      u_by : entity work.i2c_byte_engine
         generic map (CNT_W => CNT_W)
         port map (clk => clk, rst_n => rst_n,
            drive_point => drive_point, sample_point => sample_point,
            go => byte_go, dir_write => byte_dir_write, tx_byte => byte_tx,
            ack_to_send => byte_ack_send,
            sda_in => sda_in, scl_high => scl_in, abort => arb_lost_w,
            sda_req => b_sda_req, sda_bit => b_sda_bit,
            rx_byte => b_rx, ack => b_ack, ack_valid => b_ackv, byte_done => b_done,
            busy => b_busy, bit_index => b_bidx, driving => b_driving,
            bytes_done => b_bytes, acks => b_acks, nacks => b_nacks);

      u_err : entity work.i2c_err_mgr
         generic map (N_PULSES => N_PULSES, N_HALF => N_LOW, CNT_W => CNT_W)
         port map (clk => clk, rst_n => rst_n,
            addr_nack => addr_nack, data_nack => data_nack, arb_lost => arb_lost_w,
            stretch_timeout => stretch_to, in_transfer => in_transfer,
            scl_in => scl_in, sda_in => sda_in,
            start_recovery => (start_recovery and (not in_transfer)), clear => done_i,
            recovering => recovering, rec_scl_low => rec_scl_low,
            rec_sda_req => rec_sda_req, rec_sda_bit => rec_sda_bit,
            err => err_i, err_valid => err_valid, pulses_issued => rec_pulses,
            recovered => recovered, escalate => escalate, state => rec_state);

      -- ---- DECISION 1: who owns SDA ------------------------------------------
      -- Owner 0 the framer, 1 the bit engine, 2 unused here (the byte engine drives through the
      -- bit engine), 3 recovery. Lowest index wins, and any overlap is reported.
      sda_req_v <= rec_sda_req & '0' & b_sda_req & f_sda_req;
      sda_bit_v <= rec_sda_bit & '0' & b_sda_bit & f_sda_bit;

      u_sda : entity work.i2c_sda_ctrl
         generic map (N_OWNER => 4, CNT_W => CNT_W)
         port map (clk => clk, rst_n => rst_n, req => sda_req_v, bit_val => sda_bit_v,
            sda_in => sda_in, scl_in => scl_in, tx_active => b_driving,
            sda_drive_low => sda_drive_low,
            grant => grant, owned => sda_owned, tx_bit => sda_tx,
            owner_conflict => open, conflicts => sda_conflicts,
            arb_loss_now => arb_now, arb_lost => arb_lost_w, arb_losses => arb_losses,
            arb_clear => done_i);

      -- ---- DECISION 2: who owns SCL ------------------------------------------
      -- Three contributors, wired-AND locally exactly as the bus would. The handover OVERLAPS --
      -- Chapter 17.8's controller asserts `scl_yield` and the generator's enable in the same
      -- cycle -- so the line never rises between owners.
      scl_drive_low <= f_scl_low or g_scl_low or rec_scl_low;

      u_fb : entity work.i2c_bus_feedback
         generic map (STRETCH_LIMIT => STRETCH_LIMIT, CNT_W => CNT_W)
         port map (clk => clk, rst_n => rst_n,
            scl_release => not (f_scl_low or g_scl_low or rec_scl_low),
            sda_release => not (sda_req_v(0) or sda_req_v(1) or sda_req_v(3)),
            tx_active => b_driving,
            scl_in => scl_in, sda_in => sda_in,
            scl_held => scl_held, stretch_seen => stretch_seen,
            stretch_cycles => stretch_cycles,
            stretch_events => fb_ev, longest_stretch => fb_long,
            stretch_timeout => stretch_to,
            arb_loss_now => open, arb_lost => open, arb_losses => open,
            clear => done_i);

      u_reg : entity work.i2c_cmd_regs
         generic map (N_BUF => N_BYTES, CNT_W => CNT_W)
         port map (clk => clk, rst_n => rst_n,
            reg_addr => reg_addr, reg_wdata => reg_wdata, reg_we => reg_we, reg_re => reg_re,
            reg_rdata => reg_rdata,
            cmd_valid => cmd_valid, cmd_addr => cmd_addr, cmd_read => cmd_read,
            cmd_len => cmd_len, cmd_stop => cmd_stop,
            tx_data => tx_data, tx_index => tx_index,
            rx_data => rx_data, rx_index => rx_index, rx_we => rx_we,
            txn_done => done_i, txn_ok => ok_i, txn_err => err_i,
            txn_bytes => txn_bytes, txn_busy => busy_i,
            commands_issued => n_cmds);

      u_txn : entity work.i2c_txn_ctrl
         generic map (CNT_W => CNT_W)
         port map (clk => clk, rst_n => rst_n,
            cmd_valid => cmd_valid, cmd_addr => cmd_addr, cmd_read => cmd_read,
            cmd_len => cmd_len, cmd_stop => cmd_stop,
            do_start => do_start, do_restart => do_restart, do_stop => do_stop,
            scl_yield => scl_yield, frame_done => f_done, frame_busy => f_busy,
            bus_free => f_bus_free, frame_started => f_started,
            gen_enable => gen_enable, gen_idle_low => gen_idle_low,
            byte_go => byte_go, byte_dir_write => byte_dir_write, byte_tx => byte_tx,
            byte_ack_send => byte_ack_send, byte_done => b_done, byte_busy => b_busy,
            byte_ack => b_ack, byte_rx => b_rx,
            tx_data => tx_data, tx_index => tx_index,
            rx_data => rx_data, rx_index => rx_index, rx_we => rx_we,
            arb_lost => arb_lost_w,
            txn_done => done_i, txn_ok => ok_i, txn_err => open,
            txn_bytes => txn_bytes, txn_busy => busy_i,
            addr_nack => addr_nack, data_nack => data_nack,
            state => txn_state, transactions => transactions);

   end architecture rtl;

5a. The testbenches

Thirteen tests, driven through the register map — the bench writes four registers and polls, which is exactly what a driver does. Nothing reaches inside the master except the observers of §6.

#TestProperty
T1a reset master drives nothingnot one block holds a line
T2a write, from four register writes and a pollthe whole driver
T3a read, payload back through the register map
T4an address NACK reaches the driver as a distinct codenot as a timeout
T5a stretching target holds SCL for twelve cycles after every ACK
T6a data NACK from that same targeta different code
T7a combined transaction, composed by the driverwrite with no STOP, then read
T8recovery of a stuck SDAand it reached the wire; see §6
T9a stuck SCL gets no pulses§3.1.16's central asymmetry
T10recovery is refused during a transferand drives nothing; see §6
T11the data-valid rule held over the whole runchecked from the wire
T12a long sequence — six transactionsnothing accumulates
T13arbitration, and the latch that must clearsee §6
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_master_tb.sv — the self-checking testbench
   `timescale 1ns/1ps
   // -----------------------------------------------------------------------------
   // i2c_master_tb.sv
   // Independent oracle for the complete i2c_master.
   //
   // The bench is the HOST. It writes registers and polls status, exactly as a driver would,
   // and it never looks inside the master. On the other side of a wired-AND bus sit two
   // pin-level targets and a protocol monitor that sees only the two wires.
   //
   // So every test here is an end-to-end statement: a driver-visible action produces a
   // wire-visible result. That is the only level at which the claim of Chapter 17.12 can be
   // checked -- that the blocks compose into a master with no state machine added at the
   // top -- because a claim about composition cannot be tested on any one component.
   // -----------------------------------------------------------------------------
   module i2c_master_tb;

      localparam integer NL = 8, NH = 4, NSU = 2, NSMP = 2;
      localparam integer NHD = 3, NSUA = 3, NSUO = 3, NBF = 3;
      localparam [6:0]   TADDR = 7'h50, TADDR2 = 7'h22;
      localparam [3:0] R_ADDR = 4'h0, R_LEN = 4'h1, R_CTRL = 4'h2, R_TX0 = 4'h3,
                       R_RX0 = 4'h4, R_STATUS = 4'h5, R_ERR = 4'h6, R_CMD = 4'h7;

      logic clk = 1'b0, rst_n = 1'b0;
      logic [3:0] reg_addr = 4'h0;
      logic [7:0] reg_wdata = 8'h00;
      logic reg_we = 1'b0, reg_re = 1'b0;
      logic start_recovery = 1'b0;
      logic [7:0] reg_rdata;

      logic m_scl_low, m_sda_low;
      logic recovering, recovered, escalate;
      logic txn_busy, txn_done, txn_ok, arb_lost, stretch_seen;
      logic [5:0] err;
      logic [15:0] n_txn, n_scyc, n_arb, n_conf;
      logic [3:0] txn_state;

      logic t1_scl, t1_sda, t2_scl, t2_sda;
      logic  fault_sda = 1'b0, fault_scl = 1'b0;

      logic scl, sda;
      logic [3:0] scl_in, sda_in, scl_rbl, sda_rbl;
      logic [7:0] scl_h, sda_h;

      i2c_line_model #(.N_DEV(4)) bus (
         .scl_drive_low({fault_scl, t2_scl, t1_scl, m_scl_low}),
         .sda_drive_low({fault_sda, t2_sda, t1_sda, m_sda_low}),
         .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_h), .sda_holders(sda_h));

      i2c_master #(.N_LOW(NL), .N_HIGH(NH), .N_SU(NSU), .N_SAMP(NSMP),
                   .N_HD_STA(NHD), .N_SU_STA(NSUA), .N_SU_STO(NSUO), .N_BUF(NBF),
                   .STRETCH_LIMIT(0), .N_PULSES(9), .N_BYTES(8), .CNT_W(16)) dut (
         .clk(clk), .rst_n(rst_n),
         .reg_addr(reg_addr), .reg_wdata(reg_wdata), .reg_we(reg_we), .reg_re(reg_re),
         .reg_rdata(reg_rdata),
         .scl_drive_low(m_scl_low), .scl_in(scl_in[0]),
         .sda_drive_low(m_sda_low), .sda_in(sda_in[0]),
         .start_recovery(start_recovery),
         .recovering(recovering), .recovered(recovered), .escalate(escalate),
         .txn_busy(txn_busy), .txn_done(txn_done), .txn_ok(txn_ok), .err(err),
         .arb_lost(arb_lost), .stretch_seen(stretch_seen),
         .transactions(n_txn), .stretch_cycles(n_scyc), .arb_losses(n_arb),
         .sda_conflicts(n_conf), .txn_state(txn_state));

      logic load_en = 1'b0; reg [7:0] load_addr = 8'h00, load_data = 8'h00;
      logic t1_sel, t1_rd, t1_wv; wire [7:0] t1_lw;
      logic [15:0] t1_rx, t1_tx, t1_nsta, t1_nsto; wire [2:0] t1_st;

      i2c_target_model #(.MY_ADDR(TADDR), .ACK_ADDR(1'b1), .STRETCH_AFTER(0),
                         .NACK_AT(0), .N_MEM(16), .CNT_W(16)) u_t1 (
         .clk(clk), .rst_n(rst_n), .scl(scl), .sda(sda),
         .scl_drive_low(t1_scl), .sda_drive_low(t1_sda),
         .load_en(load_en), .load_addr(load_addr), .load_data(load_data),
         .selected(t1_sel), .dir_read(t1_rd), .last_written(t1_lw), .write_valid(t1_wv),
         .bytes_rx(t1_rx), .bytes_tx(t1_tx), .n_starts(t1_nsta), .n_stops(t1_nsto),
         .state(t1_st));

      // A second target that stretches after every acknowledge -- §3.1.6's byte-level
      // handshake -- and refuses its third data byte.
      logic t2_sel, t2_rd, t2_wv; wire [7:0] t2_lw;
      logic [15:0] t2_rx, t2_tx, t2_nsta, t2_nsto; wire [2:0] t2_st;

      i2c_target_model #(.MY_ADDR(TADDR2), .ACK_ADDR(1'b1), .STRETCH_AFTER(12),
                         .NACK_AT(3), .N_MEM(16), .CNT_W(16)) u_t2 (
         .clk(clk), .rst_n(rst_n), .scl(scl), .sda(sda),
         .scl_drive_low(t2_scl), .sda_drive_low(t2_sda),
         .load_en(1'b0), .load_addr(8'h00), .load_data(8'h00),
         .selected(t2_sel), .dir_read(t2_rd), .last_written(t2_lw), .write_valid(t2_wv),
         .bytes_rx(t2_rx), .bytes_tx(t2_tx), .n_starts(t2_nsta), .n_stops(t2_nsto),
         .state(t2_st));

      logic mo_start, mo_stop, mo_bit, mo_bitv, mo_byte, mo_ack, mo_ackv, mo_intr, mo_mid;
      logic [7:0] mo_byteval;
      logic [3:0] mo_bidx;
      logic [15:0] mo_nsta, mo_nsto, mo_nbyte, mo_nmid;

      i2c_proto_mon #(.CNT_W(16)) mon (
         .clk(clk), .rst_n(rst_n), .scl(scl), .sda(sda),
         .start_seen(mo_start), .stop_seen(mo_stop), .bit_seen(mo_bit), .bit_val(mo_bitv),
         .byte_seen(mo_byte), .byte_val(mo_byteval), .ack_seen(mo_ack), .ack_val(mo_ackv),
         .in_transfer(mo_intr), .framing_midbyte(mo_mid), .bit_index(mo_bidx),
         .n_starts(mo_nsta), .n_stops(mo_nsto), .n_bytes(mo_nbyte), .n_midbyte(mo_nmid));

      always #5 clk = ~clk;

      integer errors = 0;
      integer n, k;
      logic [7:0] got, st;
      integer bad_change;
      logic scl_l, sda_l;
      always @(negedge clk) begin
         if (rst_n) begin
            if (scl && scl_l && (sda != sda_l) && !mo_start && !mo_stop && mo_bidx > 4'd1)
               bad_change = bad_change + 1;
            scl_l = scl; sda_l = sda;
         end
      end

      task step; begin @(posedge clk); @(negedge clk); end endtask

      task do_reset;
         begin
            @(negedge clk);
            rst_n = 1'b0; reg_we = 1'b0; reg_re = 1'b0; start_recovery = 1'b0;
            fault_sda = 1'b0; fault_scl = 1'b0; load_en = 1'b0;
            bad_change = 0; scl_l = 1'b1; sda_l = 1'b1;
            repeat (3) @(posedge clk);
            @(negedge clk); rst_n = 1'b1;
            step;
         end
      endtask

      task wr (input [3:0] a, input [7:0] d);
         begin
            @(negedge clk); reg_addr = a; reg_wdata = d; reg_we = 1'b1;
            @(posedge clk); @(negedge clk); reg_we = 1'b0;
         end
      endtask

      task rd (input [3:0] a);
         begin
            @(negedge clk); reg_addr = a; reg_re = 1'b1;
            @(posedge clk); @(negedge clk); reg_re = 1'b0;
            got = reg_rdata;
         end
      endtask

      task preload (input [7:0] a, input [7:0] d);
         begin
            @(negedge clk); load_en = 1'b1; load_addr = a; load_data = d;
            @(posedge clk); @(negedge clk); load_en = 1'b0;
         end
      endtask

      // A driver, written the way a driver is written: post the command, then POLL.
      task xfer (input [6:0] a, input rd_dir, input [3:0] len, input stp);
         begin
            wr(R_ADDR, {a, rd_dir});
            wr(R_LEN,  {4'd0, len});
            wr(R_CTRL, {7'd0, stp});
            wr(R_CMD,  8'h01);
            n = 0;
            st = 8'h00;
            while (!st[0] && n < 12000) begin
               rd(R_STATUS); st = got; n = n + 1;
            end
            if (n >= 12000) begin
               $display("  FAIL xfer: never completed (txn_state %0d)", txn_state);
               errors = errors + 1;
            end
         end
      endtask

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

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

      // ---- integration-level observers ---------------------------------------
      //
      // Three properties of the TOP LEVEL are invisible to the block benches, because each
      // one is about whether a block's output actually reaches a pin:
      //
      //   * recovery must really clock the bus (its SCL contribution must be in the OR)
      //   * recovery must really be able to frame it (its SDA contribution likewise)
      //   * recovery must drive NOTHING when it is refused
      //
      // A block bench proves the block behaves; only the integration can prove it is wired.
      integer rec_drove_scl = 0, rec_drove_sda = 0;
      always @(posedge clk) begin
         if (!rst_n) begin rec_drove_scl <= 0; rec_drove_sda <= 0; end
         else if (recovering) begin
            if (m_scl_low) rec_drove_scl <= rec_drove_scl + 1;
            if (m_sda_low) rec_drove_sda <= rec_drove_sda + 1;
         end
      end

      initial begin
         $display("=== i2c_master: the blocks compose, and nothing is added at the top ===");

         // ----------------------------------------------------------------
         // T1. A reset master drives nothing. Not one of the blocks holds a line out of
         //     reset, which is what a shared bus depends on and what §3.1.16 has no remedy
         //     for if it fails.
         // ----------------------------------------------------------------
         do_reset;
         $display("T1  a reset master drives neither line");
         ck_bit("T1 SCL released", m_scl_low, 1'b0);
         ck_bit("T1 SDA released", m_sda_low, 1'b0);
         ck_bit("T1 both lines high", scl & sda, 1'b1);
         for (k = 0; k < 60; k = k + 1) begin
            step;
            if (m_scl_low || m_sda_low) begin
               $display("  FAIL T1 the idle master drove a line");
               errors = errors + 1;
            end
         end
         ck_int("T1 nothing on the wire", mo_nsta + mo_nsto + mo_nbyte, 0);

         // ----------------------------------------------------------------
         // T2. A WRITE, from four register writes and a poll. This is the whole driver.
         // ----------------------------------------------------------------
         do_reset;
         wr(R_TX0, 8'hA5);
         xfer(TADDR, 1'b0, 4'd1, 1'b1);
         $display("T2  a one-byte write, from four register writes and a poll");
         ck_bit("T2 done", st[0], 1'b1);
         ck_bit("T2 and ok", st[1], 1'b1);
         ck_int("T2 one byte moved", st[7:4], 1);
         ck_int("T2 the target received it", t1_lw, 8'hA5);
         ck_int("T2 two bytes on the wire", mo_nbyte, 2);
         ck_int("T2 one START and one STOP", mo_nsta + mo_nsto, 2);

         // ----------------------------------------------------------------
         // T3. A READ, and the payload comes back through the register map.
         // ----------------------------------------------------------------
         do_reset;
         preload(8'h00, 8'h3C);
         preload(8'h01, 8'h5E);
         xfer(TADDR, 1'b1, 4'd2, 1'b1);
         rd(R_RX0); ck_int("T3 the first byte", got, 8'h3C);
         rd(R_RX0); ck_int("T3 the second byte", got, 8'h5E);
         $display("T3  a two-byte read, returned through the register map");
         ck_bit("T3 ok", st[1], 1'b1);
         ck_int("T3 two bytes moved", st[7:4], 2);

         // ----------------------------------------------------------------
         // T4. AN ADDRESS NACK reaches the driver as a distinct code, not as a timeout.
         // ----------------------------------------------------------------
         do_reset;
         wr(R_TX0, 8'hFF);
         xfer(7'h7A, 1'b0, 4'd2, 1'b1);
         rd(R_ERR);
         $display("T4  an address NACK reaches the driver as its own code");
         ck_bit("T4 done", st[0], 1'b1);
         ck_bit("T4 and not ok", st[1], 1'b0);
         ck_int("T4 zero bytes moved", st[7:4], 0);
         ck_int("T4 the address-NACK code", got, 8'h01);
         ck_int("T4 the bus was framed anyway", mo_nsto, 1);

         // ----------------------------------------------------------------
         // T5. A STRETCHING TARGET holds SCL for twelve cycles after every acknowledge --
         //     §3.1.6's handshake -- and the transfer still completes.
         // ----------------------------------------------------------------
         do_reset;
         wr(R_TX0, 8'h11);
         wr(R_TX0, 8'h22);
         xfer(TADDR2, 1'b0, 4'd2, 1'b1);
         $display("T5  a target that stretches after every byte is survived, not fought");
         ck_bit("T5 ok", st[1], 1'b1);
         ck_int("T5 two bytes moved", st[7:4], 2);
         ck_int("T5 the target received both", t2_rx, 2);
         ck_int("T5 the last was 0x22", t2_lw, 8'h22);
         ck_bit("T5 and the stretch was noticed", stretch_seen, 1'b1);
         if (n_scyc < 12) begin
            $display("  FAIL T5 only %0d stretch cycles counted", n_scyc);
            errors = errors + 1;
         end

         // ----------------------------------------------------------------
         // T6. A DATA NACK from that same target, which refuses its third data byte -- a
         //     write-protect pin or a read-only location, in Chapter 16.1's terms.
         // ----------------------------------------------------------------
         do_reset;
         for (k = 0; k < 4; k = k + 1) wr(R_TX0, 8'h40 + k[7:0]);
         xfer(TADDR2, 1'b0, 4'd4, 1'b1);
         rd(R_ERR);
         $display("T6  a data NACK is a different code from an address NACK");
         ck_bit("T6 done", st[0], 1'b1);
         ck_bit("T6 not ok", st[1], 1'b0);
         ck_int("T6 the data-NACK code, not the address one", got, 8'h02);
         ck_int("T6 two of the four bytes were accepted", st[7:4], 2);
         ck_int("T6 and the bus was framed", mo_nsto, 1);

         // ----------------------------------------------------------------
         // T7. A COMBINED TRANSACTION, composed by the driver: write with no STOP, then read.
         //     Two STARTs and ONE STOP, which is what makes it one transaction.
         // ----------------------------------------------------------------
         do_reset;
         preload(8'h00, 8'h91);
         wr(R_TX0, 8'h00);
         xfer(TADDR, 1'b0, 4'd1, 1'b0);     // no STOP: hold the bus
         ck_bit("T7 phase one ok", st[1], 1'b1);
         ck_int("T7 no STOP yet", mo_nsto, 0);
         xfer(TADDR, 1'b1, 4'd1, 1'b1);     // the turnaround and the read
         rd(R_RX0);
         $display("T7  a combined transaction, composed from two commands by the driver");
         ck_int("T7 the byte came back", got, 8'h91);
         ck_int("T7 two STARTs", mo_nsta, 2);
         ck_int("T7 and exactly one STOP", mo_nsto, 1);
         ck_bit("T7 the bus is idle", mo_intr, 1'b0);

         // ----------------------------------------------------------------
         // T8. RECOVERY OF A STUCK SDA. A fault injector holds SDA low with no transfer in
         //     progress, and the master clocks it free. §3.1.16.
         // ----------------------------------------------------------------
         do_reset;
         @(negedge clk); fault_sda = 1'b1;
         for (k = 0; k < 10; k = k + 1) step;
         rd(R_ERR);
         ck_bit("T8 a stuck SDA was diagnosed", got[4], 1'b1);
         @(negedge clk); start_recovery = 1'b1;
         step;
         @(negedge clk); start_recovery = 1'b0;
         n = 0;
         while (!recovering && n < 500) begin step; n = n + 1; end
         for (k = 0; k < 60; k = k + 1) step;
         @(negedge clk); fault_sda = 1'b0;      // the wedged device finally lets go
         n = 0;
         while (recovering && n < 3000) begin step; n = n + 1; end
         $display("T8  a stuck SDA is clocked free, and the bus is left framed");
         ck_bit("T8 recovered", recovered, 1'b1);
         ck_bit("T8 no escalation", escalate, 1'b0);
         // Wired, not merely willing: recovery must have driven the CLOCK through the
         // top-level OR, and must have driven SDA to build the closing STOP. A master
         // whose recovery block is correct but whose contribution is missing from the
         // pin path reports "recovered" having put nothing on the bus.
         if (rec_drove_scl == 0) begin
            $display("  FAIL T8 recovery never drove SCL onto the bus");
            errors = errors + 1;
         end
         if (rec_drove_sda == 0) begin
            $display("  FAIL T8 recovery never drove SDA, so it cannot have framed the bus");
            errors = errors + 1;
         end
         ck_bit("T8 SDA is free", sda, 1'b1);
         ck_bit("T8 and nothing is being driven", m_scl_low | m_sda_low, 1'b0);

         // ----------------------------------------------------------------
         // T9. A STUCK SCL GETS NO PULSES. The central asymmetry of §3.1.16: the nine
         //     pulses ARE pulses on SCL, so none can reach a line something else is holding.
         // ----------------------------------------------------------------
         do_reset;
         @(negedge clk); fault_scl = 1'b1; fault_sda = 1'b1;
         for (k = 0; k < 10; k = k + 1) step;
         rd(R_ERR);
         ck_bit("T9 a stuck SCL was diagnosed", got[5], 1'b1);
         @(negedge clk); start_recovery = 1'b1;
         step;
         @(negedge clk); start_recovery = 1'b0;
         n = 0;
         while (recovering && n < 3000) begin step; n = n + 1; end
         $display("T9  a stuck SCL gets no pulses and escalates immediately");
         ck_bit("T9 escalated", escalate, 1'b1);
         ck_bit("T9 not recovered", recovered, 1'b0);
         ck_bit("T9 and the master never drove SCL against the holder", m_scl_low, 1'b0);

         // ----------------------------------------------------------------
         // T10. RECOVERY IS REFUSED DURING A TRANSFER. Both lines are low most of the time
         //      while clocking, so a detector that ran then would report a stuck bus on
         //      every byte -- and clocking a live transfer would destroy it.
         // ----------------------------------------------------------------
         do_reset;
         wr(R_TX0, 8'h5C);
         wr(R_ADDR, {TADDR, 1'b0}); wr(R_LEN, 8'h01); wr(R_CTRL, 8'h01); wr(R_CMD, 8'h01);
         n = 0;
         while (!txn_busy && n < 500) begin step; n = n + 1; end
         @(negedge clk); start_recovery = 1'b1;
         for (k = 0; k < 20; k = k + 1) step;
         @(negedge clk); start_recovery = 1'b0;
         $display("T10 recovery asked for during a transfer is refused, not run");
         ck_bit("T10 recovery did not start", recovering, 1'b0);
         // And it drove nothing. "recovering is low" is the block saying it declined;
         // these two say the pins agree, which is the property that matters to the
         // device whose live byte would otherwise be clocked apart.
         ck_int("T10 recovery drove no clock", rec_drove_scl, 0);
         ck_int("T10 recovery drove no data", rec_drove_sda, 0);
         n = 0;
         while (!txn_done && n < 8000) begin step; n = n + 1; end
         ck_bit("T10 and the transfer completed normally", txn_ok, 1'b1);
         ck_int("T10 with the byte delivered", t1_lw, 8'h5C);

         // ----------------------------------------------------------------
         // T11. THE DATA-VALID RULE HELD OVER THE WHOLE RUN. §3.1.2, checked from the wire
         //      rather than argued from the code.
         // ----------------------------------------------------------------
         $display("T11 SDA never changed mid-byte while SCL was high, over the whole run");
         ck_int("T11 no violations", bad_change, 0);
         ck_int("T11 no SDA owner conflicts, across every block", n_conf, 0);
         ck_int("T11 no arbitration losses on a single-master bus", n_arb, 0);
         ck_int("T11 and no mid-byte framing", mo_nmid, 0);

         // ----------------------------------------------------------------
         // T12. A LONG SEQUENCE, to show nothing accumulates. Six transactions, alternating
         //      direction, each one checked.
         // ----------------------------------------------------------------
         do_reset;
         for (k = 0; k < 8; k = k + 1) preload(k[7:0], 8'hE0 + k[7:0]);
         for (k = 0; k < 3; k = k + 1) begin
            wr(R_TX0, 8'h30 + k[7:0]);
            xfer(TADDR, 1'b0, 4'd1, 1'b1);
            ck_bit("T12 the write succeeded", st[1], 1'b1);
            xfer(TADDR, 1'b1, 4'd1, 1'b1);
            ck_bit("T12 the read succeeded", st[1], 1'b1);
         end
         $display("T12 six transactions back to back, and nothing accumulates");
         ck_int("T12 six transactions", n_txn, 6);
         ck_int("T12 six STARTs and six STOPs", mo_nsta + mo_nsto, 12);
         ck_int("T12 no conflicts", n_conf, 0);
         ck_int("T12 no violations", bad_change, 0);
         ck_bit("T12 and the master is idle with both lines released",
                m_scl_low | m_sda_low, 1'b0);
         // ----------------------------------------------------------------
         // T13. ARBITRATION, AND THE LATCH THAT MUST CLEAR. The fault injector acts as a
         //      competing master: it pulls SDA low while this master is transmitting a one
         //      with SCL high, which is §3.1.8's exact condition. Two things then have to
         //      be true, and the second is the one no block bench can check -- `arb_lost`
         //      is a STICKY latch, and the top level is what decides when it clears. A
         //      master that never clears it reports the first contest forever and a driver
         //      can never distinguish a new loss from an old one.
         // ----------------------------------------------------------------
         do_reset;
         wr(R_TX0, 8'hFF);                    // all ones: every bit is losable
         wr(R_ADDR, {TADDR, 1'b0}); wr(R_LEN, 8'h01); wr(R_CTRL, 8'h01); wr(R_CMD, 8'h01);
         n = 0;
         while (!txn_busy && n < 500) begin step; n = n + 1; end
         // Hold SDA down through the address phase: whatever one this master sends, the bus
         // shows a zero, and while SCL is high that is a lost arbitration.
         @(negedge clk); fault_sda = 1'b1;
         n = 0;
         while (!arb_lost && n < 4000) begin step; n = n + 1; end
         $display("T13 a competing zero on SDA is an arbitration loss, and the latch clears");
         ck_bit("T13 arbitration was reported lost", arb_lost, 1'b1);
         if (n_arb < 1) begin
            $display("  FAIL T13 the loss was not counted");
            errors = errors + 1;
         end
         @(negedge clk); fault_sda = 1'b0;
         n = 0;
         while (txn_busy && n < 8000) begin step; n = n + 1; end

         // Now a clean transaction on a quiet bus. The latch must be gone, or every later
         // transfer inherits this one's verdict.
         wr(R_TX0, 8'h2A);
         wr(R_ADDR, {TADDR, 1'b0}); wr(R_LEN, 8'h01); wr(R_CTRL, 8'h01); wr(R_CMD, 8'h01);
         n = 0;
         while (!txn_done && n < 8000) begin step; n = n + 1; end
         ck_bit("T13 and the latch cleared for the next transaction", arb_lost, 1'b0);
         ck_bit("T13 which then succeeded on a quiet bus", txn_ok, 1'b1);



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

   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_master_tb.v — the same tests in Verilog-2001
   `timescale 1ns/1ps
   // -----------------------------------------------------------------------------
   // i2c_master_tb.sv
   // Independent oracle for the complete i2c_master.
   //
   // The bench is the HOST. It writes registers and polls status, exactly as a driver would,
   // and it never looks inside the master. On the other side of a wired-AND bus sit two
   // pin-level targets and a protocol monitor that sees only the two wires.
   //
   // So every test here is an end-to-end statement: a driver-visible action produces a
   // wire-visible result. That is the only level at which the claim of Chapter 17.12 can be
   // checked -- that the blocks compose into a master with no state machine added at the
   // top -- because a claim about composition cannot be tested on any one component.
   // -----------------------------------------------------------------------------
   // (Verilog-2001 -- structurally identical to the SystemVerilog above.)
   module i2c_master_tb;

      localparam integer NL = 8, NH = 4, NSU = 2, NSMP = 2;
      localparam integer NHD = 3, NSUA = 3, NSUO = 3, NBF = 3;
      localparam [6:0]   TADDR = 7'h50, TADDR2 = 7'h22;
      localparam [3:0] R_ADDR = 4'h0, R_LEN = 4'h1, R_CTRL = 4'h2, R_TX0 = 4'h3,
                       R_RX0 = 4'h4, R_STATUS = 4'h5, R_ERR = 4'h6, R_CMD = 4'h7;

      reg clk = 1'b0, rst_n = 1'b0;
      reg [3:0] reg_addr = 4'h0;
      reg [7:0] reg_wdata = 8'h00;
      reg reg_we = 1'b0, reg_re = 1'b0;
      reg start_recovery = 1'b0;
      wire [7:0] reg_rdata;

      wire m_scl_low, m_sda_low;
      wire recovering, recovered, escalate;
      wire txn_busy, txn_done, txn_ok, arb_lost, stretch_seen;
      wire [5:0] err;
      wire [15:0] n_txn, n_scyc, n_arb, n_conf;
      wire [3:0] txn_state;

      wire t1_scl, t1_sda, t2_scl, t2_sda;
      reg  fault_sda = 1'b0, fault_scl = 1'b0;

      wire scl, sda;
      wire [3:0] scl_in, sda_in, scl_rbl, sda_rbl;
      wire [7:0] scl_h, sda_h;

      i2c_line_model #(.N_DEV(4)) bus (
         .scl_drive_low({fault_scl, t2_scl, t1_scl, m_scl_low}),
         .sda_drive_low({fault_sda, t2_sda, t1_sda, m_sda_low}),
         .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_h), .sda_holders(sda_h));

      i2c_master #(.N_LOW(NL), .N_HIGH(NH), .N_SU(NSU), .N_SAMP(NSMP),
                   .N_HD_STA(NHD), .N_SU_STA(NSUA), .N_SU_STO(NSUO), .N_BUF(NBF),
                   .STRETCH_LIMIT(0), .N_PULSES(9), .N_BYTES(8), .CNT_W(16)) dut (
         .clk(clk), .rst_n(rst_n),
         .reg_addr(reg_addr), .reg_wdata(reg_wdata), .reg_we(reg_we), .reg_re(reg_re),
         .reg_rdata(reg_rdata),
         .scl_drive_low(m_scl_low), .scl_in(scl_in[0]),
         .sda_drive_low(m_sda_low), .sda_in(sda_in[0]),
         .start_recovery(start_recovery),
         .recovering(recovering), .recovered(recovered), .escalate(escalate),
         .txn_busy(txn_busy), .txn_done(txn_done), .txn_ok(txn_ok), .err(err),
         .arb_lost(arb_lost), .stretch_seen(stretch_seen),
         .transactions(n_txn), .stretch_cycles(n_scyc), .arb_losses(n_arb),
         .sda_conflicts(n_conf), .txn_state(txn_state));

      reg load_en = 1'b0; reg [7:0] load_addr = 8'h00, load_data = 8'h00;
      wire t1_sel, t1_rd, t1_wv; wire [7:0] t1_lw;
      wire [15:0] t1_rx, t1_tx, t1_nsta, t1_nsto; wire [2:0] t1_st;

      i2c_target_model #(.MY_ADDR(TADDR), .ACK_ADDR(1'b1), .STRETCH_AFTER(0),
                         .NACK_AT(0), .N_MEM(16), .CNT_W(16)) u_t1 (
         .clk(clk), .rst_n(rst_n), .scl(scl), .sda(sda),
         .scl_drive_low(t1_scl), .sda_drive_low(t1_sda),
         .load_en(load_en), .load_addr(load_addr), .load_data(load_data),
         .selected(t1_sel), .dir_read(t1_rd), .last_written(t1_lw), .write_valid(t1_wv),
         .bytes_rx(t1_rx), .bytes_tx(t1_tx), .n_starts(t1_nsta), .n_stops(t1_nsto),
         .state(t1_st));

      // A second target that stretches after every acknowledge -- §3.1.6's byte-level
      // handshake -- and refuses its third data byte.
      wire t2_sel, t2_rd, t2_wv; wire [7:0] t2_lw;
      wire [15:0] t2_rx, t2_tx, t2_nsta, t2_nsto; wire [2:0] t2_st;

      i2c_target_model #(.MY_ADDR(TADDR2), .ACK_ADDR(1'b1), .STRETCH_AFTER(12),
                         .NACK_AT(3), .N_MEM(16), .CNT_W(16)) u_t2 (
         .clk(clk), .rst_n(rst_n), .scl(scl), .sda(sda),
         .scl_drive_low(t2_scl), .sda_drive_low(t2_sda),
         .load_en(1'b0), .load_addr(8'h00), .load_data(8'h00),
         .selected(t2_sel), .dir_read(t2_rd), .last_written(t2_lw), .write_valid(t2_wv),
         .bytes_rx(t2_rx), .bytes_tx(t2_tx), .n_starts(t2_nsta), .n_stops(t2_nsto),
         .state(t2_st));

      wire mo_start, mo_stop, mo_bit, mo_bitv, mo_byte, mo_ack, mo_ackv, mo_intr, mo_mid;
      wire [7:0] mo_byteval;
      wire [3:0] mo_bidx;
      wire [15:0] mo_nsta, mo_nsto, mo_nbyte, mo_nmid;

      i2c_proto_mon #(.CNT_W(16)) mon (
         .clk(clk), .rst_n(rst_n), .scl(scl), .sda(sda),
         .start_seen(mo_start), .stop_seen(mo_stop), .bit_seen(mo_bit), .bit_val(mo_bitv),
         .byte_seen(mo_byte), .byte_val(mo_byteval), .ack_seen(mo_ack), .ack_val(mo_ackv),
         .in_transfer(mo_intr), .framing_midbyte(mo_mid), .bit_index(mo_bidx),
         .n_starts(mo_nsta), .n_stops(mo_nsto), .n_bytes(mo_nbyte), .n_midbyte(mo_nmid));

      always #5 clk = ~clk;

      integer errors = 0;
      integer n, k;
      reg [7:0] got, st;
      integer bad_change;
      reg scl_l, sda_l;
      always @(negedge clk) begin
         if (rst_n) begin
            if (scl && scl_l && (sda != sda_l) && !mo_start && !mo_stop && mo_bidx > 4'd1)
               bad_change = bad_change + 1;
            scl_l = scl; sda_l = sda;
         end
      end

      task step; begin @(posedge clk); @(negedge clk); end endtask

      task do_reset;
         begin
            @(negedge clk);
            rst_n = 1'b0; reg_we = 1'b0; reg_re = 1'b0; start_recovery = 1'b0;
            fault_sda = 1'b0; fault_scl = 1'b0; load_en = 1'b0;
            bad_change = 0; scl_l = 1'b1; sda_l = 1'b1;
            repeat (3) @(posedge clk);
            @(negedge clk); rst_n = 1'b1;
            step;
         end
      endtask

      task wr (input [3:0] a, input [7:0] d);
         begin
            @(negedge clk); reg_addr = a; reg_wdata = d; reg_we = 1'b1;
            @(posedge clk); @(negedge clk); reg_we = 1'b0;
         end
      endtask

      task rd (input [3:0] a);
         begin
            @(negedge clk); reg_addr = a; reg_re = 1'b1;
            @(posedge clk); @(negedge clk); reg_re = 1'b0;
            got = reg_rdata;
         end
      endtask

      task preload (input [7:0] a, input [7:0] d);
         begin
            @(negedge clk); load_en = 1'b1; load_addr = a; load_data = d;
            @(posedge clk); @(negedge clk); load_en = 1'b0;
         end
      endtask

      // A driver, written the way a driver is written: post the command, then POLL.
      task xfer (input [6:0] a, input rd_dir, input [3:0] len, input stp);
         begin
            wr(R_ADDR, {a, rd_dir});
            wr(R_LEN,  {4'd0, len});
            wr(R_CTRL, {7'd0, stp});
            wr(R_CMD,  8'h01);
            n = 0;
            st = 8'h00;
            while (!st[0] && n < 12000) begin
               rd(R_STATUS); st = got; n = n + 1;
            end
            if (n >= 12000) begin
               $display("  FAIL xfer: never completed (txn_state %0d)", txn_state);
               errors = errors + 1;
            end
         end
      endtask

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

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

      // ---- integration-level observers ---------------------------------------
      //
      // Three properties of the TOP LEVEL are invisible to the block benches, because each
      // one is about whether a block's output actually reaches a pin:
      //
      //   * recovery must really clock the bus (its SCL contribution must be in the OR)
      //   * recovery must really be able to frame it (its SDA contribution likewise)
      //   * recovery must drive NOTHING when it is refused
      //
      // A block bench proves the block behaves; only the integration can prove it is wired.
      integer rec_drove_scl, rec_drove_sda;
      always @(posedge clk) begin
         if (!rst_n) begin rec_drove_scl <= 0; rec_drove_sda <= 0; end
         else if (recovering) begin
            if (m_scl_low) rec_drove_scl <= rec_drove_scl + 1;
            if (m_sda_low) rec_drove_sda <= rec_drove_sda + 1;
         end
      end

      initial begin
         $display("=== i2c_master: the blocks compose, and nothing is added at the top ===");

         // ----------------------------------------------------------------
         // T1. A reset master drives nothing. Not one of the blocks holds a line out of
         //     reset, which is what a shared bus depends on and what §3.1.16 has no remedy
         //     for if it fails.
         // ----------------------------------------------------------------
         do_reset;
         $display("T1  a reset master drives neither line");
         ck_bit("T1 SCL released", m_scl_low, 1'b0);
         ck_bit("T1 SDA released", m_sda_low, 1'b0);
         ck_bit("T1 both lines high", scl & sda, 1'b1);
         for (k = 0; k < 60; k = k + 1) begin
            step;
            if (m_scl_low || m_sda_low) begin
               $display("  FAIL T1 the idle master drove a line");
               errors = errors + 1;
            end
         end
         ck_int("T1 nothing on the wire", mo_nsta + mo_nsto + mo_nbyte, 0);

         // ----------------------------------------------------------------
         // T2. A WRITE, from four register writes and a poll. This is the whole driver.
         // ----------------------------------------------------------------
         do_reset;
         wr(R_TX0, 8'hA5);
         xfer(TADDR, 1'b0, 4'd1, 1'b1);
         $display("T2  a one-byte write, from four register writes and a poll");
         ck_bit("T2 done", st[0], 1'b1);
         ck_bit("T2 and ok", st[1], 1'b1);
         ck_int("T2 one byte moved", st[7:4], 1);
         ck_int("T2 the target received it", t1_lw, 8'hA5);
         ck_int("T2 two bytes on the wire", mo_nbyte, 2);
         ck_int("T2 one START and one STOP", mo_nsta + mo_nsto, 2);

         // ----------------------------------------------------------------
         // T3. A READ, and the payload comes back through the register map.
         // ----------------------------------------------------------------
         do_reset;
         preload(8'h00, 8'h3C);
         preload(8'h01, 8'h5E);
         xfer(TADDR, 1'b1, 4'd2, 1'b1);
         rd(R_RX0); ck_int("T3 the first byte", got, 8'h3C);
         rd(R_RX0); ck_int("T3 the second byte", got, 8'h5E);
         $display("T3  a two-byte read, returned through the register map");
         ck_bit("T3 ok", st[1], 1'b1);
         ck_int("T3 two bytes moved", st[7:4], 2);

         // ----------------------------------------------------------------
         // T4. AN ADDRESS NACK reaches the driver as a distinct code, not as a timeout.
         // ----------------------------------------------------------------
         do_reset;
         wr(R_TX0, 8'hFF);
         xfer(7'h7A, 1'b0, 4'd2, 1'b1);
         rd(R_ERR);
         $display("T4  an address NACK reaches the driver as its own code");
         ck_bit("T4 done", st[0], 1'b1);
         ck_bit("T4 and not ok", st[1], 1'b0);
         ck_int("T4 zero bytes moved", st[7:4], 0);
         ck_int("T4 the address-NACK code", got, 8'h01);
         ck_int("T4 the bus was framed anyway", mo_nsto, 1);

         // ----------------------------------------------------------------
         // T5. A STRETCHING TARGET holds SCL for twelve cycles after every acknowledge --
         //     §3.1.6's handshake -- and the transfer still completes.
         // ----------------------------------------------------------------
         do_reset;
         wr(R_TX0, 8'h11);
         wr(R_TX0, 8'h22);
         xfer(TADDR2, 1'b0, 4'd2, 1'b1);
         $display("T5  a target that stretches after every byte is survived, not fought");
         ck_bit("T5 ok", st[1], 1'b1);
         ck_int("T5 two bytes moved", st[7:4], 2);
         ck_int("T5 the target received both", t2_rx, 2);
         ck_int("T5 the last was 0x22", t2_lw, 8'h22);
         ck_bit("T5 and the stretch was noticed", stretch_seen, 1'b1);
         if (n_scyc < 12) begin
            $display("  FAIL T5 only %0d stretch cycles counted", n_scyc);
            errors = errors + 1;
         end

         // ----------------------------------------------------------------
         // T6. A DATA NACK from that same target, which refuses its third data byte -- a
         //     write-protect pin or a read-only location, in Chapter 16.1's terms.
         // ----------------------------------------------------------------
         do_reset;
         for (k = 0; k < 4; k = k + 1) wr(R_TX0, 8'h40 + k[7:0]);
         xfer(TADDR2, 1'b0, 4'd4, 1'b1);
         rd(R_ERR);
         $display("T6  a data NACK is a different code from an address NACK");
         ck_bit("T6 done", st[0], 1'b1);
         ck_bit("T6 not ok", st[1], 1'b0);
         ck_int("T6 the data-NACK code, not the address one", got, 8'h02);
         ck_int("T6 two of the four bytes were accepted", st[7:4], 2);
         ck_int("T6 and the bus was framed", mo_nsto, 1);

         // ----------------------------------------------------------------
         // T7. A COMBINED TRANSACTION, composed by the driver: write with no STOP, then read.
         //     Two STARTs and ONE STOP, which is what makes it one transaction.
         // ----------------------------------------------------------------
         do_reset;
         preload(8'h00, 8'h91);
         wr(R_TX0, 8'h00);
         xfer(TADDR, 1'b0, 4'd1, 1'b0);     // no STOP: hold the bus
         ck_bit("T7 phase one ok", st[1], 1'b1);
         ck_int("T7 no STOP yet", mo_nsto, 0);
         xfer(TADDR, 1'b1, 4'd1, 1'b1);     // the turnaround and the read
         rd(R_RX0);
         $display("T7  a combined transaction, composed from two commands by the driver");
         ck_int("T7 the byte came back", got, 8'h91);
         ck_int("T7 two STARTs", mo_nsta, 2);
         ck_int("T7 and exactly one STOP", mo_nsto, 1);
         ck_bit("T7 the bus is idle", mo_intr, 1'b0);

         // ----------------------------------------------------------------
         // T8. RECOVERY OF A STUCK SDA. A fault injector holds SDA low with no transfer in
         //     progress, and the master clocks it free. §3.1.16.
         // ----------------------------------------------------------------
         do_reset;
         @(negedge clk); fault_sda = 1'b1;
         for (k = 0; k < 10; k = k + 1) step;
         rd(R_ERR);
         ck_bit("T8 a stuck SDA was diagnosed", got[4], 1'b1);
         @(negedge clk); start_recovery = 1'b1;
         step;
         @(negedge clk); start_recovery = 1'b0;
         n = 0;
         while (!recovering && n < 500) begin step; n = n + 1; end
         for (k = 0; k < 60; k = k + 1) step;
         @(negedge clk); fault_sda = 1'b0;      // the wedged device finally lets go
         n = 0;
         while (recovering && n < 3000) begin step; n = n + 1; end
         $display("T8  a stuck SDA is clocked free, and the bus is left framed");
         ck_bit("T8 recovered", recovered, 1'b1);
         ck_bit("T8 no escalation", escalate, 1'b0);
         // Wired, not merely willing: recovery must have driven the CLOCK through the
         // top-level OR, and must have driven SDA to build the closing STOP.
         if (rec_drove_scl == 0) begin
            $display("  FAIL T8 recovery never drove SCL onto the bus");
            errors = errors + 1;
         end
         if (rec_drove_sda == 0) begin
            $display("  FAIL T8 recovery never drove SDA, so it cannot have framed the bus");
            errors = errors + 1;
         end
         ck_bit("T8 SDA is free", sda, 1'b1);
         ck_bit("T8 and nothing is being driven", m_scl_low | m_sda_low, 1'b0);

         // ----------------------------------------------------------------
         // T9. A STUCK SCL GETS NO PULSES. The central asymmetry of §3.1.16: the nine
         //     pulses ARE pulses on SCL, so none can reach a line something else is holding.
         // ----------------------------------------------------------------
         do_reset;
         @(negedge clk); fault_scl = 1'b1; fault_sda = 1'b1;
         for (k = 0; k < 10; k = k + 1) step;
         rd(R_ERR);
         ck_bit("T9 a stuck SCL was diagnosed", got[5], 1'b1);
         @(negedge clk); start_recovery = 1'b1;
         step;
         @(negedge clk); start_recovery = 1'b0;
         n = 0;
         while (recovering && n < 3000) begin step; n = n + 1; end
         $display("T9  a stuck SCL gets no pulses and escalates immediately");
         ck_bit("T9 escalated", escalate, 1'b1);
         ck_bit("T9 not recovered", recovered, 1'b0);
         ck_bit("T9 and the master never drove SCL against the holder", m_scl_low, 1'b0);

         // ----------------------------------------------------------------
         // T10. RECOVERY IS REFUSED DURING A TRANSFER. Both lines are low most of the time
         //      while clocking, so a detector that ran then would report a stuck bus on
         //      every byte -- and clocking a live transfer would destroy it.
         // ----------------------------------------------------------------
         do_reset;
         wr(R_TX0, 8'h5C);
         wr(R_ADDR, {TADDR, 1'b0}); wr(R_LEN, 8'h01); wr(R_CTRL, 8'h01); wr(R_CMD, 8'h01);
         n = 0;
         while (!txn_busy && n < 500) begin step; n = n + 1; end
         @(negedge clk); start_recovery = 1'b1;
         for (k = 0; k < 20; k = k + 1) step;
         @(negedge clk); start_recovery = 1'b0;
         $display("T10 recovery asked for during a transfer is refused, not run");
         ck_bit("T10 recovery did not start", recovering, 1'b0);
         // And it drove nothing -- the property that matters to the device whose live
         // byte would otherwise be clocked apart.
         ck_int("T10 recovery drove no clock", rec_drove_scl, 0);
         ck_int("T10 recovery drove no data", rec_drove_sda, 0);
         n = 0;
         while (!txn_done && n < 8000) begin step; n = n + 1; end
         ck_bit("T10 and the transfer completed normally", txn_ok, 1'b1);
         ck_int("T10 with the byte delivered", t1_lw, 8'h5C);

         // ----------------------------------------------------------------
         // T11. THE DATA-VALID RULE HELD OVER THE WHOLE RUN. §3.1.2, checked from the wire
         //      rather than argued from the code.
         // ----------------------------------------------------------------
         $display("T11 SDA never changed mid-byte while SCL was high, over the whole run");
         ck_int("T11 no violations", bad_change, 0);
         ck_int("T11 no SDA owner conflicts, across every block", n_conf, 0);
         ck_int("T11 no arbitration losses on a single-master bus", n_arb, 0);
         ck_int("T11 and no mid-byte framing", mo_nmid, 0);

         // ----------------------------------------------------------------
         // T12. A LONG SEQUENCE, to show nothing accumulates. Six transactions, alternating
         //      direction, each one checked.
         // ----------------------------------------------------------------
         do_reset;
         for (k = 0; k < 8; k = k + 1) preload(k[7:0], 8'hE0 + k[7:0]);
         for (k = 0; k < 3; k = k + 1) begin
            wr(R_TX0, 8'h30 + k[7:0]);
            xfer(TADDR, 1'b0, 4'd1, 1'b1);
            ck_bit("T12 the write succeeded", st[1], 1'b1);
            xfer(TADDR, 1'b1, 4'd1, 1'b1);
            ck_bit("T12 the read succeeded", st[1], 1'b1);
         end
         $display("T12 six transactions back to back, and nothing accumulates");
         ck_int("T12 six transactions", n_txn, 6);
         ck_int("T12 six STARTs and six STOPs", mo_nsta + mo_nsto, 12);
         ck_int("T12 no conflicts", n_conf, 0);
         ck_int("T12 no violations", bad_change, 0);
         ck_bit("T12 and the master is idle with both lines released",
                m_scl_low | m_sda_low, 1'b0);
         // ----------------------------------------------------------------
         // T13. ARBITRATION, AND THE LATCH THAT MUST CLEAR. The fault injector acts as a
         //      competing master: it pulls SDA low while this master is transmitting a one
         //      with SCL high, which is §3.1.8's exact condition. Two things then have to
         //      be true, and the second is the one no block bench can check -- `arb_lost`
         //      is a STICKY latch, and the top level is what decides when it clears. A
         //      master that never clears it reports the first contest forever and a driver
         //      can never distinguish a new loss from an old one.
         // ----------------------------------------------------------------
         do_reset;
         wr(R_TX0, 8'hFF);                    // all ones: every bit is losable
         wr(R_ADDR, {TADDR, 1'b0}); wr(R_LEN, 8'h01); wr(R_CTRL, 8'h01); wr(R_CMD, 8'h01);
         n = 0;
         while (!txn_busy && n < 500) begin step; n = n + 1; end
         // Hold SDA down through the address phase: whatever one this master sends, the bus
         // shows a zero, and while SCL is high that is a lost arbitration.
         @(negedge clk); fault_sda = 1'b1;
         n = 0;
         while (!arb_lost && n < 4000) begin step; n = n + 1; end
         $display("T13 a competing zero on SDA is an arbitration loss, and the latch clears");
         ck_bit("T13 arbitration was reported lost", arb_lost, 1'b1);
         if (n_arb < 1) begin
            $display("  FAIL T13 the loss was not counted");
            errors = errors + 1;
         end
         @(negedge clk); fault_sda = 1'b0;
         n = 0;
         while (txn_busy && n < 8000) begin step; n = n + 1; end

         // Now a clean transaction on a quiet bus. The latch must be gone, or every later
         // transfer inherits this one's verdict.
         wr(R_TX0, 8'h2A);
         wr(R_ADDR, {TADDR, 1'b0}); wr(R_LEN, 8'h01); wr(R_CTRL, 8'h01); wr(R_CMD, 8'h01);
         n = 0;
         while (!txn_done && n < 8000) begin step; n = n + 1; end
         ck_bit("T13 and the latch cleared for the next transaction", arb_lost, 1'b0);
         ck_bit("T13 which then succeeded on a quiet bus", txn_ok, 1'b1);



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

   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_master_tb.vhd — the same tests in VHDL
   -- ---------------------------------------------------------------------------
   -- i2c_master_tb.vhd
   -- Independent oracle for the complete i2c_master. Behavioural twin of the SV and Verilog
   -- benches.
   --
   -- The bench is the HOST. It writes registers and polls status, exactly as a driver would, and
   -- it never looks inside the master. On the other side of a wired-AND bus sit two pin-level
   -- targets and a protocol monitor that sees only the two wires.
   --
   -- So every test here is an end-to-end statement: a driver-visible action produces a
   -- wire-visible result. That is the only level at which the claim of Chapter 17.12 can be
   -- checked -- that the blocks compose into a master with no state machine added at the top --
   -- because a claim about composition cannot be tested on any one component.
   -- ---------------------------------------------------------------------------

   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   entity i2c_master_tb is
   end entity i2c_master_tb;

   architecture sim of i2c_master_tb is

      constant TCLK : time := 10 ns;
      constant NL : integer := 8;
      constant NH : integer := 4;
      constant NSU : integer := 2;
      constant NSMP : integer := 2;
      constant NHD : integer := 3;
      constant NSUA : integer := 3;
      constant NSUO : integer := 3;
      constant NBF : integer := 3;
      constant TADDR  : std_logic_vector(6 downto 0) := "1010000";   -- 0x50
      constant TADDR2 : std_logic_vector(6 downto 0) := "0100010";   -- 0x22

      constant IDX_ADDR   : std_logic_vector(3 downto 0) := x"0";
      constant IDX_LEN    : std_logic_vector(3 downto 0) := x"1";
      constant IDX_CTRL   : std_logic_vector(3 downto 0) := x"2";
      constant IDX_TX0    : std_logic_vector(3 downto 0) := x"3";
      constant IDX_RX0    : std_logic_vector(3 downto 0) := x"4";
      constant IDX_STATUS : std_logic_vector(3 downto 0) := x"5";
      constant IDX_ERR    : std_logic_vector(3 downto 0) := x"6";
      constant IDX_CMD    : std_logic_vector(3 downto 0) := x"7";

      signal clk, rst_n : std_logic := '0';
      signal raddr : std_logic_vector(3 downto 0) := (others => '0');
      signal rwdata : std_logic_vector(7 downto 0) := (others => '0');
      signal rwe, rre, start_recovery : std_logic := '0';
      signal rrdata : std_logic_vector(7 downto 0);

      signal m_scl_low, m_sda_low : std_logic;
      signal recovering, recovered, escalate : std_logic;
      signal txn_busy, txn_done, txn_ok, arb_lost, stretch_seen : std_logic;
      signal err : std_logic_vector(5 downto 0);
      signal n_txn, n_scyc, n_arb, n_conf : unsigned(15 downto 0);
      signal txn_state : unsigned(3 downto 0);

      signal t1_scl, t1_sda, t2_scl, t2_sda : std_logic;
      signal fault_sda, fault_scl : std_logic := '0';

      signal scl_drv, sda_drv : std_logic_vector(3 downto 0);
      signal scl, sda : std_logic;
      signal scl_in, sda_in, scl_rbl, sda_rbl : std_logic_vector(3 downto 0);
      signal scl_h, sda_h : unsigned(7 downto 0);

      signal load_en : std_logic := '0';
      signal load_addr, load_data : std_logic_vector(7 downto 0) := (others => '0');
      signal t1_sel, t1_rd, t1_wv : std_logic;
      signal t1_lw : std_logic_vector(7 downto 0);
      signal t1_rx, t1_tx, t1_nsta, t1_nsto : unsigned(15 downto 0);
      signal t1_st : unsigned(2 downto 0);

      signal t2_sel, t2_rd, t2_wv : std_logic;
      signal t2_lw : std_logic_vector(7 downto 0);
      signal t2_rx, t2_tx, t2_nsta, t2_nsto : unsigned(15 downto 0);
      signal t2_st : unsigned(2 downto 0);

      signal mo_start, mo_stop, mo_bit, mo_bitv, mo_byte, mo_ack, mo_ackv : std_logic;
      signal mo_intr, mo_mid : std_logic;
      signal mo_byteval : std_logic_vector(7 downto 0);
      signal mo_bidx : unsigned(3 downto 0);
      signal mo_nsta, mo_nsto, mo_nbyte, mo_nmid : unsigned(15 downto 0);

      signal bad_change : integer := 0;
      signal halt : boolean := false;

      -- ---- integration-level observers ---------------------------------------
      -- Three properties of the TOP LEVEL are invisible to the block benches, because each
      -- is about whether a block's output actually reaches a pin: recovery must really clock
      -- the bus, must really be able to frame it, and must drive NOTHING when refused.
      -- A block bench proves the block behaves; only the integration proves it is wired.
      signal rec_drove_scl, rec_drove_sda : integer := 0;
   begin

      scl_drv <= fault_scl & t2_scl & t1_scl & m_scl_low;
      sda_drv <= fault_sda & t2_sda & t1_sda & m_sda_low;

      bus_m : entity work.i2c_line_model
         generic map (N_DEV => 4)
         port map (scl_drive_low => scl_drv, sda_drive_low => sda_drv,
            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_h, sda_holders => sda_h);

      dut : entity work.i2c_master
         generic map (N_LOW => NL, N_HIGH => NH, N_SU => NSU, N_SAMP => NSMP,
                      N_HD_STA => NHD, N_SU_STA => NSUA, N_SU_STO => NSUO, N_BUF => NBF,
                      STRETCH_LIMIT => 0, N_PULSES => 9, N_BYTES => 8, CNT_W => 16)
         port map (clk => clk, rst_n => rst_n,
            reg_addr => raddr, reg_wdata => rwdata, reg_we => rwe, reg_re => rre,
            reg_rdata => rrdata,
            scl_drive_low => m_scl_low, scl_in => scl_in(0),
            sda_drive_low => m_sda_low, sda_in => sda_in(0),
            start_recovery => start_recovery,
            recovering => recovering, recovered => recovered, escalate => escalate,
            txn_busy => txn_busy, txn_done => txn_done, txn_ok => txn_ok, err => err,
            arb_lost => arb_lost, stretch_seen => stretch_seen,
            transactions => n_txn, stretch_cycles => n_scyc, arb_losses => n_arb,
            sda_conflicts => n_conf, txn_state => txn_state);

      u_t1 : entity work.i2c_target_model
         generic map (MY_ADDR => TADDR, ACK_ADDR => '1', STRETCH_AFTER => 0,
                      NACK_AT => 0, N_MEM => 16, CNT_W => 16)
         port map (clk => clk, rst_n => rst_n, scl => scl, sda => sda,
            scl_drive_low => t1_scl, sda_drive_low => t1_sda,
            load_en => load_en, load_addr => load_addr, load_data => load_data,
            selected => t1_sel, dir_read => t1_rd, last_written => t1_lw,
            write_valid => t1_wv, bytes_rx => t1_rx, bytes_tx => t1_tx,
            n_starts => t1_nsta, n_stops => t1_nsto, state => t1_st);

      -- A second target that stretches after every acknowledge -- §3.1.6's byte-level handshake,
      -- the most common real target behaviour a master must survive -- and refuses its third
      -- data byte.
      u_t2 : entity work.i2c_target_model
         generic map (MY_ADDR => TADDR2, ACK_ADDR => '1', STRETCH_AFTER => 12,
                      NACK_AT => 3, N_MEM => 16, CNT_W => 16)
         port map (clk => clk, rst_n => rst_n, scl => scl, sda => sda,
            scl_drive_low => t2_scl, sda_drive_low => t2_sda,
            load_en => '0', load_addr => x"00", load_data => x"00",
            selected => t2_sel, dir_read => t2_rd, last_written => t2_lw,
            write_valid => t2_wv, bytes_rx => t2_rx, bytes_tx => t2_tx,
            n_starts => t2_nsta, n_stops => t2_nsto, state => t2_st);

      rec_obs : process (clk, rst_n)
      begin
         if rst_n = '0' then
            rec_drove_scl <= 0; rec_drove_sda <= 0;
         elsif rising_edge(clk) then
            if recovering = '1' then
               if m_scl_low = '1' then rec_drove_scl <= rec_drove_scl + 1; end if;
               if m_sda_low = '1' then rec_drove_sda <= rec_drove_sda + 1; end if;
            end if;
         end if;
      end process;

      mon : entity work.i2c_proto_mon
         generic map (CNT_W => 16)
         port map (clk => clk, rst_n => rst_n, scl => scl, sda => sda,
            start_seen => mo_start, stop_seen => mo_stop, bit_seen => mo_bit,
            bit_val => mo_bitv, byte_seen => mo_byte, byte_val => mo_byteval,
            ack_seen => mo_ack, ack_val => mo_ackv, in_transfer => mo_intr,
            framing_midbyte => mo_mid, bit_index => mo_bidx,
            n_starts => mo_nsta, n_stops => mo_nsto, n_bytes => mo_nbyte,
            n_midbyte => mo_nmid);

      clkgen : process
      begin
         while not halt loop
            clk <= '0'; wait for TCLK/2;
            clk <= '1'; wait for TCLK/2;
         end loop;
         wait;
      end process;

      -- The §3.1.2 watchdog, over the whole run.
      meas : process (clk, rst_n)
         variable scl_l, sda_l : std_logic := '1';
      begin
         if rst_n = '0' then
            scl_l := '1'; sda_l := '1'; bad_change <= 0;
         elsif falling_edge(clk) then
            if scl = '1' and scl_l = '1' and sda /= sda_l
               and mo_start = '0' and mo_stop = '0' and mo_bidx > 1 then
               bad_change <= bad_change + 1;
            end if;
            scl_l := scl; sda_l := sda;
         end if;
      end process;

      stim : process
         variable errn : integer := 0;
         variable n : integer;
         variable got, st : std_logic_vector(7 downto 0);

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

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

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

         procedure do_reset is
         begin
            wait until falling_edge(clk);
            rst_n <= '0'; rwe <= '0'; rre <= '0'; start_recovery <= '0';
            fault_sda <= '0'; fault_scl <= '0'; load_en <= '0';
            for i in 0 to 2 loop wait until rising_edge(clk); end loop;
            wait until falling_edge(clk); rst_n <= '1';
            step;
         end procedure;

         procedure wr (a : std_logic_vector(3 downto 0); d : integer) is
         begin
            wait until falling_edge(clk);
            raddr <= a; rwdata <= std_logic_vector(to_unsigned(d, 8)); rwe <= '1';
            wait until rising_edge(clk); wait until falling_edge(clk); rwe <= '0';
         end procedure;

         procedure rd (a : std_logic_vector(3 downto 0)) is
         begin
            wait until falling_edge(clk);
            raddr <= a; rre <= '1';
            wait until rising_edge(clk); wait until falling_edge(clk); rre <= '0';
            got := rrdata;
         end procedure;

         procedure preload (a : integer; d : integer) is
         begin
            wait until falling_edge(clk);
            load_en <= '1';
            load_addr <= std_logic_vector(to_unsigned(a, 8));
            load_data <= std_logic_vector(to_unsigned(d, 8));
            wait until rising_edge(clk); wait until falling_edge(clk); load_en <= '0';
         end procedure;

         -- A driver, written the way a driver is written: post the command, then POLL.
         procedure xfer (a : std_logic_vector(6 downto 0); rd_dir : std_logic;
                         len : integer; stp : std_logic) is
            variable av : std_logic_vector(7 downto 0);
         begin
            av := a & rd_dir;
            wr(IDX_ADDR, to_integer(unsigned(av)));
            wr(IDX_LEN,  len);
            if stp = '1' then wr(IDX_CTRL, 1); else wr(IDX_CTRL, 0); end if;
            wr(IDX_CMD,  1);
            n := 0;
            st := (others => '0');
            while st(0) = '0' and n < 12000 loop
               rd(IDX_STATUS); st := got; n := n + 1;
            end loop;
            if n >= 12000 then
               report "  FAIL xfer: never completed (txn_state "
                      & integer'image(to_integer(txn_state)) & ")" severity note;
               errn := errn + 1;
            end if;
         end procedure;

      begin
         report "=== i2c_master: the blocks compose, and nothing is added at the top ==="
                severity note;

         -- T1. A reset master drives neither line. Not one of the blocks holds a line out of
         --     reset, which is what a shared bus depends on.
         do_reset;
         report "T1  a reset master drives neither line" severity note;
         ck_bit("T1 SCL released", m_scl_low, '0');
         ck_bit("T1 SDA released", m_sda_low, '0');
         ck_bit("T1 both lines high", scl and sda, '1');
         for j in 0 to 59 loop
            step;
            if m_scl_low = '1' or m_sda_low = '1' then
               report "  FAIL T1 the idle master drove a line" severity note;
               errn := errn + 1;
            end if;
         end loop;
         ck_int("T1 nothing on the wire",
                to_integer(mo_nsta) + to_integer(mo_nsto) + to_integer(mo_nbyte), 0);

         -- T2. A WRITE, from four register writes and a poll. This is the whole driver.
         do_reset;
         wr(IDX_TX0, 16#A5#);
         xfer(TADDR, '0', 1, '1');
         report "T2  a one-byte write, from four register writes and a poll" severity note;
         ck_bit("T2 done", st(0), '1');
         ck_bit("T2 and ok", st(1), '1');
         ck_int("T2 one byte moved", to_integer(unsigned(st(7 downto 4))), 1);
         ck_int("T2 the target received it", to_integer(unsigned(t1_lw)), 16#A5#);
         ck_int("T2 two bytes on the wire", to_integer(mo_nbyte), 2);
         ck_int("T2 one START and one STOP",
                to_integer(mo_nsta) + to_integer(mo_nsto), 2);

         -- T3. A READ, and the payload comes back through the register map.
         do_reset;
         preload(0, 16#3C#);
         preload(1, 16#5E#);
         xfer(TADDR, '1', 2, '1');
         rd(IDX_RX0); ck_int("T3 the first byte", to_integer(unsigned(got)), 16#3C#);
         rd(IDX_RX0); ck_int("T3 the second byte", to_integer(unsigned(got)), 16#5E#);
         report "T3  a two-byte read, returned through the register map" severity note;
         ck_bit("T3 ok", st(1), '1');
         ck_int("T3 two bytes moved", to_integer(unsigned(st(7 downto 4))), 2);

         -- T4. AN ADDRESS NACK reaches the driver as its own code, not as a timeout.
         do_reset;
         wr(IDX_TX0, 16#FF#);
         xfer("1111010", '0', 2, '1');
         rd(IDX_ERR);
         report "T4  an address NACK reaches the driver as its own code" severity note;
         ck_bit("T4 done", st(0), '1');
         ck_bit("T4 and not ok", st(1), '0');
         ck_int("T4 zero bytes moved", to_integer(unsigned(st(7 downto 4))), 0);
         ck_int("T4 the address-NACK code", to_integer(unsigned(got)), 16#01#);
         ck_int("T4 the bus was framed anyway", to_integer(mo_nsto), 1);

         -- T5. A STRETCHING TARGET is survived, not fought.
         do_reset;
         wr(IDX_TX0, 16#11#);
         wr(IDX_TX0, 16#22#);
         xfer(TADDR2, '0', 2, '1');
         report "T5  a target that stretches after every byte is survived, not fought"
                severity note;
         ck_bit("T5 ok", st(1), '1');
         ck_int("T5 two bytes moved", to_integer(unsigned(st(7 downto 4))), 2);
         ck_int("T5 the target received both", to_integer(t2_rx), 2);
         ck_int("T5 the last was 0x22", to_integer(unsigned(t2_lw)), 16#22#);
         ck_bit("T5 and the stretch was noticed", stretch_seen, '1');
         if to_integer(n_scyc) < 12 then
            report "  FAIL T5 only " & integer'image(to_integer(n_scyc))
                   & " stretch cycles counted" severity note;
            errn := errn + 1;
         end if;

         -- T6. A DATA NACK is a different code from an address NACK.
         do_reset;
         for j in 0 to 3 loop wr(IDX_TX0, 16#40# + j); end loop;
         xfer(TADDR2, '0', 4, '1');
         rd(IDX_ERR);
         report "T6  a data NACK is a different code from an address NACK" severity note;
         ck_bit("T6 done", st(0), '1');
         ck_bit("T6 not ok", st(1), '0');
         ck_int("T6 the data-NACK code, not the address one",
                to_integer(unsigned(got)), 16#02#);
         ck_int("T6 two of the four bytes were accepted",
                to_integer(unsigned(st(7 downto 4))), 2);
         ck_int("T6 and the bus was framed", to_integer(mo_nsto), 1);

         -- T7. A COMBINED TRANSACTION, composed by the driver: two STARTs and ONE STOP.
         do_reset;
         preload(0, 16#91#);
         wr(IDX_TX0, 16#00#);
         xfer(TADDR, '0', 1, '0');            -- no STOP: hold the bus
         ck_bit("T7 phase one ok", st(1), '1');
         ck_int("T7 no STOP yet", to_integer(mo_nsto), 0);
         xfer(TADDR, '1', 1, '1');            -- the turnaround and the read
         rd(IDX_RX0);
         report "T7  a combined transaction, composed from two commands by the driver"
                severity note;
         ck_int("T7 the byte came back", to_integer(unsigned(got)), 16#91#);
         ck_int("T7 two STARTs", to_integer(mo_nsta), 2);
         ck_int("T7 and exactly one STOP", to_integer(mo_nsto), 1);
         ck_bit("T7 the bus is idle", mo_intr, '0');

         -- T8. RECOVERY OF A STUCK SDA. §3.1.16.
         do_reset;
         wait until falling_edge(clk); fault_sda <= '1';
         for j in 0 to 9 loop step; end loop;
         rd(IDX_ERR);
         ck_bit("T8 a stuck SDA was diagnosed", got(4), '1');
         wait until falling_edge(clk); start_recovery <= '1';
         step;
         wait until falling_edge(clk); start_recovery <= '0';
         n := 0;
         while recovering = '0' and n < 500 loop step; n := n + 1; end loop;
         for j in 0 to 59 loop step; end loop;
         wait until falling_edge(clk); fault_sda <= '0';   -- the wedged device lets go
         n := 0;
         while recovering = '1' and n < 3000 loop step; n := n + 1; end loop;
         report "T8  a stuck SDA is clocked free, and the bus is left framed" severity note;
         ck_bit("T8 recovered", recovered, '1');
         ck_bit("T8 no escalation", escalate, '0');
         -- Wired, not merely willing: recovery must have driven the CLOCK through the
         -- top-level OR, and SDA to build the closing STOP.
         if rec_drove_scl = 0 then
            report "  FAIL T8 recovery never drove SCL onto the bus" severity note;
            errn := errn + 1;
         end if;
         if rec_drove_sda = 0 then
            report "  FAIL T8 recovery never drove SDA, so it cannot have framed the bus"
                   severity note;
            errn := errn + 1;
         end if;
         ck_bit("T8 SDA is free", sda, '1');
         ck_bit("T8 and nothing is being driven", m_scl_low or m_sda_low, '0');

         -- T9. A STUCK SCL GETS NO PULSES -- the central asymmetry of §3.1.16.
         do_reset;
         wait until falling_edge(clk); fault_scl <= '1'; fault_sda <= '1';
         for j in 0 to 9 loop step; end loop;
         rd(IDX_ERR);
         ck_bit("T9 a stuck SCL was diagnosed", got(5), '1');
         wait until falling_edge(clk); start_recovery <= '1';
         step;
         wait until falling_edge(clk); start_recovery <= '0';
         n := 0;
         while recovering = '1' and n < 3000 loop step; n := n + 1; end loop;
         report "T9  a stuck SCL gets no pulses and escalates immediately" severity note;
         ck_bit("T9 escalated", escalate, '1');
         ck_bit("T9 not recovered", recovered, '0');
         ck_bit("T9 and the master never drove SCL against the holder", m_scl_low, '0');

         -- T10. RECOVERY IS REFUSED DURING A TRANSFER.
         do_reset;
         wr(IDX_TX0, 16#5C#);
         wr(IDX_ADDR, 16#A0#); wr(IDX_LEN, 1); wr(IDX_CTRL, 1); wr(IDX_CMD, 1);
         n := 0;
         while txn_busy = '0' and n < 500 loop step; n := n + 1; end loop;
         wait until falling_edge(clk); start_recovery <= '1';
         for j in 0 to 19 loop step; end loop;
         wait until falling_edge(clk); start_recovery <= '0';
         report "T10 recovery asked for during a transfer is refused, not run" severity note;
         ck_bit("T10 recovery did not start", recovering, '0');
         -- And it drove nothing -- the property that matters to the device whose live
         -- byte would otherwise be clocked apart.
         ck_int("T10 recovery drove no clock", rec_drove_scl, 0);
         ck_int("T10 recovery drove no data", rec_drove_sda, 0);
         n := 0;
         while txn_done = '0' and n < 8000 loop step; n := n + 1; end loop;
         ck_bit("T10 and the transfer completed normally", txn_ok, '1');
         ck_int("T10 with the byte delivered", to_integer(unsigned(t1_lw)), 16#5C#);

         -- T11. THE DATA-VALID RULE HELD OVER THE WHOLE RUN.
         report "T11 SDA never changed mid-byte while SCL was high, over the whole run"
                severity note;
         ck_int("T11 no violations", bad_change, 0);
         ck_int("T11 no SDA owner conflicts, across every block", to_integer(n_conf), 0);
         ck_int("T11 no arbitration losses on a single-master bus", to_integer(n_arb), 0);
         ck_int("T11 and no mid-byte framing", to_integer(mo_nmid), 0);

         -- T12. A LONG SEQUENCE, to show nothing accumulates.
         do_reset;
         for j in 0 to 7 loop preload(j, 16#E0# + j); end loop;
         for j in 0 to 2 loop
            wr(IDX_TX0, 16#30# + j);
            xfer(TADDR, '0', 1, '1');
            ck_bit("T12 the write succeeded", st(1), '1');
            xfer(TADDR, '1', 1, '1');
            ck_bit("T12 the read succeeded", st(1), '1');
         end loop;
         report "T12 six transactions back to back, and nothing accumulates" severity note;
         ck_int("T12 six transactions", to_integer(n_txn), 6);
         ck_int("T12 six STARTs and six STOPs",
                to_integer(mo_nsta) + to_integer(mo_nsto), 12);
         ck_int("T12 no conflicts", to_integer(n_conf), 0);
         ck_int("T12 no violations", bad_change, 0);
         ck_bit("T12 and the master is idle with both lines released",
                m_scl_low or m_sda_low, '0');
         -- T13. ARBITRATION, AND THE LATCH THAT MUST CLEAR. The fault injector acts as a
         --      competing master: it pulls SDA low while this master transmits a one with
         --      SCL high, which is §3.1.8's exact condition. The second half is what no
         --      block bench can check -- arb_lost is a STICKY latch and the top level
         --      decides when it clears. A master that never clears it reports the first
         --      contest forever.
         do_reset;
         wr(IDX_TX0, 16#FF#);                 -- all ones: every bit is losable
         wr(IDX_ADDR, to_integer(unsigned(TADDR & '0')));
         wr(IDX_LEN, 1); wr(IDX_CTRL, 1); wr(IDX_CMD, 1);
         n := 0;
         while txn_busy = '0' and n < 500 loop step; n := n + 1; end loop;
         wait until falling_edge(clk); fault_sda <= '1';
         n := 0;
         while arb_lost = '0' and n < 4000 loop step; n := n + 1; end loop;
         report "T13 a competing zero on SDA is an arbitration loss, and the latch clears"
                severity note;
         ck_bit("T13 arbitration was reported lost", arb_lost, '1');
         if to_integer(n_arb) < 1 then
            report "  FAIL T13 the loss was not counted" severity note;
            errn := errn + 1;
         end if;
         wait until falling_edge(clk); fault_sda <= '0';
         n := 0;
         while txn_busy = '1' and n < 8000 loop step; n := n + 1; end loop;

         -- A clean transaction on a quiet bus: the latch must be gone.
         wr(IDX_TX0, 16#2A#);
         wr(IDX_ADDR, to_integer(unsigned(TADDR & '0')));
         wr(IDX_LEN, 1); wr(IDX_CTRL, 1); wr(IDX_CMD, 1);
         n := 0;
         while txn_done = '0' and n < 8000 loop step; n := n + 1; end loop;
         ck_bit("T13 and the latch cleared for the next transaction", arb_lost, '0');
         ck_bit("T13 which then succeeded on a quiet bus", txn_ok, '1');



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

   end architecture sim;

5b. Execution

DesignSystemVerilogVerilog-2001VHDLFinish
i2c_masterPASS 13/13PASS 13/13PASS 13/1344220 ns, all three

6. Mutation Testing — What Only Integration Can Catch

Eight defects, all of them at the wiring level, because that is the only thing this file contains. Five survived the original bench — the highest survival rate anywhere in the module, and the reason is instructive: every block had been verified, so the bench had been written as if the blocks were the thing under test.

#Injected defectExpected detectionResult
M1recovery allowed during a transferT10 after strengtheningKILLED (3)
M2the SCL contributions ANDed instead of ORedT2, T3, T12KILLED (38)
M3recovery dropped from the SCL pathT8 after strengtheningKILLED (2)
M4the framer dropped from the SCL pathT2, T7KILLED (34)
M5SDA priority reversed—EQUIVALENT, proven; see below
M6arbitration judged without transmit intentT3, T12KILLED (36)
M7recovery's SDA contribution droppedT8 after strengtheningKILLED (2)
M8the arbitration latch never clearedT13 newKILLED (3)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
valid non-equivalent mutants: 7      killed: 7      survived: 0
documented equivalent mutant: 1      (M5, proven by measurement)
baseline PASS before injection; PASS after restore.

The four survivors were all "is it wired?" questions

M1, M3, M7 and M8 share a shape that no block bench can reach:

The fix was three observers that watch the pins while a block believes it is acting:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
rec_drove_scl   cycles where recovery was active AND the master's SCL was driven
rec_drove_sda   the same for SDA

T8 now requires both to be non-zero — recovery must have clocked the bus and driven SDA to build its closing STOP. T10 requires both to be zero during a refused request, which is the property that matters to the device whose live byte would otherwise be clocked apart.

M8 needed a new test entirely. arb_lost is a sticky latch and the top level decides when it clears — arb_clear(txn_done). Nothing in the original bench ever lost arbitration, because a single-master bus cannot. T13 uses the fault injector as a competing master: it holds SDA low while the master transmits 0xFF, so every bit is losable, and then checks the second half that no block bench can see — that a subsequent clean transaction finds the latch cleared. A master that never clears it reports the first contest forever, and a driver cannot distinguish a new loss from an old one.

Note also where T13 had to be placed: after T12, because T11 asserts n_arb == 0 on a single-master bus. That false-positive check is worth keeping, so the arbitration test goes last rather than weakening it.

M5 is equivalent, and the proof is a measurement

Reversing the framer's and bit engine's relative priority survived. Rather than assume or assert, the question was measured — instrument the top level and count the cycles in which both request SDA:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
PROBE simultaneous framer+bit-engine SDA requests: 0 cycles
   (over the entire thirteen-test regression)

They never contend. During a framing sequence the byte engine is inactive; during a byte the framer is idle. So the priority never resolves anything, and reversing it cannot change behaviour.

That is a real finding about the design rather than a gap in the bench:

7. Verification Connection — What an Integration Bench Is For

Azvya Education Pvt. Ltd.VLSI Mentor
integration_env.sv — the checks that only exist at the top
   // Every block in this master has its own verified bench. So what is the top-level
   // environment FOR? Section 6 answers it empirically: five of eight wiring defects
   // survived a bench written as though the blocks were under test.
   //
   // THREE CLASSES OF CHECK BELONG ONLY HERE.
   //
   // 1. CONNECTIVITY UNDER ACTIVITY. Not "is the net present" -- a netlist check finds
   //    that -- but "does this block's output reach the pin WHILE the block believes it
   //    is acting". The observers of section 6 are exactly this, and they are cheap:
   //
   //       assert property (@(posedge clk) recovering && rec_expects_clock
   //                        |-> ##[0:2] scl_drive_low);
   //
   // 2. STICKY STATE ACROSS TRANSACTIONS. arb_lost, the error codes, the byte counters:
   //    a block bench runs one scenario and checks the outcome. Only a sequence of
   //    transactions can show that transaction N+1 does not inherit N's verdict, and
   //    that is a top-level decision (arb_clear, and the command register's clear-on-
   //    write of section 17.2 T10).
   //
   // 3. CROSS-BLOCK TIMING HANDOVERS. The SCL handover overlaps by one cycle in both
   //    directions. Neither the framer's bench nor the generator's can test it, because
   //    each has only one of the two owners. The property is:
   //
   //       never (SCL rises while a transfer is open and neither owner is driving)
   //
   //    which is a statement about two blocks' outputs and the bus at once.
   //
   // WHAT DOES NOT BELONG HERE: re-testing block behaviour. A top-level bench that
   // re-checks tHD;STA is slow, redundant, and will be deleted the first time it is in
   // somebody's way -- taking its connectivity checks with it. Keep them separable.
   //
   // THE DRIVER IS THE STIMULUS. This bench writes four registers and polls, which is
   // what firmware does. That is deliberate: a top-level bench that pokes internal
   // signals to set up a scenario is testing a configuration the product cannot reach.

8. FPGA and ASIC Implications

On an FPGA, this file is where the three-signal pin form meets the actual tri-state buffer. scl_drive_low and sda_drive_low drive output enables with data inputs tied low; scl_in and sda_in come from the buffers' input pins through two-flop synchronisers. The synchronisers belong here rather than inside a block, because there is exactly one pad per line and duplicating the synchroniser per consumer would let two blocks disagree about what the bus did.

Timing closure is trivial — the longest combinational path is the SDA arbiter's four-request priority cone plus the pad — and that is the decomposition paying off again: no block contains a path that crosses another block.

On an ASIC, the same structure maps onto two open-drain pad cells, and the integration-level requirement is that escalate reach something able to act. A reset controller, a PMIC sequencer, or a firmware-visible status bit; Chapter 17.11 §9 made the point and this is the level at which it is wired or not.

The register interface is deliberately a trivial address/data/strobe port rather than APB or AHB. Wrapping it is an integration task with its own verification, and keeping the seam explicit means the master can be dropped into either without editing anything inside it.

9. Debugging — The Master That Recovered Nothing, Successfully

Symptom

An FPGA design integrates a verified I2C master. Every block-level regression passes. On the board, normal transactions work perfectly. When a sensor occasionally wedges the bus by holding SDA low, the driver's recovery routine reports success -- the master sets its recovered flag and clears its error -- and the bus remains dead. A scope shows no activity at all on SCL during the reported recovery.

Root Cause

A missing term in one OR expression at the top level. The error manager was correct, verified, and unwired: its clock contribution never reached the pad, so nine pulses were generated onto a dangling net. Every block bench passed because each one connects the block under test to a bus model directly -- the very wiring the integration omitted is the wiring a block bench supplies for itself. The defect is structurally invisible below the top level, and the symptom is worse than a failure because the master reports success.

Fix
Restore the term, and then add the check that would have caught it: while recovery is active, the master's SCL output must actually be driven at some point. That is an integration-level observer -- count cycles where recovering and the pin-facing drive are both asserted, and require the count to be non-zero -- and it is what test T8 now asserts, along with the same check on SDA for the closing STOP. Note that a netlist connectivity check would NOT have caught it: the net exists and is driven by two of the three sources, so nothing is dangling. The property is about activity, not connectivity.

Three generalisations.

Every block was correct and the system did nothing. Block-level verification is necessary and says nothing about composition. The wiring a block bench supplies for itself is precisely the wiring the integration can omit.

A connectivity check would have passed. The net existed and had two drivers. The missing one is only detectable by asking whether it ever contributed, which is a question about activity over time rather than about structure.

Reporting success was worse than failing. A recovery that escalated would have sent the driver to a hardware reset, which would have worked. Reporting recovered left the bus dead with no further action taken — and the flag was truthful about the block's internal state.

10. Common Misconceptions

"An I²C master is a big state machine." The finished top level has no case statement. Every state lives in the block whose time base it belongs to. §1.

"The FSM should be designed first." Design it first and it must hold four unrelated time bases, so it acquires counters — which are the other three machines, admitted late and untested. §1.

"Integration is just wiring, so it needs no verification." Five of eight wiring defects survived a bench that had been written as though the blocks were under test. §6.

"If every block passes, the system works." Every block was correct in §9 and the system drove nothing. Block benches supply the very wiring the integration can omit. §9.

"A netlist connectivity check catches a missing contribution." Not when the net has other drivers. The property is whether a source ever contributed, which is activity, not structure. §9.

"Priority in the SDA arbiter sequences normal operation." It never resolves anything: measured, the framer and bit engine contend for zero cycles across the whole regression. It exists to keep the bus defined when a bug occurs. §6.

"A surviving mutant means a missing test." M5 survives because the contention it reorders never happens. That was established by measurement, not assumed. §6.

"Recovery can be gated in software." The gate is one AND with !in_transfer in hardware. Software cannot see the transfer state with the timing resolution required. §2.

"Synchronisers belong in the blocks that read the lines." One pad per line means one synchroniser per line, or two blocks can disagree about what the bus did. §8.

"A top-level bench should re-check block behaviour." It becomes slow and redundant and gets deleted — taking its connectivity checks with it. §7.

11. Reason It Through

Why does designing the FSM first produce a design that gets rewritten?

Because it must hold four states that change on four unrelated events, so it needs either four sets of transitions per state or bolted-on counters. Those counters are the other three machines without their invariants. §1.

Name the three decisions the top level makes, and why each cannot live in a block.

Who owns SDA, because four blocks want it and no one of them can see the others. Who owns SCL, because the handover spans two blocks. When recovery may run, because only the top level sees both the request and the transfer state. §2.

A verified recovery block reports nine pulses issued and the scope shows an idle SCL. Where is the defect?

In the top-level OR that combines SCL contributions — recovery's term is missing, so its pulses reach a net that goes nowhere. The block is correct and unwired. §9.

Why would a netlist connectivity check not have found it?

Because the net exists and is driven by the other two sources. Nothing is dangling; what is missing is one source's contribution, which is only visible as activity over time. §9.

Why did four wiring mutants survive a bench in which every block was already verified?

Because each block bench connects its DUT to a bus model itself — supplying exactly the wiring the integration can omit. A block behaving correctly is independent of whether its outputs reach a pin. §6.

M5 reversed the SDA priority and nothing changed. How was that established rather than assumed?

By instrumenting the top level and counting cycles in which the framer and bit engine both request SDA. The count was zero across the whole regression, so the priority never resolves anything. §6.

If the priority is never exercised, why keep it?

Because it keeps the bus in a defined state when a bug does produce simultaneous requests — which Chapter 17.10 §10 shows happens with two software threads. The resolving half is a safety net; owner_conflict is the load-bearing half. §6.

Why is T13 placed after T12 rather than with the other feedback tests?

Because T11 asserts zero arbitration losses on a single-master bus — a false-positive check worth keeping. Injecting a competing master earlier would break it. §6.

12. Understanding Check

13. Summary

There is no state machine in the finished master's top level. Every state already exists inside a block that needed it, and integration is wiring plus three decisions.

That follows from decomposing by time base rather than by vocabulary. The four bases change on four unrelated events, so a single FSM would take their product and then acquire counters — which are the other three machines without their invariants.

The three decisions are who owns SDA, who owns SCL, and when recovery may run. Each spans blocks that cannot see one another, which is exactly why they cannot live lower down.

The SCL handover overlaps deliberately, so the line never rises between owners and a framer's SDA fall cannot become a START where a STOP was meant.

One AND gate separates a recovery feature from a corruption source — start_recovery && !in_transfer.

Eight wiring mutants, and five survived the original bench. The highest survival rate in the module, because the bench had been written as though the already-verified blocks were the thing under test.

Four of the five were "is it wired?" questions. A block bench proves the block behaves; it connects the DUT to a bus model itself, which is the very wiring the integration can omit.

The fix was observers that watch the pins while a block believes it is acting — recovery must actually drive the clock and SDA when it runs, and must drive nothing when it is refused.

The fifth needed a whole new test, because arb_lost is sticky and only a sequence of transactions shows that the next one does not inherit the last one's verdict — which required a competing master, and placement after the test that asserts a quiet bus.

And one mutant is equivalent, proven by measurement: the framer and bit engine contend for zero cycles across the whole regression, so reversing their priority changes nothing. The arbiter's resolving half is a safety net for a bug; its reporting half is what earns its keep.

14. What Comes Next

The master works, end to end, verified from the register interface down to the pins. What it is not yet is a configurable engineering block.

Chapter 17.13 closes the module by turning the parameters into a coherent set: cycle counts derived from a system frequency and a speed mode by the ceiling rule, elaboration-time guards that refuse an illegal configuration, counter widths that follow from the counts rather than being guessed, and the achieved frequency reported as an output because it is generally not the one that was requested.

It is also where two findings from earlier in the module come due. Chapter 17.2 §7 showed a defect invisible at N_BUF = 8; Chapter 17.3 §10 showed a period formula unfalsifiable at 100 MHz. Both said the same thing — the parameter space is part of the test space — and the final chapter is where that stops being an observation and becomes a test.

Continue learning

Related tutorials