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

  • How to Use This Course
  • Reading Path
  • CDC/RDC Glossary
  • CDC/RDC Metrics Reference
  • CDC/RDC Interview Prep
  • CDC/RDC Signoff Cheatsheet
  • Clock Domains and Failure Modes8
  • Metastability and Synchronizers16
  • CDC Protocols and Handshakes16
  • Reconvergence and Gray Coding16
  • Reset Domain Crossing16
  • Architecture and Design Rules16
  • CDC Signoff Methodology24
  • RDC Signoff16
  • Debug and Case Studies11
  • CDC and RDC Interview Prep30
  • Capstone: Sign Off a Multi-clock Peripheral4
    • Capstone Brief
    • Classify the Report
    • Fix and Verify
    • Waive and Sign Off
Loading lesson

‹Capstone: Sign Off a Multi-clock Peripheral
  1. Topics
  2. CDC & RDC
  3. Capstone: Sign Off a Multi-clock Peripheral
  4. Fix and Verify

Part 11 · Capstone Signoff Lab · Advanced

Fix and Verify

Repair the three capstone defects and prove each fix structurally, with assertions, and in simulation.

On this page

    Three findings from the report need RTL, not paperwork. The event gets a toggle synchroniser, the status word gets a Gray-coded crossing, and the configuration domain gets its own reset synchroniser. Each fix below is the standard pattern from its own section — linked, not re-taught — applied to the same module names, peri_top and rst_sync, that the brief introduced.

    Fix 1: the event becomes a toggle

    The engine flips eng_toggle on every event instead of emitting a pulse, the level crosses through a two-flop synchroniser in the configuration domain (tg_a, tg_b), and an edge-detect register (tg_c) with an XOR (tg_b ^ tg_c) rebuilds one clean pulse per flip. The level persists until the next event, so no destination edge can miss it. The rule that makes it safe is spacing: events arrive every 16 engine cycles, which is 160 ns, while two configuration periods are 54 ns — so consecutive flips can never merge inside one destination period. The full scheme and its re-toggle rule are taught in the toggle synchroniser lesson with the spacing limits beside it.

    Fix 2: the status word goes Gray

    The engine Gray-encodes the counter (eng_count ^ (eng_count >> 1)), the Gray word crosses through two flops per bit, and the configuration domain decodes it back to binary. This works because the counter is monotonic: it advances by one, so the Gray word changes exactly one bit per step, and a synchroniser sampling mid-change can only deliver the old or the new value — both genuine. A binary count changing from 7 to 8 flips all four bits and could sample as anything; the Gray version cannot tear. Why per-bit synchronisation fails on plain binary is the subject of the bus tearing lesson.

    Fix 3: every domain gets a reset synchroniser

    The second rst_sync instance, u_rst_cfg, gives the configuration domain what the engine domain already had: asynchronous assert with release synchronised to its own clock. Every flop in the design now releases on its own clock edge, so no domain wakes half-reset. The required structure and the deassertion hazard it removes are covered in the reset synchroniser lesson. The threshold capture is handled in Fix 4 below, because it is the one crossing that is not fixed with a synchroniser.

    Fix 4: the threshold becomes quasi-static by construction

    The threshold crosses from clk_cfg to clk_eng as a 4-bit bus, which a plain synchroniser cannot protect. Instead of waiving a promise about software, the design makes the promise true. Writes are accepted only while the engine is fully stopped (cfg_run low and the engine's run state, synchronised back through two flops, low). The run request cannot start until two quiet cycles after the last write, so the threshold has been stable for at least two configuration periods before the engine copies it. The engine copies it exactly once per run, at start, and ignores writes while running. The bus is therefore stable whenever the engine samples it, which is the argument the waiver in the next lesson relies on. This is the pattern from the quasi-static lessons: a run handshake around a configuration bus.

    The fixed design with its testbench

    The listing below is self-contained: both synchroniser-correct modules plus a testbench that exercises each crossing deterministically — fixed clock periods (10 ns engine, 27 ns configuration with staggered edges), a fixed threshold pattern, PASS/FAIL reporting, $finish, and a watchdog. It checks four things: outputs stay zero under reset, every sampled status word is a value the engine genuinely produced (the no-tear check against recent engine history), a write attempted while the engine runs is ignored and the threshold never changes while it runs, and after the run stops and every event drains, the number of events delivered equals the number launched (none lost, none doubled).

    rst_sync.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
    `timescale 1ns/1ps
    module rst_sync (
      input wire clk, input wire rst_n_async, output wire rst_n_sync
    );
      reg [1:0] sh;
      always @(posedge clk or negedge rst_n_async) begin
        if (!rst_n_async) sh <= 2'b00;
        else sh <= {sh[0], 1'b1};
      end
      assign rst_n_sync = sh[1];
    endmodule
    
    module peri_top (
      input wire clk_cfg, input wire clk_eng, input wire rst_n_async,
      input wire cfg_wr, input wire [3:0] cfg_wdata, input wire cfg_run,
      output wire [3:0] status_cfg, output wire event_cfg
    );
      wire rst_n_eng, rst_n_cfg;
      rst_sync u_rst_eng (.clk(clk_eng), .rst_n_async(rst_n_async), .rst_n_sync(rst_n_eng));
      rst_sync u_rst_cfg (.clk(clk_cfg), .rst_n_async(rst_n_async), .rst_n_sync(rst_n_cfg));
    
      reg [3:0] cfg_thresh_q;
      reg run_cfg, wr_q, ack_a, ack_b;       // clk_cfg side of the run handshake
      reg run_a, run_b;                       // run request synchronised into clk_eng
      reg [3:0] eng_count, eng_thresh;
      reg eng_toggle;
      reg tg_a, tg_b, tg_c;
      reg [3:0] gr_a, gr_b;
      wire [3:0] eng_gray = eng_count ^ (eng_count >> 1);
      wire idle_cfg = ~run_cfg & ~ack_b;      // engine fully stopped, as seen from clk_cfg
    
      // Quasi-static threshold by construction: writes are accepted only while the engine is
      // stopped, and run can start only after two quiet cycles without a write.
      always @(posedge clk_cfg or negedge rst_n_cfg) begin
        if (!rst_n_cfg) begin
          cfg_thresh_q <= 4'h0; run_cfg <= 1'b0; wr_q <= 1'b0; ack_a <= 1'b0; ack_b <= 1'b0;
        end else begin
          wr_q <= cfg_wr;
          if (cfg_wr && idle_cfg) cfg_thresh_q <= cfg_wdata;
          run_cfg <= cfg_run & ~cfg_wr & ~wr_q;
          ack_a <= run_b;                     // engine run state, synchronised back (two flops)
          ack_b <= ack_a;
        end
      end
    
      always @(posedge clk_eng or negedge rst_n_eng) begin
        if (!rst_n_eng) begin
          run_a <= 1'b0; run_b <= 1'b0;
          eng_count <= 4'h0; eng_thresh <= 4'h0; eng_toggle <= 1'b0;
        end else begin
          run_a <= run_cfg;                   // two-flop synchroniser for the run request
          run_b <= run_a;
          if (run_a & ~run_b) eng_thresh <= cfg_thresh_q;   // one capture per run, threshold is stable
          if (run_b) begin
            eng_count <= (eng_count == 4'hF) ? 4'h0 : eng_count + 4'h1;
            if (eng_count == eng_thresh) eng_toggle <= ~eng_toggle;
          end
        end
      end
    
      always @(posedge clk_cfg or negedge rst_n_cfg) begin
        if (!rst_n_cfg) begin tg_a <= 1'b0; tg_b <= 1'b0; tg_c <= 1'b0; end
        else begin tg_a <= eng_toggle; tg_b <= tg_a; tg_c <= tg_b; end
      end
      assign event_cfg = tg_b ^ tg_c;
    
      always @(posedge clk_cfg or negedge rst_n_cfg) begin
        if (!rst_n_cfg) begin gr_a <= 4'h0; gr_b <= 4'h0; end
        else begin gr_a <= eng_gray; gr_b <= gr_a; end
      end
      assign status_cfg[3] = gr_b[3];
      assign status_cfg[2] = gr_b[3] ^ gr_b[2];
      assign status_cfg[1] = gr_b[3] ^ gr_b[2] ^ gr_b[1];
      assign status_cfg[0] = gr_b[3] ^ gr_b[2] ^ gr_b[1] ^ gr_b[0];
    endmodule
    

    How each fix is verified

    One proof is never enough for a crossing: structure says the right circuit is there, assertions say its rules hold on every run, and simulation says it behaves on the tested scenarios. The table names all three per fix, so the gate can check each column independently.

    Fix

    Structural

    Assertion

    Simulation (in the testbench above)

    Toggle event (F1)

    Source toggle flop, two configuration flops with no logic between, edge detect on the last flop only

    One destination pulse per toggle; minimum event spacing of two configuration periods

    Events launched in the engine domain equal events delivered in the configuration domain after a fixed drain time — none lost, none doubled

    Gray status (F2)

    Gray encode at source, two flops per bit, binary decode after the last flop; source monotonic

    Every sampled status word is a value the engine produced (history-membership check)

    Membership check on every configuration edge after reset: any torn word fails the run

    Per-domain reset synchronisers (F3)

    One reset synchroniser per domain; every flop releases on its own clock

    Release aligned to the domain clock; no flop releases on the asynchronous edge

    Outputs read zero under asserted reset; staggered per-domain release before counting starts

    Threshold capture (F4, waived)

    Direct capture with a run handshake: writes accepted only while the engine is stopped, one capture per run (no synchroniser by intent)

    Threshold never changes while the engine runs (the testbench monitor is the runnable form)

    A write attempted while running is ignored and the monitor stays quiet; argued in full in the signoff lesson

    Interview

    Draw the toggle path with both clocks labelled, state the 160 ns spacing against the 54 ns two-period rule, and volunteer the failure mode when the rule breaks: merged events with a silently diverging count. The follow-up is always Gray scope — answer with monotonicity before the interviewer asks.

    Common pitfall

    Gray code only protects monotonic sequences. This engine's free-running counter qualifies; a threshold register rewritten with arbitrary values would not — a jump from 7 to 0 in binary is still one Gray step only if the design walks through the intermediate codes. Never point at an encoder and stop: the proof is encoder plus monotonic source, and the assertion must pin the monotonic part.

    Check yourself The testbench compares events launched with events delivered after a drain time instead of expecting an exact count in a time window. Why is that more robust, and what would a mismatch mean?

    A fixed time window can be off by one at its boundaries (a flip in flight at the start or end), so a correct design could fail or a faulty one pass by luck. Counting launches in the source domain and deliveries in the destination domain after the source stops removes the boundary: fewer delivered than launched means an event was lost (spacing or toggle fault); more means a double delivery (edge-detect or re-toggle fault).

    Check yourself Simulation passes on this fixed version. Does that prove the crossings are safe?

    No — it proves the tested scenarios behave. Safety comes from the other two columns: the structure guarantees delivery across all phases, and the assertions re-check the rules on every future run. Simulation is the weakest of the three proofs, kept because it is the most readable.

    Exercise

    Extend it: halve the configuration period in the testbench (faster destination) and re-run — it still passes, showing the spacing margin. Then, in a scratch copy, swap the toggle path back to the first version's plain two-flop event synchroniser and observe what the count check reports. Explain the number from the 10 ns pulse width against the destination period — and if the run still passes, explain why that pass proves nothing: the phase luck from the first lesson's trap, reproduced on demand.

    Related topics

    • Back: Classify the Report

    • Next: Waive and Sign Off

    • Toggle Synchronizer

    • Bus Tearing

    • Reset Synchroniser Structure

    • RTL: CDC Protocols

    • RTL: Reset Strategies

    Previous

    165 / 179

    Next: Waive and Sign Off

    Up next

    Up nextWaive and Sign Off

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Capstone Brief