Skip to content
VLSI Mentor

I²C · Module 4

SCL Generation and the Bit Period

A legal I²C clock is not a frequency. LOW and HIGH are separately constrained phases, the controller pulls SCL low and releases it rather than driving it high, and a naive integer divider satisfies none of that. Build a parameterised phase generator in three languages and measure it.

Modules 1 to 3 established what the bus is, how devices share it safely, and who is responsible for what. Not one of those chapters said when. Chapter 3.1 asserted that the controller "generates the timing" and left the sentence standing without ever explaining what a legal clock looks like.

That is the gap Module 4 closes, and it starts here — because everything else about timing is measured against SCL. This chapter derives why time needs constraining at all, then builds the hardware that produces it.

1. Start From the Receiver, Not the Clock

The instinct is to begin with a timing table. Begin with a question instead.

A receiving device has to turn a voltage on SDA into a bit. To do that it must look at the line at some instant and decide. So: what happens if it looks while SDA is changing?

There is no good answer. The line is somewhere between levels, the input's decision is not guaranteed, and two devices looking at the same instant need not agree. The received bit is not wrong — it is undefined, which is worse, because a wrong bit is reproducible and an undefined one is not.

Everything in I²C timing follows from removing that possibility. The bus needs an arrangement in which the receiver always looks while the value is settled, and since no device can read another's mind, the arrangement has to be a rule about time that both sides implement independently.

That rule needs two halves. SCL has to say when to look — that is this chapter. SDA has to be stable around that instant — that is Chapter 4.2.

2. Signals Do Not Teleport

One habit has to be unlearned before any of this is believable, and RTL simulation is what teaches it.

In a simulator a signal goes from 0 to 1 at a timestamp. There is no in-between. That is an extremely useful abstraction and it is not what a wire does.

A real transition takes time. The driver has propagation delay before it starts. The conductor's voltage moves at a rate set by what is driving it and what is loaded on it — and Chapter 2.4 showed that on this bus the two directions are not even symmetric: a falling edge is actively driven and sharp, while a rising edge is a pull-up charging real capacitance. A receiving input then decides by comparing against a threshold, so the moment the line "becomes" HIGH is the moment it crosses that threshold, not the moment the driver released it.

So there are three different clocks in this subject, and confusing them is the most common source of I²C timing bugs:

DomainUnitWho controls it
RTL cycle timinginteger internal clock cyclesthe design's counters
Bus timingreal time on SDA and SCLthe design plus its clock frequency
Electrical transition timingrise and fall shapethe board — resistor and capacitance

A counter in RTL can be perfectly correct and still produce an illegal bus waveform, because the conversion between the first two rows depends on a clock frequency the RTL does not see, and the third row is not in the RTL at all. §6 makes that conversion explicit.

3. Why an Integer Divider Is the Wrong Model

Here is the design nearly everyone writes first:

Azvya Education Pvt. Ltd.VLSI Mentor
the naive divider — ARCHITECTURAL PSEUDOCODE, shown to be rejected
   // Divide the internal clock down to the target SCL frequency and toggle.
   always @(posedge clk)
       if (div_count == N-1) begin
           div_count <= 0;
           scl       <= ~scl;     // <-- two separate mistakes in one line
       end else
           div_count <= div_count + 1;

It produces a square wave at roughly the right frequency, and it is wrong in ways that matter.

It drives SCL HIGH. scl <= ~scl means the design actively asserts a 1. Chapter 2.3 deleted that capability from every participant for a reason, and a controller that drives SCL high reintroduces exactly the fault Chapter 2.2 rejected. The only two legal states are pull LOW and release.

It makes LOW and HIGH the same length. A 50/50 duty cycle is a consequence of toggling, not a requirement anybody asked for — and the specification constrains the LOW and HIGH phases separately. A waveform whose average frequency is exactly right can still violate one phase's minimum while the other is generously long. That failure mode is the subject of §9's second debug case, and it is invisible to anyone who measures only frequency.

It has nowhere to put the events that matter. A controller does not only need a clock; it needs to know when to change SDA and when the receiver will be looking. Those happen at particular points inside the bit, and a design whose only state is "which half am I in" has no vocabulary for them.

It assumes releasing SCL makes SCL high. It does not, and that becomes load-bearing later. Releasing is a statement about what this controller is doing; the line's level is the resolved result of every participant. §7 develops the consequence.

So the right model is not a divider. It is an explicit phase machine with named phases, separately parameterised durations, and event markers at the positions a transfer needs.

4. The Phase Model

A block diagram of an SCL timing generator. An internal clock and an enable feed a phase state machine with two states, LOW and HIGH. A down counter loaded from separate LOW_CYCLES and HIGH_CYCLES parameters times each phase. The state machine drives a pull-low intent output and three single-cycle tick outputs marking the start of the low phase, the start of the high phase, and the completion of a bit period.Internal clockthe only time sourceenablerun or release the busPhase counterloaded per phasePhase machineLOW then HIGHLOW_CYCLES, HIGH_CYCLESindependent lengthsscl_drive_lowpull LOW, or releaselow / high / bit tickswhere events attach12
Figure 1 \u2014 one bit period as two separately-counted phases. The controller pulls SCL low for the LOW phase and releases it for the HIGH phase; each has its own length. The tick outputs mark the phase boundaries, which is where the rest of a controller hangs its data-launch and data-sample decisions.

Read the figure for what it replaces. There is no scl register and no toggle. The output is an intent — scl_drive_low — and the two phases are counted from two different parameters. The ticks exist so that a bit engine built on top has defined instants to work with, rather than having to re-derive them from a divider's internal count.

