I²C · Module 18
Sampling SDA and SCL — Edge Detection Inside a Slave
The block every other one in a target is written against: two asynchronous wires turned into five synchronous facts. Three of its decisions produce defects nothing later can recover from — what the samplers reset to, which signals the edge pulses come from, and why the synchroniser belongs here and nowhere else.
Chapter 18.1 argued that a target's architecture is observation-driven: every time base advances on something it has just seen. This chapter builds the seeing.
It is a short block — four flops and six continuous assignments — with a long argument, because it is the only block in the module that touches a pin. Everything above it inherits whatever it gets wrong.
1. Why a Target Needs This, and a Master Does Not in the Same Way
Module 17's master read the lines back too. But it always knew what it meant to do, and it owned the clock — so it knew when a bit slot began. It read the bus to check whether its intent had taken effect: §3.1.6's stretch, §3.1.8's arbitration.
A target knows neither. So this block is the target's entire sense of time, and everything above it is a reaction to what this block reports.
2. The Event Set, and Why It Is Only Five Things
Everything else a target needs is one of those combined with a level:
| A target needs to know | Which is |
|---|---|
| a received bit is valid | scl_rise — §3.1.2 makes SDA stable only while SCL is HIGH |
| SDA may be changed | after scl_fall — the only phase §3.1.2 permits it |
| a START happened | sda_fall while scl_q — 18.3 |
| a STOP happened | sda_rise while scl_q — 18.3 |
That last pair is the reason the levels and the edges are both outputs. Framing is an SDA edge qualified by an SCL level, so a front end that reported only edges would make framing undetectable, and one that reported only levels would make it ambiguous.
3. Decision One — What the Samplers Reset To
The idle bus is both lines high, so that is the only reset value that cannot manufacture an event. Mutation M1 below does exactly this and fails seventeen checks.
4. Decision Two — Which Signals the Edge Pulses Come From
The pulses are computed from the settled value and a delay register, not from the raw pin against the first flop:
scl_rise = scl_sync[0] & ~scl_d both operands have been through the full chainThe delay register is a third stage, after the two-deep chain. Both operands of every comparison are therefore values that have already been stable for a full cycle. Mutations M2 and M6 attack this from either side.
5. Decision Three — Why the Synchroniser Lives Here
There is one pad per line, so there must be one synchroniser per line.
Two consumers with their own synchronisers can disagree about what the bus did — one sees the edge a cycle before the other — and a protocol engine built on two inconsistent views of the same wire has a failure mode no block-level test can reach. Chapter 17.12 §8 made the same argument at the master's top level.
6. What This Chapter Does Not Own
This module uses exactly one consequence of that contract, and §7 is it.
7. Every Event Arrives Late, and the Direction Matters
The pin, the synchronised level, and the one-cycle pulses
10 cyclesTwo cycles, both times. That is SYNC_DEPTH, and it is not an implementation detail — it is part of every later timing argument in the module:
Late is safe for receiving. A bit sampled two cycles after SCL rose is still inside the high phase, provided the system clock is fast enough that two cycles are a small fraction of it. The bit was stable for the whole phase, so a late sample reads the same value.
Late eats into the acknowledge window. Chapter 18.5 must assert SDA inside one SCL low phase, and it only learns the phase began two cycles after it did. The synchroniser latency plus the decision must fit — which is why 18.1 §8 called the clock ratio a design constraint rather than a free choice.
Late is harmless for framing. A START detected two cycles late is still detected, and nothing in the protocol requires a target to respond to one within a bounded time — the master's next act is to clock, which takes a whole phase.
8. The Front End, in Three Languages
// -----------------------------------------------------------------------------
// i2c_slave_sync.sv
// The sampling front end: two asynchronous bus lines into one synchronous event set.
//
// WHY A SLAVE NEEDS THIS AND A MASTER DOES NOT, in the same way. Module 17's master
// reads the lines back to learn whether its own intent took effect -- §3.1.6's stretch,
// §3.1.8's arbitration -- but it always knows what it MEANT to do, and it owns the
// clock, so it knows when a bit slot begins. A slave knows neither. Every event that
// structures a transfer is produced by a device somewhere else on the board, and the
// only evidence of any of it is two wires.
//
// So this block is the slave's entire sense of time, and everything above it is a
// reaction to what this block reports.
//
// THE EVENT SET, and it is deliberately small. Five facts, from which every later
// chapter's logic is built:
//
// scl_q / sda_q the synchronised levels -- what the lines ARE
// scl_rise / scl_fall one-cycle pulses -- SCL changed
// sda_rise / sda_fall one-cycle pulses -- SDA changed
//
// Everything else a slave needs is a combination of those with a level:
//
// a data bit is valid at scl_rise (§3.1.2)
// SDA may be changed after scl_fall (§3.1.2)
// START = sda_fall while scl_q (Chapter 18.3)
// STOP = sda_rise while scl_q (Chapter 18.3)
//
// WHY THE RESYNCHRONISER IS HERE AND NOT IN EVERY CONSUMER. There is one pad per line,
// so there must be one synchroniser per line. Two consumers with their own
// synchronisers can disagree about what the bus did -- one sees the edge a cycle before
// the other -- and a protocol engine built on two inconsistent views of the same wire
// has a failure mode no block-level test can reach. Module 17 §17.12 made the same
// argument at its top level.
//
// WHAT THIS CHAPTER DOES NOT OWN. The DEPTH of the synchroniser, the metastability
// argument behind it, spike filtering and oversampling ratios all belong to Module 19
// (19.4 and 19.5). Here the two flops are an interface CONTRACT -- the lines arrive
// already synchronised and the edges are already clean -- and the only fact this module
// needs from it is the consequence in §7: every event reaches the protocol engine LATE,
// and lateness has a direction that is safe for some obligations and not for others.
//
// SYNCHRONISER-DRIVEN EDGE DETECTION, not level comparison against a raw pin. The
// edge pulses are computed from the SECOND flop and a third delay register, so a pulse
// is only ever produced from a value that has already been stable for a full cycle. A
// design that compared the raw pin against its first flop would emit an edge pulse
// derived from a possibly-metastable sample, which is a one-cycle spurious START on a
// quiet bus -- rare, unreproducible, and fatal.
// -----------------------------------------------------------------------------
module i2c_slave_sync #(
// Synchroniser depth. TWO is the contract this module is written against; Module
// 19.4 owns the argument for the number. Exposed so an integrator can deepen it
// without editing logic, and so a testbench can show the latency is a parameter.
parameter int SYNC_DEPTH = 2
) (
input logic clk,
input logic rst_n,
// The bus, straight off the pads. Asynchronous to clk: that is the whole problem.
input logic scl_pin,
input logic sda_pin,
// The synchronised levels -- what the lines ARE, as far as this slave can know.
output logic scl_q,
output logic sda_q,
// One-cycle events. Exactly one cycle, which every consumer relies on: a two-cycle
// "edge" makes a bit counter advance twice and is the most common cause of a slave
// that loses a bit somewhere in the middle of a byte.
output logic scl_rise,
output logic scl_fall,
output logic sda_rise,
output logic sda_fall
);
// The synchroniser chain, plus one extra delay stage for edge detection. The chain
// is SYNC_DEPTH deep; sync[SYNC_DEPTH-1] is the settled value and `*_d` is that
// value one cycle ago.
logic [SYNC_DEPTH-1:0] scl_sync, sda_sync;
logic scl_d, sda_d;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// Reset to the IDLE bus, both lines high. Resetting to zero would make the
// first cycle after release look like a pair of rising edges on a bus that
// never moved -- and a rising edge on SDA while SCL is high is a STOP, so a
// slave resetting to zero announces a STOP to itself on every reset.
scl_sync <= {SYNC_DEPTH{1'b1}};
sda_sync <= {SYNC_DEPTH{1'b1}};
scl_d <= 1'b1;
sda_d <= 1'b1;
end else begin
scl_sync <= {scl_pin, scl_sync[SYNC_DEPTH-1:1]};
sda_sync <= {sda_pin, sda_sync[SYNC_DEPTH-1:1]};
scl_d <= scl_sync[0];
sda_d <= sda_sync[0];
end
end
assign scl_q = scl_sync[0];
assign sda_q = sda_sync[0];
// Edges from the SETTLED value against its own delayed copy. Both operands have
// been through the full chain, so neither can be a metastable sample.
assign scl_rise = scl_sync[0] & ~scl_d;
assign scl_fall = ~scl_sync[0] & scl_d;
assign sda_rise = sda_sync[0] & ~sda_d;
assign sda_fall = ~sda_sync[0] & sda_d;
endmodule // -----------------------------------------------------------------------------
// i2c_slave_sync.v
// The sampling front end: two asynchronous bus lines into one synchronous event set.
//
// WHY A SLAVE NEEDS THIS AND A MASTER DOES NOT, in the same way. Module 17's master
// reads the lines back to learn whether its own intent took effect -- §3.1.6's stretch,
// §3.1.8's arbitration -- but it always knows what it MEANT to do, and it owns the
// clock, so it knows when a bit slot begins. A slave knows neither. Every event that
// structures a transfer is produced by a device somewhere else on the board, and the
// only evidence of any of it is two wires.
//
// So this block is the slave's entire sense of time, and everything above it is a
// reaction to what this block reports.
//
// THE EVENT SET, and it is deliberately small. Five facts, from which every later
// chapter's logic is built:
//
// scl_q / sda_q the synchronised levels -- what the lines ARE
// scl_rise / scl_fall one-cycle pulses -- SCL changed
// sda_rise / sda_fall one-cycle pulses -- SDA changed
//
// Everything else a slave needs is a combination of those with a level:
//
// a data bit is valid at scl_rise (§3.1.2)
// SDA may be changed after scl_fall (§3.1.2)
// START = sda_fall while scl_q (Chapter 18.3)
// STOP = sda_rise while scl_q (Chapter 18.3)
//
// WHY THE RESYNCHRONISER IS HERE AND NOT IN EVERY CONSUMER. There is one pad per line,
// so there must be one synchroniser per line. Two consumers with their own
// synchronisers can disagree about what the bus did -- one sees the edge a cycle before
// the other -- and a protocol engine built on two inconsistent views of the same wire
// has a failure mode no block-level test can reach. Module 17 §17.12 made the same
// argument at its top level.
//
// WHAT THIS CHAPTER DOES NOT OWN. The DEPTH of the synchroniser, the metastability
// argument behind it, spike filtering and oversampling ratios all belong to Module 19
// (19.4 and 19.5). Here the two flops are an interface CONTRACT -- the lines arrive
// already synchronised and the edges are already clean -- and the only fact this module
// needs from it is the consequence in §7: every event reaches the protocol engine LATE,
// and lateness has a direction that is safe for some obligations and not for others.
//
// SYNCHRONISER-DRIVEN EDGE DETECTION, not level comparison against a raw pin. The
// edge pulses are computed from the SECOND flop and a third delay register, so a pulse
// is only ever produced from a value that has already been stable for a full cycle. A
// design that compared the raw pin against its first flop would emit an edge pulse
// derived from a possibly-metastable sample, which is a one-cycle spurious START on a
// quiet bus -- rare, unreproducible, and fatal.
// (Verilog-2001 -- structurally identical to the SystemVerilog above.)
// -----------------------------------------------------------------------------
module i2c_slave_sync #(
// Synchroniser depth. TWO is the contract this module is written against; Module
// 19.4 owns the argument for the number. Exposed so an integrator can deepen it
// without editing logic, and so a testbench can show the latency is a parameter.
parameter integer SYNC_DEPTH = 2
) (
input wire clk,
input wire rst_n,
// The bus, straight off the pads. Asynchronous to clk: that is the whole problem.
input wire scl_pin,
input wire sda_pin,
// The synchronised levels -- what the lines ARE, as far as this slave can know.
output wire scl_q,
output wire sda_q,
// One-cycle events. Exactly one cycle, which every consumer relies on: a two-cycle
// "edge" makes a bit counter advance twice and is the most common cause of a slave
// that loses a bit somewhere in the middle of a byte.
output wire scl_rise,
output wire scl_fall,
output wire sda_rise,
output wire sda_fall
);
// The synchroniser chain, plus one extra delay stage for edge detection. The chain
// is SYNC_DEPTH deep; sync[SYNC_DEPTH-1] is the settled value and `*_d` is that
// value one cycle ago.
reg [SYNC_DEPTH-1:0] scl_sync, sda_sync;
reg scl_d, sda_d;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// Reset to the IDLE bus, both lines high. Resetting to zero would make the
// first cycle after release look like a pair of rising edges on a bus that
// never moved -- and a rising edge on SDA while SCL is high is a STOP, so a
// slave resetting to zero announces a STOP to itself on every reset.
scl_sync <= {SYNC_DEPTH{1'b1}};
sda_sync <= {SYNC_DEPTH{1'b1}};
scl_d <= 1'b1;
sda_d <= 1'b1;
end else begin
scl_sync <= {scl_pin, scl_sync[SYNC_DEPTH-1:1]};
sda_sync <= {sda_pin, sda_sync[SYNC_DEPTH-1:1]};
scl_d <= scl_sync[0];
sda_d <= sda_sync[0];
end
end
assign scl_q = scl_sync[0];
assign sda_q = sda_sync[0];
// Edges from the SETTLED value against its own delayed copy. Both operands have
// been through the full chain, so neither can be a metastable sample.
assign scl_rise = scl_sync[0] & ~scl_d;
assign scl_fall = ~scl_sync[0] & scl_d;
assign sda_rise = sda_sync[0] & ~sda_d;
assign sda_fall = ~sda_sync[0] & sda_d;
endmodule -- -----------------------------------------------------------------------------
-- i2c_slave_sync.vhd
-- The sampling front end, in VHDL. Behaviourally identical to the SystemVerilog and
-- Verilog versions: same ports, same generic, same reset values, same event timing.
-- -----------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_slave_sync is
generic (
-- Synchroniser depth. TWO is the contract; Module 19.4 owns the argument for the
-- number. `positive` refuses a zero-deep chain at elaboration.
SYNC_DEPTH : positive := 2
);
port (
clk : in std_logic;
rst_n : in std_logic;
-- The bus, straight off the pads. Asynchronous to clk.
scl_pin : in std_logic;
sda_pin : in std_logic;
-- The synchronised levels.
scl_q : out std_logic;
sda_q : out std_logic;
-- One-cycle events. Exactly one cycle, which every consumer relies on.
scl_rise : out std_logic;
scl_fall : out std_logic;
sda_rise : out std_logic;
sda_fall : out std_logic
);
end entity i2c_slave_sync;
architecture rtl of i2c_slave_sync is
-- The chain plus one delay stage for edge detection. Index 0 is the settled value,
-- matching the SystemVerilog's scl_sync[0].
signal scl_sync, sda_sync : std_logic_vector(SYNC_DEPTH-1 downto 0)
:= (others => '1');
signal scl_d, sda_d : std_logic := '1';
begin
process (clk, rst_n)
begin
if rst_n = '0' then
-- Reset to the IDLE bus, both lines high. Resetting to zero would make the
-- first released cycle look like a pair of rising edges on a bus that never
-- moved -- and SDA rising while SCL is high is a STOP.
scl_sync <= (others => '1');
sda_sync <= (others => '1');
scl_d <= '1';
sda_d <= '1';
elsif rising_edge(clk) then
scl_sync <= scl_pin & scl_sync(SYNC_DEPTH-1 downto 1);
sda_sync <= sda_pin & sda_sync(SYNC_DEPTH-1 downto 1);
scl_d <= scl_sync(0);
sda_d <= sda_sync(0);
end if;
end process;
scl_q <= scl_sync(0);
sda_q <= sda_sync(0);
-- Edges from the SETTLED value against its own delayed copy, so neither operand can
-- be a metastable sample.
scl_rise <= scl_sync(0) and not scl_d;
scl_fall <= (not scl_sync(0)) and scl_d;
sda_rise <= sda_sync(0) and not sda_d;
sda_fall <= (not sda_sync(0)) and sda_d;
end architecture rtl;8a. The testbenches
Twelve checks. The bench drives the two pins asynchronously to clk — changing them between clock edges, which is what a real bus does — and every wait is bounded.
| # | Test | Property |
|---|---|---|
| T1 | reset presents an idle bus | not a pair of edges |
| T2 | a quiet bus produces nothing | the false-positive test |
| T3 | one edge is one pulse, one cycle wide | not two, and not a level |
| T4 | the level and the event agree | the pulse cannot drift from the line |
| T5 | the two lines are independent, both ways | including that SCL activity yields no SDA events |
| T6 | two edges in one cycle are both reported | a START is an SDA edge against an SCL level |
| T7 | the latency is the synchroniser depth | and the level is synchronised too |
| T8 | eight clock pulses give eight rises and eight falls | the shape of every byte |
| T9 | levels and events never disagree | whatever the pin did |
| T10 | reset mid-activity returns to the idle view | without inventing edges |
Two of these repay attention.
T3's width check is the one that protects every consumer. A two-cycle "edge" makes a bit counter advance twice, and it is invisible to a test that only counts events — so the bench watches for a pulse being high two cycles running, separately from counting them.
T7 measures a latency, and measuring one has a trap. @(posedge clk) returns in the active region, before the non-blocking updates of that edge have been applied — so a loop that tests the pulse there sees the previous cycle's value and reports one cycle too many. The bench settles with #1 first. The VHDL version needs wait for 1 ns for the same reason, and without it the three languages disagree by one.
// -----------------------------------------------------------------------------
// i2c_slave_sync_tb.sv
// Independent oracle for i2c_slave_sync.
//
// The bench drives the two pins directly and asynchronously to clk -- changing them
// between clock edges, which is what a real bus does -- and checks the event set the
// rest of the slave is built on. Every wait is bounded.
// -----------------------------------------------------------------------------
`timescale 1ns/1ps
module i2c_slave_sync_tb;
localparam integer SD = 2;
logic clk = 1'b0, rst_n = 1'b0;
logic scl_pin = 1'b1, sda_pin = 1'b1;
logic scl_q, sda_q, scl_rise, scl_fall, sda_rise, sda_fall;
integer errors = 0;
integer n, k;
// ---- independent observers -------------------------------------------------
//
// Event counters, plus a check that no pulse is ever wider than one cycle. The
// width check is the one that matters most: a two-cycle "edge" makes every consumer
// count twice, and it is invisible to a test that only counts events.
integer n_scl_r = 0, n_scl_f = 0, n_sda_r = 0, n_sda_f = 0;
integer wide = 0;
logic scl_r_d = 1'b0, scl_f_d = 1'b0, sda_r_d = 1'b0, sda_f_d = 1'b0;
always @(posedge clk) begin
if (rst_n) begin
if (scl_rise) n_scl_r <= n_scl_r + 1;
if (scl_fall) n_scl_f <= n_scl_f + 1;
if (sda_rise) n_sda_r <= n_sda_r + 1;
if (sda_fall) n_sda_f <= n_sda_f + 1;
if ((scl_rise & scl_r_d) | (scl_fall & scl_f_d) |
(sda_rise & sda_r_d) | (sda_fall & sda_f_d)) wide <= wide + 1;
end
scl_r_d <= scl_rise; scl_f_d <= scl_fall;
sda_r_d <= sda_rise; sda_f_d <= sda_fall;
end
i2c_slave_sync #(.SYNC_DEPTH(SD)) dut (
.clk(clk), .rst_n(rst_n), .scl_pin(scl_pin), .sda_pin(sda_pin),
.scl_q(scl_q), .sda_q(sda_q),
.scl_rise(scl_rise), .scl_fall(scl_fall),
.sda_rise(sda_rise), .sda_fall(sda_fall));
always #5 clk = ~clk;
task step; begin @(posedge clk); @(negedge clk); end endtask
task do_reset;
begin
@(negedge clk); rst_n = 1'b0; scl_pin = 1'b1; sda_pin = 1'b1;
n_scl_r = 0; n_scl_f = 0; n_sda_r = 0; n_sda_f = 0; wide = 0;
step; step;
@(negedge clk); rst_n = 1'b1;
step;
end
endtask
task ck_bit (input [200*8:1] what, input g, input e);
begin
if (g !== e) begin
$display(" FAIL %0s: got %0b expected %0b", what, g, e);
errors = errors + 1;
end
end
endtask
task ck_int (input [200*8:1] what, input integer g, input integer e);
begin
if (g !== e) begin
$display(" FAIL %0s: got %0d expected %0d", what, g, e);
errors = errors + 1;
end
end
endtask
initial begin
$display("=== i2c_slave_sync: two asynchronous lines into one event set ===");
// ----------------------------------------------------------------
// T1. RESET PRESENTS AN IDLE BUS. Both levels high and no events. A slave that
// reset its samplers to zero would see a pair of rising edges on its first
// released cycle -- and SDA rising while SCL is high is a STOP, so it would
// announce a STOP to itself out of reset, on a bus that never moved.
// ----------------------------------------------------------------
do_reset;
$display("T1 reset presents an idle bus, not a pair of edges");
ck_bit("T1 SCL reads high", scl_q, 1'b1);
ck_bit("T1 SDA reads high", sda_q, 1'b1);
ck_int("T1 no SCL rises", n_scl_r, 0);
ck_int("T1 no SCL falls", n_scl_f, 0);
ck_int("T1 no SDA rises", n_sda_r, 0);
ck_int("T1 no SDA falls", n_sda_f, 0);
// ----------------------------------------------------------------
// T2. A QUIET BUS PRODUCES NOTHING, for as long as you care to wait. The
// false-positive test: if this block invents events on an idle bus, every
// framing result above it is worthless.
// ----------------------------------------------------------------
do_reset;
for (k = 0; k < 40; k = k + 1) step;
$display("T2 a quiet bus produces no events at all");
ck_int("T2 still no events", n_scl_r + n_scl_f + n_sda_r + n_sda_f, 0);
// ----------------------------------------------------------------
// T3. ONE FALLING EDGE IS ONE PULSE, one cycle wide. Not two, and not a level.
// ----------------------------------------------------------------
do_reset;
@(negedge clk); scl_pin = 1'b0;
for (k = 0; k < 8; k = k + 1) step;
$display("T3 one edge is exactly one pulse, one cycle wide");
ck_int("T3 exactly one SCL fall", n_scl_f, 1);
ck_int("T3 and no SCL rise", n_scl_r, 0);
ck_int("T3 no pulse was wider than a cycle", wide, 0);
ck_bit("T3 the level followed", scl_q, 1'b0);
// ----------------------------------------------------------------
// T4. THE LEVEL AND THE EVENT AGREE. After a rise the level is high; the pulse
// is not an independent signal that can drift from what the line reads.
// ----------------------------------------------------------------
@(negedge clk); scl_pin = 1'b1;
for (k = 0; k < 8; k = k + 1) step;
ck_int("T4 exactly one SCL rise", n_scl_r, 1);
ck_bit("T4 and the level is high again", scl_q, 1'b1);
// ----------------------------------------------------------------
// T5. THE TWO LINES ARE INDEPENDENT. Nothing couples them here, because nothing
// couples them on the bus: Chapter 18.3 is what combines them, and it can
// only do that correctly if this block keeps them separate.
// ----------------------------------------------------------------
do_reset;
@(negedge clk); sda_pin = 1'b0;
for (k = 0; k < 8; k = k + 1) step;
$display("T5 the two lines are independent here; 18.3 is what combines them");
ck_int("T5 one SDA fall", n_sda_f, 1);
ck_int("T5 and no SCL event", n_scl_r + n_scl_f, 0);
ck_bit("T5 SCL still reads high", scl_q, 1'b1);
// ... and the other direction, which is the half that is easy to omit: clocking
// SCL must produce NO SDA events at all. A block that derived one line's edges
// from the other would pass the test above and fail this one, and the two lines
// being coupled is exactly the defect that makes Chapter 18.3's framing detector
// fire on every clock pulse.
do_reset;
for (k = 0; k < 4; k = k + 1) begin
@(negedge clk); scl_pin = 1'b0; step; step; step;
@(negedge clk); scl_pin = 1'b1; step; step; step;
end
for (k = 0; k < 4; k = k + 1) step;
ck_int("T5 clocking SCL produced four falls", n_scl_f, 4);
ck_int("T5 and NO SDA event whatsoever", n_sda_r + n_sda_f, 0);
// ----------------------------------------------------------------
// T6. SIMULTANEOUS EDGES ON BOTH LINES ARE BOTH REPORTED. A slave must be able
// to see them together, because a START is precisely an SDA edge evaluated
// against an SCL LEVEL -- so losing one of two same-cycle events would make
// framing undetectable in exactly the case it matters.
// ----------------------------------------------------------------
do_reset;
@(negedge clk); scl_pin = 1'b0; sda_pin = 1'b0;
for (k = 0; k < 8; k = k + 1) step;
$display("T6 two edges in one cycle are both reported");
ck_int("T6 one SCL fall", n_scl_f, 1);
ck_int("T6 one SDA fall", n_sda_f, 1);
// ----------------------------------------------------------------
// T7. THE LATENCY IS THE SYNCHRONISER DEPTH, and it is a fact worth pinning
// rather than an implementation detail: Chapter 18.5's acknowledge window is
// measured from an edge this block reports, so the number of cycles between
// the pin moving and the pulse appearing is part of every later timing
// argument. Module 19.4 owns WHY the depth is what it is.
// ----------------------------------------------------------------
do_reset;
@(negedge clk);
scl_pin = 1'b0; // the pin moves here
n = 0;
// The `#1` matters. `@(posedge clk)` returns in the active region, BEFORE the
// non-blocking updates of that edge have been applied, so a loop that tests the
// pulse there sees the previous cycle's value and reports one cycle too many.
// Measuring a latency requires sampling after the NBA region has settled.
while (!scl_fall && n < 20) begin @(posedge clk); #1; n = n + 1; end
$display("T7 the pin-to-event latency is the synchroniser depth (%0d cycles)", n);
ck_int("T7 latency equals SYNC_DEPTH", n, SD);
// AND THE LEVEL IS SYNCHRONISED TOO, not the raw pin passed through. Only the
// first cycle after the pin moves can tell the difference: a block that reported
// the pin directly would already show the new value, while a synchronised level
// still shows the old one. Without this the level output could bypass the
// synchroniser entirely and every test above would still pass.
do_reset;
@(negedge clk); scl_pin = 1'b0; // the pin moves
@(posedge clk); #1; // exactly one cycle later
ck_bit("T7 the reported LEVEL still lags the pin by a cycle", scl_q, 1'b1);
@(posedge clk); #1; // and by SYNC_DEPTH it has caught up
ck_bit("T7 and catches up after SYNC_DEPTH cycles", scl_q, 1'b0);
// ----------------------------------------------------------------
// T8. A FULL CLOCK PULSE PRODUCES EXACTLY ONE RISE AND ONE FALL. Eight of them,
// which is the shape of every byte the slave will ever see.
// ----------------------------------------------------------------
do_reset;
@(negedge clk); scl_pin = 1'b1;
for (k = 0; k < 8; k = k + 1) begin
@(negedge clk); scl_pin = 1'b0;
step; step;
@(negedge clk); scl_pin = 1'b1;
step; step;
end
for (k = 0; k < 6; k = k + 1) step;
$display("T8 eight clock pulses give eight rises and eight falls, no more");
ck_int("T8 eight falls", n_scl_f, 8);
ck_int("T8 eight rises", n_scl_r, 8);
ck_int("T8 and no pulse was wide", wide, 0);
// ----------------------------------------------------------------
// T9. A PIN THAT MOVES AND MOVES BACK INSIDE THE SYNCHRONISER still produces a
// consistent level-and-edge pair. This is not a glitch-filtering test --
// Module 19.5 owns filtering -- it is a check that the block never reports an
// edge that disagrees with the level it also reports.
// ----------------------------------------------------------------
do_reset;
@(negedge clk); scl_pin = 1'b0;
@(negedge clk); scl_pin = 1'b1; // back again, one cycle later
for (k = 0; k < 10; k = k + 1) step;
$display("T9 levels and events never disagree, whatever the pin did");
ck_bit("T9 the level is what the pin settled to", scl_q, 1'b1);
ck_int("T9 falls and rises are balanced", n_scl_f, n_scl_r);
// ----------------------------------------------------------------
// T10. RESET IN THE MIDDLE OF ACTIVITY returns to the idle view, and does not
// emit the edges that the levels appear to have taken. A slave reset while
// SCL is low must not believe SCL rose when reset released it.
// ----------------------------------------------------------------
@(negedge clk); scl_pin = 1'b0; sda_pin = 1'b0;
for (k = 0; k < 6; k = k + 1) step;
@(negedge clk); rst_n = 1'b0;
step; step;
n_scl_r = 0; n_scl_f = 0; n_sda_r = 0; n_sda_f = 0;
@(negedge clk); rst_n = 1'b1;
for (k = 0; k < 8; k = k + 1) step;
$display("T10 reset returns to the idle view without inventing edges");
// The pins are still LOW, so after reset the levels must fall to match them --
// one fall each, and no rise anywhere.
ck_int("T10 no spurious SCL rise", n_scl_r, 0);
ck_int("T10 no spurious SDA rise", n_sda_r, 0);
if (errors == 0) $display("=== i2c_slave_sync: ALL CHECKS PASSED ===");
else $display("=== i2c_slave_sync: %0d CHECK(S) FAILED ===", errors);
$finish;
end
endmodule // -----------------------------------------------------------------------------
// i2c_slave_sync_tb.v
// Independent oracle for i2c_slave_sync.
//
// The bench drives the two pins directly and asynchronously to clk -- changing them
// between clock edges, which is what a real bus does -- and checks the event set the
// rest of the slave is built on. Every wait is bounded.
// -----------------------------------------------------------------------------
`timescale 1ns/1ps
module i2c_slave_sync_tb;
localparam integer SD = 2;
reg clk = 1'b0, rst_n = 1'b0;
reg scl_pin = 1'b1, sda_pin = 1'b1;
wire scl_q, sda_q, scl_rise, scl_fall, sda_rise, sda_fall;
integer errors = 0;
integer n, k;
// ---- independent observers -------------------------------------------------
//
// Event counters, plus a check that no pulse is ever wider than one cycle. The
// width check is the one that matters most: a two-cycle "edge" makes every consumer
// count twice, and it is invisible to a test that only counts events.
integer n_scl_r = 0, n_scl_f = 0, n_sda_r = 0, n_sda_f = 0;
integer wide = 0;
reg scl_r_d = 1'b0, scl_f_d = 1'b0, sda_r_d = 1'b0, sda_f_d = 1'b0;
always @(posedge clk) begin
if (rst_n) begin
if (scl_rise) n_scl_r <= n_scl_r + 1;
if (scl_fall) n_scl_f <= n_scl_f + 1;
if (sda_rise) n_sda_r <= n_sda_r + 1;
if (sda_fall) n_sda_f <= n_sda_f + 1;
if ((scl_rise & scl_r_d) | (scl_fall & scl_f_d) |
(sda_rise & sda_r_d) | (sda_fall & sda_f_d)) wide <= wide + 1;
end
scl_r_d <= scl_rise; scl_f_d <= scl_fall;
sda_r_d <= sda_rise; sda_f_d <= sda_fall;
end
i2c_slave_sync #(.SYNC_DEPTH(SD)) dut (
.clk(clk), .rst_n(rst_n), .scl_pin(scl_pin), .sda_pin(sda_pin),
.scl_q(scl_q), .sda_q(sda_q),
.scl_rise(scl_rise), .scl_fall(scl_fall),
.sda_rise(sda_rise), .sda_fall(sda_fall));
always #5 clk = ~clk;
task step; begin @(posedge clk); @(negedge clk); end endtask
task do_reset;
begin
@(negedge clk); rst_n = 1'b0; scl_pin = 1'b1; sda_pin = 1'b1;
n_scl_r = 0; n_scl_f = 0; n_sda_r = 0; n_sda_f = 0; wide = 0;
step; step;
@(negedge clk); rst_n = 1'b1;
step;
end
endtask
task ck_bit (input [200*8:1] what, input g, input e);
begin
if (g !== e) begin
$display(" FAIL %0s: got %0b expected %0b", what, g, e);
errors = errors + 1;
end
end
endtask
task ck_int (input [200*8:1] what, input integer g, input integer e);
begin
if (g !== e) begin
$display(" FAIL %0s: got %0d expected %0d", what, g, e);
errors = errors + 1;
end
end
endtask
initial begin
$display("=== i2c_slave_sync: two asynchronous lines into one event set ===");
// ----------------------------------------------------------------
// T1. RESET PRESENTS AN IDLE BUS. Both levels high and no events. A slave that
// reset its samplers to zero would see a pair of rising edges on its first
// released cycle -- and SDA rising while SCL is high is a STOP, so it would
// announce a STOP to itself out of reset, on a bus that never moved.
// ----------------------------------------------------------------
do_reset;
$display("T1 reset presents an idle bus, not a pair of edges");
ck_bit("T1 SCL reads high", scl_q, 1'b1);
ck_bit("T1 SDA reads high", sda_q, 1'b1);
ck_int("T1 no SCL rises", n_scl_r, 0);
ck_int("T1 no SCL falls", n_scl_f, 0);
ck_int("T1 no SDA rises", n_sda_r, 0);
ck_int("T1 no SDA falls", n_sda_f, 0);
// ----------------------------------------------------------------
// T2. A QUIET BUS PRODUCES NOTHING, for as long as you care to wait. The
// false-positive test: if this block invents events on an idle bus, every
// framing result above it is worthless.
// ----------------------------------------------------------------
do_reset;
for (k = 0; k < 40; k = k + 1) step;
$display("T2 a quiet bus produces no events at all");
ck_int("T2 still no events", n_scl_r + n_scl_f + n_sda_r + n_sda_f, 0);
// ----------------------------------------------------------------
// T3. ONE FALLING EDGE IS ONE PULSE, one cycle wide. Not two, and not a level.
// ----------------------------------------------------------------
do_reset;
@(negedge clk); scl_pin = 1'b0;
for (k = 0; k < 8; k = k + 1) step;
$display("T3 one edge is exactly one pulse, one cycle wide");
ck_int("T3 exactly one SCL fall", n_scl_f, 1);
ck_int("T3 and no SCL rise", n_scl_r, 0);
ck_int("T3 no pulse was wider than a cycle", wide, 0);
ck_bit("T3 the level followed", scl_q, 1'b0);
// ----------------------------------------------------------------
// T4. THE LEVEL AND THE EVENT AGREE. After a rise the level is high; the pulse
// is not an independent signal that can drift from what the line reads.
// ----------------------------------------------------------------
@(negedge clk); scl_pin = 1'b1;
for (k = 0; k < 8; k = k + 1) step;
ck_int("T4 exactly one SCL rise", n_scl_r, 1);
ck_bit("T4 and the level is high again", scl_q, 1'b1);
// ----------------------------------------------------------------
// T5. THE TWO LINES ARE INDEPENDENT. Nothing couples them here, because nothing
// couples them on the bus: Chapter 18.3 is what combines them, and it can
// only do that correctly if this block keeps them separate.
// ----------------------------------------------------------------
do_reset;
@(negedge clk); sda_pin = 1'b0;
for (k = 0; k < 8; k = k + 1) step;
$display("T5 the two lines are independent here; 18.3 is what combines them");
ck_int("T5 one SDA fall", n_sda_f, 1);
ck_int("T5 and no SCL event", n_scl_r + n_scl_f, 0);
ck_bit("T5 SCL still reads high", scl_q, 1'b1);
// ... and the other direction, which is the half that is easy to omit: clocking
// SCL must produce NO SDA events at all. A block that derived one line's edges
// from the other would pass the test above and fail this one, and the two lines
// being coupled is exactly the defect that makes Chapter 18.3's framing detector
// fire on every clock pulse.
do_reset;
for (k = 0; k < 4; k = k + 1) begin
@(negedge clk); scl_pin = 1'b0; step; step; step;
@(negedge clk); scl_pin = 1'b1; step; step; step;
end
for (k = 0; k < 4; k = k + 1) step;
ck_int("T5 clocking SCL produced four falls", n_scl_f, 4);
ck_int("T5 and NO SDA event whatsoever", n_sda_r + n_sda_f, 0);
// ----------------------------------------------------------------
// T6. SIMULTANEOUS EDGES ON BOTH LINES ARE BOTH REPORTED. A slave must be able
// to see them together, because a START is precisely an SDA edge evaluated
// against an SCL LEVEL -- so losing one of two same-cycle events would make
// framing undetectable in exactly the case it matters.
// ----------------------------------------------------------------
do_reset;
@(negedge clk); scl_pin = 1'b0; sda_pin = 1'b0;
for (k = 0; k < 8; k = k + 1) step;
$display("T6 two edges in one cycle are both reported");
ck_int("T6 one SCL fall", n_scl_f, 1);
ck_int("T6 one SDA fall", n_sda_f, 1);
// ----------------------------------------------------------------
// T7. THE LATENCY IS THE SYNCHRONISER DEPTH, and it is a fact worth pinning
// rather than an implementation detail: Chapter 18.5's acknowledge window is
// measured from an edge this block reports, so the number of cycles between
// the pin moving and the pulse appearing is part of every later timing
// argument. Module 19.4 owns WHY the depth is what it is.
// ----------------------------------------------------------------
do_reset;
@(negedge clk);
scl_pin = 1'b0; // the pin moves here
n = 0;
// The `#1` matters. `@(posedge clk)` returns in the active region, BEFORE the
// non-blocking updates of that edge have been applied, so a loop that tests the
// pulse there sees the previous cycle's value and reports one cycle too many.
// Measuring a latency requires sampling after the NBA region has settled.
while (!scl_fall && n < 20) begin @(posedge clk); #1; n = n + 1; end
$display("T7 the pin-to-event latency is the synchroniser depth (%0d cycles)", n);
ck_int("T7 latency equals SYNC_DEPTH", n, SD);
// AND THE LEVEL IS SYNCHRONISED TOO, not the raw pin passed through. Only the
// first cycle after the pin moves can tell the difference: a block that reported
// the pin directly would already show the new value, while a synchronised level
// still shows the old one. Without this the level output could bypass the
// synchroniser entirely and every test above would still pass.
do_reset;
@(negedge clk); scl_pin = 1'b0; // the pin moves
@(posedge clk); #1; // exactly one cycle later
ck_bit("T7 the reported LEVEL still lags the pin by a cycle", scl_q, 1'b1);
@(posedge clk); #1; // and by SYNC_DEPTH it has caught up
ck_bit("T7 and catches up after SYNC_DEPTH cycles", scl_q, 1'b0);
// ----------------------------------------------------------------
// T8. A FULL CLOCK PULSE PRODUCES EXACTLY ONE RISE AND ONE FALL. Eight of them,
// which is the shape of every byte the slave will ever see.
// ----------------------------------------------------------------
do_reset;
@(negedge clk); scl_pin = 1'b1;
for (k = 0; k < 8; k = k + 1) begin
@(negedge clk); scl_pin = 1'b0;
step; step;
@(negedge clk); scl_pin = 1'b1;
step; step;
end
for (k = 0; k < 6; k = k + 1) step;
$display("T8 eight clock pulses give eight rises and eight falls, no more");
ck_int("T8 eight falls", n_scl_f, 8);
ck_int("T8 eight rises", n_scl_r, 8);
ck_int("T8 and no pulse was wide", wide, 0);
// ----------------------------------------------------------------
// T9. A PIN THAT MOVES AND MOVES BACK INSIDE THE SYNCHRONISER still produces a
// consistent level-and-edge pair. This is not a glitch-filtering test --
// Module 19.5 owns filtering -- it is a check that the block never reports an
// edge that disagrees with the level it also reports.
// ----------------------------------------------------------------
do_reset;
@(negedge clk); scl_pin = 1'b0;
@(negedge clk); scl_pin = 1'b1; // back again, one cycle later
for (k = 0; k < 10; k = k + 1) step;
$display("T9 levels and events never disagree, whatever the pin did");
ck_bit("T9 the level is what the pin settled to", scl_q, 1'b1);
ck_int("T9 falls and rises are balanced", n_scl_f, n_scl_r);
// ----------------------------------------------------------------
// T10. RESET IN THE MIDDLE OF ACTIVITY returns to the idle view, and does not
// emit the edges that the levels appear to have taken. A slave reset while
// SCL is low must not believe SCL rose when reset released it.
// ----------------------------------------------------------------
@(negedge clk); scl_pin = 1'b0; sda_pin = 1'b0;
for (k = 0; k < 6; k = k + 1) step;
@(negedge clk); rst_n = 1'b0;
step; step;
n_scl_r = 0; n_scl_f = 0; n_sda_r = 0; n_sda_f = 0;
@(negedge clk); rst_n = 1'b1;
for (k = 0; k < 8; k = k + 1) step;
$display("T10 reset returns to the idle view without inventing edges");
// The pins are still LOW, so after reset the levels must fall to match them --
// one fall each, and no rise anywhere.
ck_int("T10 no spurious SCL rise", n_scl_r, 0);
ck_int("T10 no spurious SDA rise", n_sda_r, 0);
if (errors == 0) $display("=== i2c_slave_sync: ALL CHECKS PASSED ===");
else $display("=== i2c_slave_sync: %0d CHECK(S) FAILED ===", errors);
$finish;
end
endmodule -- -----------------------------------------------------------------------------
-- i2c_slave_sync_tb.vhd
-- Independent oracle for i2c_slave_sync. Behavioural twin of the SystemVerilog and
-- Verilog benches: the same ten tests, the same observers, the same finish time.
-- -----------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_slave_sync_tb is
end entity i2c_slave_sync_tb;
architecture sim of i2c_slave_sync_tb is
constant SD : positive := 2;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal scl_pin : std_logic := '1';
signal sda_pin : std_logic := '1';
signal scl_q, sda_q : std_logic;
signal scl_rise, scl_fall, sda_rise, sda_fall : std_logic;
signal halt : boolean := false;
-- Observers. SIGNALS rather than process variables, so they update on the next edge
-- exactly as the SystemVerilog registers do -- a variable would advance immediately
-- and the counts would lead the other two languages by a cycle.
signal n_scl_r, n_scl_f, n_sda_r, n_sda_f : integer := 0;
signal wide : integer := 0;
signal clr : boolean := false;
begin
dut : entity work.i2c_slave_sync
generic map (SYNC_DEPTH => SD)
port map (clk => clk, rst_n => rst_n, scl_pin => scl_pin, sda_pin => sda_pin,
scl_q => scl_q, sda_q => sda_q,
scl_rise => scl_rise, scl_fall => scl_fall,
sda_rise => sda_rise, sda_fall => sda_fall);
clkgen : process
begin
while not halt loop
clk <= '0'; wait for 5 ns;
clk <= '1'; wait for 5 ns;
end loop;
wait;
end process;
obs : process (clk, clr)
variable r_d, f_d, sr_d, sf_d : std_logic := '0';
begin
if clr then
n_scl_r <= 0; n_scl_f <= 0; n_sda_r <= 0; n_sda_f <= 0; wide <= 0;
elsif rising_edge(clk) then
if rst_n = '1' then
if scl_rise = '1' then n_scl_r <= n_scl_r + 1; end if;
if scl_fall = '1' then n_scl_f <= n_scl_f + 1; end if;
if sda_rise = '1' then n_sda_r <= n_sda_r + 1; end if;
if sda_fall = '1' then n_sda_f <= n_sda_f + 1; end if;
if (scl_rise = '1' and r_d = '1') or (scl_fall = '1' and f_d = '1') or
(sda_rise = '1' and sr_d = '1') or (sda_fall = '1' and sf_d = '1') then
wide <= wide + 1;
end if;
end if;
r_d := scl_rise; f_d := scl_fall; sr_d := sda_rise; sf_d := sda_fall;
end if;
end process;
stim : process
variable err : integer := 0;
variable n, k : integer;
procedure step is
begin
wait until rising_edge(clk); wait until falling_edge(clk);
end procedure;
procedure do_reset is
begin
wait until falling_edge(clk);
rst_n <= '0'; scl_pin <= '1'; sda_pin <= '1';
clr <= true; wait for 1 ns; clr <= false;
step; step;
wait until falling_edge(clk); rst_n <= '1';
step;
end procedure;
procedure ck_bit (what : string; g : std_logic; e : std_logic) is
begin
if g /= e then
report " FAIL " & what severity note;
err := err + 1;
end if;
end procedure;
procedure ck_int (what : string; g : integer; e : integer) is
begin
if g /= e then
report " FAIL " & what & ": got " & integer'image(g)
& " expected " & integer'image(e) severity note;
err := err + 1;
end if;
end procedure;
begin
report "=== i2c_slave_sync: two asynchronous lines into one event set ===" severity note;
-- T1. Reset presents an idle bus, not a pair of edges.
do_reset;
report "T1 reset presents an idle bus, not a pair of edges" severity note;
ck_bit("T1 SCL reads high", scl_q, '1');
ck_bit("T1 SDA reads high", sda_q, '1');
ck_int("T1 no SCL rises", n_scl_r, 0);
ck_int("T1 no SCL falls", n_scl_f, 0);
ck_int("T1 no SDA rises", n_sda_r, 0);
ck_int("T1 no SDA falls", n_sda_f, 0);
-- T2. A quiet bus produces nothing.
do_reset;
for k in 0 to 39 loop step; end loop;
report "T2 a quiet bus produces no events at all" severity note;
ck_int("T2 still no events", n_scl_r + n_scl_f + n_sda_r + n_sda_f, 0);
-- T3. One edge is one pulse, one cycle wide.
do_reset;
wait until falling_edge(clk); scl_pin <= '0';
for k in 0 to 7 loop step; end loop;
report "T3 one edge is exactly one pulse, one cycle wide" severity note;
ck_int("T3 exactly one SCL fall", n_scl_f, 1);
ck_int("T3 and no SCL rise", n_scl_r, 0);
ck_int("T3 no pulse was wider than a cycle", wide, 0);
ck_bit("T3 the level followed", scl_q, '0');
-- T4. The level and the event agree.
wait until falling_edge(clk); scl_pin <= '1';
for k in 0 to 7 loop step; end loop;
ck_int("T4 exactly one SCL rise", n_scl_r, 1);
ck_bit("T4 and the level is high again", scl_q, '1');
-- T5. The two lines are independent.
do_reset;
wait until falling_edge(clk); sda_pin <= '0';
for k in 0 to 7 loop step; end loop;
report "T5 the two lines are independent here; 18.3 is what combines them" severity note;
ck_int("T5 one SDA fall", n_sda_f, 1);
ck_int("T5 and no SCL event", n_scl_r + n_scl_f, 0);
ck_bit("T5 SCL still reads high", scl_q, '1');
-- ... and the other direction, the half that is easy to omit: clocking SCL must
-- produce NO SDA events at all. A block deriving one line's edges from the other
-- passes the test above and fails this one.
do_reset;
for k in 0 to 3 loop
wait until falling_edge(clk); scl_pin <= '0'; step; step; step;
wait until falling_edge(clk); scl_pin <= '1'; step; step; step;
end loop;
for k in 0 to 3 loop step; end loop;
ck_int("T5 clocking SCL produced four falls", n_scl_f, 4);
ck_int("T5 and NO SDA event whatsoever", n_sda_r + n_sda_f, 0);
-- T6. Two edges in one cycle are both reported.
do_reset;
wait until falling_edge(clk); scl_pin <= '0'; sda_pin <= '0';
for k in 0 to 7 loop step; end loop;
report "T6 two edges in one cycle are both reported" severity note;
ck_int("T6 one SCL fall", n_scl_f, 1);
ck_int("T6 one SDA fall", n_sda_f, 1);
-- T7. The pin-to-event latency is the synchroniser depth.
do_reset;
wait until falling_edge(clk);
scl_pin <= '0';
n := 0;
-- The 1 ns settle matters: sampling in the same delta as the clock edge reads the
-- value from before the signal updates, and reports one cycle too many.
while scl_fall = '0' and n < 20 loop
wait until rising_edge(clk); wait for 1 ns; n := n + 1;
end loop;
report "T7 the pin-to-event latency is the synchroniser depth" severity note;
ck_int("T7 latency equals SYNC_DEPTH", n, SD);
-- AND THE LEVEL IS SYNCHRONISED TOO, not the raw pin passed through. Only the
-- first cycle after the pin moves can tell the difference.
do_reset;
wait until falling_edge(clk); scl_pin <= '0';
wait until rising_edge(clk); wait for 1 ns;
ck_bit("T7 the reported LEVEL still lags the pin by a cycle", scl_q, '1');
wait until rising_edge(clk); wait for 1 ns;
ck_bit("T7 and catches up after SYNC_DEPTH cycles", scl_q, '0');
-- T8. Eight clock pulses give eight rises and eight falls.
do_reset;
wait until falling_edge(clk); scl_pin <= '1';
for k in 0 to 7 loop
wait until falling_edge(clk); scl_pin <= '0';
step; step;
wait until falling_edge(clk); scl_pin <= '1';
step; step;
end loop;
for k in 0 to 5 loop step; end loop;
report "T8 eight clock pulses give eight rises and eight falls, no more" severity note;
ck_int("T8 eight falls", n_scl_f, 8);
ck_int("T8 eight rises", n_scl_r, 8);
ck_int("T8 and no pulse was wide", wide, 0);
-- T9. Levels and events never disagree.
do_reset;
wait until falling_edge(clk); scl_pin <= '0';
wait until falling_edge(clk); scl_pin <= '1';
for k in 0 to 9 loop step; end loop;
report "T9 levels and events never disagree, whatever the pin did" severity note;
ck_bit("T9 the level is what the pin settled to", scl_q, '1');
ck_int("T9 falls and rises are balanced", n_scl_f, n_scl_r);
-- T10. Reset mid-activity returns to the idle view without inventing edges.
wait until falling_edge(clk); scl_pin <= '0'; sda_pin <= '0';
for k in 0 to 5 loop step; end loop;
wait until falling_edge(clk); rst_n <= '0';
step; step;
clr <= true; wait for 1 ns; clr <= false;
wait until falling_edge(clk); rst_n <= '1';
for k in 0 to 7 loop step; end loop;
report "T10 reset returns to the idle view without inventing edges" severity note;
ck_int("T10 no spurious SCL rise", n_scl_r, 0);
ck_int("T10 no spurious SDA rise", n_sda_r, 0);
if err = 0 then
report "=== i2c_slave_sync: ALL CHECKS PASSED ===" severity note;
else
report "=== i2c_slave_sync: " & integer'image(err) & " CHECK(S) FAILED ==="
severity note;
end if;
halt <= true;
wait;
end process;
end architecture sim;8b. Execution
| Design | SystemVerilog | Verilog-2001 | VHDL | Finish |
|---|---|---|---|---|
i2c_slave_sync | PASS 12/12 | PASS 12/12 | PASS 12/12 | 2520 ns, all three |
9. Mutation Testing
Eight defects, one per claim above.
| # | Injected defect | Expected detection | Result |
|---|---|---|---|
| M1 | reset the samplers LOW — a released bus looks like two rising edges | T1, T2 | KILLED (17) |
| M2 | detect edges from the raw pin against the first flop | T3, T9 | KILLED (4) |
| M3 | the fall pulse is the inverse of the rise | T3, T4 | KILLED (4) |
| M4 | SDA edges taken from SCL — the two lines coupled | T5 after strengthening | KILLED (2) |
| M5 | the reported level is the raw pin | T7 after strengthening | KILLED (2) |
| M6 | the delay stage tracks the pin, so the pulse is two cycles wide | T3 | KILLED (6) |
| M7 | no delay stage: the edge becomes a level | T3, T8 | KILLED (6) |
| M8 | the chain shifts the wrong way | T7 | KILLED (2) |
baseline: PASS (verified before injecting anything)
killed: 8 survived: 0 score: 8/8
restored: PASSTwo survived first, and both were untested halves
M4 coupled the two lines — sda_rise computed from SCL. T5 tested independence in one direction: moving SDA produces no SCL event. Nothing tested the other direction, that clocking SCL produces no SDA event. A block that derived one line's edges from the other passed.
That is the half that matters most, because it is the one that breaks 18.3: coupled lines make the framing detector fire on every clock pulse. The fix was four SCL pulses and an assertion that n_sda_r + n_sda_f is zero.
M5 bypassed the synchroniser for the level output — reporting the raw pin while the pulses stayed correct. Every test passed, because after a few cycles the reported level equals the pin either way. Only the first cycle after the pin moves can tell them apart.
pin moves -> a synchronised level still shows the OLD value
-> a raw-pin level already shows the new oneSo the bench now checks the level one cycle after the pin moves and again after SYNC_DEPTH. Without it, the level output could bypass the synchroniser entirely — and a consumer that combined a synchronised edge with an unsynchronised level would evaluate framing against a value that had not settled.
10. Verification Connection — What a Front End Can and Cannot Be Asked
// 1. A PULSE IS A PULSE. One cycle, never two. Every consumer counts on it, and a
// two-cycle edge makes a bit counter advance twice.
property p_pulse_is_one_cycle;
@(posedge clk) disable iff (!rst_n) scl_fall |=> !scl_fall;
endproperty
a_pulse: assert property (p_pulse_is_one_cycle);
// 2. THE PULSE AGREES WITH THE LEVEL. A fall pulse must coincide with the level
// having just become low -- they cannot drift apart, which is mutation M5.
property p_pulse_matches_level;
@(posedge clk) disable iff (!rst_n) scl_fall |-> !scl_q;
endproperty
a_agree: assert property (p_pulse_matches_level);
// 3. THE LINES ARE INDEPENDENT. An SDA event may not be caused by an SCL change.
// Stated as: an SDA pulse requires the SDA level to have changed.
property p_sda_independent;
@(posedge clk) disable iff (!rst_n) sda_fall |-> (!sda_q && $past(sda_q));
endproperty
a_indep: assert property (p_sda_independent);
// WHAT CANNOT BE ASSERTED HERE, and it is the important boundary.
//
// "the reported level equals the pin" is FALSE BY DESIGN for SYNC_DEPTH cycles after
// every change -- that lag is the block's purpose. So there is no assertion relating
// the pin to the output except one with an explicit delay, and writing it would
// hard-code the depth into the verification of a parameterised block.
//
// Nor can anything here assert that the input was GLITCH-FREE. This block does not
// filter; it synchronises. A spike shorter than a clock period may or may not be
// sampled, and which it is is not this block's contract -- Module 19.5 owns filtering
// and is where that property becomes assertable at all.
//
// COVERAGE worth carrying, because a target's front end is easy to under-stimulate:
//
// cover: both lines change in the SAME cycle (T6 -- a START needs it)
// cover: a line changes and changes back within SYNC_DEPTH (T9)
// cover: reset asserted while both lines are LOW (T10 -- the dangerous one)
//
// The last bin is the one a regression built from clean transfers never hits, and it
// is exactly the case mutation M1 breaks.11. FPGA and ASIC Implications
On an FPGA this block is the boundary between the pad and the design, and its two flops should be constrained as a synchroniser so the tools do not retime or merge them. The depth, the false-path constraint and the metastability arithmetic are 19.4's subject; what belongs here is that the chain must be per line and singular, because there is one pad per line and a second synchroniser creates a second opinion.
The SYNC_DEPTH parameter exists so an integrator can deepen the chain on a faster clock without touching logic. Note what deepening costs: every event arrives later, which consumes acknowledge margin in 18.5 and nothing else. That is a clean trade, and it is only clean because this block is the single entry point.
On an ASIC the pad's own input filter sits ahead of this block and suppresses the spikes Table 10's tSP describes — which is why no digital filter appears here. Adding one would duplicate the pad and consume high-phase margin for nothing; Chapter 17.6 §9 made the same point from the master's side.
Reset behaviour is the ASIC-relevant detail. The samplers must come out of reset showing an idle bus, and during a power-up ramp — before reset deasserts — the pads must be released so this target cannot hold a bus down while its own logic is undefined.
12. Debugging — The Target That Acknowledged a Byte Nobody Sent
A newly integrated I2C target works correctly on the bench. On the board it occasionally acknowledges a byte during a transaction addressed to a different device, corrupting that device's transfer. The rate is roughly one occurrence per hour of bus activity and it has never been reproduced in simulation. Power-cycling changes nothing; slowing the bus makes it rarer but does not eliminate it.
Edge pulses derived from a possibly-metastable sample. Comparing the raw pin against the first flop means the comparison's older operand has been through one flop and the newer has been through none -- so a value that has not settled can produce a transition the settled chain never shows. The spurious SDA fall coincided with a stale SCL level that still read high, and the framing detector did the only thing it could with those two inputs: it reported a START. Nothing in the framing detector, the address block or the acknowledge generator was wrong; all three behaved correctly on evidence that was manufactured one layer below them.
Derive both operands of every edge comparison from values that have been through the full chain: compare the settled value against a third delay register, so neither operand can be unsettled. Then note why simulation was silent -- the bench drove the pins in alignment with clk, so no sample ever landed near a transition. A bench for a block whose entire purpose is to absorb asynchrony must drive its inputs ASYNCHRONOUSLY, between clock edges, which is what the published testbench does. The mutation that reproduces the defect is M2, and it fails four checks; the suite could not have found it while the stimulus was synchronous.Three generalisations.
Three correct blocks produced a wrong result. The framing detector, the address block and the acknowledge generator all behaved exactly as specified, on evidence fabricated below them. That is the characteristic hazard of a single-entry-point architecture: it localises the risk, and it also localises the blast radius to everything.
The defect was in which signals a comparison used, not in the comparison. Both versions are two-input equality checks on the same wire; only the settling history of the operands differs, and that history is invisible in a schematic.
A synchronous bench cannot test a synchroniser. Driving the pins in alignment with clk guarantees no sample is ever taken near a transition — which is the only condition under which the defect exists. The published bench changes the pins between clock edges for exactly this reason.
13. Common Misconceptions
"A target can sample SDA on its own clock and work out the rest." SDA alone is ambiguous: a falling edge is a START if SCL is high and ordinary data preparation if it is low. Both lines are needed, and the level of one qualifies the edge of the other. §2.
"Edges are enough; the levels are redundant." Framing is an SDA edge qualified by an SCL level. A front end reporting only edges makes framing undetectable. §2.
"Reset the samplers to zero — it is the safe default." Zero is the active state on an open-drain bus. Resetting low makes the first released cycle look like two rising edges, and SDA rising while SCL is high is a STOP. §3.
"Compare the pin against its first flop — it is one flop cheaper." It emits edge pulses derived from possibly-metastable samples: a one-cycle spurious START on a quiet bus. §4 and §12.
"Each consumer can synchronise the lines itself." Then two consumers can disagree about what the bus did, and no block-level test can reach the inconsistency. One pad, one synchroniser. §5.
"The synchroniser depth is this block's business." The depth, the metastability argument and the filtering all belong to Module 19. Here two flops are a contract and the only exported consequence is a latency. §6.
"A two-cycle edge pulse is harmless." It makes every consumer count twice. It is also invisible to a test that counts events rather than measuring widths. §8a.
"Late events are a problem to be minimised." Late is safe for receiving and for framing, and costs margin only in the acknowledge window. Knowing the direction is worth more than minimising the number. §7.
"If the testbench passes, the front end is verified." Two of eight mutants survived the first suite, and both were the untested half of a property already being tested. §9.
"Simulation would have caught a metastability-related bug." Not if the bench drives the pins in alignment with the clock, which guarantees no sample lands near a transition. §12.
14. Reason It Through
Why does a target need both lines, when the data it wants is only on SDA?
Because SDA's meaning depends entirely on SCL's level: the same falling edge is a START while SCL is high and an ordinary data bit while it is low. Those are the two most different things on the bus. §2.
Why must the samplers reset high rather than low?
Because both lines high is the idle bus. Resetting low means the first released cycle shows two rising edges, and SDA rising while SCL is high is a STOP — so the target announces a STOP to itself out of reset. §3.
What exactly is wrong with comparing the raw pin against the first synchroniser flop?
One operand has been through a flop and the other has not, so a value that has not settled can produce a transition the settled chain never shows — a spurious one-cycle edge. §4 and §12.
Why is one synchroniser per line, in one place, an architectural requirement rather than tidiness?
Because two synchronisers on the same wire can report the edge in different cycles, and a protocol engine built on two inconsistent views has a failure mode no block-level test can reach. §5.
Every event arrives two cycles late. Which obligation does that threaten, and which does it not?
It threatens the acknowledge, which must be asserted inside one SCL low phase that the target only learns has begun two cycles in. It does not threaten receiving — the bit is stable for the whole high phase — or framing, which has no bounded response time. §7.
Both survivors of the first mutation run were "the other half" of something. Explain each.
M4 coupled the lines, and independence had been tested only in the SDA-moves-SCL-quiet direction. M5 bypassed the synchroniser for the level, and only the latency of the pulse had been measured, never the level's. §9.
Why can a bench that drives the pins on clock-edge boundaries never find the §12 defect?
Because the defect only exists when a sample is taken near a transition. Aligning the stimulus to clk guarantees that never happens, so the metastable operand the bug depends on never occurs. §12.
What property of this block is deliberately not assertable, and why?
That the reported level equals the pin. It is false by design for SYNC_DEPTH cycles after every change — the lag is the point — so any assertion relating them would have to hard-code the depth of a parameterised block. §10.
15. Understanding Check
16. Summary
This block is the target's entire sense of time. A master reads the bus to check its own intent took effect; a target reads it to learn what is being asked at all.
Five facts, and no more: two synchronised levels and four one-cycle pulses. Everything a later chapter needs is one of those combined with a level — which is why both are outputs.
No bit counter lives here, because a counter must be reset by framing derived from this block's own outputs, and the two jobs have different reset conditions.
The samplers reset to the idle bus, both lines high. Resetting low makes the first released cycle look like two rising edges, and SDA rising while SCL is high is a STOP.
Both operands of every edge comparison are fully settled values. Comparing the raw pin against the first flop emits edges derived from possibly-metastable samples — a one-cycle spurious START that every block above will believe.
One pad per line means one synchroniser per line, in one place. Two consumers with their own synchronisers can disagree about what the bus did.
The depth, the metastability argument and the filtering belong to Module 19. Here two flops are a contract, and the one exported consequence is a latency of exactly SYNC_DEPTH cycles — measured, not assumed.
Lateness has a direction. Safe for receiving and framing; it costs margin only in the acknowledge window, which is why the clock ratio is a design constraint.
Eight mutants, eight killed — after two survived, and both were the other half of a property already under test: independence has two directions, and a synchroniser affects both the edges and the levels.
And a synchronous bench cannot test a synchroniser. Driving the pins in alignment with clk guarantees no sample lands near a transition, which is the only condition under which the §12 defect exists.
17. What Comes Next
The target can now see. Chapter 18.3 makes it understand — and the step is smaller than it sounds, because framing detection is two AND gates:
start = sda_fall && scl_q
stop = sda_rise && scl_qThe chapter's substance is not the detection. It is what a START resets, and the distinction that is not on the wire at all: a first START and a repeated START are the identical edge, and only the target's own state separates them. That distinction is what 18.10 needs in order to keep a register pointer alive across a turnaround while resetting the frame around it.
Continue learning
Related tutorials
- Related topic
START and STOP Detection Inside a Slave
Detection is two AND gates. The substance is what a START resets, and the distinction that is not on the wire at all — a first START and a repeated START are the identical edge, separated only by state the target keeps itself.
- Related topic
ACK Generation and Its Timing Window
Where most slave designs first fail. The acknowledge has three parts, the window is bounded at both ends by falling edges, and asserting early is not early — SDA falling while SCL is high is a START, so a premature acknowledge restarts the transaction instead of acknowledging a byte.
- Related topic
The Transmit Datapath — Sourcing Read Data in Time
The hardest datapath in a target, because the master decides when the next bit is wanted and the slave must have decided what it is one phase earlier. Builds the pre-fetch, shows why a transmitted one is a release, and why the ninth slot must be given back.
- Related topic
The SCL Timing Generator — Phases, Strobes and the Readback Rule
Where Table 10's microseconds become counts of system-clock cycles. Derives the period budget that must include rise and fall time, shows why rounding down is always illegal and rounding up always legal, and builds a generator that leaves its low phase only when the line actually reads back high — which implements clock stretching and clock synchronization with no extra logic.
