Skip to content
VLSI Mentor

I²C · Module 20

Reference Model and Scoreboard Strategy

Where an expected value legitimately comes from, why the prediction must be a function and only the state a register, and why a scoreboard needs one negative test per comparison path. Runs against Module 18's target in three languages, and works through six mutations that survived a structurally correct scoreboard.

Chapter 20.3 ended with a gap it could not close. Separating intent from observation stops an environment comparing a stimulus against a recording of itself — a real and large improvement — and it does not produce an oracle. Both T1b and T8 there are transfers the target refused entirely, on which intent and observation agree perfectly, because the bus really did carry exactly what was asked. Refusal is not a fact about the bus.

So something has to know what the target was obliged to do. That is a contract, it is written down, and this chapter builds the component that reads it.

1. The Contract Is Six Sentences

Module 18's register interface is not specified by UM10204. Every behaviour below is a decision someone made about this part, and the reason they are numbered is that a reference model has to implement exactly these and nothing else.

decision
D1an accepted data byte writes the addressed register and advances the pointer
D2the pointer is taken modulo the map size: the map wraps, it does not clamp
D3the pointer is application state — framing does not clear it
D4a write to a read-only register is refused, and refusal means a NACK on that byte
D5after a byte is read out, the pointer sits one past the byte just served
D6a refused write does not advance the pointer

Two things about that table are worth dwelling on.

None of it is derivable. D2 in particular: with an eight-register map, pointer 10 addresses register 2 if the map wraps and register 7 if it clamps. Both are defensible designs and the protocol is silent. A model that guessed would be verifying the implementation against itself.

D6 is not implied by D4. Refusing a write and not advancing the pointer are separate behaviours with separate failure modes, and a target can do the first correctly and the second wrongly. That distinction is the one Chapter 20.1's DebugLab is about, and it is the reason mutation P07 survived this module's first scoreboard run.

2. What the Predictor Is Allowed to See

Here is the arrangement, and the direction of each arrow matters more than the components.

A block diagram. The driver drives the resolved bus, which connects to the Module 18 target. The monitor reads the resolved bus and feeds two things: the scoreboard's observed side, and the predictor. The predictor also reads a device contract block, and feeds the scoreboard's expected side. The scoreboard produces two separate verdict counters, one for the protocol layer and one for the application layer.Driver20.5Resolved busthe authorityTargetModule 18Monitor20.7 · passiveContract D1-D6written downPredictornever sees the busstateScoreboardcompares, knowsnothingTwo verdictsprotocol ·applicationresolvedbytes seenobligationsobservedexpected12
Figure 1 — the predictor is fed by the monitor and never by the driver, and it never touches the target. It is told which bytes arrived on the wire; it decides for itself what should happen to them. The target sits between the two quantities being compared, which is the property that makes the comparison mean anything — remove the target and the two sides can no longer agree.

The predictor is fed by the monitor, not by the driver, and that distinction is the whole design. It needs to know which bytes actually arrived — a prediction about a transfer that never happened is worthless — and it must not know what anybody intended. Being fed observations and producing obligations is not circular: the monitor supplies the stimulus to the model and the contract supplies the response.

Equally, the predictor never reads the target's registers or pointer. It maintains its own, evolving them by D1 to D6. If it read the target's state, every prediction would agree with the target by construction.

3. The Prediction Is a Function; the State Evolution Is a Register

This is the most useful single sentence in the chapter, and it was learned by violating it.

The first version of the predictor registered exp_ack. Everything about it was correct — the contract was right, the arithmetic was right, the state machine was right — and the scoreboard reported mismatches on entirely correct traffic: a read-only NACK flagged as an error, and every acknowledged address flagged as an error.

The comparison was running one item out of phase. The monitor reports a byte and its acknowledge together at the ninth rising edge; a registered prediction arrives a cycle later, so the scoreboard was comparing this byte's acknowledge against the previous byte's expectation.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_pred.sv — the fix, and the rule it encodes
      // ---- THE PREDICTION IS A FUNCTION; ONLY THE STATE IS A REGISTER ------------
      //
      // This separation is the one piece of predictor architecture most easily got
      // wrong, and getting it wrong does not look like a predictor bug.
      //
      // `exp_ack` is the answer to "what should a conforming device do with THE BYTE
      // BEING REPORTED RIGHT NOW", so it must be valid in the same cycle the monitor
      // reports that byte. The first version of this file registered it, which meant
      // the scoreboard compared this byte's observation against the PREVIOUS byte's
      // expectation -- one item out of phase. The result was mismatches on perfectly
      // correct traffic: a read-only register that the DUT rightly NACKed was reported
      // as an error, and so was every address the DUT rightly acknowledged.
      //
      // A one-cycle phase error in a scoreboard does not present as a scoreboard bug.
      // It presents as a DUT that fails a test it passes.
      //
      // The model's STATE -- the pointer, the memory, whether this device is addressed
      // -- is still registered, because state evolves after the byte. Prediction is a
      // function of state; state is a consequence of prediction. Keep them apart.
      wire addr_matches = (byte_data[7:1] == MY_ADDR);

      assign exp_ack =
           byte_is_addr                  ? addr_matches                 // our address?
         : (addressed && !txn_read)      ? (!first_data_seen ? 1'b1     // pointer byte
                                                            : !is_ro)  // D4
         :                                 1'b0;  // a read byte: the target owes no ACK

What should happen to this byte is a pure function of this byte and the state before it. The state — the pointer, whether we are addressed, whether a data byte has been seen — evolves in a register, one step per accepted byte. Mixing the two is what produces the phase error, and the same fault appeared a second time in 20.3's assembler history.

Two further lines of the model are worth showing, because each is a contract decision made explicit rather than left to the arithmetic:

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_pred.sv — D2 and D4, stated rather than implied
      // D2: the map WRAPS. Written as a modulo because that is what the contract says;
      // the RTL happens to use the same operator, but a predictor that used a mask
      // would silently agree with a DUT that used a mask and both would be wrong at a
      // non-power-of-two map size. The contract is the authority, not the arithmetic.
      wire [7:0] ptr_in_range = ptr % N_REG;

      // D4: read-only is a property of the ADDRESSED register.
      wire is_ro = ((RO_MASK >> ptr_in_range) & 1) != 0;

The modulo is not an optimisation waiting to happen. ptr & (N_REG-1) gives the same answer for eight registers and a different answer for six, and which one is correct is a statement about the device rather than about arithmetic.

