SPI · Module 15
CDC and Constraint Failure Modes
Six crossings, one correct and five broken. Under a benign stimulus all six pass; under a hostile one three of the five are found; and two pass both — the two that lose data on silicon. That is the argument for CDC review as an activity separate from CDC verification, and it is not an argument about rigour.
Chapter 15.1 ended with two columns of a table that were identical in every row, and a note that the difference between them — one synchroniser flop against two — is the one rule no testbench in this module can check.
This chapter makes that concrete, by building five broken crossings alongside a correct one and measuring all six.
Six designs. One is right. Which of the five faults does a regression find, and which does it not?
1. The Six
HAZ 0 CORRECT a toggle through a two-flop synchroniser for the event,
and a Gray-coded vector.
HAZ 1 ONE FLOP the same design with a one-flop synchroniser.
Functionally identical in every simulation ever run.
HAZ 2 PULSE the event crossed as a one-cycle PULSE instead of a toggle.
Correct whenever the source period exceeds the
destination's, which is most benches.
HAZ 3 BUS the vector through N independent synchronisers instead of
Gray coded. Correct whenever the bits arrive together,
which is every bench without skew.
HAZ 4 NOT GRAY a vector encoded `v ^ (v >> 2)` instead of `v ^ (v >> 1)`.
It looks like a Gray code, it round-trips correctly, and
consecutive values can differ in TWO bits.
HAZ 5 ASYNC RELEASE correct crossing logic, and the destination's reset is
released asynchronously. Correct in every simulation,
because a simulated flop leaving reset leaves it cleanly.Hazard 4 deserves a note now, because it is the most instructive of the five. v ^ (v >> 2) is a bijection, so a decoder written to match it recovers the value perfectly and every data test passes. What it is not is a Gray code: consecutive values can differ in two bits, so the property the encoding exists for is gone while nothing about the data looks wrong. It is the mistake a mistyped shift produces, and it survives every functional check.
2. The Block Diagram
3. The Benign Stimulus
A source slower than the destination, no skew, one event every four source cycles — which is what a reasonable bench does.
haz events bad values what is wrong with it
0 40/40 0/838 nothing
1 40/40 0/838 one synchroniser flop
2 40/40 0/838 a pulse, not a toggle
3 40/40 0/838 a bus, independently synced
4 40/40 0/838 an encoding that is not Gray
5 40/40 0/838 an asynchronous reset releaseAll six pass. Five of them are broken.
That is the measurement this chapter exists for, and it is asserted rather than merely displayed — a bench that only printed the table would let a reader assume the broken ones failed somewhere.
4. The Hostile Stimulus
Built from what Chapter 15.1 and Chapter 15.5 had to learn, and getting it right took one correction worth recording.
haz events bad values verdict what is wrong with it
0 60/60 0/471 passes nothing
1 60/60 0/471 passes one synchroniser flop
2 48/60 0/471 FOUND a pulse, not a toggle
3 60/60 7/471 FOUND a bus, independently synced
4 60/60 4/471 FOUND an encoding that is not Gray
5 60/60 0/471 passes an asynchronous reset releaseThree of the five are found. Hazards 1 and 5 pass both.
5. The Three Kinds Of Fault
found by a HOSTILE STIMULUS 2, 3, 4 a better bench finds these
found by a REVIEW 1, 5 nothing else will
correct 0That split is the chapter's argument, and it is worth being precise about the second row.
Hazard 1 differs from the correct design by a settling time. A one-flop synchroniser can be sampled while its input is changing and drive a metastable value into logic about to decide on it. A simulated flop always captures a defined value, so the two designs are behaviourally identical — in every simulator, at every ratio, with every stimulus, forever.
Hazard 5 differs by a recovery time. An asynchronously released reset lets different flops leave reset on different cycles, so a state machine can come up in a state no encoding defines. A simulated flop leaving reset leaves it cleanly and simultaneously, so again the two designs are identical.
6. Building the Six — Three HDLs
The circuit
One module, six behaviours selected by a parameter, sharing everything else so that the difference is only the crossing. The skew seam is a port pair for Chapter 15.5's reason.
// spi_cdc_hazards.sv
//
// Chapter 15.9 -- the failures that pass simulation and fail on hardware.
//
// Six crossings, selected by a parameter. One is correct and five are broken, and the
// point of the file is that they are not broken in the same WAY:
//
// HAZ 0 CORRECT a toggle through a two-flop synchroniser for the event,
// and a Gray-coded vector.
//
// HAZ 1 ONE FLOP the same design with a one-flop synchroniser. Functionally
// identical in every simulation ever run, at every ratio,
// with every stimulus -- because the difference is a
// settling time and no simulator models one.
//
// HAZ 2 PULSE the event crossed as a one-cycle PULSE instead of a
// toggle. Correct whenever the source period is longer than
// the destination's, which is most benches.
//
// HAZ 3 BUS the vector through N independent synchronisers instead of
// Gray coded. Correct whenever the bits arrive together,
// which is every bench without skew.
//
// HAZ 4 NOT GRAY a vector encoded with `v ^ (v >> 2)` instead of
// `v ^ (v >> 1)`. It looks like a Gray code, it round-trips
// correctly, and consecutive values can differ in two bits
// -- so the guarantee the encoding exists for is gone while
// every functional test still passes.
//
// HAZ 5 ASYNC RELEASE correct crossing logic, and the destination's reset is
// released asynchronously. Correct in every simulation,
// because a simulated flop leaving reset always leaves it
// cleanly.
//
// THE TABLE THIS PRODUCES IS THE CHAPTER.
//
// Under a benign stimulus all six pass. Under a hostile one -- skew on the vector, a
// source faster than the destination -- hazards 2, 3 and 4 fail and are found. Hazards
// 1 and 5 pass BOTH, and they are the two that lose data on silicon.
//
// So the six split into three kinds, and the kinds need three different activities:
//
// found by a HOSTILE STIMULUS 2, 3, 4 a better bench finds these
// found by a REVIEW 1, 5 nothing else will
// correct 0
//
// That is the whole argument for CDC review as a separate activity from CDC
// verification. Not because review is more rigorous, but because two of the five
// faults here are invisible to every simulation that can be written, and a team whose
// only gate is a green regression ships them.
module spi_cdc_hazards #(
parameter int HAZ = 0,
parameter int W = 8,
parameter int SYNC_N = 2
) (
input wire src_clk,
input wire src_rst_n,
input wire src_event, // one source cycle per event
input wire [W-1:0] src_vec, // the value to carry across
input wire dst_clk,
input wire dst_rst_n, // asynchronous assert; release differs by hazard
output reg dst_stb, // one destination cycle per delivered event
output wire [W-1:0] dst_vec, // the recovered value
// --- the seam the skew is injected on -----------------------------------
// In a real design these two are the same net: `enc_out` is the source
// register's output and `enc_in` is what the destination's synchroniser samples.
// They are brought out separately so that a testbench can put SKEW between them,
// which is where routing skew actually lives -- on the wires from one flop to
// another.
//
// The first version of the bench skewed the value BEFORE the source register
// instead, and that corrupted the source: with a skew spread comparable to the
// source's clock period, the source latched a mixture of its own old and new
// value and every crossing including the correct one observed impossible values.
// Skew belongs between two flops, not in front of the first one, and having the
// seam in the port list makes that impossible to get wrong by accident.
output wire [W-1:0] enc_out,
input wire [W-1:0] enc_in
);
// How deep the event synchroniser actually is. Hazard 1 is ONE, and nothing else
// in the file changes -- which is exactly why no simulation distinguishes it.
localparam int ESYNC = (HAZ == 1) ? 1 : SYNC_N;
// --- the source's encoding ----------------------------------------------
reg tog_src; // used by every hazard except 2
reg pulse_src; // hazard 2 only
reg [W-1:0] enc_src;
// Hazard 4's encoding: `v ^ (v >> 2)`. It is a bijection, so a decoder written
// to match it recovers the value perfectly and every data test passes. What it
// is not is a GRAY code: consecutive values can differ in two bits, and the
// whole reason a pointer is Gray coded is that they cannot.
function automatic [W-1:0] encode(input [W-1:0] v);
encode = (HAZ == 3) ? v // hazard 3: no encoding at all
: (HAZ == 4) ? (v ^ (v >> 2)) // hazard 4: looks like Gray
: (v ^ (v >> 1)); // correct
endfunction
function automatic [W-1:0] decode(input [W-1:0] g);
integer k;
begin
if (HAZ == 3) begin
decode = g;
end else if (HAZ == 4) begin
// The inverse of `v ^ (v >> 2)`, so the round trip is exact.
decode[W-1] = g[W-1];
if (W >= 2) decode[W-2] = g[W-2];
for (k = W-3; k >= 0; k = k - 1)
decode[k] = decode[k+2] ^ g[k];
end else begin
decode[W-1] = g[W-1];
for (k = W-2; k >= 0; k = k - 1)
decode[k] = decode[k+1] ^ g[k];
end
end
endfunction
always_ff @(posedge src_clk or negedge src_rst_n) begin
if (!src_rst_n) begin
tog_src <= 1'b0;
pulse_src <= 1'b0;
enc_src <= {W{1'b0}};
end else begin
pulse_src <= src_event; // one cycle wide, and then gone
if (src_event) begin
tog_src <= ~tog_src;
enc_src <= encode(src_vec);
end
end
end
// --- the destination's reset --------------------------------------------
// Hazard 5 releases the destination's reset asynchronously: the raw signal is
// used directly, with no synchroniser, so different flops may leave reset on
// different cycles. Every simulation shows them leaving together, because a
// simulated flop has no recovery time.
reg [1:0] rrel_sr;
wire dst_rst_sync_n = (HAZ == 5) ? dst_rst_n : rrel_sr[1];
always_ff @(posedge dst_clk or negedge dst_rst_n) begin
if (!dst_rst_n) rrel_sr <= 2'b00;
else rrel_sr <= {rrel_sr[0], 1'b1};
end
// --- the destination's recovery -----------------------------------------
// The chain is declared SYNC_N deep and TAPPED at stage ESYNC-1, rather than
// declared ESYNC deep. Declaring it ESYNC deep is the obvious way to write it and
// does not elaborate: at ESYNC = 1 the shift expression's `esr[ESYNC-2:0]` is
// `esr[-1:0]`, a descending slice of nothing. Tapping a full-width chain keeps one
// shift expression for both cases, and the flops the tap does not reach are
// trimmed by synthesis -- so hazard 1 really is one flop in the implementation as
// well as in the behaviour.
reg [SYNC_N-1:0] esr;
reg ed;
reg [W-1:0] vsr [0:SYNC_N-1];
integer vi;
wire arrived = (HAZ == 2) ? (esr[ESYNC-1] & ~ed) // a rising edge on a pulse
: (esr[ESYNC-1] ^ ed); // either edge of a toggle
assign enc_out = enc_src;
assign dst_vec = decode(vsr[SYNC_N-1]);
always_ff @(posedge dst_clk or negedge dst_rst_sync_n) begin
if (!dst_rst_sync_n) begin
esr <= {SYNC_N{1'b0}};
ed <= 1'b0;
dst_stb <= 1'b0;
for (vi = 0; vi < SYNC_N; vi = vi + 1)
vsr[vi] <= {W{1'b0}};
end else begin
esr <= {esr[SYNC_N-2:0], (HAZ == 2) ? pulse_src : tog_src};
ed <= esr[ESYNC-1];
vsr[0] <= enc_in;
for (vi = 1; vi < SYNC_N; vi = vi + 1)
vsr[vi] <= vsr[vi-1];
dst_stb <= arrived;
end
end
endmodule// spi_cdc_hazards.v
//
// Chapter 15.9 -- the failures that pass simulation and fail on hardware.
//
// Six crossings, selected by a parameter. One is correct and five are broken, and the
// point of the file is that they are not broken in the same WAY:
//
// HAZ 0 CORRECT a toggle through a two-flop synchroniser for the event,
// and a Gray-coded vector.
//
// HAZ 1 ONE FLOP the same design with a one-flop synchroniser. Functionally
// identical in every simulation ever run, at every ratio,
// with every stimulus -- because the difference is a
// settling time and no simulator models one.
//
// HAZ 2 PULSE the event crossed as a one-cycle PULSE instead of a
// toggle. Correct whenever the source period is longer than
// the destination's, which is most benches.
//
// HAZ 3 BUS the vector through N independent synchronisers instead of
// Gray coded. Correct whenever the bits arrive together,
// which is every bench without skew.
//
// HAZ 4 NOT GRAY a vector encoded with `v ^ (v >> 2)` instead of
// `v ^ (v >> 1)`. It looks like a Gray code, it round-trips
// correctly, and consecutive values can differ in two bits
// -- so the guarantee the encoding exists for is gone while
// every functional test still passes.
//
// HAZ 5 ASYNC RELEASE correct crossing logic, and the destination's reset is
// released asynchronously. Correct in every simulation,
// because a simulated flop leaving reset always leaves it
// cleanly.
//
// THE TABLE THIS PRODUCES IS THE CHAPTER.
//
// Under a benign stimulus all six pass. Under a hostile one -- skew on the vector, a
// source faster than the destination -- hazards 2, 3 and 4 fail and are found. Hazards
// 1 and 5 pass BOTH, and they are the two that lose data on silicon.
//
// So the six split into three kinds, and the kinds need three different activities:
//
// found by a HOSTILE STIMULUS 2, 3, 4 a better bench finds these
// found by a REVIEW 1, 5 nothing else will
// correct 0
//
// That is the whole argument for CDC review as a separate activity from CDC
// verification. Not because review is more rigorous, but because two of the five
// faults here are invisible to every simulation that can be written, and a team whose
// only gate is a green regression ships them.
module spi_cdc_hazards #(
parameter HAZ = 0,
parameter W = 8,
parameter SYNC_N = 2
) (
input wire src_clk,
input wire src_rst_n,
input wire src_event, // one source cycle per event
input wire [W-1:0] src_vec, // the value to carry across
input wire dst_clk,
input wire dst_rst_n, // asynchronous assert; release differs by hazard
output reg dst_stb, // one destination cycle per delivered event
output wire [W-1:0] dst_vec, // the recovered value
// --- the seam the skew is injected on -----------------------------------
// In a real design these two are the same net: `enc_out` is the source
// register's output and `enc_in` is what the destination's synchroniser samples.
// They are brought out separately so that a testbench can put SKEW between them,
// which is where routing skew actually lives -- on the wires from one flop to
// another.
//
// The first version of the bench skewed the value BEFORE the source register
// instead, and that corrupted the source: with a skew spread comparable to the
// source's clock period, the source latched a mixture of its own old and new
// value and every crossing including the correct one observed impossible values.
// Skew belongs between two flops, not in front of the first one, and having the
// seam in the port list makes that impossible to get wrong by accident.
output wire [W-1:0] enc_out,
input wire [W-1:0] enc_in
);
// How deep the event synchroniser actually is. Hazard 1 is ONE, and nothing else
// in the file changes -- which is exactly why no simulation distinguishes it.
localparam ESYNC = (HAZ == 1) ? 1 : SYNC_N;
// --- the source's encoding ----------------------------------------------
reg tog_src; // used by every hazard except 2
reg pulse_src; // hazard 2 only
reg [W-1:0] enc_src;
// Hazard 4's encoding: `v ^ (v >> 2)`. It is a bijection, so a decoder written
// to match it recovers the value perfectly and every data test passes. What it
// is not is a GRAY code: consecutive values can differ in two bits, and the
// whole reason a pointer is Gray coded is that they cannot.
function [W-1:0] encode;
input [W-1:0] v;
encode = (HAZ == 3) ? v // hazard 3: no encoding at all
: (HAZ == 4) ? (v ^ (v >> 2)) // hazard 4: looks like Gray
: (v ^ (v >> 1)); // correct
endfunction
function [W-1:0] decode;
input [W-1:0] g;
integer k;
begin
if (HAZ == 3) begin
decode = g;
end else if (HAZ == 4) begin
// The inverse of `v ^ (v >> 2)`, so the round trip is exact.
decode[W-1] = g[W-1];
if (W >= 2) decode[W-2] = g[W-2];
for (k = W-3; k >= 0; k = k - 1)
decode[k] = decode[k+2] ^ g[k];
end else begin
decode[W-1] = g[W-1];
for (k = W-2; k >= 0; k = k - 1)
decode[k] = decode[k+1] ^ g[k];
end
end
endfunction
always @(posedge src_clk or negedge src_rst_n) begin
if (!src_rst_n) begin
tog_src <= 1'b0;
pulse_src <= 1'b0;
enc_src <= {W{1'b0}};
end else begin
pulse_src <= src_event; // one cycle wide, and then gone
if (src_event) begin
tog_src <= ~tog_src;
enc_src <= encode(src_vec);
end
end
end
// --- the destination's reset --------------------------------------------
// Hazard 5 releases the destination's reset asynchronously: the raw signal is
// used directly, with no synchroniser, so different flops may leave reset on
// different cycles. Every simulation shows them leaving together, because a
// simulated flop has no recovery time.
reg [1:0] rrel_sr;
wire dst_rst_sync_n = (HAZ == 5) ? dst_rst_n : rrel_sr[1];
always @(posedge dst_clk or negedge dst_rst_n) begin
if (!dst_rst_n) rrel_sr <= 2'b00;
else rrel_sr <= {rrel_sr[0], 1'b1};
end
// --- the destination's recovery -----------------------------------------
// The chain is declared SYNC_N deep and TAPPED at stage ESYNC-1, rather than
// declared ESYNC deep. Declaring it ESYNC deep is the obvious way to write it and
// does not elaborate: at ESYNC = 1 the shift expression's `esr[ESYNC-2:0]` is
// `esr[-1:0]`, a descending slice of nothing. Tapping a full-width chain keeps one
// shift expression for both cases, and the flops the tap does not reach are
// trimmed by synthesis -- so hazard 1 really is one flop in the implementation as
// well as in the behaviour.
reg [SYNC_N-1:0] esr;
reg ed;
reg [W-1:0] vsr [0:SYNC_N-1];
integer vi;
wire arrived = (HAZ == 2) ? (esr[ESYNC-1] & ~ed) // a rising edge on a pulse
: (esr[ESYNC-1] ^ ed); // either edge of a toggle
assign enc_out = enc_src;
assign dst_vec = decode(vsr[SYNC_N-1]);
always @(posedge dst_clk or negedge dst_rst_sync_n) begin
if (!dst_rst_sync_n) begin
esr <= {SYNC_N{1'b0}};
ed <= 1'b0;
dst_stb <= 1'b0;
for (vi = 0; vi < SYNC_N; vi = vi + 1)
vsr[vi] <= {W{1'b0}};
end else begin
esr <= {esr[SYNC_N-2:0], (HAZ == 2) ? pulse_src : tog_src};
ed <= esr[ESYNC-1];
vsr[0] <= enc_in;
for (vi = 1; vi < SYNC_N; vi = vi + 1)
vsr[vi] <= vsr[vi-1];
dst_stb <= arrived;
end
end
endmodule-- spi_cdc_hazards.vhd
--
-- Chapter 15.9 -- the failures that pass simulation and fail on hardware.
--
-- Six crossings, selected by a parameter. One is correct and five are broken, and the
-- point of the file is that they are not broken in the same WAY:
--
-- HAZ 0 CORRECT a toggle through a two-flop synchroniser for the event,
-- and a Gray-coded vector.
--
-- HAZ 1 ONE FLOP the same design with a one-flop synchroniser. Functionally
-- identical in every simulation ever run, at every ratio,
-- with every stimulus -- because the difference is a
-- settling time and no simulator models one.
--
-- HAZ 2 PULSE the event crossed as a one-cycle PULSE instead of a
-- toggle. Correct whenever the source period is longer than
-- the destination's, which is most benches.
--
-- HAZ 3 BUS the vector through N independent synchronisers instead of
-- Gray coded. Correct whenever the bits arrive together,
-- which is every bench without skew.
--
-- HAZ 4 NOT GRAY a vector encoded with `v ^ (v >> 2)` instead of
-- `v ^ (v >> 1)`. It looks like a Gray code, it round-trips
-- correctly, and consecutive values can differ in two bits
-- -- so the guarantee the encoding exists for is gone while
-- every functional test still passes.
--
-- HAZ 5 ASYNC RELEASE correct crossing logic, and the destination's reset is
-- released asynchronously. Correct in every simulation,
-- because a simulated flop leaving reset always leaves it
-- cleanly.
--
-- THE TABLE THIS PRODUCES IS THE CHAPTER.
--
-- Under a benign stimulus all six pass. Under a hostile one -- skew on the vector, a
-- source faster than the destination -- hazards 2, 3 and 4 fail and are found. Hazards
-- 1 and 5 pass BOTH, and they are the two that lose data on silicon.
--
-- So the six split into three kinds, and the kinds need three different activities:
--
-- found by a HOSTILE STIMULUS 2, 3, 4 a better bench finds these
-- found by a REVIEW 1, 5 nothing else will
-- correct 0
--
-- That is the whole argument for CDC review as a separate activity from CDC
-- verification. Not because review is more rigorous, but because two of the five
-- faults here are invisible to every simulation that can be written, and a team whose
-- only gate is a green regression ships them.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_cdc_hazards is
generic (
HAZ : natural := 0;
W : positive := 8;
SYNC_N : positive := 2
);
port (
src_clk : in std_logic;
src_rst_n : in std_logic;
src_event : in std_logic; -- one source cycle per event
src_vec : in std_logic_vector(W - 1 downto 0);
dst_clk : in std_logic;
dst_rst_n : in std_logic; -- async assert; release differs by hazard
dst_stb : out std_logic; -- one destination cycle per delivered event
dst_vec : out std_logic_vector(W - 1 downto 0);
-- the seam the skew is injected on. In a real design these two are the same
-- net; they are separate so a bench can put SKEW between them, which is where
-- routing skew actually lives -- on the wires from one flop to another.
enc_out : out std_logic_vector(W - 1 downto 0);
enc_in : in std_logic_vector(W - 1 downto 0)
);
end entity;
architecture rtl of spi_cdc_hazards is
-- How deep the event synchroniser actually is. Hazard 1 is ONE, and nothing else
-- in the file changes -- which is exactly why no simulation distinguishes it.
--
-- Written as a function rather than `1 when HAZ = 1 else SYNC_N`, because a
-- conditional EXPRESSION is VHDL-2019 and only a conditional signal ASSIGNMENT is
-- older than that -- so the obvious one-liner compiles for some readers and not
-- others.
function esync_depth return positive is
begin
if HAZ = 1 then
return 1;
else
return SYNC_N;
end if;
end function;
constant ESYNC : positive := esync_depth;
signal tog_src : std_logic := '0';
signal pulse_src : std_logic := '0';
signal enc_src : std_logic_vector(W - 1 downto 0) := (others => '0');
signal rrel_sr : std_logic_vector(1 downto 0) := "00";
signal dst_rst_sync_n : std_logic;
signal esr : std_logic_vector(SYNC_N - 1 downto 0) := (others => '0');
signal ed : std_logic := '0';
type vec_arr is array (0 to SYNC_N - 1) of std_logic_vector(W - 1 downto 0);
signal vsr : vec_arr := (others => (others => '0'));
signal arrived : std_logic;
signal stb_r : std_logic := '0';
signal src_pick : std_logic;
-- Hazard 4's encoding: `v xor (v srl 2)`. It is a bijection, so a decoder written
-- to match it recovers the value perfectly and every data test passes. What it is
-- NOT is a Gray code: consecutive values can differ in two bits, and the whole
-- reason a pointer is Gray coded is that they cannot.
function encode(v : std_logic_vector) return std_logic_vector is
variable u : unsigned(v'range) := unsigned(v);
begin
if HAZ = 3 then
return v; -- no encoding at all
elsif HAZ = 4 then
return std_logic_vector(u xor shift_right(u, 2));
else
return std_logic_vector(u xor shift_right(u, 1));
end if;
end function;
function decode(g : std_logic_vector) return std_logic_vector is
variable r : std_logic_vector(g'range);
begin
if HAZ = 3 then
return g;
elsif HAZ = 4 then
-- the inverse of `v xor (v srl 2)`, so the round trip is exact
r(r'high) := g(g'high);
if W >= 2 then r(r'high - 1) := g(g'high - 1); end if;
for k in g'high - 2 downto 0 loop
r(k) := r(k + 2) xor g(k);
end loop;
return r;
else
r(r'high) := g(g'high);
for k in g'high - 1 downto 0 loop
r(k) := r(k + 1) xor g(k);
end loop;
return r;
end if;
end function;
begin
enc_out <= enc_src;
dst_vec <= decode(vsr(SYNC_N - 1));
dst_stb <= stb_r;
src_pick <= pulse_src when HAZ = 2 else tog_src;
source : process (src_clk, src_rst_n)
begin
if src_rst_n = '0' then
tog_src <= '0';
pulse_src <= '0';
enc_src <= (others => '0');
elsif rising_edge(src_clk) then
pulse_src <= src_event; -- one cycle wide, and then gone
if src_event = '1' then
tog_src <= not tog_src;
enc_src <= encode(src_vec);
end if;
end if;
end process;
-- Hazard 5 releases the destination's reset asynchronously: the raw signal is used
-- directly, with no synchroniser, so different flops may leave reset on different
-- cycles. Every simulation shows them leaving together, because a simulated flop
-- has no recovery time.
dst_rst_sync_n <= dst_rst_n when HAZ = 5 else rrel_sr(1);
rel : process (dst_clk, dst_rst_n)
begin
if dst_rst_n = '0' then
rrel_sr <= "00";
elsif rising_edge(dst_clk) then
rrel_sr <= rrel_sr(0) & '1';
end if;
end process;
arrived <= (esr(ESYNC - 1) and not ed) when HAZ = 2 -- a rising edge on a pulse
else (esr(ESYNC - 1) xor ed); -- either edge of a toggle
destination : process (dst_clk, dst_rst_sync_n)
begin
if dst_rst_sync_n = '0' then
esr <= (others => '0');
ed <= '0';
stb_r <= '0';
vsr <= (others => (others => '0'));
elsif rising_edge(dst_clk) then
esr <= esr(SYNC_N - 2 downto 0) & src_pick;
ed <= esr(ESYNC - 1);
vsr(0) <= enc_in;
for i in 1 to SYNC_N - 1 loop
vsr(i) <= vsr(i - 1);
end loop;
stb_r <= arrived;
end if;
end process;
end architecture;The testbench
Two stimuli, six designs, and two counting methods — the strobe-gated event count and the free-read value count.
// spi_cdc_hazards_tb.sv
//
// Six designs, two stimuli, one table -- and the table is the chapter's argument.
//
// THE BENIGN STIMULUS is what a reasonable bench does: a source slower than the
// destination, the vector's bits changing together, one event well separated from the
// next. All six designs pass it, including the five that are broken.
//
// THE HOSTILE STIMULUS is what this module's earlier chapters learned to build, and
// getting it right took one correction worth recording. The first attempt used a
// source faster than the destination AND an event every source cycle, and it broke the
// CORRECT design too -- which is Chapter 15.1's conclusion doing its job: at that
// event rate no scheme conserves events, so the stimulus discriminated nothing.
//
// The two knobs have to be separated, because the hazards use them differently:
//
// PULSE WIDTH is set by the source's clock PERIOD. An 8 ns source period against a
// 10 ns destination makes a one-cycle pulse narrower than a sampling
// interval, which is what hazard 2 cannot survive.
//
// EVENT RATE is set by the GAP between events. Eight source cycles apart is one
// event every 32 ns, comfortably inside the destination's reach, so a
// correct toggle conserves every one.
//
// So the hostile stimulus is a FAST source sending SPARSE events, with skew on the
// vector. That combination is hostile to the hazards and benign to the correct
// design -- which is the only kind of hostile stimulus worth having.
//
// AND TWO DO NOT APPEAR UNDER EITHER, ever, in any simulation that can be written.
// Those two are the chapter, and the bench's job is to say so in its own log rather
// than leave a reader to notice that two columns are green.
`timescale 1ns/1ps
module spi_cdc_hazards_tb;
localparam int W = 8;
localparam int SYNC_N = 2;
localparam int NHAZ = 6;
localparam int HIST = 6;
localparam time SKEW = 1;
integer shalf = 20; // source half-period
integer dhalf = 5; // destination half-period, fixed
reg src_clk = 1'b0;
reg dst_clk = 1'b0;
initial forever begin #(shalf); src_clk = ~src_clk; end
always #5 dst_clk = ~dst_clk;
reg src_rst_n = 1'b1, dst_rst_n = 1'b1;
reg src_event = 1'b0;
reg [W-1:0] cnt = {W{1'b0}};
// The skew is injected on the SEAM between each design's source register and its
// destination synchroniser, which is where routing skew lives. It is switchable,
// because the bench has to show that the benign case passes as well as that the
// hostile one finds things.
//
// The spread is 0 to 3 ns rather than 0 to 7: the skew on a path is bounded by
// what routing can add, and a spread approaching the source's clock period is not
// a skew, it is a missing pipeline stage. Three nanoseconds against an 8 ns source
// period is ordinary.
reg use_skew = 1'b0;
wire [W-1:0] src_vec = cnt;
wire [NHAZ-1:0] dst_stb;
wire [W-1:0] dst_vec [0:NHAZ-1];
wire [W-1:0] enc_out [0:NHAZ-1];
wire [W-1:0] enc_skew [0:NHAZ-1];
genvar hi, gi;
generate
for (hi = 0; hi < NHAZ; hi = hi + 1) begin : g_haz
// Each bit of the seam gets its own delay when skew is enabled, and none
// when it is not. `(gi % 4)` keeps the spread at 0 to 3 ns.
for (gi = 0; gi < W; gi = gi + 1) begin : g_skew
assign #((gi % 4) * SKEW) enc_skew[hi][gi] =
use_skew ? enc_out[hi][gi] : 1'bx;
end
spi_cdc_hazards #(.HAZ(hi), .W(W), .SYNC_N(SYNC_N)) u (
.src_clk(src_clk), .src_rst_n(src_rst_n),
.src_event(src_event), .src_vec(src_vec),
.dst_clk(dst_clk), .dst_rst_n(dst_rst_n),
.dst_stb(dst_stb[hi]), .dst_vec(dst_vec[hi]),
.enc_out(enc_out[hi]),
// With skew off the seam is a plain wire, which is what it is in a
// real design; with it on, each bit arrives at its own time.
.enc_in(use_skew ? enc_skew[hi] : enc_out[hi])
);
end
endgenerate
integer errors = 0;
initial begin
#4_000_000;
$display("FAIL: the simulation did not finish within its time limit");
$finish;
end
// Bookkeeping, per hazard: events delivered, and values observed that the source
// did not hold recently. The recency window is Chapter 15.5's -- asking whether a
// value ever existed is useless against a counter.
integer delivered [0:NHAZ-1];
integer impossible [0:NHAZ-1];
integer generated = 0;
// A SECOND observation, and the reason it exists is the whole difference between
// hazard 3 and the correct design. Hazard 3's premise is "synchronise the bus and
// read it whenever you like" -- so the bench has to read it whenever, on EVERY
// destination cycle, not only when a flag says the value is ready.
//
// Checking only at the strobe hides hazards 3 and 4 completely: the strobe arrives
// two destination cycles after the update, by which time three nanoseconds of skew
// has long settled, so a strobe-gated check sees a clean value every time. That is
// not a flaw in the stimulus, it is the point -- a design that reads a synchronised
// bus ONLY on a flag has accidentally built the correct scheme, and a design that
// reads it freely has not.
integer any_bad [0:NHAZ-1];
integer any_obs = 0;
reg [W-1:0] hist [0:HIST-1];
integer hj;
function automatic is_recent(input [W-1:0] v);
integer j;
begin
is_recent = 1'b0;
for (j = 0; j < HIST; j = j + 1)
if (hist[j] == v) is_recent = 1'b1;
end
endfunction
always @(posedge src_clk) if (src_rst_n) begin
for (hj = HIST-1; hj > 0; hj = hj - 1) hist[hj] = hist[hj-1];
hist[0] = cnt;
end
integer m;
always @(posedge dst_clk) if (dst_rst_n && src_rst_n) begin
any_obs = any_obs + 1;
for (m = 0; m < NHAZ; m = m + 1) begin
if (dst_stb[m]) begin
delivered[m] = delivered[m] + 1;
if (!is_recent(dst_vec[m])) impossible[m] = impossible[m] + 1;
end
// read freely, which is what a synchronised bus invites
if (!is_recent(dst_vec[m])) any_bad[m] = any_bad[m] + 1;
end
end
task automatic restart;
integer i;
begin
src_event = 1'b0;
src_rst_n = 1'b1; dst_rst_n = 1'b1;
repeat (2) @(posedge dst_clk);
src_rst_n = 1'b0; dst_rst_n = 1'b0;
repeat (4) @(posedge dst_clk);
repeat (2) @(posedge src_clk);
src_rst_n = 1'b1; dst_rst_n = 1'b1;
repeat (8) @(posedge dst_clk);
for (i = 0; i < NHAZ; i = i + 1) begin
delivered[i] = 0;
impossible[i] = 0;
any_bad[i] = 0;
end
generated = 0;
any_obs = 0;
// The counter restarts at zero so that the reset value the destination
// holds is a value the source genuinely has, and the window starts full
// of it rather than of a value left over from the previous phase.
cnt = {W{1'b0}};
for (i = 0; i < HIST; i = i + 1) hist[i] = cnt;
end
endtask
// `step` is how much the counter advances per event. One keeps a Gray code one
// bit apart; two is what a design that counts by two would do, and it is here to
// show that hazard 4's fault is about the ENCODING rather than about the counter.
task automatic run_events(input integer n, input integer gap, input integer step);
integer i, g;
begin
for (i = 0; i < n; i = i + 1) begin
@(negedge src_clk);
cnt = cnt + step;
src_event = 1'b1;
generated = generated + 1;
@(negedge src_clk);
src_event = 1'b0;
for (g = 1; g < gap; g = g + 1) @(negedge src_clk);
end
repeat (40) @(posedge dst_clk);
end
endtask
integer k;
reg [NHAZ-1:0] benign_ok, hostile_ok;
// A hazard "passes" a stimulus when every event was delivered and no impossible
// value was observed. That is exactly what a functional regression checks, and
// deliberately nothing more.
// A hazard "passes" a stimulus when every event was delivered AND no impossible
// value was ever observed -- reading the bus freely, because that is what its
// structure invites. Exactly what a functional regression checks, and deliberately
// nothing more.
function automatic passes(input integer h);
passes = (delivered[h] == generated) && (any_bad[h] == 0);
endfunction
initial begin
// =============================================================
// THE BENIGN STIMULUS: source slower than destination, no skew, one event
// every four source cycles. This is a reasonable bench.
// =============================================================
shalf = 20; use_skew = 1'b0;
restart();
run_events(40, 4, 1);
$display(" a benign stimulus: source period 40 ns against a 10 ns destination, no skew, one event every four source cycles");
// The description goes LAST in each row. `%-30s` left-justifies in some
// simulators and not others, so a description in the middle of a row leaves the
// numbers ragged in one language and aligned in another -- and a table that
// only lines up in one of three published listings is worse than one that lines
// up in none.
$display(" haz events bad values what is wrong with it");
for (k = 0; k < NHAZ; k = k + 1) begin
benign_ok[k] = passes(k);
$display(" %3d %3d/%-3d %4d/%-5d %0s", k,
delivered[k], generated, any_bad[k], any_obs,
(k == 0) ? "nothing" :
(k == 1) ? "one synchroniser flop" :
(k == 2) ? "a pulse, not a toggle" :
(k == 3) ? "a bus, independently synced" :
(k == 4) ? "an encoding that is not Gray" :
"an asynchronous reset release");
end
// 1. EVERY ONE OF THEM PASSES. This is the claim the chapter rests on, and it
// has to be asserted rather than observed -- a bench that merely printed
// the table would let a reader assume the broken ones failed somewhere.
for (k = 0; k < NHAZ; k = k + 1) begin
if (!benign_ok[k]) begin
$display(" FAIL: hazard %0d did not pass the benign stimulus, so the chapter's claim that all six look correct is not what this bench measured",
k);
errors = errors + 1;
end
end
$display(" all six pass. Five of them are broken");
// =============================================================
// THE HOSTILE STIMULUS: source faster than destination, per-bit skew on the
// vector, an event every source cycle. This is what Chapters 15.1 and 15.5
// learned to build.
// =============================================================
shalf = 4; use_skew = 1'b1;
restart();
run_events(60, 8, 1);
$display(" a hostile stimulus: source period 8 ns against a 10 ns destination -- so a one-cycle pulse is narrower than a sampling interval -- with up to 3 ns of per-bit skew on the seam between the two registers, and events eight source cycles apart so that a correct crossing still conserves every one");
$display(" haz events bad values verdict what is wrong with it");
for (k = 0; k < NHAZ; k = k + 1) begin
hostile_ok[k] = passes(k);
$display(" %3d %3d/%-3d %4d/%-5d %0s %0s", k,
delivered[k], generated, any_bad[k], any_obs,
hostile_ok[k] ? "passes" : "FOUND ",
(k == 0) ? "nothing" :
(k == 1) ? "one synchroniser flop" :
(k == 2) ? "a pulse, not a toggle" :
(k == 3) ? "a bus, independently synced" :
(k == 4) ? "an encoding that is not Gray" :
"an asynchronous reset release");
end
// 2. THE PULSE AND THE BUS ARE FOUND. These are the two that a better bench
// catches, and they are why building the hostile stimulus was worth it.
if (hostile_ok[2]) begin
$display(" FAIL: hazard 2 (a pulse crossed as a level) must be found by a source faster than the destination -- Chapter 15.1 measured exactly this");
errors = errors + 1;
end
if (hostile_ok[3]) begin
$display(" FAIL: hazard 3 (a bus through independent synchronisers) must be found by per-bit skew when the bus is read freely -- Chapter 15.5 measured exactly this");
errors = errors + 1;
end
if (hostile_ok[4]) begin
$display(" FAIL: hazard 4 (an encoding that is not a Gray code) must be found by per-bit skew, because consecutive values can differ in two bits and a partial observation is then a third value");
errors = errors + 1;
end
$display(" the pulse and the bus are FOUND, by the two stimuli Chapters 15.1 and 15.5 had to be written to produce");
// 3. AND THE TWO THAT ARE NEVER FOUND. Asserted as passing, which is an
// unusual thing for a bench to assert and is the chapter's whole point: a
// regression that goes green on these is behaving correctly, and the
// designs are still wrong.
if (!hostile_ok[1]) begin
$display(" FAIL: hazard 1 (one synchroniser flop) should pass even the hostile stimulus -- if this bench can fail it, the bench is modelling something a simulator does not have");
errors = errors + 1;
end
if (!hostile_ok[5]) begin
$display(" FAIL: hazard 5 (an asynchronous reset release) should pass even the hostile stimulus, for the same reason");
errors = errors + 1;
end
$display(" and hazards 1 and 5 PASS BOTH. A one-flop synchroniser differs from a two-flop one by a settling time, and an asynchronous reset release differs from a synchronous one by a recovery time, and a simulator models neither -- so no stimulus, no ratio, no seed and no coverage target will ever distinguish them from the correct design");
// 4. THE CORRECT DESIGN PASSES BOTH, which is the control. Without it the
// hostile stimulus could be failing everything for a reason unrelated to
// the hazards.
if (!hostile_ok[0]) begin
$display(" FAIL: the correct design must survive the hostile stimulus too, or the stimulus is testing the bench rather than the hazards");
errors = errors + 1;
end
// 5. AND HAZARD 4 IS THE INTERESTING MIDDLE CASE. Its encoding round-trips
// perfectly, so every data test passes; what is broken is the GUARANTEE,
// and the guarantee only matters when a partial observation happens. It is
// found here only because skew makes partial observations common.
$display(" hazard 4 is the middle case worth studying: `v ^ (v >> 2)` is a bijection, so it decodes perfectly and every data test passes, and it is not a Gray code -- consecutive values can differ in two bits, so the property the encoding exists for is gone while nothing about the data looks wrong. It was found here only because skew makes partial observations common; a bench without skew reports it correct");
if (errors == 0)
$display("PASS: six crossings, one correct and five broken, and under a benign stimulus -- a source slower than the destination, no skew, well-separated events -- ALL SIX pass. That is the measurement the chapter exists for. Under a hostile stimulus built from what Chapters 15.1 and 15.5 had to learn, three of the five are found: a pulse crossed as a level fails once the source outruns the destination, a bus through independent synchronisers fails once its bits arrive skewed, and an encoding that is a bijection but not a Gray code fails once partial observations become common. The remaining two pass BOTH, and they are the two that lose data on silicon -- a one-flop synchroniser differs from a two-flop one by a settling time and an asynchronous reset release differs from a synchronous one by a recovery time, and a simulator models neither, so no stimulus, ratio, seed or coverage target can distinguish them from the correct design. That is the argument for CDC review as an activity separate from CDC verification, and it is not about rigour: two of these five faults are invisible to every simulation that can be written, so a team whose only gate is a green regression ships them -- which means the review has to check STRUCTURE, counting flops per crossing and asking how every reset is released, because those are the only two questions that reach the faults no bench can");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule// spi_cdc_hazards_tb.v
//
// Six designs, two stimuli, one table -- and the table is the chapter's argument.
//
// THE BENIGN STIMULUS is what a reasonable bench does: a source slower than the
// destination, the vector's bits changing together, one event well separated from the
// next. All six designs pass it, including the five that are broken.
//
// THE HOSTILE STIMULUS is what this module's earlier chapters learned to build, and
// getting it right took one correction worth recording. The first attempt used a
// source faster than the destination AND an event every source cycle, and it broke the
// CORRECT design too -- which is Chapter 15.1's conclusion doing its job: at that
// event rate no scheme conserves events, so the stimulus discriminated nothing.
//
// The two knobs have to be separated, because the hazards use them differently:
//
// PULSE WIDTH is set by the source's clock PERIOD. An 8 ns source period against a
// 10 ns destination makes a one-cycle pulse narrower than a sampling
// interval, which is what hazard 2 cannot survive.
//
// EVENT RATE is set by the GAP between events. Eight source cycles apart is one
// event every 32 ns, comfortably inside the destination's reach, so a
// correct toggle conserves every one.
//
// So the hostile stimulus is a FAST source sending SPARSE events, with skew on the
// vector. That combination is hostile to the hazards and benign to the correct
// design -- which is the only kind of hostile stimulus worth having.
//
// AND TWO DO NOT APPEAR UNDER EITHER, ever, in any simulation that can be written.
// Those two are the chapter, and the bench's job is to say so in its own log rather
// than leave a reader to notice that two columns are green.
`timescale 1ns/1ps
module spi_cdc_hazards_tb;
localparam W = 8;
localparam SYNC_N = 2;
localparam NHAZ = 6;
localparam HIST = 6;
localparam time SKEW = 1;
integer shalf; // source half-period
integer dhalf; // destination half-period, fixed
reg src_clk;
reg dst_clk;
initial forever begin #(shalf); src_clk = ~src_clk; end
always #5 dst_clk = ~dst_clk;
reg src_rst_n, dst_rst_n;
reg src_event;
reg [W-1:0] cnt;
// The skew is injected on the SEAM between each design's source register and its
// destination synchroniser, which is where routing skew lives. It is switchable,
// because the bench has to show that the benign case passes as well as that the
// hostile one finds things.
//
// The spread is 0 to 3 ns rather than 0 to 7: the skew on a path is bounded by
// what routing can add, and a spread approaching the source's clock period is not
// a skew, it is a missing pipeline stage. Three nanoseconds against an 8 ns source
// period is ordinary.
reg use_skew;
wire [W-1:0] src_vec = cnt;
wire [NHAZ-1:0] dst_stb;
wire [W-1:0] dst_vec [0:NHAZ-1];
wire [W-1:0] enc_out [0:NHAZ-1];
wire [W-1:0] enc_skew [0:NHAZ-1];
genvar hi, gi;
generate
for (hi = 0; hi < NHAZ; hi = hi + 1) begin : g_haz
// Each bit of the seam gets its own delay when skew is enabled, and none
// when it is not. `(gi % 4)` keeps the spread at 0 to 3 ns.
for (gi = 0; gi < W; gi = gi + 1) begin : g_skew
assign #((gi % 4) * SKEW) enc_skew[hi][gi] =
use_skew ? enc_out[hi][gi] : 1'bx;
end
spi_cdc_hazards #(.HAZ(hi), .W(W), .SYNC_N(SYNC_N)) u (
.src_clk(src_clk), .src_rst_n(src_rst_n),
.src_event(src_event), .src_vec(src_vec),
.dst_clk(dst_clk), .dst_rst_n(dst_rst_n),
.dst_stb(dst_stb[hi]), .dst_vec(dst_vec[hi]),
.enc_out(enc_out[hi]),
// With skew off the seam is a plain wire, which is what it is in a
// real design; with it on, each bit arrives at its own time.
.enc_in(use_skew ? enc_skew[hi] : enc_out[hi])
);
end
endgenerate
integer errors;
initial begin
#4_000_000;
$display("FAIL: the simulation did not finish within its time limit");
$finish;
end
// Bookkeeping, per hazard: events delivered, and values observed that the source
// did not hold recently. The recency window is Chapter 15.5's -- asking whether a
// value ever existed is useless against a counter.
integer delivered [0:NHAZ-1];
integer impossible [0:NHAZ-1];
integer generated;
// A SECOND observation, and the reason it exists is the whole difference between
// hazard 3 and the correct design. Hazard 3's premise is "synchronise the bus and
// read it whenever you like" -- so the bench has to read it whenever, on EVERY
// destination cycle, not only when a flag says the value is ready.
//
// Checking only at the strobe hides hazards 3 and 4 completely: the strobe arrives
// two destination cycles after the update, by which time three nanoseconds of skew
// has long settled, so a strobe-gated check sees a clean value every time. That is
// not a flaw in the stimulus, it is the point -- a design that reads a synchronised
// bus ONLY on a flag has accidentally built the correct scheme, and a design that
// reads it freely has not.
integer any_bad [0:NHAZ-1];
integer any_obs;
reg [W-1:0] hist [0:HIST-1];
integer hj;
function is_recent;
input [W-1:0] v;
integer j;
begin
is_recent = 1'b0;
for (j = 0; j < HIST; j = j + 1)
if (hist[j] == v) is_recent = 1'b1;
end
endfunction
always @(posedge src_clk) if (src_rst_n) begin
for (hj = HIST-1; hj > 0; hj = hj - 1) hist[hj] = hist[hj-1];
hist[0] = cnt;
end
integer m;
always @(posedge dst_clk) if (dst_rst_n && src_rst_n) begin
any_obs = any_obs + 1;
for (m = 0; m < NHAZ; m = m + 1) begin
if (dst_stb[m]) begin
delivered[m] = delivered[m] + 1;
if (!is_recent(dst_vec[m])) impossible[m] = impossible[m] + 1;
end
// read freely, which is what a synchronised bus invites
if (!is_recent(dst_vec[m])) any_bad[m] = any_bad[m] + 1;
end
end
task restart;
integer i;
begin
src_event = 1'b0;
src_rst_n = 1'b1; dst_rst_n = 1'b1;
repeat (2) @(posedge dst_clk);
src_rst_n = 1'b0; dst_rst_n = 1'b0;
repeat (4) @(posedge dst_clk);
repeat (2) @(posedge src_clk);
src_rst_n = 1'b1; dst_rst_n = 1'b1;
repeat (8) @(posedge dst_clk);
for (i = 0; i < NHAZ; i = i + 1) begin
delivered[i] = 0;
impossible[i] = 0;
any_bad[i] = 0;
end
generated = 0;
any_obs = 0;
// The counter restarts at zero so that the reset value the destination
// holds is a value the source genuinely has, and the window starts full
// of it rather than of a value left over from the previous phase.
cnt = {W{1'b0}};
for (i = 0; i < HIST; i = i + 1) hist[i] = cnt;
end
endtask
// `step` is how much the counter advances per event. One keeps a Gray code one
// bit apart; two is what a design that counts by two would do, and it is here to
// show that hazard 4's fault is about the ENCODING rather than about the counter.
task run_events;
input integer n;
input integer gap;
input integer step;
integer i, g;
begin
for (i = 0; i < n; i = i + 1) begin
@(negedge src_clk);
cnt = cnt + step;
src_event = 1'b1;
generated = generated + 1;
@(negedge src_clk);
src_event = 1'b0;
for (g = 1; g < gap; g = g + 1) @(negedge src_clk);
end
repeat (40) @(posedge dst_clk);
end
endtask
integer k;
reg [NHAZ-1:0] benign_ok, hostile_ok;
// A hazard "passes" a stimulus when every event was delivered and no impossible
// value was observed. That is exactly what a functional regression checks, and
// deliberately nothing more.
// A hazard "passes" a stimulus when every event was delivered AND no impossible
// value was ever observed -- reading the bus freely, because that is what its
// structure invites. Exactly what a functional regression checks, and deliberately
// nothing more.
function passes;
input integer h;
passes = (delivered[h] == generated) && (any_bad[h] == 0);
endfunction
initial begin
// =============================================================
// THE BENIGN STIMULUS: source slower than destination, no skew, one event
// every four source cycles. This is a reasonable bench.
// =============================================================
shalf = 20; use_skew = 1'b0;
restart();
run_events(40, 4, 1);
$display(" a benign stimulus: source period 40 ns against a 10 ns destination, no skew, one event every four source cycles");
// The description goes LAST in each row. `%0s` left-justifies in some
// simulators and not others, so a description in the middle of a row leaves the
// numbers ragged in one language and aligned in another -- and a table that
// only lines up in one of three published listings is worse than one that lines
// up in none.
$display(" haz events bad values what is wrong with it");
for (k = 0; k < NHAZ; k = k + 1) begin
benign_ok[k] = passes(k);
$display(" %3d %3d/%-3d %4d/%-5d %0s", k,
delivered[k], generated, any_bad[k], any_obs,
(k == 0) ? "nothing" :
(k == 1) ? "one synchroniser flop" :
(k == 2) ? "a pulse, not a toggle" :
(k == 3) ? "a bus, independently synced" :
(k == 4) ? "an encoding that is not Gray" :
"an asynchronous reset release");
end
// 1. EVERY ONE OF THEM PASSES. This is the claim the chapter rests on, and it
// has to be asserted rather than observed -- a bench that merely printed
// the table would let a reader assume the broken ones failed somewhere.
for (k = 0; k < NHAZ; k = k + 1) begin
if (!benign_ok[k]) begin
$display(" FAIL: hazard %0d did not pass the benign stimulus, so the chapter's claim that all six look correct is not what this bench measured",
k);
errors = errors + 1;
end
end
$display(" all six pass. Five of them are broken");
// =============================================================
// THE HOSTILE STIMULUS: source faster than destination, per-bit skew on the
// vector, an event every source cycle. This is what Chapters 15.1 and 15.5
// learned to build.
// =============================================================
shalf = 4; use_skew = 1'b1;
restart();
run_events(60, 8, 1);
$display(" a hostile stimulus: source period 8 ns against a 10 ns destination -- so a one-cycle pulse is narrower than a sampling interval -- with up to 3 ns of per-bit skew on the seam between the two registers, and events eight source cycles apart so that a correct crossing still conserves every one");
$display(" haz events bad values verdict what is wrong with it");
for (k = 0; k < NHAZ; k = k + 1) begin
hostile_ok[k] = passes(k);
$display(" %3d %3d/%-3d %4d/%-5d %0s %0s", k,
delivered[k], generated, any_bad[k], any_obs,
hostile_ok[k] ? "passes" : "FOUND ",
(k == 0) ? "nothing" :
(k == 1) ? "one synchroniser flop" :
(k == 2) ? "a pulse, not a toggle" :
(k == 3) ? "a bus, independently synced" :
(k == 4) ? "an encoding that is not Gray" :
"an asynchronous reset release");
end
// 2. THE PULSE AND THE BUS ARE FOUND. These are the two that a better bench
// catches, and they are why building the hostile stimulus was worth it.
if (hostile_ok[2]) begin
$display(" FAIL: hazard 2 (a pulse crossed as a level) must be found by a source faster than the destination -- Chapter 15.1 measured exactly this");
errors = errors + 1;
end
if (hostile_ok[3]) begin
$display(" FAIL: hazard 3 (a bus through independent synchronisers) must be found by per-bit skew when the bus is read freely -- Chapter 15.5 measured exactly this");
errors = errors + 1;
end
if (hostile_ok[4]) begin
$display(" FAIL: hazard 4 (an encoding that is not a Gray code) must be found by per-bit skew, because consecutive values can differ in two bits and a partial observation is then a third value");
errors = errors + 1;
end
$display(" the pulse and the bus are FOUND, by the two stimuli Chapters 15.1 and 15.5 had to be written to produce");
// 3. AND THE TWO THAT ARE NEVER FOUND. Asserted as passing, which is an
// unusual thing for a bench to assert and is the chapter's whole point: a
// regression that goes green on these is behaving correctly, and the
// designs are still wrong.
if (!hostile_ok[1]) begin
$display(" FAIL: hazard 1 (one synchroniser flop) should pass even the hostile stimulus -- if this bench can fail it, the bench is modelling something a simulator does not have");
errors = errors + 1;
end
if (!hostile_ok[5]) begin
$display(" FAIL: hazard 5 (an asynchronous reset release) should pass even the hostile stimulus, for the same reason");
errors = errors + 1;
end
$display(" and hazards 1 and 5 PASS BOTH. A one-flop synchroniser differs from a two-flop one by a settling time, and an asynchronous reset release differs from a synchronous one by a recovery time, and a simulator models neither -- so no stimulus, no ratio, no seed and no coverage target will ever distinguish them from the correct design");
// 4. THE CORRECT DESIGN PASSES BOTH, which is the control. Without it the
// hostile stimulus could be failing everything for a reason unrelated to
// the hazards.
if (!hostile_ok[0]) begin
$display(" FAIL: the correct design must survive the hostile stimulus too, or the stimulus is testing the bench rather than the hazards");
errors = errors + 1;
end
// 5. AND HAZARD 4 IS THE INTERESTING MIDDLE CASE. Its encoding round-trips
// perfectly, so every data test passes; what is broken is the GUARANTEE,
// and the guarantee only matters when a partial observation happens. It is
// found here only because skew makes partial observations common.
$display(" hazard 4 is the middle case worth studying: `v ^ (v >> 2)` is a bijection, so it decodes perfectly and every data test passes, and it is not a Gray code -- consecutive values can differ in two bits, so the property the encoding exists for is gone while nothing about the data looks wrong. It was found here only because skew makes partial observations common; a bench without skew reports it correct");
if (errors == 0)
$display("PASS: six crossings, one correct and five broken, and under a benign stimulus -- a source slower than the destination, no skew, well-separated events -- ALL SIX pass. That is the measurement the chapter exists for. Under a hostile stimulus built from what Chapters 15.1 and 15.5 had to learn, three of the five are found: a pulse crossed as a level fails once the source outruns the destination, a bus through independent synchronisers fails once its bits arrive skewed, and an encoding that is a bijection but not a Gray code fails once partial observations become common. The remaining two pass BOTH, and they are the two that lose data on silicon -- a one-flop synchroniser differs from a two-flop one by a settling time and an asynchronous reset release differs from a synchronous one by a recovery time, and a simulator models neither, so no stimulus, ratio, seed or coverage target can distinguish them from the correct design. That is the argument for CDC review as an activity separate from CDC verification, and it is not about rigour: two of these five faults are invisible to every simulation that can be written, so a team whose only gate is a green regression ships them -- which means the review has to check STRUCTURE, counting flops per crossing and asking how every reset is released, because those are the only two questions that reach the faults no bench can");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
initial begin
src_rst_n = 1'b1;
dst_rst_n = 1'b1;
shalf = 20;
dhalf = 5;
src_clk = 1'b0;
dst_clk = 1'b0;
src_event = 1'b0;
cnt = {W{1'b0}};
use_skew = 1'b0;
errors = 0;
generated = 0;
any_obs = 0;
end
endmodule-- spi_cdc_hazards_tb.vhd
--
-- Six designs, two stimuli, one table -- and the table is the chapter's argument.
--
-- THE BENIGN STIMULUS is what a reasonable bench does: a source slower than the
-- destination, the vector's bits changing together, one event well separated from the
-- next. All six designs pass it, including the five that are broken.
--
-- THE HOSTILE STIMULUS is what this module's earlier chapters learned to build, and
-- getting it right took one correction worth recording. The first attempt used a
-- source faster than the destination AND an event every source cycle, and it broke the
-- CORRECT design too -- which is Chapter 15.1's conclusion doing its job: at that
-- event rate no scheme conserves events, so the stimulus discriminated nothing.
--
-- The two knobs have to be separated, because the hazards use them differently:
--
-- PULSE WIDTH is set by the source's clock PERIOD. An 8 ns source period against a
-- 10 ns destination makes a one-cycle pulse narrower than a sampling
-- interval, which is what hazard 2 cannot survive.
--
-- EVENT RATE is set by the GAP between events. Eight source cycles apart is one
-- event every 32 ns, comfortably inside the destination's reach, so a
-- correct toggle conserves every one.
--
-- So the hostile stimulus is a FAST source sending SPARSE events, with skew on the
-- vector. That combination is hostile to the hazards and benign to the correct
-- design -- which is the only kind of hostile stimulus worth having.
--
-- AND TWO DO NOT APPEAR UNDER EITHER, ever, in any simulation that can be written.
-- Those two are the chapter, and the bench's job is to say so in its own log rather
-- than leave a reader to notice that two columns are green.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_cdc_hazards_tb is
end entity;
architecture sim of spi_cdc_hazards_tb is
constant W : positive := 8;
constant SYNC_N : positive := 2;
constant NHAZ : positive := 6;
constant HIST : positive := 6;
constant SKEW : time := 1 ns;
signal shalf : time := 20 ns; -- source half-period
signal src_clk : std_logic := '0';
signal dst_clk : std_logic := '0';
signal halt : boolean := false;
signal src_rst_n, dst_rst_n : std_logic := '1';
signal src_event : std_logic := '0';
signal cnt : unsigned(W - 1 downto 0) := (others => '0');
signal src_vec : std_logic_vector(W - 1 downto 0);
-- The skew is injected on the SEAM between each design's source register and its
-- destination synchroniser, which is where routing skew lives. The spread is 0 to
-- 3 ns rather than 0 to 7: skew on a path is bounded by what routing can add, and
-- a spread approaching the source's clock period is not a skew, it is a missing
-- pipeline stage.
signal use_skew : boolean := false;
type vec_arr is array (0 to NHAZ - 1) of std_logic_vector(W - 1 downto 0);
signal dst_vec : vec_arr;
signal enc_out : vec_arr;
signal enc_skew : vec_arr;
signal enc_in : vec_arr;
signal dst_stb : std_logic_vector(NHAZ - 1 downto 0);
-- Bookkeeping, owned by the observer process; the stimulus asks for a clear through
-- a strobe, because two drivers on a signal resolve to 'X' in VHDL.
type cnt_arr is array (0 to NHAZ - 1) of natural;
signal delivered : cnt_arr := (others => 0);
signal any_bad : cnt_arr := (others => 0);
signal any_obs : natural := 0;
signal stat_clr : std_logic := '0';
-- The source's recent values, newest first, maintained in SOURCE time. Named
-- `src_hist` rather than `hist` because VHDL is case-INSENSITIVE and `hist` would
-- collide with the constant HIST.
type hist_arr is array (0 to HIST - 1) of std_logic_vector(W - 1 downto 0);
signal src_hist : hist_arr := (others => (others => '0'));
signal generated : natural := 0;
signal gen_clr : std_logic := '0';
begin
srcclk : process
begin
while not halt loop
src_clk <= '0'; wait for shalf; src_clk <= '1'; wait for shalf;
end loop;
wait;
end process;
dstclk : process
begin
while not halt loop
dst_clk <= '0'; wait for 5 ns; dst_clk <= '1'; wait for 5 ns;
end loop;
wait;
end process;
src_vec <= std_logic_vector(cnt);
hazgen : for h in 0 to NHAZ - 1 generate
-- Each bit of the seam gets its own delay; `(g mod 4)` keeps the spread at
-- 0 to 3 ns.
skewgen : for g in 0 to W - 1 generate
enc_skew(h)(g) <= enc_out(h)(g) after (g mod 4) * SKEW;
end generate;
-- With skew off the seam is a plain wire, which is what it is in a real design.
enc_in(h) <= enc_skew(h) when use_skew else enc_out(h);
u : entity work.spi_cdc_hazards
generic map (HAZ => h, W => W, SYNC_N => SYNC_N)
port map (src_clk => src_clk, src_rst_n => src_rst_n,
src_event => src_event, src_vec => src_vec,
dst_clk => dst_clk, dst_rst_n => dst_rst_n,
dst_stb => dst_stb(h), dst_vec => dst_vec(h),
enc_out => enc_out(h), enc_in => enc_in(h));
end generate;
histproc : process (src_clk)
begin
if rising_edge(src_clk) then
if src_rst_n = '1' then
for j in HIST - 1 downto 1 loop
src_hist(j) <= src_hist(j - 1);
end loop;
src_hist(0) <= std_logic_vector(cnt);
end if;
end if;
end process;
-- A SECOND observation, and the reason it exists is the whole difference between
-- hazard 3 and the correct design. Hazard 3's premise is "synchronise the bus and
-- read it whenever you like", so the bench reads it whenever -- on EVERY
-- destination cycle, not only when a flag says the value is ready. Checking only at
-- the strobe hides hazards 3 and 4 completely, because the strobe arrives two
-- destination cycles after the update and three nanoseconds of skew has long
-- settled by then.
observe : process (dst_clk)
impure function is_recent(v : std_logic_vector) return boolean is
begin
for j in 0 to HIST - 1 loop
if src_hist(j) = v then return true; end if;
end loop;
return false;
end function;
begin
if rising_edge(dst_clk) then
if stat_clr = '1' then
delivered <= (others => 0);
any_bad <= (others => 0);
any_obs <= 0;
elsif dst_rst_n = '1' and src_rst_n = '1' then
any_obs <= any_obs + 1;
for m in 0 to NHAZ - 1 loop
if dst_stb(m) = '1' then
delivered(m) <= delivered(m) + 1;
end if;
if not is_recent(dst_vec(m)) then
any_bad(m) <= any_bad(m) + 1;
end if;
end loop;
end if;
end if;
end process;
watchdog : process
begin
wait for 4 ms;
if not halt then
report "FAIL: the simulation did not finish within its time limit"
severity failure;
end if;
wait;
end process;
stim : process
variable errs : natural := 0;
type bool_arr is array (0 to NHAZ - 1) of boolean;
variable benign_ok, hostile_ok : bool_arr;
-- VHDL-2008 has no conditional EXPRESSION, so a two-way choice inside a
-- concatenation needs a function. Both strings are the same length so the
-- column stays aligned.
function if_pass(ok : boolean) return string is
begin
if ok then return "passes"; else return "FOUND "; end if;
end function;
function what(h : natural) return string is
begin
case h is
when 0 => return "nothing";
when 1 => return "one synchroniser flop";
when 2 => return "a pulse, not a toggle";
when 3 => return "a bus, independently synced";
when 4 => return "an encoding that is not Gray";
when others => return "an asynchronous reset release";
end case;
end function;
-- A hazard "passes" when every event was delivered AND no impossible value was
-- ever observed reading the bus freely. Exactly what a functional regression
-- checks, and deliberately nothing more.
impure function passes(h : natural) return boolean is
begin
return delivered(h) = generated and any_bad(h) = 0;
end function;
procedure restart is
begin
src_event <= '0';
src_rst_n <= '1'; dst_rst_n <= '1';
for i in 1 to 2 loop wait until rising_edge(dst_clk); end loop;
src_rst_n <= '0'; dst_rst_n <= '0';
for i in 1 to 4 loop wait until rising_edge(dst_clk); end loop;
for i in 1 to 2 loop wait until rising_edge(src_clk); end loop;
src_rst_n <= '1'; dst_rst_n <= '1';
for i in 1 to 8 loop wait until rising_edge(dst_clk); end loop;
-- The counter restarts at zero so the reset value the destination holds is
-- a value the source genuinely has.
cnt <= (others => '0');
gen_clr <= '1';
stat_clr <= '1';
for i in 1 to 2 loop wait until rising_edge(dst_clk); end loop;
gen_clr <= '0';
stat_clr <= '0';
for i in 1 to 2 loop wait until rising_edge(dst_clk); end loop;
end procedure;
procedure run_events(n : natural; gap : natural) is
begin
for i in 1 to n loop
wait until falling_edge(src_clk);
cnt <= cnt + 1;
src_event <= '1';
wait until falling_edge(src_clk);
src_event <= '0';
for g in 2 to gap loop wait until falling_edge(src_clk); end loop;
end loop;
for i in 1 to 40 loop wait until rising_edge(dst_clk); end loop;
end procedure;
begin
-- THE BENIGN STIMULUS: source slower than destination, no skew, one event every
-- four source cycles. This is a reasonable bench.
shalf <= 20 ns; use_skew <= false;
restart;
run_events(40, 4);
report " a benign stimulus: source period 40 ns against a 10 ns destination, no skew, one event every four source cycles";
report " haz events bad values what is wrong with it";
for k in 0 to NHAZ - 1 loop
benign_ok(k) := passes(k);
report " " & integer'image(k) & " " &
integer'image(delivered(k)) & "/" & integer'image(generated) &
" " & integer'image(any_bad(k)) & "/" & integer'image(any_obs) &
" " & what(k);
end loop;
-- 1. EVERY ONE OF THEM PASSES. The claim the chapter rests on, asserted rather
-- than observed -- a bench that merely printed the table would let a reader
-- assume the broken ones failed somewhere.
for k in 0 to NHAZ - 1 loop
if not benign_ok(k) then
report " FAIL: hazard " & integer'image(k) &
" did not pass the benign stimulus, so the chapter's claim that all six look correct is not what this bench measured";
errs := errs + 1;
end if;
end loop;
report " all six pass. Five of them are broken";
-- THE HOSTILE STIMULUS: a FAST source sending SPARSE events, with skew on the
-- seam. The two knobs are separated because the hazards use them differently --
-- pulse width comes from the source PERIOD and event rate from the GAP -- and a
-- first attempt that made both hostile broke the correct design too, which
-- discriminated nothing.
shalf <= 4 ns; use_skew <= true;
restart;
run_events(60, 8);
report " a hostile stimulus: source period 8 ns against a 10 ns destination -- so a one-cycle pulse is narrower than a sampling interval -- with up to 3 ns of per-bit skew on the seam between the two registers, and events eight source cycles apart so that a correct crossing still conserves every one";
report " haz events bad values verdict what is wrong with it";
for k in 0 to NHAZ - 1 loop
hostile_ok(k) := passes(k);
report " " & integer'image(k) & " " &
integer'image(delivered(k)) & "/" & integer'image(generated) &
" " & integer'image(any_bad(k)) & "/" & integer'image(any_obs) &
" " & (if_pass(hostile_ok(k))) & " " & what(k);
end loop;
-- 2. THE PULSE, THE BUS AND THE FALSE GRAY CODE ARE FOUND.
if hostile_ok(2) then
report " FAIL: hazard 2 (a pulse crossed as a level) must be found by a source faster than the destination -- Chapter 15.1 measured exactly this";
errs := errs + 1;
end if;
if hostile_ok(3) then
report " FAIL: hazard 3 (a bus through independent synchronisers) must be found by per-bit skew when the bus is read freely -- Chapter 15.5 measured exactly this";
errs := errs + 1;
end if;
if hostile_ok(4) then
report " FAIL: hazard 4 (an encoding that is not a Gray code) must be found by per-bit skew, because consecutive values can differ in two bits and a partial observation is then a third value";
errs := errs + 1;
end if;
report " the pulse, the bus and the false Gray code are FOUND, by the two stimuli Chapters 15.1 and 15.5 had to be written to produce";
-- 3. AND THE TWO THAT ARE NEVER FOUND, asserted as PASSING -- an unusual thing
-- for a bench to assert and the chapter's whole point.
if not hostile_ok(1) then
report " FAIL: hazard 1 (one synchroniser flop) should pass even the hostile stimulus -- if this bench can fail it, the bench is modelling something a simulator does not have";
errs := errs + 1;
end if;
if not hostile_ok(5) then
report " FAIL: hazard 5 (an asynchronous reset release) should pass even the hostile stimulus, for the same reason";
errs := errs + 1;
end if;
report " and hazards 1 and 5 PASS BOTH. A one-flop synchroniser differs from a two-flop one by a settling time, and an asynchronous reset release differs from a synchronous one by a recovery time, and a simulator models neither -- so no stimulus, no ratio, no seed and no coverage target will ever distinguish them from the correct design";
-- 4. THE CORRECT DESIGN PASSES BOTH, which is the control.
if not hostile_ok(0) then
report " FAIL: the correct design must survive the hostile stimulus too, or the stimulus is testing the bench rather than the hazards";
errs := errs + 1;
end if;
report " hazard 4 is the middle case worth studying: `v xor (v srl 2)` is a bijection, so it decodes perfectly and every data test passes, and it is not a Gray code -- consecutive values can differ in two bits, so the property the encoding exists for is gone while nothing about the data looks wrong. It was found here only because skew makes partial observations common; a bench without skew reports it correct";
if errs = 0 then
report "PASS: six crossings, one correct and five broken, and under a benign stimulus -- a source slower than the destination, no skew, well-separated events -- ALL SIX pass. That is the measurement the chapter exists for. Under a hostile stimulus built from what Chapters 15.1 and 15.5 had to learn, three of the five are found: a pulse crossed as a level fails once the source outruns the destination, a bus through independent synchronisers fails once its bits arrive skewed, and an encoding that is a bijection but not a Gray code fails once partial observations become common. The remaining two pass BOTH, and they are the two that lose data on silicon -- a one-flop synchroniser differs from a two-flop one by a settling time and an asynchronous reset release differs from a synchronous one by a recovery time, and a simulator models neither, so no stimulus, ratio, seed or coverage target can distinguish them from the correct design. That is the argument for CDC review as an activity separate from CDC verification, and it is not about rigour: two of these five faults are invisible to every simulation that can be written, so a team whose only gate is a green regression ships them -- which means the review has to check STRUCTURE, counting flops per crossing and asking how every reset is released, because those are the only two questions that reach the faults no bench can";
else
report "FAIL: " & integer'image(errs) & " error(s)" severity error;
end if;
halt <= true;
wait;
end process;
-- `generated` is owned here so that the stimulus does not drive it from a second
-- place; it counts the events the stimulus asked for.
gencount : process (src_clk)
begin
if rising_edge(src_clk) then
if gen_clr = '1' then
generated <= 0;
elsif src_rst_n = '1' and src_event = '1' then
generated <= generated + 1;
end if;
end if;
end process;
end architecture;The exact bad-value counts are phase- and simulator-dependent and are not asserted: the three language benches report different totals from the same designs, for the reason Chapter 15.5 gives. What is asserted is the verdict per hazard, which is identical in all three because it follows from the structure.
7. Why a Verification Engineer Cares
Assert that the broken designs PASS the benign stimulus. This is the chapter's central and most unusual move. Without it, the table is a display and a reader can assume a failure happened somewhere off-screen. With it, "all six pass" is a measurement.
Separate the knobs in a hostile stimulus. §4's callout: a stimulus hostile on every axis at once breaks the correct design and separates nothing. Pulse width and event rate are independent, they break different hazards, and a useful hostile stimulus is extreme on one axis and comfortable on the other.
Read a synchronised bus the way its own premise invites. §4's second callout. Checking hazard 3 only at a flag hides it completely — and hides it for the right reason, because a flag-gated read is the correct scheme. The bench has to read freely to measure the design as built.
And assert the two invisible faults as PASSING. hostile_ok[1] and hostile_ok[5] are required to be true, with a comment saying that a bench able to fail them is modelling something a simulator does not have. That protects the chapter against a future contributor who "fixes" the apparent gap by making the RTL fail — which would be making the RTL model metastability.
// Properties for the six. The interesting thing about this set is what is NOT in it:
// there is no property that distinguishes hazard 0 from hazard 1, or hazard 0 from
// hazard 5, because no such property exists in a simulable semantics.
property p_gray_one_bit_at_a_time;
// Catches hazard 4, and it is the only one of the five that a bound assertion
// finds without a stimulus. `v ^ (v >> 2)` violates it; `v ^ (v >> 1)` cannot.
@(posedge src_clk) disable iff (!src_rst_n)
src_update |=> ($countones(enc_src ^ $past(enc_src)) <= 1);
endproperty
property p_event_is_a_toggle_not_a_pulse;
// Catches hazard 2 structurally: the source's event signal must be a level that
// persists, never a one-cycle pulse. Written as "it does not return on its own".
@(posedge src_clk) disable iff (!src_rst_n)
(tog_src != $past(tog_src)) |=> $stable(tog_src) until_with (src_event[->1]);
endproperty
property p_vector_crosses_as_one_encoded_value;
// Catches hazard 3, and note it is a STRUCTURAL claim dressed as a temporal one:
// the synchronised vector must only ever equal an encoded source value, never a
// mixture. It needs the bench's recency window to be checkable at all.
@(posedge dst_clk) disable iff (!dst_rst_n)
1 |-> src_held_recently(decode(vsr[SYNC_N-1]));
endproperty
property p_reset_released_synchronously;
// Hazard 5, and this is the closest a property can get -- which is not close.
// It says the release is registered, not that recovery time is met, because
// recovery time is not in the semantics.
@(posedge dst_clk)
$rose(dst_rst_sync_n) |-> $past(dst_rst_n);
endproperty
// AND THE TWO THAT CANNOT BE WRITTEN, recorded here rather than omitted:
//
// synchroniser DEPTH -- a lint rule counting flops between a domain crossing's
// source and its first fanout. Not a temporal property.
// recovery TIME -- a static timing check on the reset path. Not simulable.
//
// A property file that silently lacks these invites the reading that the list is
// complete.// Coverage. The axes are the two that separate the findable hazards from the
// unfindable ones -- and the third coverpoint is the honest admission that two of the
// six cannot be distinguished by any of them.
covergroup cg_hazards @(posedge dst_clk);
option.per_instance = 1;
// Source period against the destination's, in tenths. Below 10 a one-cycle pulse
// is narrower than a sampling interval, which is hazard 2's boundary.
pulse_width: coverpoint src_period_tenths {
bins narrow = {[1:9]};
bins equal = {10};
bins wide = {[11:$]};
}
// Skew on the seam, in destination-clock tenths. Zero is the bin that makes
// hazards 3 and 4 look correct.
skew: coverpoint seam_skew_tenths {
bins none = {0};
bins small = {[1:3]};
bins large = {[4:$]};
}
// Whether the bus was read freely or only on the flag. The second hides hazards
// 3 and 4 entirely, and covering both is how a suite knows which it measured.
read_style: coverpoint bus_read_freely { bins freely = {1}; bins on_flag = {0}; }
x_pulse_skew: cross pulse_width, skew;
x_skew_style: cross skew, read_style;
// There is deliberately no coverpoint for hazards 1 and 5. Nothing in a coverage
// model reaches them, and a bin that claimed to would be worse than its absence.
endgroup8. Why an FPGA or ASIC Engineer Cares
The two invisible faults are both found by tools that are not simulators. Synchroniser depth is a lint and CDC-checker finding: every commercial CDC tool identifies a one-flop crossing and reports it. Reset recovery is a static timing finding: the reset path has a recovery and removal check, and an unsynchronised release either fails it or is waived. So the faults are not undetectable — they are undetectable by simulation, and the tools that do find them are the ones a functional-verification-only flow does not run.
The vendor CDC checker's waiver list is the review. These tools find crossings and ask what protects each one; the answers accepted are a synchroniser declaration, a Gray declaration, a handshake declaration, or a waiver. A waiver on an unprotected multi-bit bus is hazard 3, signed off — which is why the waiver list, not the report, is the document a reviewer reads.
Hazard 4 is invisible to a CDC checker too, and that makes it the most interesting of the five from a tooling point of view. The tool sees a multi-bit crossing with a Gray declaration and is satisfied; the declaration is a claim about the encoder, and the encoder is wrong. Only the bound assertion in §7 or a reading of the encoder finds it.
And hazard 1's attribute makes it worse, not better. ASYNC_REG on a one-flop chain tells timing analysis to stop reporting the path — so the design is quieter after the attribute than before it, and the one flop is now protected from the tool's complaints as well as from retiming.
9. Failure Signature — A Product That Works Until The Fab Changes
The symptom:
"Two years in production. A shrink to a new process node, same RTL, same constraints, same regression — and now about one unit in a thousand hangs at power-on. It always passes at test and fails at the customer."
What is happening: hazard 5, or hazard 1, exposed by a process whose flops have a different recovery or resolution characteristic. Nothing about the design changed; the physics under it did.
Why the two-year history and the passing regression make this the most expensive fault in the module: every hypothesis is about the change, the change is a process node nobody controls, and the design has been outside its unstated assumption since day one. The regression was green for two years and is green now.
How it is found in practice: not by simulation, and usually not by debugging the failing units. It is found by running a CDC checker and a reset-path timing report on a design that has never had either — at which point the finding is immediate and the fix is a reset synchroniser or a second flop, both of which are a day's work. The expensive part was the two years.
10. Common Misconceptions
"More seeds, more stimulus and more coverage will find a CDC bug." They will find hazards 2, 3 and 4. They will never find hazards 1 and 5, because the mechanism is not in the semantics.
"A green regression at many clock ratios verifies a crossing." §3's table is a green regression, and five of its six designs are broken.
"CDC review is a slower, more careful version of CDC verification." It answers different questions — how many flops, and how is each reset released — and both are unanswerable by running anything.
"If a fault is not simulable, it is not detectable." It is detectable by lint, by a CDC checker and by static timing. What it is not is detectable by the one tool a functional-verification flow runs.
"A Gray declaration to the CDC tool proves the encoding is a Gray code." It is a claim about the encoder, and hazard 4's encoder round-trips perfectly while differing in two bits. The tool accepts the declaration and the bug survives.
"ASYNC_REG on a synchroniser makes it safe." On a one-flop chain it makes the design quieter, by stopping the tool reporting the path it should have been reporting.
11. Reason It Through
Q. Which of the five faults would a formal property check find, and which would it miss?
Hazard 4 — the one-bit-at-a-time property is provable or refutable from the RTL alone. Hazard 2 and hazard 3 are findable formally if the environment model includes skew or an unconstrained sampling relationship, which is a modelling decision rather than a given. Hazards 1 and 5 are missed, because formal reasons about the same semantics as simulation: a flop captures a defined value and leaves reset cleanly. Formal raises the ceiling on the findable faults and does not change the boundary.
Q. Why is hazard 4 harder to find than hazard 3, given that both are vector crossings?
Because hazard 3 announces itself to a CDC tool as an unprotected multi-bit crossing, and hazard 4 presents a declared Gray crossing whose declaration is false. The tool checks that a declaration exists rather than that the encoder implements it. So hazard 3 needs a waiver to survive and hazard 4 survives with the tool's approval — which makes the bound assertion in §7 the only automated check that reaches it.
Q. A reviewer counts flops on every crossing and checks every reset release, and signs off. What has that review established, and what has it not?
It has established the two things no simulation can — that each crossing has enough stages and that each reset is released synchronously. It has established nothing about pulse width, encoding correctness or bus coherence, all of which are findable by stimulus and are not what the review looked at. The two activities are complements: a review without a hostile-stimulus regression misses hazards 2, 3 and 4, and a regression without a review misses 1 and 5.
Q. Why does the bench assert that hazards 1 and 5 pass, rather than simply not testing them?
Because not testing them leaves the gap undocumented, and the next person to read the suite has no way to know it exists. Asserting the pass, with a comment that a bench able to fail it would be modelling something a simulator does not have, puts the limitation in the regression's own output — which is the same reasoning as Chapter 15.3's asserting that a dangerous ratio passes and Chapter 15.7's printing what it cannot check.
Q. A team proposes replacing the CDC review with a rule that all crossings must use one of three approved library modules. Is that a good substitute?
It is a better substitute than most, and it moves the review rather than removing it. The review becomes "is every crossing one of the three?" — which is cheap, mechanical and checkable by a script. What it does not cover is hazard 4's shape: a library module used correctly with a wrong input, or an approved module instantiated around a value that does not change by one step. So the rule closes the structural questions and leaves the applicability question, which still has to be asked per instance.
12. Understanding Check
13. Summary
Six crossings, one correct and five broken. Under a benign stimulus — a source slower than the destination, no skew, well-separated events — all six pass. That is the measurement this chapter exists for, and it is asserted rather than displayed.
Under a hostile stimulus built from what Chapter 15.1 and Chapter 15.5 had to learn, three of the five are found: a pulse crossed as a level fails once the source outruns the destination, a bus through independent synchronisers fails once its bits arrive skewed, and an encoding that is a bijection but not a Gray code fails once partial observations become common.
Building that stimulus took one correction: the first attempt was extreme on both axes and broke the correct design too. Pulse width comes from the source's period and event rate from the gap between events, they break different hazards, and a useful hostile stimulus is extreme on one and comfortable on the other.
The remaining two pass both, and they are the two that lose data on silicon. A one-flop synchroniser differs from a two-flop one by a settling time; an asynchronous reset release differs from a synchronous one by a recovery time; and a simulator models neither. So no stimulus, ratio, seed or coverage target can distinguish them from the correct design.
That is the argument for CDC review as an activity separate from CDC verification, and it is not about rigour. Two of these five faults are invisible to every simulation that can be written, so a team whose only gate is a green regression ships them — by doing exactly what verification is for and getting the answer verification is able to give. The review has to check structure, and the two questions that reach these faults are how many flops per crossing and how is every reset released.
They are not undetectable, though — they are undetectable by simulation. Synchroniser depth is a lint and CDC-checker finding; reset recovery is a static timing finding. The tools that find them are the ones a functional-verification-only flow does not run. And hazard 4 escapes even the CDC checker, because a Gray declaration is a claim about an encoder the tool does not read.
For verification: assert that the broken designs pass the benign stimulus, or the table is a display rather than a measurement; separate the knobs; read a synchronised bus the way its own premise invites; and assert the two invisible faults as passing, so the gap lives in the regression's output rather than in someone's memory.
14. What Comes Next
Module 15 set out to answer one question — what a slave should do about a clock it does not own — and it has answered it in four parts. Two architectures, each with a precondition that is a number rather than a preference. Four ways to carry an event, none of which creates bandwidth. Three ways to carry a value, of which one suits counters, one needs the source to hold still, and one buffers. And a constraints file that describes a board, cannot be simulated, and moves three of Module 14's published requirements the moment a pad register is added.
The thread through all of it is a single distinction: what a measurement can settle, and what only a reading can. Chapter 15.3's two limits, Chapter 15.7's printed list of what its bench cannot check, and this chapter's two green columns are the same observation arriving three times.
Module 16 leaves the clock behind and returns to the bus: multiple slaves, shared lines, and the arbitration and addressing that a two-device board never needed.
Continue learning
Related tutorials
- Related topic
Architecture B — Oversampling SCLK
The oversampler's precondition has two parts of different kinds: one arithmetic, exact and fully settled by simulation — below which the recovered edge count is meaningless, with a half-period of exactly half the system period recovering zero edges from forty — and one metastability, whose sharpest row is a ratio where simulation recovers every edge and silicon does not.
- Related topic
Back-to-Back Transactions and Inter-Frame Gap
How soon chip select may fall again after it rises: the minimum deselect time, why a device needs it, what the gap costs in throughput, and the hardware enforcement that keeps software from violating it.
- Related topic
Slave Microarchitecture and Clocking Assumptions
The two architectures available to a slave that does not own its clock, the one precondition the chosen architecture rests on and why no simulation can test it, why MOSI must pass through exactly as many flops as SCLK, and a delay-matched front end verified in three HDLs.
- Related topic
Why an SPI Slave Is a Clock-Domain Problem
An externally generated SCLK turns a shift register into a CDC question, and there are only four ways to carry an event across: a level, a synchronised level, a toggle, and a handshake. Each is limited by something different, no scheme creates bandwidth, and the difference that matters most is the one no simulation can show.
