Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • RTL Foundations36
  • Combinational Design25
  • Sequential Design27
  • FSM & Control27
  • Timing & Clocking39
  • Reset & CDC34
    • Reset Architecture
    • Metastability and Synchronisation
    • Asynchronous FIFO
    • CDC Protocols
    • Reset Domain Crossing
    • CDC Verification
      • Rules
      • Simulation
      • Assertion Patterns
      • FIFO Tests
      • Signoff
      • Reading a CDC Report
      • A CDC Mini Project
  • Memory & Buses57
  • Advanced RTL28
  • Senior & Interview76
  • Glossary and Notation
Loading lesson

‹CDC Verification
  1. Topics
  2. RTL Design
  3. Reset & CDC
  4. CDC Verification
  5. A CDC Mini Project

Part 6 · Reset & CDC · Intermediate

A CDC Mini Project

Build a two-clock block from the course's synchroniser, handshake, async FIFO and reset synchroniser, then prove it with a hostile test over 20 seeds.

On this page

    Why this matters

    Each CDC structure can pass its own lesson and the block built from them can still fail: a data synchroniser added in the wrong place corrupts commands, and a raw reset release fails only some seeds. The test has to be hostile and run over many seeds.

    After this lesson you can: build a two-clock block from a synchroniser, a handshake, an async FIFO and per-domain resets, list every crossing with its scheme, and prove it with a hostile test and mutants.

    The project: one block, two clocks, eight crossings

    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.

    The mini project: the fast domain on the left, the slow domain on the right, and eight numbered crossings: the enable, req, the held command word, ack, the FIFO write pointer, the FIFO data, the FIFO read pointer and the reset

    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.

    The spec

    Function

    Direction

    Requirement

    Enable

    fast to slow

    Level control. en_fast holds each value for at least 3 slow cycles; en_slow follows it with no extra or missing change

    Commands

    fast to slow

    8-bit opcode and 16-bit argument. Every accepted command arrives once, in order, uncorrupted, as a one-cycle cmd_valid_slow pulse

    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.

    cdc_mini.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // 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
    endmodule

    The reference solution: reuse, then connect

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

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 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.

    The hostile test

    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.

    Diagram
    $ 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 64

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

    Two classic mistakes the test catches

    Synchronising the held command word bit by bit

    design.sv
    1
    2
    3
    4
    5
    6
    // 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);
    Diagram
    FATAL: cdc_mut1.sv:358: FAIL: seed 1 epoch 1: command 25 sent 48:6a77, received 4b:4276
           Time: 8352462  Scope: tb_cdc_mini

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

    Releasing both domains at once

    design.sv
    // bad-example: the raw asynchronous release reaches both domains directly
    assign rst_fast_n = arst_n;
    assign rst_slow_n = arst_n;
    Diagram
    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_mini

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

    Common pitfall

    Stopping at one passing seed. The reset mutant above passes seed 1. A hostile test is evidence only with its counts: several seeds, window hits on every crossing, and each mutant shown to fail. A seed that passes with zero hits on a crossing has not tested that crossing.

    Done when

    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: hs_src loads data only in IDLE, the command scoreboard, and a max-delay constraint

    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

    Warning

    meta_ff returns a random old or new value inside a fixed window and nothing more: it has no late resolution, and the reset check is a timing model, not a recovery/removal analysis. Passing seeds show the protocols survive random capture; reliability needs the MTBF budget and STA.

    Stretch goals

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

    Check yourself Crossing 3 carries 24 bits with no synchroniser. Why is that legal here, and what must implementation add?

    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.

    Check yourself The bit-by-bit mutant passes with ideal flops. Which number in the PASS output tells you the reference run could have caught it?

    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.

    Project files

    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

    cdc_mini.sv

    The design: async FIFO, handshake crossing, reset synchronizers and the two-clock top

    tb_cdc_mini.sv

    The hostile bench: random clock ratios, metastability perturbation and reset releases

    run.sh

    Diagram
    #!/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=7

    Expected output: the line a correct design makes the simulation print at the end of its run.

    Diagram
    PASS: seed 1, 10 epochs: 2000 commands, 5041 results, 2505 enable changes, 1860 FIFO-full cycles, 20 reset releases

    Testbench

    The reused modules exactly as their lessons list them, the reference solution, and the hostile test. Run it with +seed=N for other seeds.

    meta_ff.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    // ---- 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/1ps

    Remember

    • List every signal that crosses the boundary and give each one a scheme: a level synchroniser, a handshake, Gray pointers or a FIFO.

    • Data held under a protocol needs a max-delay constraint, not a synchroniser; per-bit synchronisers on it can make it worse.

    • A passing run counts only with its numbers: several seeds, window hits on every crossing, and each mutant shown to fail.

    Rule of thumb: no crossing without a scheme, no seed count of one, no pass without window hits.

    How this was checked

    Tool: 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.

    Keep going

    • Practice: CDC multi-bit bus transfer: data corruption without a handshake

    • Practice: CDC reset synchronisation: asynchronous reset deassertion

    • Practice: Asynchronous FIFO with CDC handling

    • Code lab: CDC handshake

    • Code lab: Async FIFO gray ptr

    • Code lab: Reset synchronizer

    • Interview: run a mock interview on this topic

    Related topics

    • Reading a CDC report

    • Signoff

    • 4-Phase Request/Acknowledge

    Previous

    188 / 350

    Next: Memory & Buses

    Up next

    Up nextMemory & Buses

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Rules
    • Simulation
    • Assertion Patterns