Skip to content
VLSI Mentor

I²C · Module 5

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.

Chapter 5.1 counted the reserved space and found exactly two edges in it. This chapter takes the falling one.

It is the single most important event on the bus. Every transfer begins with it, every device on the segment has to react to it, and it is the only thing that moves the bus from free to busy. It is also the simplest piece of hardware in this module — which makes it the right place to establish the detection pattern that Chapter 5.3 and Chapter 5.4 both build on.

1. The Definition, and What It Does Not Say

UM10204 §3.1.4 is one sentence:

A HIGH to LOW transition on the SDA line while SCL is HIGH defines a START condition.

Read what that sentence leaves out, because the omissions are the interesting part.

It says nothing about who — that comes from the next sentence in the specification, which states that START and STOP conditions are always generated by the master. It says nothing about how long the condition lasts, or how long SCL must remain high around it; those are timing parameters and they are Chapter 5.5. It says nothing about what follows, which is the address byte and is Module 6. And it says nothing about how a device is supposed to notice, which is the part this chapter builds.

What it does say is that the condition is a transition, not a level. This is worth pausing on because it determines the shape of every detector in this module. A level can be tested with a comparison; a transition cannot be tested at all without knowing what came before. The definition therefore hands the implementer a requirement for memory before any of the rest of the design exists.

Idle, then START, then the first clock pulse

10 cycles
Ten intervals. Both lines begin high with the bus idle. SDA then falls while SCL is still high, which is the START condition and makes the bus busy. Afterwards SCL is pulled low to begin the first clock pulse, and clocking continues with SDA held low.bus freebus freebus busybus busyidle — both lines releasedidle — both lines releasedSTART — SDA falls, SCL highSTART — SDA falls, SCL highfirst SCL low phase beginsfirst SCL low phase beginssclsda1100000000t0t1t2t3t4t5t6t7t8t9
Figure 1 — the START condition, and the bus state change it causes. SDA falls while SCL is still high; that edge is the START. Only afterwards does the controller pull SCL low to begin the first clock pulse. The interval between the two edges is a specified minimum, not an implementation choice — Chapter 5.5 measures it. Conceptual figure: interval widths are chosen for legibility and carry no specification proportion.

2. The Same Edge, Twice, Meaning Different Things

The reason a START is detectable at all is that ordinary data is forbidden from producing the same waveform — and it is worth seeing the two side by side, because on SDA alone they are identical.

An ordinary data bit carrying a zero also involves SDA going from high to low. The falling edge is the same edge, with the same direction and the same shape. What differs is only what SCL was doing at the time.

A data zero and a START — the identical SDA edge

10 cycles
Ten intervals. SDA first falls while SCL is low, which is ordinary data preparation, and the resulting zero is sampled during the following high phase. SDA is later released high while SCL is low, a one is sampled, and then SDA falls again while SCL is high, which is a START condition.the zero is read herethe zero is readherea one is read herea one is read heredata: SDA falls, SCL lowdata: SDA falls, SCL lowSTART: SDA falls, SCL highSTART: SDA falls, SCL highsclsda1100001110t0t1t2t3t4t5t6t7t8t9
Figure 2 — one falling edge, two meanings. At the third interval SDA falls while SCL is low: that is ordinary preparation of a data bit, and the zero is read in the high phase that follows. At the last interval SDA falls while SCL is high: the same direction, the same shape, and now it is a START. The level of the other conductor is the entire difference, which is why no detector can work on SDA alone. Conceptual figure.

3. What Every Device Does When a START Arrives

This is the part that makes START the critical event rather than merely the first one. A START is a broadcast, and it is unconditional. Every device on the segment sees it — there is no addressing yet, because the address has not been transmitted — and every device must act on it.

Three things happen, and the third is the one that gets designed wrong.

The bus becomes busy. UM10204: the bus is considered busy after the START condition. Any other potential controller on the segment must now not attempt a transfer of its own. On a single-controller bus this is bookkeeping; on a multi-controller bus it is the first line of defence, and what happens when two controllers issue a START at nearly the same instant is Module 13.

Every target begins listening for its address. The byte after the START is an address, so every device has to start shifting in bits and comparing. Most of them will not match and will go back to ignoring the bus, and that is the expected outcome — the majority of devices on a healthy bus spend most of their time deciding a transfer is not for them. Chapter 6.5 builds the comparison.

Every target abandons whatever partial state it had. This is the one worth stating explicitly. A START is not just "a transfer begins" — it is also "any transfer that was in progress is over, however incomplete". A target that was half way through receiving a byte does not get to finish it. A target that was waiting for a second address byte does not get to wait any longer. The START resets the protocol state of every device unconditionally.

That last rule is what makes the bus recoverable. Because a START always means "start over", a controller that has lost track of what a target is doing has a way to resynchronise everyone: issue a START. Without the unconditional-reset rule, a confused bus would need an out-of-band mechanism to recover, and there is no out-of-band on two wires.

4. Why the START Comes Before the Clock

Look again at Figure 1 and note the order: SDA falls first, and only afterwards does SCL go low.

That ordering is not a stylistic choice, and it is not arbitrary. The START is defined as an SDA edge during SCL's high phase, so SCL has to still be high when SDA moves — which means the controller cannot begin its first clock pulse until the START edge has already happened. The clock waits for the framing, not the other way round.

The specification makes the dependency explicit in its timing table, where the parameter governing the interval between the START edge and the first clock pulse carries the condition note "After this period, the first clock pulse is generated." The interval has a specified minimum. Chapter 5.5 measures it and builds the sequencer that honours it; what matters here is the causal order, because a design that pulls SCL low too eagerly does not produce a slightly-marginal START — it produces no START at all, since SDA's edge then lands during a low phase and is ordinary data.

5. Deriving the Detector

Now build the hardware, and derive it rather than presenting it.

