t27.aiРусский

The two-flop synchronizer

You will learn

Why two flops are the fix, and what the generated model of the crossing looks like.

The fix is two flops in the destination clock, nothing between them: the opening one may go metastable, and the second samples its settled output a full period later. Each extra stage multiplies MTBF by 2 to the power T_clk times log2 e over tau -- 295400 in Q10 at 100 MHz, the spec's stage_gain_q10(). The recording shows t27c gen on clock_domain.t27: the printed Zig is the crossing model the whole course leans on, and wc -l counts it. What the two flops cost is latency: two destination clocks from edge to settled output, the same price the release pipe of module 4 pays.

Try it

In the recording, find the wc -l count of the generated model; then in the spec frame find the two_flop strategy and say which module took it apart.

Open the interactive lesson →

t27c gen on clock_domain.t27 -- the CDC model as Zig
t27c gen on clock_domain.t27 -- the CDC model as Zig ↗

t27c gen prints the clock-domain model it compiled, and wc -l says how many lines of Zig it is.

specs/fpga/clock_domain.t27

// SPDX-License-Identifier: Apache-2.0
// t27/specs/fpga/clock_domain.t27
// Clock Domain Abstraction for Trinity T27 FPGA HIR
// Defines clock sources, PLL configs, and cross-domain crossing
// Uses flat structs (parser-compatible)
// phi^2 + 1/phi^2 = 3 | TRINITY

module ClockDomain {

    // === Clock source kind ===

    pub const ClkSrcKind = enum(i8) {
        external = 0,
        pll = 1,
        dcm = 2,
        mmcm = 3,
    };

    // === Clock edge ===

    pub const ClkEdge = enum(i8) {
        posedge = 0,
        negedge = 1,
    };

    // === Crossing strategy ===

    pub const CrossStrategy = enum(i8) {
        no_cross = 0,
        two_flop = 1,
        fifo_async = 2,
        handshake = 3,
    };

    // === Clock source descriptor ===

    pub struct ClkSource {
        name : &str,
        kind : i8,
        freq_hz : u32,
        phase_deg : u32,
        jitter_ps : u32,
    }

    // === Clock domain descriptor ===

    pub struct ClkDomain {
        name : &str,
        source_name : &str,
        freq_hz : u32,
        edge : i8,
    }

    // === Cross-domain crossing descriptor ===

    pub struct ClockCrossing {
        src_domain : &str,
        dst_domain : &str,
        strategy : i8,
        data_width : u32,
    }

    // === Constructor helpers ===

    fn ext_clock(name: &str, freq_hz: u32) -> ClkSource {
        return ClkSource{
            .name = name,
            .kind = 0,
            .freq_hz = freq_hz,
            .phase_deg = 0,
            .jitter_ps = 0,
        };
    }

    fn pll_clock(name: &str, freq_hz: u32, phase_deg: u32) -> ClkSource {
        return ClkSource{
            .name = name,
            .kind = 1,
            .freq_hz = freq_hz,
            .phase_deg = phase_deg,
            .jitter_ps = 0,
        };
    }

    fn make_domain(name: &str, source_name: &str, freq_hz: u32) -> ClkDomain {
        return ClkDomain{
            .name = name,
            .source_name = source_name,
            .freq_hz = freq_hz,
            .edge = 0,
        };
    }

    fn make_crossing(src: &str, dst: &str, strategy: i8, data_width: u32) -> ClockCrossing {
        return ClockCrossing{
            .src_domain = src,
            .dst_domain = dst,
            .strategy = strategy,
            .data_width = data_width,
        };
    }

    // === Query functions ===

    fn is_external(src: ClkSource) -> bool {
        return src.kind == 0;
    }

    fn is_pll(src: ClkSource) -> bool {
        return src.kind == 1;
    }

    fn period_ns(domain: ClkDomain) -> u32 {
        if (domain.freq_hz == 0) {
            return 0;
        }
        return 1000000000 / domain.freq_hz;
    }

    fn half_period_ns(domain: ClkDomain) -> u32 {
        return period_ns(domain) / 2;
    }

    fn same_domain(a: ClkDomain, b: ClkDomain) -> bool {
        return a.name == b.name;
    }

    fn needs_crossing(a: ClkDomain, b: ClkDomain) -> bool {
        if (a.name == b.name) {
            return false;
        }
        if (a.freq_hz == b.freq_hz and a.source_name == b.source_name) {
            return false;
        }
        return true;
    }

    fn crossing_data_bits(cross: ClockCrossing) -> u32 {
        return cross.data_width;
    }

    fn is_async_cross(cross: ClockCrossing) -> bool {
        return cross.strategy == 2;
    }

    // === Tests ===

    test ext_clock_is_external
        given c = ext_clock("sys_clk", 12000000)
        then is_external(c) == true
        and is_pll(c) == false

    test pll_clock_is_pll
        given c = pll_clock("pll_clk", 100000000, 0)
        then is_pll(c) == true
        and is_external(c) == false

    test period_12mhz
        given d = make_domain("sys", "sys_clk", 12000000)
        then period_ns(d) == 83

    test period_100mhz
        given d = make_domain("fast", "pll_clk", 100000000)
        then period_ns(d) == 10

    test half_period
        given d = make_domain("sys", "sys_clk", 12000000)
        then half_period_ns(d) == 41

    test same_domain_true
        given a = make_domain("sys", "clk", 12000000)
        then same_domain(a, a) == true

    test same_domain_false
        given a = make_domain("sys", "clk", 12000000)
        and b = make_domain("fast", "pll", 100000000)
        then same_domain(a, b) == false

    test needs_crossing_diff_freq
        given a = make_domain("sys", "clk", 12000000)
        and b = make_domain("fast", "pll", 100000000)
        then needs_crossing(a, b) == true

    test needs_crossing_same
        given a = make_domain("sys", "clk", 12000000)
        then needs_crossing(a, a) == false

    test crossing_data_bits
        given c = make_crossing("sys", "fast", 1, 32)
        then crossing_data_bits(c) == 32

    test async_cross_fifo
        given c = make_crossing("sys", "fast", 2, 16)
        then is_async_cross(c) == true

    test sync_cross_not_async
        given c = make_crossing("sys", "fast", 1, 8)
        then is_async_cross(c) == false

    // === Invariants ===

    invariant ext_clock_has_freq
        given c = ext_clock("inv", 12000000)
        assert c.freq_hz > 0

    invariant period_positive_for_valid_freq
        given d = make_domain("inv", "clk", 12000000)
        assert period_ns(d) > 0

    invariant half_period_half_of_period
        given d = make_domain("inv", "clk", 12000000)
        assert half_period_ns(d) == period_ns(d) / 2

    invariant same_domain_reflexive
        given d = make_domain("inv", "clk", 12000000)
        assert same_domain(d, d) == true

    invariant needs_crossing_symmetric
        given a = make_domain("a", "clk", 12000000)
        and b = make_domain("b", "pll", 100000000)
        assert needs_crossing(a, b) == needs_crossing(b, a)

    // === Benchmarks ===

    bench period_ns_latency
        measure: nanoseconds to period_ns(make_domain("b", "c", 100000000))
        target: < 50ns
}

// phi^2 + 1/phi^2 = 3 | TRINITY

Open the lesson's spec in the player ↗

All lessons