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:
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
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 eventsThe 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 counter | majority 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 clocks | no | yes — 3-of-5 accepts 1,0,1,0,1 |
| latency | exactly N_SAMP, always, both edges | depends on the pattern |
| cost at large N | one counter, one register | N 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 cycles4. The Design
// -----------------------------------------------------------------------------
// 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
endmoduleThree 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_sys | clock period | samples a 50 ns pulse can hit | minimum N_SAMP |
|---|---|---|---|
| 12 MHz | 83.3 ns | 1 | 2 |
| 25 MHz | 40.0 ns | 2 | 3 |
| 50 MHz | 20.0 ns | 3 | 4 |
| 100 MHz | 10.0 ns | 5 | 6 |
| 200 MHz | 5.0 ns | 10 | 11 |
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_sys | shortest legal Fast-mode HIGH | spans | N_SAMP that rejects a legal clock pulse |
|---|---|---|---|
| 50 MHz | 600 ns | 30 clocks | ≥ 31 |
| 100 MHz | 600 ns | 60 clocks | ≥ 61 |
So at 50 MHz the usable window is:
4 ≤ N_SAMP ≤ 306. Verifying a Threshold
// -----------------------------------------------------------------------------
// 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
endmoduleThree 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.
// -----------------------------------------------------------------------------
// 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 // -----------------------------------------------------------------------------
// 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 -- -----------------------------------------------------------------------------
-- 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; -- -----------------------------------------------------------------------------
-- 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
| # | mutation | verdict | caught by |
|---|---|---|---|
| E01 | threshold one too lax | KILLED (11) | T7 — at exactly one width |
| E02 | threshold one too strict | KILLED (12) | T7 — at exactly one width |
| E03 | threshold hard-coded to 3 | KILLED (10) | T7 at N_SAMP = 1 and 4 |
| E04 | counter accumulates, no reset on agreement | KILLED (3) | T9/T10 — two spikes adding up |
| E05 | reset to 0 | KILLED (23) | T1 — a START invented on an idle bus |
| E06 | edge pulses never cleared | KILLED (12) | T6 — two-cycle edges |
| E07 | rise and fall swapped | KILLED (12) | T7/T8 — see below |
| E08 | filter never adopts the new level | KILLED (31) | T2/T3 |
| E09 | rejection counter tied off | KILLED (2) | T4 — a dead diagnostic |
| E10 | asymmetric: falls accepted instantly | KILLED (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:
// 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);
endproperty9. 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
// 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.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.
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
- Related topic
Sampling SDA and SCL — Edge Detection Inside a Slave
The block every other one in a target is written against: two asynchronous wires turned into five synchronous facts. Three of its decisions produce defects nothing later can recover from — what the samplers reset to, which signals the edge pulses come from, and why the synchroniser belongs here and nowhere else.
- Related topic
FPGA as Master and as Target — Integration Patterns
Assembles everything: Module 18's verified target behind Module 19's pad, synchronizer and filter, proven end to end on a wired-AND bus in three languages. Shows the controller-side shape on Module 17's real interface, where a soft CPU attaches, and closes with two mutations that cannot be killed in simulation — the module's thesis stated as evidence.
- Related topic
Hardware Bring-Up and On-Chip Debug
What to do on the morning the board arrives, in what order, with what instrument. Builds a synthesizable bus-health block that answers 'is the bus even there' at a glance, works through a probe set that spans layers rather than concentrating on the FSM, and shows why an analyzer and an ILA disagreeing is the most localising evidence available.
- Related topic
Simulation vs Synthesized Hardware — Where Behavior Diverges
Eight classes of mismatch that pass in RTL simulation and fail on a board, each with why simulation passes, what hardware does instead, and what evidence exposes it. Includes a bus model with a rise time, and an experiment running Module 18's verified target against it whose result is not the expected one.
