Skip to content
VLSI Mentor

I²C · Module 19

Simulation vs Synthesized Hardware — Where Behavior Diverges

Eight classes of mismatch that pass in RTL simulation and fail on a board, each with why simulation passes, what hardware does instead, and what evidence exposes it. Includes a bus model with a rise time, and an experiment running Module 18's verified target against it whose result is not the expected one.

Modules 17 and 18 finished with thirty tri-HDL combinations passing, eighty-six mutations killed, zero valid survivors and a published-code drift of zero. That is a strong result, and it is a statement about RTL simulation.

This chapter is about the distance between that statement and a working board. It is a catalogue, and then an experiment whose result surprised me.

1. Why Simulation Passes

RTL simulation is not a weak approximation of hardware. It is a different, self-consistent model, and it is exact about the things it models. The mismatches all come from the same place: it is exact about a set of things that does not include the ones this module has been about.

what simulation haswhat hardware has instead
a net that changes value instantlya conductor with resistance and capacitance
flip-flops that sample in zero time, always resolvingan aperture, and a resolution time constant
Z as a real value on any netno tri-state routing inside the fabric
one global time, identical for every observera threshold each receiver crosses at its own moment
a design whose structure is what you wrotea netlist a tool inferred, placed and routed
registers whose reset value is what you declaredpower-on state, and a reset that arrives somewhere
a testbench's idea of the other devicethe other device

Each row is a mismatch class. The rest of the chapter works through them in the order this module built them.

2. The Catalogue

Class 1 — an ideal bus has no rise time

Why simulation passes. Module 16's i2c_line_model makes a released line read 1 immediately. Every test in Modules 17 and 18 is written against that, and they are right to be: the model's job was ownership and arbitration.

What hardware does. Chapter 19.3: tr = 0.8473 · Rp · Cb, hundreds of nanoseconds. And crucially the two edges are asymmetric — falling is a transistor, rising is a resistor.

Evidence that exposes it. Section 3's experiment, in simulation. Or a scope triggered on SCL's rising edge, which shows a curve rather than an edge.

How to catch it earlier. Model it. Section 3 does.

Class 2 — "I released it, so it is high"

Why simulation passes. On an ideal bus the assumption is true, always, so a design containing it is indistinguishable from a correct one.

What hardware does. The line is still low for tr after release. A controller that samples too soon reads its own release as somebody else's pull, and reports arbitration loss on a bus with one controller.

Evidence. Chapter 19.1's T8 and T4 in simulation; i2c_bus_rc's T7 makes the mismatch a failing test.

How to catch it earlier. Never substitute drive intent for an observation — 19.2's always-live input path.

Class 3 — internal tri-state resolves in a simulator and not in fabric

Why simulation passes. Four-valued nets resolve Z against a pullup happily, anywhere in the hierarchy.

What hardware does. FPGA interconnect has no tri-state routing. The tool rewrites the net as multiplexers — changing timing and area — or refuses, and which you get depends on tool, version and family.

Evidence. Synthesis reports. Nothing in RTL simulation, ever.

How to catch it earlier. Keep Z at top-level inout ports only (19.2 §5).

Class 4 — metastability is absent from RTL simulation

Why simulation passes. A simulated flop has no aperture. Driving an edge into the clock edge yields whichever value the event ordering picks — deterministically, every run.

What hardware does. The flop may take longer than usual to resolve, with a probability exponential in the settling time allowed.

Evidence. A CDC report, and field failure statistics. No test.

How to catch it earlier. Structurally: a synchronizer, and a constraint that keeps its internal path timed (19.4, 19.6).

Class 5 — a false path that silently removes a safety mechanism

Why simulation passes. Constraints are not in the simulation at all. Behaviour is identical before and after the edit.

What hardware does. The synchronizer's internal path stops being timed, misses setup, and the MTBF drops by orders of magnitude — producing rare, random, internally-consistent wrong data.

Evidence. Reading the constraints. Not a timing report, which will say the design met timing.

How to catch it earlier. 19.6's static check, which rejects set_false_path with no endpoints and any path to all_registers.

Class 6 — a reset value that is wrong for the signal

Why simulation passes. It does not, necessarily — but only if a test looks at the release of reset, and most do not, because reset is treated as setup rather than as stimulus.

What hardware does. The same thing, deterministically: a synchronizer resetting to 0 on a bus that idles HIGH manufactures a START at every reset release.

Evidence. 19.4's T2 and 19.5's T1 — both check the state during reset and immediately after its release.

How to catch it earlier. Treat the release of reset as an event with consequences, and reset every register to its signal's idle state.

Class 7 — a filter threshold carried across a change of clock

Why simulation passes. The tests are written in clocks, and so is the threshold, so they agree with each other at any frequency.

What hardware does. The obligation is in nanoseconds. 19.5's DebugLab: N_SAMP = 3 rejects a 50 ns spike at 25 MHz and accepts it at 100 MHz.

Evidence. The arithmetic, and n_rejected on an ILA.

How to catch it earlier. Derive the count from a frequency and a duration, and check both bounds at elaboration.

Class 8 — the things that are not the RTL at all

These deserve naming because they are the most common causes of a first power-on failure and none of them is a design bug:

A pin constraint naming the wrong package pin. The design is perfect and connected to nothing. Caught by continuity, or by 19.8's step 2.

The wrong I/O standard or bank voltage. A 3.3 V bus on a 1.8 V bank reads a constant level.

No external pull-up fitted. 19.3.

An inverted output enable. 19.1's DebugLab — total failure from power-on, and invisible to every RTL test because the mapping is outside the simulated path.

No common ground between boards. Two devices with no shared reference have no agreed meaning for "low".

3. The Experiment: Does the Verified Target Survive a Rise Time?

