Skip to content
VLSI Mentor

I²C · Module 4

The Data-Valid Rule — SDA Stable While SCL Is High

One sentence governs every bit on an I²C bus, and it is derived rather than decreed: the receiver needs a settled value at the instant it looks. What falls out is that an SDA edge while SCL is HIGH cannot be data — which is why the bus reserves it for framing.

Chapter 4.1 built a clock with legal phases, which answers when the receiver looks. It deliberately left the other half open: what SDA must be doing around that instant.

The answer is a single sentence, and it is the most quoted rule in I²C. It is also the one most often learned as a slogan rather than derived — which matters, because the derivation is what makes the next consequence obvious instead of arbitrary. This chapter derives it, builds a checker that detects violations of it in three languages, and then shows what the rule makes possible.

1. What the Receiver Actually Needs

Return to the receiver from Chapter 4.1. It has to decide what bit is on SDA, and SCL tells it when. Put those together and ask what has to be true.

The receiver observes SDA during the HIGH phase of SCL. That is the interval SCL exists to delimit. For the observation to produce a defined answer, SDA must not be moving during it — because a line in transit is between levels, and an input asked to decide about a voltage between levels is not guaranteed to produce a stable answer, nor the same answer as another input on the same conductor.

So the requirement writes itself:

During ordinary data transfer, SDA must remain stable while SCL is HIGH.

That is the data-valid rule. Notice it was not decreed — it is the minimum condition under which a receiver can do its job at all, and any two-wire bus with this structure would need something equivalent.

The corollary is the useful half: if SDA must be stable while SCL is HIGH, then the transmitter's opportunity to change SDA is while SCL is LOW. The LOW phase is when the bus prepares; the HIGH phase is when it is read. That is the rhythm of every bit.

Two legal ordinary bits — SDA changes only while SCL is low

10 cycles
Ten intervals showing SCL and SDA for two ordinary data bits. SCL alternates between low and high phases. SDA changes only during the low phases and is held constant throughout each high phase, so the receiver observes a settled value each time.stable — bit is readstable — bit is readstable — bit is readstable — bit is readSDA prepared while SCL is lowSDA prepared while SCL islowreceiver observes a settled 1receiver observes a settled1next bit: a settled 0next bit: a settled 0sclsda0111100000t0t1t2t3t4t5t6t7t8t9
Figure 1 \u2014 a legal ordinary bit. SDA is prepared while SCL is low and then held unchanged across the whole high phase, so the receiver observes a settled value. The value shown is a 1 followed by a 0; what matters is not the values but that each change happens in a low phase and nothing moves during a high phase.

Two things this figure is doing that are worth naming, because they are what a reader should take from it rather than "SCL goes up and down".

The change is placed early in the LOW phase, not late. That is the §1 caution made visual: the transmitter gives the change time to settle before the observation window opens. A design that moved that edge rightwards, closer to SCL's rise, would look almost identical on this figure and be progressively less safe.

The HIGH phase contains no SDA activity at all. Not "little" — none. The rule is not about minimising change, it is about there being none.

3. Breaking the Rule

An illegal ordinary bit — SDA changes while SCL is high

10 cycles
Ten intervals showing SCL and SDA. During the second high phase of SCL, SDA changes from one to zero part way through, so the receiver's observation window contains a transition. The affected interval is marked as a violation.observation corruptedobservationcorruptedthis bit is finethis bit is fineSDA moved during SCL HIGHSDA moved during SCL HIGHsclsda0111111000t0t1t2t3t4t5t6t7t8t9
Figure 2 \u2014 the same bit, with SDA changing in the middle of the high phase. The receiver's observation window now contains a transition, so what it reads is not determined by what the transmitter intended. This is a timing-rule violation even though both the old and new values are perfectly ordinary bits.

What makes this instructive is how ordinary the two values are. Nothing here is a strange voltage or a malformed level — SDA went from a clean 1 to a clean 0, both entirely legal bit values. The fault is when, not what.

That is the point of separating the three legality questions in Chapter 4.1. A capture containing this bit may well decode into a plausible byte, because a receiver will read something. The byte may even be correct on one board and wrong on another, depending on exactly where each device's observation lands relative to the transition. Intermittent, board-dependent, and perfectly plausible when decoded — which is the signature of a timing violation rather than a logic bug.

4. The Consequence Nobody Expects

Now the part that turns a constraint into an opportunity, and it is the reason this chapter is marked Critical.

If SDA is never allowed to change while SCL is HIGH during ordinary data, then an SDA edge during SCL HIGH is not ambiguous data — it is something that cannot be data at all. The rule has created a signal pattern that is guaranteed to be unused by the data path.

A protocol designer looking at that has been handed a free signalling channel. There is a pattern every participant can recognise, that no legal data transfer can accidentally produce, and that requires no extra wire. So I²C uses it deliberately: transitions of SDA while SCL is HIGH carry framing meaning — the events that delimit where a transfer begins and ends.

An SDA edge during SCL HIGH — reserved for framing, not data

