I²C · Module 13
I²C SCL Synchronization — How Masters Agree on One Clock
Two sentences of the specification give a complete combining rule: the low phase is the longest and the high phase the shortest. The synchronized clock therefore belongs to neither master.
Chapter 13.1 left two masters on one wire, both believing they own a START that the wired-AND merged into a single edge. Neither knows the other is there.
Before either can discover it, something has to happen that neither of them arranges: they have to agree on a clock. Both are driving SCL, and SCL is one wire.
There is no negotiation, no handshake and no logic that computes the agreement. The wired-AND does it, and the answer is available in two sentences of the specification.
1. The Rule, in Two Sentences
Chapter 12.2 quoted §3.1.7 for a purpose it was not written for. This is the purpose it was written for.
Those two marked sentences are the whole rule:
observed LOW = the LONGEST of the participating masters' low periods — slowest-low wins
observed HIGH = the SHORTEST of their high periods — fastest-high wins
Two extrema, in opposite directions, and each one falls directly out of the asymmetry Chapter 12.2 §2 established.
The low phase takes a maximum because ending it requires everyone to release, and a release is a request that the wired-AND may decline. One master still holding is enough to keep the line down, so the last one to let go decides.
The high phase takes a minimum because ending it requires only one master to pull, and a pull-down is a command no one can override. The first master to finish decides.
Same mechanism, opposite direction, because the two phases are ended by opposite kinds of action.
2. The Consequence: the Clock Belongs to Neither Master
Put numbers on it. Master A runs 150 ticks low and 120 high; master B runs 80 low and 50 high.
| low | high | period | |
|---|---|---|---|
| master A wanted | 150 | 120 | 270 |
| master B wanted | 80 | 50 | 130 |
| the bus gets | 150 (A's) | 50 (B's) | 200 |
Neither master is running its own clock. A is being clocked faster in its high phase than it configured; B is being clocked far slower in its low phase. And the resulting period, 200, is neither 270 nor 130.
Three things follow, and they are the practical substance of the chapter.
Both masters must tolerate a clock they did not configure. Which is exactly what Chapter 12.2's generator already does: it counts its low phase from its own intent and its high phase from the observed rise. A generator built that way participates in synchronization for free and does not need to know the concept exists.
The combined clock is still compliant, and the check is one-sided. The low phase became longer — always compliant, because tLOW has a minimum and no maximum (Chapter 11.2 §1). The high phase became shorter, which is the direction that can violate tHIGH(min) — so the fastest master's high phase is the one that has to be legal, and a master with a marginal tHIGH drags the whole bus into non-compliance. That is the one real compliance hazard in this chapter and §10 returns to it.
And the frequency is lower than the faster master's. A master configured for 400 kHz sharing with one configured for 100 kHz gets something slower than 400 kHz — the bus does not run at the faster rate, nor at the average, but at a rate determined term-by-term from the two extrema.
3. The Wait State, Drawn
The last to release ends the low phase; the first to finish ends the high phase
10 cyclesRead master B's row against the line. B released at interval 2 and the line did not move, because A was still holding it — and B's own high-phase count does not start there. It starts at interval 4, when the line actually rises. That is the single design decision Chapter 12.2 §3 is built around, and here it is what makes B's clock bend to the bus rather than fight it.
The B status row is a bus lane because it carries text, and the three signal rows use kind: "signal" rather than kind: "clock" because the specific levels of specific intervals are the claim — a clock lane renders a generic square wave and would destroy it (Chapter 11.1 §3).
4. Convergence — What the Word Actually Names
§3.1.7's rule assumes something it states only in passing: "a HIGH to LOW transition on the SCL line causes the masters concerned to start counting off their LOW period". Every master counts from the same event.
A master that joins part-way through a period has not had that event. Its counters are offset, so the first combined period is irregular — and one period later every master is counting from the same falling edge and the rule holds exactly.
That transient is what "synchronization" names. Not the steady state, which is just arithmetic — the process of two independent clocks becoming one.
§7's test 4 asserts it, and its design is the point: it requires the first combined period to differ from the settled one. A suite in which every period is identical has not exercised synchronization at all, however many masters it instantiated.
5. One Line of Code That Differs Between Single- and Multi-Master
Chapter 12.2 §5 established that a single-master generator seeing the line go low during its own high phase should abandon the period: nothing legitimate does that, and logging it would report a tHIGH violation the generator did not cause.
On a multi-master bus that is exactly backwards. §3.1.7: "the first master to complete its HIGH period pulls the SCL line LOW again" — and that master may not be this one. A low line during my high phase means the phase ended, and I should join the low phase and start counting.
| the line goes low during my high phase | single-master | multi-master |
|---|---|---|
| what it means | something is wrong | another master finished first |
| correct response | abandon the period | join the low phase |
| getting it wrong costs | phantom tHIGH violations | every period dropped |
Same observation, opposite response, decided by the configuration rather than by the logic. A design that gets it backwards either invents violations on a one-master bus or reports almost no periods on a two-master one — and mutation O2 in §8 is the second of those.
That is the only line in the generator that changes, which is worth saying plainly: multi-master clock synchronization costs one state and one branch. Everything else was already required.
6. Two Masters and the Wired-AND, in One Module
The design contains both masters and the connection between them, because the combining rule lives in the connection rather than in either participant. Splitting them into two instances with the AND outside would hide the subject.
// SCL SYNCHRONIZATION: HOW TWO MASTERS AGREE ON ONE CLOCK. Chapter 12.2 used UM10204 section 3.1.7
// for the purpose it was NOT written for -- clock stretching. This is what it was written for:
//
// "This means that a HIGH to LOW transition on the SCL line causes the masters concerned to start
// counting off their LOW period and, once a master clock has gone LOW, it holds the SCL line in
// that state until the clock HIGH state is reached. However, if another clock is still within its
// LOW period, the LOW to HIGH transition of this clock may not change the state of the SCL line.
// THE SCL LINE IS THEREFORE HELD LOW BY THE MASTER WITH THE LONGEST LOW PERIOD. Masters with
// shorter LOW periods enter a HIGH wait-state during this time. When all masters concerned have
// counted off their LOW period, the clock line is released and goes HIGH. ... all the masters
// start counting their HIGH periods. THE FIRST MASTER TO COMPLETE ITS HIGH PERIOD PULLS THE SCL
// LINE LOW AGAIN."
//
// Two sentences, two extrema, and together they are the whole combining rule:
//
// observed LOW = MAX over participating masters of their low periods (slowest-low wins)
// observed HIGH = MIN over participating masters of their high periods (fastest-high wins)
//
// The synchronized clock therefore belongs to NEITHER master. It is slower in its low phase than
// the faster master wanted and faster in its high phase than the slower master wanted, so both
// masters are running a clock neither of them configured. That is not a compromise the protocol
// negotiates -- it is what a wired-AND does to two square waves, and there is no logic anywhere
// that computes it.
//
// This block contains BOTH masters and the wired-AND between them, because the rule lives in the
// connection rather than in either participant. Each master is the synchronizing generator of
// Chapter 12.2 -- it counts its low phase from its own intent and its high phase from the observed
// rise -- with ONE difference, and the difference matters:
//
// A SINGLE-master generator that sees the line go low during its own high phase should ABANDON
// the period: nothing legitimate does that, and logging it would report a tHIGH violation it did
// not cause. (Chapter 12.2 section 5.)
//
// A MULTI-master generator must treat it as the high phase ENDING, because section 3.1.7 says
// the fastest master ends it and that master may not be this one.
//
// Same observation, opposite response, decided by the configuration rather than by the logic. A
// design that gets this backwards either drops every period on a two-master bus or reports
// phantom violations on a one-master bus, and mutation B5 is the first of those.
module i2c_scl_sync_pair #(
parameter int TICK_W = 20,
parameter int T_LOW_A = 130, // master A's intended phases
parameter int T_HIGH_A = 60,
parameter int T_LOW_B = 90, // master B is faster low and slower high, so each master
parameter int T_HIGH_B = 100 // wins one extremum -- see the testbench
)(
input logic clk,
input logic rst_n,
input logic enable_a,
input logic enable_b,
// ---- the wired-AND line, which is the synchronized clock ----
output logic scl_line,
output logic a_drive_low,
output logic b_drive_low,
output logic a_waiting, // released, in section 3.1.7's "HIGH wait-state"
output logic b_waiting,
// ---- the observed phases of the combined clock ----
output logic phase_valid,
output logic [TICK_W-1:0] t_low_obs,
output logic [TICK_W-1:0] t_high_obs,
output logic [TICK_W-1:0] n_periods,
// ---- extrema of the combined clock ----
output logic [TICK_W-1:0] min_low_seen,
output logic [TICK_W-1:0] min_high_seen,
output logic [TICK_W-1:0] n_a_waits,
output logic [TICK_W-1:0] n_b_waits
);
localparam logic [TICK_W-1:0] TLA = T_LOW_A;
localparam logic [TICK_W-1:0] THA = T_HIGH_A;
localparam logic [TICK_W-1:0] TLB = T_LOW_B;
localparam logic [TICK_W-1:0] THB = T_HIGH_B;
localparam logic [1:0] S_IDLE = 2'd0, S_LOW = 2'd1, S_WAIT = 2'd2, S_HIGH = 2'd3;
// ---- the wired-AND. A pull-down is a command; a release is a request. ----
assign scl_line = !a_drive_low && !b_drive_low;
logic scl_q;
wire scl_rise = scl_line && !scl_q;
wire scl_fall = !scl_line && scl_q;
logic [1:0] st_a, st_b;
logic [TICK_W-1:0] cnt_a, cnt_b;
wire [TICK_W-1:0] cnt_a_now = cnt_a + 1'b1;
wire [TICK_W-1:0] cnt_b_now = cnt_b + 1'b1;
// ---- measurement of the COMBINED clock, taken from the line ----
logic [TICK_W-1:0] meas, low_meas;
wire [TICK_W-1:0] meas_now = meas + 1'b1;
always_ff @(posedge clk) begin
if (!rst_n) begin
scl_q <= 1'b1;
a_drive_low <= 1'b0;
b_drive_low <= 1'b0;
a_waiting <= 1'b0;
b_waiting <= 1'b0;
st_a <= S_IDLE;
st_b <= S_IDLE;
cnt_a <= '0;
cnt_b <= '0;
meas <= '0;
low_meas <= '0;
phase_valid <= 1'b0;
t_low_obs <= '0;
t_high_obs <= '0;
n_periods <= '0;
n_a_waits <= '0;
n_b_waits <= '0;
min_low_seen <= {TICK_W{1'b1}};
min_high_seen <= {TICK_W{1'b1}};
end else begin
scl_q <= scl_line;
phase_valid <= 1'b0;
// ================= master A =================
case (st_a)
S_IDLE: begin
a_drive_low <= 1'b0;
a_waiting <= 1'b0;
if (enable_a) begin a_drive_low <= 1'b1; cnt_a <= '0; st_a <= S_LOW; end
end
S_LOW: begin
// Counting off MY low period. Section 3.1.7 starts this at the line's fall, and
// since I am pulling it down the two coincide.
if (cnt_a_now >= TLA) begin
a_drive_low <= 1'b0; // release: a REQUEST
cnt_a <= '0;
st_a <= S_WAIT;
end else cnt_a <= cnt_a_now;
end
S_WAIT: begin
// Section 3.1.7's "HIGH wait-state": I have let go and the line is still low,
// so the other master has the longer low period.
if (scl_line) begin
a_waiting <= 1'b0;
cnt_a <= '0;
st_a <= S_HIGH;
end else if (!a_waiting) begin
a_waiting <= 1'b1;
n_a_waits <= n_a_waits + 1'b1;
end
end
S_HIGH: begin
// Counting off MY high period, started by the observed rise (Chapter 12.2).
if (cnt_a_now >= THA) begin
// I am the first to finish: section 3.1.7 says I pull the line low again.
if (enable_a) begin a_drive_low <= 1'b1; cnt_a <= '0; st_a <= S_LOW; end
else st_a <= S_IDLE;
end else if (!scl_line) begin
// The OTHER master finished its high period first and ended the phase. In a
// MULTI-master design this is normal and my high phase is over -- I join the
// low phase rather than abandoning the period.
a_drive_low <= 1'b1;
cnt_a <= '0;
st_a <= S_LOW;
end else cnt_a <= cnt_a_now;
end
default: st_a <= S_IDLE;
endcase
// ================= master B: identical logic, its own parameters =================
case (st_b)
S_IDLE: begin
b_drive_low <= 1'b0;
b_waiting <= 1'b0;
if (enable_b) begin b_drive_low <= 1'b1; cnt_b <= '0; st_b <= S_LOW; end
end
S_LOW: begin
if (cnt_b_now >= TLB) begin
b_drive_low <= 1'b0;
cnt_b <= '0;
st_b <= S_WAIT;
end else cnt_b <= cnt_b_now;
end
S_WAIT: begin
if (scl_line) begin
b_waiting <= 1'b0;
cnt_b <= '0;
st_b <= S_HIGH;
end else if (!b_waiting) begin
b_waiting <= 1'b1;
n_b_waits <= n_b_waits + 1'b1;
end
end
S_HIGH: begin
if (cnt_b_now >= THB) begin
if (enable_b) begin b_drive_low <= 1'b1; cnt_b <= '0; st_b <= S_LOW; end
else st_b <= S_IDLE;
end else if (!scl_line) begin
b_drive_low <= 1'b1;
cnt_b <= '0;
st_b <= S_LOW;
end else cnt_b <= cnt_b_now;
end
default: st_b <= S_IDLE;
endcase
// ================= measure the COMBINED clock, from the LINE =================
// Not from either master's intent: the synchronized clock is a property of the wire.
if (scl_rise) begin
low_meas <= meas_now; // the low phase just ended
meas <= '0;
end else if (scl_fall) begin
t_low_obs <= low_meas;
t_high_obs <= meas_now; // the high phase just ended
phase_valid <= 1'b1;
n_periods <= n_periods + 1'b1;
if (low_meas < min_low_seen) min_low_seen <= low_meas;
if (meas_now < min_high_seen) min_high_seen <= meas_now;
meas <= '0;
end else begin
meas <= meas_now;
end
end
end
endmodule `timescale 1ns/1ps
// 100 MHz sample clock.
//
// TWO THINGS THE EXPECTATIONS HAVE TO ACCOUNT FOR, both of them real and both explained in the
// chapter rather than tolerated here:
//
// 1. CONVERGENCE. Section 3.1.7's rule assumes every master starts counting its low period at the
// SAME event -- "a HIGH to LOW transition on the SCL line causes the masters concerned to start
// counting off their LOW period". A master enabled part-way through a period has not yet had
// that event, so the FIRST combined period is irregular. Convergence takes exactly one period,
// and demonstrating it is what "synchronization" means. Every steady-state check below therefore
// discards the first combined period, and test 4 asserts that it really was different.
//
// 2. OBSERVATION LATENCY. A master learns that the line moved by OBSERVING it, one cycle after the
// fact, while it drives the line from a register one cycle before the fact. Those offsets CANCEL
// for a master that initiated the edge and ADD for one that did not -- the release-is-a-request
// asymmetry of Chapter 12.2 section 2, appearing for the third time.
//
// Section 3.1.7 says the high phase is ended by the master with the SHORTEST high period, so:
//
// observed high = min(high periods) + 1 -- always: its count starts at an observed
// rise and ends at a registered drive
// observed low = max(low periods) + 0 -- IF the same master also has the shortest
// high, because then it initiated the fall
// max(low periods) + 1 -- IF the extrema belong to DIFFERENT masters
//
// So the latency shows up precisely in the interesting case -- the one where the combined clock
// belongs to neither master. Instances 1 and 2 have one master winning both extrema; instance 3
// splits them, and only instance 3 sees the +1 on its low phase.
//
// The two masters are configured so that EACH WINS ONE EXTREMUM, which is what makes the combining
// rule observable:
//
// master A : low 130, high 60
// master B : low 90, high 100
//
// expected combined low = max(130, 90) = 130 -- A's, the slowest low
// expected combined high = min( 60,100) = 60 -- A's, the fastest high
//
// A wins both here, so a second configuration below swaps them: B low 200, high 40 gives a
// combined 200/40 with B winning both. And a third gives one each. A suite where one master wins
// both extrema every time cannot distinguish max/min from "just use master A".
module i2c_scl_sync_pair_tb;
localparam int TICK_W = 20;
// Independently computed expectations -- NOT read from the DUT.
function automatic int imax(input int x, input int y); return (x > y) ? x : y; endfunction
function automatic int imin(input int x, input int y); return (x < y) ? x : y; endfunction
// Steady-state observed phases, including the observation latency of note 2. The low phase's
// latency depends on WHETHER THE SAME MASTER WINS BOTH EXTREMA, because only then is the
// longest-low master the one that initiated the falling edge.
function automatic int exp_high(input int ha, input int hb); return imin(ha,hb) + 1; endfunction
function automatic int exp_low(input int la, input int lb, input int ha, input int hb);
bit max_low_is_a, min_high_is_a;
begin
max_low_is_a = (la > lb);
min_high_is_a = (ha < hb);
return imax(la,lb) + ((max_low_is_a == min_high_is_a) ? 0 : 1);
end
endfunction
logic clk = 1'b0, rst_n = 1'b0;
logic en_a = 1'b0, en_b = 1'b0;
always #5 clk = ~clk;
// ---------------- instance 1: A low 130 high 60, B low 90 high 100 ----------------
localparam int LA1 = 130, HA1 = 60, LB1 = 90, HB1 = 100;
logic s1_line, s1_ad, s1_bd, s1_aw, s1_bw, s1_pv;
logic [TICK_W-1:0] s1_low, s1_high, s1_np, s1_minl, s1_minh, s1_naw, s1_nbw;
i2c_scl_sync_pair #(.TICK_W(TICK_W), .T_LOW_A(LA1), .T_HIGH_A(HA1),
.T_LOW_B(LB1), .T_HIGH_B(HB1)) u1 (
.clk(clk), .rst_n(rst_n), .enable_a(en_a), .enable_b(en_b),
.scl_line(s1_line), .a_drive_low(s1_ad), .b_drive_low(s1_bd),
.a_waiting(s1_aw), .b_waiting(s1_bw),
.phase_valid(s1_pv), .t_low_obs(s1_low), .t_high_obs(s1_high), .n_periods(s1_np),
.min_low_seen(s1_minl), .min_high_seen(s1_minh),
.n_a_waits(s1_naw), .n_b_waits(s1_nbw));
// ---------------- instance 2: B wins BOTH extrema ----------------
localparam int LA2 = 100, HA2 = 90, LB2 = 200, HB2 = 40;
logic s2_line, s2_ad, s2_bd, s2_aw, s2_bw, s2_pv;
logic [TICK_W-1:0] s2_low, s2_high, s2_np, s2_minl, s2_minh, s2_naw, s2_nbw;
i2c_scl_sync_pair #(.TICK_W(TICK_W), .T_LOW_A(LA2), .T_HIGH_A(HA2),
.T_LOW_B(LB2), .T_HIGH_B(HB2)) u2 (
.clk(clk), .rst_n(rst_n), .enable_a(en_a), .enable_b(en_b),
.scl_line(s2_line), .a_drive_low(s2_ad), .b_drive_low(s2_bd),
.a_waiting(s2_aw), .b_waiting(s2_bw),
.phase_valid(s2_pv), .t_low_obs(s2_low), .t_high_obs(s2_high), .n_periods(s2_np),
.min_low_seen(s2_minl), .min_high_seen(s2_minh),
.n_a_waits(s2_naw), .n_b_waits(s2_nbw));
// ---------------- instance 3: ONE extremum each ----------------
localparam int LA3 = 150, HA3 = 120, LB3 = 80, HB3 = 50;
logic s3_line, s3_ad, s3_bd, s3_aw, s3_bw, s3_pv;
logic [TICK_W-1:0] s3_low, s3_high, s3_np, s3_minl, s3_minh, s3_naw, s3_nbw;
i2c_scl_sync_pair #(.TICK_W(TICK_W), .T_LOW_A(LA3), .T_HIGH_A(HA3),
.T_LOW_B(LB3), .T_HIGH_B(HB3)) u3 (
.clk(clk), .rst_n(rst_n), .enable_a(en_a), .enable_b(en_b),
.scl_line(s3_line), .a_drive_low(s3_ad), .b_drive_low(s3_bd),
.a_waiting(s3_aw), .b_waiting(s3_bw),
.phase_valid(s3_pv), .t_low_obs(s3_low), .t_high_obs(s3_high), .n_periods(s3_np),
.min_low_seen(s3_minl), .min_high_seen(s3_minh),
.n_a_waits(s3_naw), .n_b_waits(s3_nbw));
int errors = 0;
int first_low = 0, first_high = 0;
// Instance 3's period log: convergence is asserted there because that is the instance whose
// extrema belong to different masters, and therefore the one with something to converge to.
int n3 = 0, p3_low [0:7], p3_high[0:7];
// The log clears on reset, so a period logged before the mid-test reset cannot be mistaken for
// the first combined one. Index arithmetic across a reset is exactly the kind of stale-state
// trap Chapter 11.6 section 5 records for tBUF.
always @(posedge clk)
if (!rst_n) n3 = 0;
else if (s3_pv && n3 < 8) begin
p3_low[n3] = s3_low; p3_high[n3] = s3_high; n3 = n3 + 1;
end
int n3_at_both = 0;
task automatic tick(input int n); begin repeat (n) @(negedge clk); end endtask
// Wait for n reported periods on instance 1, then settle so a read is safe.
task automatic wait_p1(input int n);
int seen;
begin seen = 0;
while (seen < n) begin @(posedge clk); if (s1_pv) seen++; end
@(negedge clk);
end
endtask
initial begin
tick(4); rst_n = 1'b1; tick(4);
// ---- 1. reset: MINIMUM trackers at their maximum ------------------------------------
if (s1_minl !== {TICK_W{1'b1}} || s1_minh !== {TICK_W{1'b1}}) begin
$display("FAIL: minimum trackers did not start at their maximum"); errors++; end
if (s1_np !== '0 || s1_naw !== '0 || s1_nbw !== '0) begin
$display("FAIL: counters nonzero out of reset"); errors++; end
if (s1_line !== 1'b1) begin
$display("FAIL: SCL not released out of reset"); errors++; end
// ---- 2. master A alone: the line is exactly A's clock ---------------------------------
// A single master is the degenerate case of the rule -- max and min over one element.
// With one master there is no observation latency on the LOW phase: A initiates its own
// falling edge, so its count and the line agree. The HIGH phase still carries +1, because
// its count begins at an edge A observes and ends at a drive A registers.
en_a = 1'b1;
wait_p1(4);
if (s1_low != LA1) begin
$display("FAIL: master A alone gave low=%0d, expected its own %0d (it initiates its own fall, so no observation latency)",
s1_low, LA1); errors++; end
if (s1_high != HA1 + 1) begin
$display("FAIL: master A alone gave high=%0d, expected %0d (+1 observation latency)",
s1_high, HA1 + 1); errors++; end
if (s1_nbw !== '0) begin
$display("FAIL: master B reported a wait state while disabled"); errors++; end
en_a = 1'b0;
tick(400);
// ---- 3. both masters: slowest-low, fastest-high ---------------------------------------
// THE test of the chapter. The expectation is computed here from the parameters, not read
// from the DUT, so what is under test is the combining rule rather than a shared constant.
rst_n = 1'b0; tick(4); rst_n = 1'b1; tick(4);
n3_at_both = n3; // mark where instance 3's combined periods begin
en_a = 1'b1; en_b = 1'b1;
wait_p1(2); // discard the pre-convergence period
first_low = s1_low;
first_high = s1_high;
wait_p1(4); // now in steady state
if (s1_low != exp_low(LA1, LB1, HA1, HB1)) begin
$display("FAIL: combined LOW = %0d, expected %0d -- the line is held low by the master with the LONGEST low period",
s1_low, exp_low(LA1,LB1,HA1,HB1)); errors++; end
if (s1_high != exp_high(HA1, HB1)) begin
$display("FAIL: combined HIGH = %0d, expected min(%0d,%0d)+1 = %0d -- the FIRST master to finish its high period pulls the line low",
s1_high, HA1, HB1, exp_high(HA1,HB1)); errors++; end
// ---- 4. CONVERGENCE is observable: the first combined period differs from the rest ------
// This is what "synchronization" names. A master joining part-way through has not yet seen
// the falling edge that starts everyone's low count, so its first period is irregular. One
// period later every master is counting from the same event and the rule holds exactly.
// Instance 3's first combined period must differ from its second: a master that joined
// part-way through has not yet seen the falling edge everyone else counted from.
// Index-independent: reset leaves a partial period in the log, so rather than counting
// entries, find the first index where two consecutive periods agree. That is where the pair
// has converged, and the value there must be the combining rule's answer.
begin
int k, settled_at;
settled_at = -1;
for (k = 0; k + 1 < n3; k++)
if (settled_at < 0 && p3_low[k] == p3_low[k+1]) settled_at = k;
if (settled_at < 0) begin
$display("FAIL: instance 3 never settled in %0d logged periods -- the pair did not converge",
n3); errors++; end
else begin
if (p3_low[settled_at] != exp_low(LA3, LB3, HA3, HB3)) begin
$display("FAIL: instance 3 settled at low=%0d, expected %0d",
p3_low[settled_at], exp_low(LA3,LB3,HA3,HB3)); errors++; end
// Convergence must be OBSERVABLE: at least one earlier period differed. If every
// period were identical the test would not be exercising synchronization at all.
if (settled_at == 0) begin
$display("FAIL: instance 3's very first period was already the settled one -- convergence was not observable");
errors++; end
else if (p3_low[settled_at - 1] == p3_low[settled_at]) begin
$display("FAIL: no period before the settled one differed from it"); errors++; end
end
end
// ---- 5. the faster-low master entered a wait state, the slower one did not ------------
// Section 3.1.7: "Masters with shorter LOW periods enter a HIGH wait-state during this
// time." B's low period is shorter, so B waits and A never does.
if (s1_nbw === '0) begin
$display("FAIL: master B (low %0d < %0d) never entered a wait state", LB1, LA1);
errors++; end
if (s1_naw !== '0) begin
$display("FAIL: master A (the longest low period) entered a wait state %0d times -- it is the one holding the line down",
s1_naw); errors++; end
// ---- 6. the combined clock belongs to NEITHER master -----------------------------------
// Its low phase is A's and its high phase is B's... no: its high phase is the SHORTER of
// the two, which here is A's. The point is that the pair (low, high) matches no single
// master's configuration unless one master wins both extrema.
if (s1_low == LB1 && s1_high == HB1) begin
$display("FAIL: the combined clock is exactly master B's configuration"); errors++; end
// ---- 7. instance 2: B wins BOTH extrema ------------------------------------------------
// The control for test 3. If the rule were "always use master A" or "use the first
// master", instance 1 would still pass and this would not.
if (s2_low != exp_low(LA2, LB2, HA2, HB2)) begin
$display("FAIL: instance 2 combined LOW = %0d, expected %0d",
s2_low, exp_low(LA2,LB2,HA2,HB2)); errors++; end
if (s2_high != exp_high(HA2, HB2)) begin
$display("FAIL: instance 2 combined HIGH = %0d, expected %0d",
s2_high, exp_high(HA2,HB2)); errors++; end
if (s2_naw === '0) begin
$display("FAIL: instance 2 master A (low %0d < %0d) never waited", LA2, LB2); errors++; end
if (s2_nbw !== '0) begin
$display("FAIL: instance 2 master B (longest low) waited %0d times", s2_nbw); errors++; end
// ---- 8. instance 3: ONE extremum each --------------------------------------------------
// The configuration that pins the rule down completely: the combined low comes from A and
// the combined high from B, so the result is neither master's clock.
if (s3_low != exp_low(LA3, LB3, HA3, HB3)) begin
$display("FAIL: instance 3 combined LOW = %0d, expected %0d (the extrema belong to DIFFERENT masters here, so the longest-low master did not initiate the fall)",
s3_low, exp_low(LA3,LB3,HA3,HB3)); errors++; end
if (s3_high != exp_high(HA3, HB3)) begin
$display("FAIL: instance 3 combined HIGH = %0d, expected %0d", s3_high, exp_high(HA3,HB3));
errors++; end
if (s3_low == LB3 || s3_high == HA3) begin
$display("FAIL: instance 3's combined clock matched a single master's configuration");
errors++; end
// ---- 9. the combined period is NOT the sum of either master's ---------------------------
// A consequence worth asserting: the synchronized frequency is lower than the faster
// master's and its duty cycle is different from both.
if ((s3_low + s3_high) == (LA3 + HA3 + 2) || (s3_low + s3_high) == (LB3 + HB3 + 2)) begin
$display("FAIL: instance 3's combined period equals a single master's period");
errors++; end
// ---- 10. removing one master hands the clock back to the other ------------------------
// Section 3.1.7 says "the masters concerned", so a master that stops participating stops
// contributing an extremum. B leaves and the line must become exactly A's clock again.
en_b = 1'b0;
tick(600);
begin
int lo0, hi0;
wait_p1(3);
lo0 = s1_low; hi0 = s1_high;
wait_p1(2);
if (s1_low != LA1 || s1_high != HA1 + 1) begin
$display("FAIL: after master B stopped, the line measured %0d/%0d, expected A's own %0d/%0d",
s1_low, s1_high, LA1, HA1 + 1); errors++; end
end
// ---- 11. disabling both releases the line ---------------------------------------------
en_a = 1'b0;
tick(600);
if (s1_line !== 1'b1) begin
$display("FAIL: SCL left low after both masters were disabled -- that is a stuck bus");
errors++; end
if (errors == 0)
$display("PASS: the combined clock is slowest-low and fastest-high, the shorter-low master waits, the result belongs to neither master, and a departing master stops contributing an extremum");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
initial begin
#4000000;
$display("FAIL: watchdog expired");
$finish;
end
endmodule // SCL SYNCHRONIZATION: HOW TWO MASTERS AGREE ON ONE CLOCK. Chapter 12.2 used UM10204 section 3.1.7
// for the purpose it was NOT written for -- clock stretching. This is what it was written for:
//
// "This means that a HIGH to LOW transition on the SCL line causes the masters concerned to start
// counting off their LOW period and, once a master clock has gone LOW, it holds the SCL line in
// that state until the clock HIGH state is reached. However, if another clock is still within its
// LOW period, the LOW to HIGH transition of this clock may not change the state of the SCL line.
// THE SCL LINE IS THEREFORE HELD LOW BY THE MASTER WITH THE LONGEST LOW PERIOD. Masters with
// shorter LOW periods enter a HIGH wait-state during this time. When all masters concerned have
// counted off their LOW period, the clock line is released and goes HIGH. ... all the masters
// start counting their HIGH periods. THE FIRST MASTER TO COMPLETE ITS HIGH PERIOD PULLS THE SCL
// LINE LOW AGAIN."
//
// Two sentences, two extrema, and together they are the whole combining rule:
//
// observed LOW = MAX over participating masters of their low periods (slowest-low wins)
// observed HIGH = MIN over participating masters of their high periods (fastest-high wins)
//
// The synchronized clock therefore belongs to NEITHER master. It is slower in its low phase than
// the faster master wanted and faster in its high phase than the slower master wanted, so both
// masters are running a clock neither of them configured. That is not a compromise the protocol
// negotiates -- it is what a wired-AND does to two square waves, and there is no logic anywhere
// that computes it.
//
// This block contains BOTH masters and the wired-AND between them, because the rule lives in the
// connection rather than in either participant. Each master is the synchronizing generator of
// Chapter 12.2 -- it counts its low phase from its own intent and its high phase from the observed
// rise -- with ONE difference, and the difference matters:
//
// A SINGLE-master generator that sees the line go low during its own high phase should ABANDON
// the period: nothing legitimate does that, and logging it would report a tHIGH violation it did
// not cause. (Chapter 12.2 section 5.)
//
// A MULTI-master generator must treat it as the high phase ENDING, because section 3.1.7 says
// the fastest master ends it and that master may not be this one.
//
// Same observation, opposite response, decided by the configuration rather than by the logic. A
// design that gets this backwards either drops every period on a two-master bus or reports
// phantom violations on a one-master bus, and mutation B5 is the first of those.
// (Verilog-2001 -- structurally identical to the SystemVerilog above.)
module i2c_scl_sync_pair #(
parameter TICK_W = 20,
parameter T_LOW_A = 130, // master A's intended phases
parameter T_HIGH_A = 60,
parameter T_LOW_B = 90, // master B is faster low and slower high, so each master
parameter T_HIGH_B = 100 // wins one extremum -- see the testbench
)(
input wire clk,
input wire rst_n,
input wire enable_a,
input wire enable_b,
// ---- the wired-AND line, which is the synchronized clock ----
output wire scl_line,
output reg a_drive_low,
output reg b_drive_low,
output reg a_waiting, // released, in section 3.1.7's "HIGH wait-state"
output reg b_waiting,
// ---- the observed phases of the combined clock ----
output reg phase_valid,
output reg [TICK_W-1:0] t_low_obs,
output reg [TICK_W-1:0] t_high_obs,
output reg [TICK_W-1:0] n_periods,
// ---- extrema of the combined clock ----
output reg [TICK_W-1:0] min_low_seen,
output reg [TICK_W-1:0] min_high_seen,
output reg [TICK_W-1:0] n_a_waits,
output reg [TICK_W-1:0] n_b_waits
);
localparam [TICK_W-1:0] TLA = T_LOW_A;
localparam [TICK_W-1:0] THA = T_HIGH_A;
localparam [TICK_W-1:0] TLB = T_LOW_B;
localparam [TICK_W-1:0] THB = T_HIGH_B;
localparam [1:0] S_IDLE = 2'd0, S_LOW = 2'd1, S_WAIT = 2'd2, S_HIGH = 2'd3;
// ---- the wired-AND. A pull-down is a command; a release is a request. ----
assign scl_line = !a_drive_low && !b_drive_low;
reg scl_q;
wire scl_rise = scl_line && !scl_q;
wire scl_fall = !scl_line && scl_q;
reg [1:0] st_a, st_b;
reg [TICK_W-1:0] cnt_a, cnt_b;
wire [TICK_W-1:0] cnt_a_now = cnt_a + 1'b1;
wire [TICK_W-1:0] cnt_b_now = cnt_b + 1'b1;
// ---- measurement of the COMBINED clock, taken from the line ----
reg [TICK_W-1:0] meas, low_meas;
wire [TICK_W-1:0] meas_now = meas + 1'b1;
always @(posedge clk) begin
if (!rst_n) begin
scl_q <= 1'b1;
a_drive_low <= 1'b0;
b_drive_low <= 1'b0;
a_waiting <= 1'b0;
b_waiting <= 1'b0;
st_a <= S_IDLE;
st_b <= S_IDLE;
cnt_a <= {TICK_W{1'b0}};
cnt_b <= {TICK_W{1'b0}};
meas <= {TICK_W{1'b0}};
low_meas <= {TICK_W{1'b0}};
phase_valid <= 1'b0;
t_low_obs <= {TICK_W{1'b0}};
t_high_obs <= {TICK_W{1'b0}};
n_periods <= {TICK_W{1'b0}};
n_a_waits <= {TICK_W{1'b0}};
n_b_waits <= {TICK_W{1'b0}};
min_low_seen <= {TICK_W{1'b1}};
min_high_seen <= {TICK_W{1'b1}};
end else begin
scl_q <= scl_line;
phase_valid <= 1'b0;
// ================= master A =================
case (st_a)
S_IDLE: begin
a_drive_low <= 1'b0;
a_waiting <= 1'b0;
if (enable_a) begin a_drive_low <= 1'b1; cnt_a <= {TICK_W{1'b0}}; st_a <= S_LOW; end
end
S_LOW: begin
// Counting off MY low period. Section 3.1.7 starts this at the line's fall, and
// since I am pulling it down the two coincide.
if (cnt_a_now >= TLA) begin
a_drive_low <= 1'b0; // release: a REQUEST
cnt_a <= {TICK_W{1'b0}};
st_a <= S_WAIT;
end else cnt_a <= cnt_a_now;
end
S_WAIT: begin
// Section 3.1.7's "HIGH wait-state": I have let go and the line is still low,
// so the other master has the longer low period.
if (scl_line) begin
a_waiting <= 1'b0;
cnt_a <= {TICK_W{1'b0}};
st_a <= S_HIGH;
end else if (!a_waiting) begin
a_waiting <= 1'b1;
n_a_waits <= n_a_waits + 1'b1;
end
end
S_HIGH: begin
// Counting off MY high period, started by the observed rise (Chapter 12.2).
if (cnt_a_now >= THA) begin
// I am the first to finish: section 3.1.7 says I pull the line low again.
if (enable_a) begin a_drive_low <= 1'b1; cnt_a <= {TICK_W{1'b0}}; st_a <= S_LOW; end
else st_a <= S_IDLE;
end else if (!scl_line) begin
// The OTHER master finished its high period first and ended the phase. In a
// MULTI-master design this is normal and my high phase is over -- I join the
// low phase rather than abandoning the period.
a_drive_low <= 1'b1;
cnt_a <= {TICK_W{1'b0}};
st_a <= S_LOW;
end else cnt_a <= cnt_a_now;
end
default: st_a <= S_IDLE;
endcase
// ================= master B: identical logic, its own parameters =================
case (st_b)
S_IDLE: begin
b_drive_low <= 1'b0;
b_waiting <= 1'b0;
if (enable_b) begin b_drive_low <= 1'b1; cnt_b <= {TICK_W{1'b0}}; st_b <= S_LOW; end
end
S_LOW: begin
if (cnt_b_now >= TLB) begin
b_drive_low <= 1'b0;
cnt_b <= {TICK_W{1'b0}};
st_b <= S_WAIT;
end else cnt_b <= cnt_b_now;
end
S_WAIT: begin
if (scl_line) begin
b_waiting <= 1'b0;
cnt_b <= {TICK_W{1'b0}};
st_b <= S_HIGH;
end else if (!b_waiting) begin
b_waiting <= 1'b1;
n_b_waits <= n_b_waits + 1'b1;
end
end
S_HIGH: begin
if (cnt_b_now >= THB) begin
if (enable_b) begin b_drive_low <= 1'b1; cnt_b <= {TICK_W{1'b0}}; st_b <= S_LOW; end
else st_b <= S_IDLE;
end else if (!scl_line) begin
b_drive_low <= 1'b1;
cnt_b <= {TICK_W{1'b0}};
st_b <= S_LOW;
end else cnt_b <= cnt_b_now;
end
default: st_b <= S_IDLE;
endcase
// ================= measure the COMBINED clock, from the LINE =================
// Not from either master's intent: the synchronized clock is a property of the wire.
if (scl_rise) begin
low_meas <= meas_now; // the low phase just ended
meas <= {TICK_W{1'b0}};
end else if (scl_fall) begin
t_low_obs <= low_meas;
t_high_obs <= meas_now; // the high phase just ended
phase_valid <= 1'b1;
n_periods <= n_periods + 1'b1;
if (low_meas < min_low_seen) min_low_seen <= low_meas;
if (meas_now < min_high_seen) min_high_seen <= meas_now;
meas <= {TICK_W{1'b0}};
end else begin
meas <= meas_now;
end
end
end
endmodule `timescale 1ns/1ps
// 100 MHz sample clock.
//
// TWO THINGS THE EXPECTATIONS HAVE TO ACCOUNT FOR, both of them real and both explained in the
// chapter rather than tolerated here:
//
// 1. CONVERGENCE. Section 3.1.7's rule assumes every master starts counting its low period at the
// SAME event -- "a HIGH to LOW transition on the SCL line causes the masters concerned to start
// counting off their LOW period". A master enabled part-way through a period has not yet had
// that event, so the FIRST combined period is irregular. Convergence takes exactly one period,
// and demonstrating it is what "synchronization" means. Every steady-state check below therefore
// discards the first combined period, and test 4 asserts that it really was different.
//
// 2. OBSERVATION LATENCY. A master learns that the line moved by OBSERVING it, one cycle after the
// fact, while it drives the line from a register one cycle before the fact. Those offsets CANCEL
// for a master that initiated the edge and ADD for one that did not -- the release-is-a-request
// asymmetry of Chapter 12.2 section 2, appearing for the third time.
//
// Section 3.1.7 says the high phase is ended by the master with the SHORTEST high period, so:
//
// observed high = min(high periods) + 1 -- always: its count starts at an observed
// rise and ends at a registered drive
// observed low = max(low periods) + 0 -- IF the same master also has the shortest
// high, because then it initiated the fall
// max(low periods) + 1 -- IF the extrema belong to DIFFERENT masters
//
// So the latency shows up precisely in the interesting case -- the one where the combined clock
// belongs to neither master. Instances 1 and 2 have one master winning both extrema; instance 3
// splits them, and only instance 3 sees the +1 on its low phase.
//
// The two masters are configured so that EACH WINS ONE EXTREMUM, which is what makes the combining
// rule observable:
//
// master A : low 130, high 60
// master B : low 90, high 100
//
// expected combined low = max(130, 90) = 130 -- A's, the slowest low
// expected combined high = min( 60,100) = 60 -- A's, the fastest high
//
// A wins both here, so a second configuration below swaps them: B low 200, high 40 gives a
// combined 200/40 with B winning both. And a third gives one each. A suite where one master wins
// both extrema every time cannot distinguish max/min from "just use master A".
// (Verilog-2001 testbench -- same stimulus, same checks.)
module i2c_scl_sync_pair_tb;
localparam TICK_W = 20;
// Independently computed expectations -- NOT read from the DUT.
function integer imax(input integer x, input integer y); imax = (x > y) ? x : y; endfunction
function integer imin(input integer x, input integer y); imin = (x < y) ? x : y; endfunction
// Steady-state observed phases, including the observation latency of note 2. The low phase's
// latency depends on WHETHER THE SAME MASTER WINS BOTH EXTREMA, because only then is the
// longest-low master the one that initiated the falling edge.
function integer exp_high(input integer ha, input integer hb); exp_high = imin(ha,hb) + 1; endfunction
function integer exp_low(input integer la, input integer lb, input integer ha, input integer hb);
reg max_low_is_a, min_high_is_a;
begin
max_low_is_a = (la > lb);
min_high_is_a = (ha < hb);
exp_low = imax(la,lb) + ((max_low_is_a == min_high_is_a) ? 0 : 1);
end
endfunction
reg clk = 1'b0, rst_n = 1'b0;
reg en_a = 1'b0, en_b = 1'b0;
always #5 clk = ~clk;
// ---------------- instance 1: A low 130 high 60, B low 90 high 100 ----------------
localparam LA1 = 130, HA1 = 60, LB1 = 90, HB1 = 100;
wire s1_line, s1_ad, s1_bd, s1_aw, s1_bw, s1_pv;
wire [TICK_W-1:0] s1_high;
wire [TICK_W-1:0] s1_low, s1_np, s1_minl, s1_minh, s1_naw, s1_nbw;
i2c_scl_sync_pair #(.TICK_W(TICK_W), .T_LOW_A(LA1), .T_HIGH_A(HA1),
.T_LOW_B(LB1), .T_HIGH_B(HB1)) u1 (
.clk(clk), .rst_n(rst_n), .enable_a(en_a), .enable_b(en_b),
.scl_line(s1_line), .a_drive_low(s1_ad), .b_drive_low(s1_bd),
.a_waiting(s1_aw), .b_waiting(s1_bw),
.phase_valid(s1_pv), .t_low_obs(s1_low), .t_high_obs(s1_high), .n_periods(s1_np),
.min_low_seen(s1_minl), .min_high_seen(s1_minh),
.n_a_waits(s1_naw), .n_b_waits(s1_nbw));
// ---------------- instance 2: B wins BOTH extrema ----------------
localparam LA2 = 100, HA2 = 90, LB2 = 200, HB2 = 40;
wire s2_line, s2_ad, s2_bd, s2_aw, s2_bw, s2_pv;
wire [TICK_W-1:0] s2_high;
wire [TICK_W-1:0] s2_low, s2_np, s2_minl, s2_minh, s2_naw, s2_nbw;
i2c_scl_sync_pair #(.TICK_W(TICK_W), .T_LOW_A(LA2), .T_HIGH_A(HA2),
.T_LOW_B(LB2), .T_HIGH_B(HB2)) u2 (
.clk(clk), .rst_n(rst_n), .enable_a(en_a), .enable_b(en_b),
.scl_line(s2_line), .a_drive_low(s2_ad), .b_drive_low(s2_bd),
.a_waiting(s2_aw), .b_waiting(s2_bw),
.phase_valid(s2_pv), .t_low_obs(s2_low), .t_high_obs(s2_high), .n_periods(s2_np),
.min_low_seen(s2_minl), .min_high_seen(s2_minh),
.n_a_waits(s2_naw), .n_b_waits(s2_nbw));
// ---------------- instance 3: ONE extremum each ----------------
localparam LA3 = 150, HA3 = 120, LB3 = 80, HB3 = 50;
wire s3_line, s3_ad, s3_bd, s3_aw, s3_bw, s3_pv;
wire [TICK_W-1:0] s3_high;
wire [TICK_W-1:0] s3_low, s3_np, s3_minl, s3_minh, s3_naw, s3_nbw;
i2c_scl_sync_pair #(.TICK_W(TICK_W), .T_LOW_A(LA3), .T_HIGH_A(HA3),
.T_LOW_B(LB3), .T_HIGH_B(HB3)) u3 (
.clk(clk), .rst_n(rst_n), .enable_a(en_a), .enable_b(en_b),
.scl_line(s3_line), .a_drive_low(s3_ad), .b_drive_low(s3_bd),
.a_waiting(s3_aw), .b_waiting(s3_bw),
.phase_valid(s3_pv), .t_low_obs(s3_low), .t_high_obs(s3_high), .n_periods(s3_np),
.min_low_seen(s3_minl), .min_high_seen(s3_minh),
.n_a_waits(s3_naw), .n_b_waits(s3_nbw));
integer errors = 0;
// Hoisted to module scope: Verilog-2001 permits a variable declaration only at
// module level or in a NAMED block, and every call site below is sequential.
integer seen = 0;
integer k = 0;
integer settled_at = 0;
integer lo0 = 0;
integer hi0 = 0;
integer first_low = 0;
integer first_high = 0;
// Instance 3's period log: convergence is asserted there because that is the instance whose
// extrema belong to different masters, and therefore the one with something to converge to.
integer n3 = 0;
integer p3_low[0:7];
integer p3_high[0:7];
// The log clears on reset, so a period logged before the mid-test reset cannot be mistaken for
// the first combined one. Index arithmetic across a reset is exactly the kind of stale-state
// trap Chapter 11.6 section 5 records for tBUF.
always @(posedge clk)
if (!rst_n) n3 = 0;
else if (s3_pv && n3 < 8) begin
p3_low[n3] = s3_low; p3_high[n3] = s3_high; n3 = n3 + 1;
end
integer n3_at_both = 0;
task tick(input integer n); begin repeat (n) @(negedge clk); end endtask
// Wait for n reported periods on instance 1, then settle so a read is safe.
task wait_p1(input integer n);
begin seen = 0;
while (seen < n) begin @(posedge clk); if (s1_pv) seen = seen + 1; end
@(negedge clk);
end
endtask
initial begin
tick(4); rst_n = 1'b1; tick(4);
// ---- 1. reset: MINIMUM trackers at their maximum ------------------------------------
if (s1_minl !== {TICK_W{1'b1}} || s1_minh !== {TICK_W{1'b1}}) begin
$display("FAIL: minimum trackers did not start at their maximum"); errors = errors + 1; end
if (s1_np !== {TICK_W{1'b0}} || s1_naw !== {TICK_W{1'b0}} || s1_nbw !== {TICK_W{1'b0}}) begin
$display("FAIL: counters nonzero out of reset"); errors = errors + 1; end
if (s1_line !== 1'b1) begin
$display("FAIL: SCL not released out of reset"); errors = errors + 1; end
// ---- 2. master A alone: the line is exactly A's clock ---------------------------------
// A single master is the degenerate case of the rule -- max and min over one element.
// With one master there is no observation latency on the LOW phase: A initiates its own
// falling edge, so its count and the line agree. The HIGH phase still carries +1, because
// its count begins at an edge A observes and ends at a drive A registers.
en_a = 1'b1;
wait_p1(4);
if (s1_low != LA1) begin
$display("FAIL: master A alone gave low=%0d, expected its own %0d (it initiates its own fall, so no observation latency)",
s1_low, LA1); errors = errors + 1; end
if (s1_high != HA1 + 1) begin
$display("FAIL: master A alone gave high=%0d, expected %0d (+1 observation latency)",
s1_high, HA1 + 1); errors = errors + 1; end
if (s1_nbw !== {TICK_W{1'b0}}) begin
$display("FAIL: master B reported a wait state while disabled"); errors = errors + 1; end
en_a = 1'b0;
tick(400);
// ---- 3. both masters: slowest-low, fastest-high ---------------------------------------
// THE test of the chapter. The expectation is computed here from the parameters, not read
// from the DUT, so what is under test is the combining rule rather than a shared constant.
rst_n = 1'b0; tick(4); rst_n = 1'b1; tick(4);
n3_at_both = n3; // mark where instance 3's combined periods begin
en_a = 1'b1; en_b = 1'b1;
wait_p1(2); // discard the pre-convergence period
first_low = s1_low;
first_high = s1_high;
wait_p1(4); // now in steady state
if (s1_low != exp_low(LA1, LB1, HA1, HB1)) begin
$display("FAIL: combined LOW = %0d, expected %0d -- the line is held low by the master with the LONGEST low period",
s1_low, exp_low(LA1,LB1,HA1,HB1)); errors = errors + 1; end
if (s1_high != exp_high(HA1, HB1)) begin
$display("FAIL: combined HIGH = %0d, expected min(%0d,%0d)+1 = %0d -- the FIRST master to finish its high period pulls the line low",
s1_high, HA1, HB1, exp_high(HA1,HB1)); errors = errors + 1; end
// ---- 4. CONVERGENCE is observable: the first combined period differs from the rest ------
// This is what "synchronization" names. A master joining part-way through has not yet seen
// the falling edge that starts everyone's low count, so its first period is irregular. One
// period later every master is counting from the same event and the rule holds exactly.
// Instance 3's first combined period must differ from its second: a master that joined
// part-way through has not yet seen the falling edge everyone else counted from.
// Index-independent: reset leaves a partial period in the log, so rather than counting
// entries, find the first index where two consecutive periods agree. That is where the pair
// has converged, and the value there must be the combining rule's answer.
begin
settled_at = -1;
for (k = 0; k + 1 < n3; k = k + 1)
if (settled_at < 0 && p3_low[k] == p3_low[k+1]) settled_at = k;
if (settled_at < 0) begin
$display("FAIL: instance 3 never settled in %0d logged periods -- the pair did not converge",
n3); errors = errors + 1; end
else begin
if (p3_low[settled_at] != exp_low(LA3, LB3, HA3, HB3)) begin
$display("FAIL: instance 3 settled at low=%0d, expected %0d",
p3_low[settled_at], exp_low(LA3,LB3,HA3,HB3)); errors = errors + 1; end
// Convergence must be OBSERVABLE: at least one earlier period differed. If every
// period were identical the test would not be exercising synchronization at all.
if (settled_at == 0) begin
$display("FAIL: instance 3's very first period was already the settled one -- convergence was not observable");
errors = errors + 1; end
else if (p3_low[settled_at - 1] == p3_low[settled_at]) begin
$display("FAIL: no period before the settled one differed from it"); errors = errors + 1; end
end
end
// ---- 5. the faster-low master entered a wait state, the slower one did not ------------
// Section 3.1.7: "Masters with shorter LOW periods enter a HIGH wait-state during this
// time." B's low period is shorter, so B waits and A never does.
if (s1_nbw === {TICK_W{1'b0}}) begin
$display("FAIL: master B (low %0d < %0d) never entered a wait state", LB1, LA1);
errors = errors + 1; end
if (s1_naw !== {TICK_W{1'b0}}) begin
$display("FAIL: master A (the longest low period) entered a wait state %0d times -- it is the one holding the line down",
s1_naw); errors = errors + 1; end
// ---- 6. the combined clock belongs to NEITHER master -----------------------------------
// Its low phase is A's and its high phase is B's... no: its high phase is the SHORTER of
// the two, which here is A's. The point is that the pair (low, high) matches no single
// master's configuration unless one master wins both extrema.
if (s1_low == LB1 && s1_high == HB1) begin
$display("FAIL: the combined clock is exactly master B's configuration"); errors = errors + 1; end
// ---- 7. instance 2: B wins BOTH extrema ------------------------------------------------
// The control for test 3. If the rule were "always use master A" or "use the first
// master", instance 1 would still pass and this would not.
if (s2_low != exp_low(LA2, LB2, HA2, HB2)) begin
$display("FAIL: instance 2 combined LOW = %0d, expected %0d",
s2_low, exp_low(LA2,LB2,HA2,HB2)); errors = errors + 1; end
if (s2_high != exp_high(HA2, HB2)) begin
$display("FAIL: instance 2 combined HIGH = %0d, expected %0d",
s2_high, exp_high(HA2,HB2)); errors = errors + 1; end
if (s2_naw === {TICK_W{1'b0}}) begin
$display("FAIL: instance 2 master A (low %0d < %0d) never waited", LA2, LB2); errors = errors + 1; end
if (s2_nbw !== {TICK_W{1'b0}}) begin
$display("FAIL: instance 2 master B (longest low) waited %0d times", s2_nbw); errors = errors + 1; end
// ---- 8. instance 3: ONE extremum each --------------------------------------------------
// The configuration that pins the rule down completely: the combined low comes from A and
// the combined high from B, so the result is neither master's clock.
if (s3_low != exp_low(LA3, LB3, HA3, HB3)) begin
$display("FAIL: instance 3 combined LOW = %0d, expected %0d (the extrema belong to DIFFERENT masters here, so the longest-low master did not initiate the fall)",
s3_low, exp_low(LA3,LB3,HA3,HB3)); errors = errors + 1; end
if (s3_high != exp_high(HA3, HB3)) begin
$display("FAIL: instance 3 combined HIGH = %0d, expected %0d", s3_high, exp_high(HA3,HB3));
errors = errors + 1; end
if (s3_low == LB3 || s3_high == HA3) begin
$display("FAIL: instance 3's combined clock matched a single master's configuration");
errors = errors + 1; end
// ---- 9. the combined period is NOT the sum of either master's ---------------------------
// A consequence worth asserting: the synchronized frequency is lower than the faster
// master's and its duty cycle is different from both.
if ((s3_low + s3_high) == (LA3 + HA3 + 2) || (s3_low + s3_high) == (LB3 + HB3 + 2)) begin
$display("FAIL: instance 3's combined period equals a single master's period");
errors = errors + 1; end
// ---- 10. removing one master hands the clock back to the other ------------------------
// Section 3.1.7 says "the masters concerned", so a master that stops participating stops
// contributing an extremum. B leaves and the line must become exactly A's clock again.
en_b = 1'b0;
tick(600);
begin
wait_p1(3);
lo0 = s1_low; hi0 = s1_high;
wait_p1(2);
if (s1_low != LA1 || s1_high != HA1 + 1) begin
$display("FAIL: after master B stopped, the line measured %0d/%0d, expected A's own %0d/%0d",
s1_low, s1_high, LA1, HA1 + 1); errors = errors + 1; end
end
// ---- 11. disabling both releases the line ---------------------------------------------
en_a = 1'b0;
tick(600);
if (s1_line !== 1'b1) begin
$display("FAIL: SCL left low after both masters were disabled -- that is a stuck bus");
errors = errors + 1; end
if (errors == 0)
$display("PASS: the combined clock is slowest-low and fastest-high, the shorter-low master waits, the result belongs to neither master, and a departing master stops contributing an extremum");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
initial begin
#4000000;
$display("FAIL: watchdog expired");
$finish;
end
endmodule -- SCL SYNCHRONIZATION: HOW TWO MASTERS AGREE ON ONE CLOCK -- the VHDL form. Both masters and the
-- wired-AND between them live in one entity, because the combining rule lives in the connection
-- rather than in either participant.
--
-- UM10204 section 3.1.7: "THE SCL LINE IS THEREFORE HELD LOW BY THE MASTER WITH THE LONGEST LOW
-- PERIOD. Masters with shorter LOW periods enter a HIGH wait-state during this time. ... THE FIRST
-- MASTER TO COMPLETE ITS HIGH PERIOD PULLS THE SCL LINE LOW AGAIN."
--
-- observed LOW = MAX of the participating masters' low periods (slowest-low wins)
-- observed HIGH = MIN of their high periods (fastest-high wins)
--
-- Each master is the synchronizing generator of Chapter 12.2, with one difference: a low line during
-- its own high phase means the phase ENDED (another master finished first) rather than that the
-- period should be abandoned. That distinction is between a single-master and a multi-master design,
-- and getting it backwards either drops every period here or invents violations there.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_scl_sync_pair is
generic (
TICK_W : natural := 20;
T_LOW_A : natural := 130;
T_HIGH_A : natural := 60;
T_LOW_B : natural := 90;
T_HIGH_B : natural := 100
);
port (
clk : in std_logic;
rst_n : in std_logic;
enable_a : in std_logic;
enable_b : in std_logic;
scl_line : out std_logic;
a_drive_low : out std_logic;
b_drive_low : out std_logic;
a_waiting : out std_logic;
b_waiting : out std_logic;
phase_valid : out std_logic;
t_low_obs : out unsigned(TICK_W-1 downto 0);
t_high_obs : out unsigned(TICK_W-1 downto 0);
n_periods : out unsigned(TICK_W-1 downto 0);
min_low_seen : out unsigned(TICK_W-1 downto 0);
min_high_seen : out unsigned(TICK_W-1 downto 0);
n_a_waits : out unsigned(TICK_W-1 downto 0);
n_b_waits : out unsigned(TICK_W-1 downto 0)
);
end entity;
architecture rtl of i2c_scl_sync_pair is
constant TLA : unsigned(TICK_W-1 downto 0) := to_unsigned(T_LOW_A, TICK_W);
constant THA : unsigned(TICK_W-1 downto 0) := to_unsigned(T_HIGH_A, TICK_W);
constant TLB : unsigned(TICK_W-1 downto 0) := to_unsigned(T_LOW_B, TICK_W);
constant THB : unsigned(TICK_W-1 downto 0) := to_unsigned(T_HIGH_B, TICK_W);
type state_t is (S_IDLE, S_LOW, S_WAIT, S_HIGH);
signal st_a, st_b : state_t := S_IDLE;
signal s_ad, s_bd, s_aw, s_bw : std_logic := '0';
signal s_line : std_logic;
signal scl_q : std_logic := '1';
signal cnt_a, cnt_b : unsigned(TICK_W-1 downto 0) := (others => '0');
signal cnt_a_now, cnt_b_now : unsigned(TICK_W-1 downto 0);
signal meas, low_meas : unsigned(TICK_W-1 downto 0) := (others => '0');
signal meas_now : unsigned(TICK_W-1 downto 0);
signal r_np : unsigned(TICK_W-1 downto 0) := (others => '0');
signal r_minl : unsigned(TICK_W-1 downto 0) := (others => '1');
signal r_minh : unsigned(TICK_W-1 downto 0) := (others => '1');
signal r_naw : unsigned(TICK_W-1 downto 0) := (others => '0');
signal r_nbw : unsigned(TICK_W-1 downto 0) := (others => '0');
signal scl_rise_s, scl_fall_s : std_logic;
begin
-- the wired-AND: a pull-down is a command, a release is a request
s_line <= '1' when (s_ad = '0' and s_bd = '0') else '0';
scl_line <= s_line;
a_drive_low <= s_ad;
b_drive_low <= s_bd;
a_waiting <= s_aw;
b_waiting <= s_bw;
scl_rise_s <= '1' when (s_line = '1' and scl_q = '0') else '0';
scl_fall_s <= '1' when (s_line = '0' and scl_q = '1') else '0';
cnt_a_now <= cnt_a + 1;
cnt_b_now <= cnt_b + 1;
meas_now <= meas + 1;
n_periods <= r_np;
min_low_seen <= r_minl;
min_high_seen <= r_minh;
n_a_waits <= r_naw;
n_b_waits <= r_nbw;
process (clk) is
begin
if rising_edge(clk) then
if rst_n = '0' then
scl_q <= '1';
s_ad <= '0';
s_bd <= '0';
s_aw <= '0';
s_bw <= '0';
st_a <= S_IDLE;
st_b <= S_IDLE;
cnt_a <= (others => '0');
cnt_b <= (others => '0');
meas <= (others => '0');
low_meas <= (others => '0');
phase_valid <= '0';
t_low_obs <= (others => '0');
t_high_obs <= (others => '0');
r_np <= (others => '0');
r_naw <= (others => '0');
r_nbw <= (others => '0');
r_minl <= (others => '1');
r_minh <= (others => '1');
else
scl_q <= s_line;
phase_valid <= '0';
-- ================= master A =================
case st_a is
when S_IDLE =>
s_ad <= '0';
s_aw <= '0';
if enable_a = '1' then
s_ad <= '1';
cnt_a <= (others => '0');
st_a <= S_LOW;
end if;
when S_LOW =>
if cnt_a_now >= TLA then
s_ad <= '0'; -- release: a REQUEST
cnt_a <= (others => '0');
st_a <= S_WAIT;
else
cnt_a <= cnt_a_now;
end if;
when S_WAIT =>
-- section 3.1.7's "HIGH wait-state"
if s_line = '1' then
s_aw <= '0';
cnt_a <= (others => '0');
st_a <= S_HIGH;
elsif s_aw = '0' then
s_aw <= '1';
r_naw <= r_naw + 1;
end if;
when S_HIGH =>
if cnt_a_now >= THA then
-- first to finish: section 3.1.7 says I pull the line low again
if enable_a = '1' then
s_ad <= '1';
cnt_a <= (others => '0');
st_a <= S_LOW;
else
st_a <= S_IDLE;
end if;
elsif s_line = '0' then
-- the OTHER master ended the high phase. In a MULTI-master design that
-- is normal: my high phase is over, so I join the low phase.
s_ad <= '1';
cnt_a <= (others => '0');
st_a <= S_LOW;
else
cnt_a <= cnt_a_now;
end if;
end case;
-- ================= master B: identical logic, its own generics =================
case st_b is
when S_IDLE =>
s_bd <= '0';
s_bw <= '0';
if enable_b = '1' then
s_bd <= '1';
cnt_b <= (others => '0');
st_b <= S_LOW;
end if;
when S_LOW =>
if cnt_b_now >= TLB then
s_bd <= '0';
cnt_b <= (others => '0');
st_b <= S_WAIT;
else
cnt_b <= cnt_b_now;
end if;
when S_WAIT =>
if s_line = '1' then
s_bw <= '0';
cnt_b <= (others => '0');
st_b <= S_HIGH;
elsif s_bw = '0' then
s_bw <= '1';
r_nbw <= r_nbw + 1;
end if;
when S_HIGH =>
if cnt_b_now >= THB then
if enable_b = '1' then
s_bd <= '1';
cnt_b <= (others => '0');
st_b <= S_LOW;
else
st_b <= S_IDLE;
end if;
elsif s_line = '0' then
s_bd <= '1';
cnt_b <= (others => '0');
st_b <= S_LOW;
else
cnt_b <= cnt_b_now;
end if;
end case;
-- ========== measure the COMBINED clock, from the LINE ==========
if scl_rise_s = '1' then
low_meas <= meas_now;
meas <= (others => '0');
elsif scl_fall_s = '1' then
t_low_obs <= low_meas;
t_high_obs <= meas_now;
phase_valid <= '1';
r_np <= r_np + 1;
if low_meas < r_minl then
r_minl <= low_meas;
end if;
if meas_now < r_minh then
r_minh <= meas_now;
end if;
meas <= (others => '0');
else
meas <= meas_now;
end if;
end if;
end if;
end process;
end architecture; -- The VHDL testbench for the synchronizing pair. Three instances with different configurations, for
-- the reason the SystemVerilog version gives: a suite in which one master wins both extrema every time
-- cannot distinguish max/min from "just use master A".
--
-- Expectations are computed here from the generics, and they account for the one-cycle OBSERVATION
-- LATENCY: a master learns the line moved by observing it, one cycle late, while it drives from a
-- register one cycle early. Those offsets cancel for the master that initiated the edge and add for
-- one that did not -- so the low phase carries +1 only when the two extrema belong to DIFFERENT
-- masters.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_scl_sync_pair_tb is
end entity;
architecture tb of i2c_scl_sync_pair_tb is
constant TICK_W : natural := 20;
constant LA1 : natural := 130; constant HA1 : natural := 60;
constant LB1 : natural := 90; constant HB1 : natural := 100;
constant LA2 : natural := 100; constant HA2 : natural := 90;
constant LB2 : natural := 200; constant HB2 : natural := 40;
constant LA3 : natural := 150; constant HA3 : natural := 120;
constant LB3 : natural := 80; constant HB3 : natural := 50;
function imax(x, y : integer) return integer is
begin if x > y then return x; else return y; end if; end function;
function imin(x, y : integer) return integer is
begin if x < y then return x; else return y; end if; end function;
function exp_high(ha, hb : integer) return integer is
begin return imin(ha, hb) + 1; end function;
-- The low phase's latency depends on whether the SAME master wins both extrema, because only
-- then is the longest-low master the one that initiated the falling edge.
function exp_low(la, lb, ha, hb : integer) return integer is
variable max_low_is_a, min_high_is_a : boolean;
begin
max_low_is_a := (la > lb);
min_high_is_a := (ha < hb);
if max_low_is_a = min_high_is_a then
return imax(la, lb);
else
return imax(la, lb) + 1;
end if;
end function;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal en_a, en_b : std_logic := '0';
signal l1, ad1, bd1, aw1, bw1, pv1 : std_logic;
signal lo1, hi1, np1, ml1, mh1, na1, nb1 : unsigned(TICK_W-1 downto 0);
signal l2, ad2, bd2, aw2, bw2, pv2 : std_logic;
signal lo2, hi2, np2, ml2, mh2, na2, nb2 : unsigned(TICK_W-1 downto 0);
signal l3, ad3, bd3, aw3, bw3, pv3 : std_logic;
signal lo3, hi3, np3, ml3, mh3, na3, nb3 : unsigned(TICK_W-1 downto 0);
signal done : boolean := false;
signal errors : integer := 0;
type int_arr is array (0 to 7) of integer;
signal p3_low : int_arr := (others => 0);
signal n3 : integer := 0;
begin
clk_gen : process is
begin
while not done loop
clk <= '0'; wait for 5 ns;
clk <= '1'; wait for 5 ns;
end loop;
wait;
end process;
u1 : entity work.i2c_scl_sync_pair
generic map (TICK_W => TICK_W, T_LOW_A => LA1, T_HIGH_A => HA1,
T_LOW_B => LB1, T_HIGH_B => HB1)
port map (clk => clk, rst_n => rst_n, enable_a => en_a, enable_b => en_b,
scl_line => l1, a_drive_low => ad1, b_drive_low => bd1,
a_waiting => aw1, b_waiting => bw1, phase_valid => pv1,
t_low_obs => lo1, t_high_obs => hi1, n_periods => np1,
min_low_seen => ml1, min_high_seen => mh1,
n_a_waits => na1, n_b_waits => nb1);
u2 : entity work.i2c_scl_sync_pair
generic map (TICK_W => TICK_W, T_LOW_A => LA2, T_HIGH_A => HA2,
T_LOW_B => LB2, T_HIGH_B => HB2)
port map (clk => clk, rst_n => rst_n, enable_a => en_a, enable_b => en_b,
scl_line => l2, a_drive_low => ad2, b_drive_low => bd2,
a_waiting => aw2, b_waiting => bw2, phase_valid => pv2,
t_low_obs => lo2, t_high_obs => hi2, n_periods => np2,
min_low_seen => ml2, min_high_seen => mh2,
n_a_waits => na2, n_b_waits => nb2);
u3 : entity work.i2c_scl_sync_pair
generic map (TICK_W => TICK_W, T_LOW_A => LA3, T_HIGH_A => HA3,
T_LOW_B => LB3, T_HIGH_B => HB3)
port map (clk => clk, rst_n => rst_n, enable_a => en_a, enable_b => en_b,
scl_line => l3, a_drive_low => ad3, b_drive_low => bd3,
a_waiting => aw3, b_waiting => bw3, phase_valid => pv3,
t_low_obs => lo3, t_high_obs => hi3, n_periods => np3,
min_low_seen => ml3, min_high_seen => mh3,
n_a_waits => na3, n_b_waits => nb3);
-- Instance 3's period log, cleared on reset so a period logged before the mid-test reset cannot
-- be mistaken for the first combined one.
logger : process (clk) is
begin
if rising_edge(clk) then
if rst_n = '0' then
n3 <= 0;
elsif pv3 = '1' and n3 < 8 then
p3_low(n3) <= to_integer(lo3);
n3 <= n3 + 1;
end if;
end if;
end process;
stim : process is
procedure tick(n : in integer) is
begin
for i in 1 to n loop
wait until falling_edge(clk);
end loop;
end procedure;
procedure wait_p1(n : in integer) is
variable seen : integer := 0;
begin
seen := 0;
while seen < n loop
wait until rising_edge(clk);
if pv1 = '1' then seen := seen + 1; end if;
end loop;
wait until falling_edge(clk);
end procedure;
procedure chk(cond : in boolean; msg : in string) is
begin
if not cond then
report "FAIL: " & msg severity error;
errors <= errors + 1;
wait for 0 ns;
end if;
end procedure;
variable settled_at : integer;
begin
tick(4); rst_n <= '1'; tick(4);
-- 1. reset
chk(ml1 = (ml1'range => '1') and mh1 = (mh1'range => '1'),
"minimum trackers did not start at their maximum");
chk(np1 = 0 and na1 = 0 and nb1 = 0, "counters nonzero out of reset");
chk(l1 = '1', "SCL not released out of reset");
-- 2. master A alone. A initiates its own fall, so the LOW phase has no observation latency;
-- the HIGH phase still carries +1.
en_a <= '1';
wait_p1(4);
chk(to_integer(lo1) = LA1,
"master A alone gave the wrong low phase (it initiates its own fall, so no latency)");
chk(to_integer(hi1) = HA1 + 1,
"master A alone gave the wrong high phase (+1 observation latency expected)");
chk(nb1 = 0, "master B reported a wait state while disabled");
en_a <= '0';
tick(400);
-- 3. both masters: slowest-low, fastest-high, in steady state
rst_n <= '0'; tick(4); rst_n <= '1'; tick(4);
en_a <= '1'; en_b <= '1';
wait_p1(2); -- discard the pre-convergence period
wait_p1(4); -- now in steady state
chk(to_integer(lo1) = exp_low(LA1, LB1, HA1, HB1),
"the combined LOW is wrong -- the line is held low by the master with the LONGEST low period");
chk(to_integer(hi1) = exp_high(HA1, HB1),
"the combined HIGH is wrong -- the FIRST master to finish its high period pulls the line low");
-- 4. convergence is observable, and it is found without index arithmetic
settled_at := -1;
for k in 0 to 6 loop
if settled_at < 0 and k + 1 < n3 and p3_low(k) = p3_low(k+1) then
settled_at := k;
end if;
end loop;
chk(settled_at >= 0, "instance 3 never settled -- the pair did not converge");
if settled_at >= 0 then
chk(p3_low(settled_at) = exp_low(LA3, LB3, HA3, HB3),
"instance 3 settled at the wrong low phase");
chk(settled_at > 0,
"instance 3's very first period was already settled -- convergence was not observable");
end if;
-- 5. the shorter-low master waits; the longest-low master does not
chk(nb1 /= 0, "master B (the shorter low period) never entered a wait state");
chk(na1 = 0, "master A (the longest low period) entered a wait state -- it holds the line down");
-- 6. the combined clock is not master B's configuration
chk(not (to_integer(lo1) = LB1 and to_integer(hi1) = HB1),
"the combined clock is exactly master B's configuration");
-- 7. instance 2: B wins BOTH extrema -- the control for test 3
chk(to_integer(lo2) = exp_low(LA2, LB2, HA2, HB2), "instance 2 combined LOW is wrong");
chk(to_integer(hi2) = exp_high(HA2, HB2), "instance 2 combined HIGH is wrong");
chk(na2 /= 0, "instance 2 master A (the shorter low period) never waited");
chk(nb2 = 0, "instance 2 master B (the longest low period) waited");
-- 8. instance 3: ONE extremum each -- the combined clock is neither master's
chk(to_integer(lo3) = exp_low(LA3, LB3, HA3, HB3),
"instance 3 combined LOW is wrong (the extrema belong to DIFFERENT masters here)");
chk(to_integer(hi3) = exp_high(HA3, HB3), "instance 3 combined HIGH is wrong");
chk(not (to_integer(lo3) = LB3 or to_integer(hi3) = HA3),
"instance 3's combined clock matched a single master's configuration");
-- 9. the combined period equals no single master's period
chk(not ((to_integer(lo3) + to_integer(hi3)) = (LA3 + HA3 + 2)
or (to_integer(lo3) + to_integer(hi3)) = (LB3 + HB3 + 2)),
"instance 3's combined period equals a single master's period");
-- 10. removing one master hands the clock back to the other
en_b <= '0';
tick(600);
wait_p1(3);
wait_p1(2);
chk(to_integer(lo1) = LA1 and to_integer(hi1) = HA1 + 1,
"after master B stopped, the line did not return to master A's own clock");
-- 11. disabling both releases the line
en_a <= '0';
tick(600);
chk(l1 = '1', "SCL left low after both masters were disabled -- that is a stuck bus");
if errors = 0 then
report "i2c_scl_sync_pair self-check complete: the combined clock is slowest-low and fastest-high, the shorter-low master waits, the result belongs to neither master, and a departing master stops contributing an extremum" severity note;
else
report "FAILURES in i2c_scl_sync_pair" severity error;
end if;
done <= true;
wait;
end process;
end architecture;6a. Five Decisions Worth Defending
Both masters and the wired-AND are inside one module. §6's argument. The rule is a property of the wire, so the wire is in the design under test.
Each master's low count runs from its own intent and its high count from the observed rise. Chapter 12.2 §3's single decision, now doing double duty: it is what makes a master tolerate stretching and what makes it synchronize. Mutation O1 removes the wait state and the combined low phase collapses to the master's own.
A low line during a master's high phase joins the low phase rather than abandoning the period. §5's table. Mutation O2 is the single-master response, and the failure it produces is instructive: instance 3 settles at a low phase of 80 — master B's, the shortest — because A keeps abandoning periods and only B's clock is ever reported.
The combined phases are measured from the LINE, not from either master's intent. The synchronized clock is a property of the wire, so a measurement taken from a master's own counters would report what that master wanted rather than what the bus did. Mutation O3 shifts the low measurement by one edge.
Three instances, configured so that different masters win the extrema. §7's structure. A suite in which one master wins both extrema every time cannot distinguish max/min from "just use master A", and that is the most plausible wrong implementation.
6b. Verified Execution
$ iverilog -g2012 -o d2 i2c_scl_sync_pair.sv i2c_scl_sync_pair_tb.sv && ./d2
PASS: the combined clock is slowest-low and fastest-high, the shorter-low master waits, the
result belongs to neither master, and a departing master stops contributing an extremum
i2c_scl_sync_pair_tb.sv:286: $finish called at 40780000 (1ps)
$ iverilog -g2005 -o v2 i2c_scl_sync_pair.v i2c_scl_sync_pair_tb.v && ./v2
PASS: the combined clock is slowest-low and fastest-high, the shorter-low master waits, the
result belongs to neither master, and a departing master stops contributing an extremum
i2c_scl_sync_pair_tb.v:298: $finish called at 40780000 (1ps)
$ nvc -a i2c_scl_sync_pair.vhd i2c_scl_sync_pair_tb.vhd
$ nvc -e i2c_scl_sync_pair_tb && nvc -r i2c_scl_sync_pair_tb --stop-time=2000us
** Note: 40780ns+1: i2c_scl_sync_pair self-check complete: the combined clock is slowest-low
and fastest-high, the shorter-low master waits, the result belongs to neither master, and a
departing master stops contributing an extremumAll three at 40780 ns.
7. What the Testbench Proves
Three instances run on the same enables, with the parameters arranged so that each pattern of extremum-ownership is exercised:
| instance | master A | master B | longest low | shortest high |
|---|---|---|---|---|
| 1 | 130 / 60 | 90 / 100 | A | A |
| 2 | 100 / 90 | 200 / 40 | B | B |
| 3 | 150 / 120 | 80 / 50 | A | B |
| # | stimulus | what it establishes |
|---|---|---|
| 1 | reset | the minimum trackers read their maximum; SCL released |
| 2 | master A alone | the line is exactly A's clock — the degenerate case of the rule |
| 3 | both masters, steady state | low is the max, high is the min |
| 4 | the first combined period | differs from the settled one, and settles in one period |
| 5 | the shorter-low master | entered a wait state; the longest-low master did not |
| 6 | the combined clock | is not master B's configuration |
| 7 | instance 2 | the rule holds with B winning both — the control for test 3 |
| 8 | instance 3 | the rule holds with one extremum each |
| 9 | instance 3's period | equals no single master's period |
| 10 | master B stops | the line returns to A's own clock |
| 11 | both disabled | SCL is released, not left low |
Test 7 is the control that makes test 3 mean something. In instance 1, master A wins both extrema — so an implementation that simply used master A's clock and ignored master B would pass test 3 perfectly. Instance 2 inverts the ownership, and instance 3 splits it. Only all three together pin the rule down.
Test 4 is the convergence assertion, and it is written index-independently: it scans the logged periods for the first pair that agree, requires that settled value to match the combining rule, and requires at least one earlier period to have differed. Reset leaves a partial period in the log, so counting entries from a fixed offset is exactly the stale-index trap Chapter 11.6 §5 records for tBUF.
Test 5 is §3's wait state, asserted from both sides. The shorter-low master must wait; the longest-low master must not — because it is the one holding the line down and has nothing to wait for. A design that put every master into a wait state would pass the first half and fail the second.
Test 9 is worth more than it looks. It asserts that instance 3's combined period equals neither master's period, which is §2's headline claim stated as a check rather than as prose. If the combined clock ever matched a single master's configuration in that instance, the rule would not be doing anything.
Test 11 guards the module's worst failure. Two masters disabled mid-low-phase would leave SCL held down by both, which is the stuck-clock condition Chapter 12.4 §3 shows has no protocol-level recovery.
8. Mutation Testing
Five defects injected into the SystemVerilog pair.
| # | injected defect | outcome |
|---|---|---|
| O1 | master A's wait state is skipped, so its high count starts at its own release | killed — tests 3 and 7 |
| O2 | a low line during A's high phase abandons the period (single-master behaviour) | killed — test 8 |
| O3 | the combined low is measured to the wrong edge | killed — test 2 |
| O4 | the minimum trackers start at zero | killed — test 1 |
| O5 | master B never reports entering a wait state | killed — test 5 |
Five injected, five killed. Three worth recording.
O1 is the mutation that proves the wait state is load-bearing. Replacing its condition with an unconditional pass makes each master proceed to its high phase the instant it releases, regardless of the line. The failure message is instance 2 combined LOW = 203, expected 200 — and notice that the error is small. It is not a collapse; it is a three-tick discrepancy, because master A releases early and its next pull-down arrives slightly out of step. A suite checking the low phase against a range would have missed it entirely, which is why §7 asserts exact values.
O2 produces the most informative failure in the module. Making a low line abandon the period gives instance 3 settled at low=80, expected 151 — and 80 is master B's low period, the shortest. Master A abandons every period it is in, so only B's clock is ever reported, and the combining rule appears to have inverted from max to min. A designer reading that symptom alone would very plausibly conclude the rule was backwards rather than that a branch was wrong.
O5 is not a functional defect at all — the synchronization is perfectly correct with it in place. What it removes is the evidence: n_b_waits stays at zero, so nothing records that master B ever deferred. On a real bus that count is how you discover which master is dominating the low phase, and a design without it is one you cannot characterise. The same argument Chapter 11.8 §8 makes for its spike counter: a filter you cannot audit is a filter you cannot trust.
9. Verification Connection — Asserting an Extremum
// The rule is two extrema, so it is two properties, and they are NOT symmetrical -- which is the
// whole point. Each one follows from a different half of the release-versus-command asymmetry.
// The low phase is a MAXIMUM: the line cannot rise until every master has released, so the
// observed low phase is at least as long as every participant's own low count.
property p_low_is_the_maximum;
@(posedge clk) $rose(scl_line) |-> (t_low_obs >= T_LOW_A && t_low_obs >= T_LOW_B);
endproperty
assert property (p_low_is_the_maximum)
else $error("the observed low phase was shorter than a master's own low period -- the line rose while somebody was still holding it");
// The high phase is a MINIMUM: one master pulling low ends it, so the observed high phase is at
// most the shortest participant's high count. Note the inequality points the other way.
property p_high_is_the_minimum;
@(posedge clk) $fell(scl_line) |-> (t_high_obs <= T_HIGH_A + 1 && t_high_obs <= T_HIGH_B + 1);
endproperty
assert property (p_high_is_the_minimum)
else $error("the observed high phase outlasted the shortest master's high period -- somebody failed to pull the line low");
// A master that RELEASED must not be driving. Trivial, and it is the property that catches a
// generator which treats its own release as having taken effect -- the mutation O1 shape.
property p_release_means_released;
@(posedge clk) (a_waiting) |-> !a_drive_low;
endproperty
assert property (p_release_means_released)
else $error("a master reported waiting while still driving SCL low");
// The COMPLIANCE property, and it is the one that matters on a real bus: synchronization can only
// SHORTEN the high phase, so tHIGH(min) has to be met by the FASTEST master, not by this one.
// Section 2's hazard, as an assertion that fires on the bus rather than on a datasheet.
property p_combined_high_still_legal;
@(posedge clk) $fell(scl_line) |-> (t_high_obs >= T_HIGH_MIN);
endproperty
assert property (p_combined_high_still_legal)
else $error("the SYNCHRONIZED high phase violated tHIGH(min) -- check the fastest master, not this one");
// And the negative property. A stretch or a slow rise lengthens the low phase, which is always
// compliant, so a low-phase measurement must never be reported as a violation here.
property p_long_low_is_never_a_violation;
@(posedge clk) (t_low_obs > T_LOW_MIN) |-> !viol_low;
endproperty
assert property (p_long_low_is_never_a_violation)
else $error("a long synchronized low phase was reported as a violation -- tLOW has no maximum"); covergroup i2c_sync_cg with function sample(int t_low_obs, int t_high_obs,
int la, int ha, int lb, int hb,
int n_a_waits, int n_b_waits, int period_index);
// THE coverpoint for this chapter, and the one a naive suite leaves three-quarters empty:
// WHICH master won each extremum. A suite where one master always wins both cannot
// distinguish the combining rule from "use master A", which is mutation O1's territory.
ownership: coverpoint {(la > lb), (ha < hb)} {
bins a_both = {2'b11}; // A has the longest low AND the shortest high
bins b_both = {2'b00};
bins split_a_low = {2'b10}; // the interesting case: the clock is neither master's
bins split_b_low = {2'b01};
}
// How FAR apart the two masters are. Nearly-equal clocks are a distinct regime: the wait state
// is one or two cycles and the convergence transient is almost invisible, so a suite of
// similar masters exercises the rule without exercising its consequences.
disparity: coverpoint ((la > lb) ? (la - lb) : (lb - la)) {
bins identical = {0};
bins close = {[1:20]};
bins moderate = {[21:100]};
bins extreme = {[101:$]};
}
ownership_x_disparity: cross ownership, disparity;
// CONVERGENCE, which only exists in the first period or two after a master joins. Sampling
// only the steady state misses the process the chapter is named after.
phase_of_life: coverpoint period_index {
bins first_combined = {0};
bins second = {1};
bins settled = {[2:$]};
}
ownership_x_life: cross ownership, phase_of_life;
// Who actually deferred. This is the evidence mutation O5 removes, and covering it is what
// lets a report say which master dominated the low phase.
deferral: coverpoint {(n_a_waits > 0), (n_b_waits > 0)} {
bins neither = {2'b00}; // only one master participating
bins a_waited = {2'b10};
bins b_waited = {2'b01};
bins both = {2'b11}; // reachable only if ownership changes mid-run
}
endgroup10. FPGA and ASIC Implications
The pair is two four-state machines, four counters and a measurement block — around 200 flops at TICK_W = 20. In a real system the two masters are in different devices, so what ships is half of this: one state machine, and the discipline to sequence it on the observed line.
The compliance hazard is the high phase, and it belongs to the fastest master. §2's second consequence deserves restating as a design rule, because it is counter-intuitive:
On a multi-master bus,
tHIGH(min)must be met by the master with the shortest high phase, and that master may not be yours.
A master configured with a comfortable 1.0 µs high phase sharing a bus with one configured at 0.62 µs gets 0.62 µs, and if that second master is marginal against Fast-mode's 0.6 µs then every device on the bus is receiving a marginal clock — including devices that only ever talk to the compliant master. There is no way for the compliant master to detect this from its own configuration, and Chapter 11.2's envelope checker instantiated on the bus is the only thing that will report it.
The low phase is never a hazard, which is worth stating so nobody spends effort on it. Synchronization only lengthens it, and tLOW has a minimum with no maximum — the same one-sidedness that makes clock stretching legal.
Sizing: the low phase can be far longer than any single master's. With N masters the observed low phase is the maximum over all of them, so a counter sized for one master's worst case is undersized. It is the same conclusion Chapter 12.1 §10 reaches for stretching, and on a bus with both effects present they compound: the observed low phase is the maximum over every master's low count and every slave's stretch.
And a generator that can be disabled must release SCL. Two masters both parked low is the one fault on this bus with no protocol-level remedy, and it costs one line in the reset path to prevent.
11. Debugging — The Bus That Ran at a Frequency Neither Master Had Configured
Pitfall — expecting the combined clock to be one of the two configured clocks
// A board with two masters, both correct, both reading back SDA and SCL, both implementing
// arbitration. Multi-master done properly -- and then configured like this:
//
// // Application processor: Fast-mode, comfortable margins.
// localparam APP_LOW = 150; // 1.50 us at 100 MHz
// localparam APP_HIGH = 100; // 1.00 us -> 400 kHz, both minima clear easily
//
// // Management controller: also "Fast-mode", configured independently by a different team
// // from the same datasheet, and also compliant on its own.
// localparam BMC_LOW = 131; // 1.31 us -- just above tLOW(min) of 1.3 us
// localparam BMC_HIGH = 62; // 0.62 us -- just above tHIGH(min) of 0.6 us
// // -> 518 kHz on its own. Too fast for Fast-mode,
// // but nobody had checked the FREQUENCY, only
// // the two phase minima.
//
// Both configurations were reviewed. Both were signed off against Table 10's tLOW and tHIGH minima.
// Neither review computed the combined clock, because nothing in either datasheet mentions it.Intermittent read failures from one temperature sensor, at roughly one transfer in eighty. Only that sensor, only reads, and only when both masters were active -- with the management controller idle the bus was flawless for hours.
So the management controller was suspected, and its own transfers were examined first. They were fine: every byte it read was correct, every time. It was the APPLICATION processor's transfers that failed, and only while the management controller was also running.
That pointed at arbitration, and a great deal of effort went there. Both masters were confirmed to read SDA back, to detect loss correctly, and to retry. Captures showed arbitration resolving cleanly whenever it occurred -- and the failing transfers were mostly ones with no contest at all.
What resolved it was measuring the CLOCK rather than the data. With both masters active, SCL measured 1.51 us low and 0.63 us high: a 470 kHz clock with a 29 % duty cycle.
Neither master had configured that. The application processor wanted 150/100 and the management controller wanted 131/62, and section 3.1.7's rule gives:
observed low = max(150, 131) = 150 (+1 observation latency = 151) observed high = min(100, 62) = 62 (+1 = 63)
The low phase came from the application processor and the high phase from the management controller. The combined clock belonged to neither of them, and at 0.63 us the high phase was only 30 ns above Fast-mode's 0.6 us minimum -- against a sensor whose input filter and setup requirements consumed most of that.
The failing device was simply the one with the least tHIGH margin, and it was being handed a high phase set by a master it never exchanged a byte with.
Two independently compliant masters produce a combined clock that is compliant with neither of their configurations, and the combination can be far closer to a limit than either input.
Section 3.1.7's rule takes the MAXIMUM of the low periods and the MINIMUM of the high periods. So the high phase -- the one direction that can violate a Table 10 minimum -- is always set by the FASTEST master on the bus. A master with a generous 1.0 us high phase gets whatever the tightest participant configured, and has no way to discover this from its own settings.
The management controller's configuration was the real fault: 131/62 gives 518 kHz, which exceeds Fast-mode's 400 kHz. Its review had checked tLOW and tHIGH against Table 10 and never checked the FREQUENCY, which is exactly the three-constraint error Chapter 11.2 section 2 is about -- a legal tLOW and a legal tHIGH do not make a legal clock.
And the investigation went to arbitration because the symptom appeared only when both masters were active, which is also true of synchronization. Both masters being present is the precondition for both mechanisms, so the symptom does not distinguish them -- but the failing transfers were uncontested, which should have ruled arbitration out on day one.
12. Common Misconceptions
"The masters negotiate a clock." Nothing negotiates. The wired-AND produces the combination, and no logic anywhere computes it.
"The bus runs at the faster master's rate." It runs at a rate assembled term by term: the longest low phase and the shortest high phase, which is generally neither master's period.
"The bus runs at the slower master's rate." Also no — the high phase comes from the faster master. Only the low phase comes from the slower one.
"Synchronization can violate tLOW." It can only lengthen the low phase, and tLOW has a minimum with no maximum. It is the high phase that can become non-compliant.
"My master's tHIGH margin is my own business." On a multi-master bus the shortest high phase wins, so the tightest master sets tHIGH for every device — including ones it never addresses.
"A master that released SCL has ended its low phase." It has requested that the phase end. The line stays low until every master has released, and the requesting master sits in the wait state §3.1.7 names.
"A low line during my high phase is an error." On a single-master bus, yes. On a multi-master bus it means another master finished first, which §3.1.7 explicitly permits — and treating it as an error drops almost every period.
"Synchronization is steady-state arithmetic." The arithmetic is the steady state; the synchronization is the transient in which two offset clocks converge, which takes about one period and is the thing the word names.
13. Reason It Through
Why is the low phase a maximum and the high phase a minimum?
Because ending the low phase requires every master to release, and a release is a request the wired-AND may decline — so the last one to let go decides. Ending the high phase requires only one master to pull low, and a pull-down is a command nobody can override — so the first one to finish decides. Same mechanism, opposite direction, because the two phases end by opposite kinds of action.
Master A is configured 150/120 and master B 80/50. What does the bus run at, and whose clock is it?
Low 150 (A's), high 50 (B's), period 200. It is neither master's clock: A wanted 270 and B wanted 130.
Which Table 10 minimum can synchronization violate, and whose configuration decides it?
tHIGH(min), because the high phase is shortened to the minimum over all masters — so it is decided by the fastest master on the bus, which may be one you have no control over. tLOW(min) cannot be violated, because the low phase only ever gets longer.
A multi-master generator drops almost every period. What single branch is likely wrong?
The one handling a low line during its own high phase. If it abandons the period — correct single-master behaviour — then on a two-master bus it abandons every period another master ends, which is most of them. It should instead treat the low line as the high phase having ended and join the low phase.
Why must a convergence test require the first combined period to differ from the settled one?
Because if every period were identical the test would not be exercising synchronization at all — only the steady-state arithmetic. The transient in which two offset clocks converge is the process the word names, and asserting that it happened is what distinguishes a suite that tested it from one that ran two masters.
"It only fails when both masters are active." Why does that not point at arbitration?
Because it is equally the precondition for synchronization, and the two mechanisms are the only things that require two masters. The discriminator is whether the failing transfers were contested: uncontested failures rule arbitration out and leave the clock.
14. Understanding Check
15. Summary
Two sentences of §3.1.7 give the whole rule. The line is held low by the master with the longest low period; the first master to complete its high period pulls it low again. So the low phase is a maximum and the high phase a minimum.
The two extrema point in opposite directions because the two phases end by opposite actions — a release, which the wired-AND may decline, versus a pull-down, which it cannot.
The synchronized clock belongs to neither master, and its period is generally neither of theirs.
Only the high phase is a compliance hazard, and the fastest master decides it. The low phase only lengthens, which is always legal.
Synchronization names the transient, not the arithmetic. Masters that join part-way through have not seen the falling edge everyone counts from, so the first combined period is irregular and the pair converges in about one period.
One branch differs between a single-master and a multi-master generator: what a low line during your own high phase means. Everything else was already required to tolerate clock stretching.
And a master that released has not ended the low phase — it has asked to, and it waits.
16. What Comes Next
The two masters now share a clock. Neither knows the other exists, and both are about to put an address on SDA.
Chapter 13.3 is where the contest is decided, and it is decided by the same wire that just built the clock — with no arbiter, no priority and no protocol exchange. The mechanism is a single asymmetric test each master performs on itself, once per bit:
I sent a one. Is the line low?
If it is, somebody else is pulling it down, and that somebody wins. If it is not, nothing is known — which is why the test is asymmetric and why sending a zero is never evidence of anything.
The chapter also settles what "no information is lost" actually means, because it is routinely stated in a form that is wrong: the bus does not carry the bitwise AND of the two messages. It carries the winner's message, unchanged — and there is a pair of bytes for which those two readings differ in every bit but one.
Continue learning
Related tutorials
- Related topic
Repeated START in Practice — Why Not STOP Then START
Two sequences that look nearly identical in a driver's source are completely different on the wire. This chapter measures the difference, builds the passive monitor that tells them apart from two wires alone, and names exactly what a STOP costs on a shared bus.
- Related topic
tSU;STA and tHD;STA — START and Repeated-START Margins
The first parameters measured between two different signals rather than against a clock edge — and the timing-level reason a repeated START is not simply a START in the middle of a transfer.
- Related topic
The I²C Stretching Mechanism — Holding SCL Low
Stretching needed no new mechanism: the specification already described it for multi-master synchronization. One sentence decides whether a master survives it — and getting it wrong collapses the high phase on the bit a stretch ended.
- Related topic
I²C SDA Arbitration — Wired-AND Decides Bit by Bit
Arbitration with no arbiter, no priority and no protocol exchange — resolved by one asymmetric test each master performs on itself. Settles what 'no information is lost' actually means.