Class 1 is the one mismatch on this list that can be moved into simulation, because a rise time has a digital consequence: for some number of clocks after the last device releases, a sampler still reads LOW. That is modellable without modelling anything analogue.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_bus_rc.sv — a wired-AND bus with a rise time
   // -----------------------------------------------------------------------------
   // i2c_bus_rc.sv
   // A bus model with a RISE TIME, and the reason Chapter 19.7 exists.
   //
   // Module 16's `i2c_line_model` resolves a wired-AND bus IDEALLY: the instant every
   // device releases, the line reads 1. That model is correct for what it was built to
   // verify -- ownership, arbitration, acknowledge -- and Modules 17 and 18 are
   // verified against it with 30/30 passing and zero surviving mutants.
   //
   // It is also the single most consequential simplification in those two modules,
   // because a real released line does not become 1 instantly. Chapter 19.3 worked the
   // arithmetic: tr = 0.8473 * Rp * Cb, which is hundreds of nanoseconds on an ordinary
   // board.
   //
   // THIS MODEL ADDS EXACTLY ONE THING: a released line takes RISE_CLKS clocks to read
   // as HIGH. Everything else is the same wired-AND. It is not an analogue model and
   // does not pretend to be -- there is no ramp, no threshold and no curve. It is the
   // DIGITAL CONSEQUENCE of a slow edge: for RISE_CLKS clocks after the last device
   // lets go, a sampler still reads LOW.
   //
   // WHAT THAT BUYS: a mismatch that RTL simulation normally cannot show becomes
   // simulatable. A design that assumes "released means high" passes against the ideal
   // model and fails against this one, in simulation, deterministically -- which turns
   // a hardware-only failure into a testbench.
   //
   // WHAT IT STILL CANNOT SHOW, and Chapter 19.7 §3 is explicit about it: a falling
   // edge is fast (a transistor pulling down) while a rising edge is slow (a resistor
   // charging), and the asymmetry is the point. This model has that asymmetry. It does
   // NOT have metastability, threshold variation between devices, temperature, or the
   // fact that two devices may see the crossing at different instants. Those remain
   // outside simulation entirely.
   // -----------------------------------------------------------------------------

   module i2c_bus_rc #(
      parameter int N_DEV     = 2,
      // Clocks a released line takes to be readable as HIGH. 0 reproduces the ideal
      // model of Module 16 exactly, which is what makes the comparison in T1 possible.
      parameter int RISE_CLKS = 0
   ) (
      input  logic clk,
      input  logic [N_DEV-1:0] drive_low,

      // The resolved level every device reads back.
      output wire  line,
      // How many devices are currently holding the line down.
      output wire  [7:0] holders,
      // 1 while the line has been released by everyone but has not yet risen. This is
      // the state the ideal model cannot represent, exposed so a bench can assert that
      // it was actually entered -- a test for this model is a test that the interesting
      // case occurred, not just that nothing broke.
      output wire  rising
   );

      integer i;
      wire  any_low;

      // INITIALISED TO THE SETTLED VALUE, so an untouched bus reads HIGH from time
      // zero. A real idle bus IS high -- nothing is pulling it and the pull-up has had
      // forever to charge -- so starting the counter anywhere else would model a board
      // that powers up with its bus mysteriously low. Left uninitialised it is X, and
      // at RISE_CLKS = 0 the comparison `X >= 0` makes `line` X for the whole
      // simulation, which is how the first version of this model failed T1.
      //
      // NOTE this module is a VERIFICATION COMPONENT, not synthesisable hardware: it
      // models a shared conductor, which is not something a design contains. The
      // declaration initialiser is appropriate here for that reason and would not be
      // in an RTL block.
      logic [15:0] rise_cnt = RISE_CLKS[15:0];

      // CONTINUOUS ASSIGNS, not `always @(*)`. An `always @(*)` block is triggered by a
      // CHANGE on something it reads, so if `drive_low` is initialised to its first
      // value and never changes before the first check, the block never runs and its
      // outputs stay X for the whole simulation. That is how the first version of this
      // model reported X for an idle bus, and it is a trap worth naming because the
      // symptom -- one failing test at exactly the first stimulus value -- looks like a
      // reset problem rather than a sensitivity problem.
      //
      // The popcount function mirrors Module 16's `i2c_line_model`, which counts holders
      // the same way for the same reason.
      function automatic [7:0] popcount (input [N_DEV-1:0] v);
         begin
            popcount = 8'd0;
            for (i = 0; i < N_DEV; i = i + 1) if (v[i]) popcount = popcount + 8'd1;
         end
      endfunction

      assign any_low = |drive_low;
      assign holders = popcount(drive_low);

      // The charge counter. Held at 0 while anybody pulls (a transistor discharges the
      // net quickly, so the FALLING edge is modelled as immediate -- which is the
      // asymmetry this model exists to carry). Counts up once everybody has let go.
      always @(posedge clk) begin
         if (any_low) rise_cnt <= 16'd0;
         else if (rise_cnt < RISE_CLKS[15:0]) rise_cnt <= rise_cnt + 16'd1;
      end

      // A released line reads LOW until it has been released for RISE_CLKS clocks.
      assign rising = ~any_low && (rise_cnt < RISE_CLKS[15:0]);
      assign line   = ~any_low && (rise_cnt >= RISE_CLKS[15:0]);

   endmodule

