Skip to content
VLSI Mentor

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 parametertSP
what it constrainsthe waveform a device produceswhat a device accepts
how it is verifiedmeasure the bus, compareinject a spike, check it had no effect
a passing result looks likea number within a limitnothing happening
the block involveda monitor, outside the patha 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 + 1 sample 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 cycles
Ten intervals showing a raw bus input and the filter's output. The raw input dips low for a single interval early on, a spike, and the output does not follow it. Later the raw input falls and stays low, and the output follows that transition after a delay of three intervals. A region row marks the suppressed spike and the delayed legitimate edge.latency: tSP + 1latency: tSP + 1spike: shorter than tSPspike: shorter than tSPreal edge beginsreal edge beginsoutput follows, tSP+1 lateoutput follows, tSP+1 lateraw infilteredregion00spikespikespikespikewaitwaitwaitrealt0t1t2t3t4t5t6t7t8t9
A spike shorter than tSP is suppressed and never appears on the output. A legitimate transition appears, delayed by the filter's latency. The suppression and the delay are the same mechanism — one cannot be had without the other.

The 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:

stepdelay
SCL actually falls on the wire—
the filter passes the falling edgetSP + 1
the state machine decides the next bitits own logic
the output register drives SDA1 clock
the pad and the line settletr 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

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_spike_filter.sv — the module’s only datapath block — a filter with real latency
   // 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
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_spike_filter_tb.sv — ten scenarios, including the exactly-tSP boundary
   `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
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_spike_filter.v — the same filter in Verilog-2001
   // 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
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_spike_filter_tb.v — the Verilog testbench, structurally identical
   `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
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_spike_filter.vhd — the same filter in VHDL
   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;
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_spike_filter_tb.vhd — the VHDL testbench, single-writer throughout
   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

Azvya Education Pvt. Ltd.VLSI Mentor
terminal — three simulators, one result, one finish time
   $ 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 transparent

All 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.

#stimuluswhat it establishes
1resetthe output resets HIGH — a filter resetting low would glitch the bus
2a 2-tick spikesuppressed, and counted
3a spike of exactly T_SPrejected — §5's boundary
4a pulse of T_SP + 1propagates — the other side of the boundary
5a legitimate transitionnot counted as a spike
6that transition's latencymeasured as exactly T_SP + 1
7six short spikes in a rowdo not combine into an adopted transition; all six counted
8an upward spike from a steady low linesuppressed and counted — both polarities
9the same stimulus on the T_SP = 0 instancepropagates undelayed, and reports zero spikes
10a steady lineproduces 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 defectoutcome
H1a pulse of exactly tSP is adopted — the widest spike propagateskilled — test 3
H2the persistence counter is not reset when the line returnskilled — tests 2 and 7
H3the filter resets its output LOW — a START on every resetkilled — test 1
H4rejected spikes are not reportedkilled — test 2
H5T_SP = 0 is no longer transparent — an irreducible one-tick delaykilled — 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

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_spike_props.sv — properties about what must NOT appear
   // 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
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_spike_cov.sv — pulse widths around the threshold, and the control
   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}; }
   endgroup

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
Buggy Code
// 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.
Symptom

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.

Root Cause

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