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}