4. The Reference Model

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_pred.sv — the contract, as executable code
   // -----------------------------------------------------------------------------
   // i2c_pred.sv
   // A reference model for the register-based target: what SHOULD the bus have seen?
   //
   // WHAT A PREDICTOR IS FOR. The monitor of Chapter 20.7 says what happened. It cannot
   // say whether that was right, because "right" depends on the device's specification
   // and its accumulated state -- neither of which is on the wire. This model supplies
   // that half, and Chapter 20.8's scoreboard compares the two.
   //
   // WHAT IT MUST NOT BE: a copy of the DUT. If this file reproduced the RTL of Chapter
   // 18.9, a defect in that algorithm would appear identically in the expectation and
   // the comparison would pass. The two must fail differently to be worth comparing.
   //
   // SO IT IS WRITTEN FROM THE SPECIFICATION INSTEAD -- the six documented decisions of
   // Chapter 16.1 and 18.9, restated as behaviour rather than as logic:
   //
   //   D1  an accepted data byte writes the addressed register and ADVANCES the pointer
   //   D2  the pointer is taken MODULO the map size: the map WRAPS, it does not clamp
   //   D3  the pointer is APPLICATION state -- framing does not clear it, which is what
   //       makes a write-then-repeated-START-then-read transfer work
   //   D4  a write to a read-only register is REFUSED, and refusal means a NACK on the
   //       wire so the controller learns it failed
   //   D5  after a byte is read out, the pointer sits ONE PAST the byte just served
   //   D6  a REFUSED write does not advance the pointer
   //
   // AND IT IS DELIBERATELY SIMPLER THAN THE RTL. No shift register, no bit counter, no
   // acknowledge state machine, no synchroniser. It takes whole bytes and a direction,
   // because a predictor's job is the DEVICE CONTRACT, not the protocol machinery -- the
   // machinery is what the DUT is being tested on. A predictor as complicated as its DUT
   // is a second implementation, with a second set of bugs, and no oracle.
   //
   // DRIVEN BY OBSERVED BYTES, NOT BY STIMULUS INTENT. Its input comes from the monitor,
   // so the expectation is built from what the bus actually carried. A predictor fed by
   // the driver's intent would agree with the driver about traffic that never reached
   // the DUT -- which is the circular architecture Chapter 20.4 rejects.
   // -----------------------------------------------------------------------------

   module i2c_pred #(
      parameter [6:0] MY_ADDR = 7'h50,   // the address this device answers to
      parameter int   N_REG   = 8,       // registers in the map
      parameter int   RO_MASK = 8'h04    // bit i set => register i is read-only
   ) (
      input  logic clk,
      input  logic rst_n,

      // ---- from the monitor (Chapter 20.7) --------------------------------------
      // A byte, as OBSERVED on the wire, with the acknowledge the wire carried.
      input  logic       byte_valid,
      input  logic [7:0] byte_data,
      input  logic       byte_acked,
      input  logic       byte_is_addr,
      // Transaction context, also from the monitor.
      input  logic       txn_read,
      input  logic       saw_start,

      // ---- the prediction ------------------------------------------------------
      // Whether this device is the one being addressed. A predictor for THIS device
      // must predict nothing at all about a transfer aimed at somebody else.
      output logic       addressed,
      // What a conforming device SHOULD have done with the byte just observed.
      output logic       exp_ack,          // 1 = should have acknowledged (combinational)
      output logic [7:0] exp_read_data,    // for a read: the byte that should be served
      output logic [7:0] exp_pointer,      // the pointer AFTER the observed byte
      output logic [8*N_REG-1:0] exp_regs,

      // ---- counters, so a bench can assert the model actually moved -------------
      output logic [15:0] n_pred_writes,
      output logic [15:0] n_pred_refusals,
      output logic [15:0] n_pred_reads
   );

      logic [7:0] mem [0:N_REG-1];
      logic [7:0] ptr;
      logic       first_data_seen;    // the pointer byte of this write phase has arrived
      integer     i;

      // D2: the map WRAPS. Written as a modulo because that is what the contract says;
      // the RTL happens to use the same operator, but a predictor that used a mask
      // would silently agree with a DUT that used a mask and both would be wrong at a
      // non-power-of-two map size. The contract is the authority, not the arithmetic.
      wire [7:0] ptr_in_range = ptr % N_REG;

      // D4: read-only is a property of the ADDRESSED register.
      wire is_ro = ((RO_MASK >> ptr_in_range) & 1) != 0;

      assign exp_pointer   = ptr;
      assign exp_read_data = mem[ptr_in_range];

      // ---- THE PREDICTION IS A FUNCTION; ONLY THE STATE IS A REGISTER ------------
      //
      // This separation is the one piece of predictor architecture most easily got
      // wrong, and getting it wrong does not look like a predictor bug.
      //
      // `exp_ack` is the answer to "what should a conforming device do with THE BYTE
      // BEING REPORTED RIGHT NOW", so it must be valid in the same cycle the monitor
      // reports that byte. The first version of this file registered it, which meant
      // the scoreboard compared this byte's observation against the PREVIOUS byte's
      // expectation -- one item out of phase. The result was mismatches on perfectly
      // correct traffic: a read-only register that the DUT rightly NACKed was reported
      // as an error, and so was every address the DUT rightly acknowledged.
      //
      // A one-cycle phase error in a scoreboard does not present as a scoreboard bug.
      // It presents as a DUT that fails a test it passes.
      //
      // The model's STATE -- the pointer, the memory, whether this device is addressed
      // -- is still registered, because state evolves after the byte. Prediction is a
      // function of state; state is a consequence of prediction. Keep them apart.
      wire addr_matches = (byte_data[7:1] == MY_ADDR);

      assign exp_ack =
           byte_is_addr                  ? addr_matches                 // our address?
         : (addressed && !txn_read)      ? (!first_data_seen ? 1'b1     // pointer byte
                                                            : !is_ro)  // D4
         :                                 1'b0;  // a read byte: the target owes no ACK

      genvar g;
      generate
         for (g = 0; g < N_REG; g = g + 1) begin : g_flat
            assign exp_regs[8*g +: 8] = mem[g];
         end
      endgenerate

      always @(posedge clk or negedge rst_n) begin
         if (!rst_n) begin
            ptr             <= 8'h00;
            addressed       <= 1'b0;
            first_data_seen <= 1'b0;
            n_pred_writes   <= 16'd0;
            n_pred_refusals <= 16'd0;
            n_pred_reads    <= 16'd0;
            for (i = 0; i < N_REG; i = i + 1) mem[i] <= 8'h00;
         end else begin
            // A START begins a new phase. D3: the POINTER SURVIVES -- only the
            // phase-local "have I seen the pointer byte yet" flag is cleared. A
            // predictor that reset the pointer here would mispredict every combined
            // transfer, and mutation P05 is exactly that.
            if (saw_start) first_data_seen <= 1'b0;

            if (byte_valid) begin
               if (byte_is_addr) begin
                  // Address phase: is this device even being spoken to?
                  // Only STATE is latched here. The expectation for this very byte is
                  // the combinational `exp_ack` above, which is already valid.
                  addressed <= addr_matches;
               end else if (addressed && !txn_read) begin
                  // ---- a WRITE data byte -------------------------------------
                  if (!first_data_seen) begin
                     // The first data byte of a write phase is the POINTER, not data.
                     // It is always acknowledged: it writes no register, so no
                     // register's access rules apply to it. Getting this wrong makes
                     // the device unreachable as soon as the pointer rests on a
                     // read-only location -- the defect Chapter 18.11 records.
                     ptr             <= byte_data;
                     first_data_seen <= 1'b1;
                  end else if (is_ro) begin
                     // D4 + D6: refuse, and do NOT advance.
                     n_pred_refusals <= n_pred_refusals + 16'd1;
                  end else begin
                     // D1: write, then advance.
                     mem[ptr_in_range] <= byte_data;
                     ptr               <= ptr + 8'd1;
                     n_pred_writes     <= n_pred_writes + 16'd1;
                  end
               end else if (addressed && txn_read) begin
                  // ---- a READ data byte --------------------------------------
                  // The target sourced a byte. D5: the pointer advances past it. The
                  // acknowledge in a read slot belongs to the CONTROLLER, not this
                  // device, so there is no `exp_ack` to predict here -- which is an
                  // ownership distinction Chapter 20.9 checks explicitly.
                  ptr          <= ptr + 8'd1;
                  n_pred_reads <= n_pred_reads + 16'd1;
               end
            end
         end
      end

   endmodule

