Skip to main content

bao1x_api/
clocks.rs

1pub const FREQ_OSC_MHZ: u32 = 48;
2const FD_MAX: u32 = 256;
3
4/// This takes in the FD input frequency (the frequency to be divided) in MHz
5/// and the fd value, and returns the resulting divided frequency.
6pub fn divide_by_fd(fd: u32, in_freq_hz: u32) -> u32 {
7    // shift by 1_000 to prevent overflow
8    let in_freq_khz = in_freq_hz / 1_000;
9    let out_freq_khz = (in_freq_khz * (fd + 1)) / FD_MAX;
10    // restore to Hz
11    out_freq_khz * 1_000
12}
13
14/// Takes in the FD input frequency in MHz, and then the desired frequency.
15/// Returns Some((fd value, deviation in *hz*, not MHz)) if the requirement is satisfiable
16/// Returns None if the equation is ill-formed.
17/// *not tested*
18#[allow(dead_code)]
19pub fn clk_to_fd(fd_in_mhz: u32, desired_mhz: u32) -> Option<(u32, i32)> {
20    let platonic_fd: u32 = (desired_mhz * 256) / fd_in_mhz;
21    if platonic_fd > 0 {
22        let actual_fd = platonic_fd - 1;
23        let actual_clk = divide_by_fd(actual_fd, fd_in_mhz * 1_000_000);
24        Some((actual_fd, desired_mhz as i32 - actual_clk as i32))
25    } else {
26        None
27    }
28}
29
30#[derive(num_derive::FromPrimitive, num_derive::ToPrimitive, Debug)]
31pub enum PowerOp {
32    Wfi,
33    Invalid,
34}
35
36const FDW: u32 = 8;
37const MODULUS: u64 = 1 << FDW; // 256
38
39#[derive(Debug, Clone, Copy)]
40pub struct ClockDividerParams {
41    pub fd0: u8,
42    pub fd2: u8,
43    pub actual_freq_hz: u32,
44    pub error_ppm: u32,
45}
46
47/// Integer division with rounding: round(a / b)
48#[inline]
49pub fn div_round(a: u64, b: u64) -> u64 { (a + b / 2) / b }
50
51/// Find optimal fd0/fd2 using only integer arithmetic.
52///
53/// Equation: f_out = f_base × (fd2 + 1) / ((fd0 + 1) × 256)
54/// Valid range: f_base/65536 ≤ f_target ≤ f_base
55pub fn find_optimal_divider(base_freq_hz: u32, target_freq_hz: u32) -> Option<ClockDividerParams> {
56    let base = base_freq_hz as u64;
57    let target = target_freq_hz as u64;
58
59    if target == 0 || base == 0 {
60        return None;
61    }
62
63    // Check bounds: max_div = 65536, min_div = 1
64    // target must be in [base/65536, base]
65    if target > base || base > target * 65536 {
66        return None;
67    }
68
69    let mut best: Option<ClockDividerParams> = None;
70
71    for fd0 in 0u32..=255 {
72        let fd0_plus_1 = (fd0 as u64) + 1;
73
74        // Solve: target/base = (fd2+1) / ((fd0+1) × 256)
75        //    =>  fd2+1 = target × (fd0+1) × 256 / base
76        let fd2_plus_1 = div_round(target * fd0_plus_1 * MODULUS, base);
77
78        if fd2_plus_1 < 1 || fd2_plus_1 > 256 {
79            continue;
80        }
81
82        let fd2 = (fd2_plus_1 - 1) as u8;
83
84        // actual = base × (fd2+1) / ((fd0+1) × 256)
85        let divisor = fd0_plus_1 * MODULUS;
86        let actual_freq_hz = div_round(base * fd2_plus_1, divisor) as u32;
87
88        // error_ppm = |actual - target| / target × 1_000_000
89        //           = |base×(fd2+1) - target×divisor| × 1_000_000 / (target × divisor)
90        let actual_scaled = base * fd2_plus_1;
91        let target_scaled = target * divisor;
92        let diff = if actual_scaled >= target_scaled {
93            actual_scaled - target_scaled
94        } else {
95            target_scaled - actual_scaled
96        };
97        let error_ppm = div_round(diff * 1_000_000, target_scaled) as u32;
98
99        let is_better = match &best {
100            None => true,
101            Some(b) => error_ppm < b.error_ppm || (error_ppm == b.error_ppm && fd0 < b.fd0 as u32),
102        };
103
104        if is_better {
105            best = Some(ClockDividerParams { fd0: fd0 as u8, fd2, actual_freq_hz, error_ppm });
106        }
107    }
108
109    best
110}
111
112const FREF_HZ: u64 = 48_000_000;
113const VCO_MIN_HZ: u64 = 1_000_000_000;
114const VCO_MAX_HZ: u64 = 3_000_000_000;
115const FRAC_BITS: u32 = 24;
116const FRAC_SCALE: u64 = 1 << FRAC_BITS; // 16777216
117
118#[derive(Debug, Clone, Copy)]
119pub struct PllParams {
120    pub m: u8,     // prediv: 1-4 (for 48 MHz ref)
121    pub n: u16,    // fbdiv: 8-4095 (excluding 11)
122    pub frac: u32, // 0 to 2^24-1
123    pub q0: u8,    // postdiv0: 1-8
124    pub q1: u8,    // postdiv1: 1-8
125    pub vco_freq_hz: u32,
126    pub actual_freq_hz: u32,
127    pub error_ppm: u32,
128}
129
130/// Check if N is valid per spec: 8, 9, 10, 12, 13, ... 4095 (11 excluded)
131#[inline]
132fn is_valid_n(n: u16) -> bool { n >= 8 && n <= 4095 && n != 11 }
133
134/// Calculate VCO frequency: Fvco = Fref × (N + frac/2^24) / M
135/// Returns Hz, or None on overflow
136fn calc_vco_hz(m: u8, n: u16, frac: u32) -> Option<u64> {
137    // Fvco = Fref × (N × 2^24 + frac) / (M × 2^24)
138    let n_plus_f_scaled = (n as u64) * FRAC_SCALE + (frac as u64);
139    let numerator = FREF_HZ.checked_mul(n_plus_f_scaled)?;
140    Some(numerator / ((m as u64) * FRAC_SCALE))
141}
142
143/// Check if VCO frequency is within valid range (1-3 GHz)
144fn is_vco_valid(m: u8, n: u16, frac: u32) -> bool {
145    calc_vco_hz(m, n, frac).map(|vco| vco >= VCO_MIN_HZ && vco <= VCO_MAX_HZ).unwrap_or(false)
146}
147
148const COMMON_CLOCKS: [(u32, PllParams); 7] = [
149    (
150        700_000_000,
151        PllParams {
152            m: 4,
153            n: 175,
154            frac: 0,
155            q0: 1,
156            q1: 3,
157            vco_freq_hz: 2100000000,
158            actual_freq_hz: 700000000,
159            error_ppm: 0,
160        },
161    ),
162    (
163        696_000_000,
164        PllParams {
165            m: 1,
166            n: 29,
167            frac: 0,
168            q0: 1,
169            q1: 2,
170            vco_freq_hz: 1392000000,
171            actual_freq_hz: 696000000,
172            error_ppm: 5714,
173        },
174    ),
175    (
176        400_000_000,
177        PllParams {
178            m: 1,
179            n: 25,
180            frac: 0,
181            q0: 1,
182            q1: 3,
183            vco_freq_hz: 1200000000,
184            actual_freq_hz: 400000000,
185            error_ppm: 0,
186        },
187    ),
188    (
189        350_000_000,
190        PllParams {
191            m: 4,
192            n: 175,
193            frac: 0,
194            q0: 1,
195            q1: 6,
196            vco_freq_hz: 2100000000,
197            actual_freq_hz: 350000000,
198            error_ppm: 0,
199        },
200    ),
201    (
202        200_000_000,
203        PllParams {
204            m: 1,
205            n: 25,
206            frac: 0,
207            q0: 1,
208            q1: 6,
209            vco_freq_hz: 1200000000,
210            actual_freq_hz: 200000000,
211            error_ppm: 0,
212        },
213    ),
214    (
215        100_000_000,
216        PllParams {
217            m: 1,
218            n: 25,
219            frac: 0,
220            q0: 2,
221            q1: 6,
222            vco_freq_hz: 1200000000,
223            actual_freq_hz: 100000000,
224            error_ppm: 0,
225        },
226    ),
227    // overclock
228    (
229        800_000_000,
230        PllParams {
231            m: 3,
232            n: 100,
233            frac: 0,
234            q0: 1,
235            q1: 2,
236            vco_freq_hz: 1600000000,
237            actual_freq_hz: 800000000,
238            error_ppm: 0,
239        },
240    ),
241];
242/// Find optimal PLL parameters for target frequency.
243///
244/// If `allow_frac` is false, only integer solutions (frac=0) are considered.
245/// If `allow_frac` is true, fractional solutions are allowed but integer
246/// solutions are preferred when they achieve the same error.
247pub fn find_pll_params(target_freq_hz: u32, allow_frac: bool) -> Option<PllParams> {
248    let target = target_freq_hz as u64;
249
250    // check a short list of the most common frequencies and just return the value, because
251    // the search space for an optimal solution is pretty big.
252    if let Some(memoized) = COMMON_CLOCKS.iter().find(|(freq, _param)| *freq == target_freq_hz) {
253        return Some(memoized.1);
254    }
255
256    if target == 0 {
257        return None;
258    }
259
260    let mut best: Option<PllParams> = None;
261
262    // TODO: reduce this search space somewhat? need to see if this overhead is acceptable.
263    // alternatively we can use this to pre-compute params that are frequently re-used.
264
265    // M is constrained by PFD frequency: 10 MHz ≤ 48 MHz / M ≤ 100 MHz
266    // With Fref = 48 MHz: M ∈ {1, 2, 3, 4}
267    for m in 1u8..=4 {
268        for q0 in 1u8..=8 {
269            for q1 in 1u8..=8 {
270                let total_div = (m as u64) * (q0 as u64) * (q1 as u64);
271
272                // From: Fout = Fref × (N + F) / (M × Q0 × Q1)
273                // Solve: N + F = Fout × M × Q0 × Q1 / Fref
274                //
275                // In fixed point (scaled by 2^24):
276                // N × 2^24 + frac = target × total_div × 2^24 / Fref
277
278                let n_plus_f_scaled = div_round(target * total_div * FRAC_SCALE, FREF_HZ);
279
280                let n_base = (n_plus_f_scaled / FRAC_SCALE) as u16;
281                let frac_remainder = (n_plus_f_scaled % FRAC_SCALE) as u32;
282
283                // Try integer solution first (frac = 0), then fractional if allowed
284                let candidates: &[(u16, u32)] = if allow_frac {
285                    &[
286                        (n_base, 0),              // Round down, no frac
287                        (n_base + 1, 0),          // Round up, no frac
288                        (n_base, frac_remainder), // Exact fractional
289                    ]
290                } else {
291                    &[(n_base, 0), (n_base + 1, 0)]
292                };
293
294                for &(n, frac) in candidates {
295                    if !is_valid_n(n) {
296                        continue;
297                    }
298
299                    if !is_vco_valid(m, n, frac) {
300                        continue;
301                    }
302
303                    // Calculate actual output frequency
304                    let vco_hz = calc_vco_hz(m, n, frac).unwrap();
305                    // crate::println!("vco_hz {}", vco_hz);
306                    let actual_hz = vco_hz / (q0 as u64) / (q1 as u64);
307                    // crate::println!("pll0 freq {}, q0 {} q1 {}", actual_hz, q0, q1);
308
309                    // Clamp to u32 range
310                    if actual_hz > u32::MAX as u64 {
311                        continue;
312                    }
313
314                    let actual_freq_hz = actual_hz as u32;
315                    let vco_freq_hz = vco_hz as u32;
316
317                    // Calculate error in PPM
318                    let diff = actual_freq_hz.abs_diff(target_freq_hz) as u64;
319                    let error_ppm = (diff * 1_000_000 / target) as u32;
320
321                    let candidate = PllParams { m, n, frac, q0, q1, vco_freq_hz, actual_freq_hz, error_ppm };
322
323                    // Determine if this candidate is better
324                    let dominated = best.as_ref().is_some_and(|b| {
325                        if error_ppm != b.error_ppm {
326                            error_ppm > b.error_ppm
327                        } else {
328                            // Same error: prefer no frac, then lower Q (less jitter)
329                            if frac > 0 && b.frac == 0 {
330                                true
331                            } else if frac == 0 && b.frac > 0 {
332                                false
333                            } else {
334                                // Prefer lower total post-division (lower jitter)
335                                (q0 as u16) * (q1 as u16) >= (b.q0 as u16) * (b.q1 as u16)
336                            }
337                        }
338                    });
339
340                    if !dominated {
341                        best = Some(candidate);
342                    }
343                }
344            }
345        }
346    }
347
348    best
349}