Phase and lock
You will learn
How fine the phase knob is, how the lock model behaves, and what one widened window breaks.
Phase moves in steps of the VCO period divided by 56 -- a faster VCO is a finer knob, which is why phase work wants the VCO high. Lock is modelled with a settling window the header labels as an assumption. The recording widens that assumption by one sed: VCO_MIN_KHZ from 800000 to 400000. The 500 MHz configuration the rejection test names slides inside the widened window, and exactly one test fails, vco_below_the_window_is_rejected -- a test that names the number it refuses is a test that guards the assumption. The recording puts the line back and prints the sha256 of the restored spec.
Try it
In the recording, find the sed line, the test that fails and the sha256 of the restored spec; then in the spec frame compute the phase step for a 1000 MHz VCO.

VCO_MIN_KHZ 800000 to 400000 in one sed: the 500 MHz config the rejection test calls out slides inside the widened window and vco_below_the_window_is_rejected fails; git restores the spec.
specs/fpga/mmcm.t27
// SPDX-License-Identifier: Apache-2.0
// t27/specs/fpga/mmcm.t27
// MMCM clock generator model for Trinity T27 FPGA HIR
// One clock in, one multiplied VCO, one divided clock out (MMCME2, Artix-7)
// All times integer; frequencies in kHz unless a name says MHz
// phi^2 + 1/phi^2 = 3 | TRINITY
//
// RANGES AND THEIR SOURCES. The divide/multiply ranges below (DIVCLK 1..106,
// CLKFBOUT_MULT 2.000..64.000 in 0.125 steps, CLKOUT_DIVIDE 1..128, phase step
// 1/56 of the VCO period) are the MMCME2 envelope as documented in AMD/Xilinx
// UG472, "7 Series FPGAs Clocking Resources". The VCO window 800..1600 MHz,
// the input window 70..800 MHz and the lock window 500 us are TEACHING
// ASSUMPTIONS: UG472 and DS181 list the exact VCO and Fin limits per speed
// grade and the exact lock time per device, and a real design reads its own
// numbers from its own timing report, not from this file. They are kept here,
// labelled, so the course can teach the shape of the rule (VCO must land in a
// window; the output divides it down) without pretending to a datasheet it
// does not have.
module Mmcm {
// === Envelope constants (see header for sources) ===
pub const DIVCLK_MIN : u32 = 1;
pub const DIVCLK_MAX : u32 = 106;
pub const MULT_MIN_EIGHTHS : u32 = 16; // 2.000
pub const MULT_MAX_EIGHTHS : u32 = 512; // 64.000
pub const CLKOUT_DIV_MIN : u32 = 1;
pub const CLKOUT_DIV_MAX : u32 = 128;
pub const FIN_MIN_MHZ : u32 = 70; // assumption, see header
pub const FIN_MAX_MHZ : u32 = 800; // assumption, see header
pub const VCO_MIN_KHZ : u32 = 800000; // 800 MHz, assumption, see header
pub const VCO_MAX_KHZ : u32 = 1600000; // 1600 MHz, assumption, see header
pub const PHASE_GRAN_DIVISOR : u32 = 56; // 1/56 of the VCO period, UG472
pub const LOCK_WINDOW_US : u32 = 500; // assumption, see header
// === MMCM configuration ===
pub struct MmcmConfig {
name : &str,
fin_mhz : u32,
divclk_divide : u32,
clkfbout_mult_eighths : u32, // M * 8, so 0.125 steps stay integer
clkout0_divide : u32,
}
fn mmcm(name: &str, fin_mhz: u32, divclk_divide: u32, mult_eighths: u32, clkout0_divide: u32) -> MmcmConfig {
return MmcmConfig{
.name = name,
.fin_mhz = fin_mhz,
.divclk_divide = divclk_divide,
.clkfbout_mult_eighths = mult_eighths,
.clkout0_divide = clkout0_divide,
};
}
// === Frequency arithmetic ===
// F_VCO = F_IN * M / D, in kHz; caller must keep divclk_divide >= 1
fn vco_khz(cfg: MmcmConfig) -> u32 {
if cfg.divclk_divide == 0 {
return 0;
}
return cfg.fin_mhz * 1000 * cfg.clkfbout_mult_eighths / (8 * cfg.divclk_divide);
}
fn vco_mhz(cfg: MmcmConfig) -> u32 {
return vco_khz(cfg) / 1000;
}
// F_OUT = F_VCO / O, in kHz; caller must keep clkout0_divide >= 1
fn clkout_khz(cfg: MmcmConfig) -> u32 {
if cfg.clkout0_divide == 0 {
return 0;
}
return vco_khz(cfg) / cfg.clkout0_divide;
}
fn clkout_mhz(cfg: MmcmConfig) -> u32 {
return clkout_khz(cfg) / 1000;
}
fn is_vco_in_range(cfg: MmcmConfig) -> bool {
var v : u32 = vco_khz(cfg);
return v >= VCO_MIN_KHZ and v <= VCO_MAX_KHZ;
}
// VCO period in femtoseconds, so a 1600 MHz VCO keeps 625000 fs of it
fn vco_period_fs(cfg: MmcmConfig) -> i64 {
var v : u32 = vco_khz(cfg);
if v == 0 {
return 0;
}
return 1000000000000 / (v as i64);
}
// Fine phase step: 1/56 of the VCO period (UG472). More VCO MHz means a
// finer phase knob, which is why a high VCO is wanted for phase work.
fn phase_step_fs(cfg: MmcmConfig) -> i64 {
return vco_period_fs(cfg) / (PHASE_GRAN_DIVISOR as i64);
}
// === Lock model ===
pub struct LockState {
lock_counter_us : u32,
locked : bool,
}
fn unlocked_state() -> LockState {
return LockState{ .lock_counter_us = 0, .locked = false };
}
// One microsecond of settling with a legal, unchanging configuration
fn lock_step(s: LockState) -> LockState {
if s.locked {
return s;
}
if s.lock_counter_us + 1 >= LOCK_WINDOW_US {
return LockState{ .lock_counter_us = LOCK_WINDOW_US, .locked = true };
}
return LockState{ .lock_counter_us = s.lock_counter_us + 1, .locked = false };
}
// A reconfiguration drops lock immediately and restarts the window
fn relock(s: LockState) -> LockState {
return unlocked_state();
}
// Settle the model for a given number of microseconds without stepping
// one microsecond at a time: lock arrives exactly at the window edge
fn lock_after(us: u32) -> LockState {
if us >= LOCK_WINDOW_US {
return LockState{ .lock_counter_us = LOCK_WINDOW_US, .locked = true };
}
return LockState{ .lock_counter_us = us, .locked = false };
}
// === Validation ===
fn validate_mmcm(cfg: MmcmConfig) -> u32 {
var errors : u32 = 0;
if cfg.name == "" {
errors = errors + 1;
}
if cfg.divclk_divide < DIVCLK_MIN or cfg.divclk_divide > DIVCLK_MAX {
errors = errors + 1;
}
if cfg.clkfbout_mult_eighths < MULT_MIN_EIGHTHS or cfg.clkfbout_mult_eighths > MULT_MAX_EIGHTHS {
errors = errors + 1;
}
if cfg.clkout0_divide < CLKOUT_DIV_MIN or cfg.clkout0_divide > CLKOUT_DIV_MAX {
errors = errors + 1;
}
if cfg.fin_mhz < FIN_MIN_MHZ or cfg.fin_mhz > FIN_MAX_MHZ {
errors = errors + 1;
}
// Only judge the VCO once the dividers can form a frequency at all
if cfg.divclk_divide >= DIVCLK_MIN and is_vco_in_range(cfg) == false {
errors = errors + 1;
}
return errors;
}
// === Tests ===
test vco_multiplies_the_input
given cfg = mmcm("sys_mmcm", 200, 1, 40, 10)
then vco_khz(cfg) == 1000000
and vco_mhz(cfg) == 1000
test clkout_divides_the_vco
given cfg = mmcm("sys_mmcm", 200, 1, 40, 10)
then clkout_khz(cfg) == 100000
and clkout_mhz(cfg) == 100
test divclk_divides_first
given cfg = mmcm("half_in", 200, 2, 40, 10)
then vco_khz(cfg) == 500000
and vco_mhz(cfg) == 500
test mult_moves_in_eighth_steps
given cfg = mmcm("fractional", 100, 1, 65, 1)
then vco_khz(cfg) == 812500
and vco_mhz(cfg) == 812
test vco_below_the_window_is_rejected
given cfg = mmcm("too_low", 100, 1, 40, 5)
then vco_mhz(cfg) == 500
and is_vco_in_range(cfg) == false
test vco_above_the_window_is_rejected
given cfg = mmcm("too_high", 200, 1, 512, 8)
then vco_mhz(cfg) == 12800
and is_vco_in_range(cfg) == false
test a_legal_100mhz_request_passes
given cfg = mmcm("sys_mmcm", 200, 1, 40, 10)
then validate_mmcm(cfg) == 0
and is_vco_in_range(cfg) == true
test vco_out_of_range_is_an_error
given cfg = mmcm("bad_vco", 100, 1, 40, 5)
then validate_mmcm(cfg) > 0
test zero_divclk_is_an_error
given cfg = mmcm("zero_d", 200, 0, 40, 10)
then validate_mmcm(cfg) > 0
and vco_khz(cfg) == 0
test mult_above_64_is_an_error
given cfg = mmcm("big_m", 200, 1, 520, 10)
then validate_mmcm(cfg) > 0
test input_above_the_window_is_an_error
given cfg = mmcm("fast_in", 900, 1, 40, 10)
then validate_mmcm(cfg) > 0
test phase_step_is_one_fifty_sixth_of_the_vco
given cfg = mmcm("sys_mmcm", 200, 1, 40, 10)
then vco_period_fs(cfg) == 1000000
and phase_step_fs(cfg) == 17857
test a_faster_vco_gives_a_finer_phase_step
given slow = mmcm("slow", 200, 1, 24, 3)
and fast = mmcm("fast", 200, 1, 48, 6)
then vco_mhz(slow) == 600
and vco_mhz(fast) == 1200
and phase_step_fs(fast) < phase_step_fs(slow)
test lock_arrives_within_the_window
then lock_after(LOCK_WINDOW_US).locked == true
test lock_does_not_arrive_early
then lock_after(499).locked == false
test relock_drops_the_lock
given s = lock_after(LOCK_WINDOW_US)
and r = relock(s)
then s.locked == true
and r.locked == false
and r.lock_counter_us == 0
// === Invariants ===
invariant a_legal_vco_is_positive
given cfg = mmcm("inv", 200, 1, 40, 10)
assert vco_khz(cfg) > 0
invariant the_output_is_below_the_vco
given cfg = mmcm("inv", 200, 1, 40, 10)
assert clkout_khz(cfg) <= vco_khz(cfg)
invariant the_phase_step_is_never_negative
given cfg = mmcm("inv", 200, 1, 40, 10)
assert phase_step_fs(cfg) > 0
invariant lock_needs_the_whole_window
given s = lock_after(499)
assert s.locked == false
// === Benchmarks ===
bench vco_arithmetic
measure: nanoseconds for vco_khz(mmcm("b", 200, 1, 40, 10))
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.