The requirement is to notice a high-to-low transition on SDA that occurs while SCL is high. Start from what a synchronous design can actually see: on each clock edge it gets one sample of each conductor. Everything else has to be reconstructed.

A transition needs two samples. "SDA went from high to low" is a statement about two consecutive observations, so the detector must store the previous one. One bit of history per conductor is the minimum, and as it turns out the minimum is also sufficient.

"While SCL is high" needs to be true across the transition, not at a point. This is the subtle part, and it is the same subtlety Chapter 4.2 worked through for its stability checker. The naive reading is to test SCL on the current sample: did SDA just fall, and is SCL high now? That condition is satisfied by a case which is not a START — an SDA edge that happened while SCL was still low, in the very same sampling interval that SCL rose. In a sampled model those two events are indistinguishable inside one interval, and the conservative choice is to require SCL high on both samples:

Azvya Education Pvt. Ltd.VLSI Mentor
MATHEMATICAL DERIVATION — the detection condition, term by term
   start_detected  =  scl_prev  AND  scl_now  AND  sda_prev  AND  NOT sda_now
                      ^^^^^^^^       ^^^^^^^       ^^^^^^^^        ^^^^^^^^^^
                      SCL was        SCL still     SDA was         SDA is now
                      already        high after    high before     low -- the
                      high before    the edge      the edge        falling edge
                      the edge

   Each of the four terms rejects a different non-START case:

     drop scl_prev  -> an SDA fall coincident with SCL RISING is reported
     drop scl_now   -> an SDA fall coincident with SCL FALLING is reported
     drop sda_prev  -> a level is reported instead of an edge
     drop sda_now   -> the opposite edge (a STOP) is reported as a START

Four terms, four distinct jobs. §8 injects a fault into each and confirms the count — and the lesson from Chapter 4.2 applies again here, because the two SCL terms are each wrong at only one boundary and a test that exercises neither cannot tell a complete guard from a partial one.

6. The Detector in Three Languages

