pico_pll_config/lib.rs
1//! A procedural macro for generating PLL configuration parameters.
2//!
3//! The implementation is based on the Python script provided in the RP2040 SDK:
4//! $PICO_SDK/src/rp2_common/hardware_clocks/scripts/vcocalc.py
5//!
6//! The macro takes a frequency (in kHz) as a literal and expands to an expression
7//! of type `Option<PLLConfig>`.
8//!
9//! The algorithm searches over an expanded parameter space (REFDIV, FBDIV, PD1, and PD2)
10//! using hard-coded defaults (e.g. a 12 MHz input, minimum reference frequency 5 MHz,
11//! VCO limits between 750 and 1600 MHz) and selects the configuration with the smallest
12//! error relative to the requested output frequency (converted from kHz to MHz).
13
14use proc_macro::TokenStream;
15use quote::quote;
16use syn::{parse_macro_input, LitInt};
17
18// The defaultts are:
19// * 12 MHz input,
20// * 5 MHz minimum reference frequency,
21// * VCO between 750 and 1600 MHz,
22// * no locked REFDIV,
23// * and default tie-break aka prefer the higher VCO.
24
25const XOSC_MHZ: f64 = 12.0;
26const REF_MIN: f64 = 5.0;
27const VCO_MIN: f64 = 750.0;
28const VCO_MAX: f64 = 1600.0;
29const LOW_VCO: bool = false;
30const LOCKED_REFDIV: Option<u8> = None;
31
32mod pll {
33 /// A simple newtype wrapper for a frequency in Hertz just to make it distinct from other values.
34 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
35 pub struct HertzU32(pub u32);
36
37 /// Extended PLL configuration parameters.
38 #[derive(Debug, PartialEq)]
39 pub struct PLLConfigExtended {
40 /// Voltage Controlled Oscillator frequency (in Hz).
41 pub vco_freq: HertzU32,
42 /// Reference divider.
43 pub refdiv: u8,
44 /// Feedback divider.
45 pub fbdiv: u16,
46 /// Post Divider 1.
47 pub post_div1: u8,
48 /// Post Divider 2.
49 pub post_div2: u8,
50 /// Achieved system clock (output) frequency in MHz.
51 pub sys_clk_mhz: f64,
52 }
53
54 /// Finds a PLL configuration by searching over an expanded parameter space.
55 /// All frequencies here are in MHz except for the returned VCO frequency (in Hz).
56 ///
57 /// * `input_mhz` - The input (reference) oscillator frequency (e.g. 12.0).
58 /// * `requested_mhz` - The desired output frequency (e.g. 480.0).
59 /// * `vco_min` - Minimum allowed VCO frequency (e.g. 750.0).
60 /// * `vco_max` - Maximum allowed VCO frequency (e.g. 1600.0).
61 /// * `ref_min` - Minimum allowed reference frequency (e.g. 5.0).
62 /// * `locked_refdiv` - If Some(n), restricts the search to REFDIV == n.
63 /// * `low_vco` - If true, among equally good solutions prefer the one with a lower VCO frequency;
64 /// otherwise, prefer higher VCO.
65 pub fn find_pll_config_extended(
66 input_mhz: f64,
67 requested_mhz: f64,
68 vco_min: f64,
69 vco_max: f64,
70 ref_min: f64,
71 locked_refdiv: Option<u8>,
72 low_vco: bool,
73 ) -> Option<PLLConfigExtended> {
74 // Fixed ranges (as in the Python script)
75 let fbdiv_range = 16..=320; // valid FBDIV values
76 let postdiv_range = 1..=7; // valid post divider values
77
78 // Allowed REFDIV values:
79 // refdiv_min is fixed at 1; refdiv_max is defined here as 63.
80 let refdiv_min: u8 = 1;
81 let refdiv_max: u8 = 63;
82 // Compute maximum allowed REFDIV based on the input frequency and minimum reference frequency.
83 // (In the Python script: int(input / ref_min) is used.)
84 let max_possible = ((input_mhz / ref_min).floor() as u8).min(refdiv_max);
85 let max_refdiv = if max_possible < refdiv_min {
86 refdiv_min
87 } else {
88 max_possible
89 };
90
91 // If locked, use just that value; otherwise, iterate from refdiv_min up through max_refdiv.
92 let refdiv_iter: Box<dyn Iterator<Item = u8>> = if let Some(lock) = locked_refdiv {
93 Box::new(std::iter::once(lock))
94 } else {
95 Box::new(refdiv_min..=max_refdiv)
96 };
97
98 // We'll track the best candidate as a tuple:
99 // (achieved_out (MHz), fbdiv, pd1, pd2, refdiv, vco (MHz))
100 let mut best: Option<(f64, u16, u8, u8, u8, f64)> = None;
101 // Start with a relatively large error margin (here we use the requested frequency itself).
102 let mut best_margin = requested_mhz;
103
104 for refdiv in refdiv_iter {
105 for fbdiv in fbdiv_range.clone() {
106 // Compute VCO in MHz: vco = (input_mhz / refdiv) * fbdiv.
107 let vco = (input_mhz / (refdiv as f64)) * (fbdiv as f64);
108 if vco < vco_min || vco > vco_max {
109 continue;
110 }
111 // Loop over post divider combinations.
112 for pd2 in postdiv_range.clone() {
113 for pd1 in postdiv_range.clone() {
114 let divider = (pd1 * pd2) as f64;
115 // Check that the VCO (scaled to kHz) divides exactly by the divider.
116 // (This ensures that the achieved output frequency is an integer value when computed in kHz.)
117 if (vco * 1000.0) % divider != 0.0 {
118 continue;
119 }
120 // Compute output frequency in MHz.
121 let out = vco / divider;
122 let margin = (out - requested_mhz).abs();
123
124 // Determine whether this candidate is “better.”
125 // In case of equal margin (within 1e-9) we compare the VCO frequency.
126 let update = if let Some((_, _, _, _, _, best_vco)) = best {
127 (margin < best_margin)
128 || ((margin - best_margin).abs() < 1e-9
129 && (if low_vco {
130 vco < best_vco
131 } else {
132 vco > best_vco
133 }))
134 } else {
135 true
136 };
137
138 if update {
139 best_margin = margin;
140 best = Some((out, fbdiv, pd1, pd2, refdiv, vco));
141 }
142 }
143 }
144 }
145 }
146
147 best.map(|(out, fbdiv, pd1, pd2, refdiv, vco)| {
148 // Compute VCO frequency in Hz.
149 let vco_hz = (vco * 1_000_000.0).round() as u32;
150 PLLConfigExtended {
151 vco_freq: HertzU32(vco_hz),
152 refdiv,
153 fbdiv,
154 post_div1: pd1,
155 post_div2: pd2,
156 sys_clk_mhz: out,
157 }
158 })
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::pll::find_pll_config_extended;
166
167 struct TestCase {
168 requested_mhz: f64,
169 achieved_mhz: f64,
170 expected_refdiv: u8,
171 expected_fbdiv: u16,
172 expected_pd1: u8,
173 expected_pd2: u8,
174 expected_vco: f64, // in MHz
175 }
176
177 #[test]
178 fn test_pll_config_extended() {
179 let test_cases = [
180 // Requested: 480.0 MHz -> Expected: REFDIV=1, FBDIV=120, PD1=3, PD2=1, VCO=1440.0 MHz.
181 TestCase {
182 requested_mhz: 480.0,
183 achieved_mhz: 480.0,
184 expected_refdiv: 1,
185 expected_fbdiv: 120,
186 expected_pd1: 3,
187 expected_pd2: 1,
188 expected_vco: 1440.0,
189 },
190 // Requested: 250.0 MHz -> Expected: REFDIV=1, FBDIV=125, PD1=6, PD2=1, VCO=1500.0 MHz.
191 TestCase {
192 requested_mhz: 250.0,
193 achieved_mhz: 250.0,
194 expected_refdiv: 1,
195 expected_fbdiv: 125,
196 expected_pd1: 6,
197 expected_pd2: 1,
198 expected_vco: 1500.0,
199 },
200 // Requested: 176.0 MHz -> Expected: REFDIV=1, FBDIV=132, PD1=3, PD2=3, VCO=1584.0 MHz.
201 TestCase {
202 requested_mhz: 176.0,
203 achieved_mhz: 176.0,
204 expected_refdiv: 1,
205 expected_fbdiv: 132,
206 expected_pd1: 3,
207 expected_pd2: 3,
208 expected_vco: 1584.0,
209 },
210 // Requested: 130.0 MHz -> Expected: REFDIV=1, FBDIV=130, PD1=6, PD2=2, VCO=1560.0 MHz.
211 TestCase {
212 requested_mhz: 130.0,
213 achieved_mhz: 130.0,
214 expected_refdiv: 1,
215 expected_fbdiv: 130,
216 expected_pd1: 6,
217 expected_pd2: 2,
218 expected_vco: 1560.0,
219 },
220 // Requested: 32.0 MHz -> Expected: REFDIV=1, FBDIV=112, PD1=7, PD2=6, VCO=1344.0 MHz.
221 TestCase {
222 requested_mhz: 32.0,
223 achieved_mhz: 32.0,
224 expected_refdiv: 1,
225 expected_fbdiv: 112,
226 expected_pd1: 7,
227 expected_pd2: 6,
228 expected_vco: 1344.0,
229 },
230 // Requested: 20.0 MHz -> Expected: REFDIV=1, FBDIV=70, PD1=7, PD2=6, VCO=840.0 MHz.
231 TestCase {
232 requested_mhz: 20.0,
233 achieved_mhz: 20.0,
234 expected_refdiv: 1,
235 expected_fbdiv: 70,
236 expected_pd1: 7,
237 expected_pd2: 6,
238 expected_vco: 840.0,
239 },
240 // Requested: 125.0 MHz -> Expected: REFDIV=1, FBDIV=125, PD1=6, PD2=2, VCO=1500.0 MHz.
241 TestCase {
242 requested_mhz: 125.0,
243 achieved_mhz: 125.0,
244 expected_refdiv: 1,
245 expected_fbdiv: 125,
246 expected_pd1: 6,
247 expected_pd2: 2,
248 expected_vco: 1500.0,
249 },
250 // Requested: 48.0 MHz -> Expected: REFDIV=1, FBDIV=120, PD1=6, PD2=5, VCO=1440.0 MHz.
251 TestCase {
252 requested_mhz: 48.0,
253 achieved_mhz: 48.0,
254 expected_refdiv: 1,
255 expected_fbdiv: 120,
256 expected_pd1: 6,
257 expected_pd2: 5,
258 expected_vco: 1440.0,
259 },
260 // Requested: 125.0 MHz -> Expected: REFDIV=1, FBDIV=125, PD1=6, PD2=2, VCO=1500.0 MHz.
261 TestCase {
262 requested_mhz: 125.0,
263 achieved_mhz: 125.0,
264 expected_refdiv: 1,
265 expected_fbdiv: 125,
266 expected_pd1: 6,
267 expected_pd2: 2,
268 expected_vco: 1500.0,
269 },
270 ];
271
272 for tc in &test_cases {
273 let config = find_pll_config_extended(
274 XOSC_MHZ,
275 tc.requested_mhz,
276 VCO_MIN,
277 VCO_MAX,
278 REF_MIN,
279 LOCKED_REFDIV,
280 LOW_VCO,
281 )
282 .unwrap_or_else(|| {
283 panic!("No PLL config found for requested {} MHz", tc.requested_mhz)
284 });
285
286 // Recompute the achieved output frequency:
287 // output = VCO / (pd1 * pd2)
288 let achieved = (tc.expected_vco) / (config.post_div1 as f64 * config.post_div2 as f64);
289 assert!(
290 (achieved - tc.achieved_mhz).abs() < 1e-6,
291 "Achieved frequency mismatch for {} MHz requested: got {} MHz, expected {} MHz",
292 tc.requested_mhz,
293 achieved,
294 tc.achieved_mhz
295 );
296 // Check REFDIV, FBDIV, and post dividers.
297 assert_eq!(
298 config.refdiv, tc.expected_refdiv,
299 "REFDIV mismatch for {} MHz requested",
300 tc.requested_mhz
301 );
302 assert_eq!(
303 config.fbdiv, tc.expected_fbdiv,
304 "FBDIV mismatch for {} MHz requested",
305 tc.requested_mhz
306 );
307 assert_eq!(
308 config.post_div1, tc.expected_pd1,
309 "PD1 mismatch for {} MHz requested",
310 tc.requested_mhz
311 );
312 assert_eq!(
313 config.post_div2, tc.expected_pd2,
314 "PD2 mismatch for {} MHz requested",
315 tc.requested_mhz
316 );
317
318 // Also check that the computed VCO equals the expected value.
319 let computed_vco = XOSC_MHZ / (config.refdiv as f64) * (config.fbdiv as f64);
320 assert!(
321 (computed_vco - tc.expected_vco).abs() < 1e-6,
322 "VCO mismatch for {} MHz requested: got {} MHz, expected {} MHz",
323 tc.requested_mhz,
324 computed_vco,
325 tc.expected_vco
326 );
327 }
328 }
329}
330
331/// The `pll_config` proc macro takes a frequency in kilohertz as a literal and
332/// expands to an expression of type `Option<PLLConfig>`.
333///
334/// # Example
335///
336/// ```rust
337/// use pico_pll_config::pll_config;
338/// use rp2040_hal::pll::PLLConfig;
339///
340/// // 480000 represents 480 MHz (i.e. 480000 kHz)
341/// let config = pll_config!(480000);
342/// const CONFIG: PLLConfig = pll_config!(480000).unwrap();
343/// ```
344#[proc_macro]
345pub fn pll_config(input: TokenStream) -> TokenStream {
346 // Parse the input as an integer literal.
347 let input_lit = parse_macro_input!(input as LitInt);
348 let freq_khz: u64 = input_lit.base10_parse().expect("Invalid integer literal");
349
350 let requested_mhz = freq_khz as f64 / 1000.0;
351 let result = pll::find_pll_config_extended(
352 XOSC_MHZ,
353 requested_mhz,
354 VCO_MIN,
355 VCO_MAX,
356 REF_MIN,
357 LOCKED_REFDIV,
358 LOW_VCO,
359 );
360
361 let expanded = if let Some(ref config) = result {
362 let vco_mhz = config.vco_freq.0 / 1_000_000;
363 let refdiv = config.refdiv;
364 let post_div1 = config.post_div1;
365 let post_div2 = config.post_div2;
366 quote! {
367 Some(rp2040_hal::pll::PLLConfig {
368 vco_freq: fugit::HertzU32::MHz(#vco_mhz),
369 refdiv: #refdiv,
370 post_div1: #post_div1,
371 post_div2: #post_div2,
372 })
373 }
374 } else {
375 quote! { None }
376 };
377
378 TokenStream::from(expanded)
379}