The behaviour every implementation below shares, stated once:

  • Reset releases SCL. A controller in reset must not hold the bus down for everybody else.
  • Disabled releases SCL. Same reasoning, and it is a real failure mode: a controller that stops mid-transfer while pulling LOW leaves the whole segment stuck.
  • The LOW phase lasts exactly LOW_CYCLES internal clock cycles with scl_drive_low asserted.
  • The HIGH phase lasts exactly HIGH_CYCLES cycles with scl_drive_low released.
  • low_tick and high_tick are one-cycle pulses at the start of their phases; bit_tick is a one-cycle pulse marking a completed bit period.
  • Both parameters must be at least 1. A zero-length phase is not a slower clock, it is a missing phase.

4a. SystemVerilog

Azvya Education Pvt. Ltd.VLSI Mentor
scl_timing_gen.sv — SYNTHESIZABLE RTL. Two separately-counted phases, release semantics preserved.
   module scl_timing_gen #(
       parameter int LOW_CYCLES  = 5,
       parameter int HIGH_CYCLES = 3
   )(
       input  logic clk,
       input  logic rst_n,
       input  logic enable,
       output logic scl_drive_low,
       output logic phase_low,
       output logic phase_high,
       output logic low_tick,
       output logic high_tick,
       output logic bit_tick
   );
       // Both phases must be at least one cycle: a zero-length phase is not a
       // slower clock, it is a missing phase.
       initial begin
           if (LOW_CYCLES  < 1) $fatal(1, "scl_timing_gen: LOW_CYCLES must be >= 1");
           if (HIGH_CYCLES < 1) $fatal(1, "scl_timing_gen: HIGH_CYCLES must be >= 1");
       end

       localparam int MAXC = (LOW_CYCLES > HIGH_CYCLES) ? LOW_CYCLES : HIGH_CYCLES;
       localparam int CW   = (MAXC <= 1) ? 1 : $clog2(MAXC);

       typedef enum logic { PH_LOW = 1'b0, PH_HIGH = 1'b1 } phase_e;
       phase_e          phase;
       logic [CW-1:0]   cnt;
       logic            running;

       always_ff @(posedge clk) begin
           if (!rst_n) begin
               phase <= PH_LOW; cnt <= '0; running <= 1'b0;
               scl_drive_low <= 1'b0;                  // RELEASE on reset: a disabled
               low_tick <= 1'b0; high_tick <= 1'b0;    //   controller must not hold the
               bit_tick <= 1'b0;                       //   bus LOW for everyone else
           end else begin
               low_tick <= 1'b0; high_tick <= 1'b0; bit_tick <= 1'b0;
               if (!enable) begin
                   // Disabled: release SCL and return to a known phase. Stopping while
                   // pulling LOW would leave the whole bus stuck.
                   phase <= PH_LOW; cnt <= '0; running <= 1'b0; scl_drive_low <= 1'b0;
               end else if (!running) begin
                   running <= 1'b1; phase <= PH_LOW;
                   cnt <= CW'(LOW_CYCLES - 1);
                   scl_drive_low <= 1'b1; low_tick <= 1'b1;
               end else if (cnt != '0) begin
                   cnt <= cnt - 1'b1;
               end else if (phase == PH_LOW) begin
                   phase <= PH_HIGH; cnt <= CW'(HIGH_CYCLES - 1);
                   scl_drive_low <= 1'b0;              // RELEASE, never drive 1
                   high_tick <= 1'b1;
               end else begin
                   phase <= PH_LOW; cnt <= CW'(LOW_CYCLES - 1);
                   scl_drive_low <= 1'b1;
                   low_tick <= 1'b1; bit_tick <= 1'b1; // one bit period completed
               end
           end
       end

       assign phase_low  = running && (phase == PH_LOW);
       assign phase_high = running && (phase == PH_HIGH);
   endmodule

The testbench measures the phases rather than waiting a fixed time and assuming. That distinction is the whole point of a timing testbench: a test that waits LOW_CYCLES cycles and then checks something has assumed the answer it was supposed to verify. Here measure_bit counts the clock cycles for which the controller is actually pulling SCL low, then the cycles for which it has released, and compares both against the parameters.

The parameters are deliberately asymmetric — LOW_CYCLES = 5, HIGH_CYCLES = 3 — so that any implementation which quietly produces a 50/50 duty cycle fails immediately. A symmetric test case would pass against exactly the naive divider §3 rejected.

Azvya Education Pvt. Ltd.VLSI Mentor
scl_timing_gen_tb.sv — SELF-CHECKING TESTBENCH, SIMULATION ONLY. Measures phases; asymmetric parameters.
   module scl_timing_gen_tb;
       localparam int LOW_CYCLES = 5, HIGH_CYCLES = 3;   // deliberately asymmetric

       logic clk = 1'b0, rst_n, enable;
       logic scl_drive_low, phase_low, phase_high, low_tick, high_tick, bit_tick;
       int errors = 0;

       scl_timing_gen #(.LOW_CYCLES(LOW_CYCLES), .HIGH_CYCLES(HIGH_CYCLES)) dut (.*);
       always #5 clk = ~clk;

       // WATCHDOG. A timing testbench that waits on a phase boundary can hang
       // forever against a broken generator, and a hang is a much worse failure
       // report than a FAIL line. Bound the whole run.
       initial begin
           #20000;
           $display("FAIL: watchdog expired -- the generator never produced the expected phases");
           $finish;
       end

       // MEASURE the phase, never assume it. Count clk cycles for which the
       // controller is pulling SCL LOW, then for which it has released it.
       task automatic measure_bit(output int low_len, output int high_len);
           low_len = 0; high_len = 0;
           while (scl_drive_low !== 1'b1) @(negedge clk);   // align to a LOW phase
           while (scl_drive_low === 1'b1) begin low_len++;  @(negedge clk); end
           while (scl_drive_low === 1'b0) begin high_len++; @(negedge clk); end
       endtask

       int lo, hi, ticks;
       initial begin
           rst_n = 0; enable = 0;
           repeat (2) @(negedge clk);
           // 1 -- RESET: released, not driving, no ticks, no phase claimed.
           if (scl_drive_low !== 1'b0) begin $error("reset must RELEASE scl, not pull it low"); errors++; end
           if (phase_low || phase_high) begin $error("no phase may be claimed while idle"); errors++; end
           rst_n = 1; @(negedge clk);

           // 2 -- DISABLED: still released, still idle.
           repeat (4) @(negedge clk);
           if (scl_drive_low !== 1'b0) begin $error("disabled must RELEASE scl"); errors++; end
           if (low_tick || high_tick || bit_tick) begin $error("no ticks while disabled"); errors++; end

           // 3 -- ENABLE and measure three consecutive bit periods exactly.
           enable = 1;
           for (int b = 0; b < 3; b++) begin
               measure_bit(lo, hi);
               if (lo !== LOW_CYCLES) begin
                   $error("bit %0d: LOW phase was %0d cycles, expected %0d", b, lo, LOW_CYCLES); errors++; end
               if (hi !== HIGH_CYCLES) begin
                   $error("bit %0d: HIGH phase was %0d cycles, expected %0d", b, hi, HIGH_CYCLES); errors++; end
           end

           // 4 -- TICK WIDTH: bit_tick must be exactly one cycle wide.
           // Sample ONCE PER CYCLE, at the negedge, or a single-cycle pulse is
           // counted twice -- once at the posedge and once at the negedge.
           @(negedge clk);
           while (bit_tick !== 1'b1) @(negedge clk);
           ticks = 0;
           while (bit_tick === 1'b1) begin ticks++; @(negedge clk); end
           if (ticks !== 1) begin $error("bit_tick was %0d cycles wide, expected 1", ticks); errors++; end

           // 5 -- PHASE EXCLUSIVITY: never both, and exactly one while running.
           repeat (20) begin
               @(negedge clk);
               if (phase_low && phase_high) begin $error("both phases asserted"); errors++; end
               if (!phase_low && !phase_high) begin $error("no phase asserted while running"); errors++; end
               // And the drive intent must agree with the claimed phase.
               if (phase_low  && scl_drive_low !== 1'b1) begin $error("LOW phase must pull SCL low"); errors++; end
               if (phase_high && scl_drive_low !== 1'b0) begin $error("HIGH phase must RELEASE SCL"); errors++; end
           end

           // 6 -- DISABLE mid-stream must leave the bus RELEASED, not held low.
           @(negedge clk); enable = 0;
           repeat (3) @(negedge clk);
           if (scl_drive_low !== 1'b0) begin $error("disable left SCL pulled LOW -- bus would be stuck"); errors++; end

           if (errors == 0) $display("PASS: LOW=%0d HIGH=%0d measured exactly; release semantics held", LOW_CYCLES, HIGH_CYCLES);
           else             $display("FAIL: %0d error(s)", errors);
           $finish;
       end
   endmodule

Two details in that testbench are worth stealing for your own timing work.

The watchdog. A timing testbench waits on phase boundaries, so against a broken generator it can wait forever — and a hang is a far worse failure report than a FAIL line, because it tells you nothing about what went wrong. Bounding the run converts a hang into a diagnosis. Two of §5's mutations are caught by exactly this and by nothing else.

Sampling once per cycle. The tick-width check samples only at the falling edge. Sampling at both edges counts a one-cycle pulse as two — which is a bug I hit while writing this testbench, and the reason the comment is there.

4b. Verilog

Identical hardware. reg/wire typing, localparam phase encoding instead of an enum, $display/$finish instead of $fatal for the parameter guard, and $display reporting in the testbench. $clog2 is a Verilog-2005 system function, so the counter width is derived the same way.

Azvya Education Pvt. Ltd.VLSI Mentor
scl_timing_gen.v — SYNTHESIZABLE RTL. The same phase generator in Verilog.
   module scl_timing_gen #(
       parameter LOW_CYCLES  = 5,
       parameter HIGH_CYCLES = 3
   )(
       input  wire clk,
       input  wire rst_n,
       input  wire enable,
       output reg  scl_drive_low,
       output wire phase_low,
       output wire phase_high,
       output reg  low_tick,
       output reg  high_tick,
       output reg  bit_tick
   );
       // Both phases must be at least one cycle: a zero-length phase is not a
       // slower clock, it is a missing phase.
       initial begin
           if (LOW_CYCLES  < 1) begin $display("FATAL: LOW_CYCLES must be >= 1");  $finish; end
           if (HIGH_CYCLES < 1) begin $display("FATAL: HIGH_CYCLES must be >= 1"); $finish; end
       end

       localparam MAXC = (LOW_CYCLES > HIGH_CYCLES) ? LOW_CYCLES : HIGH_CYCLES;
       localparam CW   = (MAXC <= 1) ? 1 : $clog2(MAXC);   // Verilog-2005 system function

       localparam PH_LOW = 1'b0, PH_HIGH = 1'b1;

       reg            phase;
       reg [CW-1:0]   cnt;
       reg            running;

       always @(posedge clk) begin
           if (!rst_n) begin
               phase <= PH_LOW; cnt <= {CW{1'b0}}; running <= 1'b0;
               scl_drive_low <= 1'b0;                  // RELEASE on reset
               low_tick <= 1'b0; high_tick <= 1'b0; bit_tick <= 1'b0;
           end else begin
               low_tick <= 1'b0; high_tick <= 1'b0; bit_tick <= 1'b0;
               if (!enable) begin
                   phase <= PH_LOW; cnt <= {CW{1'b0}}; running <= 1'b0;
                   scl_drive_low <= 1'b0;              // disabled must RELEASE
               end else if (!running) begin
                   running <= 1'b1; phase <= PH_LOW;
                   cnt <= LOW_CYCLES - 1;
                   scl_drive_low <= 1'b1; low_tick <= 1'b1;
               end else if (cnt != {CW{1'b0}}) begin
                   cnt <= cnt - 1'b1;
               end else if (phase == PH_LOW) begin
                   phase <= PH_HIGH; cnt <= HIGH_CYCLES - 1;
                   scl_drive_low <= 1'b0;              // RELEASE, never drive 1
                   high_tick <= 1'b1;
               end else begin
                   phase <= PH_LOW; cnt <= LOW_CYCLES - 1;
                   scl_drive_low <= 1'b1;
                   low_tick <= 1'b1; bit_tick <= 1'b1;
               end
           end
       end

       assign phase_low  = running && (phase == PH_LOW);
       assign phase_high = running && (phase == PH_HIGH);
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
scl_timing_gen_tb.v — SELF-CHECKING TESTBENCH, SIMULATION ONLY. The same measurements in Verilog idiom.
   module scl_timing_gen_tb;
       parameter LOW_CYCLES = 5, HIGH_CYCLES = 3;      // deliberately asymmetric

       reg  clk, rst_n, enable;
       wire scl_drive_low, phase_low, phase_high, low_tick, high_tick, bit_tick;
       integer errors, lo, hi, ticks, b, k;

       scl_timing_gen #(.LOW_CYCLES(LOW_CYCLES), .HIGH_CYCLES(HIGH_CYCLES)) dut (
           .clk(clk), .rst_n(rst_n), .enable(enable), .scl_drive_low(scl_drive_low),
           .phase_low(phase_low), .phase_high(phase_high), .low_tick(low_tick),
           .high_tick(high_tick), .bit_tick(bit_tick));

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

       // MEASURE the phase, never assume it.
       task measure_bit;
           output integer low_len;
           output integer high_len;
           begin
               low_len = 0; high_len = 0;
               while (scl_drive_low !== 1'b1) @(negedge clk);
               while (scl_drive_low === 1'b1) begin low_len = low_len + 1;  @(negedge clk); end
               while (scl_drive_low === 1'b0) begin high_len = high_len + 1; @(negedge clk); end
           end
       endtask

       initial begin
           errors = 0; rst_n = 0; enable = 0;
           repeat (2) @(negedge clk);
           if (scl_drive_low !== 1'b0) begin $display("FAIL: reset must RELEASE scl"); errors=errors+1; end
           if (phase_low || phase_high) begin $display("FAIL: no phase while idle"); errors=errors+1; end
           rst_n = 1; @(negedge clk);

           repeat (4) @(negedge clk);
           if (scl_drive_low !== 1'b0) begin $display("FAIL: disabled must RELEASE scl"); errors=errors+1; end
           if (low_tick || high_tick || bit_tick) begin $display("FAIL: no ticks while disabled"); errors=errors+1; end

           enable = 1;
           for (b = 0; b < 3; b = b + 1) begin
               measure_bit(lo, hi);
               if (lo !== LOW_CYCLES) begin
                   $display("FAIL: bit %0d LOW was %0d cycles, expected %0d", b, lo, LOW_CYCLES); errors=errors+1; end
               if (hi !== HIGH_CYCLES) begin
                   $display("FAIL: bit %0d HIGH was %0d cycles, expected %0d", b, hi, HIGH_CYCLES); errors=errors+1; end
           end

           // Sample once per cycle or a one-cycle pulse is counted twice.
           @(negedge clk);
           while (bit_tick !== 1'b1) @(negedge clk);
           ticks = 0;
           while (bit_tick === 1'b1) begin ticks = ticks + 1; @(negedge clk); end
           if (ticks !== 1) begin $display("FAIL: bit_tick was %0d cycles wide", ticks); errors=errors+1; end

           for (k = 0; k < 20; k = k + 1) begin
               @(negedge clk);
               if (phase_low && phase_high) begin $display("FAIL: both phases asserted"); errors=errors+1; end
               if (!phase_low && !phase_high) begin $display("FAIL: no phase while running"); errors=errors+1; end
               if (phase_low  && scl_drive_low !== 1'b1) begin $display("FAIL: LOW phase must pull low"); errors=errors+1; end
               if (phase_high && scl_drive_low !== 1'b0) begin $display("FAIL: HIGH phase must RELEASE"); errors=errors+1; end
           end

           @(negedge clk); enable = 0;
           repeat (3) @(negedge clk);
           if (scl_drive_low !== 1'b0) begin $display("FAIL: disable left SCL pulled LOW"); errors=errors+1; end

           if (errors == 0) $display("PASS: LOW=%0d HIGH=%0d measured exactly; release semantics held", LOW_CYCLES, HIGH_CYCLES);
           else             $display("FAIL: %0d error(s)", errors);
           $finish;
       end
   endmodule

4c. VHDL

VHDL contributes the cleanest statement of two things. The phase is an enumerated type, so PH_LOW and PH_HIGH are names rather than encodings and the tool chooses the bits. And the parameter guard is not a check at all — declaring the generics as positive makes a zero-length phase impossible at elaboration, which is stronger than any runtime assertion.

As in Chapter 2.3 and Chapter 3.4, the outputs are driven from internal signals because the control logic reads its own state and an out port is not readable in VHDL-93.

Azvya Education Pvt. Ltd.VLSI Mentor
scl_timing_gen.vhd — SYNTHESIZABLE RTL. Enumerated phase type; positive generics forbid a zero-length phase.
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   entity scl_timing_gen is
       generic (
           LOW_CYCLES  : positive := 5;      -- `positive` makes a zero-length phase
           HIGH_CYCLES : positive := 3       --   impossible at elaboration
       );
       port (
           clk           : in  std_logic;
           rst_n         : in  std_logic;
           enable        : in  std_logic;
           scl_drive_low : out std_logic;
           phase_low     : out std_logic;
           phase_high    : out std_logic;
           low_tick      : out std_logic;
           high_tick     : out std_logic;
           bit_tick      : out std_logic
       );
   end entity;

   architecture rtl of scl_timing_gen is
       type phase_t is (PH_LOW, PH_HIGH);
       signal phase   : phase_t := PH_LOW;
       signal cnt     : natural range 0 to 2**16 := 0;
       signal running : std_logic := '0';
       -- Internal copies: the control decision reads the running/phase state, and an
       -- `out` port is not readable in VHDL-93.
       signal drv     : std_logic := '0';
       signal lt, ht, bt : std_logic := '0';
   begin
       scl_drive_low <= drv;
       low_tick      <= lt;
       high_tick     <= ht;
       bit_tick      <= bt;
       phase_low     <= '1' when running = '1' and phase = PH_LOW  else '0';
       phase_high    <= '1' when running = '1' and phase = PH_HIGH else '0';

       process (clk)
       begin
           if rising_edge(clk) then
               if rst_n = '0' then
                   phase <= PH_LOW; cnt <= 0; running <= '0';
                   drv <= '0';                        -- RELEASE on reset
                   lt <= '0'; ht <= '0'; bt <= '0';
               else
                   lt <= '0'; ht <= '0'; bt <= '0';
                   if enable = '0' then
                       phase <= PH_LOW; cnt <= 0; running <= '0';
                       drv <= '0';                    -- disabled must RELEASE
                   elsif running = '0' then
                       running <= '1'; phase <= PH_LOW;
                       cnt <= LOW_CYCLES - 1;
                       drv <= '1'; lt <= '1';
                   elsif cnt /= 0 then
                       cnt <= cnt - 1;
                   elsif phase = PH_LOW then
                       phase <= PH_HIGH; cnt <= HIGH_CYCLES - 1;
                       drv <= '0';                    -- RELEASE, never drive '1'
                       ht <= '1';
                   else
                       phase <= PH_LOW; cnt <= LOW_CYCLES - 1;
                       drv <= '1';
                       lt <= '1'; bt <= '1';
                   end if;
               end if;
           end if;
       end process;
   end architecture;
Azvya Education Pvt. Ltd.VLSI Mentor
scl_timing_gen_tb.vhd — SELF-CHECKING TESTBENCH, SIMULATION ONLY. Measures phases with assert report severity.
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   entity scl_timing_gen_tb is
   end entity;

   architecture sim of scl_timing_gen_tb is
       constant LOW_CYCLES  : positive := 5;      -- deliberately asymmetric
       constant HIGH_CYCLES : positive := 3;

       signal clk           : std_logic := '0';
       signal rst_n         : std_logic := '0';
       signal enable        : std_logic := '0';
       signal scl_drive_low : std_logic;
       signal phase_low     : std_logic;
       signal phase_high    : std_logic;
       signal low_tick      : std_logic;
       signal high_tick     : std_logic;
       signal bit_tick      : std_logic;
       signal errors        : natural := 0;
   begin
       dut : entity work.scl_timing_gen
           generic map (LOW_CYCLES => LOW_CYCLES, HIGH_CYCLES => HIGH_CYCLES)
           port map (clk => clk, rst_n => rst_n, enable => enable,
                     scl_drive_low => scl_drive_low, phase_low => phase_low,
                     phase_high => phase_high, low_tick => low_tick,
                     high_tick => high_tick, bit_tick => bit_tick);

       clk <= not clk after 5 ns;

       stim : process
           variable lo, hi, ticks : natural;
           variable errs : natural := 0;

           -- MEASURE the phase, never assume it.
           procedure measure_bit (variable low_len : out natural;
                                  variable high_len : out natural) is
               variable l, h : natural := 0;
           begin
               l := 0; h := 0;
               while scl_drive_low /= '1' loop wait until falling_edge(clk); end loop;
               while scl_drive_low = '1' loop l := l + 1; wait until falling_edge(clk); end loop;
               while scl_drive_low = '0' loop h := h + 1; wait until falling_edge(clk); end loop;
               low_len := l; high_len := h;
           end procedure;
       begin
           wait until falling_edge(clk);
           wait until falling_edge(clk);
           if scl_drive_low /= '0' then
               report "reset must RELEASE scl" severity error; errs := errs + 1; end if;
           if phase_low = '1' or phase_high = '1' then
               report "no phase may be claimed while idle" severity error; errs := errs + 1; end if;
           rst_n <= '1';
           wait until falling_edge(clk);

           for i in 0 to 3 loop wait until falling_edge(clk); end loop;
           if scl_drive_low /= '0' then
               report "disabled must RELEASE scl" severity error; errs := errs + 1; end if;
           if low_tick = '1' or high_tick = '1' or bit_tick = '1' then
               report "no ticks while disabled" severity error; errs := errs + 1; end if;

           enable <= '1';
           for b in 0 to 2 loop
               measure_bit(lo, hi);
               if lo /= LOW_CYCLES then
                   report "LOW phase duration is wrong" severity error; errs := errs + 1; end if;
               if hi /= HIGH_CYCLES then
                   report "HIGH phase duration is wrong" severity error; errs := errs + 1; end if;
           end loop;

           -- Sample once per cycle or a one-cycle pulse is counted twice.
           wait until falling_edge(clk);
           while bit_tick /= '1' loop wait until falling_edge(clk); end loop;
           ticks := 0;
           while bit_tick = '1' loop ticks := ticks + 1; wait until falling_edge(clk); end loop;
           if ticks /= 1 then
               report "bit_tick is not exactly one cycle wide" severity error; errs := errs + 1; end if;

           for k in 0 to 19 loop
               wait until falling_edge(clk);
               if phase_low = '1' and phase_high = '1' then
                   report "both phases asserted" severity error; errs := errs + 1; end if;
               if phase_low = '0' and phase_high = '0' then
                   report "no phase asserted while running" severity error; errs := errs + 1; end if;
               if phase_low = '1' and scl_drive_low /= '1' then
                   report "LOW phase must pull SCL low" severity error; errs := errs + 1; end if;
               if phase_high = '1' and scl_drive_low /= '0' then
                   report "HIGH phase must RELEASE SCL" severity error; errs := errs + 1; end if;
           end loop;

           wait until falling_edge(clk);
           enable <= '0';
           for i in 0 to 2 loop wait until falling_edge(clk); end loop;
           if scl_drive_low /= '0' then
               report "disable left SCL pulled LOW -- bus would be stuck" severity error;
               errs := errs + 1; end if;

           errors <= errs;
           if errs = 0 then
               report "scl_timing_gen self-check complete: phases measured exactly" severity note;
           else
               report "scl_timing_gen self-check FAILED" severity error;
           end if;
           wait;
       end process;
   end architecture;

4d. Cross-Language Parity

Ports, generics, reset polarity and synchrony, phase semantics, tick widths, enable behaviour and the test sequence are identical, and all three testbenches complete at the same simulated time against the same asymmetric parameters.

ConcernSystemVerilogVerilogVHDL
Phase representationtypedef enum logiclocalparam encodingenumerated phase_t
Parameter guard$fatal in initial$display + $finishpositive generic — rejected at elaboration
Counter width$clog2$clog2natural range
Reading own stateoutputs read directlyoutputs read directlyinternal signals, ports driven from them

The VHDL row is the instructive one: two of those four differences are VHDL expressing something earlier than the other two languages can. A zero-length phase becomes unrepresentable rather than detected.

scl_timing_gen — one and a bit bit-periods, LOW=5 and HIGH=3

10 cycles
Ten internal clock cycles of the SCL timing generator simulation. Enable is high throughout. The pull-low intent output is asserted for five consecutive cycles, then released for three, then asserted again. The low tick pulses for one cycle at the start of each low phase, the high tick pulses at the start of the high phase, and the bit tick pulses once at the boundary where a bit period completes. An observed SCL row shows the resolved bus level following the release.LOW — 5 cyclesLOW — 5 cyclesHIGH — 3 cyclesHIGH — 3 cyclesLOW phase beginsLOW phase beginsSCL released — HIGH phaseSCL released — HIGH phasebit complete; next LOWbit complete; next LOWclkenablescl_drive_lowscl_observed0000011100low_tickhigh_tickbit_tickt0t1t2t3t4t5t6t7t8t9
Figure 2 \u2014 the generator's own simulation with LOW_CYCLES = 5 and HIGH_CYCLES = 3. The controller pulls SCL low for five cycles and releases it for three, and the ticks mark each boundary. Note that the drive-intent row and the observed-SCL row are different signals \u2014 §7 explains why that distinction is not pedantic.

This is an RTL simulation trace: every row except scl_observed is a port of the module above, and the cycle counts are the module's measured behaviour. scl_observed is drawn as what the bus resolves to when nothing else is pulling it — which is the ideal case, and the next section is about why it is only the ideal case.

5. Mutation Testing — Proving the Testbench Can Fail

A passing testbench proves nothing until you know it can fail. Six deliberate bugs were injected into the generator and the testbench run against each:

MutationWhat breaksResult
HIGH phase drives 1 instead of releasingthe open-drain contract is brokenFAIL — watchdog
Off-by-one on the LOW reloadLOW becomes 4 cycles instead of 5FAIL — 2 errors
Off-by-one on the HIGH reloadHIGH becomes 2 cycles instead of 3FAIL — 3 errors
Reset holds SCL LOW instead of releasinga reset controller jams the whole busFAIL — 1 error
Disable leaves SCL pulled LOWthe bus would be stuck after a stopFAIL — 3 errors
bit_tick suppressed on the phase boundaryno consumer could ever advance a bitFAIL — watchdog

Every mutation was caught. Two are worth dwelling on, and for the same reason.

Driving 1 in the HIGH phase means scl_drive_low never returns to 0, so the measuring loop waits forever for a release that never comes. Suppressing bit_tick does the same thing to a different check. In both cases the failure mode is not a wrong answer — it is no answer at all, and the watchdog is what converts an infinite hang into a reported failure. A testbench without one would simply appear to stop, which is the least useful outcome available: a hang is indistinguishable from a slow simulation, and CI reports it as a timeout rather than as the specific broken invariant.

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

6. From a Time Requirement to a Cycle Count

The parameters above are in internal clock cycles and the specification's requirements are in time. Somebody has to convert, and the direction of rounding is not a matter of taste.

Work an example. It is illustrative — the 5 µs figure is chosen to make the arithmetic clean, not quoted from a mode requirement, and Module 11 owns the real numbers.

Azvya Education Pvt. Ltd.VLSI Mentor
converting a minimum duration into a cycle count
   internal clock       f_clk  = 100 MHz
   internal period      T_clk  = 1 / 100 MHz = 10 ns
   required LOW time    t_LOW >= 5 us  (illustrative)

   cycles needed = 5 us / 10 ns = 500        exactly

   LOW_CYCLES = 500

Now make it awkward, which is the realistic case:

Azvya Education Pvt. Ltd.VLSI Mentor
the same conversion when it does not divide evenly
   required LOW time    t_LOW >= 4.7 us
   cycles needed        = 4.7 us / 10 ns = 470.0        -> 470, fine

   required LOW time    t_LOW >= 4.992 us
   cycles needed        = 4.992 us / 10 ns = 499.2

   floor(499.2) = 499 cycles = 4.990 us   -> SHORT of the minimum. ILLEGAL.
   ceil (499.2) = 500 cycles = 5.000 us   -> satisfies the minimum. LEGAL.

For a minimum-duration requirement, always round up. Rounding down produces a phase that is shorter than the specification allows, by a margin small enough that it will work on a bench and fail somewhere else. The rule generalises:

Azvya Education Pvt. Ltd.VLSI Mentor
the rounding rule, and its opposite
   MINIMUM duration requirement  ->  cycles = ceil (required_time / T_clk)
   MAXIMUM duration requirement  ->  cycles = floor(required_time / T_clk)

A minimum wants at least that long, so overshoot is safe and undershoot is not. A maximum is the reverse. Getting the direction wrong is a real and quiet bug: both choices compile, both look plausible in a register map, and only one is legal.

Two consequences worth carrying:

The cycle count depends on the internal clock. Port the same controller to a design with a different clock and every count changes. That is why a serious controller derives its counts from a configuration — registers, generics, or a computed constant — rather than hard-coding numbers that were correct on one board.

Ceiling rounding costs a little speed. Rounding up lengthens a phase, so the achieved SCL frequency is slightly below the theoretical maximum. That is the correct trade: a bus a fraction slower than it could be still works, and a bus a fraction faster than legal may not.

7. Releasing Is Not Driving High

One more property of the generator deserves stating plainly, because it is the hinge between this chapter and two later modules.

scl_drive_low = 0 means this controller has stopped pulling SCL low. It does not mean SCL is high. Chapter 2.5 established the rule: the line rises only when every participant has released it, and then only after the pull-up has charged the bus capacitance.

Two consequences follow, and neither is developed here:

The observed HIGH phase can be longer than HIGH_CYCLES. A target is permitted to keep holding SCL low after the controller releases it, which stretches the phase. The generator's counter is unaffected — it counted its three cycles — but the bus spent longer than that. A controller that assumes its counter describes the bus will mis-sequence the transfer. Module 12 develops this.

The observed HIGH phase is also shortened by the rising edge. Chapter 2.4 showed that a released line takes real time to cross a receiver's threshold, so part of the interval the controller counted as HIGH is spent climbing rather than being read as HIGH. That is why a specification must bound rise time and bus capacitance at all, and why the achieved timing depends on the board. Module 11.7 owns it.

The design rule for now: the generator produces intent, and the bus produces the level. A controller that wants to know what actually happened has to read SCL back — which is precisely the scl_in port Chapter 2.1 argued for.

8. FPGA and ASIC Implications

Both inherit the boundary from Module 2; neither changes it.

On an FPGA, scl_drive_low drives the output-enable of an I/O buffer at the top level, and the pin is inout with the pull-up on the board. Three practical points: the internal clock frequency sets your timing granularity — a 100 MHz clock cannot express a duration to better than 10 ns, which is usually ample here but is the reason the conversion in §6 exists; the counter width follows from the largest count, so a slow SCL from a fast clock needs more bits than people expect; and scl_in is an asynchronous input that must be synchronised before logic uses it, which itself adds delay to the observed timing and therefore has to be designed rather than bolted on. Module 19 owns the implementation detail.

On an ASIC, the generator becomes a programmable peripheral: the phase counts live in timing registers loaded by software, often behind a prescaler, so one design supports several speed modes. The reference is a peripheral clock unrelated to the bus, which is exactly the conversion in §6 done at run time rather than at elaboration. Two further realities: pad delay sits between the register and the pin and eats a little of every margin; and process, voltage and temperature move the real delays, so a design that just satisfies a minimum at nominal conditions may not at the corners. That is an argument for margin in the count, not for a tighter simulation.

9. Debugging — Two Timing Bugs That Look Like Nothing

The clock that measured 400 kHz and was still illegal

Pitfall — an off-by-one phase counter, and a duty cycle nobody checked
Buggy Code
// A controller is written with a single divider parameter, the way section 3
// rejected, plus an off-by-one in the reload:
 if (cnt == 0) begin
     cnt <= DIVIDER;          // <-- reload with DIVIDER, not DIVIDER-1
     scl <= ~scl;             // <-- and a 50/50 toggle
 end else
     cnt <= cnt - 1;
//
// Bench check: measure SCL with a scope, read 398 kHz for a 400 kHz target,
// declare it correct within tolerance, move on.
//
// Two separate faults are hiding behind that one measurement. The reload makes
// each phase one cycle longer than intended, which is why the frequency came out
// slightly low. And the toggle makes LOW and HIGH equal, which the specification
// never asked for -- it constrains them SEPARATELY.
Symptom

The bus works against most devices, most of the time. Frequency measures correct. A capture decodes into sensible bytes. Then one particular target -- often the slowest part on the bus, or a part from a different vendor -- starts NACKing intermittently, and more often when the board is warm or when another device has been added. The investigation goes to that target: its datasheet, its address, its supply. Nothing is wrong with it. Swapping it for a different part number "fixes" the problem, which cements the wrong conclusion that the original part was marginal. Nothing in the digital RTL simulation ever failed, because the RTL produced exactly the waveform it was written to produce.

Root Cause

Two independent bugs that a frequency measurement cannot separate. The OFF-BY-ONE lengthened both phases by one internal cycle. On its own that is harmless -- slightly slow is legal -- and it is the reason the frequency reading looked "close enough", which removed the incentive to look further. The DUTY CYCLE is the real fault. Because the divider toggles, LOW and HIGH are equal. The specification bounds t_LOW and t_HIGH independently and they are NOT equal requirements, so a 50/50 waveform at a correct average frequency can satisfy one and violate the other. Add the board's rise time, which eats into the observed HIGH period (Chapter 2.4), and the HIGH phase a slow receiver actually sees is shorter still. The target that NACKs is the one whose requirement is tightest -- which is why the fault appears to belong to that device and does not. The measurement that hid it: asking "is the frequency right?" instead of "is each PHASE long enough?" Average frequency is a property of the sum of the phases and says nothing about either one.

Fix
// Count the phases separately, and derive each count from its own requirement:
 localparam LOW_CYCLES  = ceil_div(T_LOW_MIN_NS,  CLK_PERIOD_NS);
 localparam HIGH_CYCLES = ceil_div(T_HIGH_MIN_NS, CLK_PERIOD_NS);
// with ceiling rounding for minimums (section 6), never floor.
//
// Then verify by MEASURING each phase, not the frequency. The testbench in this
// chapter does exactly that, and it uses ASYMMETRIC parameters (5 and 3) so a
// 50/50 implementation cannot pass. An implementation with the off-by-one fails
// the same check, because measured LOW would be 6 where 5 was required.
//
// On hardware, the equivalent measurement is a scope with cursors on ONE phase --
// not a frequency counter. And measure the HIGH phase at the RECEIVER's threshold,
// because the part of the interval spent climbing is not HIGH as far as the
// receiver is concerned.
//
// The general lesson: a correct average is not a correct waveform. Any timing
// requirement stated per-phase must be verified per-phase.

The engineering lesson: frequency is an average, and averages hide asymmetry. When a bus works against most devices and fails against one, the suspicion should fall on a per-phase requirement rather than on the device that noticed — because the device that fails first is simply the one with the least margin, not the one that is broken. The diagnostic habit is to measure the phase the specification constrains, at the threshold the receiver actually uses.

10. Common Misconceptions

11. Reason It Through

Work these through before reading the answers.

Your measured SCL frequency is exactly at the target, but t_HIGH measures shorter than its minimum. Is the bus legal?

No. Frequency is the sum of the two phases, so a short HIGH can be compensated by a long LOW and still produce the right average. The specification constrains each phase independently, precisely because the receiver's requirement is about the interval it gets to observe, not about the repetition rate. This is the §9 debug case in one sentence.

A timing counter needs 499.2 cycles to satisfy a minimum duration. Should the RTL use 499 or 500?

500. ceil for a minimum. 499 cycles is shorter than the requirement, and by a margin small enough to pass on a bench and fail elsewhere. The instinct to "round to nearest" is wrong here because the requirement is one-sided — there is no credit for being close.

The controller releases SCL and its counter says the HIGH phase has ended, but the bus is still low. What are the possibilities?

Two, and they need different responses. Either the rising edge has not completed — the pull-up is still charging the bus capacitance, which is a loading question from Chapter 2.4 — or another participant is still holding SCL down, which is the clock-stretching case Module 12 develops. They are distinguished by shape: a line still climbing has a curve, a line being held is flat. Note what both have in common: the controller's counter was right and the bus disagreed, which is why the counter is not the authority on what happened.

A design works at 100 kHz and fails when configured for 400 kHz, with no RTL change other than the counts. Where would you look first?

At what shortened. Four candidates, in rough order. The phase counts may have been recomputed with floor rounding, so a minimum is now violated where the slower setting had slack. The rise time did not change when the bit period did, so it now consumes a much larger fraction of the HIGH phase — the same board is electrically marginal at the higher rate. A target on the bus may not support the faster mode at all. And any fixed internal delay, such as a synchroniser on scl_in, is now a bigger fraction of the bit. Notice that only the first is an RTL bug; the rest are the conversion between the three time domains in §2.

12. Understanding Check

13. Summary

Timing constraints exist because a receiver that looks at SDA while it is changing gets an undefined value, not a wrong one. Removing that possibility requires a rule about time that both sides implement independently: SCL says when to look, and SDA must be stable around that instant.

Three questions fail independently — semantic (is the bit the intended value), transition (was SDA allowed to change then), duration (was each level held long enough) — and a capture can decode perfectly while violating the last two.

Three time domains must be kept apart: RTL cycles, bus time, and electrical transition time. Signals do not teleport; a real edge takes time, and on this bus the two directions are not symmetric.

An integer divider is the wrong model. It drives SCL high, which the architecture forbids; it makes the phases equal, which the specification never asked for; it has nowhere to attach the events a transfer needs; and it assumes releasing makes the line high. The right model is a phase machine with independently parameterised LOW and HIGH durations and tick outputs at the boundaries — built here in three languages, measured rather than assumed, and mutation-tested.

Convert time to cycles with ceiling rounding for minimums and floor for maximums. Rounding a minimum down produces a violation small enough to pass on a bench.

And releasing is not driving high. The generator produces intent; the bus produces the level, later and possibly not at all if somebody else is holding it.

14. What Comes Next

SCL now has a legal shape, which answers when the receiver looks. The other half of the rule is what SDA must be doing around that instant — and it turns out to be a single sentence with remarkable consequences, including the one that makes framing possible at all.

Chapter 4.2 establishes the data-valid rule, builds a checker that detects violations of it in three languages, and shows why an SDA edge during SCL HIGH is not merely illegal but reserved.

Browse the full path on the I²C tutorials index. For the electrical facts this chapter depends on, see Open-Drain Outputs and Pull-Up Resistors.

Continue learning