At RISE_CLKS = 0 it reproduces Module 16's ideal model exactly, which is what makes a controlled comparison possible. Its own bench verifies both that and the slow behaviour, and demonstrates the mismatch as a test that asserts a failure:

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_bus_rc_tb.sv — the self-checking testbench
   // -----------------------------------------------------------------------------
   // i2c_bus_rc_tb.sv
   // Independent oracle for i2c_bus_rc, and a demonstration of the mismatch.
   //
   // THE BENCH HAS TWO JOBS, and the second is the chapter's argument.
   //
   //   1. verify the model: at RISE_CLKS = 0 it must be indistinguishable from the
   //      ideal wired-AND of Module 16, and at RISE_CLKS = N a released line must read
   //      LOW for exactly N clocks. T1 to T5.
   //
   //   2. demonstrate that a design which assumes "released means HIGH" passes against
   //      the ideal model and FAILS against this one. T6 and T7 instantiate the same
   //      naive sampler twice, on the two models, and require the verdicts to DIFFER.
   //      A test that required them to agree would have proved the opposite.
   //
   // T7 is therefore a test that asserts a FAILURE, which is unusual enough to state
   // plainly: if the naive sampler ever passes against the RC model, this chapter's
   // central claim is wrong and the bench must say so.
   //
   // Every wait is a fixed number of clocks; nothing waits on the DUT.
   // -----------------------------------------------------------------------------
   `timescale 1ns/1ps

   module i2c_bus_rc_tb;

      localparam int N = 2;
      localparam int RISE = 4;        // a released line takes 4 clocks to read HIGH

      logic clk = 1'b0;
      logic [N-1:0] drv = 2'b00;

      // the ideal bus (RISE_CLKS = 0) and the slow bus (RISE_CLKS = 4), same stimulus
      logic ideal_line, slow_line, ideal_rising, slow_rising;
      logic [7:0] ideal_holders, slow_holders;

      integer errors = 0;
      integer n, k;

      i2c_bus_rc #(.N_DEV(N), .RISE_CLKS(0)) u_ideal (
         .clk(clk), .drive_low(drv), .line(ideal_line),
         .holders(ideal_holders), .rising(ideal_rising));

      i2c_bus_rc #(.N_DEV(N), .RISE_CLKS(RISE)) u_slow (
         .clk(clk), .drive_low(drv), .line(slow_line),
         .holders(slow_holders), .rising(slow_rising));

      // ---- the naive sampler, instantiated on BOTH buses ------------------------
      // "I released the line, so it is high now." This is the assumption under test,
      // and it is written as a one-line observer rather than a module because that is
      // how it appears in real code: not as a design decision, as an obvious step.
      //
      // `sampled_*` is what each copy concludes the line was, one clock after this
      // device released it.
      logic dev0_released;
      logic sampled_ideal, sampled_slow;
      logic n_wrong_ideal_seen = 1'b0, n_wrong_slow_seen = 1'b0;

      always @(posedge clk) begin
         dev0_released <= ~drv[0];
         // Each copy samples its own bus one clock after the release.
         if (dev0_released) begin
            sampled_ideal <= ideal_line;
            sampled_slow  <= slow_line;
            // Record whether the naive assumption "released => HIGH" was violated.
            if (drv == 2'b00 && !ideal_line) n_wrong_ideal_seen <= 1'b1;
            if (drv == 2'b00 && !slow_line)  n_wrong_slow_seen  <= 1'b1;
         end
      end

      always #5 clk = ~clk;

      task step; begin @(posedge clk); @(negedge clk); 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

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

      initial begin
         $display("=== i2c_bus_rc: a bus with a rise time, and the mismatch it exposes ===");

         // ----------------------------------------------------------------
         // T0. AN IDLE BUS IS HIGH FROM THE START, ON BOTH MODELS.
         //
         //     Checked BEFORE any clock edge, because that is the only window in which
         //     a wrong initial value is visible: the counter reaches its terminal value
         //     within RISE clocks, so by the time any other test looks, a model that
         //     started low has already caught up. Mutation F09 -- which starts the
         //     charge counter at zero instead of at its settled value -- survived every
         //     other test in this list for exactly that reason.
         //
         //     Physically this is the premise the whole model rests on: nothing is
         //     pulling, and the pull-up has had forever to charge. A bus that powers up
         //     LOW is modelling a board with a fault.
         // ----------------------------------------------------------------
         #1;
         $display("T0  an untouched bus reads HIGH before the first clock, on both models");
         ck("T0 nobody is pulling",          slow_holders, 0);
         ck("T0 the ideal bus is already high", ideal_line, 1);
         ck("T0 and so is the slow bus",        slow_line,  1);
         ck("T0 the slow bus is not 'charging'", slow_rising, 0);
         step;
         ck("T0 still high after one clock",    slow_line,  1);

         // ----------------------------------------------------------------
         // T1. AT RISE_CLKS = 0 THIS IS MODULE 16's MODEL. The whole comparison rests
         //     on the slow model being the ideal model plus one parameter, so the ideal
         //     case is checked first over every drive combination.
         // ----------------------------------------------------------------
         drv = 2'b00; step; step; step; step; step; step;
         for (k = 0; k < 4; k = k + 1) begin
            drv = k[1:0];
            step;
            ck_idx("T1 ideal: line is high iff nobody pulls", k, ideal_line,
                   (k == 0) ? 1 : 0);
            ck_idx("T1 ideal: holder count", k, ideal_holders,
                   (k[0] ? 1 : 0) + (k[1] ? 1 : 0));
            ck_idx("T1 ideal: never in the rising state", k, ideal_rising, 0);
         end
         $display("T1  at RISE_CLKS=0 the model is the ideal wired-AND of Module 16");

         // ----------------------------------------------------------------
         // T2. A PULL IS IMMEDIATE ON BOTH MODELS. The falling edge is a transistor
         //     discharging the net, so it is fast; the asymmetry between the two edges
         //     is the physical fact this model carries.
         // ----------------------------------------------------------------
         drv = 2'b00; repeat (8) step;
         ck("T2 both buses idle high", ideal_line & slow_line, 1);
         drv = 2'b01; step;
         $display("T2  pulling low is immediate on both models -- a transistor is fast");
         ck("T2 ideal went low", ideal_line, 0);
         ck("T2 slow went low too", slow_line, 0);
         ck("T2 and neither is 'rising'", ideal_rising | slow_rising, 0);

         // ----------------------------------------------------------------
         // T3. A RELEASE IS NOT. The ideal line is high on the next clock; the slow one
         //     takes RISE clocks, and reports `rising` while it is charging.
         // ----------------------------------------------------------------
         drv = 2'b00;
         for (n = 1; n <= RISE + 2; n = n + 1) begin
            step;
            ck_idx("T3 ideal is high immediately", n, ideal_line, 1);
            ck_idx("T3 slow is high only after RISE clocks", n, slow_line,
                   (n >= RISE) ? 1 : 0);
            ck_idx("T3 and reports 'rising' until then", n, slow_rising,
                   (n < RISE) ? 1 : 0);
         end
         $display("T3  releasing takes RISE clocks on the slow model, 0 on the ideal one");

         // ----------------------------------------------------------------
         // T4. A PULL DURING THE RISE ABORTS IT. This is the case a naive counter gets
         //     wrong: the line was charging, somebody pulled, and the charge is lost --
         //     so the next release starts the rise again from the beginning rather than
         //     continuing.
         // ----------------------------------------------------------------
         drv = 2'b01; repeat (3) step;              // settled low
         drv = 2'b00; step; step;                    // 2 of RISE=4 clocks of charging
         ck("T4 still charging, not yet high", slow_line, 0);
         ck("T4 and it says so",               slow_rising, 1);
         drv = 2'b01; step;                          // pulled again mid-rise
         ck("T4 the pull wins immediately",    slow_line, 0);
         ck("T4 and charging stopped",         slow_rising, 0);
         drv = 2'b00;
         for (n = 1; n <= RISE + 1; n = n + 1) begin
            step;
            ck_idx("T4 the rise restarts from zero", n, slow_line, (n >= RISE) ? 1 : 0);
         end
         $display("T4  a pull during the rise discards the charge -- it restarts, not resumes");

         // ----------------------------------------------------------------
         // T5. TWO HOLDERS, AND ONE LETTING GO CHANGES NOTHING. The rise begins when the
         //     LAST device releases, not the first.
         // ----------------------------------------------------------------
         drv = 2'b11; repeat (3) step;
         ck("T5 two holders", slow_holders, 2);
         drv = 2'b01; repeat (RISE + 2) step;
         ck("T5 one holder left, so still low", slow_line, 0);
         ck("T5 and not charging",              slow_rising, 0);
         drv = 2'b00;
         repeat (RISE) step;
         ck("T5 now it rises", slow_line, 1);
         $display("T5  the rise starts when the LAST device releases");

         // ----------------------------------------------------------------
         // T6. THE NAIVE SAMPLER IS RIGHT ON THE IDEAL BUS. "I released it, so it is
         //     high." Against Module 16's model that assumption never fails -- which is
         //     exactly why Modules 17 and 18 could pass 30/30 while containing it.
         // ----------------------------------------------------------------
         n_wrong_ideal_seen = 1'b0; n_wrong_slow_seen = 1'b0;
         drv = 2'b01; repeat (4) step;
         drv = 2'b00; repeat (2) step;
         $display("T6  on the IDEAL bus, 'released means high' is never contradicted");
         ck("T6 the ideal bus never contradicted the assumption", n_wrong_ideal_seen, 0);

         // ----------------------------------------------------------------
         // T7. AND WRONG ON THE SLOW ONE. Same stimulus, same sampler, different
         //     verdict. This test asserts that the assumption WAS contradicted, which is
         //     the chapter's claim stated as a check: if the naive sampler ever agrees
         //     with the slow bus, this chapter is wrong.
         // ----------------------------------------------------------------
         $display("T7  on the SLOW bus, the same assumption is contradicted");
         ck("T7 the slow bus DID contradict it", n_wrong_slow_seen, 1);
         ck("T7 so the two models disagree about the same stimulus",
            (n_wrong_ideal_seen === n_wrong_slow_seen) ? 1 : 0, 0);

         // ----------------------------------------------------------------
         // T8. AND THE DISAGREEMENT IS A FUNCTION OF RISE TIME, NOT OF LUCK. Swept over
         //     the delay after release: the naive sampler is correct exactly when it
         //     waits at least RISE clocks. That is the whole mismatch, as a number.
         // ----------------------------------------------------------------
         for (k = 0; k <= RISE + 1; k = k + 1) begin
            drv = 2'b01; repeat (4) step;      // settle low
            drv = 2'b00;                        // release
            for (n = 0; n < k; n = n + 1) step; // wait k clocks
            step;                               // then sample
            ck_idx("T8 the line reads high iff at least RISE clocks were waited", k,
                   slow_line, (k + 1 >= RISE) ? 1 : 0);
         end
         $display("T8  correctness is a function of how long you waited: %0d clocks", RISE);

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

   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_bus_rc.v — the same model in Verilog-2001
   // -----------------------------------------------------------------------------
   // i2c_bus_rc.v
   // A bus model with a RISE TIME, and the reason Chapter 19.7 exists.
   //
   // Module 16's `i2c_line_model` resolves a wired-AND bus IDEALLY: the instant every
   // device releases, the line reads 1. That model is correct for what it was built to
   // verify -- ownership, arbitration, acknowledge -- and Modules 17 and 18 are
   // verified against it with 30/30 passing and zero surviving mutants.
   //
   // It is also the single most consequential simplification in those two modules,
   // because a real released line does not become 1 instantly. Chapter 19.3 worked the
   // arithmetic: tr = 0.8473 * Rp * Cb, which is hundreds of nanoseconds on an ordinary
   // board.
   //
   // THIS MODEL ADDS EXACTLY ONE THING: a released line takes RISE_CLKS clocks to read
   // as HIGH. Everything else is the same wired-AND. It is not an analogue model and
   // does not pretend to be -- there is no ramp, no threshold and no curve. It is the
   // DIGITAL CONSEQUENCE of a slow edge: for RISE_CLKS clocks after the last device
   // lets go, a sampler still reads LOW.
   //
   // WHAT THAT BUYS: a mismatch that RTL simulation normally cannot show becomes
   // simulatable. A design that assumes "released means high" passes against the ideal
   // model and fails against this one, in simulation, deterministically -- which turns
   // a hardware-only failure into a testbench.
   //
   // WHAT IT STILL CANNOT SHOW, and Chapter 19.7 §3 is explicit about it: a falling
   // edge is fast (a transistor pulling down) while a rising edge is slow (a resistor
   // charging), and the asymmetry is the point. This model has that asymmetry. It does
   // NOT have metastability, threshold variation between devices, temperature, or the
   // fact that two devices may see the crossing at different instants. Those remain
   // outside simulation entirely.
   //
   // (Verilog-2001 -- structurally identical to the SystemVerilog above.)
   // -----------------------------------------------------------------------------

   module i2c_bus_rc #(
      parameter N_DEV     = 2,
      // Clocks a released line takes to be readable as HIGH. 0 reproduces the ideal
      // model of Module 16 exactly, which is what makes the comparison in T1 possible.
      parameter RISE_CLKS = 0
   ) (
      input  wire  clk,
      input  wire  [N_DEV-1:0] drive_low,

      // The resolved level every device reads back.
      output wire  line,
      // How many devices are currently holding the line down.
      output wire  [7:0] holders,
      // 1 while the line has been released by everyone but has not yet risen. This is
      // the state the ideal model cannot represent, exposed so a bench can assert that
      // it was actually entered -- a test for this model is a test that the interesting
      // case occurred, not just that nothing broke.
      output wire  rising
   );

      integer i;
      wire  any_low;

      // INITIALISED TO THE SETTLED VALUE, so an untouched bus reads HIGH from time
      // zero. A real idle bus IS high -- nothing is pulling it and the pull-up has had
      // forever to charge -- so starting the counter anywhere else would model a board
      // that powers up with its bus mysteriously low. Left uninitialised it is X, and
      // at RISE_CLKS = 0 the comparison `X >= 0` makes `line` X for the whole
      // simulation, which is how the first version of this model failed T1.
      //
      // NOTE this module is a VERIFICATION COMPONENT, not synthesisable hardware: it
      // models a shared conductor, which is not something a design contains. The
      // declaration initialiser is appropriate here for that reason and would not be
      // in an RTL block.
      reg  [15:0] rise_cnt = RISE_CLKS[15:0];

      // CONTINUOUS ASSIGNS, not `always @(*)`. An `always @(*)` block is triggered by a
      // CHANGE on something it reads, so if `drive_low` is initialised to its first
      // value and never changes before the first check, the block never runs and its
      // outputs stay X for the whole simulation. That is how the first version of this
      // model reported X for an idle bus, and it is a trap worth naming because the
      // symptom -- one failing test at exactly the first stimulus value -- looks like a
      // reset problem rather than a sensitivity problem.
      //
      // The popcount function mirrors Module 16's `i2c_line_model`, which counts holders
      // the same way for the same reason.
      function [7:0] popcount (input [N_DEV-1:0] v);
         begin
            popcount = 8'd0;
            for (i = 0; i < N_DEV; i = i + 1) if (v[i]) popcount = popcount + 8'd1;
         end
      endfunction

      assign any_low = |drive_low;
      assign holders = popcount(drive_low);

      // The charge counter. Held at 0 while anybody pulls (a transistor discharges the
      // net quickly, so the FALLING edge is modelled as immediate -- which is the
      // asymmetry this model exists to carry). Counts up once everybody has let go.
      always @(posedge clk) begin
         if (any_low) rise_cnt <= 16'd0;
         else if (rise_cnt < RISE_CLKS[15:0]) rise_cnt <= rise_cnt + 16'd1;
      end

      // A released line reads LOW until it has been released for RISE_CLKS clocks.
      assign rising = ~any_low && (rise_cnt < RISE_CLKS[15:0]);
      assign line   = ~any_low && (rise_cnt >= RISE_CLKS[15:0]);

   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_bus_rc_tb.v — the same tests in Verilog-2001
   // -----------------------------------------------------------------------------
   // i2c_bus_rc_tb.v
   // Independent oracle for i2c_bus_rc, and a demonstration of the mismatch.
   //
   // THE BENCH HAS TWO JOBS, and the second is the chapter's argument.
   //
   //   1. verify the model: at RISE_CLKS = 0 it must be indistinguishable from the
   //      ideal wired-AND of Module 16, and at RISE_CLKS = N a released line must read
   //      LOW for exactly N clocks. T1 to T5.
   //
   //   2. demonstrate that a design which assumes "released means HIGH" passes against
   //      the ideal model and FAILS against this one. T6 and T7 instantiate the same
   //      naive sampler twice, on the two models, and require the verdicts to DIFFER.
   //      A test that required them to agree would have proved the opposite.
   //
   // T7 is therefore a test that asserts a FAILURE, which is unusual enough to state
   // plainly: if the naive sampler ever passes against the RC model, this chapter's
   // central claim is wrong and the bench must say so.
   //
   // Every wait is a fixed number of clocks; nothing waits on the DUT.
   //
   // (Verilog-2001 -- the same tests as the SystemVerilog bench.)
   // -----------------------------------------------------------------------------
   `timescale 1ns/1ps

   module i2c_bus_rc_tb;

      localparam N = 2;
      localparam RISE = 4;        // a released line takes 4 clocks to read HIGH

      reg  clk = 1'b0;
      reg  [N-1:0] drv = 2'b00;

      // the ideal bus (RISE_CLKS = 0) and the slow bus (RISE_CLKS = 4), same stimulus
      wire ideal_line, slow_line, ideal_rising, slow_rising;
      wire [7:0] ideal_holders, slow_holders;

      integer errors = 0;
      integer n, k;

      i2c_bus_rc #(.N_DEV(N), .RISE_CLKS(0)) u_ideal (
         .clk(clk), .drive_low(drv), .line(ideal_line),
         .holders(ideal_holders), .rising(ideal_rising));

      i2c_bus_rc #(.N_DEV(N), .RISE_CLKS(RISE)) u_slow (
         .clk(clk), .drive_low(drv), .line(slow_line),
         .holders(slow_holders), .rising(slow_rising));

      // ---- the naive sampler, instantiated on BOTH buses ------------------------
      // "I released the line, so it is high now." This is the assumption under test,
      // and it is written as a one-line observer rather than a module because that is
      // how it appears in real code: not as a design decision, as an obvious step.
      //
      // `sampled_*` is what each copy concludes the line was, one clock after this
      // device released it.
      reg  dev0_released;
      reg  sampled_ideal, sampled_slow;
      reg  n_wrong_ideal_seen = 1'b0, n_wrong_slow_seen = 1'b0;

      always @(posedge clk) begin
         dev0_released <= ~drv[0];
         // Each copy samples its own bus one clock after the release.
         if (dev0_released) begin
            sampled_ideal <= ideal_line;
            sampled_slow  <= slow_line;
            // Record whether the naive assumption "released => HIGH" was violated.
            if (drv == 2'b00 && !ideal_line) n_wrong_ideal_seen <= 1'b1;
            if (drv == 2'b00 && !slow_line)  n_wrong_slow_seen  <= 1'b1;
         end
      end

      always #5 clk = ~clk;

      task step; begin @(posedge clk); @(negedge clk); 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

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

      initial begin
         $display("=== i2c_bus_rc: a bus with a rise time, and the mismatch it exposes ===");

         // ----------------------------------------------------------------
         // T0. AN IDLE BUS IS HIGH FROM THE START, ON BOTH MODELS.
         //
         //     Checked BEFORE any clock edge, because that is the only window in which
         //     a wrong initial value is visible: the counter reaches its terminal value
         //     within RISE clocks, so by the time any other test looks, a model that
         //     started low has already caught up. Mutation F09 -- which starts the
         //     charge counter at zero instead of at its settled value -- survived every
         //     other test in this list for exactly that reason.
         //
         //     Physically this is the premise the whole model rests on: nothing is
         //     pulling, and the pull-up has had forever to charge. A bus that powers up
         //     LOW is modelling a board with a fault.
         // ----------------------------------------------------------------
         #1;
         $display("T0  an untouched bus reads HIGH before the first clock, on both models");
         ck("T0 nobody is pulling",          slow_holders, 0);
         ck("T0 the ideal bus is already high", ideal_line, 1);
         ck("T0 and so is the slow bus",        slow_line,  1);
         ck("T0 the slow bus is not 'charging'", slow_rising, 0);
         step;
         ck("T0 still high after one clock",    slow_line,  1);

         // ----------------------------------------------------------------
         // T1. AT RISE_CLKS = 0 THIS IS MODULE 16's MODEL. The whole comparison rests
         //     on the slow model being the ideal model plus one parameter, so the ideal
         //     case is checked first over every drive combination.
         // ----------------------------------------------------------------
         drv = 2'b00; step; step; step; step; step; step;
         for (k = 0; k < 4; k = k + 1) begin
            drv = k[1:0];
            step;
            ck_idx("T1 ideal: line is high iff nobody pulls", k, ideal_line,
                   (k == 0) ? 1 : 0);
            ck_idx("T1 ideal: holder count", k, ideal_holders,
                   (k[0] ? 1 : 0) + (k[1] ? 1 : 0));
            ck_idx("T1 ideal: never in the rising state", k, ideal_rising, 0);
         end
         $display("T1  at RISE_CLKS=0 the model is the ideal wired-AND of Module 16");

         // ----------------------------------------------------------------
         // T2. A PULL IS IMMEDIATE ON BOTH MODELS. The falling edge is a transistor
         //     discharging the net, so it is fast; the asymmetry between the two edges
         //     is the physical fact this model carries.
         // ----------------------------------------------------------------
         drv = 2'b00; repeat (8) step;
         ck("T2 both buses idle high", ideal_line & slow_line, 1);
         drv = 2'b01; step;
         $display("T2  pulling low is immediate on both models -- a transistor is fast");
         ck("T2 ideal went low", ideal_line, 0);
         ck("T2 slow went low too", slow_line, 0);
         ck("T2 and neither is 'rising'", ideal_rising | slow_rising, 0);

         // ----------------------------------------------------------------
         // T3. A RELEASE IS NOT. The ideal line is high on the next clock; the slow one
         //     takes RISE clocks, and reports `rising` while it is charging.
         // ----------------------------------------------------------------
         drv = 2'b00;
         for (n = 1; n <= RISE + 2; n = n + 1) begin
            step;
            ck_idx("T3 ideal is high immediately", n, ideal_line, 1);
            ck_idx("T3 slow is high only after RISE clocks", n, slow_line,
                   (n >= RISE) ? 1 : 0);
            ck_idx("T3 and reports 'rising' until then", n, slow_rising,
                   (n < RISE) ? 1 : 0);
         end
         $display("T3  releasing takes RISE clocks on the slow model, 0 on the ideal one");

         // ----------------------------------------------------------------
         // T4. A PULL DURING THE RISE ABORTS IT. This is the case a naive counter gets
         //     wrong: the line was charging, somebody pulled, and the charge is lost --
         //     so the next release starts the rise again from the beginning rather than
         //     continuing.
         // ----------------------------------------------------------------
         drv = 2'b01; repeat (3) step;              // settled low
         drv = 2'b00; step; step;                    // 2 of RISE=4 clocks of charging
         ck("T4 still charging, not yet high", slow_line, 0);
         ck("T4 and it says so",               slow_rising, 1);
         drv = 2'b01; step;                          // pulled again mid-rise
         ck("T4 the pull wins immediately",    slow_line, 0);
         ck("T4 and charging stopped",         slow_rising, 0);
         drv = 2'b00;
         for (n = 1; n <= RISE + 1; n = n + 1) begin
            step;
            ck_idx("T4 the rise restarts from zero", n, slow_line, (n >= RISE) ? 1 : 0);
         end
         $display("T4  a pull during the rise discards the charge -- it restarts, not resumes");

         // ----------------------------------------------------------------
         // T5. TWO HOLDERS, AND ONE LETTING GO CHANGES NOTHING. The rise begins when the
         //     LAST device releases, not the first.
         // ----------------------------------------------------------------
         drv = 2'b11; repeat (3) step;
         ck("T5 two holders", slow_holders, 2);
         drv = 2'b01; repeat (RISE + 2) step;
         ck("T5 one holder left, so still low", slow_line, 0);
         ck("T5 and not charging",              slow_rising, 0);
         drv = 2'b00;
         repeat (RISE) step;
         ck("T5 now it rises", slow_line, 1);
         $display("T5  the rise starts when the LAST device releases");

         // ----------------------------------------------------------------
         // T6. THE NAIVE SAMPLER IS RIGHT ON THE IDEAL BUS. "I released it, so it is
         //     high." Against Module 16's model that assumption never fails -- which is
         //     exactly why Modules 17 and 18 could pass 30/30 while containing it.
         // ----------------------------------------------------------------
         n_wrong_ideal_seen = 1'b0; n_wrong_slow_seen = 1'b0;
         drv = 2'b01; repeat (4) step;
         drv = 2'b00; repeat (2) step;
         $display("T6  on the IDEAL bus, 'released means high' is never contradicted");
         ck("T6 the ideal bus never contradicted the assumption", n_wrong_ideal_seen, 0);

         // ----------------------------------------------------------------
         // T7. AND WRONG ON THE SLOW ONE. Same stimulus, same sampler, different
         //     verdict. This test asserts that the assumption WAS contradicted, which is
         //     the chapter's claim stated as a check: if the naive sampler ever agrees
         //     with the slow bus, this chapter is wrong.
         // ----------------------------------------------------------------
         $display("T7  on the SLOW bus, the same assumption is contradicted");
         ck("T7 the slow bus DID contradict it", n_wrong_slow_seen, 1);
         ck("T7 so the two models disagree about the same stimulus",
            (n_wrong_ideal_seen === n_wrong_slow_seen) ? 1 : 0, 0);

         // ----------------------------------------------------------------
         // T8. AND THE DISAGREEMENT IS A FUNCTION OF RISE TIME, NOT OF LUCK. Swept over
         //     the delay after release: the naive sampler is correct exactly when it
         //     waits at least RISE clocks. That is the whole mismatch, as a number.
         // ----------------------------------------------------------------
         for (k = 0; k <= RISE + 1; k = k + 1) begin
            drv = 2'b01; repeat (4) step;      // settle low
            drv = 2'b00;                        // release
            for (n = 0; n < k; n = n + 1) step; // wait k clocks
            step;                               // then sample
            ck_idx("T8 the line reads high iff at least RISE clocks were waited", k,
                   slow_line, (k + 1 >= RISE) ? 1 : 0);
         end
         $display("T8  correctness is a function of how long you waited: %0d clocks", RISE);

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

   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_bus_rc.vhd — the same model in VHDL
   -- -----------------------------------------------------------------------------
   -- i2c_bus_rc.vhd
   -- A bus model with a RISE TIME, and the reason Chapter 19.7 exists.
   -- Behavioural twin of the SystemVerilog and Verilog models.
   --
   -- Module 16's `i2c_line_model` resolves a wired-AND bus IDEALLY: the instant every
   -- device releases, the line reads '1'. This model adds exactly one thing -- a
   -- released line takes RISE_CLKS clocks to read HIGH -- which is the DIGITAL
   -- CONSEQUENCE of the slow edge Chapter 19.3 costed. It is not an analogue model and
   -- has no ramp, threshold or curve.
   --
   -- THE ASYMMETRY IS THE POINT: a falling edge is a transistor discharging the net and
   -- is modelled as immediate; a rising edge is a resistor charging it and takes time.
   --
   -- WHAT IT STILL CANNOT SHOW: metastability, threshold variation between devices,
   -- temperature, or two devices seeing the crossing at different instants. Those stay
   -- outside simulation.
   --
   -- THIS IS A VERIFICATION COMPONENT, not synthesisable hardware: it models a shared
   -- conductor, which is not something a design contains.
   -- -----------------------------------------------------------------------------
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   entity i2c_bus_rc is
      generic (
         N_DEV     : integer := 2;
         -- Clocks a released line takes to be readable as HIGH. 0 reproduces Module
         -- 16's ideal model exactly, which is what makes the comparison in T1 possible.
         RISE_CLKS : integer := 0
      );
      port (
         clk       : in  std_logic;
         drive_low : in  std_logic_vector(N_DEV-1 downto 0);

         -- The resolved level every device reads back.
         line    : out std_logic;
         -- How many devices are currently holding the line down.
         holders : out unsigned(7 downto 0);
         -- '1' while the line has been released by everyone but has not yet risen --
         -- the state the ideal model cannot represent, exposed so a bench can assert
         -- that the interesting case actually occurred.
         rising  : out std_logic
      );
   end entity i2c_bus_rc;

   architecture model of i2c_bus_rc is

      function popcount (v : std_logic_vector) return unsigned is
         variable n : unsigned(7 downto 0) := (others => '0');
      begin
         for i in v'range loop
            if v(i) = '1' then n := n + 1; end if;
         end loop;
         return n;
      end function;

      function any_set (v : std_logic_vector) return boolean is
      begin
         for i in v'range loop
            if v(i) = '1' then return true; end if;
         end loop;
         return false;
      end function;

      -- INITIALISED TO THE SETTLED VALUE, so an untouched bus reads HIGH from time
      -- zero. A real idle bus IS high. Starting at 0 models a board that powers up with
      -- its bus low, and mutation F09 is exactly that -- it survived every test until
      -- T0 was added to check the state before the first clock edge.
      signal rise_cnt : integer range 0 to 65535 := RISE_CLKS;
      signal any_low  : boolean;

   begin

      any_low <= any_set(drive_low);
      holders <= popcount(drive_low);

      -- The charge counter. Held at 0 while anybody pulls, so the falling edge is
      -- immediate; counts up once everybody has let go.
      process (clk)
      begin
         if rising_edge(clk) then
            if any_low then
               rise_cnt <= 0;
            elsif rise_cnt < RISE_CLKS then
               rise_cnt <= rise_cnt + 1;
            end if;
         end if;
      end process;

      rising <= '1' when (not any_low) and rise_cnt <  RISE_CLKS else '0';
      line   <= '1' when (not any_low) and rise_cnt >= RISE_CLKS else '0';

   end architecture model;
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_bus_rc_tb.vhd — the same tests in VHDL
   -- -----------------------------------------------------------------------------
   -- i2c_bus_rc_tb.vhd
   -- Independent oracle for i2c_bus_rc, and a demonstration of the mismatch.
   -- Behavioural twin of the SystemVerilog and Verilog benches.
   --
   -- THE BENCH HAS TWO JOBS, and the second is the chapter's argument.
   --   1. verify the model: at RISE_CLKS = 0 it is indistinguishable from Module 16's
   --      ideal wired-AND, and at RISE_CLKS = N a released line reads LOW for exactly
   --      N clocks. T0 to T5.
   --   2. demonstrate that a design assuming "released means HIGH" passes against the
   --      ideal model and FAILS against this one. T6 and T7 run the same naive sampler
   --      on both buses and require the verdicts to DIFFER.
   --
   -- T7 asserts a FAILURE, which is unusual enough to state plainly: if the naive
   -- sampler ever passes against the RC model, this chapter's claim is wrong.
   -- -----------------------------------------------------------------------------
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   entity i2c_bus_rc_tb is
   end entity i2c_bus_rc_tb;

   architecture sim of i2c_bus_rc_tb is

      constant N    : integer := 2;
      constant RISE : integer := 4;   -- a released line takes 4 clocks to read HIGH

      signal clk : std_logic := '0';
      signal drv : std_logic_vector(N-1 downto 0) := (others => '0');

      signal ideal_line, slow_line, ideal_rising, slow_rising : std_logic;
      signal ideal_holders, slow_holders : unsigned(7 downto 0);

      signal dev0_released : std_logic := '0';
      signal wrong_ideal_seen, wrong_slow_seen : std_logic := '0';
      -- Clear REQUEST, not a second driver on the flags. `std_logic` is a resolved
      -- type, so two processes driving one signal produce 'X' with no error and no
      -- warning -- which is how the first version of this bench reported T7 failing
      -- against a correct model: the stimulus process drove the flags to '0' while the
      -- observer drove one to '1', the resolution function returned 'X', and b2i('X')
      -- is 0. One driver per signal; the clear arrives as a request.
      signal clr_flags : boolean := false;

      signal halt : boolean := false;

   begin

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

      u_ideal : entity work.i2c_bus_rc
         generic map (N_DEV => N, RISE_CLKS => 0)
         port map (clk => clk, drive_low => drv, line => ideal_line,
                   holders => ideal_holders, rising => ideal_rising);

      u_slow : entity work.i2c_bus_rc
         generic map (N_DEV => N, RISE_CLKS => RISE)
         port map (clk => clk, drive_low => drv, line => slow_line,
                   holders => slow_holders, rising => slow_rising);

      -- The naive sampler, watching BOTH buses: "I released the line, so it is high."
      -- This is the assumption under test, written as an observer rather than a module
      -- because that is how it appears in real code -- not as a design decision, as an
      -- obvious step.
      naive : process (clk, clr_flags)
      begin
         if clr_flags then
            wrong_ideal_seen <= '0';
            wrong_slow_seen  <= '0';
         elsif rising_edge(clk) then
            if drv = "00" then
               dev0_released <= '1';
               if ideal_line = '0' then wrong_ideal_seen <= '1'; end if;
               if slow_line  = '0' then wrong_slow_seen  <= '1'; end if;
            else
               dev0_released <= '0';
            end if;
         end if;
      end process;

      stim : process
         variable err : integer := 0;

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

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

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

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

      begin
         report "=== i2c_bus_rc: a bus with a rise time, and the mismatch it exposes ==="
                severity note;

         -- T0. An idle bus is HIGH from the start, on both models. Checked BEFORE any
         --     clock edge, because that is the only window in which a wrong initial
         --     value is visible: the counter reaches its terminal value within RISE
         --     clocks, so by the time any other test looks, a model that started low has
         --     already caught up. Mutation F09 survived every other test for that reason.
         wait for 1 ns;
         report "T0  an untouched bus reads HIGH before the first clock, on both models"
                severity note;
         ck("T0 nobody is pulling",              to_integer(slow_holders), 0);
         ck("T0 the ideal bus is already high",  b2i(ideal_line), 1);
         ck("T0 and so is the slow bus",         b2i(slow_line),  1);
         ck("T0 the slow bus is not 'charging'", b2i(slow_rising), 0);
         step;
         ck("T0 still high after one clock",     b2i(slow_line),  1);

         -- T1. At RISE_CLKS = 0 this is Module 16's model. The whole comparison rests on
         --     the slow model being the ideal model plus one generic.
         drv <= "00"; for k in 1 to 6 loop step; end loop;
         for k in 0 to 3 loop
            drv <= std_logic_vector(to_unsigned(k, N));
            step;
            if k = 0 then ck_idx("T1 ideal: line is high iff nobody pulls", k, b2i(ideal_line), 1);
            else          ck_idx("T1 ideal: line is high iff nobody pulls", k, b2i(ideal_line), 0); end if;
            ck_idx("T1 ideal: holder count", k, to_integer(ideal_holders),
                   (k mod 2) + ((k / 2) mod 2));
            ck_idx("T1 ideal: never in the rising state", k, b2i(ideal_rising), 0);
         end loop;
         report "T1  at RISE_CLKS=0 the model is the ideal wired-AND of Module 16"
                severity note;

         -- T2. A pull is immediate on both models: a transistor discharging the net is
         --     fast, and the asymmetry between the two edges is the physical fact here.
         drv <= "00"; for k in 1 to 8 loop step; end loop;
         ck("T2 both buses idle high", b2i(ideal_line) * b2i(slow_line), 1);
         drv <= "01"; step;
         report "T2  pulling low is immediate on both models -- a transistor is fast"
                severity note;
         ck("T2 ideal went low",        b2i(ideal_line), 0);
         ck("T2 slow went low too",     b2i(slow_line), 0);
         ck("T2 and neither is rising", b2i(ideal_rising) + b2i(slow_rising), 0);

         -- T3. A release is not. The ideal line is high on the next clock; the slow one
         --     takes RISE clocks and reports `rising` while charging.
         drv <= "00";
         for n in 1 to RISE + 2 loop
            step;
            ck_idx("T3 ideal is high immediately", n, b2i(ideal_line), 1);
            if n >= RISE then ck_idx("T3 slow is high only after RISE clocks", n, b2i(slow_line), 1);
            else              ck_idx("T3 slow is high only after RISE clocks", n, b2i(slow_line), 0); end if;
            if n < RISE then ck_idx("T3 and reports rising until then", n, b2i(slow_rising), 1);
            else             ck_idx("T3 and reports rising until then", n, b2i(slow_rising), 0); end if;
         end loop;
         report "T3  releasing takes RISE clocks on the slow model, 0 on the ideal one"
                severity note;

         -- T4. A pull during the rise aborts it. The charge is lost, so the next release
         --     starts the rise again from the beginning rather than continuing.
         drv <= "01"; for k in 1 to 3 loop step; end loop;
         drv <= "00"; step; step;
         ck("T4 still charging, not yet high", b2i(slow_line), 0);
         ck("T4 and it says so",               b2i(slow_rising), 1);
         drv <= "01"; step;
         ck("T4 the pull wins immediately", b2i(slow_line), 0);
         ck("T4 and charging stopped",      b2i(slow_rising), 0);
         drv <= "00";
         for n in 1 to RISE + 1 loop
            step;
            if n >= RISE then ck_idx("T4 the rise restarts from zero", n, b2i(slow_line), 1);
            else              ck_idx("T4 the rise restarts from zero", n, b2i(slow_line), 0); end if;
         end loop;
         report "T4  a pull during the rise discards the charge -- it restarts, not resumes"
                severity note;

         -- T5. Two holders, and one letting go changes nothing: the rise begins when the
         --     LAST device releases.
         drv <= "11"; for k in 1 to 3 loop step; end loop;
         ck("T5 two holders", to_integer(slow_holders), 2);
         drv <= "01"; for k in 1 to RISE + 2 loop step; end loop;
         ck("T5 one holder left, so still low", b2i(slow_line), 0);
         ck("T5 and not charging",              b2i(slow_rising), 0);
         drv <= "00";
         for k in 1 to RISE loop step; end loop;
         ck("T5 now it rises", b2i(slow_line), 1);
         report "T5  the rise starts when the LAST device releases" severity note;

         -- T6. The naive sampler is right on the IDEAL bus. Against Module 16's model the
         --     assumption never fails, which is why Modules 17 and 18 could pass 30/30
         --     while containing it.
         clr_flags <= true;  wait for 1 ns;
         clr_flags <= false; wait for 1 ns;
         drv <= "01"; for k in 1 to 4 loop step; end loop;
         drv <= "00"; step; step;
         report "T6  on the IDEAL bus, 'released means high' is never contradicted"
                severity note;
         ck("T6 the ideal bus never contradicted the assumption", b2i(wrong_ideal_seen), 0);

         -- T7. And wrong on the slow one. Same stimulus, same sampler, different verdict.
         report "T7  on the SLOW bus, the same assumption is contradicted" severity note;
         ck("T7 the slow bus DID contradict it", b2i(wrong_slow_seen), 1);
         if wrong_ideal_seen = wrong_slow_seen then
            ck("T7 so the two models disagree about the same stimulus", 1, 0);
         end if;

         -- T8. And the disagreement is a function of rise time, not of luck. Swept over
         --     the delay after release: the naive sampler is correct exactly when it
         --     waits at least RISE clocks. That is the whole mismatch, as a number.
         for k in 0 to RISE + 1 loop
            drv <= "01"; for j in 1 to 4 loop step; end loop;
            drv <= "00";
            for j in 1 to k loop step; end loop;
            step;
            if k + 1 >= RISE then
               ck_idx("T8 the line reads high iff at least RISE clocks were waited", k,
                      b2i(slow_line), 1);
            else
               ck_idx("T8 the line reads high iff at least RISE clocks were waited", k,
                      b2i(slow_line), 0);
            end if;
         end loop;
         report "T8  correctness is a function of how long you waited" severity note;

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

   end architecture sim;

And then the interesting part

With that model in hand, the obvious experiment: take Module 18's integrated target and its published bench, unchanged, and swap only the bus model.

RISE_CLKSfailing checks, published benchafter lengthening the bench's stallwhat still fails
000—
111stretch_waits: 18, expected 1
211stretch_waits
321stretch_waits
421stretch_waits
621stretch_waits
832stretch_waits; "the bus is free"

I expected the target to break. It does not.

What actually failed, and why it is not the design

Two checks failed, both inside T6, the clock-stretching test. Neither is a defect in the target.

the slave is stretching — the bench's stall expired. The bench asserts stall_load for one clock, loading a 250-clock countdown, and expects the target to stretch when the acknowledge slot closes. On a slower bus everything takes longer, so by the time the slot closes the countdown has run out and the target correctly does not stretch.

That diagnosis was tested, not assumed: lengthening the bench's STALL_N from 250 to 4000 makes the check pass at every rise time from 1 to 6. The stimulus was arriving too early relative to a slower bus.

the master had to wait: got 18, expected 1 — an absolute count. stretch_waits is a bench counter, incremented inside the bench's own master model whenever it blocks waiting for SCL to rise. On a slow bus it blocks on every slow rise, so 18 instead of 1.

This one cannot be "fixed", because the expectation itself is the problem: how many times did my master model block is not a protocol fact. It is a property of the bench's master and the bus it was written against.

At RISE_CLKS = 8 a third bench assumption breaks — T7 checks sda for the bus being free immediately after releasing it, which on a slow enough bus has not happened yet. Same class, same lesson.

4. What the Mutations Found

Sixteen mutations across three languages on i2c_bus_rc, all killed. One is worth reporting because it exposed a missing test.

#mutationverdict
F01rise time ignored — reverts to the ideal modelKILLED (12)
F02rise is off by one (too fast)KILLED (3)
F03a pull does not discard the charge (it resumes)KILLED (16)
F04the falling edge is slowed too — asymmetry lostKILLED (3)
F05rising tied offKILLED (4)
F06counter saturates one shortKILLED (13)
F07holders ignores a deviceKILLED (3)
F08any_low sees only device 0KILLED (1)
F09idle bus starts LOWsurvived, then killed (3)

F09 changes the charge counter's initial value so the bus starts LOW and rises over RISE clocks from time zero. It survived every test, because every test began with enough settling for the model to catch up before anything was checked. The repair is T0: check the bus level before the first clock edge, which is the only window where a wrong initial value is visible.

That is the same shape as 19.1's deleted monitor and 19.6's two dead checks: a property that is true for the wrong reason in every window you happened to look at.

5. A Structured Way to Debug the Gap

When a design passes simulation and fails on hardware, the useful first question is not "where is the bug" but "which model was wrong":

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1. Is it in the RTL at all?
   → Class 8. Pin constraints, I/O standard, pull-ups, ground, enable polarity.
     Check these FIRST: they are common, cheap to check, and not design bugs.

2. Did the tool build what I wrote?
   → Class 3. Synthesis and implementation reports. Internal tri-state, inference.

3. Did I tell the tool the right things?
   → Classes 4 and 5. Constraints and the CDC report. Read them; do not infer
     them from a timing summary that says PASS.

4. Was my model of the environment right?
   → Classes 1, 2 and 7. The bus is slower than the model, the other device is
     less polite than the model, the clock is not the one the parameter assumed.

5. Only now: is the protocol logic wrong?
   → And if Modules 17 and 18's verification is in place, it probably is not.

The order matters because it is roughly the order of decreasing likelihood and increasing cost, and because step 5 is where people start.

6. Focused Verification Insight

An environment model is a verification artifact and deserves the same rigour as a DUT. i2c_bus_rc has its own bench, its own mutation set and its own discovered bug. A bus model nobody verified is an unexamined assumption sitting underneath every result in the project.

Module 20's environment should be parameterisable in the environment's own non-idealities, not just in protocol stimulus. A rise time, a stretch-happy target, a controller that clocks slowly — each is a knob, and Section 3 shows what turning one finds: the bench's assumptions.

Coverage worth having here is a cross: each protocol scenario against each environment configuration. Section 3's table is that cross with one axis being RISE_CLKS, and the interesting cells are the ones where the verdict changes.

7. Misconceptions

8. Debugging

Simulation passes, the board reads 0xFF from every register

Pitfall — the mismatch is not in the RTL, and four candidates fit the symptom
Buggy Code
// A verified I2C target on its first board. Every RTL test passes, in all three
// languages. On hardware, the controller's every read returns 0xFF and every write
// is NACKed.
//
// 0xFF is the signature worth recognising: it is what a master reads when SDA is
// never pulled low by anybody. Each of the nine bits, including the acknowledge,
// was released -- so the master saw the pull-up, eight times, then a NACK.
//
// That single observation eliminates most of the design. If the target had matched
// the address and then mishandled the data, some bit somewhere would be low. All
// ones means NOTHING EVER PULLED THE LINE DOWN, which is a statement about the
// output path, not about the protocol engine.
//
// Four candidates fit, and they are not distinguishable from the symptom alone:
//
//   (a) the output enable is inverted, so the pad drives when it should release
//       and releases when it should drive  -- Chapter 19.1's DebugLab
//   (b) the pin constraint names the wrong package pin, so the target is driving
//       a pin that is not connected to the bus
//   (c) the I/O standard or bank voltage is wrong, so the pad cannot pull the
//       line below the master's threshold
//   (d) the target is never selected, because SCL is not arriving at all -- so it
//       never gets as far as an acknowledge
//
// Note (a) would usually give the OPPOSITE symptom -- a permanently LOW bus -- so
// it is the least likely of the four here, and that asymmetry is itself a clue.
Symptom

Every read returns 0xFF. Every write is NACKed. Reset and retry change nothing; the behaviour is completely deterministic, which already argues against anything timing- or metastability-related.

An external analyser shows well-formed I2C: START, address, nine clock pulses, and the ninth bit HIGH -- a clean NACK -- then data bytes also all ones. The MASTER is behaving correctly and the bus is electrically healthy: the lines idle high and go low cleanly when the master drives them, so the pull-ups are fitted and sized.

That last observation is the important one. It eliminates candidate (c) for the lines the MASTER drives, and it eliminates "no pull-up" entirely, because a bus with no pull-up would not idle high.

An ILA on the target tells the rest:

- scl_pin and sda_pin, synchronised: both toggling, matching the analyser - the framing block: START detected, correctly - the address comparator: MATCHED, and selected asserted - sda_drive_low: ASSERTED during the acknowledge slot

So the target decided to acknowledge and asserted its drive intent. The wire shows no acknowledge. The logic and the pin disagree, and everything upstream of the pin is correct.

Root Cause

The disagreement between sda_drive_low (asserted) and the wire (released) places the fault strictly between the RTL output and the package pin -- which is the territory of Chapters 19.1 and 19.2, and is outside everything Modules 17 and 18 simulate.

On this board it was candidate (b): the SDA pin constraint named a pin one position away from the one routed to the bus. The target was driving an unconnected pin perfectly correctly. SCL was constrained correctly, which is why the target saw the clock, framed the transfer and matched the address -- and is also why the failure looked like a protocol problem rather than a wiring one.

The general method is the part worth keeping, and it is Section 5's order:

1. the symptom 0xFF says "nothing pulled the line down" -- an output-path claim 2. the analyser says the bus is electrically healthy and the master is correct 3. the ILA says the target's INTENT was right 4. therefore the fault is between intent and pin, and the RTL is not involved

Three observations, each eliminating a layer, before touching the design. The alternative -- starting from the protocol engine because that is where the interesting logic is -- searches the one region the evidence had already cleared.

Distinguishing the four candidates takes one measurement each: probe the pin the constraint names and confirm it toggles (b); check the bank voltage against the bus voltage (c); confirm SCL arrives at the target (d); and compare drive intent with the pin, which is what caught it here (a and b together).

9. Reason It Through

10. Questions

11. What This Chapter Settled

Eight classes of mismatch, each with why simulation passes, what hardware does instead, and what evidence exposes it. The common factor is that every one is a mismatch about the environment, not the protocol — which is what is left once the protocol has been verified properly.

One class was moved into simulation. i2c_bus_rc models a rise time's digital consequence, is verified in three languages, and survived sixteen mutations after T0 was added to catch an initial value that was true for the wrong reason everywhere else.

And the experiment did not produce the result it was set up to produce. Module 18's target passes every data and protocol check on a bus with a six-clock rise time; the failures are the bench's own timing assumptions, and the one that cannot be repaired is an expectation about how often the bench's master blocked. Changing an environment model falsifies the bench first. That is worth more than the confirmation I was looking for.

What none of this has been is a procedure. The catalogue tells you what can go wrong and Section 5 orders the questions, but nothing so far says what to do on the morning the board arrives, in what order, with what instrument. Chapter 19.8 is that morning.

Continue learning

Related tutorials