Skip to content
VLSI Mentor

I²C · Module 19

Oversampling, Edge Detection and Spike Filtering

A synchronizer reports every disturbance faithfully, including the ones the specification lets you ignore. Builds an agreement-counter filter whose threshold is one number with one meaning, works out what that number must be at a given system clock, and shows why the upper bound — not rejecting legal traffic — matters as much as the lower one.

Chapter 19.4 ended on a test, T8, whose whole purpose was to prove a limitation: a one-clock spike goes into a synchronizer and a clean one-clock pulse comes out. The synchronizer did its job perfectly, and the disturbance is still there — now with crisp edges and a definite width, which is arguably worse than before.

This chapter is the mechanism that removes it, and the arithmetic that stops you removing too much.

1. Two Numbers, and the Gap Between Them

The specification's position is narrower than most people assume, and Chapter 11.8 works it out properly: tSP is 0 … 50 ns, and what it mandates is not that you have a filter. It mandates that if pulses get through your input, the widest one that does must be at most 50 ns. A device that suppresses nothing is compliant.

So there are two numbers and an implementation decision between them:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  a disturbance of ≤ 50 ns    → you are permitted to reject it
  a legal SCL HIGH phase      → you must NOT reject it
                                 (600 ns minimum in Fast mode)

The gap between 50 ns and 600 ns is where the threshold goes. It is a wide gap — more than a factor of ten — and that width is the reason this parameter is usually wrong by an order of magnitude rather than by a little.

2. Where the Filter Goes

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   pin ──▶ synchroniser ──▶ filter ──▶ edge detect ──▶ protocol engine
   19.2     19.4             19.5        18.2            18.3 onward

           settles the      rejects     turns a level    acts on events
           sampling flop    short       into one-cycle
                            runs        events

The filter takes the synchronized level, never the pin, and the reason is not stylistic.

3. The Architecture: An Agreement Counter

The filter holds an output level. Each clock the input disagrees with that level, a counter advances. Each clock it agrees, the counter resets to zero. When the counter reaches N_SAMP, the output adopts the new level.

That is the whole mechanism, and it was chosen over the more common shift-register-and-majority-vote for three reasons worth stating:

agreement countermajority of N
acceptance rule"N consecutive clocks of the new level""more than half of the last N samples"
can accept a level the line never held for N clocksnoyes — 3-of-5 accepts 1,0,1,0,1
latencyexactly N_SAMP, always, both edgesdepends on the pattern
cost at large None counter, one registerN registers plus a population count

The second row is the one that matters. A majority window will accept a pattern that was never stable, which means the filtered output can report a level the bus never had. Test T10 drives exactly that pattern — two clocks low, one high, two low — and requires it to be rejected. An agreement counter rejects it because the single agreeing clock resets the run; a majority-of-5 would have counted four lows out of five and accepted.

N_SAMP = 3: one disturbance rejected, one accepted

14 cycles
A fourteen-cycle waveform. The synchronised input goes low for two cycles, returns high for two, then goes low for four, then high for four. The agreement counter rises to two during the first disturbance and resets to zero when the line returns. During the second disturbance it reaches two and the level is adopted on the third clock, at which point the filtered output goes low and a one-cycle fall pulse is emitted. The counter returns to zero on acceptance. The return to high is adopted three clocks later and emits a one-cycle rise.rejected: too shortrejected:too shortaccumulating agreementaccumulatingagreementdisturbance 1 startsdisturbance 1 startsabandoned at 2 < 3abandoned at 2 < 3accepted on the 3rd clockaccepted on the 3rd clockclkline_syncagree_cnt00120012001200line_filtline_fallline_riset0t1t2t3t4t5t6t7t8t9t10t11t12t13
Two details are worth reading off the trace. The fall is emitted at cycle 8 — three clocks after the level first changed on the wire at cycle 6 — and the filter could not emit sooner, because until then it did not yet know this was not another spike. And `agree_cnt` returns to 0 on acceptance rather than resting at 3: the counter measures a run in progress, so adopting the level ends the run.
Figure 1 — SIMULATED, values read from an Icarus trace of the published module at N_SAMP = 3. Two disturbances: the first is two clocks wide, one short of the threshold, and the held level never moves. The second is four clocks wide, so on the third consecutive disagreeing clock the level is adopted and a one-cycle fall is emitted. The return to HIGH costs the same three clocks and emits a rise at cycle 12.

