Add a synchroniser to every bit of a command bus that already crosses under a handshake, and the block gets worse, not safer: FAIL: seed 1 epoch 1: command 25 sent 48:6a77, received 4b:4276. With ideal flops the same design passes 20 seeds out of 20. This project builds a small two-clock block from the modules of this part of the course and a test hostile enough to tell those two cases apart.
Each numbered dot is one place where a signal leaves one clock and is used by the other. The checklist at the end asks for a scheme for every dot.
Dots 1, 2, 4: single-bit levels, each through a two-flop synchroniser on the receiving clock.
Dots 3 and 6: multi-bit data with no synchroniser. That is safe only because a protocol keeps the data still while the other side reads it.
Dots 5 and 7: Gray-coded FIFO pointers, which change one bit per step. Dot 8: one reset source, released separately in each domain.
Function | Direction | Requirement |
|---|---|---|
Enable | fast to slow | Level control. |
Commands | fast to slow | 8-bit opcode and 16-bit argument. Every accepted command arrives once, in order, uncorrupted, as a one-cycle |
Results | slow to fast | 24-bit words streamed through a FIFO with back-pressure on both sides; none lost, repeated or reordered |
Reset | both | One asynchronous source. Each domain asserts at once and releases on its own clock |
Clocks | both | About 200 MHz and 37 MHz, unrelated: a ratio near 5.4 that is never an integer |
The starter interface fixes the ports, so a solution can be dropped into the hostile test unchanged.
// snippet: starter interface (the complete listing is under Testbench)
module cdc_mini #(parameter int AW = 3) ( // result FIFO depth 2**AW = 8
input logic arst_n, // one asynchronous reset source
input logic clk_fast, // fast domain, about 200 MHz
output logic rst_fast_n,
input logic en_fast,
input logic cmd_valid_fast,
output logic cmd_ready_fast,
input logic [7:0] cmd_op_fast,
input logic [15:0] cmd_arg_fast,
output logic res_valid_fast,
input logic res_ready_fast,
output logic [23:0] res_data_fast,
input logic clk_slow, // slow domain, about 37 MHz
output logic rst_slow_n,
output logic en_slow,
output logic cmd_valid_slow, // one-cycle pulse per command
output logic [7:0] cmd_op_slow,
output logic [15:0] cmd_arg_slow,
input logic res_valid_slow,
output logic res_ready_slow,
input logic [23:0] res_data_slow
);
// your crossings here
endmoduleNo new synchroniser is written. reset_sync comes from Async Assert, Sync Deassert, sync2 and hs_cdc from 4-Phase Request/Acknowledge, and async_fifo from Full, Empty and the Complete Design. The whole solution is these instances:
// snippet: the body of cdc_mini
reset_sync #(2) u_rst_fast (.clk(clk_fast), .arst_n, .srst_n(rst_fast_n));
reset_sync #(2) u_rst_slow (.clk(clk_slow), .arst_n, .srst_n(rst_slow_n));
sync2 u_en (.clk(clk_slow), .d(en_fast), .q(en_slow)); // crossing 1
hs_cdc #(24) u_cmd ( // crossings 2, 3, 4
.clk_s(clk_fast), .rst_s_n(rst_fast_n), .clk_d(clk_slow), .rst_d_n(rst_slow_n),
.send_valid(cmd_valid_fast), .send_data({cmd_op_fast, cmd_arg_fast}), .send_ready(cmd_ready_fast),
.out_valid(cmd_valid_slow), .out_data({cmd_op_slow, cmd_arg_slow}));
logic wfull, rempty;
async_fifo #(.DW(24), .AW(AW), .SKEW(1)) u_res ( // crossings 5, 6, 7
.wclk(clk_slow), .wrst_n(rst_slow_n), .winc(res_valid_slow), .wdata(res_data_slow),
.wfull(wfull), .walmost_full(),
.rclk(clk_fast), .rrst_n(rst_fast_n), .rinc(res_ready_fast), .rdata(res_data_fast),
.rempty(rempty), .ralmost_empty());
assign res_ready_slow = !wfull;
assign res_valid_fast = !rempty;Port naming: hs_cdc calls its source clock clk_s; here the source is the fast domain, so clk_s is clk_fast.
Simulation build: sync2 and the FIFO's ptr_sync with SKEW = 1 contain meta_ff, the course's random-capture model. A synthesis build uses the plain two-flop chains (sync2_rtl, SKEW = 0) with the same connections.
Reset order: the FIFO's write side takes rst_slow_n and its read side rst_fast_n; both clear together from one arst_n, as the async FIFO reset lesson requires.
One run is 10 epochs. Each epoch asserts arst_n, draws a new fast period (4.5 to 5.5 ns) and slow period (24 to 30 ns), holds reset for 5 to 8 slow cycles so the pointer synchronisers flush, and releases at a random instant. Then it sends 200 commands with random gaps of 0 to 120 fast cycles, toggles the enable every 24 to 80 fast cycles, and drains.
Clocks: both carry plus or minus 0.1 ns of jitter on every half period, and the slow clock starts at a random phase.
Back-pressure: the slow engine sends a result in 3 cycles out of 4 and only while en_slow is high; the fast reader alternates random readiness with stalls of 50 to 400 cycles, so the FIFO fills.
Scoreboards: each command is checked in the slow domain against the oldest accepted one; each result in the fast domain against the expected list. Each epoch ends by checking the enable's change count and final value.
Reset check: a release that does not come from the domain's own clock edge must fall at least 0.15 ns after one edge (removal) and 0.30 ns before the next (recovery), the windows of the recovery and removal lesson.
$ vvp -n all.vvp +seed=1
PASS: seed 1, 10 epochs: 2000 commands, 5041 results, 2505 enable changes, 1860 FIFO-full cycles, 20 reset releases
window hits: enable 61, req 30, ack 337, write pointer 461, read pointer 64Seeds 1 to 20 all print PASS. The second line is the coverage: how often a change landed inside a synchroniser's setup or hold window. Every crossing was hit, so the random-capture model really did return old and new values there. The req count is low because the handshake locks most req edges to a fixed phase of the slow clock; the long random gaps are there to break that lock.
// bad-example: inside hs_cdc, every bit of the held word through its own sync2
logic [W-1:0] data_sync;
for (genvar i = 0; i < W; i++) begin : g_bit
sync2 u_data (.clk(clk_d), .d(data[i]), .q(data_sync[i]));
end
hs_dst #(W) d (.clk(clk_d), .rst_n(rst_d_n), .req_sync, .data(data_sync), .ack, .out_valid, .out_data);FATAL: cdc_mut1.sv:358: FAIL: seed 1 epoch 1: command 25 sent 48:6a77, received 4b:4276
Time: 8352462 Scope: tb_cdc_miniAll 20 seeds fail, the latest at command 939. The word and req change on the same fast edge. When that edge lands in the slow flop's window, req can resolve to new while some data bits resolve to old. One edge later hs_dst sees req_sync and captures data_sync, which still holds the old bits: half of the previous command. With the windows set to zero, the same mutant passes 20 of 20 seeds, so only the random-capture model shows it. The fix is to delete the data synchronisers. The word is held from before req rises until after ack, so it needs a max-delay constraint, not a synchroniser.
// bad-example: the raw asynchronous release reaches both domains directly
assign rst_fast_n = arst_n;
assign rst_slow_n = arst_n;FATAL: cdc_mut2.sv:296: FAIL: seed 7 epoch 1: rst_fast_n released 0.27 ns before a clk_fast edge (recovery 0.30 ns)
Time: 209121 Scope: tb_cdc_miniThis mutant fails only 10 of the 20 seeds. A release at a random instant lands in the 0.45 ns of windows in about 9% of 5 ns fast cycles, so a seed with 10 fast releases misses all of them roughly 4 times in 10. The fix is the reference: one reset_sync per domain, so each domain releases on its own clock edge.
Item | Done when | Evidence in the reference solution |
|---|---|---|
Every crossing listed | Each signal crossing the boundary has a row with its scheme | 1 enable: sync2. 2 req, 4 ack: sync2. 3 word: held under req/ack. 5, 7 pointers: Gray through ptr_sync. 6 FIFO data: written before its pointer crosses. 8 reset: reset_sync per domain |
No bare multi-bit crossing | Every bus crosses under a protocol: handshake, Gray code or FIFO | Crossings 3, 5, 6 and 7; no bus has per-bit synchronisers |
Reset sequencing | One source, assert at once, release per domain, held until the synchronisers flush | 0 recovery or removal failures in 20 releases per seed; reset held 5 to 8 slow cycles |
Hostile test passes | N seeds pass with window hits on every crossing; each mutant fails | 20 of 20 seeds pass; mutant 1 fails 20 of 20, mutant 2 fails 10 of 20 |
CDC-report review | Each crossing classed as in Reading a CDC Report, fixed or waived with evidence | The review below |
No CDC tool runs in this course's flow, so the review below is an illustrative, vendor-neutral sketch of what a structural tool would raise. It uses the classes of Reading a CDC Report.
Crossing | Likely class | Disposition |
|---|---|---|
1, 2, 4 | Synchronised single bit | Clean: approved module, only the second flop's output used, levels held 3 or more slow cycles |
3 | Multi-bit crossing, no synchroniser | Waive as held under the handshake; evidence: |
5, 7 | Bus through per-bit synchronisers | Waive as Gray-coded from a register: one bit changes per step; evidence: the pointer check from Assertion Patterns |
6 | Multi-bit crossing, no synchroniser | Waive as FIFO memory: an entry is read only after its write pointer has crossed |
8 | Reset crossing | Clean: one reset_sync per domain |
Replace the 4-phase handshake with mux recirculation and compare commands per microsecond.
Bind the assertion patterns to crossings 2 to 5: req held until ack, data stable while req is high, Gray one bit per step.
Use walmost_full from Almost Full and Almost Empty to stop the engine early, then size the FIFO for a burst of 16 results.
Reset only the slow domain in the middle of traffic and make the block survive it, following Reset Isolation Pattern.
The word is loaded only in IDLE and held from before req rises until ack has returned, so the slow side never captures it while it changes. Implementation must bound the data path delay below the earliest capture, about two slow periods, with a max-delay constraint, as the 4-phase lesson shows.
The req window hits: 30 in seed 1. Those are the edges where req, and the word launched with it, landed inside the slow flop's window, which is exactly when the mutant corrupts a command. With zero hits on req the test could not have caught it.
The complete listing below contains every file of this project, each starting at a // ---- file: <name> ---- line. Save each part under its name in one folder, then run the script.
File | What it holds |
|---|---|
| The design: async FIFO, handshake crossing, reset synchronizers and the two-clock top |
| The hostile bench: random clock ratios, metastability perturbation and reset releases |
run.sh
#!/bin/sh
set -e
iverilog -g2012 -Wall -o all.vvp cdc_mini.sv tb_cdc_mini.sv
vvp -n all.vvp +seed=1 # try other seeds: +seed=7Expected output: the line a correct design makes the simulation print at the end of its run.
PASS: seed 1, 10 epochs: 2000 commands, 5041 results, 2505 enable changes, 1860 FIFO-full cycles, 20 reset releasesThe reused modules exactly as their lessons list them, the reference solution, and the hostile test. Run it with +seed=N for other seeds.
// ---- file: cdc_mini.sv ----
`timescale 1ns/1ps
// Functional old/new perturbation, not an analog resolution-time model.
// events counts bit/window exposures (not unique transactions or physical failures).
module meta_ff #(parameter real TCQ = 0.25, TSU = 0.3, THD = 0.15) (input logic clk, d, output logic q, output int events);
realtime t_d = -100.0, t_clk = -100.0;
initial begin q = 1'b0; events = 0; end
always @(d) begin
t_d = $realtime;
if ($realtime - t_clk < THD) begin events++; q <= #TCQ $urandom_range(0, 1); end
end
always @(posedge clk) begin
t_clk = $realtime;
if ($realtime - t_d < TSU) begin events++; q <= #TCQ $urandom_range(0, 1); end
else q <= #TCQ d;
end
endmodule
// Pointer synchroniser: two flops in the destination domain.
// SKEW = 1 is a SIMULATION model (one metastability-aware flop per bit); SKEW = 0 is the plain synthesisable circuit.
module ptr_sync #(parameter int W = 5, parameter bit SKEW = 0) (
input logic clk, rst_n,
input logic [W-1:0] d,
output logic [W-1:0] q
);
int window_hits;
if (SKEW) begin : g_meta
logic [W-1:0] q1;
int first_hits [W], second_hits [W];
always_comb begin
window_hits = 0;
for (int i = 0; i < W; i++) window_hits += first_hits[i];
end
for (genvar i = 0; i < W; i++) begin : g_bit
meta_ff f1 (.clk, .d(d[i]), .q(q1[i]), .events(first_hits[i]));
meta_ff f2 (.clk, .d(q1[i]), .q(q[i]), .events(second_hits[i]));
end
end else begin : g_plain
assign window_hits = 0;
logic [W-1:0] q1;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin q1 <= '0; q <= '0; end
else begin q1 <= d; q <= q1; end
end
endmodule
module async_fifo #(
parameter int DW = 8,
parameter int AW = 4, // depth = 2**AW; legal range AW >= 1 (DEPTH >= 2)
parameter bit GRAY = 1, // 0 = (wrong) binary pointers across the crossing, for the experiment
parameter bit SKEW = 0, // simulation-only skew model in the pointer synchronisers
parameter int MARGIN = 2 // legal range 0 <= MARGIN <= DEPTH
) (
input logic wclk, wrst_n, winc,
input logic [DW-1:0] wdata,
output logic wfull, walmost_full,
input logic rclk, rrst_n, rinc,
output logic [DW-1:0] rdata,
output logic rempty, ralmost_empty
);
localparam int DEPTH = 1 << AW;
if (AW < 1) begin : g_aw_check // a one-entry FIFO has no address bits, so the pointer slices below would be empty
$error("async_fifo: AW must be at least 1 (DEPTH >= 2)");
end
if (MARGIN < 0 || MARGIN > DEPTH) begin : g_margin_check
initial $fatal(1, "async_fifo: MARGIN must be between 0 and DEPTH inclusive");
end
logic [DW-1:0] mem [DEPTH];
// ---------------- write domain ----------------
logic [AW:0] wbin, wbin_next, wgray, wgray_next, wptr_out;
logic [AW:0] rptr_sync_w; // read pointer, synchronised into the write domain
logic [AW:0] rbin_in_w;
logic wfull_next;
assign wbin_next = wbin + (winc & ~wfull);
assign wgray_next = (wbin_next >> 1) ^ wbin_next;
assign wptr_out = GRAY ? wgray : wbin;
// full: next write pointer has caught up with the (synchronised) read pointer, one lap ahead
if (GRAY) begin : g_full_gray
localparam logic [AW:0] TOP2 = 3 << (AW - 1); // the two top bits set (valid down to AW = 1)
assign wfull_next = (wgray_next == (rptr_sync_w ^ TOP2)); // Gray code of (rptr + DEPTH): the two top bits inverted
end else begin : g_full_bin
assign wfull_next = (wbin_next[AW] != rptr_sync_w[AW]) && (wbin_next[AW-1:0] == rptr_sync_w[AW-1:0]);
end
always_ff @(posedge wclk or negedge wrst_n)
if (!wrst_n) begin wbin <= '0; wgray <= '0; wfull <= 1'b0; end
else begin wbin <= wbin_next; wgray <= wgray_next; wfull <= wfull_next; end
always_ff @(posedge wclk)
if (winc && !wfull) mem[wbin[AW-1:0]] <= wdata;
// ---------------- read domain ----------------
logic [AW:0] rbin, rbin_next, rgray, rgray_next, rptr_out;
logic [AW:0] wptr_sync_r; // write pointer, synchronised into the read domain
logic rempty_next;
assign rbin_next = rbin + (rinc & ~rempty);
assign rgray_next = (rbin_next >> 1) ^ rbin_next;
assign rptr_out = GRAY ? rgray : rbin;
assign rempty_next = GRAY ? (rgray_next == wptr_sync_r) : (rbin_next == wptr_sync_r);
always_ff @(posedge rclk or negedge rrst_n)
if (!rrst_n) begin rbin <= '0; rgray <= '0; rempty <= 1'b1; end
else begin rbin <= rbin_next; rgray <= rgray_next; rempty <= rempty_next; end
assign rdata = mem[rbin[AW-1:0]]; // first-word-fall-through read
// ---------------- crossings ----------------
ptr_sync #(AW + 1, SKEW) u_r2w (.clk(wclk), .rst_n(wrst_n), .d(rptr_out), .q(rptr_sync_w));
ptr_sync #(AW + 1, SKEW) u_w2r (.clk(rclk), .rst_n(rrst_n), .d(wptr_out), .q(wptr_sync_r));
// ---------------- almost flags (conservative: computed from synchronised, hence stale, pointers) ----------------
function automatic logic [AW:0] to_bin(logic [AW:0] g);
logic [AW:0] b;
b[AW] = g[AW];
for (int i = AW - 1; i >= 0; i--) b[i] = b[i+1] ^ g[i];
return b;
endfunction
logic [AW:0] r_in_w_bin, w_in_r_bin, fill_w, fill_r;
assign r_in_w_bin = GRAY ? to_bin(rptr_sync_w) : rptr_sync_w;
assign w_in_r_bin = GRAY ? to_bin(wptr_sync_r) : wptr_sync_r;
assign fill_w = wbin - r_in_w_bin; // occupancy as the write side sees it (>= true occupancy)
assign fill_r = w_in_r_bin - rbin; // occupancy as the read side sees it (<= true occupancy)
assign walmost_full = (fill_w >= DEPTH - MARGIN);
assign ralmost_empty = (fill_r <= MARGIN);
endmodule
module sync2 (input logic clk, d, output logic q);
logic q1;
meta_ff f1 (.clk, .d, .q(q1));
meta_ff f2 (.clk, .d(q1), .q);
endmodule
// ---------------- 4-phase request/acknowledge transfer of one word ----------------
module hs_src #(parameter int W = 8) (
input logic clk, rst_n,
input logic send_valid, // producer offers a word
output logic send_ready, // high only when a new word can be accepted
input logic [W-1:0] send_data,
output logic req, // request to the destination
output logic [W-1:0] data, // held STABLE while req is high
input logic ack_sync // acknowledge, already synchronised into this domain
);
typedef enum logic [1:0] {IDLE, WAIT_ACK, WAIT_NACK} state_t;
state_t state;
assign send_ready = (state == IDLE);
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin state <= IDLE; req <= 1'b0; data <= '0; end
else unique case (state)
IDLE: if (send_valid) begin data <= send_data; req <= 1'b1; state <= WAIT_ACK; end
WAIT_ACK: if (ack_sync) begin req <= 1'b0; state <= WAIT_NACK; end // destination has the data
WAIT_NACK: if (!ack_sync) state <= IDLE; // destination released: ready again
default: state <= IDLE;
endcase
endmodule
module hs_dst #(parameter int W = 8) (
input logic clk, rst_n,
input logic req_sync, // request, already synchronised into this domain
input logic [W-1:0] data, // crosses WITHOUT a synchroniser: legal because it is stable while req is high
output logic ack,
output logic out_valid, // one-cycle pulse with new data
output logic [W-1:0] out_data
);
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin ack <= 1'b0; out_valid <= 1'b0; out_data <= '0; end
else begin
out_valid <= 1'b0;
if (req_sync && !ack) begin ack <= 1'b1; out_data <= data; out_valid <= 1'b1; end // capture, then acknowledge
else if (!req_sync) ack <= 1'b0;
end
endmodule
module hs_cdc #(parameter int W = 8) (
input logic clk_s, rst_s_n, clk_d, rst_d_n,
input logic send_valid, input logic [W-1:0] send_data, output logic send_ready,
output logic out_valid, output logic [W-1:0] out_data
);
logic req, ack, req_sync, ack_sync;
logic [W-1:0] data;
hs_src #(W) s (.clk(clk_s), .rst_n(rst_s_n), .send_valid, .send_ready, .send_data, .req, .data, .ack_sync);
sync2 u_req (.clk(clk_d), .d(req), .q(req_sync)); // request crosses to the destination clock
hs_dst #(W) d (.clk(clk_d), .rst_n(rst_d_n), .req_sync, .data, .ack, .out_valid, .out_data);
sync2 u_ack (.clk(clk_s), .d(ack), .q(ack_sync)); // acknowledge crosses back
endmodule
// Asynchronous assert, synchronous release.
module reset_sync #(parameter int STAGES = 2) (
input logic clk,
input logic arst_n, // asynchronous reset input (from a pin or another domain)
output logic srst_n // reset for this clock domain
);
if (STAGES < 2) begin : g_stages_check // legal range: STAGES >= 2
$error("reset_sync: STAGES must be at least 2");
end
logic [STAGES-1:0] sr;
always_ff @(posedge clk or negedge arst_n)
if (!arst_n) sr <= '0; // assert: immediately, no clock needed
else sr <= {sr[STAGES-2:0], 1'b1}; // release: shifts in 1s, one stage per clock
assign srst_n = sr[STAGES-1];
endmodule
// Reference solution: four crossings, each built from a module of this part of the course.
// Simulation build: sync2 and the FIFO's ptr_sync (SKEW = 1) use the meta_ff capture model.
module cdc_mini #(parameter int AW = 3) ( // result FIFO depth 2**AW = 8
input logic arst_n, // one asynchronous reset source
// fast domain, about 200 MHz
input logic clk_fast,
output logic rst_fast_n,
input logic en_fast, // level: hold for 3 or more slow cycles
input logic cmd_valid_fast,
output logic cmd_ready_fast,
input logic [7:0] cmd_op_fast,
input logic [15:0] cmd_arg_fast,
output logic res_valid_fast,
input logic res_ready_fast,
output logic [23:0] res_data_fast,
// slow domain, about 37 MHz
input logic clk_slow,
output logic rst_slow_n,
output logic en_slow,
output logic cmd_valid_slow, // one-cycle pulse per command
output logic [7:0] cmd_op_slow,
output logic [15:0] cmd_arg_slow,
input logic res_valid_slow,
output logic res_ready_slow,
input logic [23:0] res_data_slow
);
// Reset: asserted at once in both domains, released by each domain's own clock.
reset_sync #(2) u_rst_fast (.clk(clk_fast), .arst_n, .srst_n(rst_fast_n));
reset_sync #(2) u_rst_slow (.clk(clk_slow), .arst_n, .srst_n(rst_slow_n));
// Crossing 1, fast to slow: a level through a two-flop synchroniser.
sync2 u_en (.clk(clk_slow), .d(en_fast), .q(en_slow));
// Crossings 2 and 3, fast to slow and back: req and ack of a 4-phase handshake.
// The 24-bit word is held stable while req is high and is captured without a synchroniser.
hs_cdc #(24) u_cmd (
.clk_s(clk_fast), .rst_s_n(rst_fast_n), .clk_d(clk_slow), .rst_d_n(rst_slow_n),
.send_valid(cmd_valid_fast), .send_data({cmd_op_fast, cmd_arg_fast}), .send_ready(cmd_ready_fast),
.out_valid(cmd_valid_slow), .out_data({cmd_op_slow, cmd_arg_slow}));
// Crossings 4 and 5, slow to fast and back: Gray pointers of an async FIFO.
logic wfull, rempty;
async_fifo #(.DW(24), .AW(AW), .SKEW(1)) u_res (
.wclk(clk_slow), .wrst_n(rst_slow_n), .winc(res_valid_slow), .wdata(res_data_slow),
.wfull(wfull), .walmost_full(),
.rclk(clk_fast), .rrst_n(rst_fast_n), .rinc(res_ready_fast), .rdata(res_data_fast),
.rempty(rempty), .ralmost_empty());
assign res_ready_slow = !wfull;
assign res_valid_fast = !rempty;
endmodule
// Hostile test: every epoch draws new clock periods, a new phase and a new reset-release instant,
// then sends random commands with random gaps, toggles the enable, and back-pressures the FIFO
// on both sides. Run with +seed=N for another seed.
// ---- file: tb_cdc_mini.sv ----
`timescale 1ns/1psTool: Icarus Verilog 13.0, run on 2026-10-17. Command: iverilog -g2012 -s tb_cdc_mini -o all.vvp all.sv, then vvp -n all.vvp +seed=N for N = 1 to 20; all.sv is the Testbench listing (meta_ff, ptr_sync and async_fifo from Full, Empty and the Complete Design; sync2 and hs_cdc from 4-Phase Request/Acknowledge; reset_sync from Async Assert, Sync Deassert; cdc_mini; tb_cdc_mini). Result: 20 of 20 seeds PASS; seed 1: 2000 commands, 5041 results, 2505 enable changes, 1860 FIFO-full cycles, 20 reset releases; window hits enable 61, req 30, ack 337, write pointer 461, read pointer 64.
Mutant 1 (sync2 on every bit of the held command word inside hs_cdc): 20 of 20 seeds fail; seed 1: FAIL: seed 1 epoch 1: command 25 sent 48:6a77, received 4b:4276. With meta_ff windows set to zero (TSU = THD = 0) the same mutant passes 20 of 20 seeds. Mutant 2 (rst_fast_n and rst_slow_n tied to arst_n): 10 of 20 seeds fail; seed 7: rst_fast_n released 0.27 ns before a clk_fast edge (recovery 0.30 ns).
Scope and limits: meta_ff is the course's functional random-capture model (fixed window, no late resolution); the reset check is a recovery/removal timing model with the 0.30 ns and 0.15 ns windows of the recovery and removal lesson, and treats an edge-aligned release as synchronous; the CDC-report review on the page is illustrative, since no CDC tool is installed. No MTBF or STA claim.