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(®s));
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(®s));
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(®s));
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(®s));
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(®s));
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(®s));
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(®s, 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(®s, 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(®s, 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(®s, 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(®s, ®s, 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}