4. The Design

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_glitch_filter.sv — the agreement counter and its edge detector
   // -----------------------------------------------------------------------------
   // i2c_glitch_filter.sv
   // A spike filter for one already-synchronised bus line, plus its edge detector.
   //
   // POSITION IN THE CHAIN, which is the whole reason this is a separate module:
   //
   //   pin  ->  synchroniser (19.4)  ->  THIS FILTER  ->  edge detect  ->  protocol
   //            gives the flop time      rejects short     one-cycle
   //            to settle                disturbances      events
   //
   // It takes the SYNCHRONISED level, never the pin. Filtering an asynchronous signal
   // would mean sampling an unsettled flop N times instead of once, which is more
   // exposure to the problem 19.4 exists to contain, not less.
   //
   // THE ARCHITECTURE: an agreement counter. The filter holds an output level. Each
   // clock the input disagrees with that level, a counter advances; each clock it
   // agrees, the counter resets to zero. When the counter reaches N_SAMP the output
   // adopts the new level.
   //
   // WHY THIS SHAPE and not a shift register with a majority vote:
   //
   //   - the acceptance rule is one number with one meaning: "N consecutive clocks of
   //     the new level". A majority window accepts 3-of-5 patterns that were never
   //     stable, which is harder to reason about and harder to state in a datasheet.
   //   - the cost is exactly N_SAMP clocks of latency, always, on both edges. A
   //     majority window's latency depends on the pattern.
   //   - it is one counter and one register at any N_SAMP, where a shift register is
   //     N_SAMP registers plus a population count.
   //
   // WHAT IT COSTS, stated plainly because this is the parameter that gets set wrong:
   // every real edge is delayed by N_SAMP clocks, IN ADDITION to the synchroniser's
   // latency. Set N_SAMP too high and legal bus activity disappears -- a START whose
   // SDA edge is 3 clocks from its SCL edge cannot be distinguished from a spike by a
   // filter that needs 4 clocks of agreement. The chapter body works the arithmetic;
   // T7 sweeps the boundary; mutation E07 makes the filter eat a real edge.
   //
   // THE GUARANTEE, and its exact form: a disturbance shorter than N_SAMP clocks of
   // the new level NEVER reaches the output. A disturbance of N_SAMP clocks or longer
   // always does. There is no third case and no probabilistic middle -- which is what
   // makes the boundary testable rather than approximately testable.
   // -----------------------------------------------------------------------------

   module i2c_glitch_filter #(
      // Consecutive clocks of the new level required before it is accepted. 1 disables
      // filtering (the output follows the input with one clock of register delay);
      // larger values reject wider disturbances and delay real edges equally.
      parameter int N_SAMP = 3
   ) (
      input  logic clk,
      input  logic rst_n,

      // The SYNCHRONISED line level from Chapter 19.4. Not the pin.
      input  logic line_sync,

      // The filtered level: what the line has been for at least N_SAMP clocks.
      output logic line_filt,

      // One-cycle events on the FILTERED level. Exactly one cycle, which every
      // consumer in Module 18 relies on.
      output logic line_rise,
      output logic line_fall,

      // Diagnostics. `n_rejected` counts disturbances that started to change the level
      // and did not last long enough -- a number worth bringing out to an ILA, because
      // a bus that is working but rejecting thousands of spikes a second is a bus with
      // a signal-integrity problem that has not failed yet.
      output logic [15:0] n_rejected,
      output logic [15:0] n_accepted
   );

      // Clocks the input has disagreed with the held level, consecutively.
      logic [7:0] agree_cnt;
      logic       filt_q;
      logic       filt_d;      // the previous filtered level, for edge detection

      assign line_filt = filt_q;

      always @(posedge clk or negedge rst_n) begin
         if (!rst_n) begin
            // RESET TO BUS-IDLE, for the same reason as Chapter 19.4's chain: an I²C
            // line idles HIGH, and a filter resetting to 0 would present a falling edge
            // at the release of reset, which Chapter 18.3 reads as a START.
            filt_q     <= 1'b1;
            filt_d     <= 1'b1;
            agree_cnt  <= 8'd0;
            line_rise  <= 1'b0;
            line_fall  <= 1'b0;
            n_rejected <= 16'd0;
            n_accepted <= 16'd0;
         end else begin
            filt_d    <= filt_q;
            line_rise <= 1'b0;
            line_fall <= 1'b0;

            if (line_sync == filt_q) begin
               // Agreement with the held level. Any partial run is abandoned, and if a
               // run was genuinely in progress it was a rejected disturbance.
               if (agree_cnt != 8'd0) n_rejected <= n_rejected + 1'b1;
               agree_cnt <= 8'd0;
            end else begin
               // Disagreement: the candidate new level has now persisted one more clock.
               if (agree_cnt + 8'd1 >= N_SAMP[7:0]) begin
                  // Long enough. Adopt it, and emit the edge on the FILTERED level.
                  filt_q     <= line_sync;
                  agree_cnt  <= 8'd0;
                  n_accepted <= n_accepted + 1'b1;
                  if (line_sync) line_rise <= 1'b1;
                  else           line_fall <= 1'b1;
               end else begin
                  agree_cnt <= agree_cnt + 8'd1;
               end
            end
         end
      end

   endmodule

Three points, one of which is inherited from the previous chapter and one of which is a diagnostic worth keeping.

The reset value is the idle level, for exactly the reason 19.4 gave. A filter resetting to zero presents a falling edge on SDA at the release of reset, which Chapter 18.3 reads as a START. Mutation E05 is that one character, and it fails twenty-three checks.

The guarantee has no probabilistic middle. A disturbance shorter than N_SAMP clocks of the new level never reaches the output; one of N_SAMP or longer always does. There is no third case, which is what makes the boundary testable exactly rather than approximately — and Section 6's sweep is what exact testing looks like.

n_rejected is worth bringing out to a pin. A bus that is working but rejecting thousands of spikes a second is a bus with a signal-integrity problem that has not failed yet. This is the counter that turns "it works" into "it works, and here is how much margin is left", and Chapter 19.8 puts it in the probe list for that reason.

5. Choosing N_SAMP

Two bounds, both arithmetic.

The lower bound — rejecting what you are allowed to reject

A 50 ns disturbance, worst-case aligned, can be sampled ⌈50 ns × f_sys⌉ times. To guarantee it is never accepted, N_SAMP must exceed that:

f_sysclock periodsamples a 50 ns pulse can hitminimum N_SAMP
12 MHz83.3 ns12
25 MHz40.0 ns23
50 MHz20.0 ns34
100 MHz10.0 ns56
200 MHz5.0 ns1011

The threshold is a function of your clock, not a constant. A design ported from a 25 MHz board to a 100 MHz one with N_SAMP unchanged has silently stopped meeting the obligation it was written for — and nothing will fail, because a device that suppresses less is still compliant. It just no longer does what its author believed.

The upper bound — not deleting what you must keep

The filter delays every edge by N_SAMP clocks. If that delay approaches the length of a legal SCL phase, the phase itself starts to look like a disturbance:

f_sysshortest legal Fast-mode HIGHspansN_SAMP that rejects a legal clock pulse
50 MHz600 ns30 clocks≥ 31
100 MHz600 ns60 clocks≥ 61

So at 50 MHz the usable window is:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  4  ≤  N_SAMP  ≤  30

6. Verifying a Threshold

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_glitch_filter_tb.sv — the self-checking testbench
   // -----------------------------------------------------------------------------
   // i2c_glitch_filter_tb.sv
   // Independent oracle for i2c_glitch_filter, swept across the acceptance boundary.
   //
   // THE CENTRAL TEST IS A SWEEP, NOT A GLITCH. A bench that injected one narrow pulse
   // and one wide pulse would pass against a filter whose threshold was off by one in
   // either direction, because a single narrow pulse cannot tell "rejects 1 clock" from
   // "rejects 4 clocks". T7 therefore drives EVERY pulse width from 1 to N_SAMP+2 and
   // requires the verdict to flip at exactly N_SAMP -- which is the only stimulus shape
   // that pins a threshold rather than sampling around it.
   //
   // THREE INSTANCES at N_SAMP = 1, 3 and 4, because a threshold that ignored its
   // parameter would pass every test at one width.
   //
   // Every wait is a fixed number of clocks; nothing waits on the DUT.
   // -----------------------------------------------------------------------------
   `timescale 1ns/1ps

   module i2c_glitch_filter_tb;

      logic clk = 1'b0, rst_n = 1'b0;
      logic line = 1'b1;              // the synchronised level driven into all three

      logic f1, r1, fa1, f3, r3, fa3, f4, r4, fa4;
      logic [15:0] rej1, acc1, rej3, acc3, rej4, acc4;

      integer errors = 0;
      integer w, n, obs_edges;

      i2c_glitch_filter #(.N_SAMP(1)) u1 (
         .clk(clk), .rst_n(rst_n), .line_sync(line), .line_filt(f1),
         .line_rise(r1), .line_fall(fa1), .n_rejected(rej1), .n_accepted(acc1));

      i2c_glitch_filter #(.N_SAMP(3)) u3 (
         .clk(clk), .rst_n(rst_n), .line_sync(line), .line_filt(f3),
         .line_rise(r3), .line_fall(fa3), .n_rejected(rej3), .n_accepted(acc3));

      i2c_glitch_filter #(.N_SAMP(4)) u4 (
         .clk(clk), .rst_n(rst_n), .line_sync(line), .line_filt(f4),
         .line_rise(r4), .line_fall(fa4), .n_rejected(rej4), .n_accepted(acc4));

      // Observer: counts every cycle in which the N_SAMP=3 instance emitted an edge, so
      // a filter that emitted two-cycle "pulses" is caught. Module 18's consumers all
      // rely on exactly one cycle.
      integer wide_edges = 0;
      integer n_rise3 = 0, n_fall3 = 0;
      logic r3_d = 1'b0, fa3_d = 1'b0;
      always @(posedge clk) begin
         if (rst_n) begin
            if ((r3 & r3_d) | (fa3 & fa3_d)) wide_edges <= wide_edges + 1;
            if (r3)  n_rise3 <= n_rise3 + 1;
            if (fa3) n_fall3 <= n_fall3 + 1;
         end
         r3_d <= r3; fa3_d <= fa3;
      end

      always #5 clk = ~clk;

      task step; begin @(posedge clk); @(negedge clk); end endtask

      task do_reset;
         begin
            @(negedge clk); rst_n = 1'b0; line = 1'b1;
            step; step;
            @(negedge clk); rst_n = 1'b1; step;
            wide_edges = 0; n_rise3 = 0; n_fall3 = 0;
         end
      endtask

      task ck (input [200*8:1] what, input integer g, input integer e);
         begin
            if (g !== e) begin
               $display("  FAIL %0s: got %0d expected %0d", what, g, e);
               errors = errors + 1;
            end
         end
      endtask

      task ck_idx (input [200*8:1] what, input integer idx,
                   input integer g, input integer e);
         begin
            if (g !== e) begin
               $display("  FAIL %0s[%0d]: got %0d expected %0d", what, idx, g, e);
               errors = errors + 1;
            end
         end
      endtask

      // Drive `line` to `v` across exactly `width` rising edges, then return it.
      //
      // NOTE THE ABSENCE OF A TRAILING `@(negedge clk)`. `step` ends on a negedge, so
      // the loop already leaves the bench between edges; waiting for another negedge
      // before releasing would let one MORE rising edge sample the pulse value, making
      // every width one greater than requested. The first draft did exactly that, and
      // the result was a filter that appeared to accept two-clock disturbances at
      // N_SAMP = 3 -- a bench bug that looked precisely like an off-by-one in the DUT.
      task pulse (input v, input integer width);
         begin
            @(negedge clk); line = v;
            for (n = 0; n < width; n = n + 1) step;
            line = ~v;
         end
      endtask

      initial begin
         $display("=== i2c_glitch_filter: the boundary is the specification ===");

         // ----------------------------------------------------------------
         // T1. RESET IS BUS-IDLE. Same argument as Chapter 19.4's chain: a filter
         //     resetting to 0 presents a falling edge at the release of reset, and
         //     Chapter 18.3 reads that as a START on an idle bus.
         // ----------------------------------------------------------------
         @(negedge clk); rst_n = 1'b0; line = 1'b1; step; step;
         $display("T1  reset holds the idle level, so no edge is manufactured");
         ck("T1 N=1 idles high", f1, 1);
         ck("T1 N=3 idles high", f3, 1);
         ck("T1 N=4 idles high", f4, 1);
         ck("T1 no rise emitted in reset", r3, 0);
         ck("T1 no fall emitted in reset", fa3, 0);
         @(negedge clk); rst_n = 1'b1; step; step;
         ck("T1 still idle after release", f3, 1);
         ck("T1 and no edge was emitted",  fa3, 0);
         ck("T1 nothing counted as accepted", acc3, 0);

         // ----------------------------------------------------------------
         // T2. A SUSTAINED CHANGE IS ACCEPTED, AND COSTS EXACTLY N_SAMP CLOCKS. The
         //     latency is the parameter, checked at all three instances so a filter
         //     with a hard-coded delay is separated from one that uses N_SAMP.
         // ----------------------------------------------------------------
         do_reset;
         @(negedge clk); line = 1'b0;
         for (n = 1; n <= 6; n = n + 1) begin
            @(posedge clk); #1;
            ck_idx("T2 N=1 accepts after 1 clock", n, f1, (n >= 1) ? 0 : 1);
            ck_idx("T2 N=3 accepts after 3 clocks", n, f3, (n >= 3) ? 0 : 1);
            ck_idx("T2 N=4 accepts after 4 clocks", n, f4, (n >= 4) ? 0 : 1);
         end
         $display("T2  acceptance latency is exactly N_SAMP: 1, 3 and 4 clocks");

         // ----------------------------------------------------------------
         // T3. AND SYMMETRICALLY ON THE WAY BACK. A filter that was fast on one edge
         //     and slow on the other would pass T2 and skew every bit period, because
         //     I²C uses the two SCL edges for different purposes.
         // ----------------------------------------------------------------
         @(negedge clk); line = 1'b1;
         for (n = 1; n <= 6; n = n + 1) begin
            @(posedge clk); #1;
            ck_idx("T3 N=1 releases after 1 clock", n, f1, (n >= 1) ? 1 : 0);
            ck_idx("T3 N=3 releases after 3 clocks", n, f3, (n >= 3) ? 1 : 0);
            ck_idx("T3 N=4 releases after 4 clocks", n, f4, (n >= 4) ? 1 : 0);
         end
         $display("T3  rejection is symmetric: both edges cost the same N_SAMP");

         // ----------------------------------------------------------------
         // T4. A ONE-CLOCK SPIKE IS REJECTED AT N_SAMP = 3. This is the test everyone
         //     writes, and on its own it is nearly worthless -- see T7.
         // ----------------------------------------------------------------
         do_reset;
         pulse(1'b0, 1);
         step; step; step; step;
         $display("T4  a one-clock spike never reaches the filtered output");
         ck("T4 N=3 output never moved", f3, 1);
         ck("T4 no fall was emitted",    fa3, 0);
         ck("T4 nothing accepted",       acc3, 0);
         ck("T4 and it was counted as rejected", rej3, 1);

         // ----------------------------------------------------------------
         // T5. THE SAME SPIKE IS ACCEPTED AT N_SAMP = 1. Which proves the rejection in
         //     T4 came from the threshold and not from the spike being unrepresentable.
         //     Without this, T4 would also pass on a filter that ignored its input.
         // ----------------------------------------------------------------
         ck("T5 N=1 did see the same spike", acc1 > 0, 1);

         // ----------------------------------------------------------------
         // T6. AN EDGE IS EXACTLY ONE CYCLE WIDE. Every consumer in Module 18 counts
         //     bits on these pulses; a two-cycle "edge" advances a bit counter twice.
         // ----------------------------------------------------------------
         do_reset;
         pulse(1'b0, 6);
         step; step;
         $display("T6  accepted edges are exactly one cycle wide");
         ck("T6 the fall was seen",        acc3 > 0, 1);
         ck("T6 no edge was ever 2 cycles wide", wide_edges, 0);

         // ----------------------------------------------------------------
         // T7. THE BOUNDARY SWEEP -- THE TEST THAT ACTUALLY PINS THE THRESHOLD.
         //
         //     Every pulse width from 1 to N_SAMP+2 is driven, and the verdict must
         //     flip at exactly N_SAMP. A filter whose threshold were 2 or 4 instead of
         //     3 would pass T4, T5 and T6 unchanged and fail here, at exactly one
         //     width. This is the difference between testing that a filter filters and
         //     testing WHERE it filters.
         //
         //     The rule being pinned: a disturbance of fewer than N_SAMP clocks never
         //     reaches the output; one of N_SAMP or more always does.
         // ----------------------------------------------------------------
         for (w = 1; w <= 5; w = w + 1) begin
            do_reset;
            // Driven inline, and the verdict sampled INSIDE the window: two clocks
            // after the line returns is still short of the return's own N_SAMP, so
            // nothing the return does can have been accepted yet. Waiting longer would
            // mean the check could be satisfied by the return instead of by the
            // disturbance -- which is how the edge-polarity mutation E07 survived the
            // first version of this bench.
            @(negedge clk); line = 1'b0;
            for (n = 0; n < w; n = n + 1) step;
            line = 1'b1;
            step; step;
            // At N_SAMP=3: widths 1 and 2 are rejected, 3 and above accepted.
            ck_idx("T7 N=3 accepts iff width >= 3", w, (acc3 > 0) ? 1 : 0,
                   (w >= 3) ? 1 : 0);
            // At N_SAMP=4: widths 1..3 rejected, 4 and above accepted.
            ck_idx("T7 N=4 accepts iff width >= 4", w, (acc4 > 0) ? 1 : 0,
                   (w >= 4) ? 1 : 0);
            // At N_SAMP=1 every pulse is accepted -- the disable case.
            ck_idx("T7 N=1 accepts every width", w, (acc1 > 0) ? 1 : 0, 1);
            // A rejected disturbance must leave the level untouched.
            if (w < 3) ck_idx("T7 N=3 level untouched when rejected", w, f3, 1);
            // And the edge that IS emitted must be the falling one. Checked here as
            // well as in T8 so a swapped polarity fails on both transitions rather
            // than relying on one of them.
            ck_idx("T7 an accepted fall emits line_fall", w, n_fall3, (w >= 3) ? 1 : 0);
            ck_idx("T7 and never line_rise",             w, n_rise3, 0);
            step; step; step; step;
         end
         $display("T7  the threshold is exactly N_SAMP, pinned by a width sweep 1..5");

         // ----------------------------------------------------------------
         // T8. THE SAME SWEEP ON A RISING DISTURBANCE. A filter with an asymmetric
         //     threshold -- strict on one polarity, lax on the other -- passes T7 and
         //     corrupts one of the two SCL edges. Polarity symmetry has to be tested,
         //     not assumed from the code's shape.
         // ----------------------------------------------------------------
         for (w = 1; w <= 5; w = w + 1) begin
            do_reset;
            // Establish a settled LOW first, then disturb it upward.
            @(negedge clk); line = 1'b0;
            step; step; step; step; step;
            ck_idx("T8 settled low before the disturbance", w, f3, 0);
            n_rise3 = 0; n_fall3 = 0;
            // The disturbance is driven inline rather than through `pulse` so the
            // verdict can be sampled INSIDE the window, before the line's return can be
            // accepted. An earlier draft waited six clocks and then checked the rise
            // count -- long enough for the RETURN transition to be accepted too, so a
            // filter with its edge polarity swapped emitted a "rise" for the return and
            // satisfied a check about the disturbance. The observation window has to end
            // before the next event can start, or the check is about the wrong edge.
            @(negedge clk); line = 1'b1;
            for (n = 0; n < w; n = n + 1) step;
            line = 1'b0;
            // Two clocks is inside the return's own N_SAMP = 3 window, so nothing the
            // return does can have been accepted yet.
            step; step;
            ck_idx("T8 N=3 emits a rise iff width >= 3", w, n_rise3, (w >= 3) ? 1 : 0);
            ck_idx("T8 and never a FALL for a rising disturbance", w, n_fall3, 0);
            step; step; step; step;
         end
         $display("T8  the threshold and the edge polarity are the same both ways");

         // ----------------------------------------------------------------
         // T9. BACK-TO-BACK DISTURBANCES, AND A PARTIAL RUN IS ABANDONED. Two
         //     sub-threshold pulses separated by one agreeing clock must NOT combine
         //     into an accepted change -- the counter has to reset on agreement rather
         //     than accumulate. A filter that summed them would accept a level the line
         //     never held, which is worse than either filtering or not filtering.
         // ----------------------------------------------------------------
         do_reset;
         pulse(1'b0, 2);      // 2 clocks low  -- below the threshold of 3
         step;                // 1 clock high  -- agreement, so the run must reset
         pulse(1'b0, 2);      // 2 more clocks low
         step; step; step; step;
         $display("T9  two sub-threshold disturbances do not add up");
         ck("T9 the level never changed",  f3, 1);
         ck("T9 nothing was accepted",     acc3, 0);
         ck("T9 both were counted rejected", rej3, 2);

         // ----------------------------------------------------------------
         // T10. AND A RUN THAT IS INTERRUPTED AT THE LAST MOMENT. Two clocks of the new
         //      level, one clock back, then two more: still rejected. This is the case a
         //      majority-of-N window would accept and an agreement counter must not.
         // ----------------------------------------------------------------
         do_reset;
         // Written without intervening `@(negedge clk)` waits for the same reason
         // `pulse` has none: `step` already ends on a negedge, so an extra wait would
         // let one more rising edge sample the previous value and every segment would
         // be a clock longer than it reads.
         @(negedge clk); line = 1'b0; step; step;     // 2 rising edges low
         line = 1'b1; step;                          // 1 rising edge high
         line = 1'b0; step; step;                    // 2 more rising edges low
         line = 1'b1; step; step; step;
         $display("T10 an interrupted run is not a run: 4-of-5 low is still rejected");
         ck("T10 the level held",       f3, 1);
         ck("T10 nothing accepted",     acc3, 0);

         if (errors == 0) $display("=== i2c_glitch_filter: ALL CHECKS PASSED ===");
         else             $display("=== i2c_glitch_filter: %0d CHECK(S) FAILED ===", errors);
         $finish;
      end

   endmodule

Three of the ten tests exist because of specific failure modes.

T8 mirrors the sweep on a rising disturbance. A threshold that is strict on one polarity and lax on the other passes T7 completely and corrupts one of the two SCL edges — and I²C uses the two edges for different jobs.

T9 and T10 test that a partial run is abandoned. Two sub-threshold disturbances separated by an agreeing clock must not combine. T10 is the majority-vote case: four lows out of five consecutive samples, rejected, because they were never four consecutive lows.

T6 checks that an emitted edge is exactly one cycle wide, because every bit counter in Module 18 advances on these pulses and a two-cycle "edge" advances them twice.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_glitch_filter.v — the same design in Verilog-2001
   // -----------------------------------------------------------------------------
   // i2c_glitch_filter.v
   // A spike filter for one already-synchronised bus line, plus its edge detector.
   //
   // POSITION IN THE CHAIN, which is the whole reason this is a separate module:
   //
   //   pin  ->  synchroniser (19.4)  ->  THIS FILTER  ->  edge detect  ->  protocol
   //            gives the flop time      rejects short     one-cycle
   //            to settle                disturbances      events
   //
   // It takes the SYNCHRONISED level, never the pin. Filtering an asynchronous signal
   // would mean sampling an unsettled flop N times instead of once, which is more
   // exposure to the problem 19.4 exists to contain, not less.
   //
   // THE ARCHITECTURE: an agreement counter. The filter holds an output level. Each
   // clock the input disagrees with that level, a counter advances; each clock it
   // agrees, the counter resets to zero. When the counter reaches N_SAMP the output
   // adopts the new level.
   //
   // WHY THIS SHAPE and not a shift register with a majority vote:
   //
   //   - the acceptance rule is one number with one meaning: "N consecutive clocks of
   //     the new level". A majority window accepts 3-of-5 patterns that were never
   //     stable, which is harder to reason about and harder to state in a datasheet.
   //   - the cost is exactly N_SAMP clocks of latency, always, on both edges. A
   //     majority window's latency depends on the pattern.
   //   - it is one counter and one register at any N_SAMP, where a shift register is
   //     N_SAMP registers plus a population count.
   //
   // WHAT IT COSTS, stated plainly because this is the parameter that gets set wrong:
   // every real edge is delayed by N_SAMP clocks, IN ADDITION to the synchroniser's
   // latency. Set N_SAMP too high and legal bus activity disappears -- a START whose
   // SDA edge is 3 clocks from its SCL edge cannot be distinguished from a spike by a
   // filter that needs 4 clocks of agreement. The chapter body works the arithmetic;
   // T7 sweeps the boundary; mutation E07 makes the filter eat a real edge.
   //
   // THE GUARANTEE, and its exact form: a disturbance shorter than N_SAMP clocks of
   // the new level NEVER reaches the output. A disturbance of N_SAMP clocks or longer
   // always does. There is no third case and no probabilistic middle -- which is what
   // makes the boundary testable rather than approximately testable.
   //
   // (Verilog-2001 -- structurally identical to the SystemVerilog above.)
   // -----------------------------------------------------------------------------

   module i2c_glitch_filter #(
      // Consecutive clocks of the new level required before it is accepted. 1 disables
      // filtering (the output follows the input with one clock of register delay);
      // larger values reject wider disturbances and delay real edges equally.
      parameter N_SAMP = 3
   ) (
      input  wire  clk,
      input  wire  rst_n,

      // The SYNCHRONISED line level from Chapter 19.4. Not the pin.
      input  wire  line_sync,

      // The filtered level: what the line has been for at least N_SAMP clocks.
      output wire  line_filt,

      // One-cycle events on the FILTERED level. Exactly one cycle, which every
      // consumer in Module 18 relies on.
      output reg   line_rise,
      output reg   line_fall,

      // Diagnostics. `n_rejected` counts disturbances that started to change the level
      // and did not last long enough -- a number worth bringing out to an ILA, because
      // a bus that is working but rejecting thousands of spikes a second is a bus with
      // a signal-integrity problem that has not failed yet.
      output reg   [15:0] n_rejected,
      output reg   [15:0] n_accepted
   );

      // Clocks the input has disagreed with the held level, consecutively.
      reg  [7:0] agree_cnt;
      reg        filt_q;
      reg        filt_d;      // the previous filtered level, for edge detection

      assign line_filt = filt_q;

      always @(posedge clk or negedge rst_n) begin
         if (!rst_n) begin
            // RESET TO BUS-IDLE, for the same reason as Chapter 19.4's chain: an I²C
            // line idles HIGH, and a filter resetting to 0 would present a falling edge
            // at the release of reset, which Chapter 18.3 reads as a START.
            filt_q     <= 1'b1;
            filt_d     <= 1'b1;
            agree_cnt  <= 8'd0;
            line_rise  <= 1'b0;
            line_fall  <= 1'b0;
            n_rejected <= 16'd0;
            n_accepted <= 16'd0;
         end else begin
            filt_d    <= filt_q;
            line_rise <= 1'b0;
            line_fall <= 1'b0;

            if (line_sync == filt_q) begin
               // Agreement with the held level. Any partial run is abandoned, and if a
               // run was genuinely in progress it was a rejected disturbance.
               if (agree_cnt != 8'd0) n_rejected <= n_rejected + 1'b1;
               agree_cnt <= 8'd0;
            end else begin
               // Disagreement: the candidate new level has now persisted one more clock.
               if (agree_cnt + 8'd1 >= N_SAMP[7:0]) begin
                  // Long enough. Adopt it, and emit the edge on the FILTERED level.
                  filt_q     <= line_sync;
                  agree_cnt  <= 8'd0;
                  n_accepted <= n_accepted + 1'b1;
                  if (line_sync) line_rise <= 1'b1;
                  else           line_fall <= 1'b1;
               end else begin
                  agree_cnt <= agree_cnt + 8'd1;
               end
            end
         end
      end

   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_glitch_filter_tb.v — the same tests in Verilog-2001
   // -----------------------------------------------------------------------------
   // i2c_glitch_filter_tb.v
   // Independent oracle for i2c_glitch_filter, swept across the acceptance boundary.
   //
   // THE CENTRAL TEST IS A SWEEP, NOT A GLITCH. A bench that injected one narrow pulse
   // and one wide pulse would pass against a filter whose threshold was off by one in
   // either direction, because a single narrow pulse cannot tell "rejects 1 clock" from
   // "rejects 4 clocks". T7 therefore drives EVERY pulse width from 1 to N_SAMP+2 and
   // requires the verdict to flip at exactly N_SAMP -- which is the only stimulus shape
   // that pins a threshold rather than sampling around it.
   //
   // THREE INSTANCES at N_SAMP = 1, 3 and 4, because a threshold that ignored its
   // parameter would pass every test at one width.
   //
   // Every wait is a fixed number of clocks; nothing waits on the DUT.
   //
   // (Verilog-2001 -- the same tests as the SystemVerilog bench.)
   // -----------------------------------------------------------------------------
   `timescale 1ns/1ps

   module i2c_glitch_filter_tb;

      reg  clk = 1'b0, rst_n = 1'b0;
      reg  line = 1'b1;              // the synchronised level driven into all three

      wire f1, r1, fa1, f3, r3, fa3, f4, r4, fa4;
      wire [15:0] rej1, acc1, rej3, acc3, rej4, acc4;

      integer errors = 0;
      integer w, n, obs_edges;

      i2c_glitch_filter #(.N_SAMP(1)) u1 (
         .clk(clk), .rst_n(rst_n), .line_sync(line), .line_filt(f1),
         .line_rise(r1), .line_fall(fa1), .n_rejected(rej1), .n_accepted(acc1));

      i2c_glitch_filter #(.N_SAMP(3)) u3 (
         .clk(clk), .rst_n(rst_n), .line_sync(line), .line_filt(f3),
         .line_rise(r3), .line_fall(fa3), .n_rejected(rej3), .n_accepted(acc3));

      i2c_glitch_filter #(.N_SAMP(4)) u4 (
         .clk(clk), .rst_n(rst_n), .line_sync(line), .line_filt(f4),
         .line_rise(r4), .line_fall(fa4), .n_rejected(rej4), .n_accepted(acc4));

      // Observer: counts every cycle in which the N_SAMP=3 instance emitted an edge, so
      // a filter that emitted two-cycle "pulses" is caught. Module 18's consumers all
      // rely on exactly one cycle.
      integer wide_edges = 0;
      integer n_rise3 = 0, n_fall3 = 0;
      reg  r3_d = 1'b0, fa3_d = 1'b0;
      always @(posedge clk) begin
         if (rst_n) begin
            if ((r3 & r3_d) | (fa3 & fa3_d)) wide_edges <= wide_edges + 1;
            if (r3)  n_rise3 <= n_rise3 + 1;
            if (fa3) n_fall3 <= n_fall3 + 1;
         end
         r3_d <= r3; fa3_d <= fa3;
      end

      always #5 clk = ~clk;

      task step; begin @(posedge clk); @(negedge clk); end endtask

      task do_reset;
         begin
            @(negedge clk); rst_n = 1'b0; line = 1'b1;
            step; step;
            @(negedge clk); rst_n = 1'b1; step;
            wide_edges = 0; n_rise3 = 0; n_fall3 = 0;
         end
      endtask

      task ck (input [200*8:1] what, input integer g, input integer e);
         begin
            if (g !== e) begin
               $display("  FAIL %0s: got %0d expected %0d", what, g, e);
               errors = errors + 1;
            end
         end
      endtask

      task ck_idx (input [200*8:1] what, input integer idx,
                   input integer g, input integer e);
         begin
            if (g !== e) begin
               $display("  FAIL %0s[%0d]: got %0d expected %0d", what, idx, g, e);
               errors = errors + 1;
            end
         end
      endtask

      // Drive `line` to `v` across exactly `width` rising edges, then return it.
      //
      // NOTE THE ABSENCE OF A TRAILING `@(negedge clk)`. `step` ends on a negedge, so
      // the loop already leaves the bench between edges; waiting for another negedge
      // before releasing would let one MORE rising edge sample the pulse value, making
      // every width one greater than requested. The first draft did exactly that, and
      // the result was a filter that appeared to accept two-clock disturbances at
      // N_SAMP = 3 -- a bench bug that looked precisely like an off-by-one in the DUT.
      task pulse (input v, input integer width);
         begin
            @(negedge clk); line = v;
            for (n = 0; n < width; n = n + 1) step;
            line = ~v;
         end
      endtask

      initial begin
         $display("=== i2c_glitch_filter: the boundary is the specification ===");

         // ----------------------------------------------------------------
         // T1. RESET IS BUS-IDLE. Same argument as Chapter 19.4's chain: a filter
         //     resetting to 0 presents a falling edge at the release of reset, and
         //     Chapter 18.3 reads that as a START on an idle bus.
         // ----------------------------------------------------------------
         @(negedge clk); rst_n = 1'b0; line = 1'b1; step; step;
         $display("T1  reset holds the idle level, so no edge is manufactured");
         ck("T1 N=1 idles high", f1, 1);
         ck("T1 N=3 idles high", f3, 1);
         ck("T1 N=4 idles high", f4, 1);
         ck("T1 no rise emitted in reset", r3, 0);
         ck("T1 no fall emitted in reset", fa3, 0);
         @(negedge clk); rst_n = 1'b1; step; step;
         ck("T1 still idle after release", f3, 1);
         ck("T1 and no edge was emitted",  fa3, 0);
         ck("T1 nothing counted as accepted", acc3, 0);

         // ----------------------------------------------------------------
         // T2. A SUSTAINED CHANGE IS ACCEPTED, AND COSTS EXACTLY N_SAMP CLOCKS. The
         //     latency is the parameter, checked at all three instances so a filter
         //     with a hard-coded delay is separated from one that uses N_SAMP.
         // ----------------------------------------------------------------
         do_reset;
         @(negedge clk); line = 1'b0;
         for (n = 1; n <= 6; n = n + 1) begin
            @(posedge clk); #1;
            ck_idx("T2 N=1 accepts after 1 clock", n, f1, (n >= 1) ? 0 : 1);
            ck_idx("T2 N=3 accepts after 3 clocks", n, f3, (n >= 3) ? 0 : 1);
            ck_idx("T2 N=4 accepts after 4 clocks", n, f4, (n >= 4) ? 0 : 1);
         end
         $display("T2  acceptance latency is exactly N_SAMP: 1, 3 and 4 clocks");

         // ----------------------------------------------------------------
         // T3. AND SYMMETRICALLY ON THE WAY BACK. A filter that was fast on one edge
         //     and slow on the other would pass T2 and skew every bit period, because
         //     I²C uses the two SCL edges for different purposes.
         // ----------------------------------------------------------------
         @(negedge clk); line = 1'b1;
         for (n = 1; n <= 6; n = n + 1) begin
            @(posedge clk); #1;
            ck_idx("T3 N=1 releases after 1 clock", n, f1, (n >= 1) ? 1 : 0);
            ck_idx("T3 N=3 releases after 3 clocks", n, f3, (n >= 3) ? 1 : 0);
            ck_idx("T3 N=4 releases after 4 clocks", n, f4, (n >= 4) ? 1 : 0);
         end
         $display("T3  rejection is symmetric: both edges cost the same N_SAMP");

         // ----------------------------------------------------------------
         // T4. A ONE-CLOCK SPIKE IS REJECTED AT N_SAMP = 3. This is the test everyone
         //     writes, and on its own it is nearly worthless -- see T7.
         // ----------------------------------------------------------------
         do_reset;
         pulse(1'b0, 1);
         step; step; step; step;
         $display("T4  a one-clock spike never reaches the filtered output");
         ck("T4 N=3 output never moved", f3, 1);
         ck("T4 no fall was emitted",    fa3, 0);
         ck("T4 nothing accepted",       acc3, 0);
         ck("T4 and it was counted as rejected", rej3, 1);

         // ----------------------------------------------------------------
         // T5. THE SAME SPIKE IS ACCEPTED AT N_SAMP = 1. Which proves the rejection in
         //     T4 came from the threshold and not from the spike being unrepresentable.
         //     Without this, T4 would also pass on a filter that ignored its input.
         // ----------------------------------------------------------------
         ck("T5 N=1 did see the same spike", acc1 > 0, 1);

         // ----------------------------------------------------------------
         // T6. AN EDGE IS EXACTLY ONE CYCLE WIDE. Every consumer in Module 18 counts
         //     bits on these pulses; a two-cycle "edge" advances a bit counter twice.
         // ----------------------------------------------------------------
         do_reset;
         pulse(1'b0, 6);
         step; step;
         $display("T6  accepted edges are exactly one cycle wide");
         ck("T6 the fall was seen",        acc3 > 0, 1);
         ck("T6 no edge was ever 2 cycles wide", wide_edges, 0);

         // ----------------------------------------------------------------
         // T7. THE BOUNDARY SWEEP -- THE TEST THAT ACTUALLY PINS THE THRESHOLD.
         //
         //     Every pulse width from 1 to N_SAMP+2 is driven, and the verdict must
         //     flip at exactly N_SAMP. A filter whose threshold were 2 or 4 instead of
         //     3 would pass T4, T5 and T6 unchanged and fail here, at exactly one
         //     width. This is the difference between testing that a filter filters and
         //     testing WHERE it filters.
         //
         //     The rule being pinned: a disturbance of fewer than N_SAMP clocks never
         //     reaches the output; one of N_SAMP or more always does.
         // ----------------------------------------------------------------
         for (w = 1; w <= 5; w = w + 1) begin
            do_reset;
            // Driven inline, and the verdict sampled INSIDE the window: two clocks
            // after the line returns is still short of the return's own N_SAMP, so
            // nothing the return does can have been accepted yet. Waiting longer would
            // mean the check could be satisfied by the return instead of by the
            // disturbance -- which is how the edge-polarity mutation E07 survived the
            // first version of this bench.
            @(negedge clk); line = 1'b0;
            for (n = 0; n < w; n = n + 1) step;
            line = 1'b1;
            step; step;
            // At N_SAMP=3: widths 1 and 2 are rejected, 3 and above accepted.
            ck_idx("T7 N=3 accepts iff width >= 3", w, (acc3 > 0) ? 1 : 0,
                   (w >= 3) ? 1 : 0);
            // At N_SAMP=4: widths 1..3 rejected, 4 and above accepted.
            ck_idx("T7 N=4 accepts iff width >= 4", w, (acc4 > 0) ? 1 : 0,
                   (w >= 4) ? 1 : 0);
            // At N_SAMP=1 every pulse is accepted -- the disable case.
            ck_idx("T7 N=1 accepts every width", w, (acc1 > 0) ? 1 : 0, 1);
            // A rejected disturbance must leave the level untouched.
            if (w < 3) ck_idx("T7 N=3 level untouched when rejected", w, f3, 1);
            // And the edge that IS emitted must be the falling one. Checked here as
            // well as in T8 so a swapped polarity fails on both transitions rather
            // than relying on one of them.
            ck_idx("T7 an accepted fall emits line_fall", w, n_fall3, (w >= 3) ? 1 : 0);
            ck_idx("T7 and never line_rise",             w, n_rise3, 0);
            step; step; step; step;
         end
         $display("T7  the threshold is exactly N_SAMP, pinned by a width sweep 1..5");

         // ----------------------------------------------------------------
         // T8. THE SAME SWEEP ON A RISING DISTURBANCE. A filter with an asymmetric
         //     threshold -- strict on one polarity, lax on the other -- passes T7 and
         //     corrupts one of the two SCL edges. Polarity symmetry has to be tested,
         //     not assumed from the code's shape.
         // ----------------------------------------------------------------
         for (w = 1; w <= 5; w = w + 1) begin
            do_reset;
            // Establish a settled LOW first, then disturb it upward.
            @(negedge clk); line = 1'b0;
            step; step; step; step; step;
            ck_idx("T8 settled low before the disturbance", w, f3, 0);
            n_rise3 = 0; n_fall3 = 0;
            // The disturbance is driven inline rather than through `pulse` so the
            // verdict can be sampled INSIDE the window, before the line's return can be
            // accepted. An earlier draft waited six clocks and then checked the rise
            // count -- long enough for the RETURN transition to be accepted too, so a
            // filter with its edge polarity swapped emitted a "rise" for the return and
            // satisfied a check about the disturbance. The observation window has to end
            // before the next event can start, or the check is about the wrong edge.
            @(negedge clk); line = 1'b1;
            for (n = 0; n < w; n = n + 1) step;
            line = 1'b0;
            // Two clocks is inside the return's own N_SAMP = 3 window, so nothing the
            // return does can have been accepted yet.
            step; step;
            ck_idx("T8 N=3 emits a rise iff width >= 3", w, n_rise3, (w >= 3) ? 1 : 0);
            ck_idx("T8 and never a FALL for a rising disturbance", w, n_fall3, 0);
            step; step; step; step;
         end
         $display("T8  the threshold and the edge polarity are the same both ways");

         // ----------------------------------------------------------------
         // T9. BACK-TO-BACK DISTURBANCES, AND A PARTIAL RUN IS ABANDONED. Two
         //     sub-threshold pulses separated by one agreeing clock must NOT combine
         //     into an accepted change -- the counter has to reset on agreement rather
         //     than accumulate. A filter that summed them would accept a level the line
         //     never held, which is worse than either filtering or not filtering.
         // ----------------------------------------------------------------
         do_reset;
         pulse(1'b0, 2);      // 2 clocks low  -- below the threshold of 3
         step;                // 1 clock high  -- agreement, so the run must reset
         pulse(1'b0, 2);      // 2 more clocks low
         step; step; step; step;
         $display("T9  two sub-threshold disturbances do not add up");
         ck("T9 the level never changed",  f3, 1);
         ck("T9 nothing was accepted",     acc3, 0);
         ck("T9 both were counted rejected", rej3, 2);

         // ----------------------------------------------------------------
         // T10. AND A RUN THAT IS INTERRUPTED AT THE LAST MOMENT. Two clocks of the new
         //      level, one clock back, then two more: still rejected. This is the case a
         //      majority-of-N window would accept and an agreement counter must not.
         // ----------------------------------------------------------------
         do_reset;
         // Written without intervening `@(negedge clk)` waits for the same reason
         // `pulse` has none: `step` already ends on a negedge, so an extra wait would
         // let one more rising edge sample the previous value and every segment would
         // be a clock longer than it reads.
         @(negedge clk); line = 1'b0; step; step;     // 2 rising edges low
         line = 1'b1; step;                          // 1 rising edge high
         line = 1'b0; step; step;                    // 2 more rising edges low
         line = 1'b1; step; step; step;
         $display("T10 an interrupted run is not a run: 4-of-5 low is still rejected");
         ck("T10 the level held",       f3, 1);
         ck("T10 nothing accepted",     acc3, 0);

         if (errors == 0) $display("=== i2c_glitch_filter: ALL CHECKS PASSED ===");
         else             $display("=== i2c_glitch_filter: %0d CHECK(S) FAILED ===", errors);
         $finish;
      end

   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_glitch_filter.vhd — the same design in VHDL
   -- -----------------------------------------------------------------------------
   -- i2c_glitch_filter.vhd
   -- A spike filter for one already-synchronised bus line, plus its edge detector.
   -- Behavioural twin of the SystemVerilog and Verilog designs.
   --
   -- POSITION IN THE CHAIN:
   --
   --   pin -> synchroniser (19.4) -> THIS FILTER -> edge detect -> protocol
   --
   -- It takes the SYNCHRONISED level, never the pin. Filtering an asynchronous signal
   -- would mean sampling an unsettled flop N times instead of once.
   --
   -- THE ARCHITECTURE: an agreement counter. Each clock the input disagrees with the
   -- held level the counter advances; each clock it agrees the counter resets to zero.
   -- At N_SAMP the output adopts the new level. The guarantee has no probabilistic
   -- middle: a disturbance shorter than N_SAMP clocks NEVER reaches the output, one of
   -- N_SAMP or longer ALWAYS does -- which is what makes the boundary testable.
   --
   -- WHAT IT COSTS: every real edge is delayed by N_SAMP clocks, in addition to the
   -- synchroniser's latency. Set N_SAMP too high and legal bus activity disappears.
   -- -----------------------------------------------------------------------------
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   entity i2c_glitch_filter is
      generic (
         -- Consecutive clocks of the new level required before it is accepted. 1
         -- disables filtering; larger values reject wider disturbances and delay real
         -- edges equally.
         N_SAMP : positive := 3
      );
      port (
         clk   : in std_logic;
         rst_n : in std_logic;

         -- The SYNCHRONISED line level from Chapter 19.4. Not the pin.
         line_sync : in std_logic;

         -- The filtered level: what the line has been for at least N_SAMP clocks.
         line_filt : out std_logic;

         -- One-cycle events on the FILTERED level.
         line_rise : out std_logic;
         line_fall : out std_logic;

         -- Diagnostics. A bus that works but rejects thousands of spikes a second is a
         -- bus with a signal-integrity problem that has not failed yet.
         n_rejected : out unsigned(15 downto 0);
         n_accepted : out unsigned(15 downto 0)
      );
   end entity i2c_glitch_filter;

   architecture rtl of i2c_glitch_filter is
      signal agree_cnt : unsigned(7 downto 0) := (others => '0');
      signal filt_q    : std_logic := '1';
      signal filt_d    : std_logic := '1';
      signal rej_i     : unsigned(15 downto 0) := (others => '0');
      signal acc_i     : unsigned(15 downto 0) := (others => '0');
   begin

      line_filt  <= filt_q;
      n_rejected <= rej_i;
      n_accepted <= acc_i;

      process (clk, rst_n)
      begin
         if rst_n = '0' then
            -- RESET TO BUS-IDLE, for the same reason as Chapter 19.4's chain: a filter
            -- resetting to '0' presents a falling edge at the release of reset, which
            -- Chapter 18.3 reads as a START.
            filt_q    <= '1';
            filt_d    <= '1';
            agree_cnt <= (others => '0');
            line_rise <= '0';
            line_fall <= '0';
            rej_i     <= (others => '0');
            acc_i     <= (others => '0');
         elsif rising_edge(clk) then
            filt_d    <= filt_q;
            line_rise <= '0';
            line_fall <= '0';

            if line_sync = filt_q then
               -- Agreement: any partial run is abandoned, and a run that was genuinely
               -- in progress was a rejected disturbance.
               if agree_cnt /= 0 then
                  rej_i <= rej_i + 1;
               end if;
               agree_cnt <= (others => '0');
            else
               -- Disagreement: the candidate level has persisted one more clock.
               if to_integer(agree_cnt) + 1 >= N_SAMP then
                  filt_q    <= line_sync;
                  agree_cnt <= (others => '0');
                  acc_i     <= acc_i + 1;
                  if line_sync = '1' then
                     line_rise <= '1';
                  else
                     line_fall <= '1';
                  end if;
               else
                  agree_cnt <= agree_cnt + 1;
               end if;
            end if;
         end if;
      end process;

   end architecture rtl;
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_glitch_filter_tb.vhd — the same tests in VHDL
   -- -----------------------------------------------------------------------------
   -- i2c_glitch_filter_tb.vhd
   -- Independent oracle for i2c_glitch_filter, swept across the acceptance boundary.
   -- Behavioural twin of the SystemVerilog and Verilog benches.
   --
   -- THE CENTRAL TEST IS A SWEEP, NOT A GLITCH. One narrow pulse and one wide pulse
   -- would pass against a filter whose threshold was off by one in either direction.
   -- T7 drives EVERY width from 1 to 5 and requires the verdict to flip at exactly
   -- N_SAMP, which is the only stimulus shape that pins a threshold.
   --
   -- THREE INSTANCES at N_SAMP = 1, 3 and 4, because a threshold that ignored its
   -- parameter would pass every test at one width.
   -- -----------------------------------------------------------------------------
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   entity i2c_glitch_filter_tb is
   end entity i2c_glitch_filter_tb;

   architecture sim of i2c_glitch_filter_tb is

      signal clk   : std_logic := '0';
      signal rst_n : std_logic := '0';
      signal line  : std_logic := '1';

      signal f1, r1, fa1, f3, r3, fa3, f4, r4, fa4 : std_logic;
      signal rej1, acc1, rej3, acc3, rej4, acc4 : unsigned(15 downto 0);

      signal wide_edges : integer := 0;
      signal n_rise3, n_fall3 : integer := 0;
      signal clr_obs : boolean := false;

      signal halt : boolean := false;

   begin

      clkgen : process
      begin
         while not halt loop
            clk <= '0'; wait for 5 ns;
            clk <= '1'; wait for 5 ns;
         end loop;
         wait;
      end process;

      u1 : entity work.i2c_glitch_filter
         generic map (N_SAMP => 1)
         port map (clk => clk, rst_n => rst_n, line_sync => line, line_filt => f1,
                   line_rise => r1, line_fall => fa1, n_rejected => rej1,
                   n_accepted => acc1);

      u3 : entity work.i2c_glitch_filter
         generic map (N_SAMP => 3)
         port map (clk => clk, rst_n => rst_n, line_sync => line, line_filt => f3,
                   line_rise => r3, line_fall => fa3, n_rejected => rej3,
                   n_accepted => acc3);

      u4 : entity work.i2c_glitch_filter
         generic map (N_SAMP => 4)
         port map (clk => clk, rst_n => rst_n, line_sync => line, line_filt => f4,
                   line_rise => r4, line_fall => fa4, n_rejected => rej4,
                   n_accepted => acc4);

      -- Observer: counts edges emitted by the N_SAMP=3 instance, and any edge that was
      -- asserted for two consecutive clocks. Module 18's consumers all rely on exactly
      -- one cycle.
      obs : process (clk, clr_obs)
         variable r_d, f_d : std_logic := '0';
      begin
         if clr_obs then
            wide_edges <= 0; n_rise3 <= 0; n_fall3 <= 0;
         elsif rising_edge(clk) then
            if rst_n = '1' then
               if (r3 = '1' and r_d = '1') or (fa3 = '1' and f_d = '1') then
                  wide_edges <= wide_edges + 1;
               end if;
               if r3  = '1' then n_rise3 <= n_rise3 + 1; end if;
               if fa3 = '1' then n_fall3 <= n_fall3 + 1; end if;
            end if;
            r_d := r3; f_d := fa3;
         end if;
      end process;

      stim : process
         variable err : integer := 0;

         function b2i (b : std_logic) return integer is
         begin
            if b = '1' then return 1; else return 0; end if;
         end function;

         function gt0 (u : unsigned) return integer is
         begin
            if u > 0 then return 1; else return 0; end if;
         end function;

         procedure step is
         begin
            wait until rising_edge(clk);
            wait until falling_edge(clk);
         end procedure;

         procedure clear_obs is
         begin
            clr_obs <= true; wait for 1 ns; clr_obs <= false; wait for 1 ns;
         end procedure;

         procedure do_reset is
         begin
            wait until falling_edge(clk);
            rst_n <= '0'; line <= '1';
            step; step;
            wait until falling_edge(clk);
            rst_n <= '1';
            step;
            clear_obs;
         end procedure;

         procedure ck (what : string; g : integer; e : integer) is
         begin
            if g /= e then
               report "  FAIL " & what & ": got " & integer'image(g)
                      & " expected " & integer'image(e) severity note;
               err := err + 1;
            end if;
         end procedure;

         procedure ck_idx (what : string; idx : integer; g : integer; e : integer) is
         begin
            if g /= e then
               report "  FAIL " & what & "[" & integer'image(idx) & "]: got "
                      & integer'image(g) & " expected " & integer'image(e) severity note;
               err := err + 1;
            end if;
         end procedure;

      begin
         report "=== i2c_glitch_filter: the boundary is the specification ===" severity note;

         -- T1. Reset is bus-idle. A filter resetting to '0' presents a falling edge at
         --     the release of reset, and Chapter 18.3 reads that as a START.
         wait until falling_edge(clk);
         rst_n <= '0'; line <= '1'; step; step;
         report "T1  reset holds the idle level, so no edge is manufactured" severity note;
         ck("T1 N=1 idles high", b2i(f1), 1);
         ck("T1 N=3 idles high", b2i(f3), 1);
         ck("T1 N=4 idles high", b2i(f4), 1);
         ck("T1 no rise emitted in reset", b2i(r3), 0);
         ck("T1 no fall emitted in reset", b2i(fa3), 0);
         wait until falling_edge(clk); rst_n <= '1'; step; step;
         ck("T1 still idle after release", b2i(f3), 1);
         ck("T1 and no edge was emitted",  b2i(fa3), 0);
         ck("T1 nothing counted as accepted", to_integer(acc3), 0);

         -- T2. A sustained change is accepted, and costs exactly N_SAMP clocks.
         do_reset;
         wait until falling_edge(clk); line <= '0';
         for n in 1 to 6 loop
            wait until rising_edge(clk); wait for 1 ns;
            if n >= 1 then ck_idx("T2 N=1 accepts after 1 clock", n, b2i(f1), 0);
            else           ck_idx("T2 N=1 accepts after 1 clock", n, b2i(f1), 1); end if;
            if n >= 3 then ck_idx("T2 N=3 accepts after 3 clocks", n, b2i(f3), 0);
            else           ck_idx("T2 N=3 accepts after 3 clocks", n, b2i(f3), 1); end if;
            if n >= 4 then ck_idx("T2 N=4 accepts after 4 clocks", n, b2i(f4), 0);
            else           ck_idx("T2 N=4 accepts after 4 clocks", n, b2i(f4), 1); end if;
         end loop;
         report "T2  acceptance latency is exactly N_SAMP: 1, 3 and 4 clocks" severity note;

         -- T3. And symmetrically on the way back. A filter fast on one edge and slow on
         --     the other would pass T2 and skew every bit period.
         wait until falling_edge(clk); line <= '1';
         for n in 1 to 6 loop
            wait until rising_edge(clk); wait for 1 ns;
            if n >= 1 then ck_idx("T3 N=1 releases after 1 clock", n, b2i(f1), 1);
            else           ck_idx("T3 N=1 releases after 1 clock", n, b2i(f1), 0); end if;
            if n >= 3 then ck_idx("T3 N=3 releases after 3 clocks", n, b2i(f3), 1);
            else           ck_idx("T3 N=3 releases after 3 clocks", n, b2i(f3), 0); end if;
            if n >= 4 then ck_idx("T3 N=4 releases after 4 clocks", n, b2i(f4), 1);
            else           ck_idx("T3 N=4 releases after 4 clocks", n, b2i(f4), 0); end if;
         end loop;
         report "T3  rejection is symmetric: both edges cost the same N_SAMP" severity note;

         -- T4. A one-clock spike is rejected at N_SAMP = 3. This is the test everyone
         --     writes, and on its own it is nearly worthless -- see T7.
         do_reset;
         wait until falling_edge(clk); line <= '0';
         step;
         line <= '1';
         step; step; step; step;
         report "T4  a one-clock spike never reaches the filtered output" severity note;
         ck("T4 N=3 output never moved", b2i(f3), 1);
         ck("T4 no fall was emitted",    b2i(fa3), 0);
         ck("T4 nothing accepted",       to_integer(acc3), 0);
         ck("T4 and it was counted as rejected", to_integer(rej3), 1);

         -- T5. The same spike IS accepted at N_SAMP = 1, which proves the rejection in
         --     T4 came from the threshold and not from the spike being unrepresentable.
         ck("T5 N=1 did see the same spike", gt0(acc1), 1);

         -- T6. An accepted edge is exactly one cycle wide. Every consumer in Module 18
         --     counts bits on these pulses; a two-cycle edge advances a counter twice.
         do_reset;
         wait until falling_edge(clk); line <= '0';
         for n in 1 to 6 loop step; end loop;
         line <= '1';
         step; step;
         report "T6  accepted edges are exactly one cycle wide" severity note;
         ck("T6 the fall was seen",              gt0(acc3), 1);
         ck("T6 no edge was ever 2 cycles wide", wide_edges, 0);

         -- T7. THE BOUNDARY SWEEP -- the test that actually pins the threshold.
         --
         --     Every width from 1 to 5 is driven and the verdict must flip at exactly
         --     N_SAMP. A filter whose threshold were 2 or 4 instead of 3 would pass
         --     T4, T5 and T6 unchanged and fail here at exactly one width.
         --
         --     The verdict is sampled INSIDE the window: two clocks after the line
         --     returns is still short of the return's own N_SAMP, so nothing the return
         --     does can have been accepted yet. Waiting longer would let the check be
         --     satisfied by the return instead of by the disturbance, which is how the
         --     edge-polarity mutation survived the first version of this bench.
         for w in 1 to 5 loop
            do_reset;
            wait until falling_edge(clk); line <= '0';
            for n in 1 to w loop step; end loop;
            line <= '1';
            step; step;
            if w >= 3 then ck_idx("T7 N=3 accepts iff width >= 3", w, gt0(acc3), 1);
            else           ck_idx("T7 N=3 accepts iff width >= 3", w, gt0(acc3), 0); end if;
            if w >= 4 then ck_idx("T7 N=4 accepts iff width >= 4", w, gt0(acc4), 1);
            else           ck_idx("T7 N=4 accepts iff width >= 4", w, gt0(acc4), 0); end if;
            ck_idx("T7 N=1 accepts every width", w, gt0(acc1), 1);
            if w < 3 then
               ck_idx("T7 N=3 level untouched when rejected", w, b2i(f3), 1);
            end if;
            -- The edge that IS emitted must be the falling one.
            if w >= 3 then ck_idx("T7 an accepted fall emits line_fall", w, n_fall3, 1);
            else           ck_idx("T7 an accepted fall emits line_fall", w, n_fall3, 0); end if;
            ck_idx("T7 and never line_rise", w, n_rise3, 0);
            step; step; step; step;
         end loop;
         report "T7  the threshold is exactly N_SAMP, pinned by a width sweep 1..5"
                severity note;

         -- T8. The same sweep on a RISING disturbance. A filter with an asymmetric
         --     threshold -- strict on one polarity, lax on the other -- passes T7 and
         --     corrupts one of the two SCL edges.
         for w in 1 to 5 loop
            do_reset;
            wait until falling_edge(clk); line <= '0';
            for n in 1 to 5 loop step; end loop;
            ck_idx("T8 settled low before the disturbance", w, b2i(f3), 0);
            clear_obs;
            wait until falling_edge(clk); line <= '1';
            for n in 1 to w loop step; end loop;
            line <= '0';
            step; step;
            if w >= 3 then ck_idx("T8 N=3 emits a rise iff width >= 3", w, n_rise3, 1);
            else           ck_idx("T8 N=3 emits a rise iff width >= 3", w, n_rise3, 0); end if;
            ck_idx("T8 and never a FALL for a rising disturbance", w, n_fall3, 0);
            step; step; step; step;
         end loop;
         report "T8  the threshold and the edge polarity are the same both ways"
                severity note;

         -- T9. Back-to-back disturbances: a partial run is abandoned. Two sub-threshold
         --     pulses separated by an agreeing clock must NOT combine into an accepted
         --     change -- the counter resets on agreement rather than accumulating.
         do_reset;
         wait until falling_edge(clk); line <= '0'; step; step;
         line <= '1'; step; step;
         line <= '0'; step; step;
         line <= '1'; step; step; step; step;
         report "T9  two sub-threshold disturbances do not add up" severity note;
         ck("T9 the level never changed",     b2i(f3), 1);
         ck("T9 nothing was accepted",        to_integer(acc3), 0);
         ck("T9 both were counted rejected",  to_integer(rej3), 2);

         -- T10. A run interrupted at the last moment. Two clocks of the new level, one
         --      clock back, then two more: still rejected. This is the case a
         --      majority-of-N window would accept and an agreement counter must not.
         do_reset;
         wait until falling_edge(clk); line <= '0'; step; step;
         line <= '1'; step;
         line <= '0'; step; step;
         line <= '1'; step; step; step;
         report "T10 an interrupted run is not a run: 4-of-5 low is still rejected"
                severity note;
         ck("T10 the level held",   b2i(f3), 1);
         ck("T10 nothing accepted", to_integer(acc3), 0);

         if err = 0 then
            report "=== i2c_glitch_filter: ALL CHECKS PASSED ===" severity note;
         else
            report "=== i2c_glitch_filter: " & integer'image(err) & " CHECK(S) FAILED ==="
                   severity note;
         end if;
         halt <= true;
         wait;
      end process;

   end architecture sim;

7. What the Mutations Found

#mutationverdictcaught by
E01threshold one too laxKILLED (11)T7 — at exactly one width
E02threshold one too strictKILLED (12)T7 — at exactly one width
E03threshold hard-coded to 3KILLED (10)T7 at N_SAMP = 1 and 4
E04counter accumulates, no reset on agreementKILLED (3)T9/T10 — two spikes adding up
E05reset to 0KILLED (23)T1 — a START invented on an idle bus
E06edge pulses never clearedKILLED (12)T6 — two-cycle edges
E07rise and fall swappedKILLED (12)T7/T8 — see below
E08filter never adopts the new levelKILLED (31)T2/T3
E09rejection counter tied offKILLED (2)T4 — a dead diagnostic
E10asymmetric: falls accepted instantlyKILLED (22)T3/T8

8. Focused Verification Insight

Coverage at this layer is the boundary, not the protocol. The bins that matter are: a disturbance of exactly N_SAMP − 1 clocks (rejected), exactly N_SAMP (accepted), each polarity, and an interrupted run. Those four are the filter's entire contract. A coverage model built on "spikes injected" without partitioning by width measures effort, not the property.

n_rejected is the one signal a monitor should watch that has nothing to do with correctness. It is a margin indicator. Module 20's environment can assert that a clean bench run leaves it at zero — and a soak test that leaves it non-zero has found a stimulus-integrity problem in the bench, which is worth knowing before it is mistaken for a DUT bug.

Assertions, as concepts — Icarus supports no concurrent assertions, so none of this is executed:

Azvya Education Pvt. Ltd.VLSI Mentor
filter_properties.sv — assertion CONCEPT, not executed
   // The filtered level changes only after N_SAMP consecutive disagreeing clocks.
   // Written for N_SAMP = 3; the general form needs a local variable or a sequence
   // repetition with a parameterised count.
   property p_no_change_without_persistence;
      @(posedge clk) disable iff (!rst_n)
         $changed(line_filt) |-> $past(line_sync, 1) == line_filt
                              && $past(line_sync, 2) == line_filt
                              && $past(line_sync, 3) == line_filt;
   endproperty

   // Edges are exactly one cycle and mutually exclusive.
   property p_edge_is_one_cycle;
      @(posedge clk) line_fall |=> !line_fall;
   endproperty
   property p_edges_exclusive;
      @(posedge clk) not (line_rise && line_fall);
   endproperty

9. Misconceptions

10. Debugging

The port to the faster board stopped rejecting anything

Pitfall — a filter threshold in clocks, carried across a change of clock frequency
Buggy Code
// An I2C target, working, on a 25 MHz board. The filter threshold was chosen
// deliberately and documented:
//
//     localparam N_SAMP = 3;   // rejects <= 50 ns spikes at 25 MHz (40 ns/clock)
//
// The comment is correct at 25 MHz: 50 ns spans at most 2 samples, so 3 consecutive
// disagreeing clocks cannot be produced by a 50 ns disturbance.
//
// The design is then moved to a new board with a 100 MHz system clock, because an
// unrelated processing block needed the throughput. The I2C block is untouched --
// it is a verified, self-contained module with a parameter, and nobody changed the
// parameter.
//
// At 100 MHz the clock period is 10 ns, so a 50 ns disturbance spans FIVE samples.
// N_SAMP = 3 now accepts it.
Symptom

Nothing fails. That is the entire problem.

The block passes its own regression -- the bench instantiates N_SAMP = 1, 3 and 4 and every test passes, because the tests are written in CLOCKS and the relationship between clocks and nanoseconds is not in the bench at all.

On the bench and on the first boards, the bus works. In the field, a subset of units in electrically noisy installations show occasional corrupted register reads. The rate is roughly one transfer in ten thousand, it correlates with a nearby motor drive, and it is absent on every unit in the lab.

An ILA capture with n_rejected on it shows the real state: the counter is incrementing thousands of times per second on affected units, and -- crucially -- n_accepted occasionally increments during a bus idle period, which is the fingerprint of a disturbance being taken for a real edge.

The unit that was never wrong is the specification: a device that suppresses less is still compliant. Nothing in the toolchain, the simulation or the standard objects to what happened.

Root Cause

N_SAMP is a count of clocks; the obligation it implements is a duration in nanoseconds. The two are related by the system clock, and the system clock changed by 4x while the parameter did not.

25 MHz, 40 ns/clock: a 50 ns pulse hits at most 2 samples -> N_SAMP=3 rejects it 100 MHz, 10 ns/clock: a 50 ns pulse hits at most 5 samples -> N_SAMP=3 ACCEPTS it

The module is not wrong, the bench is not wrong, and the comment was accurate when written. What was missing is that the parameter's correct value is DERIVED from something outside the module, and nothing in the module recorded the derivation in a form a tool could check.

Two fixes, and the second is the real one.

The immediate fix is N_SAMP = 6 at 100 MHz, from the table in Section 5.

The structural fix is to stop carrying the threshold as a raw clock count. Give the module the clock frequency and the duration it must reject, and let it compute the count -- so a change of clock changes the count automatically, and a configuration that cannot satisfy the requirement can be made to fail at elaboration rather than in the field:

parameter integer CLK_HZ = 100_000_000; parameter integer TSP_NS = 50; localparam integer N_SAMP = ((TSP_NS * (CLK_HZ / 1_000_000)) / 1000) + 1;

That form is not in the published module above, deliberately: this chapter's subject is the threshold's meaning and its two bounds, and a module that computes its own parameter would hide the arithmetic the chapter exists to teach. In a real design, compute it -- and add an elaboration-time check that the result is below the upper bound from Section 5 as well as above the lower one.

The evidence that would have caught it: the ILA's n_rejected and n_accepted, which is why Section 4 argues for bringing both out. A filter with no visible reject count is a filter whose margin nobody can measure.

11. Reason It Through

12. Questions

13. What This Chapter Settled

The specification permits rejecting disturbances up to 50 ns and forbids rejecting legal traffic, and the gap between those is where N_SAMP lives. At 50 MHz that window is 4 to 30 clocks — wide enough that the parameter is rarely marginally wrong and often wrong by an order of magnitude, usually because it was carried across a change of clock frequency as a raw count.

The filter is an agreement counter, so its rule is one sentence and its latency is exactly N_SAMP on both edges. Eighteen mutations across three languages, all killed — including one, the rise/fall swap, that first required the bench's observation window to be narrowed rather than a new test to be added.

The front end is now complete: a pad, a pull-up, a synchronizer and a filter, with every layer verified against the bus model the protocol cores were verified against. What has not been said is what the tool should be told about any of it. SCL toggles, which makes it look like a clock; the synchronizer has a path the tool will happily try to time; and nothing so far has expressed to a timing engine which of these relationships it should check and which it cannot. Chapter 19.6 is that conversation.

Continue learning

Related tutorials