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.

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
All lessons
Module 1 · What a clock is
One edge, one world: what shares a clock edge shares a world, the period and the jitter of a real edge, and where the clock enters a board.
Module 2 · Clock trees
Skew and insertion delay, the global buffer network, and the trap of gating a clock with logic.
Module 3 · PLL and MMCM
Multiply and divide one clock into another, move its phase in steps of the VCO, and which clocks the analyzer treats as related.
Module 4 · Resets
Assert asynchronously, release synchronously: the three reset kinds, the release pipe, and the tree a reset grows.
Module 5 · Metastability
The setup-hold window, the mean time between failures in integer arithmetic, and the two flops that fix it.
Module 6 · Crossing many bits
Why a binary bus tears, why Gray code does not, and the handshake that moves a pulse between worlds.
Module 7 · The asynchronous FIFO
Pointers, flags and depth: the buffer that moves a stream between two clocks.
Module 8 · Constraints
The lines that tell the analyzer what a clock is, which paths not to check, and what the pins must meet.
Module 9 · On the board
A CDC report, one crossing captured at the flip-flops, and the bitstream diff that closes the course.