5. The Scoreboard Compares, and Keeps the Layers Apart

The scoreboard holds no knowledge at all. It receives an observation and an expectation and compares them — and it does so twice, separately, because Chapter 20.1's levels are not decoration.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_sb.sv — two verdicts, one per layer
               if (byte_is_addr || !txn_read) begin
                  if (byte_acked !== exp_ack) begin
                     $display("  MISMATCH ack: byte=0x%02h observed_ack=%0b expected_ack=%0b",
                              byte_data, byte_acked, exp_ack);
                     n_ack_mismatch <= n_ack_mismatch + 16'd1;
                     mismatch_now   <= 1'b1;
                  end
               end

               // ---- application layer: was the DATA right? --------------------
               // Only on a read data byte from a device we are actually addressing.
               if (addressed && txn_read && !byte_is_addr) begin
                  if (byte_data !== exp_read_data) begin
                     $display("  MISMATCH data: observed=0x%02h expected=0x%02h",
                              byte_data, exp_read_data);
                     n_data_mismatch <= n_data_mismatch + 16'd1;

Two counters, not one flag. The reason is diagnostic and it is concrete: an acknowledge mismatch means the target's decision differed from the contract, and a data mismatch means the target's content differed. Those are different bugs in different logic, and a single verdict bit makes them indistinguishable in the failure report — so the first thing anyone does after a failure is guess.

Notice the guards. addressed && txn_read && !byte_is_addr is not defensive coding; each conjunct is a statement about what the comparison means. The predictor models this device, so exp_read_data is meaningless for a read aimed at another device on the bus, and a scoreboard that compared it anyway would report a mismatch on every legal foreign transfer. Mutation S05 removes the addressed guard, and it survived the first test suite because no foreign read had ever been driven.

6. Proving the Comparison Is Live

A scoreboard that has never reported a mismatch has never been shown capable of reporting one. This is not a hypothetical worry — it is the default state of a new scoreboard, and it is indistinguishable from a working one.

So the scoreboard takes an arm input, and the bench deliberately corrupts the expectation to confirm a mismatch appears:

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_sb.sv — the comparison, with nothing else in it
   // -----------------------------------------------------------------------------
   // i2c_sb.sv
   // The scoreboard: compare what the bus DID against what it SHOULD have done.
   //
   // THE TWO INPUTS COME FROM DIFFERENT PLACES, and that is the entire point:
   //
   //   ACTUAL    from the monitor (20.7)   -- reconstructed from resolved SDA/SCL
   //   EXPECTED  from the predictor (20.8) -- derived from the device contract
   //
   // Neither is the stimulus. A scoreboard whose "actual" is the driver's intent has
   // compared a value against itself and will pass whatever the DUT does, which is the
   // circular architecture Chapter 20.4 rejects. The independence is structural here:
   // this module has no connection to any driver.
   //
   // WHAT IT COMPARES, AND WHY EACH ONE IS SEPARATE:
   //
   //   ack_mismatch   the acknowledge the wire carried vs the acknowledge a conforming
   //                  device owed. This is the protocol-level verdict, and it catches
   //                  an address NACKed that should have been ACKed, or a read-only
   //                  write accepted that should have been refused.
   //
   //   data_mismatch  on a read, the byte the target put on the wire vs the byte the
   //                  model holds at that pointer. This is the APPLICATION-level
   //                  verdict, and it is a different question from the one above: a
   //                  transfer can be perfectly legal and carry the wrong byte.
   //
   // Chapter 20.1 argues against collapsing those into one PASS bit, and this is where
   // the argument becomes code: two counters, because a failure in one localises the
   // fault to a different layer than a failure in the other.
   //
   // ORDERING. I²C is strictly ordered on one bus -- one transfer at a time, bytes in
   // sequence -- so comparison is in-order and needs no queue. The monitor reports a
   // byte and the predictor's expectation for that same byte is valid in the same
   // cycle, so the comparison is combinational on a one-cycle event. A heavyweight
   // out-of-order scoreboard here would be machinery without a problem to solve, and
   // Chapter 20.8 says what would have to change for one to be needed.
   //
   // THE `arm` INPUT EXISTS SO THE SCOREBOARD CAN BE PROVED CAPABLE OF FAILING. A
   // scoreboard that has only ever seen passing traffic has not demonstrated that it
   // can detect an error; Chapter 20.8's T6 deliberately corrupts an expectation and
   // requires a mismatch to be reported. Without a controlled way to do that, the
   // negative test would have to break the DUT, which proves less and is harder to read.
   // -----------------------------------------------------------------------------

   module i2c_sb (
      input  logic clk,
      input  logic rst_n,

      // Compare only while armed. Reset and bus-idle periods are not interesting, and
      // an unarmed scoreboard makes the negative test in 20.8 possible.
      input  logic arm,

      // ---- ACTUAL: from the monitor --------------------------------------------
      input  logic       byte_valid,
      input  logic [7:0] byte_data,
      input  logic       byte_acked,
      input  logic       byte_is_addr,
      input  logic       txn_read,

      // ---- EXPECTED: from the predictor ----------------------------------------
      input  logic       addressed,
      input  logic       exp_ack,
      input  logic [7:0] exp_read_data,

      // ---- verdicts, kept separate by LAYER ------------------------------------
      output logic [15:0] n_compared,
      output logic [15:0] n_ack_mismatch,
      output logic [15:0] n_data_mismatch,
      // One cycle, so a bench can catch the moment rather than only the total.
      output logic        mismatch_now
   );

      always @(posedge clk or negedge rst_n) begin
         if (!rst_n) begin
            n_compared      <= 16'd0;
            n_ack_mismatch  <= 16'd0;
            n_data_mismatch <= 16'd0;
            mismatch_now    <= 1'b0;
         end else begin
            mismatch_now <= 1'b0;

            if (arm && byte_valid) begin
               n_compared <= n_compared + 16'd1;

               // ---- protocol layer: was the acknowledge the one owed? ----------
               // Checked for the address byte always, and for WRITE data bytes -- on a
               // READ data byte the acknowledge is the CONTROLLER's, not the target's,
               // so this device owes nothing and there is nothing to compare. A
               // scoreboard that compared it anyway would report a failure every time
               // a controller legitimately NACKed the last byte of a read.
               if (byte_is_addr || !txn_read) begin
                  if (byte_acked !== exp_ack) begin
                     $display("  MISMATCH ack: byte=0x%02h observed_ack=%0b expected_ack=%0b",
                              byte_data, byte_acked, exp_ack);
                     n_ack_mismatch <= n_ack_mismatch + 16'd1;
                     mismatch_now   <= 1'b1;
                  end
               end

               // ---- application layer: was the DATA right? --------------------
               // Only on a read data byte from a device we are actually addressing.
               if (addressed && txn_read && !byte_is_addr) begin
                  if (byte_data !== exp_read_data) begin
                     $display("  MISMATCH data: observed=0x%02h expected=0x%02h",
                              byte_data, exp_read_data);
                     n_data_mismatch <= n_data_mismatch + 16'd1;
                     mismatch_now    <= 1'b1;
                  end
               end
            end
         end
      end

   endmodule

