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:
| Domain | Unit | Who controls it |
|---|---|---|
| RTL cycle timing | integer internal clock cycles | the design's counters |
| Bus timing | real time on SDA and SCL | the design plus its clock frequency |
| Electrical transition timing | rise and fall shape | the 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:
// 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
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_CYCLESinternal clock cycles withscl_drive_lowasserted. - The HIGH phase lasts exactly
HIGH_CYCLEScycles withscl_drive_lowreleased. low_tickandhigh_tickare one-cycle pulses at the start of their phases;bit_tickis 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
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);
endmoduleThe 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.
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
endmoduleTwo 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.
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 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
endmodule4c. 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.
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; 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.
| Concern | SystemVerilog | Verilog | VHDL |
|---|---|---|---|
| Phase representation | typedef enum logic | localparam encoding | enumerated phase_t |
| Parameter guard | $fatal in initial | $display + $finish | positive generic — rejected at elaboration |
| Counter width | $clog2 | $clog2 | natural range |
| Reading own state | outputs read directly | outputs read directly | internal 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 cyclesThis 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:
| Mutation | What breaks | Result |
|---|---|---|
HIGH phase drives 1 instead of releasing | the open-drain contract is broken | FAIL — watchdog |
| Off-by-one on the LOW reload | LOW becomes 4 cycles instead of 5 | FAIL — 2 errors |
| Off-by-one on the HIGH reload | HIGH becomes 2 cycles instead of 3 | FAIL — 3 errors |
| Reset holds SCL LOW instead of releasing | a reset controller jams the whole bus | FAIL — 1 error |
| Disable leaves SCL pulled LOW | the bus would be stuck after a stop | FAIL — 3 errors |
bit_tick suppressed on the phase boundary | no consumer could ever advance a bit | FAIL — 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.
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 = 500Now make it awkward, which is the realistic case:
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:
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
// 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.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.
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.
// 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_HIGHmeasures 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
Related tutorials
- Related topic
Why Two Independent Clocks Can Still Communicate
Two free-running oscillators, never measured against each other, with no feedback path — and reliable communication anyway. The bargain has four terms, and the load-bearing one is that a frame ends, which discards the receiver's prediction before accumulated error becomes dangerous.
- Related topic
SCLK Generation, Period, and Frequency
Where SCLK comes from and what one period buys. Dividing a system clock to a bus clock, why the divisor is an integer and what that costs, and how a period in nanoseconds becomes the budget every later timing parameter is spent from.
- Related topic
Input and Output Delay Constraints
A constraint cannot be simulated — set_input_delay describes a board. So the RTL is one register per pin and no logic, the bench checks only what the constraints assume, and the file is written three times for three tools. Adding that pad register also moves three numbers Module 14 published.
- Related topic
Standard Baud Rates and System-Clock Relationships
The familiar rates are what specific oscillator frequencies divide onto exactly, which is why they are powers of two rather than round decimals. A modern fabric clock does not divide onto them, and the prime factorisation shows why it cannot.