10 cycles
Ten intervals. An ordinary data bit is shown first with SDA stable through the high phase. Then SDA transitions while SCL is high, marked as a reserved framing pattern rather than data. An ordinary bit follows. The figure shows only that the pattern is distinguishable, not what the framing means.framing, not dataframing, not dataordinary data bitordinary data bitedge during HIGH — reservededge during HIGH — reservedordinary data resumesordinary data resumessclsda1111100000t0t1t2t3t4t5t6t7t8t9
Figure 3 \u2014 why the reserved pattern exists. The middle interval shows an SDA edge while SCL is high. Because the data-valid rule forbids that during ordinary transfer, the pattern is unambiguous when it appears, which is what lets the bus use it to delimit transfers without an extra conductor.

This chapter deliberately stops at "reserved". Which direction of edge means what, the timing margins those events require, how a repeated START differs from a first one, and what a receiver does in response are all specified precisely — and they belong to Module 5. Naming them here would be teaching the framing lesson badly in the wrong chapter.

What Module 4 owes you is the mechanism: the framing events are recognisable because the data-valid rule exists. The rule is not merely a constraint that framing has to work around; it is the thing that makes framing possible. Take the rule away and there is no pattern left that data could not produce.

5. Detecting a Violation in Hardware

The rule is checkable, and building the checker teaches something the waveforms cannot: what an observer can and cannot know.

The design below watches already-sampled scl_in and sda_in on a faster internal clock and raises a sticky error if SDA changes while SCL is HIGH. It takes a check_enable input which is deasserted during framing — because §4 just established that an SDA edge during SCL HIGH is legal when it is a framing event, and a checker that flagged every one of those would be useless.

Two honest limitations, stated up front because they are the interesting part:

It samples, so it can miss. A change narrower than the internal sample interval can slip between two samples. A faster observation clock narrows the window and never closes it. Real timing measurement uses edge timestamps rather than periodic sampling, which §8 develops.

It needs context it cannot derive. check_enable has to come from something that already knows whether the bus is in an ordinary data window or a framing event — which means a decoder. A checker cannot bootstrap its own context, and pretending otherwise is how you get an assertion that fires on every legal START.

5a. SystemVerilog

Azvya Education Pvt. Ltd.VLSI Mentor
sda_stability_checker.sv — SYNTHESIZABLE RTL. Sticky violation flag; framing excluded by check_enable.
   module sda_stability_checker (
       input  logic clk,
       input  logic rst_n,
       input  logic scl_in,                 // ALREADY-SAMPLED bus levels, not raw pins
       input  logic sda_in,
       input  logic check_enable,           // 1 = ordinary data window; 0 = framing
       output logic data_stability_error    // sticky until reset
   );
       logic scl_q, sda_q;

       always_ff @(posedge clk) begin
           if (!rst_n) begin
               scl_q <= 1'b1; sda_q <= 1'b1;        // idle bus is HIGH on both lines
               data_stability_error <= 1'b0;
           end else begin
               // A violation needs SCL HIGH across BOTH samples. Requiring the
               // previous sample too is what stops a change coincident with an SCL
               // edge being reported -- the change is then in the LOW phase or on
               // the boundary, neither of which this rule forbids.
               if (check_enable && scl_q && scl_in && (sda_in != sda_q))
                   data_stability_error <= 1'b1;     // sticky: a violation happened
               scl_q <= scl_in;
               sda_q <= sda_in;
           end
       end
   endmodule

One line in that module carries almost all of its subtlety: the condition requires SCL to be HIGH in both the previous and the current sample. Requiring only the current one would report a violation whenever SDA changed in the same interval that SCL rose — which is legal, because that change belongs to the low phase or the edge itself. The testbench has a dedicated case for it, and that case exists because a mutation survived without it.

