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. Reading a CDC Report

Part 6 · Reset & CDC · Intermediate

Reading a CDC Report

Sort structural CDC findings into a few classes, confirm each one in the RTL, and close it with the standard fix or a waiver that carries evidence.

On this page

    Why this matters

    A CDC report with 40 findings is a week of debate if read line by line. Sorted by class, each finding has one standard fix or one kind of waiver, and only a waiver backed by a mechanism and an assertion is safe to sign.

    After this lesson you can: sort CDC findings into classes, confirm each one in the RTL, choose the standard fix, and write a waiver whose evidence would catch the bug it waives.

    Forty findings, a handful of classes

    Two days before sign-off, the structural CDC run returns 40 findings. Read one at a time, that is a week of debate; sorted by class, most of them close in an afternoon, because each class has one standard fix or one kind of waiver.

    Every finding names a source register, a destination register and their two clocks. The class says what kind of crossing it is, and the tree turns the class into an action.

    Flowchart. A finding with source, destination and two clocks leads to the question: synchroniser on the path? Yes, one bit: clean, approved module, stability rule met. No: can it change while the destination uses it? If yes, add a 2FF, a pulse synchroniser or a handshake; if no and an assertion proves it, waive as quasi-static with evidence. Several bits, or bits that rejoin: do the bits change together? A counter needs registered Gray code or an async FIFO; data or related flags need a handshake or one consolidated bit.
    From finding to action: a synchronised single bit is clean, an unsynchronised signal gets a synchroniser unless it is provably stable while used, and bits that change together need Gray code, a FIFO, a handshake or consolidation

    Run each finding through the tree from the top. Only one leaf is a waiver, and it needs evidence, not an opinion.

    • One synchronised bit: usually clean. Confirm the instance is the approved module and that nothing taps its first flop.

    • No synchroniser: add one, unless the value cannot change while the destination uses it. That case is a waiver, and an assertion is its proof.

    • Several bits: per-bit synchronisers are wrong whenever the bits change on the same edge, whether they form one bus or were synchronised apart and meet again later.

    One design with one finding of each class

    The design below is small on purpose. Domain A runs on clk_a, domain B on clk_b, and the clocks are unrelated. Each class from the tree appears once.

    sync2.v
    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
    // Two-clock example for the CDC report walk-through. clk_a: source domain, clk_b: destination.
    module sync2 (input logic clk, rst_n, d, output logic q);   // the approved 2-flop synchroniser
      logic q1;
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin q1 <= 1'b0; q <= 1'b0; end
        else        begin q1 <= d;    q <= q1;   end
    endmodule
    
    module dom_a (
      input  logic       clk_a, rst_a_n,
      input  logic       start, err, step, req, last, cfg_we,
      input  logic [7:0] cfg_d,
      input  logic       run_b,                        // from domain B
      output logic       start_a, err_a, req_a, last_a,
      output logic [3:0] cnt_a,
      output logic [7:0] cfg_a
    );
      logic busy_a;
      sync2 u_sync_busy (.clk(clk_a), .rst_n(rst_a_n), .d(run_b), .q(busy_a));
      always_ff @(posedge clk_a or negedge rst_a_n)
        if (!rst_a_n) begin
          start_a <= 1'b0; err_a <= 1'b0; req_a <= 1'b0; last_a <= 1'b0;
          cnt_a <= 4'd0;   cfg_a <= 8'd0;
        end else begin
          start_a <= start; err_a <= err; req_a <= req; last_a <= last;
          if (step) cnt_a <= cnt_a + 4'd1;
          if (cfg_we && !start_a && !busy_a) cfg_a <= cfg_d;  // only while B is idle
        end
    endmodule
    
    module dom_b (
      input  logic       clk_b, rst_b_n,
      input  logic       start_a, err_a, req_a, last_a,
      input  logic [3:0] cnt_a,
      input  logic [7:0] cfg_a,
      output logic       run_b, err_b,
      output logic [1:0] cmd_b,
      output logic [3:0] cnt_b,
      output logic [7:0] acc_b
    );
      logic req_s, last_s;
      sync2 u_sync_start (.clk(clk_b), .rst_n(rst_b_n), .d(start_a), .q(run_b));
      always_ff @(posedge clk_b or negedge rst_b_n)            // err_a captured directly
        if (!rst_b_n) err_b <= 1'b0; else err_b <= err_a;
      for (genvar i = 0; i < 4; i++) begin : g_cnt            // one synchroniser per bit
        sync2 u_sync_cnt (.clk(clk_b), .rst_n(rst_b_n), .d(cnt_a[i]), .q(cnt_b[i]));
      end
      sync2 u_sync_req  (.clk(clk_b), .rst_n(rst_b_n), .d(req_a),  .q(req_s));
      sync2 u_sync_last (.clk(clk_b), .rst_n(rst_b_n), .d(last_a), .q(last_s));
      assign cmd_b = {req_s, last_s};                          // the two bits meet again here
      always_ff @(posedge clk_b or negedge rst_b_n)            // cfg_a used without a synchroniser
        if (!rst_b_n) acc_b <= 8'd0;
        else if (run_b) acc_b <= acc_b + cfg_a;
    endmodule
    
    module cdc_demo (
      input  logic       clk_a, rst_a_n, clk_b, rst_b_n,
      input  logic       start, err, step, req, last, cfg_we,
      input  logic [7:0] cfg_d,
      output logic       run_b, err_b,
      output logic [1:0] cmd_b,
      output logic [3:0] cnt_b,
      output logic [7:0] acc_b
    );
      logic start_a, err_a, req_a, last_a;
      logic [3:0] cnt_a;
      logic [7:0] cfg_a;
      dom_a u_a (.*);
      dom_b u_b (.*);
    endmodule

    Crossing

    How it crosses

    Class

    start_a to run_b

    u_sync_start, a sync2

    Synchronised single bit

    run_b to busy_a

    u_sync_busy, a sync2

    Synchronised single bit (status back to A)

    err_a to err_b

    Straight into a clk_b flop

    Unsynchronised single bit

    cnt_a[3:0] to cnt_b

    Four sync2, one per bit

    Bus through per-bit synchronisers

    req_a, last_a to cmd_b

    Two sync2, then joined

    Reconvergence

    cfg_a[7:0] to acc_b

    None; written only while B is idle

    Quasi-static

    The report

    No CDC tool runs in this course's flow, so the report below uses an illustrative, vendor-neutral format written for this lesson. Real tools word things differently, but each gives these facts per finding: source, destination, both clocks, a class and a severity.

    Diagram
    ILLUSTRATIVE FORMAT, written for this lesson; not tool output
    design cdc_demo   clocks clk_a, clk_b (asynchronous)
    
    F1  info       synchronised single bit
        from u_a.start_a          (clk_a)
        to   u_b.u_sync_start     (clk_b)
    F2  info       synchronised single bit
        from u_b.run_b            (clk_b)
        to   u_a.u_sync_busy      (clk_a)
    F3  violation  unsynchronised single bit
        from u_a.err_a            (clk_a)
        to   u_b.err_b            (clk_b)
    F4  violation  bus through per-bit synchronisers
        from u_a.cnt_a[3:0]       (clk_a)
        to   u_b.g_cnt[0:3].u_sync_cnt (clk_b)
    F5  caution    reconvergence of synchronised bits
        from u_a.req_a, u_a.last_a (clk_a)
        to   u_b.cmd_b via u_sync_req, u_sync_last (clk_b)
    F6  violation  multi-bit crossing, no synchroniser
        from u_a.cfg_a[7:0]       (clk_a)
        to   u_b.acc_b            (clk_b)
    
    summary: 2 info, 1 caution, 3 violations

    The severities follow the Rules lesson: info is a crossing the tool recognised, a caution is one it cannot prove safe, and a violation has no recognised structure. Work through the violations first, then the cautions, then skim the info lines.

    Walking the report, finding by finding

    For each finding: say what it means, confirm it in the RTL, then fix it or waive it. The numbers come from a hostile simulation of this design, listed under Testbench: 10 ns and 7.13 ns clocks with jitter, and route delays of 0 to 1.2 ns on the crossing wires.

    F1 and F2: synchronised single bits

    The tool found its approved synchroniser. Confirm that the instance is sync2, that both flops are on the destination clock, and that only q leaves the module. Then check the stability rule: start_a and run_b are levels that hold for many cycles, so two flops are enough.

    Do not skip info lines. A crossing you expected here but cannot find means the tool did not recognise your structure, and it is reported somewhere else as a violation.

    F3: an unsynchronised single bit

    Follow err_b's D input back: it reaches a clk_a register with nothing in between. In the simulation, 341 of 7,922 changes of err_a landed less than 0.3 ns before a clk_b edge. In silicon each of those is a setup violation on err_b, which can then go metastable.

    The fix is a sync2 if err is a level that holds for at least two clk_b periods. A one-cycle pulse needs a pulse synchroniser or a handshake, because two flops can miss it. Never waive this class.

    F4: a bus through per-bit synchronisers

    Each bit of cnt_a has its own synchroniser, and the source is a binary counter, so 0111 to 1000 changes four bits on one edge. The four g_cnt instances on one vector are the sign to look for in the RTL.

    In the simulation, 1,940 of 20,968 changes seen at cnt_b were not +1. The first went 7, then 6, then 8: bit 0 arrived one edge before the others, so the destination read 6, a value the source was not holding, and the counter appeared to run backwards.

    A counter that steps by one crosses as registered Gray code: the bench sends the same counter as Gray code over the same wires and sees 0 bad changes. Arbitrary data needs a 4-phase handshake or an async FIFO. A third flop per bit does not help: route skew still splits the bits across edges.

    F5: reconvergence

    req_a and last_a are each synchronised correctly, and then they meet again in cmd_b. In the RTL, find where the two synchroniser outputs join, then ask whether the source changes both bits on the same edge. Here it does: {req, last} only moves between 00 and 11.

    In the simulation, cmd_b read 01 or 10 in 1,680 clk_b cycles, although the source never sent either. The tool reports a caution because it cannot know whether the bits are related; the designer can.

    Consolidate before synchronising. If the destination only needs "last word of a request", register req_a && last_a in domain A and send that one bit. If both bits carry information, send them as one word under a handshake.

    F6: quasi-static configuration

    Eight bits of cfg_a enter domain B with no synchroniser, and acc_b reads them on every run cycle. Now read the write side: dom_a writes cfg_a only when start_a and busy_a are both low, and busy_a is run_b synchronised back into A. So cfg_a changes only while B is idle, and it has settled long before run_b rises again.

    This is the one class where a waiver is right: the value does not change while it is used, so a synchroniser would add nothing. The simulation agrees: 0 changes of cfg_a while run_b was high, in 56,087 run cycles, while 2,139 of 4,018 write attempts arrived during a run and were blocked.

    Writing the waiver

    A waiver is a claim that a finding is safe, plus its proof. Start from the general fields in Waiver rules that scale; a CDC waiver adds the evidence and the condition that voids it.

    Field

    What to write

    Finding

    The report line, with source, destination and both clocks

    Signal

    The exact register, u_a.cfg_a[7:0], never a pattern such as cfg_*

    Reason it is safe

    Why the value cannot change while the destination uses it

    Evidence

    The RTL condition that enforces the reason, and the assertion that checks it in every regression

    Owner

    The engineer who defends it at review

    Review date

    When it was last checked against the RTL

    Expiry

    The change that voids it, such as a new writer of the register

    design.sv
    // snippet: waiver evidence for F6, bound into dom_b
    a_cfg_quiet: assert property (@(posedge clk_b) disable iff (!rst_b_n)
      run_b |-> $stable(cfg_a));

    The assertion lives in domain B, where the value is used: while run_b is high, cfg_a must not change.

    A good waiver and a bad one

    Diagram
    BAD
    F6  cfg_a is quasi-static, only written while start is low. Safe.
    
    GOOD
    finding   F6  u_a.cfg_a[7:0] (clk_a) -> u_b.acc_b (clk_b), no synchroniser
    signal    u_a.cfg_a[7:0]
    reason    written only while B is idle, so stable whenever B samples it
    evidence  write condition cfg_we && !start_a && !busy_a in dom_a, where
              busy_a is run_b synchronised to clk_a (u_sync_busy);
              assertion a_cfg_quiet bound in every regression: 0 failures
    owner     dom_b RTL owner
    review    2026-10-17
    expires   any new writer of cfg_a, any change to its write condition or
              to u_sync_busy, or any use of cfg_a outside run_b

    The bad waiver states a belief; the good one names the mechanism and the check. The gap is real. With busy_a removed from the write condition, so that cfg_a is written whenever start_a is low, the same check counted 22 changes of cfg_a while B was still running: run_b falls up to two clk_b edges after start_a, and a write in that gap reaches B mid-run. The bad waiver would have hidden that bug.

    Common mistakes

    Common pitfall

    Closing findings with a zero-delay simulation. With the route delays removed, the same bench reports 0 bad bus changes in 19,998 and no reconvergence glitches, so F4 and F5 look clean. Zero-delay RTL updates every bit of a bus at the same instant and cannot show skew. A structural finding is closed by the structure, or by a waiver that names a mechanism, never by a passing zero-delay test.

    Check yourself Someone closes F4 by making each per-bit synchroniser three flops deep. Does the finding close?

    No. A third flop lowers the chance that one bit stays metastable; it does not make four bits with different route delays land on the same edge. The destination can still read a mixed value. Use registered Gray code for the counter, or a handshake or async FIFO for data.

    Check yourself A new finding: a 3-bit mode register in domain A, read in domain B through three per-bit synchronisers. Firmware writes it once at boot, while B is held in reset. Fix or waive?

    Waive it as quasi-static, with evidence: the only write happens while B is in reset, and an assertion in B checks that mode is stable whenever B is out of reset. The per-bit synchronisers are not what makes it safe. The waiver expires if mode ever becomes writable at run time; then it needs a handshake.

    Testbench

    The bench repeats sync2, dom_a and dom_b so it runs on its own. It adds jitter on clk_b, route delays on the crossing wires, a registered Gray copy of the counter as the reference fix, and one check per finding.

    sync2.v
    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
    `timescale 1ns/1ps
    // Two-clock example for the CDC report walk-through. clk_a: source domain, clk_b: destination.
    module sync2 (input logic clk, rst_n, d, output logic q);   // the approved 2-flop synchroniser
      logic q1;
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin q1 <= 1'b0; q <= 1'b0; end
        else        begin q1 <= d;    q <= q1;   end
    endmodule
    
    module dom_a (
      input  logic       clk_a, rst_a_n,
      input  logic       start, err, step, req, last, cfg_we,
      input  logic [7:0] cfg_d,
      input  logic       run_b,                        // from domain B
      output logic       start_a, err_a, req_a, last_a,
      output logic [3:0] cnt_a,
      output logic [7:0] cfg_a
    );
      logic busy_a;
      sync2 u_sync_busy (.clk(clk_a), .rst_n(rst_a_n), .d(run_b), .q(busy_a));
      always_ff @(posedge clk_a or negedge rst_a_n)
        if (!rst_a_n) begin
          start_a <= 1'b0; err_a <= 1'b0; req_a <= 1'b0; last_a <= 1'b0;
          cnt_a <= 4'd0;   cfg_a <= 8'd0;
        end else begin
          start_a <= start; err_a <= err; req_a <= req; last_a <= last;
          if (step) cnt_a <= cnt_a + 4'd1;
          if (cfg_we && !start_a && !busy_a) cfg_a <= cfg_d;  // only while B is idle
        end
    endmodule
    
    module dom_b (
      input  logic       clk_b, rst_b_n,
      input  logic       start_a, err_a, req_a, last_a,
      input  logic [3:0] cnt_a,
      input  logic [7:0] cfg_a,
      output logic       run_b, err_b,
      output logic [1:0] cmd_b,
      output logic [3:0] cnt_b,
      output logic [7:0] acc_b
    );
      logic req_s, last_s;
      sync2 u_sync_start (.clk(clk_b), .rst_n(rst_b_n), .d(start_a), .q(run_b));
      always_ff @(posedge clk_b or negedge rst_b_n)            // err_a captured directly
        if (!rst_b_n) err_b <= 1'b0; else err_b <= err_a;
      for (genvar i = 0; i < 4; i++) begin : g_cnt            // one synchroniser per bit
        sync2 u_sync_cnt (.clk(clk_b), .rst_n(rst_b_n), .d(cnt_a[i]), .q(cnt_b[i]));
      end
      sync2 u_sync_req  (.clk(clk_b), .rst_n(rst_b_n), .d(req_a),  .q(req_s));
      sync2 u_sync_last (.clk(clk_b), .rst_n(rst_b_n), .d(last_a), .q(last_s));
      assign cmd_b = {req_s, last_s};                          // the two bits meet again here
      always_ff @(posedge clk_b or negedge rst_b_n)            // cfg_a used without a synchroniser
        if (!rst_b_n) acc_b <= 8'd0;
        else if (run_b) acc_b <= acc_b + cfg_a;
    endmodule
    
    Diagram
    F3 err_a     : 341 of 7922 changes inside a 0.3 ns setup window
    F4 cnt_a     : 1940 of 20968 changes were not +1 (first 7 -> 6 -> 8)
    F4 Gray fix  : 0 bad changes
    F5 req, last : 1680 cycles showed 01 or 10 (only 00 and 11 sent)
    F6 cfg_a     : 0 changes while run_b = 1 in 56087 cycles
    F6 gate      : 2139 of 4018 write attempts blocked
    PASS: per-bit bus and reconvergence corrupt as reported; Gray and the gated cfg stay clean

    Remember

    • A per-bit synchroniser is wrong whenever bits change on the same edge, whether they form one bus or rejoin later (reconvergence).

    • A counter crosses as registered Gray code; data crosses by handshake or async FIFO; related flags are consolidated to one bit first.

    • Only quasi-static values are waived, and the waiver names the write condition, the assertion that checks it and what voids it.

    Rule of thumb: fix by class, and waive only what an assertion keeps proving.

    How this was checked

    Icarus Verilog 13.0, 2026-10-17: iverilog -g2012 -s tb_cdc_report + vvp -n on the Testbench listing prints the six counts shown and PASS (10 ns clk_a, 7.13 ns clk_b with up to 0.3 ns of half-period jitter, start offset 1.37 ns, route delays 0 to 1.2 ns, 800 us simulated). The design listing compiles with iverilog -g2012.

    Mutant run, same bench and date: with !busy_a removed from the cfg_a write condition, the check counted 22 changes of cfg_a while run_b was high (FAIL). With every route delay removed (zero-delay variant), F4 showed 0 bad changes in 19,998 and F5 0 cycles, which is the basis of the pitfall.

    Verilator 5.048, 2026-10-17: the same bench with the snippet assertion a_cfg_quiet added (verilator --binary --timing --assert) gave 0 assertion failures; with !busy_a removed, 27 failures (Verilator's random sequence differs from Icarus, hence 27 versus 22).

    Scope and limits: the CDC report is an illustrative, vendor-neutral format written for this lesson, not the output of any tool; no structural CDC tool was run. The simulation models route skew as fixed per-bit delays and window hits as timing exposure only; it does not model metastability resolution, and its counts are for this seed and these clocks.

    Keep going

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

    • Practice: A unified CDC strategy: handshake, FIFO and synchronisers in an SoC

    • Code lab: Gray code sync bus

    • Interview: run a mock interview on this topic

    Related topics

    • Rules

    • Waiver rules that scale

    • Do not synchronise buses bit by bit

    • A CDC mini project

    Previous

    187 / 350

    Next: A CDC Mini Project

    Up next

    Up nextA CDC Mini Project

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Rules
    • Simulation
    • Assertion Patterns