Skip to main content

picoem_common/
clocks.rs

1//! Clock tree primitives — ROSC/XOSC reference frequencies, PLL output
2//! math, and the cached [`ClockTree`] result.
3//!
4//! Derived clock frequencies are recomputed eagerly whenever a
5//! clock-relevant register (CLOCKS, PLL_SYS, PLL_USB) is written. The
6//! cache lives on each chip's `Bus` and is read by the Pacer.
7//!
8//! See `wrk_docs/2026.04.14 - LLD - Clock Tree Model V2.md` §4 for
9//! the full design. Phase A covered the CLOCKS side (ROSC / XOSC
10//! sources and the `CLK_SYS_DIV` divider). Phase B adds real PLL
11//! output computation from the PLL_SYS / PLL_USB register arrays.
12
13/// ROSC nominal frequency (~6.5 MHz). The RP2350 boots on ROSC;
14/// PLL configuration (if any) happens later in firmware.
15pub const ROSC_FREQ_HZ: u32 = 6_500_000;
16
17/// XOSC nominal frequency (12 MHz). Standard Pico SDK configuration.
18pub const XOSC_FREQ_HZ: u32 = 12_000_000;
19
20/// RP2350 nominal post-bootrom system clock (150 MHz). Used by the
21/// emulator to seed `clk_sys` at reset so firmware running via
22/// `load_image` (bypassing the bootrom) sees the same clock state it
23/// would on real silicon after `runtime_init_clocks`. HLD V5 §5.7.
24pub const RP2350_SYS_CLK_HZ: u32 = 150_000_000;
25
26/// RP2350 nominal post-bootrom ADC clock (48 MHz — USB PLL / 10).
27pub const RP2350_ADC_CLK_HZ: u32 = 48_000_000;
28
29/// Derived clock tree frequencies. Recomputed eagerly whenever any
30/// clock-relevant register (CLOCKS, PLL_SYS, PLL_USB) changes.
31#[derive(Debug, Clone, Copy)]
32pub struct ClockTree {
33    /// Effective system clock in Hz. Drives the Pacer.
34    pub sys_clk_hz: u32,
35    /// Effective reference clock in Hz.
36    pub ref_clk_hz: u32,
37    /// Effective peripheral clock in Hz (UART / SPI / I2C source).
38    /// Follows the selected `CLK_PERI_CTRL.AUXSRC` on RP2040; falls back
39    /// to `sys_clk_hz` when the peripheral clock is not separately
40    /// programmed (the pico-sdk default).
41    pub peri_clk_hz: u32,
42}
43
44impl Default for ClockTree {
45    fn default() -> Self {
46        Self {
47            sys_clk_hz: ROSC_FREQ_HZ,
48            ref_clk_hz: ROSC_FREQ_HZ,
49            peri_clk_hz: ROSC_FREQ_HZ,
50        }
51    }
52}
53
54impl ClockTree {
55    /// Current peripheral-clock frequency in Hz. UART baud-rate
56    /// divisors, SPI bit-rate prescalers, and I2C SCL generators all
57    /// derive their cadence from this frequency.
58    #[inline]
59    pub fn peri_hz(&self) -> u64 {
60        self.peri_clk_hz as u64
61    }
62}
63
64/// Compute a PLL's output frequency in Hz from its four-register image.
65///
66/// `regs[0]` is CS (REFDIV in `[5:0]`), `regs[2]` is FBDIV_INT
67/// (FBDIV in `[11:0]`), `regs[3]` is PRIM (POSTDIV1 in `[18:16]`,
68/// POSTDIV2 in `[14:12]`). PWR (`regs[1]`) is accepted but unused —
69/// see LLD V2 §3 note on PLL power-gating fidelity.
70///
71/// Returns **0** when `FBDIV == 0` (unconfigured PLL), rather than a
72/// `.max(1)` hack that would silently turn an unconfigured PLL into a
73/// ~244 kHz signal. The Pacer guards against 0 Hz (Phase C).
74///
75/// Uses u64 intermediates to avoid `u32` overflow: with REFDIV=1 and
76/// FBDIV=4095, `XOSC * FBDIV = 49_140_000_000` — well outside u32.
77/// The final result is clamped defensively to `u32::MAX`.
78pub fn pll_output_hz(regs: &[u32; 4]) -> u32 {
79    let fbdiv = (regs[2] & 0xFFF) as u64;
80    if fbdiv == 0 {
81        return 0;
82    }
83    let refdiv = ((regs[0] & 0x3F).max(1)) as u64;
84    let postdiv1 = (((regs[3] >> 16) & 0x7).max(1)) as u64;
85    let postdiv2 = (((regs[3] >> 12) & 0x7).max(1)) as u64;
86
87    let vco_hz = (XOSC_FREQ_HZ as u64 / refdiv) * fbdiv;
88    let out_hz_64 = vco_hz / (postdiv1 * postdiv2);
89    out_hz_64.min(u32::MAX as u64) as u32
90}
91
92// --- PLL LOCK modelling (shared between RP2350 / RP2040) --------------------
93//
94// See `wrk_docs/2026.04.15 - HLD - PLL LOCK Modelling.md` for the full design.
95// The bug being fixed: both chip crates unconditionally forced CS[31] (LOCK)
96// high on every PLL CS read, regardless of power state, FBDIV configuration,
97// or elapsed settle time. The silicon oracle
98// (`silicon_periph_diff_rp2350 pll_sys_lock_timing`) catches this at ~1133
99// sysclks after `PWR=0`, where real hardware still reports LOCK=0.
100//
101// Fix shape: three pure functions here (predicate, CS read, write-time
102// transition), plus per-chip `Option<u64> pll_*_lock_at_cycle` storage and a
103// `master_cycle` stash on each `Bus` populated at step entry. The
104// `pll_should_arm_lock` helper implements **Option B** — rearm on
105// FBDIV/REFDIV change while still powered — closing the fidelity gap called
106// out in the HLD §3 versus Option C.
107
108/// PLL lock-detect delay, in sysclks.
109///
110/// Tuned to be strictly greater than what
111/// `silicon_periph_diff_rp2350 pll_sys_lock_timing` observes on real RP2354
112/// silicon (1133 sysclks at ROSC boot clock). Not a fit to the datasheet's
113/// LOCK_DETECT_COUNTER field — that would require a richer PWR-cycle model.
114/// See `wrk_docs/2026.04.15 - HLD - PLL LOCK Modelling.md` §4.
115pub const PLL_LOCK_DELAY_SYSCLKS: u64 = 2_000;
116
117/// True iff the PLL's current register image has the VCO powered up and a
118/// non-zero feedback divider — i.e. the lock-detect counter would be running
119/// on real silicon.
120///
121/// Inspects PWR[0] (`PD`), PWR[5] (`VCOPD`), and FBDIV[11:0]. BYPASS (CS[8])
122/// is intentionally *not* inspected: the conservative interpretation is that
123/// bypass routes the reference clock around the VCO but does not itself
124/// assert LOCK (see HLD §2 / §9).
125pub fn pll_is_locked_base(regs: &[u32; 4]) -> bool {
126    let pwr = regs[1];
127    let fbdiv = regs[2] & 0xFFF;
128    let pd = (pwr & 0x01) != 0;
129    let vcopd = (pwr & 0x20) != 0;
130    fbdiv != 0 && !pd && !vcopd
131}
132
133/// Read CS with the correct LOCK bit (CS[31]) given the current lock-arm
134/// state and master cycle count.
135///
136/// LOCK reads 1 iff [`pll_is_locked_base`] is true AND the arm point has
137/// elapsed. `lock_at == None` always yields LOCK=0, regardless of the
138/// predicate — the arm has not yet been scheduled.
139pub fn pll_cs_read_with_lock(regs: &[u32; 4], lock_at: Option<u64>, now: u64) -> u32 {
140    let locked_time = matches!(lock_at, Some(arm) if now >= arm);
141    let locked = pll_is_locked_base(regs) && locked_time;
142    if locked {
143        regs[0] | (1 << 31)
144    } else {
145        regs[0] & !(1 << 31)
146    }
147}
148
149/// Compute the new `lock_at_cycle` value after a PLL register write.
150///
151/// Call this **after** the underlying `regs` have been updated with the
152/// write (alias-applied, etc.). Returns the `Option<u64>` that should be
153/// stored back into the chip's `pll_*_lock_at_cycle` field.
154///
155/// Implements HLD §4 "Option B" semantics:
156///
157/// - If the new state is not powered/configured → `None` (drop any arm).
158/// - Else if previously un-armed → `Some(now + PLL_LOCK_DELAY_SYSCLKS)` (fresh arm).
159/// - Else if REFDIV (CS[5:0]) or FBDIV (FBDIV_INT[11:0]) changed while still
160///   powered → `Some(now + PLL_LOCK_DELAY_SYSCLKS)` (re-arm per silicon).
161/// - Else → `prev_lock_at` unchanged.
162///
163/// PRIM changes (POSTDIV1/POSTDIV2) deliberately do **not** rearm: those
164/// are post-VCO and have no effect on the lock-detect counter per silicon
165/// behaviour.
166pub fn pll_should_arm_lock(
167    old_regs: &[u32; 4],
168    new_regs: &[u32; 4],
169    prev_lock_at: Option<u64>,
170    now: u64,
171) -> Option<u64> {
172    if !pll_is_locked_base(new_regs) {
173        None
174    } else if prev_lock_at.is_none() {
175        Some(now + PLL_LOCK_DELAY_SYSCLKS)
176    } else if (old_regs[0] & 0x3F) != (new_regs[0] & 0x3F) {
177        // REFDIV changed while still powered → re-arm.
178        Some(now + PLL_LOCK_DELAY_SYSCLKS)
179    } else if (old_regs[2] & 0xFFF) != (new_regs[2] & 0xFFF) {
180        // FBDIV changed while still powered → re-arm.
181        Some(now + PLL_LOCK_DELAY_SYSCLKS)
182    } else {
183        prev_lock_at
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    // --- pll_is_locked_base -------------------------------------------------
192
193    #[test]
194    fn pll_is_locked_base_false_when_pd_set() {
195        // PWR=0x01 (PD only), FBDIV=100 → predicate false despite FBDIV.
196        let regs: [u32; 4] = [0x01, 0x01, 100, 0];
197        assert!(!pll_is_locked_base(&regs));
198    }
199
200    #[test]
201    fn pll_is_locked_base_false_when_vcopd_set() {
202        // PWR=0x20 (VCOPD only), FBDIV=100 → predicate false.
203        let regs: [u32; 4] = [0x01, 0x20, 100, 0];
204        assert!(!pll_is_locked_base(&regs));
205    }
206
207    #[test]
208    fn pll_is_locked_base_false_when_fbdiv_zero() {
209        // PWR=0 (everything powered), FBDIV=0 → predicate false (unconfigured).
210        let regs: [u32; 4] = [0x01, 0, 0, 0];
211        assert!(!pll_is_locked_base(&regs));
212    }
213
214    #[test]
215    fn pll_is_locked_base_true_when_powered_and_configured() {
216        // PWR=0, FBDIV=100 → predicate true.
217        let regs: [u32; 4] = [0x01, 0, 100, 0];
218        assert!(pll_is_locked_base(&regs));
219    }
220
221    #[test]
222    fn pll_is_locked_base_false_at_reset_defaults() {
223        // Reset image: PWR=0x2D (PD+DSMPD+POSTDIVPD+VCOPD all set), FBDIV=0.
224        let regs: [u32; 4] = [0x0000_0001, 0x0000_002D, 0, 0x0007_7000];
225        assert!(!pll_is_locked_base(&regs));
226    }
227
228    #[test]
229    fn pll_is_locked_base_ignores_bypass() {
230        // CS[8]=1 (BYPASS) but PWR=0 and FBDIV=100 → still true; BYPASS is
231        // orthogonal to the lock-detect predicate (see HLD §9).
232        let regs: [u32; 4] = [0x101, 0, 100, 0];
233        assert!(pll_is_locked_base(&regs));
234    }
235
236    // --- pll_cs_read_with_lock ---------------------------------------------
237
238    #[test]
239    fn pll_cs_read_returns_lock_set_when_locked_and_past_arm() {
240        let regs: [u32; 4] = [0x01, 0, 100, 0];
241        let cs = pll_cs_read_with_lock(&regs, Some(10), 1_000);
242        assert_eq!(cs & (1 << 31), 1 << 31);
243        assert_eq!(cs & 0x3F, 0x01, "REFDIV bits preserved under LOCK merge");
244    }
245
246    #[test]
247    fn pll_cs_read_returns_lock_clear_before_arm() {
248        let regs: [u32; 4] = [0x01, 0, 100, 0];
249        let cs = pll_cs_read_with_lock(&regs, Some(5_000), 10);
250        assert_eq!(cs & (1 << 31), 0, "LOCK must be 0 before arm cycle");
251    }
252
253    #[test]
254    fn pll_cs_read_returns_lock_clear_when_base_false() {
255        // Powered-down PLL: even with a past arm, LOCK must read 0.
256        let regs: [u32; 4] = [0x01, 0x2D, 100, 0];
257        let cs = pll_cs_read_with_lock(&regs, Some(10), 10_000);
258        assert_eq!(cs & (1 << 31), 0);
259    }
260
261    #[test]
262    fn pll_cs_read_returns_lock_clear_when_arm_none() {
263        let regs: [u32; 4] = [0x01, 0, 100, 0];
264        let cs = pll_cs_read_with_lock(&regs, None, 10_000);
265        assert_eq!(cs & (1 << 31), 0, "None arm → LOCK=0 regardless of now");
266    }
267
268    // --- pll_should_arm_lock ------------------------------------------------
269
270    #[test]
271    fn pll_should_arm_lock_drops_when_powered_down() {
272        // Start with PWR=0, transition to PWR=0x2D → predicate flips false,
273        // lock should be dropped.
274        let old: [u32; 4] = [0x01, 0, 100, 0];
275        let new: [u32; 4] = [0x01, 0x2D, 100, 0];
276        let prev = Some(2_000);
277        let now = 500;
278        assert_eq!(pll_should_arm_lock(&old, &new, prev, now), None);
279    }
280
281    #[test]
282    fn pll_should_arm_lock_arms_from_powered_down() {
283        // Previously un-armed (PWR=0x2D reset), transition to PWR=0 + FBDIV=100
284        // → arm at now + delay.
285        let old: [u32; 4] = [0x01, 0x2D, 0, 0];
286        let new: [u32; 4] = [0x01, 0, 100, 0];
287        let now = 100;
288        assert_eq!(
289            pll_should_arm_lock(&old, &new, None, now),
290            Some(100 + PLL_LOCK_DELAY_SYSCLKS)
291        );
292    }
293
294    #[test]
295    fn pll_should_arm_lock_rearms_on_refdiv_change() {
296        // Already powered+armed, change REFDIV (CS[5:0]) → rearm.
297        let old: [u32; 4] = [0x01, 0, 100, 0];
298        let new: [u32; 4] = [0x05, 0, 100, 0];
299        let prev = Some(500);
300        let now = 10_000;
301        assert_eq!(
302            pll_should_arm_lock(&old, &new, prev, now),
303            Some(10_000 + PLL_LOCK_DELAY_SYSCLKS)
304        );
305    }
306
307    #[test]
308    fn pll_should_arm_lock_rearms_on_fbdiv_change() {
309        // Already powered+armed, change FBDIV (FBDIV_INT[11:0]) → rearm.
310        let old: [u32; 4] = [0x01, 0, 100, 0];
311        let new: [u32; 4] = [0x01, 0, 125, 0];
312        let prev = Some(500);
313        let now = 10_000;
314        assert_eq!(
315            pll_should_arm_lock(&old, &new, prev, now),
316            Some(10_000 + PLL_LOCK_DELAY_SYSCLKS)
317        );
318    }
319
320    #[test]
321    fn pll_should_arm_lock_keeps_existing_on_prim_change() {
322        // PRIM (POSTDIV1/2) changes are post-VCO → do NOT rearm.
323        let old: [u32; 4] = [0x01, 0, 100, 0x0007_7000];
324        let new: [u32; 4] = [0x01, 0, 100, 0x0002_2000];
325        let prev = Some(500);
326        let now = 10_000;
327        assert_eq!(pll_should_arm_lock(&old, &new, prev, now), Some(500));
328    }
329
330    #[test]
331    fn pll_should_arm_lock_noop_when_nothing_changes() {
332        // Identical before/after → keep prev_lock_at as-is (even None).
333        let regs: [u32; 4] = [0x01, 0, 100, 0];
334        assert_eq!(pll_should_arm_lock(&regs, &regs, Some(42), 1_000), Some(42));
335        // Unconfigured case: predicate is false → always None.
336        let unconf: [u32; 4] = [0x01, 0x2D, 0, 0];
337        assert_eq!(pll_should_arm_lock(&unconf, &unconf, None, 1_000), None);
338    }
339}