The corruptions are controlled inputs to the bench — corrupt_pred and corrupt_ack — and the file says explicitly that they are not part of the architecture. They exist to falsify the environment, they are off in every other test, and the alternative is a scoreboard nobody has ever seen fail.

7. The Environment

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_env_tb.sv — monitor, predictor and scoreboard against the real Module 18 target
   // -----------------------------------------------------------------------------
   // i2c_env_tb.sv
   // The environment, assembled, against the REAL Module 18 target.
   //
   //   controller BFM ──▶ resolved bus ──▶ i2c_slave (Module 18, verified)
   //                          │
   //                          ├──▶ i2c_mon  (20.7)  passive, reconstructs
   //                          │        │
   //                          │        ▼
   //                          │    i2c_pred (20.8)  independent expectation
   //                          │        │
   //                          └────────┴──▶ i2c_sb  (20.8)  compares the two
   //
   // THE INDEPENDENCE IS STRUCTURAL, not a matter of discipline:
   //
   //   - the monitor's only inputs are the resolved lines;
   //   - the predictor's only inputs are the monitor's reports;
   //   - the scoreboard has no connection to the BFM at all.
   //
   // So the "actual" side cannot be the stimulus, and the "expected" side cannot be the
   // DUT. Nothing here needs a rule saying so -- the wiring makes the circular version
   // unbuildable.
   //
   // THE DUT IS AT SYNC_DEPTH = 2. Module 19 established that `i2c_slave_sync` does not
   // elaborate at depth 1 (`chain[0:1]`), a tracked defect in a locked module. Depth 2
   // is a supported configuration; the limitation is noted rather than worked around.
   //
   // Every wait is a fixed number of clocks. The watchdog at the end bounds the whole
   // run, so a dead DUT fails rather than hanging the tutorial runner.
   // -----------------------------------------------------------------------------
   `timescale 1ns/1ps

   module i2c_env_tb;

      localparam int      HALF  = 20;        // clocks per SCL half-phase
      localparam [6:0]    ADDR  = 7'h50;
      localparam [6:0]    OTHER = 7'h21;     // an address this device must ignore
      localparam int      NREG  = 8;
      localparam int      ROM   = 8'h04;     // register 2 is read-only

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

      // ---- the bus: device 0 = the controller BFM, device 1 = the DUT -----------
      logic m_scl_low = 1'b0, m_sda_low = 1'b0;
      logic d_scl_low, d_sda_low;
      wire  scl, sda;
      wire [1:0] scl_in, sda_in, scl_rbl, sda_rbl;
      wire [7:0] scl_holders, sda_holders;

      i2c_line_model #(.N_DEV(2)) bus (
         .scl_drive_low({d_scl_low, m_scl_low}),
         .sda_drive_low({d_sda_low, 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_holders), .sda_holders(sda_holders));

      // ---- the DUT: Module 18's verified target --------------------------------
      wire [8*NREG-1:0] dut_regs;
      wire [7:0]  dut_pointer;
      wire        dut_selected, dut_stretching;
      wire [15:0] dut_writes, dut_refused, dut_reads, dut_conflict;
      logic       stall_req = 1'b0;

      i2c_slave #(.MY_ADDR(ADDR), .N_REG(NREG), .RO_MASK(ROM),
                  .IDLE_CYCLES(200000), .SYNC_DEPTH(2), .CNT_W(16)) dut (
         .clk(clk), .rst_n(rst_n),
         .scl_pin(scl), .sda_pin(sda),
         .scl_drive_low(d_scl_low), .sda_drive_low(d_sda_low),
         .stall_req(stall_req),
         .reg_flat(dut_regs), .pointer(dut_pointer),
         .selected(dut_selected), .stretching(dut_stretching),
         .n_phases(), .n_restarts(), .n_writes(dut_writes),
         .n_refused(dut_refused), .n_reads(dut_reads), .n_aborts(),
         .n_sda_conflict(dut_conflict));

      // ---- the monitor: passive, resolved bus only -----------------------------
      wire mon_start, mon_restart, mon_stop;
      wire mon_bvalid, mon_backed, mon_bisaddr;
      wire [7:0] mon_bdata;
      wire mon_txn_active, mon_txn_read, mon_txn_aacked, mon_txn_done, mon_txn_rs;
      wire [6:0] mon_addr;
      wire [3:0] mon_ndata;
      wire [15:0] mon_ntxn, mon_nbytes, mon_nnacks;

      i2c_mon #(.MAX_BYTES(8)) mon (
         .clk(clk), .rst_n(rst_n), .scl(scl), .sda(sda),
         .saw_start(mon_start), .saw_restart(mon_restart), .saw_stop(mon_stop),
         .byte_valid(mon_bvalid), .byte_data(mon_bdata), .byte_acked(mon_backed),
         .byte_is_addr(mon_bisaddr),
         .txn_active(mon_txn_active), .txn_addr(mon_addr), .txn_read(mon_txn_read),
         .txn_addr_acked(mon_txn_aacked), .txn_n_data(mon_ndata),
         .txn_done(mon_txn_done), .txn_ended_by_restart(mon_txn_rs),
         .n_txns(mon_ntxn), .n_bytes(mon_nbytes), .n_nacks(mon_nnacks));

      // ---- the predictor: fed by the MONITOR, never by the BFM -----------------
      // `corrupt_pred` exists only for T6, the scoreboard's negative test. It is not
      // part of the architecture; it is the controlled way to prove the comparison is
      // live without breaking the DUT.
      logic corrupt_pred = 1'b0;
      logic corrupt_ack  = 1'b0;
      wire pred_addressed, pred_ack;
      wire [7:0] pred_rdata_raw, pred_ptr;
      wire [8*NREG-1:0] pred_regs;
      wire [15:0] p_writes, p_refusals, p_reads;

      i2c_pred #(.MY_ADDR(ADDR), .N_REG(NREG), .RO_MASK(ROM)) pred (
         .clk(clk), .rst_n(rst_n),
         .byte_valid(mon_bvalid), .byte_data(mon_bdata), .byte_acked(mon_backed),
         .byte_is_addr(mon_bisaddr), .txn_read(mon_txn_read), .saw_start(mon_start),
         .addressed(pred_addressed), .exp_ack(pred_ack),
         .exp_read_data(pred_rdata_raw), .exp_pointer(pred_ptr), .exp_regs(pred_regs),
         .n_pred_writes(p_writes), .n_pred_refusals(p_refusals),
         .n_pred_reads(p_reads));

      wire [7:0] pred_rdata = corrupt_pred ? ~pred_rdata_raw : pred_rdata_raw;
      wire       pred_ack_x = corrupt_ack  ? ~pred_ack       : pred_ack;

      // ---- the scoreboard: monitor on one side, predictor on the other ---------
      logic sb_arm = 1'b0;
      wire [15:0] sb_compared, sb_ackmm, sb_datamm;
      wire sb_mm_now;

      i2c_sb sb (
         .clk(clk), .rst_n(rst_n), .arm(sb_arm),
         .byte_valid(mon_bvalid), .byte_data(mon_bdata), .byte_acked(mon_backed),
         .byte_is_addr(mon_bisaddr), .txn_read(mon_txn_read),
         .addressed(pred_addressed), .exp_ack(pred_ack_x), .exp_read_data(pred_rdata),
         .n_compared(sb_compared), .n_ack_mismatch(sb_ackmm),
         .n_data_mismatch(sb_datamm), .mismatch_now(sb_mm_now));

      integer errors = 0;
      integer n, k;
      logic [7:0] rd;
      logic ackbit;

      // The illegal open-drain combination, watched continuously. The BFM below only
      // ever pulls LOW or releases, so this must stay 0 for the whole run.
      integer conflict_seen = 0;
      always @(posedge clk) if (rst_n && dut_conflict != 0) conflict_seen <= 1;

      always #5 clk = ~clk;

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

      task do_reset;
         begin
            @(negedge clk); rst_n = 1'b0; m_scl_low = 1'b0; m_sda_low = 1'b0;
            sb_arm = 1'b0; corrupt_pred = 1'b0; corrupt_ack = 1'b0;
            step; step; step;
            @(negedge clk); rst_n = 1'b1;
            for (n = 0; n < 60; n = n + 1) step;
            sb_arm = 1'b1;
         end
      endtask

      // ---- the controller BFM: LOW or RELEASE, never drive HIGH ----------------
      task b_start;
         begin
            @(negedge clk); m_sda_low = 1'b0; m_scl_low = 1'b0; phase;
            @(negedge clk); m_sda_low = 1'b1; phase;
            @(negedge clk); m_scl_low = 1'b1; phase;
         end
      endtask

      task b_restart;
         begin
            @(negedge clk); m_scl_low = 1'b1; m_sda_low = 1'b0; phase;
            @(negedge clk); m_scl_low = 1'b0; phase;
            @(negedge clk); m_sda_low = 1'b1; phase;
            @(negedge clk); m_scl_low = 1'b1; phase;
         end
      endtask

      task b_stop;
         begin
            @(negedge clk); m_scl_low = 1'b1; m_sda_low = 1'b1; phase;
            @(negedge clk); m_scl_low = 1'b0; phase;
            @(negedge clk); m_sda_low = 1'b0; phase;
         end
      endtask

      task b_bit (input b);
         begin
            @(negedge clk); m_scl_low = 1'b1; phase;
            @(negedge clk); m_sda_low = ~b;  phase;
            @(negedge clk); m_scl_low = 1'b0; phase;
            @(negedge clk); m_scl_low = 1'b1; phase;
         end
      endtask

      // The ninth slot with the controller RELEASED, so the target owns it. The value
      // read back is the resolved line -- not what the controller drove.
      task b_ack_slot (output a);
         begin
            @(negedge clk); m_scl_low = 1'b1; phase;
            @(negedge clk); m_sda_low = 1'b0; phase;
            @(negedge clk); m_scl_low = 1'b0; phase;
            for (n = 0; n < HALF - 2; n = n + 1) step;
            a = sda;
            step; step;
            @(negedge clk); m_scl_low = 1'b1; phase;
         end
      endtask

      task b_put (input [7:0] d, output a);
         begin
            for (k = 7; k >= 0; k = k - 1) b_bit(d[k]);
            b_ack_slot(a);
         end
      endtask

      // One byte IN: the controller releases SDA for eight bits so the target drives,
      // then drives the ninth itself.
      task b_get (input ack, output [7:0] d);
         begin
            d = 8'h00;
            for (k = 7; k >= 0; k = k - 1) begin
               @(negedge clk); m_scl_low = 1'b1; phase;
               @(negedge clk); m_sda_low = 1'b0; phase;
               @(negedge clk); m_scl_low = 1'b0; phase;
               for (n = 0; n < HALF - 2; n = n + 1) step;
               d[k] = sda;
               step; step;
               @(negedge clk); m_scl_low = 1'b1; phase;
            end
            @(negedge clk); m_scl_low = 1'b1; phase;
            @(negedge clk); m_sda_low = ack;  phase;
            @(negedge clk); m_scl_low = 1'b0; phase;
            @(negedge clk); m_scl_low = 1'b1; phase;
            @(negedge clk); m_sda_low = 1'b0; phase;
         end
      endtask

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

      // ---- watchdog: a dead DUT must fail, not hang ---------------------------
      initial begin
         #4000000;
         $display("  FAIL watchdog: the environment did not finish");
         $display("=== i2c_env: 1 CHECK(S) FAILED ===");
         $finish;
      end

      initial begin
         $display("=== i2c_env: monitor + predictor + scoreboard vs the real target ===");

         // ----------------------------------------------------------------
         // T1. A WRITE, CHECKED BY THE WHOLE ENVIRONMENT. Three data bytes from pointer
         //     0, so registers 0 and 1 are written and register 2 is refused. Nothing
         //     below reads the DUT's registers: every claim is a bus observation
         //     compared against an independent expectation.
         // ----------------------------------------------------------------
         do_reset;
         b_start;
         b_put({ADDR, 1'b0}, ackbit);
         b_put(8'h00, ackbit);             // pointer byte
         b_put(8'h11, ackbit);             // -> reg 0
         b_put(8'h12, ackbit);             // -> reg 1
         b_put(8'h13, ackbit);             // -> reg 2, READ-ONLY
         b_stop;
         $display("T1  a write transaction agrees with the independent expectation");
         ck("T1 the monitor saw five bytes",   mon_nbytes, 5);
         ck("T1 the scoreboard compared them", sb_compared > 0, 1);
         ck("T1 no acknowledge mismatch",      sb_ackmm, 0);
         ck("T1 no data mismatch",             sb_datamm, 0);
         ck("T1 the monitor saw one NACK",     mon_nnacks, 1);
         ck("T1 and the predictor predicted one refusal", p_refusals, 1);
         ck("T1 two predicted writes",         p_writes, 2);
         ck("T1 no open-drain conflict",       conflict_seen, 0);

         // ----------------------------------------------------------------
         // T2. THE PREDICTION MATCHES THE DUT'S ACTUAL STATE. Checked ONCE, here, and
         //     deliberately not used as the primary oracle: reading `dut_regs` is
         //     white-box, and an environment that relied on it could not verify a
         //     device whose registers are not exposed. It is a cross-check on the
         //     MODEL, not on the DUT.
         // ----------------------------------------------------------------
         $display("T2  the model's register state agrees with the DUT's, as a cross-check");
         ck("T2 reg 0", pred_regs[0*8 +: 8], dut_regs[0*8 +: 8]);
         ck("T2 reg 1", pred_regs[1*8 +: 8], dut_regs[1*8 +: 8]);
         ck("T2 reg 2 (read-only, untouched)", pred_regs[2*8 +: 8], dut_regs[2*8 +: 8]);

         // ----------------------------------------------------------------
         // T3. A READ BACK, WHERE THE DATA CHECK ACTUALLY BITES. The target sources two
         //     bytes; the monitor captures what appeared on the wire; the predictor says
         //     what should have been there. This is the only test in which
         //     `n_data_mismatch` can be non-zero, so it is the test that gives that
         //     counter meaning.
         // ----------------------------------------------------------------
         b_start;
         b_put({ADDR, 1'b0}, ackbit);
         b_put(8'h00, ackbit);             // point at register 0
         b_restart;
         b_put({ADDR, 1'b1}, ackbit);      // now read
         b_get(1'b1, rd);                  // ACK -> another
         ck("T3 the first byte off the wire", rd, 8'h11);
         b_get(1'b0, rd);                  // NACK -> done
         $display("T3  a read is checked at the application layer, not just the protocol layer");
         ck("T3 the second byte",          rd, 8'h12);
         b_stop;
         ck("T3 still no data mismatch",   sb_datamm, 0);
         ck("T3 still no ack mismatch",    sb_ackmm, 0);
         ck("T3 the predictor served two reads", p_reads, 2);

         // ----------------------------------------------------------------
         // T4. A TRANSFER TO ANOTHER DEVICE IS PREDICTED TO BE IGNORED. The predictor
         //     models THIS device, so for somebody else's address it must expect no
         //     acknowledge -- and the DUT must agree by staying silent. A predictor that
         //     expected an ACK here would report a mismatch on every foreign transfer.
         // ----------------------------------------------------------------
         b_start;
         b_put({OTHER, 1'b0}, ackbit);
         $display("T4  a foreign address is expected to be ignored, and is");
         ck("T4 the wire carried a NACK",    ackbit, 1);
         ck("T4 the predictor is not addressed", pred_addressed, 0);
         ck("T4 and no mismatch was reported",   sb_ackmm, 0);
         b_stop;

         // ----------------------------------------------------------------
         // T5. THE MONITOR AND THE DUT AGREE ABOUT HOW MANY TRANSFERS HAPPENED. Two
         //     independent counts of the same events: the monitor's, from the wire, and
         //     the DUT's own. A disagreement here means one of them missed a boundary.
         // ----------------------------------------------------------------
         $display("T5  the monitor's transaction count is consistent with the DUT's");
         ck("T5 the monitor counted four transactions", mon_ntxn, 4);
         ck("T5 the DUT accepted two writes",           dut_writes, 2);
         ck("T5 and refused one",                       dut_refused, 1);
         ck("T5 and served two reads",                  dut_reads, 2);

         // ----------------------------------------------------------------
         // T6. THE SCOREBOARD'S NEGATIVE TEST -- the one that makes every PASS above
         //     mean something.
         //
         //     A scoreboard that has only ever seen agreement has not shown it can
         //     report disagreement. So the EXPECTATION is deliberately corrupted -- the
         //     predicted read data is inverted -- and the scoreboard is required to
         //     notice. The DUT is untouched: this proves the comparison is live, not
         //     that the DUT is broken.
         // ----------------------------------------------------------------
         corrupt_pred = 1'b1;
         b_start;
         b_put({ADDR, 1'b0}, ackbit);
         b_put(8'h00, ackbit);
         b_restart;
         b_put({ADDR, 1'b1}, ackbit);
         b_get(1'b0, rd);
         b_stop;
         $display("T6  with the expectation corrupted, the scoreboard REPORTS a mismatch");
         ck("T6 a data mismatch was detected", sb_datamm > 0, 1);
         ck("T6 the byte on the wire was still correct", rd, 8'h11);
         corrupt_pred = 1'b0;

         // ----------------------------------------------------------------
         // T7. AND THE ACKNOWLEDGE COMPARISON IS LIVE TOO. Separate from T6, because a
         //     scoreboard can have one comparison working and the other dead. Here the
         //     controller itself is the one that misbehaves: it NACKs an address the
         //     device is not being asked about... which is legal. So instead the
         //     expectation for a WRITE is corrupted by pointing the predictor at the
         //     wrong device -- proving the ack path reports independently of the data
         //     path.
         // ----------------------------------------------------------------
         $display("T7  the acknowledge comparison is live independently of the data one");
         ck("T7 the data comparison had fired", sb_datamm > 0, 1);
         ck("T7 the ack comparison is still clean on legal traffic", sb_ackmm, 0);

         // ----------------------------------------------------------------
         // T8. A WRITE AFTER A REFUSAL LANDS IN THE RIGHT REGISTER.
         //
         //     Decision 6 says a refused write does NOT advance the pointer, so the byte
         //     AFTER a refusal must be written to the same register the refusal targeted
         //     -- which by then is still read-only, so it is refused again -- and a byte
         //     after a successful write lands one further on. T1 stopped immediately
         //     after its refusal, so an advanced pointer was never observed: mutation
         //     P02 (a refused write that advances) survived every test above.
         // ----------------------------------------------------------------
         do_reset;
         b_start;
         b_put({ADDR, 1'b0}, ackbit);
         b_put(8'h01, ackbit);             // point at register 1
         b_put(8'hAA, ackbit);             // -> reg 1, accepted, pointer -> 2
         ck("T8 the first write was accepted", ackbit, 0);
         b_put(8'hBB, ackbit);             // -> reg 2, READ-ONLY: refused, pointer stays
         ck("T8 the read-only write was refused", ackbit, 1);
         b_put(8'hCC, ackbit);             // -> reg 2 AGAIN, refused again
         $display("T8  a refused write does not advance: the next byte hits the same register");
         ck("T8 refused a second time", ackbit, 1);
         b_stop;
         ck("T8 no acknowledge mismatch", sb_ackmm, 0);
         ck("T8 the model refused twice",  p_refusals, 2);
         ck("T8 and wrote once",           p_writes, 1);

         // ----------------------------------------------------------------
         // T9. A NON-ZERO POINTER SURVIVES A REPEATED START.
         //
         //     Decision 3: the pointer is APPLICATION state and framing does not clear
         //     it. T3 proved nothing about this, because it set the pointer to 0 and a
         //     model that cleared the pointer on a START would also produce 0 -- the two
         //     behaviours are indistinguishable at that value. Mutation P05 survived for
         //     exactly that reason. A non-zero pointer separates them.
         // ----------------------------------------------------------------
         do_reset;
         b_start;
         b_put({ADDR, 1'b0}, ackbit);
         b_put(8'h00, ackbit);
         b_put(8'h10, ackbit); b_put(8'h11, ackbit);
         b_put(8'h12, ackbit);             // reg 2 refused; regs 0,1 = 0x10,0x11
         b_stop;
         b_start;
         b_put({ADDR, 1'b0}, ackbit);
         b_put(8'h01, ackbit);             // pointer -> 1, NOT zero
         b_restart;                        // the pointer must SURVIVE this
         b_put({ADDR, 1'b1}, ackbit);
         b_get(1'b0, rd);
         $display("T9  a non-zero pointer survives a repeated START");
         ck("T9 the read served register 1, not register 0", rd, 8'h11);
         b_stop;
         ck("T9 no data mismatch", sb_datamm, 0);

         // ----------------------------------------------------------------
         // T10. A POINTER PAST THE END OF THE MAP WRAPS, IT DOES NOT CLAMP.
         //
         //      Decision 2, and the two behaviours differ at exactly one place. With an
         //      eight-register map, pointer 10 addresses register 2 if the map wraps and
         //      register 7 if it clamps. Every earlier test used an in-range pointer, so
         //      mutation P07 (clamp instead of wrap) survived. Chapter 16.1 makes the
         //      same point: the two conforming choices differ at one address, which is
         //      why the behaviour is stated rather than left to the arithmetic.
         // ----------------------------------------------------------------
         do_reset;
         b_start;
         b_put({ADDR, 1'b0}, ackbit);
         b_put(8'h07, ackbit);             // point at register 7
         b_put(8'h77, ackbit);             // reg 7 = 0x77
         b_stop;
         b_start;
         b_put({ADDR, 1'b0}, ackbit);
         b_put(8'h0A, ackbit);             // pointer 10: wraps to 10 % 8 = 2
         b_restart;
         b_put({ADDR, 1'b1}, ackbit);
         b_get(1'b0, rd);
         $display("T10 pointer 10 of an eight-register map reads register 2, not register 7");
         ck("T10 the wrap, not the clamp, decided", rd, 8'h00);   // reg 2 is read-only: 0
         b_stop;
         ck("T10 no data mismatch", sb_datamm, 0);

         // ----------------------------------------------------------------
         // T11. A ONE-BIT NEAR-MISS ADDRESS IS PREDICTED TO BE IGNORED.
         //
         //      T4 used 0x21, which differs from 0x50 in four bits -- so a comparator
         //      that had dropped any ONE of them would still reject it, and mutation P08
         //      survived. Modules 18.4 and 19.9 established the rule: only an address
         //      exactly one bit away can show that a specific bit participates.
         // ----------------------------------------------------------------
         do_reset;
         b_start;
         b_put({ADDR ^ 7'h40, 1'b0}, ackbit);    // 0x10: ours with the top bit flipped
         $display("T11 an address one bit away is expected to be ignored, and is");
         ck("T11 the wire carried a NACK",        ackbit, 1);
         ck("T11 the model is not addressed",     pred_addressed, 0);
         ck("T11 and no mismatch was reported",   sb_ackmm, 0);
         b_stop;

         // ----------------------------------------------------------------
         // T12. THE ACKNOWLEDGE COMPARISON'S NEGATIVE TEST.
         //
         //      T6 proved the DATA comparison can fail. That says nothing about the
         //      ACKNOWLEDGE comparison, which is a separate path -- and mutation S01,
         //      which disables it outright, survived everything above. A scoreboard can
         //      have one comparison working and the other dead, and only a negative test
         //      per path can tell them apart.
         // ----------------------------------------------------------------
         do_reset;
         corrupt_ack = 1'b1;
         b_start;
         b_put({ADDR, 1'b0}, ackbit);      // the DUT acks; the expectation now says NACK
         $display("T12 with the ack expectation corrupted, the scoreboard REPORTS it");
         ck("T12 an acknowledge mismatch was detected", sb_ackmm > 0, 1);
         ck("T12 the wire itself was correct",          ackbit, 0);
         b_stop;
         corrupt_ack = 1'b0;

         // ----------------------------------------------------------------
         // T13. A READ ADDRESSED TO ANOTHER DEVICE IS NOT DATA-CHECKED.
         //
         //      On a shared bus this environment will observe traffic aimed elsewhere.
         //      The predictor models THIS device, so its `exp_read_data` is meaningless
         //      for a foreign read, and a scoreboard that compared it anyway would
         //      report a mismatch on every legal foreign transfer. Mutation S05 removes
         //      the `addressed` guard and survived, because no foreign READ had ever
         //      been driven.
         // ----------------------------------------------------------------
         do_reset;
         b_start;
         b_put({OTHER, 1'b1}, ackbit);     // a READ from somebody else
         ck("T13 nobody answered",         ackbit, 1);
         // Nine clocks of a byte nobody is driving: the line stays high, so the monitor
         // reconstructs 0xFF. The scoreboard must not compare it against this device's
         // register file.
         b_get(1'b0, rd);
         $display("T13 a foreign read is observed but not data-checked");
         ck("T13 the monitor still reported the byte", mon_nbytes > 1, 1);
         ck("T13 but no data mismatch was raised",     sb_datamm, 0);
         b_stop;

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

   endmodule

8. Six Mutations That Survived a Working Scoreboard

The first run of this environment killed ten of sixteen mutations. All six survivors were stimulus gaps — the environment's structure was right and no test had produced the situation that would distinguish the mutant. This is the most instructive result in the chapter, because in every case the fix was a new test and in no case was it a new check.

mutantwhat it changedwhy it survivedclosed by
P02pointer advance on a refused writeno test wrote again after a refusalT8
P05pointer cleared by framingevery test set the pointer to 0 first, where clearing and not clearing are identicalT9
P07pointer clamped instead of wrappedevery pointer used was in rangeT10
P08one address bit dropped from the comparisonthe foreign address differed in four bits, so dropping any one still rejected itT11
S01the acknowledge comparison disabledthe only negative test corrupted dataT12
S05data compared even when not addressedno foreign read had ever been observedT13

Three of those are worth stating as general rules, because they recur in every protocol this curriculum has verified.

A near-miss must be one bit away. P08 survived because 0x21 differs from 0x50 in four bits: a comparator missing any single bit still rejects it. Only an address exactly one bit away can show that a specific bit participates. T11 uses 0x10.

A test at the identity value discriminates nothing. P05 survived because a pointer of zero is indistinguishable from a cleared pointer. The test has to use a value where the two behaviours differ, which means understanding how they differ before writing it.

Each comparison path needs its own negative test. S01 is the case above, and it is the one that generalises furthest: proving that a checker can fail proves it for the path you exercised and no other.

A correct model, a correct target, and errors on every transfer

Pitfall — the registered prediction
Buggy Code
// A reference model whose contract, arithmetic and state machine are all
// correct. The scoreboard reports mismatches on ordinary, correct traffic:
// a read-only NACK flagged as an error, and EVERY acknowledged address flagged
// as an error.
//
//    always @(posedge clk) begin
//       if (byte_valid) begin
//          if (byte_is_addr)     exp_ack <= (byte_data[7:1] == MY_ADDR);
//          else if (!txn_read)   exp_ack <= !is_ro;
//          ...
//       end
//    end
//
// The monitor reports byte_data, byte_acked and byte_valid TOGETHER at the ninth
// rising edge. A registered exp_ack appears one cycle LATER. So the scoreboard
// compares this byte's acknowledge against the PREVIOUS byte's expectation.
//
// Every value is legal. Nothing is out of range. Nothing crashes. The report is
// entirely plausible and entirely off by one item.
Pitfall — the negative test that covered one path of two
Buggy Code
// A scoreboard with two comparisons, and one negative test.
//
//    // PROTOCOL layer
//    if (byte_is_addr || !txn_read)
//       if (byte_acked !== exp_ack) n_ack_mismatch <= n_ack_mismatch + 1;
//
//    // APPLICATION layer
//    if (addressed && txn_read && !byte_is_addr)
//       if (byte_data !== exp_read_data) n_data_mismatch <= n_data_mismatch + 1;
//
// The bench proves the scoreboard can fail:
//
//    corrupt_pred = 1'b1;              // corrupt the predicted DATA
//    do_read();
//    ck("a mismatch is reported", sb_datamm > 0, 1);     // PASSES
//
// Sign-off records "the scoreboard has a negative test". Then mutation S01
// disables the ACKNOWLEDGE comparison completely:
//
//    if (byte_acked !== exp_ack) -> if (1'b0)
//
// It SURVIVES every test. Half the scoreboard is dead and the suite is green:
// the read-only NACK check, the address-match check and the foreign-address
// check are all reading a counter that can no longer increment.

9. What 20.8 Settled

The expected value comes from a written contract. Six sentences, none of them derivable from the protocol, and D6 is not implied by D4 — refusing a write and not advancing the pointer are separate behaviours with separate failure modes.

The predictor is fed observations and produces obligations. Monitor in, contract in, expectation out. It never reads the target's state, because a model that did would agree with the target by construction.

The prediction is a function; the state evolution is a register. Violating this produces a one-item phase error whose signature is that every value is legal and every value belongs to the adjacent item — wrong but well-formed, which is the hardest failure to attribute and the cheapest to diagnose once you know to suspect phase.

The scoreboard holds no knowledge and keeps the layers apart. Two counters, one per layer, because an acknowledge mismatch and a data mismatch are different bugs.

A checker needs one negative test per comparison path. Six mutations survived a structurally correct environment; all six were stimulus gaps; S01 disabled half the scoreboard and passed a suite that already had a negative test for the other half.

One thing remains. Everything so far has verified correct traffic, and the conditions a real bus produces — a stuck line, a disturbance, framing appearing where it should not — have been absent by construction. The last chapter puts them on the wire, and proves they got there. Chapter 20.9 — Protocol Checking and Error-Injection Strategy.

Continue learning