I²C · Module 11
tSP — Spike Suppression and the I²C Input Filter
The only parameter in Table 10 that requires a device to do something rather than stay within a limit — and the only one you pay for in latency on every legitimate transition.
Every block in this module so far has been a checker: it watches the bus, measures an interval, and reports. None of them is in the signal path, and removing any of them changes nothing about how the bus behaves.
tSP is different in kind. It is not something a device may or may not achieve — it is something a device must do:
"Pulse width of spikes that must be suppressed by the input filter."
That wording makes it the only entry in Table 10 that specifies a behaviour rather than a bound. And a filter is in the path, which means this chapter's design is a datapath block with latency — latency that lands inside the budgets of the six preceding chapters and cannot be avoided.
1. The Row, and What Is Missing From It
Three unusual things about that row.
Standard-mode has no requirement, and that is the same fact as Chapter 11.7 §5's missing tr minimum. Standard-mode permits a 1000 ns rise, and an edge that slow cannot ring hard enough to produce a spurious transition. No ringing, no spikes, no filter needed. The two parameters appear together at Fast-mode because they are two halves of one defence, and they are both absent from Standard-mode for one reason.
The range starts at 0. A device whose tSP is 0 ns suppresses nothing and is compliant. So the parameter does not mandate a filter — it mandates that if pulses up to some width get through, that width must be at most 50 ns. §5 is about what tSP = 0 has to mean for an implementation.
It is the only parameter whose units describe the input rather than an interval of the protocol. Every other row measures something about the waveform the bus carries. This one measures something about what a receiver is allowed to believe.
2. An Obligation to Act, Not a Limit to Respect
The distinction matters for how the requirement is verified, and it is worth being precise about.
| every other Table 10 parameter | tSP | |
|---|---|---|
| what it constrains | the waveform a device produces | what a device accepts |
| how it is verified | measure the bus, compare | inject a spike, check it had no effect |
| a passing result looks like | a number within a limit | nothing happening |
| the block involved | a monitor, outside the path | a filter, inside the path |
Verifying tSP means proving a non-event, which is the hardest shape of test to write well. Chapter 10.3 §8 made the general argument; here it is concrete: a test that injects a 30 ns spike and observes correct behaviour proves the filter worked or proves the spike never reached the device or proves the stimulus was miswired. All three look identical.
So the suite needs a positive control: the same test with the filter disabled, showing the spike does break things. Without it, a test that passes for the wrong reason is indistinguishable from one that passes. §7's test 9 is that control, and it is the most important test in the chapter.
3. What a Filter Costs
Here is the fact the rest of the chapter follows from.
A filter that rejects pulses shorter than tSP must delay every legitimate transition by at least tSP. It cannot know a transition is legitimate until it has persisted, and persistence takes time. There is no clever implementation that avoids this — it is what the requirement means.
So the filter's latency is tSP plus the register stage that samples it:
latency =
T_SP + 1sample clocks
And that latency lands somewhere. It is not free, and the place it lands is the substance of §4.
The spike vanishes; the real edge arrives late
10 cyclesThe figure is deliberately drawn with the filter on one line. In a real device both SDA and SCL are filtered, usually identically, and §4 is about what that symmetry does and does not buy.
4. Where the Latency Lands
This is the section that connects the chapter to the rest of the module, and it has two halves that behave oppositely.
For a monitor measuring an interval, the latency cancels. If both ends of a measured interval are delayed by the same amount, the interval is unchanged. So a tSU;DAT measurement taken after the filters is the same number as one taken before, and the Chapter 11.3 §10 note that filtering "does not change the measured intervals" is exactly this.
For a device responding to an edge, the latency is additive. And this is where it hurts. Consider a slave producing a data bit:
| step | delay |
|---|---|
| SCL actually falls on the wire | — |
| the filter passes the falling edge | tSP + 1 |
| the state machine decides the next bit | its own logic |
| the output register drives SDA | 1 clock |
| the pad and the line settle | tr or tf |
The world measures tVD;DAT from SCL falling on the wire, because that is where the parameter is defined (Chapter 11.4 §1). The device's own logic starts from the filtered edge. So the filter's latency is inside tVD;DAT and the device never sees it.
That is the trap, and it is easy to fall into because the filter is usually somebody else's module — a pad-ring cell or a library component — instantiated once and forgotten.
5. Two Details That Sound Trivial
Is a pulse of exactly tSP suppressed or passed?
The table says tSP is the "pulse width of spikes that must be suppressed", and its range runs up to 50 ns inclusive. So a pulse of exactly tSP is a spike, and it must be rejected. The filter therefore accepts a change only once it has persisted beyond the threshold, not on reaching it.
That is a >= against a counter rather than a >, and getting it backwards produces a filter that is off by one pulse width — passing exactly the widest spike it was built to reject. It is invisible except at the boundary, which is why §7's test 4 drives a pulse of exactly T_SP and test 5 drives one tick longer. Mutation H2 is the inverted comparison.
What does tSP = 0 mean for a filter that is already in the path?
The parameter permits zero, so a compliant device may suppress nothing. But an RTL filter instantiated with T_SP = 0 cannot vanish — it is still a register stage.
So T_SP = 0 must mean transparent: every change passes at the next clock, with the one cycle of register latency and no suppression. That is the correct reading and §7's test 8 asserts it, because the alternatives are both wrong. A filter that still waits one extra cycle at T_SP = 0 charges latency for a service it is not providing; one that goes fully combinational changes the module's timing characteristics based on a parameter value, which is a synthesis surprise nobody wants.
And it matters for Standard-mode specifically: with no tSP requirement, a design supporting all three speed modes with one filter runs it at T_SP = 0 in Standard-mode — where the low phase is 4.7 µs and could easily absorb the latency, but there is no reason to pay it.
6. The Spike Filter in Three Languages
// THE SPIKE SUPPRESSION FILTER. Every other design in Module 11 observes the bus; this one
// is in the datapath, and it is a design OBLIGATION rather than a nicety.
//
// Table 10:
// tSP "pulse width of spikes that must be suppressed by the input filter"
// Standard-mode: no requirement
// Fast-mode: 0 to 50 ns
// Fast-mode Plus: 0 to 50 ns
//
// Read the wording carefully, because it is unusual for this table. Most entries constrain
// what a device may PRODUCE. This one constrains what a device must TOLERATE: a compliant
// Fast-mode input must reject any pulse up to 50 ns wide. A device without such a filter is
// not merely less robust -- it is non-compliant, because the specification has told it what
// its input stage must do.
//
// Note also that Standard-mode has NO tSP entry at all. Spike suppression arrived with
// Fast-mode, which is why a Standard-mode-only design may legitimately omit the filter --
// and why this block must be able to compile away to nothing when T_SP is zero.
//
// HOW IT WORKS. A new value is adopted only once it has PERSISTED for longer than T_SP
// samples. If the line returns to its previous value first, the excursion was a spike and
// is discarded -- the output never moved.
//
// AND THE PRICE, WHICH IS THE PART THAT MATTERS FOR A TIMING BUDGET: the filter delays
// every LEGITIMATE transition by T_SP + 1 samples, because it cannot distinguish a real
// edge from a long spike until enough time has passed to rule the spike out. That latency
// is not free and it is not hideable. It comes off the same period budget Chapter 11.7
// accounts for, and it applies to BOTH lines -- so a filtered SCL reports its edges late
// and a filtered SDA presents its data late. Chapter 11.9 spends it explicitly.
module i2c_spike_filter #(
// Spike width to reject, in sample-clock ticks. Fast-mode's 50 ns is 5 ticks at
// 100 MHz. Setting this to ZERO makes the filter transparent, which is the correct
// configuration for a Standard-mode-only device.
parameter int T_SP = 5,
parameter int CNT_W = 8
)(
input logic clk,
input logic rst_n,
input logic raw_in,
output logic filtered_out,
// Pulses when a spike has been rejected. Worth having: a bus that is producing spikes
// is a bus with a signal-integrity problem, and a filter that silently absorbs them
// removes the only evidence. Suppressing a spike and reporting it are different jobs.
output logic spike_rejected,
output logic [CNT_W-1:0] n_spikes
);
// The candidate value and how long it has persisted. `pending` is high while the input
// disagrees with the output and the disagreement has not yet lasted long enough.
logic pending;
logic [CNT_W-1:0] persist;
always_ff @(posedge clk) begin
if (!rst_n) begin
// An idle I2C line is HIGH, so that is the safe reset value: a filter that
// reset its output LOW would drive a glitch onto the bus at every reset, which
// for SDA is a START condition nobody asked for.
filtered_out <= 1'b1;
pending <= 1'b0;
persist <= '0;
spike_rejected <= 1'b0;
n_spikes <= '0;
end else begin
spike_rejected <= 1'b0;
if (raw_in == filtered_out) begin
// The input agrees with the output. If a disagreement was in progress, it
// has ended without lasting long enough -- that is the definition of a
// spike, so report it and discard it.
if (pending) begin
spike_rejected <= 1'b1;
n_spikes <= n_spikes + 1'b1;
end
pending <= 1'b0;
persist <= '0;
end else begin
// The input disagrees. Adopt it once it has persisted for MORE than T_SP
// samples -- strictly more, because the specification says pulses UP TO
// tSP must be suppressed, so a pulse of exactly tSP is a spike and must be
// rejected. An implementation that adopted at exactly T_SP would propagate
// the widest pulse the table tells it to reject.
if (persist >= T_SP[CNT_W-1:0]) begin
filtered_out <= raw_in;
pending <= 1'b0;
persist <= '0;
end else begin
pending <= 1'b1;
persist <= persist + 1'b1;
end
end
end
end
endmodule `timescale 1ns/1ps
// 100 MHz sample clock: one tick is 10 ns, so Fast-mode's tSP of 50 ns is 5 ticks.
//
// TWO instances are tested: one configured for Fast-mode and one with T_SP = 0, which is
// the correct configuration for a Standard-mode-only device and must be transparent. A
// filter that could not be configured away would force a latency cost on a device the
// specification never asked to pay it.
module i2c_spike_filter_tb;
localparam int T_SP = 5;
localparam int CNT_W = 8;
logic clk = 1'b0;
always #5 clk = ~clk;
logic rst_n = 1'b0;
logic raw_in = 1'b1;
logic filtered_out, spike_rejected;
logic [CNT_W-1:0] n_spikes;
// The Standard-mode instance: no spike suppression required, so no latency paid.
logic filtered_pass, spike_pass;
logic [CNT_W-1:0] n_spikes_pass;
int errors = 0;
int i, t0, t1;
logic [CNT_W-1:0] spikes_before;
i2c_spike_filter #(.T_SP(T_SP), .CNT_W(CNT_W)) dut (
.clk(clk), .rst_n(rst_n), .raw_in(raw_in),
.filtered_out(filtered_out), .spike_rejected(spike_rejected), .n_spikes(n_spikes));
i2c_spike_filter #(.T_SP(0), .CNT_W(CNT_W)) dut_pass (
.clk(clk), .rst_n(rst_n), .raw_in(raw_in),
.filtered_out(filtered_pass), .spike_rejected(spike_pass), .n_spikes(n_spikes_pass));
initial begin #500000; $display("FAIL: watchdog expired"); $finish; end
task automatic tick(input int n);
begin repeat (n) @(negedge clk); end
endtask
// A pulse of `w` ticks to the opposite of the current steady value, then back.
task automatic pulse(input logic v, input int w);
begin
raw_in = v; tick(w);
raw_in = ~v; tick(30); // settle well clear of the filter's window
end
endtask
initial begin
tick(3);
// An idle I2C line is HIGH, so a filter must reset HIGH. Resetting LOW would put a
// START condition on SDA at every reset.
if (filtered_out !== 1'b1 || filtered_pass !== 1'b1) begin
$display("FAIL: the filter did not reset HIGH -- a reset would glitch the bus");
errors++; end
rst_n = 1'b1; tick(2);
raw_in = 1'b1; tick(20);
// ---- 1: a SPIKE well inside the limit is rejected. The output must never move.
begin
spikes_before = n_spikes;
pulse(1'b0, 2);
if (n_spikes !== spikes_before + 8'd1) begin
$display("FAIL: a 2-tick spike was not counted"); errors++; end
end
// ---- 2: a spike of EXACTLY tSP must be REJECTED. The specification says pulses UP
// TO tSP must be suppressed, so the widest rejected pulse is tSP itself -- an
// implementation that adopted at exactly tSP would propagate the very pulse the
// table tells it to reject. This is the boundary that matters, and it runs the
// opposite way from every minimum boundary in this module.
begin
spikes_before = n_spikes;
raw_in = 1'b0;
for (i = 0; i < T_SP; i = i + 1) begin
@(negedge clk);
if (filtered_out !== 1'b1) begin
$display("FAIL: the output moved %0d ticks into a tSP-wide spike", i + 1);
errors++; end
end
raw_in = 1'b1; tick(30);
if (n_spikes !== spikes_before + 8'd1) begin
$display("FAIL: a spike of exactly tSP (%0d ticks) was not rejected", T_SP);
errors++; end
end
// ---- 3: a pulse ONE TICK WIDER than tSP must PROPAGATE. Together with test 2 this
// pins the threshold down; either test alone passes on a filter that is off by
// one in the other direction.
begin
spikes_before = n_spikes;
raw_in = 1'b0;
tick(T_SP + 2); // comfortably past the threshold
if (filtered_out !== 1'b0) begin
$display("FAIL: a pulse wider than tSP did not propagate"); errors++; end
if (n_spikes !== spikes_before) begin
$display("FAIL: a legitimate transition was counted as a spike"); errors++; end
raw_in = 1'b1; tick(30);
end
// ---- 4: THE LATENCY, measured. A legitimate transition appears at the output
// T_SP + 1 ticks after it appeared at the input. This is the cost the filter
// imposes on the timing budget, and it is worth measuring rather than assuming
// because it is the number Chapter 11.9 has to spend.
begin
raw_in = 1'b1; tick(20);
t0 = 0; t1 = 0;
raw_in = 1'b0;
for (i = 1; i <= 4 * (T_SP + 4); i = i + 1) begin
@(negedge clk);
if (filtered_out === 1'b0 && t1 == 0) t1 = i;
end
if (t1 !== T_SP + 1) begin
$display("FAIL: the filter latency measured %0d ticks, expected T_SP + 1 = %0d",
t1, T_SP + 1); errors++; end
raw_in = 1'b1; tick(30);
end
// ---- 5: SEVERAL spikes in a row are all rejected, and the count is exact. A filter
// whose counter did not reset between excursions could accumulate two short
// spikes into one adopted transition -- which would be worse than no filter,
// because it would invent an edge that was never on the wire.
begin
spikes_before = n_spikes;
raw_in = 1'b1; tick(20);
for (i = 0; i < 6; i = i + 1) begin
raw_in = 1'b0; tick(2);
raw_in = 1'b1; tick(2);
end
tick(30);
if (filtered_out !== 1'b1) begin
$display("FAIL: six short spikes combined into an adopted transition");
errors++; end
if (n_spikes !== spikes_before + 8'd6) begin
$display("FAIL: six spikes counted as %0d", n_spikes - spikes_before);
errors++; end
end
// ---- 6: a spike in the OPPOSITE direction, from a steady LOW. The filter must be
// symmetric -- a design that only counted one polarity would pass every test
// above and fail on a bus whose idle state it was not written for.
begin
raw_in = 1'b0; tick(30); // establish a steady low
if (filtered_out !== 1'b0) begin
$display("FAIL: setup for test 6 -- the output should be low"); errors++; end
spikes_before = n_spikes;
pulse(1'b1, 2);
if (filtered_out !== 1'b0) begin
$display("FAIL: an upward spike from a steady low propagated"); errors++; end
if (n_spikes !== spikes_before + 8'd1) begin
$display("FAIL: an upward spike was not counted"); errors++; end
raw_in = 1'b1; tick(30);
end
// ---- 7: T_SP = 0 is TRANSPARENT. A Standard-mode device has no spike-suppression
// obligation, so it must be able to configure the filter away entirely -- and
// that means zero latency as well as zero suppression. A filter with an
// irreducible one-tick delay would charge a Standard-mode design for something
// the table never asked of it.
begin
raw_in = 1'b1; tick(20);
raw_in = 1'b0;
@(negedge clk);
if (filtered_pass !== 1'b0) begin
$display("FAIL: the T_SP = 0 instance delayed a transition"); errors++; end
raw_in = 1'b1;
@(negedge clk);
if (filtered_pass !== 1'b1) begin
$display("FAIL: the T_SP = 0 instance delayed a transition back"); errors++; end
if (n_spikes_pass !== '0) begin
$display("FAIL: the transparent instance reported %0d spikes", n_spikes_pass);
errors++; end
tick(20);
end
// ---- 8: a LONG steady run produces nothing at all. The filter must be quiet on a
// quiet bus; a block that counted spikes while nothing was happening would make
// its own output useless as a signal-integrity indicator.
begin
spikes_before = n_spikes;
raw_in = 1'b1; tick(300);
if (n_spikes !== spikes_before) begin
$display("FAIL: a steady line produced %0d spikes", n_spikes - spikes_before);
errors++; end
end
if (errors == 0)
$display("PASS: a pulse of exactly tSP is rejected and one tick wider propagates, latency is T_SP+1, spikes are counted and symmetric, T_SP=0 is transparent");
else $display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule // THE SPIKE SUPPRESSION FILTER. Every other design in Module 11 observes the bus; this one
// is in the datapath, and it is a design OBLIGATION rather than a nicety.
//
// Table 10:
// tSP "pulse width of spikes that must be suppressed by the input filter"
// Standard-mode: no requirement
// Fast-mode: 0 to 50 ns
// Fast-mode Plus: 0 to 50 ns
//
// Read the wording carefully, because it is unusual for this table. Most entries constrain
// what a device may PRODUCE. This one constrains what a device must TOLERATE: a compliant
// Fast-mode input must reject any pulse up to 50 ns wide. A device without such a filter is
// not merely less robust -- it is non-compliant, because the specification has told it what
// its input stage must do.
//
// Note also that Standard-mode has NO tSP entry at all. Spike suppression arrived with
// Fast-mode, which is why a Standard-mode-only design may legitimately omit the filter --
// and why this block must be able to compile away to nothing when T_SP is zero.
//
// HOW IT WORKS. A new value is adopted only once it has PERSISTED for longer than T_SP
// samples. If the line returns to its previous value first, the excursion was a spike and
// is discarded -- the output never moved.
//
// AND THE PRICE, WHICH IS THE PART THAT MATTERS FOR A TIMING BUDGET: the filter delays
// every LEGITIMATE transition by T_SP + 1 samples, because it cannot distinguish a real
// edge from a long spike until enough time has passed to rule the spike out. That latency
// is not free and it is not hideable. It comes off the same period budget Chapter 11.7
// accounts for, and it applies to BOTH lines -- so a filtered SCL reports its edges late
// and a filtered SDA presents its data late. Chapter 11.9 spends it explicitly.
// (Verilog-2001)
module i2c_spike_filter #(
// Spike width to reject, in sample-clock ticks. Fast-mode's 50 ns is 5 ticks at
// 100 MHz. Setting this to ZERO makes the filter transparent, which is the correct
// configuration for a Standard-mode-only device.
parameter T_SP = 5,
parameter CNT_W = 8
)(
input wire clk,
input wire rst_n,
input wire raw_in,
output reg filtered_out,
// Pulses when a spike has been rejected. Worth having: a bus that is producing spikes
// is a bus with a signal-integrity problem, and a filter that silently absorbs them
// removes the only evidence. Suppressing a spike and reporting it are different jobs.
output reg spike_rejected,
output reg [CNT_W-1:0] n_spikes
);
// The candidate value and how long it has persisted. `pending` is high while the input
// disagrees with the output and the disagreement has not yet lasted long enough.
reg pending;
reg [CNT_W-1:0] persist;
always @(posedge clk) begin
if (!rst_n) begin
// An idle I2C line is HIGH, so that is the safe reset value: a filter that
// reset its output LOW would drive a glitch onto the bus at every reset, which
// for SDA is a START condition nobody asked for.
filtered_out <= 1'b1;
pending <= 1'b0;
persist <= {CNT_W{1'b0}};
spike_rejected <= 1'b0;
n_spikes <= {CNT_W{1'b0}};
end else begin
spike_rejected <= 1'b0;
if (raw_in == filtered_out) begin
// The input agrees with the output. If a disagreement was in progress, it
// has ended without lasting long enough -- that is the definition of a
// spike, so report it and discard it.
if (pending) begin
spike_rejected <= 1'b1;
n_spikes <= n_spikes + 1'b1;
end
pending <= 1'b0;
persist <= {CNT_W{1'b0}};
end else begin
// The input disagrees. Adopt it once it has persisted for MORE than T_SP
// samples -- strictly more, because the specification says pulses UP TO
// tSP must be suppressed, so a pulse of exactly tSP is a spike and must be
// rejected. An implementation that adopted at exactly T_SP would propagate
// the widest pulse the table tells it to reject.
if (persist >= T_SP) begin
filtered_out <= raw_in;
pending <= 1'b0;
persist <= {CNT_W{1'b0}};
end else begin
pending <= 1'b1;
persist <= persist + 1'b1;
end
end
end
end
endmodule `timescale 1ns/1ps
// 100 MHz sample clock: one tick is 10 ns, so Fast-mode's tSP of 50 ns is 5 ticks.
//
// TWO instances are tested: one configured for Fast-mode and one with T_SP = 0, which is
// the correct configuration for a Standard-mode-only device and must be transparent. A
// filter that could not be configured away would force a latency cost on a device the
// specification never asked to pay it.
module i2c_spike_filter_tb; // Verilog-2001
localparam T_SP = 5;
localparam CNT_W = 8;
reg clk = 1'b0;
always #5 clk = ~clk;
reg rst_n = 1'b0;
reg raw_in = 1'b1;
wire filtered_out, spike_rejected;
wire [CNT_W-1:0] n_spikes;
// The Standard-mode instance: no spike suppression required, so no latency paid.
wire filtered_pass, spike_pass;
wire [CNT_W-1:0] n_spikes_pass;
integer errors = 0;
integer i = 0, t0 = 0, t1 = 0;
reg [CNT_W-1:0] spikes_before;
i2c_spike_filter #(.T_SP(T_SP), .CNT_W(CNT_W)) dut (
.clk(clk), .rst_n(rst_n), .raw_in(raw_in),
.filtered_out(filtered_out), .spike_rejected(spike_rejected), .n_spikes(n_spikes));
i2c_spike_filter #(.T_SP(0), .CNT_W(CNT_W)) dut_pass (
.clk(clk), .rst_n(rst_n), .raw_in(raw_in),
.filtered_out(filtered_pass), .spike_rejected(spike_pass), .n_spikes(n_spikes_pass));
initial begin #500000; $display("FAIL: watchdog expired"); $finish; end
task tick;
input integer n;
begin repeat (n) @(negedge clk); end
endtask
// A pulse of `w` ticks to the opposite of the current steady value, then back.
task pulse;
input v;
input integer w;
begin
raw_in = v; tick(w);
raw_in = ~v; tick(30); // settle well clear of the filter's window
end
endtask
initial begin
tick(3);
// An idle I2C line is HIGH, so a filter must reset HIGH. Resetting LOW would put a
// START condition on SDA at every reset.
if (filtered_out !== 1'b1 || filtered_pass !== 1'b1) begin
$display("FAIL: the filter did not reset HIGH -- a reset would glitch the bus");
errors = errors + 1; end
rst_n = 1'b1; tick(2);
raw_in = 1'b1; tick(20);
// ---- 1: a SPIKE well inside the limit is rejected. The output must never move.
begin
spikes_before = n_spikes;
pulse(1'b0, 2);
if (n_spikes !== spikes_before + 8'd1) begin
$display("FAIL: a 2-tick spike was not counted"); errors = errors + 1; end
end
// ---- 2: a spike of EXACTLY tSP must be REJECTED. The specification says pulses UP
// TO tSP must be suppressed, so the widest rejected pulse is tSP itself -- an
// implementation that adopted at exactly tSP would propagate the very pulse the
// table tells it to reject. This is the boundary that matters, and it runs the
// opposite way from every minimum boundary in this module.
begin
spikes_before = n_spikes;
raw_in = 1'b0;
for (i = 0; i < T_SP; i = i + 1) begin
@(negedge clk);
if (filtered_out !== 1'b1) begin
$display("FAIL: the output moved %0d ticks into a tSP-wide spike", i + 1);
errors = errors + 1; end
end
raw_in = 1'b1; tick(30);
if (n_spikes !== spikes_before + 8'd1) begin
$display("FAIL: a spike of exactly tSP (%0d ticks) was not rejected", T_SP);
errors = errors + 1; end
end
// ---- 3: a pulse ONE TICK WIDER than tSP must PROPAGATE. Together with test 2 this
// pins the threshold down; either test alone passes on a filter that is off by
// one in the other direction.
begin
spikes_before = n_spikes;
raw_in = 1'b0;
tick(T_SP + 2); // comfortably past the threshold
if (filtered_out !== 1'b0) begin
$display("FAIL: a pulse wider than tSP did not propagate"); errors = errors + 1; end
if (n_spikes !== spikes_before) begin
$display("FAIL: a legitimate transition was counted as a spike"); errors = errors + 1; end
raw_in = 1'b1; tick(30);
end
// ---- 4: THE LATENCY, measured. A legitimate transition appears at the output
// T_SP + 1 ticks after it appeared at the input. This is the cost the filter
// imposes on the timing budget, and it is worth measuring rather than assuming
// because it is the number Chapter 11.9 has to spend.
begin
raw_in = 1'b1; tick(20);
t0 = 0; t1 = 0;
raw_in = 1'b0;
for (i = 1; i <= 4 * (T_SP + 4); i = i + 1) begin
@(negedge clk);
if (filtered_out === 1'b0 && t1 == 0) t1 = i;
end
if (t1 !== T_SP + 1) begin
$display("FAIL: the filter latency measured %0d ticks, expected T_SP + 1 = %0d",
t1, T_SP + 1); errors = errors + 1; end
raw_in = 1'b1; tick(30);
end
// ---- 5: SEVERAL spikes in a row are all rejected, and the count is exact. A filter
// whose counter did not reset between excursions could accumulate two short
// spikes into one adopted transition -- which would be worse than no filter,
// because it would invent an edge that was never on the wire.
begin
spikes_before = n_spikes;
raw_in = 1'b1; tick(20);
for (i = 0; i < 6; i = i + 1) begin
raw_in = 1'b0; tick(2);
raw_in = 1'b1; tick(2);
end
tick(30);
if (filtered_out !== 1'b1) begin
$display("FAIL: six short spikes combined into an adopted transition");
errors = errors + 1; end
if (n_spikes !== spikes_before + 8'd6) begin
$display("FAIL: six spikes counted as %0d", n_spikes - spikes_before);
errors = errors + 1; end
end
// ---- 6: a spike in the OPPOSITE direction, from a steady LOW. The filter must be
// symmetric -- a design that only counted one polarity would pass every test
// above and fail on a bus whose idle state it was not written for.
begin
raw_in = 1'b0; tick(30); // establish a steady low
if (filtered_out !== 1'b0) begin
$display("FAIL: setup for test 6 -- the output should be low"); errors = errors + 1; end
spikes_before = n_spikes;
pulse(1'b1, 2);
if (filtered_out !== 1'b0) begin
$display("FAIL: an upward spike from a steady low propagated"); errors = errors + 1; end
if (n_spikes !== spikes_before + 8'd1) begin
$display("FAIL: an upward spike was not counted"); errors = errors + 1; end
raw_in = 1'b1; tick(30);
end
// ---- 7: T_SP = 0 is TRANSPARENT. A Standard-mode device has no spike-suppression
// obligation, so it must be able to configure the filter away entirely -- and
// that means zero latency as well as zero suppression. A filter with an
// irreducible one-tick delay would charge a Standard-mode design for something
// the table never asked of it.
begin
raw_in = 1'b1; tick(20);
raw_in = 1'b0;
@(negedge clk);
if (filtered_pass !== 1'b0) begin
$display("FAIL: the T_SP = 0 instance delayed a transition"); errors = errors + 1; end
raw_in = 1'b1;
@(negedge clk);
if (filtered_pass !== 1'b1) begin
$display("FAIL: the T_SP = 0 instance delayed a transition back"); errors = errors + 1; end
if (n_spikes_pass !== {CNT_W{1'b0}}) begin
$display("FAIL: the transparent instance reported %0d spikes", n_spikes_pass);
errors = errors + 1; end
tick(20);
end
// ---- 8: a LONG steady run produces nothing at all. The filter must be quiet on a
// quiet bus; a block that counted spikes while nothing was happening would make
// its own output useless as a signal-integrity indicator.
begin
spikes_before = n_spikes;
raw_in = 1'b1; tick(300);
if (n_spikes !== spikes_before) begin
$display("FAIL: a steady line produced %0d spikes", n_spikes - spikes_before);
errors = errors + 1; end
end
if (errors == 0)
$display("PASS: a pulse of exactly tSP is rejected and one tick wider propagates, latency is T_SP+1, spikes are counted and symmetric, T_SP=0 is transparent");
else $display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
-- THE SPIKE SUPPRESSION FILTER. Every other design in Module 11 observes the bus; this one
-- is in the datapath, and it is a design OBLIGATION rather than a nicety.
--
-- Table 10:
-- tSP "pulse width of spikes that must be suppressed by the input filter"
-- Standard-mode: no requirement
-- Fast-mode: 0 to 50 ns
-- Fast-mode Plus: 0 to 50 ns
--
-- Read the wording carefully, because it is unusual for this table. Most entries constrain
-- what a device may PRODUCE. This one constrains what a device must TOLERATE: a compliant
-- Fast-mode input must reject any pulse up to 50 ns wide. A device without such a filter is
-- not merely less robust -- it is non-compliant, because the specification has told it what
-- its input stage must do.
--
-- Note also that Standard-mode has NO tSP entry at all. Spike suppression arrived with
-- Fast-mode, which is why a Standard-mode-only design may legitimately omit the filter --
-- and why this block must be able to reduce to nothing when T_SP is zero.
--
-- HOW IT WORKS. A new value is adopted only once it has PERSISTED for longer than T_SP
-- samples. If the line returns to its previous value first, the excursion was a spike and
-- is discarded -- the output never moved.
--
-- AND THE PRICE, WHICH IS THE PART THAT MATTERS FOR A TIMING BUDGET: the filter delays
-- every LEGITIMATE transition by T_SP + 1 samples, because it cannot distinguish a real
-- edge from a long spike until enough time has passed to rule the spike out. That latency
-- is not free and it is not hideable. It comes off the same period budget Chapter 11.7
-- accounts for, and it applies to BOTH lines. Chapter 11.9 spends it explicitly.
entity i2c_spike_filter is
generic (
-- Spike width to reject, in sample-clock ticks. Fast-mode's 50 ns is 5 ticks at
-- 100 MHz. Setting this to ZERO makes the filter transparent, which is the correct
-- configuration for a Standard-mode-only device.
T_SP : natural := 5;
CNT_W : positive := 8
);
port (
clk : in std_logic;
rst_n : in std_logic;
raw_in : in std_logic;
filtered_out : out std_logic;
-- Pulses when a spike has been rejected. Worth having: a bus producing spikes has a
-- signal-integrity problem, and a filter that silently absorbs them removes the only
-- evidence. Suppressing a spike and reporting it are different jobs.
spike_rejected : out std_logic;
n_spikes : out unsigned(CNT_W - 1 downto 0)
);
end entity;
architecture rtl of i2c_spike_filter is
-- The candidate value and how long it has persisted. `pending` is high while the input
-- disagrees with the output and the disagreement has not yet lasted long enough.
signal pending : std_logic := '0';
signal persist : unsigned(CNT_W - 1 downto 0) := (others => '0');
signal filt : std_logic := '1';
begin
filtered_out <= filt;
process (clk)
begin
if rising_edge(clk) then
if rst_n = '0' then
-- An idle I2C line is HIGH, so that is the safe reset value: a filter that
-- reset its output LOW would drive a glitch onto the bus at every reset,
-- which for SDA is a START condition nobody asked for.
filt <= '1';
pending <= '0';
persist <= (others => '0');
spike_rejected <= '0';
n_spikes <= (others => '0');
else
spike_rejected <= '0';
if raw_in = filt then
-- The input agrees with the output. If a disagreement was in progress it
-- has ended without lasting long enough -- that is the definition of a
-- spike, so report it and discard it.
if pending = '1' then
spike_rejected <= '1';
n_spikes <= n_spikes + 1;
end if;
pending <= '0';
persist <= (others => '0');
else
-- Adopt the input once it has persisted for MORE than T_SP samples --
-- strictly more, because the specification says pulses UP TO tSP must be
-- suppressed, so a pulse of exactly tSP is a spike and must be rejected.
-- An implementation that adopted at exactly T_SP would propagate the
-- widest pulse the table tells it to reject.
if persist >= to_unsigned(T_SP, CNT_W) then
filt <= raw_in;
pending <= '0';
persist <= (others => '0');
else
pending <= '1';
persist <= persist + 1;
end if;
end if;
end if;
end if;
end process;
end architecture; library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
-- 100 MHz sample clock: one tick is 10 ns, so Fast-mode's tSP of 50 ns is 5 ticks.
--
-- TWO instances are tested: one configured for Fast-mode and one with T_SP = 0, which is the
-- correct configuration for a Standard-mode-only device and must be transparent. A filter
-- that could not be configured away would force a latency cost on a device the specification
-- never asked to pay it.
entity i2c_spike_filter_tb is
end entity;
architecture sim of i2c_spike_filter_tb is
constant T_SP : natural := 5;
constant CNT_W : positive := 8;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal raw_in : std_logic := '1';
signal filtered_out, spike_rejected : std_logic;
signal n_spikes : unsigned(CNT_W - 1 downto 0);
-- The Standard-mode instance: no spike suppression required, so no latency paid.
signal filtered_pass, spike_pass : std_logic;
signal n_spikes_pass : unsigned(CNT_W - 1 downto 0);
signal test_done : std_logic := '0';
begin
dut : entity work.i2c_spike_filter
generic map (T_SP => T_SP, CNT_W => CNT_W)
port map (clk => clk, rst_n => rst_n, raw_in => raw_in,
filtered_out => filtered_out, spike_rejected => spike_rejected,
n_spikes => n_spikes);
dut_pass : entity work.i2c_spike_filter
generic map (T_SP => 0, CNT_W => CNT_W)
port map (clk => clk, rst_n => rst_n, raw_in => raw_in,
filtered_out => filtered_pass, spike_rejected => spike_pass,
n_spikes => n_spikes_pass);
clk <= not clk after 5 ns;
watchdog : process
begin
wait for 500 us;
if test_done = '0' then
report "watchdog expired -- the design never reached the expected state"
severity failure;
end if;
wait;
end process;
stim : process
variable errs : natural := 0;
variable spikes_before : unsigned(CNT_W - 1 downto 0);
variable t1 : natural;
procedure tick (n : in positive) is
begin
for i in 1 to n loop wait until falling_edge(clk); end loop;
end procedure;
-- A pulse of `w` ticks to the opposite of the current steady value, then back.
procedure pulse (v : in std_logic; w : in positive) is
begin
raw_in <= v; tick(w);
raw_in <= not v; tick(30); -- settle well clear of the filter's window
end procedure;
begin
tick(3);
-- An idle I2C line is HIGH, so a filter must reset HIGH. Resetting LOW would put a
-- START condition on SDA at every reset.
if filtered_out /= '1' or filtered_pass /= '1' then
report "the filter did not reset HIGH -- a reset would glitch the bus"
severity error; errs := errs + 1; end if;
rst_n <= '1'; tick(2);
raw_in <= '1'; tick(20);
-- 1: a SPIKE well inside the limit is rejected. The output must never move.
spikes_before := n_spikes;
pulse('0', 2);
if n_spikes /= spikes_before + 1 then
report "a 2-tick spike was not counted" severity error; errs := errs + 1; end if;
if filtered_out /= '1' then
report "a 2-tick spike propagated" severity error; errs := errs + 1; end if;
-- 2: a spike of EXACTLY tSP must be REJECTED. The specification says pulses UP TO
-- tSP must be suppressed, so the widest rejected pulse is tSP itself -- an
-- implementation that adopted at exactly tSP would propagate the very pulse the
-- table tells it to reject. This boundary runs the opposite way from every minimum
-- boundary in this module.
spikes_before := n_spikes;
raw_in <= '0';
for i in 1 to T_SP loop
wait until falling_edge(clk);
if filtered_out /= '1' then
report "the output moved inside a tSP-wide spike" severity error;
errs := errs + 1; end if;
end loop;
raw_in <= '1'; tick(30);
if n_spikes /= spikes_before + 1 then
report "a spike of exactly tSP was not rejected" severity error;
errs := errs + 1; end if;
-- 3: a pulse ONE TICK WIDER than tSP must PROPAGATE. Together with test 2 this pins
-- the threshold down; either alone passes on a filter off by one the other way.
spikes_before := n_spikes;
raw_in <= '0';
tick(T_SP + 2);
if filtered_out /= '0' then
report "a pulse wider than tSP did not propagate" severity error;
errs := errs + 1; end if;
if n_spikes /= spikes_before then
report "a legitimate transition was counted as a spike" severity error;
errs := errs + 1; end if;
raw_in <= '1'; tick(30);
-- 4: THE LATENCY, measured. A legitimate transition appears at the output T_SP + 1
-- ticks after it appeared at the input. This is the cost the filter imposes on the
-- timing budget, and it is worth measuring rather than assuming because it is the
-- number Chapter 11.9 has to spend.
raw_in <= '1'; tick(20);
t1 := 0;
raw_in <= '0';
for i in 1 to 4 * (T_SP + 4) loop
wait until falling_edge(clk);
if filtered_out = '0' and t1 = 0 then t1 := i; end if;
end loop;
if t1 /= T_SP + 1 then
report "the filter latency measured " & integer'image(t1)
& " ticks, expected T_SP + 1 = " & integer'image(T_SP + 1) severity error;
errs := errs + 1; end if;
raw_in <= '1'; tick(30);
-- 5: SEVERAL spikes in a row are all rejected, and the count is exact. A filter whose
-- counter did not reset between excursions could accumulate two short spikes into one
-- adopted transition -- worse than no filter, because it would invent an edge that
-- was never on the wire.
spikes_before := n_spikes;
raw_in <= '1'; tick(20);
for i in 1 to 6 loop
raw_in <= '0'; tick(2);
raw_in <= '1'; tick(2);
end loop;
tick(30);
if filtered_out /= '1' then
report "six short spikes combined into an adopted transition" severity error;
errs := errs + 1; end if;
if n_spikes /= spikes_before + 6 then
report "six spikes were not counted as six" severity error; errs := errs + 1; end if;
-- 6: a spike in the OPPOSITE direction, from a steady LOW. The filter must be
-- symmetric -- a design that only counted one polarity would pass every test above
-- and fail on a bus whose idle state it was not written for.
raw_in <= '0'; tick(30);
if filtered_out /= '0' then
report "setup for test 6 -- the output should be low" severity error;
errs := errs + 1; end if;
spikes_before := n_spikes;
pulse('1', 2);
if filtered_out /= '0' then
report "an upward spike from a steady low propagated" severity error;
errs := errs + 1; end if;
if n_spikes /= spikes_before + 1 then
report "an upward spike was not counted" severity error; errs := errs + 1; end if;
raw_in <= '1'; tick(30);
-- 7: T_SP = 0 is TRANSPARENT. A Standard-mode device has no spike-suppression
-- obligation, so it must be able to configure the filter away entirely -- and that
-- means zero latency as well as zero suppression.
raw_in <= '1'; tick(20);
raw_in <= '0';
wait until falling_edge(clk);
if filtered_pass /= '0' then
report "the T_SP = 0 instance delayed a transition" severity error;
errs := errs + 1; end if;
raw_in <= '1';
wait until falling_edge(clk);
if filtered_pass /= '1' then
report "the T_SP = 0 instance delayed a transition back" severity error;
errs := errs + 1; end if;
if n_spikes_pass /= to_unsigned(0, CNT_W) then
report "the transparent instance reported spikes" severity error;
errs := errs + 1; end if;
tick(20);
-- 8: a LONG steady run produces nothing at all. The filter must be quiet on a quiet
-- bus; a block that counted spikes while nothing was happening would make its own
-- output useless as a signal-integrity indicator.
spikes_before := n_spikes;
raw_in <= '1'; tick(300);
if n_spikes /= spikes_before then
report "a steady line produced spikes" severity error; errs := errs + 1; end if;
if errs = 0 then
report "i2c_spike_filter self-check complete: a pulse of exactly tSP is rejected "
& "and one tick wider propagates, latency is T_SP+1, spikes are counted and "
& "symmetric, T_SP=0 is transparent" severity note;
else
report "i2c_spike_filter self-check FAILED" severity error;
end if;
test_done <= '1';
wait;
end process;
end architecture;6a. Five Decisions Worth Defending
The threshold comparison is >= on the persistence counter, so a pulse of exactly T_SP is rejected. §5's first point; mutation H2 inverts it and §7's tests 4 and 5 are the pair that pins it.
T_SP = 0 is transparent, with exactly one clock of register latency. §5's second point. The degenerate case is checked rather than assumed, because a filter that cannot be configured out is a filter that taxes Standard-mode for nothing.
A change that reverts before the threshold leaves no trace, and it is counted. The persistence counter resets and the output never moves — and the spike increments a counter, because a filter with no count is a filter nobody can audit. §7's tests 2 and 7 assert both halves, and mutation H4 removes only the count.
The output is a register, so the module presents one clean timing boundary regardless of T_SP — and it resets high, matching an idle bus. A filter resetting low drives a spurious falling edge on the first clock after reset release, which on SDA is a START nobody issued. §7's test 1 asserts it and mutation H3 injects it.
Both edges are filtered by the same mechanism. A filter that suppressed only downward spikes — the intuitive case, since ringing undershoot is what Chapter 11.7 §5 describes — would pass upward spikes, and an overshoot ringing back through a threshold produces those too. §7's test 8 drives a positive-going spike from a steady low line and requires it suppressed and counted.
6b. Verified Execution
$ iverilog -g2012 -o d8 i2c_spike_filter.sv i2c_spike_filter_tb.sv && ./d8
PASS: a pulse of exactly tSP is rejected and one tick wider propagates, latency is T_SP+1,
spikes are counted and symmetric, T_SP=0 is transparent
i2c_spike_filter_tb.sv:192: $finish called at 7230000 (1ps)
$ iverilog -g2005 -o v8 i2c_spike_filter.v i2c_spike_filter_tb.v && ./v8
PASS: a pulse of exactly tSP is rejected and one tick wider propagates, latency is T_SP+1,
spikes are counted and symmetric, T_SP=0 is transparent
i2c_spike_filter_tb.v:195: $finish called at 7230000 (1ps)
$ nvc -a i2c_spike_filter.vhd i2c_spike_filter_tb.vhd
$ nvc -e i2c_spike_filter_tb && nvc -r i2c_spike_filter_tb --stop-time=200us
** Note: 7230ns+0: i2c_spike_filter self-check complete: a pulse of exactly tSP is rejected
and one tick wider propagates, latency is T_SP+1, spikes are counted and symmetric, T_SP=0
is transparentAll three at 7230 ns. This is the shortest run of any checker in the module — a filter has very little state — and the only testbench that spends most of its time on boundaries and on a degenerate configuration rather than on scenarios. §8 argues that is why it had no mutation survivors.
7. What the Testbench Proves
The suite instantiates the filter twice: once at T_SP = 5 (Fast-mode's 50 ns at a 100 MHz sample clock) and once at T_SP = 0. The second instance is not a spare — it is the control §2 argues for, and it is driven by the same stimulus.
| # | stimulus | what it establishes |
|---|---|---|
| 1 | reset | the output resets HIGH — a filter resetting low would glitch the bus |
| 2 | a 2-tick spike | suppressed, and counted |
| 3 | a spike of exactly T_SP | rejected — §5's boundary |
| 4 | a pulse of T_SP + 1 | propagates — the other side of the boundary |
| 5 | a legitimate transition | not counted as a spike |
| 6 | that transition's latency | measured as exactly T_SP + 1 |
| 7 | six short spikes in a row | do not combine into an adopted transition; all six counted |
| 8 | an upward spike from a steady low line | suppressed and counted — both polarities |
| 9 | the same stimulus on the T_SP = 0 instance | propagates undelayed, and reports zero spikes |
| 10 | a steady line | produces no spikes |
Test 9 is the most important test in the chapter, for the reason §2's callout gives. Tests 2, 3, 7 and 8 all pass by observing nothing happening, and nothing is also what a miswired stimulus produces. The T_SP = 0 instance takes the identical stimulus and does pass the spikes through, so a pass on the filtered instance means it suppressed something real rather than that nothing was ever injected.
That is why the control is a second instance rather than a second test run. Both filters see the same wires in the same simulation, so there is no possibility of the stimulus differing between the experiment and the control — the classic way a control quietly stops controlling anything.
Tests 3 and 4 are a matched pair and neither is meaningful alone. A filter with the comparison inverted passes test 4 and fails test 3; one that rejects everything passes test 3 and fails test 4. Only the pair locates the boundary.
Test 7 is the accumulation test, and it is the one most likely to be missed. Six spikes arriving back to back must not add up: a persistence counter that failed to reset on each reversal would accumulate across them and eventually adopt a transition that never persisted for T_SP at any point. Mutation H2 is that defect, and a single isolated spike does not reveal it.
Test 1 is a protocol statement disguised as a reset check. The bus idles high, so a filter must reset to 1. A filter resetting low drives a falling edge onto SDA on the first clock after reset release — which on a live bus is a spurious START. Mutation H3 injects it, and it is the kind of defect that only ever appears at power-up, in the field.
Test 6 asserts the latency is exactly T_SP + 1, not "at most". A filter with variable latency would jitter every timing measurement downstream, and Chapter 11.9's budget arithmetic assumes a constant. Mutation H5 makes the T_SP = 0 case cost an extra cycle, which test 9 then catches from the other direction.
8. Mutation Testing
Five defects injected into the SystemVerilog filter.
| # | injected defect | outcome |
|---|---|---|
| H1 | a pulse of exactly tSP is adopted — the widest spike propagates | killed — test 3 |
| H2 | the persistence counter is not reset when the line returns | killed — tests 2 and 7 |
| H3 | the filter resets its output LOW — a START on every reset | killed — test 1 |
| H4 | rejected spikes are not reported | killed — test 2 |
| H5 | T_SP = 0 is no longer transparent — an irreducible one-tick delay | killed — test 9 |
Five injected, five killed, none surviving a first run — the only design in the module with that result, and the reason is worth naming rather than taking as luck.
The tests were written from the boundary inward. A filter has one number in it, so the suite began with "exactly T_SP", "T_SP + 1" and "T_SP = 0" before any scenario was written. Every mutation in the list is a defect at a boundary or at a degenerate configuration, because in a block this small there is nowhere else for a defect to be.
That is the opposite order from the checker chapters, where scenarios came first and boundaries were added afterwards — and it is why several of those needed a second round. The general point: for a block whose behaviour is defined by a threshold, the boundary cases are not an afterthought to the scenarios, they are the specification. Scenarios then confirm the boundary logic survives in context.
H4 is worth a note because it is not a functional defect at all. Suppressing the spike count leaves the filtering perfectly correct — the output is bit-identical. What is lost is the only evidence that the filter is doing anything, and on a real board that count is the difference between "the bus is quiet" and "the bus is noisy and the filter is absorbing it". A filter with no counter is a filter you cannot audit, and test 2 asserts the count precisely so that a silent filter fails.
H5 and test 9 are the same property from two sides. The mutation makes T_SP = 0 cost an extra cycle; the test compares the transparent instance against the filtered one on identical stimulus. Neither the mutation nor the test would be possible without the design treating the degenerate configuration as a first-class case — which §5 argues for on the grounds that Standard-mode, with no tSP requirement at all, is exactly the configuration that must not pay for filtering.
9. Verification Connection — Proving a Non-Event
// The filter's core obligation is negative, and a negative property needs its antecedent to
// pin down exactly the situation being defended. Here: a raw pulse that reverts within T_SP
// must leave the output completely still.
property p_spike_suppressed;
@(posedge clk)
($changed(raw_in) ##[1:T_SP] $changed(raw_in))
|-> $stable(filtered_out) [*T_SP+2];
endproperty
assert property (p_spike_suppressed)
else $error("a pulse of at most tSP reached the output");
// And the matching POSITIVE property, because a filter that suppresses everything satisfies
// the property above perfectly. Section 2's control argument as SVA: the two properties
// together say the filter discriminates, where either alone permits a degenerate filter.
property p_real_edge_passes;
@(posedge clk)
($changed(raw_in) ##1 $stable(raw_in) [*T_SP+1])
|-> ##[0:1] (filtered_out == $past(raw_in, T_SP+1));
endproperty
assert property (p_real_edge_passes)
else $error("a transition that persisted beyond tSP did not reach the output");
// Latency is exactly T_SP+1, not "at most" -- a filter with variable latency would jitter
// every timing measurement downstream, and section 4's budget arithmetic assumes a constant.
property p_latency_exact;
@(posedge clk) $changed(filtered_out) |-> ($past(raw_in, T_SP+1) == filtered_out);
endproperty
assert property (p_latency_exact)
else $error("filter latency is not exactly tSP+1");
// The degenerate configuration, asserted rather than assumed. Section 5's second point: a
// filter that cannot be configured out taxes Standard-mode, which has no tSP requirement.
if (T_SP == 0) begin : g_transparent
assert property (@(posedge clk) filtered_out == $past(raw_in, 1))
else $error("T_SP=0 must be transparent with one clock of latency");
end covergroup i2c_spike_cg with function sample(int pulse_w, int t_sp, bit polarity,
bit bypassed, bit during_settle);
// Pulse width RELATIVE to the threshold, with single-value bins at the boundary. A
// threshold-defined block's coverage must be fine exactly where the decision changes and
// may be coarse elsewhere -- the inverse of how a scenario-driven block is binned.
width: coverpoint (pulse_w - t_sp) {
bins well_under = {[$:-3]};
bins just_under = {-2, -1};
bins exactly = {0}; // must be REJECTED, section 5
bins just_over = {1, 2}; // must be PASSED
bins well_over = {[3:$]};
}
// Both polarities, because section 6a's fifth point: overshoot ringing produces upward
// spikes on a low line, and a one-sided filter passes them.
pol: coverpoint polarity { bins downward = {0}; bins upward = {1}; }
width_x_pol: cross width, pol;
// The BYPASS axis, which is the covergroup form of section 2's positive control. A run
// with no bypassed samples has not demonstrated that any spike ever reached the device,
// so its suppression coverage proves nothing.
control: coverpoint bypassed {
bins filtered = {0};
bins bypassed_control = {1}; // the spike MUST propagate here
}
width_x_control: cross width, control;
// A spike arriving mid-settle, which is test 7's denial-of-service case and is reached
// by a different mechanism than an isolated spike.
collision: coverpoint during_settle { bins mid_transition = {1}; }
endgroupA filter is in the path, so unlike Chapter 11.7's edge rates it is not purely a config-object property — the DUT contains it and is responsible for it.
But the spikes are environment-generated, and they belong in a separate injector rather than in the protocol driver's sequence items. The reason is that a spike is not a protocol event: it has no place in a transaction description, it can occur at any point including in the middle of a bit, and a sequence item carrying "and also emit a 30 ns glitch here" conflates the bus's legal content with its corruption.
The clean structure is a protocol agent driving legal traffic and an independent noise agent injecting spikes, with the scoreboard checking that the protocol outcome is unaffected. That separation is what makes §2's control possible as a configuration — disable the filter, keep the same two agents, and the scoreboard should now fail.
It also matches how the fault actually arrives in silicon: the noise is uncorrelated with the traffic, and a stimulus that can only place spikes at protocol boundaries will never find the mid-bit case that §7's test 7 covers.
10. FPGA and ASIC Implications
The filter is a counter and a comparator per line — about 12 flops at T_SP = 5. It is the smallest block in the module and the only one that must be in the timing path of both bus inputs.
The sample clock sets what tSP you can actually implement, and the quantisation is one-sided. At 100 MHz one tick is 10 ns, so T_SP = 5 gives a 50 ns threshold exactly. At 80 MHz a tick is 12.5 ns and T_SP = 4 gives 50 ns exactly again — but T_SP = 5 would give 62.5 ns, which suppresses pulses the specification permits a device to pass. That is not a compliance failure, since tSP is an upper bound on what must be suppressed, but it adds latency for no benefit. Round the threshold down to the nearest tick, never up.
Choose tSP from the budget, not from the maximum. §4's callout is the argument: the specification permits anything from 0 to 50 ns, and 50 ns costs 60 ns of latency at 100 MHz — 12 % of an entire Fast-mode-Plus low phase. A design with a tight low-phase budget should pick the smallest tSP its board's noise environment allows, and Chapter 11.7 §5 says that environment is partly a consequence of the edge rates the same board chose.
Filter both lines, and filter them identically. Different latencies on SDA and SCL shift every measured interval by the difference, which turns a common-mode delay that cancels into a differential one that does not. Identical filters are the reason Chapter 11.3 §10 can say the measured intervals are unchanged.
The filter's latency belongs in the design's own timing documentation. It is inside tVD;DAT and tVD;ACK, the device never observes it, and it is usually instantiated from a library. A device whose datasheet quotes a tVD;DAT measured from its internal filtered edge is quoting a number the bus does not care about — and §11 is that mistake reaching a customer.
11. Debugging — The Filter That Was Free Until Fast-Mode Plus
Pitfall — a filter latency measured from the wrong edge
// An I2C slave IP, silicon-proven at Standard and Fast mode, being qualified for Fast-mode
// Plus. Its pad ring included a spike filter instantiated at the specification's maximum:
//
// i2c_spike_filter #(.T_SP(5)) u_filt_scl (.clk(clk_100m), .raw_in(scl_pad), ...);
// i2c_spike_filter #(.T_SP(5)) u_filt_sda (.clk(clk_100m), .raw_in(sda_pad), ...);
//
// // T_SP = 5 at 100 MHz = 50 ns, the specification's maximum. "Maximum robustness."
//
// The design's own timing report quoted tVD;DAT as 190 ns, measured in simulation from the
// internal scl_fall pulse to the SDA output register. Comfortably inside Fm+'s 450 ns limit.
//
// The measurement started at the FILTERED edge. The specification's starts at the wire.Standard and Fast mode qualified without incident. Fast-mode Plus failed at one customer and passed at two others, which is the pattern that always costs the most time.
The failing customer saw corrupted read data on roughly one transfer in fifty, on every unit, and only at 1 MHz. Dropping to 400 kHz made it disappear entirely. The two customers who passed were also running 1 MHz, which appeared to exonerate the part.
The datasheet was re-checked against the measured tVD;DAT of 190 ns and found compliant with room to spare. The bus was checked for rise time -- Chapter 11.7's lesson had been learned -- and the failing customer's board measured 95 ns against a 120 ns limit. Legal.
Then somebody added the numbers for the whole low phase rather than checking each parameter against its own limit:
the slave's response from its INTERNAL scl_fall 190 ns tSU;DAT the master needs 50 ns tr measured on that board 95 ns ------------------------------------------------- 335 ns tLOW(min) at Fm+ 500 ns -> 165 ns spare
That closes comfortably. But it measures the response from the FILTERED edge. The slave's logic does not begin until the filter passes the falling edge, which is T_SP + 1 = 6 clocks = 60 ns after SCL actually fell -- and the specification measures tVD;DAT from the wire:
filter latency, ahead of the slave's own logic 60 ns ------------------------------------------------- 395 ns against 500 ns
Still closing -- on THIS board. The customer that failed had a slower device variant whose response was 300 ns rather than 190, and the same 95 ns rise:
response from the internal edge 300 ns filter latency 60 ns tSU;DAT 50 ns tr 95 ns ------------------------------------------------- 505 ns against 500 ns -> DEFICIT 5 ns
And the true tVD;DAT from the pad is 300 + 60 = 360 ns, inside the 450 ns limit. Compliant on every parameter, five nanoseconds short on the sum.
The two customers who passed had rise times of about 40 ns, which bought back the shortfall. The failing customer's board was legal and simply less generous. And the datasheet's response figure was measured from an edge the bus does not know about, so it understated the real tVD;DAT by exactly the filter latency.
tVD;DAT is defined from SCL falling on the WIRE. The design measured its response from the FILTERED edge, so every published figure understated the real tVD;DAT by the filter's T_SP + 1 = 60 ns.
That alone was never a violation -- 360 ns is inside the 450 ns limit -- which is why no per-parameter check found it. The failure was in the SUM: at Fm+ the low phase is only 500 ns, and it has to contain the response, the filter that precedes it, the receiver's setup and the line's settling. No single parameter was violated; the low phase could not hold all of them.
Note which terms do NOT belong in that sum: footnote [3]'s 300 ns hold obligation is a floor on when the transition may occur, measured from the same falling edge as tVD;DAT, so adding the two double-counts the same stretch of time. Chapter 11.4 section 3 is the geometry.
The deeper cause is that T_SP was set to the specification's maximum on the reasoning that more filtering is more robust. It is, and it is not free: 50 ns of suppression costs 60 ns of latency, spent out of the tightest budget in the specification. The parameter's range starts at zero precisely so a designer can make that trade.
12. Common Misconceptions
"tSP is a timing limit like the others." It is the only parameter specifying a behaviour — something a device must do. Verifying it means proving a non-event, which needs a positive control.
"A bigger tSP is more robust." It is more suppression and more latency, in the same proportion. At Fast-mode Plus 50 ns of threshold costs 60 ns of a 500 ns low phase, and §11 is that trade going wrong.
"A filter can suppress spikes without delaying real transitions." It cannot. Distinguishing a spike from a transition requires waiting to see whether it persists, and the waiting is the latency. T_SP + 1 sample clocks, unavoidably.
"The filter's latency cancels out." It cancels for a monitor measuring an interval with both ends filtered. It is additive for a device responding to an edge, because the parameter is defined from the wire and the device starts from the filtered copy.
"A pulse of exactly tSP may be passed." tSP is the width of spikes that must be suppressed, and the range is inclusive. Exactly tSP must be rejected, which makes the comparison >= rather than >.
"tSP = 0 is a meaningless configuration." It is what a Standard-mode build uses, since Standard-mode has no tSP requirement. It must be transparent with one clock of register latency — not an extra cycle, and not combinational.
"Only downward spikes matter." Overshoot rings back through a threshold too, producing upward spikes on a low line. A one-sided filter passes them.
"Per-parameter compliance means the transfer works." Every parameter in §11's case was individually compliant and the low phase still could not hold them all. That is Chapter 11.9's subject.
13. Reason It Through
Why does Standard-mode have no tSP requirement at all?
Because it permits a 1000 ns rise, and an edge that slow cannot ring hard enough to produce a spurious transition. It is the same reason Standard-mode has no tr minimum: the two parameters are the transmitting and receiving halves of one defence against ringing, and neither is needed where edges are slow.
A filter suppresses pulses up to 50 ns. What is the minimum delay it adds to a legitimate transition, and why can it not be less?
At least 50 ns, plus the sampling register — T_SP + 1 sample clocks. It cannot be less because the only thing distinguishing a legitimate transition from a spike is that it persists, and observing persistence takes exactly as long as the threshold.
A monitor measures tSU;DAT after both lines' filters. Is the number right? What about a slave producing a bit from a filtered SCL edge?
The monitor's number is right: both ends of the interval are delayed equally, so the interval is unchanged. The slave's is not — tVD;DAT is defined from SCL falling on the wire, and the slave's logic begins at the filtered edge, so the filter latency sits inside the parameter and the slave never observes it.
A test injects a 30 ns spike and the design behaves correctly. What has it proved?
On its own, nothing. It is consistent with the filter working, with the spike never reaching the device, and with a miswired stimulus. It becomes meaningful only alongside a control in which the same spike, with the filter bypassed, does break something.
At a 80 MHz sample clock, should T_SP be 4 or 5 for a 50 ns requirement?
4, giving exactly 50 ns. T_SP = 5 gives 62.5 ns, which suppresses pulses the specification permits a device to pass — compliant, but paying latency for no benefit. Round a threshold down to the nearest tick, never up.
Why is a spike injector a separate agent from the protocol driver rather than a field on a sequence item?
Because a spike is not a protocol event: it has no place in a transaction description and can occur anywhere, including mid-bit. Putting it in a sequence item conflates legal content with corruption, and restricts spikes to protocol boundaries — so the mid-transition case never gets generated.
14. Understanding Check
15. Summary
tSP is the only parameter that requires an action rather than a limit. A device must suppress spikes, so the block is a filter in the signal path rather than a monitor beside it.
Suppression and latency are one mechanism. Distinguishing a spike from a transition means waiting to see whether it persists, so a filter delays every legitimate edge by T_SP + 1 sample clocks. There is no implementation that avoids it.
The latency cancels for a monitor and is additive for a responding device. tVD;DAT is defined from the wire, and a device's logic starts from the filtered copy — so the filter sits inside the parameter and the device never observes it. Measure response times from the pad.
At Fast-mode Plus the filter can be the term that breaks the low phase. A 300 ns response plus a 60 ns filter, 50 ns of setup and a 120 ns rise is 530 ns against a 500 ns minimum — with a true tVD;DAT of 360 ns that is well inside its limit. Choose tSP from the budget rather than from the maximum; the range starts at zero so the trade is available.
A pulse of exactly tSP must be rejected, which makes the comparison >=. And T_SP = 0 must be transparent with one clock of latency, because that is what a Standard-mode build — which has no tSP requirement — should cost.
Verifying a non-event needs a positive control. Every suppression test passes by observing nothing, and only a bypassed-filter run showing the spike propagate distinguishes a working filter from a stimulus that never arrived.
For a threshold-defined block, the boundaries are the specification. Writing the boundary tests first is why this design had no mutation survivors, where the scenario-first checkers all needed second rounds.
16. What Comes Next
Chapter 11.9 closes the module by doing the thing this chapter has been pointing at: adding the parameters up.
Eight chapters have each established one parameter and its limit, and three of them have found a worst-case sum that does not close — Chapter 11.2's clock envelope with exactly zero slack, Chapter 11.4's Fast-mode-Plus low phase short by exactly tr(max), and this chapter's filter latency turning a 30 ns margin into a 30 ns deficit. Each time the deficit named the term the specification does not expect a design to take at its maximum.
The final chapter builds that arithmetic as hardware: a block that takes a design's actual numbers, checks both budgets, and reports a margin or a deficit as a value. It is the only block in the module that computes rather than measures — and it is where per-parameter compliance is finally shown to be necessary and not sufficient.
Continue learning
Related tutorials
- Related topic
The Shared Bus — Many Targets, Sometimes Many Masters
One pair of conductors serves an entire board. Physically the bus is a broadcast medium — every device sees everything — while participation is logically selective. That gap is the central abstraction of I²C, and it also opens the multi-controller case.
- Related topic
I²C Inside an SoC — Controller, Peripherals and the Software View
Follow a transfer from software to the conductor. A driver writes registers; a controller peripheral turns that into bus activity; completion and failure come back as status and interrupts. Build the register shell in three languages and see where UVM RAL fits.
- Related topic
The Data-Valid Rule — SDA Stable While SCL Is High
One sentence governs every bit on an I²C bus, and it is derived rather than decreed: the receiver needs a settled value at the instant it looks. What falls out is that an SDA edge while SCL is HIGH cannot be data — which is why the bus reserves it for framing.
- Related topic
The START Condition
START is SDA falling while SCL is high, it is generated only by the controller, and it makes the bus busy. Derive what every device must do in response, then build a detector in three languages and find out why its two guard terms and its reset value are all load-bearing.
