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.
2. One Legal Bit
Two legal ordinary bits — SDA changes only while SCL is low
10 cyclesTwo 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 cyclesWhat 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 cyclesThis 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
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
endmoduleOne 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.
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
endmodule5b. Verilog
Same logic, same eight checks, reg/wire typing and $display reporting.
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 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
endmodule5c. 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.
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; 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:
| Mutation | What breaks | Result |
|---|---|---|
| Drop the previous-SCL term | over-reports on the SCL rising edge | FAIL — 2 errors |
| Drop the current-SCL term | over-reports on the SCL falling edge | FAIL — 1 error |
| Compare SDA against itself | the condition can never fire | FAIL — 3 errors |
| Make the error flag non-sticky | a violation is forgotten immediately | FAIL — 3 errors |
Ignore check_enable | every legal START and STOP is flagged | FAIL — 1 error |
| History registers never update | the checker compares stale samples forever | FAIL — 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.
// 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 assertion | Procedural timing checker | |
|---|---|---|
| Good at | continuous checking, concise statement, no test has to remember to look | measuring real intervals, reporting how much a requirement was missed by |
| Blind to | anything between samples; real-time durations | whatever it was not asked to measure |
| Natural units | clock cycles | time |
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:
// 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:
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;
endclassThe 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
// 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.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.
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.
// 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
Related tutorials
- Related topic
tSU;DAT and tHD;DAT — The I²C Data Window
Two parameters every bit of every byte must satisfy, measured against two different edges. One has a specified minimum of zero and is in practice among the tightest constraints on the bus — this chapter resolves that contradiction.
- Related topic
Why I²C Timing Parameters Exist
Before any parameter is named, separate the three independent questions a single bit has to answer — what it means, when it may change, and how long it must hold. Sixteen numbers in Table 10 are answers to those three questions.
- Related topic
Reading an I²C Timing Diagram
A specification timing diagram shows every relationship at once, which is why it looks impenetrable. Read it in passes instead: signals, edges, stable regions, framing, intervals, edge shapes, and finally who owes each requirement.
- Related topic
The START Condition
START is SDA falling while SCL is high, it is generated only by the controller, and it makes the bus busy. Derive what every device must do in response, then build a detector in three languages and find out why its two guard terms and its reset value are all load-bearing.