Azvya Education Pvt. Ltd.VLSI Mentor
sda_stability_checker_tb.sv — SELF-CHECKING TESTBENCH, SIMULATION ONLY. Legal, both edge boundaries, illegal, sticky, framing-excluded.
   module sda_stability_checker_tb;
       logic clk = 1'b0, rst_n, scl_in, sda_in, check_enable;
       logic data_stability_error;
       int errors = 0;

       sda_stability_checker dut (.*);
       always #5 clk = ~clk;
       initial begin #20000; $display("FAIL: watchdog expired"); $finish; end

       // Drive one ordinary bit: prepare SDA during the LOW phase, hold it through HIGH.
       task automatic legal_bit(input logic v);
           scl_in = 1'b0;            repeat (2) @(negedge clk);
           sda_in = v;               repeat (2) @(negedge clk);   // change while LOW
           scl_in = 1'b1;            repeat (3) @(negedge clk);   // stable through HIGH
           scl_in = 1'b0;            repeat (1) @(negedge clk);
       endtask

       // Drive an ILLEGAL bit: change SDA in the middle of the HIGH phase.
       task automatic illegal_bit(input logic v);
           scl_in = 1'b0;            repeat (2) @(negedge clk);
           sda_in = v;               repeat (2) @(negedge clk);
           scl_in = 1'b1;            repeat (2) @(negedge clk);
           sda_in = ~v;              repeat (2) @(negedge clk);   // <-- violation
           scl_in = 1'b0;            repeat (1) @(negedge clk);
       endtask

       // BOUNDARY: SDA settles in the SAME sample interval that SCL goes HIGH.
       // This is legal -- the change belongs to the LOW phase / the edge itself, not
       // to the HIGH window -- and it is the case the two-sample guard exists for.
       task automatic edge_coincident_bit(input logic v);
           scl_in = 1'b0;                      repeat (2) @(negedge clk);
           sda_in = v;  scl_in = 1'b1;         repeat (3) @(negedge clk);  // same instant
           scl_in = 1'b0;                      repeat (1) @(negedge clk);
       endtask

       // BOUNDARY, MIRRORED: SDA moves in the SAME sample interval SCL goes LOW.
       // Also legal -- the HIGH window has ended -- and it is the case that catches a
       // checker which looked only at the PREVIOUS SCL sample.
       task automatic fall_coincident_bit(input logic v);
           scl_in = 1'b0;                      repeat (2) @(negedge clk);
           sda_in = v;                         repeat (2) @(negedge clk);
           scl_in = 1'b1;                      repeat (3) @(negedge clk);  // stable in HIGH
           sda_in = ~v; scl_in = 1'b0;         repeat (2) @(negedge clk);  // moves as SCL falls
       endtask

       initial begin
           rst_n = 0; scl_in = 1'b1; sda_in = 1'b1; check_enable = 1'b0;
           repeat (2) @(negedge clk);
           // 1 -- RESET clears the error state.
           if (data_stability_error !== 1'b0) begin $error("error set during reset"); errors++; end
           rst_n = 1; check_enable = 1'b1; @(negedge clk);

           // 2 -- LEGAL bits must never raise the error, repeatedly.
           legal_bit(1'b0); legal_bit(1'b1); legal_bit(1'b1); legal_bit(1'b0);
           if (data_stability_error !== 1'b0) begin
               $error("legal bits raised a stability error (false positive)"); errors++; end

           // 3 -- BOUNDARY: a change coincident with the SCL rising edge is legal.
           //      A checker that looked only at the CURRENT SCL sample would report a
           //      false violation here, because SCL is HIGH and SDA just moved.
           edge_coincident_bit(1'b1); edge_coincident_bit(1'b0);
           if (data_stability_error !== 1'b0) begin
               $error("a change coincident with the SCL rising edge was falsely reported"); errors++; end

           // 3b -- BOUNDARY, MIRRORED: a change coincident with the SCL FALLING edge is
           //      legal too. A checker that looked only at the PREVIOUS SCL sample would
           //      report a false violation here. Both guard terms are needed, not one.
           fall_coincident_bit(1'b1); fall_coincident_bit(1'b0);
           if (data_stability_error !== 1'b0) begin
               $error("a change coincident with the SCL falling edge was falsely reported"); errors++; end

           // 4 -- ILLEGAL bit must be detected.
           illegal_bit(1'b0);
           if (data_stability_error !== 1'b1) begin
               $error("SDA changed while SCL was HIGH and was NOT detected"); errors++; end

           // 5 -- STICKY: the flag survives subsequent legal traffic.
           legal_bit(1'b1);
           if (data_stability_error !== 1'b1) begin
               $error("error flag did not stay sticky"); errors++; end

           // 6 -- RESET clears it again.
           rst_n = 0; repeat (2) @(negedge clk); rst_n = 1; @(negedge clk);
           if (data_stability_error !== 1'b0) begin $error("reset did not clear the error"); errors++; end

           // 7 -- FRAMING EXCLUSION: the same illegal-looking edge with the checker
           //      disabled must NOT be reported. This is how a real checker avoids
           //      flagging every legal START and STOP.
           check_enable = 1'b0;
           illegal_bit(1'b1);
           if (data_stability_error !== 1'b0) begin
               $error("an SDA edge during SCL HIGH was reported while checking was disabled"); errors++; end

           // 8 -- re-enabling must resume detection.
           check_enable = 1'b1;
           illegal_bit(1'b0);
           if (data_stability_error !== 1'b1) begin
               $error("detection did not resume after re-enable"); errors++; end

           if (errors == 0) $display("PASS: legal bits clean, SCL-HIGH changes detected, framing window excluded");
           else             $display("FAIL: %0d error(s)", errors);
           $finish;
       end
   endmodule

5b. Verilog

Same logic, same eight checks, reg/wire typing and $display reporting.

Azvya Education Pvt. Ltd.VLSI Mentor
sda_stability_checker.v — SYNTHESIZABLE RTL. The same checker in Verilog.
   module sda_stability_checker (
       input  wire clk,
       input  wire rst_n,
       input  wire scl_in,                 // ALREADY-SAMPLED bus levels, not raw pins
       input  wire sda_in,
       input  wire check_enable,           // 1 = ordinary data window; 0 = framing
       output reg  data_stability_error    // sticky until reset
   );
       reg scl_q, sda_q;

       always @(posedge clk) begin
           if (!rst_n) begin
               scl_q <= 1'b1; sda_q <= 1'b1;        // idle bus is HIGH on both lines
               data_stability_error <= 1'b0;
           end else begin
               // SCL HIGH across BOTH samples, or a change coincident with the SCL
               // rising edge would be falsely reported.
               if (check_enable && scl_q && scl_in && (sda_in != sda_q))
                   data_stability_error <= 1'b1;     // sticky
               scl_q <= scl_in;
               sda_q <= sda_in;
           end
       end
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
sda_stability_checker_tb.v — SELF-CHECKING TESTBENCH, SIMULATION ONLY. The same cases in Verilog idiom.
   module sda_stability_checker_tb;
       reg  clk, rst_n, scl_in, sda_in, check_enable;
       wire data_stability_error;
       integer errors;

       sda_stability_checker dut (.clk(clk), .rst_n(rst_n), .scl_in(scl_in),
           .sda_in(sda_in), .check_enable(check_enable),
           .data_stability_error(data_stability_error));

       initial clk = 1'b0;
       always #5 clk = ~clk;
       initial begin #20000; $display("FAIL: watchdog expired"); $finish; end

       task legal_bit; input v; begin
           scl_in = 1'b0;            repeat (2) @(negedge clk);
           sda_in = v;               repeat (2) @(negedge clk);
           scl_in = 1'b1;            repeat (3) @(negedge clk);
           scl_in = 1'b0;            repeat (1) @(negedge clk);
       end endtask

       // SDA settles in the SAME sample interval SCL goes HIGH -- legal.
       task edge_coincident_bit; input v; begin
           scl_in = 1'b0;                  repeat (2) @(negedge clk);
           sda_in = v;  scl_in = 1'b1;     repeat (3) @(negedge clk);
           scl_in = 1'b0;                  repeat (1) @(negedge clk);
       end endtask

       // SDA moves in the SAME sample interval SCL goes LOW -- also legal.
       task fall_coincident_bit; input v; begin
           scl_in = 1'b0;                  repeat (2) @(negedge clk);
           sda_in = v;                     repeat (2) @(negedge clk);
           scl_in = 1'b1;                  repeat (3) @(negedge clk);
           sda_in = ~v; scl_in = 1'b0;     repeat (2) @(negedge clk);
       end endtask

       task illegal_bit; input v; begin
           scl_in = 1'b0;            repeat (2) @(negedge clk);
           sda_in = v;               repeat (2) @(negedge clk);
           scl_in = 1'b1;            repeat (2) @(negedge clk);
           sda_in = ~v;              repeat (2) @(negedge clk);   // violation
           scl_in = 1'b0;            repeat (1) @(negedge clk);
       end endtask

       initial begin
           errors = 0; rst_n = 0; scl_in = 1'b1; sda_in = 1'b1; check_enable = 1'b0;
           repeat (2) @(negedge clk);
           if (data_stability_error !== 1'b0) begin $display("FAIL: error set during reset"); errors=errors+1; end
           rst_n = 1; check_enable = 1'b1; @(negedge clk);

           legal_bit(1'b0); legal_bit(1'b1); legal_bit(1'b1); legal_bit(1'b0);
           if (data_stability_error !== 1'b0) begin $display("FAIL: false positive on legal bits"); errors=errors+1; end

           edge_coincident_bit(1'b1); edge_coincident_bit(1'b0);
           if (data_stability_error !== 1'b0) begin
               $display("FAIL: change coincident with SCL rise falsely reported"); errors=errors+1; end

           fall_coincident_bit(1'b1); fall_coincident_bit(1'b0);
           if (data_stability_error !== 1'b0) begin
               $display("FAIL: change coincident with SCL fall falsely reported"); errors=errors+1; end

           illegal_bit(1'b0);
           if (data_stability_error !== 1'b1) begin
               $display("FAIL: SCL-HIGH change not detected"); errors=errors+1; end

           legal_bit(1'b1);
           if (data_stability_error !== 1'b1) begin $display("FAIL: error not sticky"); errors=errors+1; end

           rst_n = 0; repeat (2) @(negedge clk); rst_n = 1; @(negedge clk);
           if (data_stability_error !== 1'b0) begin $display("FAIL: reset did not clear"); errors=errors+1; end

           check_enable = 1'b0;
           illegal_bit(1'b1);
           if (data_stability_error !== 1'b0) begin
               $display("FAIL: reported while checking disabled"); errors=errors+1; end

           check_enable = 1'b1;
           illegal_bit(1'b0);
           if (data_stability_error !== 1'b1) begin
               $display("FAIL: detection did not resume"); errors=errors+1; end

           if (errors == 0) $display("PASS: legal bits clean, SCL-HIGH changes detected, framing window excluded");
           else             $display("FAIL: %0d error(s)", errors);
           $finish;
       end
   endmodule

5c. VHDL

The VHDL is the most readable of the three here, because the condition is written as a plain conjunction with no bit-level shorthand. Note that std_logic comparison against '1' is explicit rather than relying on a truthiness convention, which is a small but real clarity advantage when a condition has four terms.

Azvya Education Pvt. Ltd.VLSI Mentor
sda_stability_checker.vhd — SYNTHESIZABLE RTL. The same checker in VHDL.
   library ieee;
   use ieee.std_logic_1164.all;

   entity sda_stability_checker is
       port (
           clk                  : in  std_logic;
           rst_n                : in  std_logic;
           scl_in               : in  std_logic;   -- ALREADY-SAMPLED bus levels
           sda_in               : in  std_logic;
           check_enable         : in  std_logic;   -- 1 = ordinary data window
           data_stability_error : out std_logic    -- sticky until reset
       );
   end entity;

   architecture rtl of sda_stability_checker is
       signal scl_q, sda_q : std_logic := '1';     -- idle bus is HIGH on both lines
       signal err          : std_logic := '0';
   begin
       data_stability_error <= err;

       process (clk)
       begin
           if rising_edge(clk) then
               if rst_n = '0' then
                   scl_q <= '1'; sda_q <= '1';
                   err   <= '0';
               else
                   -- SCL HIGH across BOTH samples, or a change coincident with the
                   -- SCL rising edge would be falsely reported.
                   if check_enable = '1' and scl_q = '1' and scl_in = '1'
                      and sda_in /= sda_q then
                       err <= '1';                 -- sticky
                   end if;
                   scl_q <= scl_in;
                   sda_q <= sda_in;
               end if;
           end if;
       end process;
   end architecture;
Azvya Education Pvt. Ltd.VLSI Mentor
sda_stability_checker_tb.vhd — SELF-CHECKING TESTBENCH, SIMULATION ONLY. The same cases with assert report severity.
   library ieee;
   use ieee.std_logic_1164.all;

   entity sda_stability_checker_tb is
   end entity;

   architecture sim of sda_stability_checker_tb is
       signal clk          : std_logic := '0';
       signal rst_n        : std_logic := '0';
       signal scl_in       : std_logic := '1';
       signal sda_in       : std_logic := '1';
       signal check_enable : std_logic := '0';
       signal data_stability_error : std_logic;
   begin
       dut : entity work.sda_stability_checker
           port map (clk => clk, rst_n => rst_n, scl_in => scl_in, sda_in => sda_in,
                     check_enable => check_enable,
                     data_stability_error => data_stability_error);

       clk <= not clk after 5 ns;

       stim : process
           variable errs : natural := 0;

           procedure waitn (n : in positive) is
           begin
               for i in 1 to n loop wait until falling_edge(clk); end loop;
           end procedure;

           procedure legal_bit (v : in std_logic) is
           begin
               scl_in <= '0'; waitn(2);
               sda_in <= v;   waitn(2);            -- change while LOW
               scl_in <= '1'; waitn(3);            -- stable through HIGH
               scl_in <= '0'; waitn(1);
           end procedure;

           -- SDA settles in the SAME sample interval SCL goes HIGH -- legal.
           procedure edge_coincident_bit (v : in std_logic) is
           begin
               scl_in <= '0';              waitn(2);
               sda_in <= v; scl_in <= '1'; waitn(3);
               scl_in <= '0';              waitn(1);
           end procedure;

           -- SDA moves in the SAME sample interval SCL goes LOW -- also legal.
           procedure fall_coincident_bit (v : in std_logic) is
           begin
               scl_in <= '0';                     waitn(2);
               sda_in <= v;                       waitn(2);
               scl_in <= '1';                     waitn(3);
               sda_in <= not v; scl_in <= '0';    waitn(2);
           end procedure;

           procedure illegal_bit (v : in std_logic) is
           begin
               scl_in <= '0';    waitn(2);
               sda_in <= v;      waitn(2);
               scl_in <= '1';    waitn(2);
               sda_in <= not v;  waitn(2);         -- violation
               scl_in <= '0';    waitn(1);
           end procedure;
       begin
           waitn(2);
           if data_stability_error /= '0' then
               report "error set during reset" severity error; errs := errs + 1; end if;
           rst_n <= '1'; check_enable <= '1'; waitn(1);

           legal_bit('0'); legal_bit('1'); legal_bit('1'); legal_bit('0');
           if data_stability_error /= '0' then
               report "legal bits raised a stability error" severity error; errs := errs + 1; end if;

           edge_coincident_bit('1'); edge_coincident_bit('0');
           if data_stability_error /= '0' then
               report "change coincident with the SCL rise was falsely reported" severity error;
               errs := errs + 1; end if;

           fall_coincident_bit('1'); fall_coincident_bit('0');
           if data_stability_error /= '0' then
               report "change coincident with the SCL fall was falsely reported" severity error;
               errs := errs + 1; end if;

           illegal_bit('0');
           if data_stability_error /= '1' then
               report "SDA changed while SCL HIGH and was NOT detected" severity error;
               errs := errs + 1; end if;

           legal_bit('1');
           if data_stability_error /= '1' then
               report "error flag did not stay sticky" severity error; errs := errs + 1; end if;

           rst_n <= '0'; waitn(2); rst_n <= '1'; waitn(1);
           if data_stability_error /= '0' then
               report "reset did not clear the error" severity error; errs := errs + 1; end if;

           check_enable <= '0';
           illegal_bit('1');
           if data_stability_error /= '0' then
               report "reported while checking was disabled" severity error; errs := errs + 1; end if;

           check_enable <= '1';
           illegal_bit('0');
           if data_stability_error /= '1' then
               report "detection did not resume after re-enable" severity error; errs := errs + 1; end if;

           if errs = 0 then
               report "sda_stability_checker self-check complete" severity note;
           else
               report "sda_stability_checker self-check FAILED" severity error;
           end if;
           wait;
       end process;
   end architecture;

5d. Parity and Mutation Testing

Ports, reset semantics, the four-term detection condition, the sticky behaviour and the nine test cases are identical across the three, and all three testbenches complete at the same simulated time.

Six deliberate bugs were injected and the testbench run against each:

MutationWhat breaksResult
Drop the previous-SCL termover-reports on the SCL rising edgeFAIL — 2 errors
Drop the current-SCL termover-reports on the SCL falling edgeFAIL — 1 error
Compare SDA against itselfthe condition can never fireFAIL — 3 errors
Make the error flag non-stickya violation is forgotten immediatelyFAIL — 3 errors
Ignore check_enableevery legal START and STOP is flaggedFAIL — 1 error
History registers never updatethe checker compares stale samples foreverFAIL — 3 errors

All six were caught — but the two SCL-term mutations are the ones worth reporting honestly, because each one survived in turn.

The guard scl_q && scl_in has two terms, and a mutation that removes either one produces a checker that is wrong only at one boundary. Dropping the previous-SCL term makes the mutant over-report when SDA settles in the same interval that SCL rises; dropping the current-SCL term makes it over-report when SDA moves in the same interval that SCL falls. The first version of this testbench exercised neither boundary — it only ever changed SDA in the middle of a phase — so both mutants passed it cleanly.

Killing the first one required adding edge_coincident_bit. That looked like the end of it, and it was not: re-running the suite afterwards showed the mirror mutant still surviving, because a rising-edge test says nothing about the falling edge. Only adding fall_coincident_bit as well closed the gap.

The lesson generalises past this checker. A guard with N terms needs N boundary tests, not one, because each term exists to reject a different case and a test that exercises none of them cannot tell a correct guard from a partial one. Neither survivor was a weak mutation; both were missing tests, and the second was missing precisely because fixing the first felt like finishing the job. A mutation that survives is usually telling you something true about your verification.

The correct RTL is restored; the mutations exist only as evidence that the checks bite.

6. Should This Be an Assertion Instead?

A SystemVerilog assertion is the obvious alternative, and comparing them teaches when each is right.

Azvya Education Pvt. Ltd.VLSI Mentor
the same rule as an assertion — SYSTEMVERILOG ASSERTION, VERIFICATION ONLY
   // Simplified educational property. Assumptions are stated below and they matter.
   property sda_stable_while_scl_high;
       @(posedge clk) disable iff (!rst_n)
       (check_enable && $past(scl_in) && scl_in) |-> $stable(sda_in);
   endproperty
   assert property (sda_stable_while_scl_high)
       else $error("SDA changed while SCL was HIGH during an ordinary data window");

Read the assumptions rather than the syntax, because they are the lesson:

  • It is clocked by an internal clock, so it inherits the sampling limitation from §5 — it is an assertion about samples, not about the bus.
  • It depends on check_enable, so it needs the same external context. An assertion written without that term would fire on every legal START and STOP, and an assertion that cries wolf on legal traffic gets disabled, which is worse than having none.
  • It says nothing about how long SDA was stable before or after the window. The setup and hold margins are separate requirements, and no amount of cycle-based stability checking expresses them.

So which should you use? The honest answer is that neither is a substitute for the other, and the choice is not a style preference:

Clocked assertionProcedural timing checker
Good atcontinuous checking, concise statement, no test has to remember to lookmeasuring real intervals, reporting how much a requirement was missed by
Blind toanything between samples; real-time durationswhatever it was not asked to measure
Natural unitsclock cyclestime

Bus timing requirements are stated in real time against asynchronous external edges with analog thresholds — three properties a cycle-based assertion does not naturally express. That is why serious I²C environments use assertions for the structural rules and a procedural, timestamp-based checker for the durations. §8 sketches the latter. Reaching for "SVA everywhere" on this bus produces assertions that are either trivially true or subtly wrong about time.

7. Verification Connection — Monitor and Checker Are Different Jobs

This chapter exposes a distinction that shapes every I²C verification environment, and it is easy to conflate because both components watch the same two wires.

A transaction monitor reconstructs meaning. It watches edges and produces "target 0x48 was addressed, direction read, these bytes moved, this was the outcome". Its output is a transaction object, and a scoreboard compares it against what was expected.

A timing checker reconstructs legality. It watches the same edges and produces "the HIGH phase was this long, SDA changed this far before the rising edge, this edge violated stability". Its output is measurements and violations.

The two can disagree, and that is the whole point:

A transaction can decode correctly while violating timing. Figure 2's bit may well reconstruct into the intended byte on the particular board the simulation models. The monitor reports success; the checker reports a violation; both are right. A scoreboard watching only the monitor would pass a design that produces illegal waveforms — and that design will fail on a different board.

A transaction can fail while timing is perfectly legal. A wrong byte with immaculate edges is a logic bug, and a timing checker has nothing to say about it.

So an environment needs both, reporting separately, and neither can be inferred from the other. Modules 20 to 22 build them properly.

8. What a Real Timing Checker Does Instead

Since §5 and §6 both hit the sampling limitation, it is worth showing the shape of the thing that does not have it — conceptually, because Module 21 owns the implementation.

A real checker does not sample periodically. It timestamps edges and does arithmetic on the timestamps:

Azvya Education Pvt. Ltd.VLSI Mentor
UVM CONCEPT — VERIFICATION ONLY. Edge timestamping, which is what measuring time actually requires.
   // Not sampled on an internal clock: these are real-time observations of the
   // bus, which is what lets the checker report a DURATION rather than a count.
   time scl_rise_time, scl_fall_time, sda_change_time;

   // On each edge, record when it happened and derive the interval that just ended.
   //   on SCL rising  -> the LOW phase just ended:  now - scl_fall_time
   //   on SCL falling -> the HIGH phase just ended: now - scl_rise_time
   //   on SDA change  -> classify it: which SCL phase are we in, and is the bus
   //                     currently in an ordinary data window or a framing event?
   //
   // The last line is the hard part, and it is why a checker needs CONTEXT from a
   // decoder. An SDA edge while SCL is HIGH is a violation during data and a
   // legal framing event otherwise -- the edge alone does not say which.

Two architectural points follow, and both are things a first attempt usually gets wrong.

Measurement wants real time, so the checker lives outside any clock domain. The moment a checker is clocked, its resolution is that clock and its verdicts are about samples. That is fine for structural rules and wrong for durations.

The limits belong in configuration, not in the checker. The bounds a checker compares against differ by speed mode, so they belong in a configuration object the environment sets up:

Azvya Education Pvt. Ltd.VLSI Mentor
UVM CONCEPT — VERIFICATION ONLY. Limits as configuration, because they vary by mode.
   class i2c_timing_cfg extends uvm_object;
       // Populated per speed mode from the specification. Deliberately left
       // unpopulated here: Module 11 establishes the actual values, and a
       // plausible-looking number invented in a tutorial is worse than a blank.
       time t_low_min, t_high_min;
       time t_su_dat_min, t_hd_dat_min;
       time tr_max, tf_max;
   endclass

The empty fields are the point. Hard-coding numbers here would make the snippet look more finished and teach something unverified.

9. Debugging — The Byte That Decoded Correctly

Clean simulation, plausible capture, and a board that fails intermittently

Pitfall — a transmitter that changes SDA on the SCL rising edge
Buggy Code
// A first bit engine, built on top of the SCL generator from Chapter 4.1. The
// author reasons: "a bit is presented once per clock period, so present it when
// the clock ticks" -- and hangs the data launch on the HIGH tick:
 always @(posedge clk)
     if (high_tick)                 // <-- the instant SCL is released
         sda_drive_low <= ~next_bit;
//
// It looks tidy and symmetrical: one event per bit, at the top of the bit.
// In RTL simulation it is flawless -- SDA changes at a timestamp, SCL changes at
// the same timestamp, and the simulator resolves both instantly and consistently.
// Every testbench that decodes bytes passes.
Symptom

Simulation clean, every byte correct. On hardware, mostly fine. Then occasional corrupted bytes -- more often on a board with more devices fitted, more often warm, and more often at the faster of the two supported speeds. Different targets disagree about the same transfer: one device acknowledges and another does not, on the same bus, at the same moment. A logic analyser capture decodes into a plausible byte, sometimes the right one. That is what makes it so expensive: the capture looks like evidence that the protocol is fine, so the investigation moves to the targets, to the pull-ups, to the firmware. The one thing the capture cannot show is the relationship the fault actually consists of.

Root Cause

SDA is being changed at the moment SCL rises, which puts the transition INSIDE the receiver's observation window instead of before it. The data-valid rule is violated, and the violation is invisible in RTL simulation for a specific reason: in simulation both signals change at the same timestamp and the receiver model samples a settled value, because simulation has no transition interval. On real hardware both edges take time, SCL's rise is slow because it is a pull-up charging capacitance (Chapter 2.4), and each receiving device crosses its own threshold at its own moment -- so different devices sample at different points relative to SDA's transition and legitimately disagree. This also explains every part of the symptom pattern. More devices means more capacitance means a slower SCL rise means a wider disagreement window. Warmer means shifted thresholds and delays. Faster means the same absolute uncertainty occupies more of the bit. And a byte still decodes because a receiver always reads SOMETHING -- it is simply not guaranteed to be what was sent.

Fix
// Launch data in the LOW phase, with margin before SCL is released -- never on
// the transition into HIGH:
 always @(posedge clk)
     if (low_tick)                  // early in the LOW phase
         sda_drive_low <= ~next_bit;
// The bit then has the whole remaining LOW phase to settle before the observation
// window opens, which is exactly the placement Figure 1 shows.
//
// The verification that catches it, and why the usual one does not:
//   - a byte-decoding testbench PASSES this bug, because decoding is not the
//     property being violated;
//   - the stability checker in section 5 catches it, because it asks a different
//     question -- did SDA move while SCL was HIGH -- and that question is the one
//     the design got wrong.
//
// On hardware: put a scope, not a logic analyser, on SDA and SCL together and look
// at where the SDA edge sits relative to SCL's rise. A logic analyser applies its
// own threshold and reports two clean digital values, which is precisely the
// information that hides this fault.

The engineering lesson: a correct byte is not evidence of legal timing, and RTL simulation is structurally incapable of finding this class of bug, because the abstraction that makes simulation fast — instantaneous transitions — is the abstraction the bug lives in. The transferable habits are two. Check the relationship, not the value: a checker that asks "did SDA move while SCL was HIGH" finds it, a testbench that asks "is the byte right" never will. And when a fault scales with device count, temperature and speed, suspect a timing relationship, because those three are exactly the things that widen a transition interval.

10. Common Misconceptions

11. Reason It Through

Work these through before reading the answers.

An SDA edge occurs while SCL is HIGH. Is that automatically a protocol error?

No — and the reason is the most important idea in the chapter. That pattern cannot be data, which is precisely why the bus reserves it for framing. Whether a given instance is a violation or a legal transfer boundary depends on context: what the bus is doing at that moment. An observer without that context cannot classify the edge, which is why the checker in §5 takes a check_enable input rather than deriving legality from the edge alone.

A transmitter releases SDA before SCL rises, but the bus reaches HIGH late because the capacitance is large. Which abstraction layers are involved?

All three from Chapter 4.1. The RTL did what it was told — the release happened in the right cycle. The bus timing is different from the RTL's model, because a released line rises at a rate set by the resistor and the capacitance. And the electrical layer decides when each receiver considers the line HIGH, which is its own threshold crossing. The design is correct in cycle terms and may still be illegal in time, and no single layer's view reveals that.

A design decodes every byte correctly in simulation and corrupts bytes intermittently on a loaded board. What timing assumption might simulation be hiding?

That transitions take no time. In simulation SDA and SCL can change at the same timestamp and every receiver model still samples a settled value, because there is no interval during which the line is between levels. On hardware there is, it widens with capacitance, and different receivers cross their thresholds at different moments — so they can legitimately disagree. This is §9's bug, and the tell is that the failure rate tracks loading, temperature and speed.

Would you verify the data-valid rule with an assertion or a procedural checker?

Both, for different parts of it. An assertion states the structural rule concisely and checks it continuously without a test having to remember to look — good for "SDA did not change during the HIGH phase, in samples". A procedural, timestamp-based checker measures durations and reports by how much a requirement was missed — which a cycle-based assertion cannot express, because bus requirements are in real time against asynchronous edges. Using only the assertion leaves the margins unchecked; using only the checker gives up continuous structural checking. The judgement, rather than a rule, is the deliverable.

12. Understanding Check

13. Summary

The data-valid rule is derived, not decreed: a receiver observes SDA during SCL's HIGH phase, and an observation taken while the line is in transit has no defined result. So SDA must remain stable while SCL is HIGH during ordinary data transfer, and the transmitter's opportunity to change it is the LOW phase — early in the LOW phase, because a change still needs time to settle before the window opens.

Breaking the rule is a timing violation, not a logic error. Both the old and new values can be perfectly ordinary bits; the fault is when. And a receiver will still read something, which is why such a capture decodes plausibly and fails intermittently and board-dependently.

The rule's most important consequence is what it enables. Because ordinary data can never produce an SDA edge during SCL HIGH, that pattern is unambiguous — so the bus reserves it for framing, gaining a signalling channel with no extra conductor. Module 5 develops what the framing events are.

A hardware checker can detect violations, and building one exposes two real limits: it samples, so it can miss narrow changes, and it needs context to distinguish a violation from legal framing. The same limits apply to a clocked assertion — which is why serious environments use assertions for structural rules and a timestamp-based procedural checker for durations, and why "SVA everywhere" is the wrong instinct on a bus whose requirements are in real time.

Finally, a transaction monitor and a timing checker are different jobs whose verdicts can disagree, and neither can be inferred from the other.

14. What Comes Next

Two chapters have now produced a legal clock and a legal data relationship. Both were built one relationship at a time, which is how a design is written and not how a specification is read: a real timing diagram shows every relationship at once, and it is genuinely intimidating the first time.

Chapter 4.3 closes the module with a method for reading one — in passes, deliberately, so that a dense figure becomes a sequence of answerable questions rather than a wall of symbols.

Browse the full path on the I²C tutorials index. For the clock this rule is measured against, see SCL Generation and the Bit Period; for the edge shapes that consume its margins, Real Bus Electrical Behavior.

Continue learning