The design is the same hardware in all three: two history flip-flops, a four-term combinational condition, and a registered single-cycle output.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_start_detector.sv — SYNTHESIZABLE RTL. One sample of history is the whole mechanism.
   module i2c_start_detector (
       input  logic clk,
       input  logic rst_n,
       input  logic scl_in,          // observed bus level, not drive intent
       input  logic sda_in,          // observed bus level, not drive intent
       output logic start_detected   // single-cycle event pulse
   );
       // Previous observed samples. A framing edge is a RELATIONSHIP between two
       // samples, so one sample of history is the minimum the detector can work
       // with. Reset values are the idle bus: both lines released, therefore HIGH.
       logic scl_q, sda_q;

       always_ff @(posedge clk) begin
           if (!rst_n) begin
               scl_q <= 1'b1; sda_q <= 1'b1;
               start_detected <= 1'b0;
           end else begin
               // START = SDA HIGH->LOW while SCL is HIGH. SCL must read HIGH on
               // BOTH samples: requiring only the current sample would report a
               // START for an SDA fall that happened while SCL was still low and
               // merely became visible in the same interval SCL rose.
               start_detected <= scl_q && scl_in && sda_q && !sda_in;
               scl_q <= scl_in;
               sda_q <= sda_in;
           end
       end
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_start_detector_tb.sv — SELF-CHECKING TESTBENCH, SIMULATION ONLY. Counts pulses; both edge boundaries; reset history.
   module i2c_start_detector_tb;
       logic clk = 1'b0, rst_n, scl_in, sda_in;
       logic start_detected;
       int   errors = 0;
       int   pulses = 0;

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

       always @(posedge clk) if (rst_n && start_detected) pulses++;

       task automatic expect_pulses(input int want, input string what);
           if (pulses !== want) begin
               $display("FAIL: %s -- expected %0d START pulse(s), saw %0d", what, want, pulses);
               errors++;
           end
           pulses = 0;
       endtask

       // An ordinary data bit: SDA changes only while SCL is LOW.
       task automatic legal_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);
           scl_in = 1'b0;                   repeat (1) @(negedge clk);
       endtask

       // START: SDA falls while SCL is HIGH, then SCL is pulled low.
       task automatic do_start();
           scl_in = 1'b1; sda_in = 1'b1;    repeat (2) @(negedge clk);
           sda_in = 1'b0;                   repeat (3) @(negedge clk);  // the START edge
           scl_in = 1'b0;                   repeat (2) @(negedge clk);
       endtask

       // STOP: SDA RISES while SCL is HIGH. Must never look like a START.
       task automatic do_stop();
           scl_in = 1'b0; sda_in = 1'b0;    repeat (2) @(negedge clk);
           scl_in = 1'b1;                   repeat (2) @(negedge clk);
           sda_in = 1'b1;                   repeat (3) @(negedge clk);  // the STOP edge
       endtask

       // SDA falls while SCL is LOW -- an ordinary data change, not a START.
       task automatic sda_fall_while_low();
           scl_in = 1'b0; sda_in = 1'b1;    repeat (2) @(negedge clk);
           sda_in = 1'b0;                   repeat (2) @(negedge clk);
           scl_in = 1'b1;                   repeat (2) @(negedge clk);
           scl_in = 1'b0;                   repeat (1) @(negedge clk);
       endtask

       // BOUNDARY: SDA falls in the SAME sample interval SCL rises. Ambiguous in a
       // sampled model; this detector deliberately does not claim a START.
       task automatic fall_coincident_with_scl_rise();
           scl_in = 1'b0; sda_in = 1'b1;    repeat (2) @(negedge clk);
           sda_in = 1'b0; scl_in = 1'b1;    repeat (3) @(negedge clk);
           scl_in = 1'b0;                   repeat (1) @(negedge clk);
       endtask

       // BOUNDARY, MIRRORED: SDA falls in the SAME interval SCL FALLS. Also not a
       // START -- the HIGH window has ended. This is the case that catches a
       // detector which looked only at the PREVIOUS SCL sample.
       task automatic fall_coincident_with_scl_fall();
           scl_in = 1'b1; sda_in = 1'b1;    repeat (2) @(negedge clk);
           sda_in = 1'b0; scl_in = 1'b0;    repeat (3) @(negedge clk);
       endtask

       initial begin
           rst_n = 1'b0; scl_in = 1'b1; sda_in = 1'b1;
           repeat (2) @(negedge clk);
           if (start_detected !== 1'b0) begin $display("FAIL: pulse asserted during reset"); errors++; end
           rst_n = 1'b1; @(negedge clk); pulses = 0;

           // 1 -- ordinary data bits must never produce a START.
           legal_bit(1'b0); legal_bit(1'b1); legal_bit(1'b1); legal_bit(1'b0);
           expect_pulses(0, "ordinary data bits");

           // 2 -- a real START produces EXACTLY ONE pulse (an event, not a level).
           do_start();
           expect_pulses(1, "a single START condition");

           // 3 -- STOP is the opposite edge and must not register as a START.
           do_stop();
           expect_pulses(0, "a STOP condition");

           // 4 -- an SDA fall while SCL is LOW is ordinary data movement.
           sda_fall_while_low();
           expect_pulses(0, "SDA falling while SCL is low");

           // 5 -- BOUNDARY: coincident with the SCL rise, deliberately not a START.
           fall_coincident_with_scl_rise();
           expect_pulses(0, "SDA falling as SCL rises");

           // 5b -- BOUNDARY, MIRRORED: coincident with the SCL FALL, also not a START.
           //       Both guard terms are needed; a rise-side test alone cannot show it.
           fall_coincident_with_scl_fall();
           expect_pulses(0, "SDA falling as SCL falls");

           // 6 -- two STARTs in a row produce two separate pulses.
           do_start(); do_start();
           expect_pulses(2, "two successive STARTs");

           // 7 -- reset clears the detector and it works afterwards.
           rst_n = 1'b0; scl_in = 1'b1; sda_in = 1'b1; repeat (2) @(negedge clk);
           rst_n = 1'b1; @(negedge clk); pulses = 0;
           do_start();
           expect_pulses(1, "a START after reset");

           // 8 -- RESET HISTORY: a START arriving in the very FIRST cycle after reset
           //      release must not be missed. This is the only test that can observe
           //      what the detector assumed the bus was doing before it was watching:
           //      reset assumes an IDLE bus (both lines HIGH), so the first sample
           //      already has a valid "SDA was high, SCL was high" history to compare
           //      against. A detector that reset its history LOW silently drops this
           //      START -- and a monitor enabled onto a live bus does exactly this.
           rst_n = 1'b0; scl_in = 1'b1; sda_in = 1'b1;  repeat (2) @(negedge clk);
           rst_n = 1'b1; sda_in = 1'b0;                 repeat (3) @(negedge clk);
           expect_pulses(1, "a START in the first cycle after reset release");
           scl_in = 1'b0;                               repeat (1) @(negedge clk);

           if (errors == 0) $display("PASS: START detected once per event; data, STOP and boundary cases rejected");
           else             $display("FAIL: %0d error(s)", errors);
           $finish;
       end
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_start_detector.v — SYNTHESIZABLE RTL. The same detector in Verilog-2005.
   module i2c_start_detector (
       input  wire clk,
       input  wire rst_n,
       input  wire scl_in,          // observed bus level, not drive intent
       input  wire sda_in,          // observed bus level, not drive intent
       output reg  start_detected   // single-cycle event pulse
   );
       // One sample of history: a framing edge is a relationship between two
       // samples. Reset values are the idle bus -- both lines released, so HIGH.
       reg scl_q, sda_q;

       always @(posedge clk) begin
           if (!rst_n) begin
               scl_q <= 1'b1; sda_q <= 1'b1;
               start_detected <= 1'b0;
           end else begin
               // START = SDA HIGH->LOW while SCL is HIGH, on BOTH samples.
               start_detected <= scl_q & scl_in & sda_q & ~sda_in;
               scl_q <= scl_in;
               sda_q <= sda_in;
           end
       end
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_start_detector_tb.v — SELF-CHECKING TESTBENCH, SIMULATION ONLY. The same cases in Verilog idiom.
   module i2c_start_detector_tb;
       reg  clk, rst_n, scl_in, sda_in;
       wire start_detected;
       integer errors;
       integer pulses;
       integer step;

       i2c_start_detector dut (.clk(clk), .rst_n(rst_n), .scl_in(scl_in),
           .sda_in(sda_in), .start_detected(start_detected));

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

       always @(posedge clk) if (rst_n && start_detected) pulses = pulses + 1;

       task expect_pulses; input integer want; begin
           if (pulses !== want) begin
               $display("FAIL: step %0d -- expected %0d START pulse(s), saw %0d", step, want, pulses);
               errors = errors + 1;
           end
           pulses = 0;
           step = step + 1;
       end endtask

       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

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

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

       task sda_fall_while_low; begin
           scl_in = 1'b0; sda_in = 1'b1;    repeat (2) @(negedge clk);
           sda_in = 1'b0;                   repeat (2) @(negedge clk);
           scl_in = 1'b1;                   repeat (2) @(negedge clk);
           scl_in = 1'b0;                   repeat (1) @(negedge clk);
       end endtask

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

       // BOUNDARY, MIRRORED: SDA falls in the same interval SCL FALLS -- not a START.
       task fall_coincident_with_scl_fall; begin
           scl_in = 1'b1; sda_in = 1'b1;    repeat (2) @(negedge clk);
           sda_in = 1'b0; scl_in = 1'b0;    repeat (3) @(negedge clk);
       end endtask

       initial begin
           errors = 0; pulses = 0; step = 1;
           rst_n = 1'b0; scl_in = 1'b1; sda_in = 1'b1;
           repeat (2) @(negedge clk);
           if (start_detected !== 1'b0) begin $display("FAIL: pulse asserted during reset"); errors = errors + 1; end
           rst_n = 1'b1; @(negedge clk); pulses = 0;

           legal_bit(1'b0); legal_bit(1'b1); legal_bit(1'b1); legal_bit(1'b0);
           expect_pulses(0);

           do_start();
           expect_pulses(1);

           do_stop();
           expect_pulses(0);

           sda_fall_while_low();
           expect_pulses(0);

           fall_coincident_with_scl_rise();
           expect_pulses(0);

           fall_coincident_with_scl_fall();
           expect_pulses(0);

           do_start(); do_start();
           expect_pulses(2);

           rst_n = 1'b0; scl_in = 1'b1; sda_in = 1'b1; repeat (2) @(negedge clk);
           rst_n = 1'b1; @(negedge clk); pulses = 0;
           do_start();
           expect_pulses(1);

           // RESET HISTORY: a START in the very FIRST cycle after reset release must
           // not be missed -- reset assumes an IDLE bus, so the first sample already
           // has a valid history. Resetting the history LOW silently drops this START.
           rst_n = 1'b0; scl_in = 1'b1; sda_in = 1'b1;  repeat (2) @(negedge clk);
           rst_n = 1'b1; sda_in = 1'b0;                 repeat (3) @(negedge clk);
           expect_pulses(1);
           scl_in = 1'b0;                               repeat (1) @(negedge clk);

           if (errors == 0) $display("PASS: START detected once per event; data, STOP and boundary cases rejected");
           else             $display("FAIL: %0d error(s)", errors);
           $finish;
       end
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_start_detector.vhd — SYNTHESIZABLE RTL. The same detector in VHDL.
   library ieee;
   use ieee.std_logic_1164.all;

   entity i2c_start_detector is
       port (
           clk             : in  std_logic;
           rst_n           : in  std_logic;
           scl_in          : in  std_logic;   -- observed bus level, not drive intent
           sda_in          : in  std_logic;   -- observed bus level, not drive intent
           start_detected  : out std_logic    -- single-cycle event pulse
       );
   end entity;

   architecture rtl of i2c_start_detector is
       -- One sample of history: a framing edge is a relationship between two
       -- samples. Reset values are the idle bus -- both lines released, so HIGH.
       signal scl_q : std_logic := '1';
       signal sda_q : std_logic := '1';
   begin
       process (clk)
       begin
           if rising_edge(clk) then
               if rst_n = '0' then
                   scl_q <= '1'; sda_q <= '1';
                   start_detected <= '0';
               else
                   -- START = SDA HIGH->LOW while SCL is HIGH, on BOTH samples.
                   if scl_q = '1' and scl_in = '1' and sda_q = '1' and sda_in = '0' then
                       start_detected <= '1';
                   else
                       start_detected <= '0';
                   end if;
                   scl_q <= scl_in;
                   sda_q <= sda_in;
               end if;
           end if;
       end process;
   end architecture;
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_start_detector_tb.vhd — SELF-CHECKING TESTBENCH, SIMULATION ONLY. The same cases with assert report severity.
   library ieee;
   use ieee.std_logic_1164.all;

   entity i2c_start_detector_tb is
   end entity;

   architecture sim of i2c_start_detector_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 start_detected : std_logic;
       -- Driven only by the counting process, read by the checking process, so the
       -- checker snapshots it rather than clearing it (one driver per signal).
       signal pulses         : natural := 0;
       -- Set by the checker just before it suspends, so the watchdog can tell a
       -- finished run from a stalled one.
       signal test_done : std_logic := '0';
   begin
       dut : entity work.i2c_start_detector
           port map (clk => clk, rst_n => rst_n, scl_in => scl_in,
                     sda_in => sda_in, start_detected => start_detected);

       clk <= not clk after 5 ns;

       -- Watchdog. A broken design must produce a REPORTED FAILURE, not a silent
       -- stop: without this, a `wait until` against a stalled DUT simply runs to the
       -- simulator's time limit and prints nothing that identifies the problem.
       watchdog : process
       begin
           wait for 20 us;
           if test_done = '0' then
               report "watchdog expired -- the design never reached the expected state"
                   severity failure;
           end if;
           wait;
       end process;

       count : process (clk)
       begin
           if rising_edge(clk) then
               if rst_n = '1' and start_detected = '1' then
                   pulses <= pulses + 1;
               end if;
           end if;
       end process;

       stim : process
           variable errs : natural := 0;
           variable base : natural := 0;
           variable step : natural := 1;

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

           procedure expect_pulses (want : in natural) is
           begin
               if pulses - base /= want then
                   report "step " & integer'image(step) & ": expected " &
                          integer'image(want) & " START pulse(s), saw " &
                          integer'image(pulses - base) severity error;
                   errs := errs + 1;
               end if;
               base := pulses;
               step := step + 1;
           end procedure;

           procedure legal_bit (v : in std_logic) is
           begin
               scl_in <= '0';               waitn(2);
               sda_in <= v;                 waitn(2);
               scl_in <= '1';               waitn(3);
               scl_in <= '0';               waitn(1);
           end procedure;

           procedure do_start is
           begin
               scl_in <= '1'; sda_in <= '1'; waitn(2);
               sda_in <= '0';                waitn(3);
               scl_in <= '0';                waitn(2);
           end procedure;

           procedure do_stop is
           begin
               scl_in <= '0'; sda_in <= '0'; waitn(2);
               scl_in <= '1';                waitn(2);
               sda_in <= '1';                waitn(3);
           end procedure;

           procedure sda_fall_while_low is
           begin
               scl_in <= '0'; sda_in <= '1'; waitn(2);
               sda_in <= '0';                waitn(2);
               scl_in <= '1';                waitn(2);
               scl_in <= '0';                waitn(1);
           end procedure;

           -- BOUNDARY, MIRRORED: SDA falls in the same interval SCL FALLS -- not a START.
           procedure fall_coincident_with_scl_fall is
           begin
               scl_in <= '1'; sda_in <= '1';  waitn(2);
               sda_in <= '0'; scl_in <= '0';  waitn(3);
           end procedure;

           procedure fall_coincident_with_scl_rise is
           begin
               scl_in <= '0'; sda_in <= '1';  waitn(2);
               sda_in <= '0'; scl_in <= '1';  waitn(3);
               scl_in <= '0';                 waitn(1);
           end procedure;
       begin
           waitn(2);
           if start_detected /= '0' then
               report "pulse asserted during reset" severity error; errs := errs + 1; end if;
           rst_n <= '1'; waitn(1); base := pulses;

           legal_bit('0'); legal_bit('1'); legal_bit('1'); legal_bit('0');
           expect_pulses(0);

           do_start;  expect_pulses(1);
           do_stop;   expect_pulses(0);
           sda_fall_while_low;             expect_pulses(0);
           fall_coincident_with_scl_rise;  expect_pulses(0);
           fall_coincident_with_scl_fall;  expect_pulses(0);
           do_start; do_start;             expect_pulses(2);

           rst_n <= '0'; scl_in <= '1'; sda_in <= '1'; waitn(2);
           rst_n <= '1'; waitn(1); base := pulses;
           do_start;  expect_pulses(1);

           -- RESET HISTORY: a START in the very FIRST cycle after reset release must
           -- not be missed -- reset assumes an IDLE bus, so the first sample already
           -- has a valid history. Resetting the history LOW silently drops this START.
           rst_n <= '0'; scl_in <= '1'; sda_in <= '1';  waitn(2);
           rst_n <= '1'; sda_in <= '0';                 waitn(3);
           expect_pulses(1);
           scl_in <= '0';                               waitn(1);

           if errs = 0 then
               report "i2c_start_detector self-check complete: one pulse per START, all rejections held" severity note;
           else
               report "i2c_start_detector self-check FAILED" severity error;
           end if;
           test_done <= '1';
           wait;
       end process;
   end architecture;

6a. Cross-Language Parity

All three describe identical hardware, and the testbenches are the same eight steps in three idioms.

SystemVerilogVerilog-2005VHDL
history registerslogic scl_q, sda_qreg scl_q, sda_qsignal scl_q, sda_q : std_logic
reset history value1'b1 (idle bus)1'b1'1'
detection condition&& chain& chainand chain
outputregistered logicregistered regregistered std_logic
pulse counting in TBint incrementedinteger incrementedsignal driven by one process, snapshotted by the checker
watchdog#20000 then report and $finishsamea watchdog process, guarded by a completion flag

The VHDL testbench differs in structure for a language reason rather than a design one. A signal in VHDL may have one driver, so the pulse counter is driven by a dedicated counting process and the checker reads it and snapshots a baseline, instead of clearing a shared variable the way the Verilog versions do. The measurements are identical; the bookkeeping respects the language.

The watchdog needs a second thought in VHDL, and it is worth spelling out because every testbench in this module shares the idiom. In the Verilog versions a watchdog is one line — wait, print, $finish — and $finish ends the simulation, so a watchdog that has not fired by the time the checker finishes is simply never reached. VHDL has no $finish in the same sense: the checker ends with wait, which suspends that process and leaves the simulation running until the time limit. A naive VHDL watchdog therefore fires on every run, including successful ones, and reports a failure that did not happen.

The fix is a completion flag the checker raises just before it suspends, which the watchdog tests before complaining:

Azvya Education Pvt. Ltd.VLSI Mentor
ARCHITECTURAL PSEUDOCODE — why the VHDL watchdog needs a completion flag
   signal test_done : std_logic := '0';      -- raised by the checker at the end

   watchdog : process
   begin
       wait for 20 us;
       if test_done = '0' then               -- only complain if nothing finished
           report "watchdog expired ..." severity failure;
       end if;
       wait;
   end process;

   Verified both ways round: with correct RTL the watchdog stays silent, and with
   a deliberately stalled design (the START hold never completing, so `done` never
   asserts) it reports a failure at its limit instead of the run ending quietly.
   A watchdog that has only ever been observed NOT firing has not been tested.

That last line is the general point. A watchdog is error-handling code, and error-handling code that has never executed is unverified — which is why it is worth deliberately stalling a design once to watch the watchdog work.

Verified execution. All three testbenches were compiled and run, and all three complete at the same simulated time:

languagesimulatorresultcompletes at
SystemVerilogIcarus Verilog, -g2012PASS970 ns
Verilog-2005Icarus Verilog, -g2005PASS970 ns
VHDLnvc 1.23.0PASS970 ns

7. What the Testbench Has To Cover

A detector is a classifier, so the interesting tests are the things it must refuse, not the thing it must find. Six of the eight steps are rejections.

stepstimulusrequired resultwhy it is in the suite
1four ordinary data bitsno pulsethe common case must be silent, or the bus is unusable
2a real STARTexactly one pulsean event, not a level
3a STOPno pulsethe opposite edge must not be confused with this one
4SDA falls while SCL is lowno pulseordinary data movement
5SDA falls as SCL risesno pulseboundary: ambiguous in a sampled model
5bSDA falls as SCL fallsno pulsethe mirror boundary — the other SCL term
6two STARTs in a rowexactly two pulsesrepeatable, not a one-shot
7, 8reset, then a START in the first cycle after releaseone pulsewhat the detector assumed before it was watching

Step 2's "exactly one" is doing real work. A detector that asserted a level for as long as the condition held would pass a test that only asked did you see a START, and would then break every consumer that counts events. Counting pulses rather than checking a flag is what makes the distinction testable.

Step 8 is the one that had to be added, and §8 explains why.

i2c_start_detector — one pulse per START

10 cycles
Ten internal clock cycles. The observed SCL input is high for the first four cycles then goes low and returns high. The observed SDA input falls during the fourth cycle while SCL is high. The start detected output pulses high for exactly one cycle immediately afterwards and is low in every other cycle.SDA falls, SCL high both samplesSDA falls, SCL high bothsamplesone-cycle event pulseone-cycle event pulseclkscl_insda_instart_detectedt0t1t2t3t4t5t6t7t8t9
Figure 3 — the detector's own view, from the simulation. Ten internal clock cycles. SDA falls during the fourth cycle while SCL has been high across both samples, so the condition is satisfied that cycle and the registered output pulses for exactly one cycle afterwards. This figure is SIMULATION-DERIVED: it shows the sampled model the RTL actually implements, not the analog bus.

8. Mutation Testing — and the One That Survived

Five faults were injected into the detector and the testbench run against each.

mutationwhat it breaksresult
drop the previous-SCL guard termover-reports on the SCL rising edgeFAIL — caught by step 5
drop the current-SCL guard termover-reports on the SCL falling edgeFAIL — caught by step 5b
detect the rising edge insteadfinds STOPs and misses STARTsFAIL — step 2 saw 0 pulses
report a level instead of a pulseevent consumers double-countFAIL — step 2 saw more than one
reset the history registers LOWsee belowinitially PASSED

All five are caught now. The fifth is the one worth the space.

The mutation that survived. Resetting scl_q and sda_q to zero instead of one left the whole suite passing. It is tempting to call that mutation behaviourally equivalent — the history registers are overwritten with real bus samples on the very next clock, so how could their initial value matter?

It matters for exactly one cycle, and that cycle is a real operating condition. Consider what the detector must do if a START arrives in the first cycle after reset is released. The golden design resets its history to the idle bus, both lines high, so on that first evaluation it already holds a valid "SDA was high, SCL was high" history and correctly reports the START. The mutant holds scl_q low, cannot satisfy the guard, and silently drops a real START.

That is not a contrived case. It is what happens whenever a monitor is enabled onto a live bus, or a controller's reset is released while another controller is mid-transaction, or a device comes out of a low-power state. A detector that misses the START misses the address that follows it, and then misinterprets the rest of the transfer — which is a far more confusing failure than not detecting anything at all.

The fix was a test, not a change to the RTL: hold reset with the bus idle, then release reset and drop SDA in the same interval. The mutant now fails it.

The general lesson. A reset value that is immediately overwritten still has observable behaviour, and the observation window is the first cycle. Asking "does this reset value matter?" is the same question as "what must this block do in the cycle it starts working?" — and for anything that watches an external bus, the answer is rarely "nothing", because the bus does not wait to be watched. A mutation that survives is usually telling you something true about your verification.

9. An Assertion for the Same Property

The detector's contract can be stated directly as a property, and it is worth comparing.

Azvya Education Pvt. Ltd.VLSI Mentor
SYSTEMVERILOG ASSERTION — VERIFICATION ONLY. The detector's contract as a property.
   // The output must pulse exactly when the four-term condition held on the
   // previous cycle -- an if-and-only-if, so it catches both a missed START and
   // a spurious one. $past is what makes the two-sample history expressible.
   property start_pulse_iff_framing_edge;
       @(posedge clk) disable iff (!rst_n)
       start_detected <==> $past(scl_in && $past(scl_in) && $past(sda_in) && !sda_in);
   endproperty

   // And the event-not-level property: two consecutive asserted cycles would
   // mean a level, and every consumer that counts events would double-count.
   property start_is_a_single_cycle_pulse;
       @(posedge clk) disable iff (!rst_n)
       start_detected |=> !start_detected;
   endproperty

The second property is the more valuable of the two in practice, and it illustrates something about what assertions are good at. "This output is never asserted for two cycles in a row" is a statement about every cycle of every test, forever, and no test has to remember to check it. The procedural testbench in §6 establishes the same thing by counting pulses, which works but only in the scenarios it runs.

The first property is nearly a restatement of the implementation, which is a known hazard: an assertion derived by transcribing the RTL will agree with the RTL even when both are wrong. It earns its place only because it was written from the specification sentence — an SDA high-to-low transition while SCL is high — rather than from the code.

Note also that both are clocked on the internal clock, and therefore inherit Chapter 4.2's limitation: they are assertions about samples, not about the bus. Neither can say anything about how long SCL was actually high in nanoseconds. That is Chapter 5.5's problem and it needs timestamps, not cycles.

10. Verification Connection — A Detector Is Not a Monitor

It is worth separating two components that both watch SDA and SCL, because conflating them produces a verification environment that cannot localise a failure.

A framing detector answers one question: did this specific waveform relationship occur? Its output is an event, it has no notion of a transfer, and it cannot be wrong about anything except edges. The design in §6 is one.

A protocol monitor reconstructs meaning. It consumes framing events, then the address byte, then direction, then payload, and produces a transaction object for a scoreboard. It has a state machine, it knows what phase a transfer is in, and it can be wrong in many more ways.

The reason to keep them apart is diagnostic. When a monitor reports "no transaction seen", the question is immediately whether the bus carried no START or whether the monitor mishandled one that was there — and if framing detection is buried inside the monitor's state machine, there is no way to tell from the outside. A monitor built on a separate, separately-verified detector reduces that to a single check: did the detector fire?

This is also where the reset-value lesson from §8 comes back. A monitor connected to a bus that is already active is the normal case in a real verification environment — testbenches enable components between transactions, but hardware debug does not get that courtesy. Module 21 builds the monitor properly.

11. FPGA and ASIC Implications

The detector costs almost nothing, and that is the point. Two flip-flops and a four-input gate. There is never a reason to share one, and there is never a reason to implement framing detection in software when the hardware is this small — which is exactly why UM10204 notes that detection is easy if the device has the interfacing hardware and awkward if it does not.

The inputs must be synchronised before they reach it. SDA and SCL are asynchronous to the internal clock: they are driven by other devices, filtered by board RC, and have no relationship to the sampling clock at all. Sampling an asynchronous signal directly into logic that then fans out to several places invites metastability, and the standard remedy is a synchroniser on each line before anything else uses it. The detector in §6 assumes that has already happened — its scl_in and sda_in are already-synchronised signals, not raw pads. Module 19 owns the synchroniser and the oversampling filter, and getting it wrong produces intermittent false STARTs that no amount of protocol debugging will explain.

Sampling rate determines what the detector can see, and the specification names a floor. A START whose SDA edge and subsequent SCL fall both land inside one sampling interval is invisible to a sampled detector. UM10204's note that a software implementation must sample SDA at least twice per clock period is the same constraint from the other direction. On an FPGA with a fast internal clock this is comfortable; on a slow clock or a deeply-filtered input it is a real design calculation.

On an ASIC the detector is usually always-on. A peripheral that can be woken by bus activity needs framing detection running in a domain that is not gated off, which makes those two flip-flops part of the always-on partition and their reset a power-on reset rather than a peripheral reset. That changes the answer to §8's question about reset values: the detector may come out of reset onto a bus that has been live for hours.

12. Debugging — The START That Was Never There

A controller whose first transfer after reset was always ignored

Pitfall — pulling SCL low in the same decision that issues the START
Buggy Code
// A controller issues a START by driving both lines in what looks like the
// natural order -- get the bus to the state the byte engine expects, in as few
// cycles as possible:
//
//   ST_IDLE:
//     if (start_req) begin
//         sda_drive_low <= 1'b1;      // SDA falls -- "this is the START"
//         scl_drive_low <= 1'b1;      // and take the clock low, ready for bit 0
//         state         <= ST_BYTE;
//     end
//
// Both assignments are in the same clocked block, so both take effect on the
// same edge. To the designer this reads as "assert the START and begin", and the
// simulation waveform looks plausible: SDA goes low, SCL goes low, the byte
// engine starts shifting, and the testbench's own monitor -- which was written
// to look for 'SDA low and SCL low' as the start of a transfer -- agrees.
Symptom

No target ever acknowledges the address. Every transfer NACKs.

The capture is the confusing part. SDA and SCL both go low at the beginning of the transfer, the address bits are clocked out correctly, the bit timing measures within spec, and the waveform looks like a textbook I2C transfer to anyone scanning it quickly. A decoder in a logic analyser may even display the address and data, because many decoders resynchronise on the first falling SCL edge rather than insisting on a framing event.

Swapping in a different target changes nothing. Slowing the clock down changes nothing. Adding pull-up strength changes nothing. Every hypothesis that treats this as an electrical or a timing problem fails, because it is neither.

Root Cause

There was no START condition on the bus. Not a marginal one -- none.

A START is an SDA edge that occurs WHILE SCL IS HIGH. Driving both lines low on the same clock edge means SDA's falling edge and SCL's falling edge are simultaneous, so at no point is there an interval with SCL high and SDA newly low. Every target's framing detector -- which requires SCL high across the transition, for the reasons in section 5 -- correctly sees nothing. The targets are not ignoring the START; they never received one, so they never began listening for an address, so the address bits that followed were clocked into devices that were not paying attention.

The reason this survives review is that the design looks like it asserts the START, and the reason it survives simulation is that the testbench monitor was written with the same misunderstanding as the RTL. A monitor that defines the beginning of a transfer as 'both lines low' will agree with a controller that produces 'both lines low', and the pair of them will be consistently wrong. That is the specific hazard of writing the checker from the same mental model as the design.

Note also the ordering constraint this violates: section 4 pointed out that the clock cannot begin until the framing edge has already happened, and that the specification words the requirement as 'after this period, the first clock pulse is generated'. The bug is that ordering collapsed to zero.

Fix
// Separate the two edges into two states, with the framing edge strictly first
// and a counted interval between them:
//
//   ST_IDLE:      if (start_req) begin
//                     sda_drive_low <= 1'b1;            // the START edge, alone
//                     cnt           <= T_HD_STA - 1;    // SCL is still HIGH here
//                     state         <= ST_START_HOLD;
//                 end
//   ST_START_HOLD: if (cnt != 0) cnt <= cnt - 1;
//                  else begin
//                      scl_drive_low <= 1'b1;           // NOW the first clock pulse
//                      state         <= ST_BYTE;
//                  end
//
// That is exactly the first two states of the sequencer Chapter 5.5 builds, and
// the interval between them is tHD;STA.
//
// The verification fix matters more than the RTL fix, because the RTL fix is
// obvious once the bug is understood and the verification gap is what let it
// ship. Two changes:
//
//   1. Write the framing check from the SPECIFICATION SENTENCE, not from the
//      design's intent: 'an SDA high-to-low transition WHILE SCL IS HIGH'. The
//      detector in section 6 is that sentence, and it reports zero STARTs for
//      this controller -- immediately, on the first test.
//
//   2. Assert the count, not just the occurrence. A test that checks 'a START
//      was seen' can pass for the wrong reason; a test that checks 'exactly one
//      START was seen before the first address bit' cannot.
//
// The bench habit that finds it in minutes: trigger the analyser on the framing
// condition itself rather than on activity. An analyser set to trigger on 'SDA
// falling while SCL is high' never triggers on this board, and that null result
// is the diagnosis.

13. Common Misconceptions

"A START is SDA going low." A START is SDA going low while SCL is high. SDA goes low in the ordinary course of transmitting a zero, many times per transfer. §2 shows the identical edge in both roles, and §12 is what happens to a design that drops the qualifier.

"The controller pulls SCL low to start a transfer." It pulls SCL low after the START, to begin the first clock pulse. The framing edge comes first and the clock waits for it; reversing the order produces no START at all.

"A START tells the addressed device to get ready." There is no addressed device yet — the address has not been sent. A START is a broadcast that every device on the segment acts on, and most of them will conclude within a byte that the transfer is not theirs.

"A device that is idle can ignore the bus until it is addressed." It cannot know it is being addressed without first seeing the START and then shifting in the address byte. Framing detection is a continuous obligation.

"Testing SCL on the current sample is good enough." It is wrong at one boundary, and the mirror simplification is wrong at the other. Both SCL terms are load-bearing, and §8 injects a fault into each to prove it.

"A reset value that gets overwritten immediately cannot matter." It determines behaviour in the first cycle, and for a block watching an external bus the first cycle is a real operating condition. §8 is a worked example of that assumption being wrong.

"If the logic analyser decodes the transfer, the framing was legal." Many decoders resynchronise on clock activity rather than insisting on a framing event, so a decoder can display a plausible address and payload for a transfer that had no START at all. §12 depends on exactly this.

14. Reason It Through

A controller drives SDA low one cycle before it drives SCL low. Is that a legal START?

It produces a START edge, because there is an interval with SCL high and SDA newly low — so unlike §12's design, this one does put a framing event on the bus. Whether it is legal is a different question, and the answer is that one cycle is almost certainly too short: the interval between the START edge and the first clock pulse has a specified minimum, and one internal clock cycle of a fast FPGA is far below it. The distinction matters because the two failures look nothing alike. No START means no target ever responds; a too-short START means targets respond on some boards, at some temperatures. Chapter 5.5 quantifies it.

Why can a detector for this event be stateless while a classifier cannot?

Because the definition refers only to the two conductors. "An SDA high-to-low transition while SCL is high" is answerable from two samples of two signals and nothing else. Deciding whether that START is a first START or a repeated one requires knowing whether a transfer was already in progress, which is not visible on the conductors at that instant — so it requires state that the detector has no need for. Chapter 5.4 adds exactly that state and nothing more.

A monitor is enabled onto a bus in the middle of a transfer, and immediately reports a START. Is it broken?

Not necessarily — it depends on what the bus was doing. If a genuine START arrived in the monitor's first sampling interval, reporting it is correct, and §8 is about making sure the monitor can. If no edge occurred at all and the monitor reported one anyway, then its reset history did not match the bus it was connected to: it assumed SDA was high, the bus had SDA low, and the first real sample looked like a falling edge. That failure mode is the reason a monitor enabled onto a live bus should either be reset while the bus is idle or declare its state unknown until it has seen a STOP.

Two of the four terms in the detection condition concern SCL. Why is dropping either one a different bug?

Because each rejects a different boundary. The previous-SCL term rejects an SDA fall that coincides with SCL rising; the current-SCL term rejects one that coincides with SCL falling. A mutant missing the first over-reports at the rising boundary and is correct everywhere else; a mutant missing the second over-reports at the falling boundary and is correct everywhere else. A test suite that exercises neither boundary cannot distinguish either mutant from the golden design — which is why both boundary tests are in §7, and why Chapter 4.2 had to learn the same thing about its stability checker.

Your testbench monitor and your controller RTL were written by the same engineer from the same notes. What class of bug does that arrangement not catch?

Any bug in the shared understanding. The monitor will agree with the RTL precisely where both misread the specification, which is the situation in §12 — a controller that produced "both lines low" and a monitor that looked for "both lines low" were consistent with each other and inconsistent with the bus. The defence is to write the checker from the specification sentence rather than from the design's intent, which is a discipline rather than a tool, and to prefer checks that count events over checks that test flags.

15. Understanding Check

16. Summary

A START is a high-to-low SDA transition while SCL is high, and both halves of that sentence are load-bearing. The identical SDA edge during a low phase is an ordinary data zero.

Only the controller can generate one, and that follows from clock ownership: placing an edge inside a high phase requires deciding when the high phase is.

It is a broadcast, and it is unconditional. The bus becomes busy, every target begins listening for an address, and every target abandons whatever partial protocol state it held. That last rule is what makes the bus recoverable, and it is what makes a repeated START useful.

The framing edge precedes the first clock pulse, with a specified minimum interval between them. Collapsing that ordering does not produce a marginal START — it produces none, which is §12's failure.

Detection needs two samples and four terms. One sample of history per conductor, and a condition requiring SCL high on both samples. Each of the four terms rejects a different non-START case, and the two SCL terms are each wrong at only one boundary — so both boundaries need their own test.

A reset value that is immediately overwritten is still observable, for the length of one cycle. §8's surviving mutation is a worked example, and the case it exposed — a START in the first cycle after reset release — is ordinary rather than exotic.

A detector is not a monitor. Keeping them separate is what lets a failing environment distinguish "the bus carried no START" from "the monitor mishandled one".

17. What Comes Next

The bus can now be started, and the event that starts it can be detected and trusted. What has not been established is how a transfer ends — and it turns out that ending one is the less symmetric half of the pair, because releasing the bus is not the same as making it available.

Chapter 5.3 takes the rising edge: the STOP condition, the difference between released and free that Chapter 5.1 introduced, and a tracker that keeps the two apart — the design whose absence caused that chapter's debugging case.

Browse the full path on the I²C tutorials index. For the reserved space this edge comes from, see Bus Idle and the Framing Primitives; for the rule that makes it detectable, The Data-Valid Rule.

Continue learning