idsp/
pll.rs

1use serde::{Deserialize, Serialize};
2
3/// Type-II, sampled phase, discrete time PLL
4///
5/// This PLL tracks the frequency and phase of an input signal with respect to the sampling clock.
6/// The open loop transfer function is I^2,I from input phase to output phase and P,I from input
7/// phase to output frequency.
8///
9/// The transfer functions (for phase and frequency) contain an additional zero at Nyquist.
10///
11/// The PLL locks to any frequency (i.e. it locks to the alias in the first Nyquist zone) and is
12/// stable for any gain (1 <= shift <= 30). It has a single parameter that determines the loop
13/// bandwidth in octave steps. The gain can be changed freely between updates.
14///
15/// The frequency and phase settling time constants for a frequency/phase jump are `1 << shift`
16/// update cycles. The loop bandwidth is `1/(2*pi*(1 << shift))` in units of the sample rate.
17/// While the phase is being settled after settling the frequency, there is a typically very
18/// small frequency overshoot.
19///
20/// All math is naturally wrapping 32 bit integer. Phase and frequency are understood modulo that
21/// overflow in the first Nyquist zone. Expressing the IIR equations in other ways (e.g. single
22/// (T)-DF-{I,II} biquad/IIR) would break on overflow (i.e. every cycle).
23///
24/// There are no floating point rounding errors here. But there is integer quantization/truncation
25/// error of the `shift` lowest bits leading to a phase offset for very low gains. Truncation
26/// bias is applied. Rounding is "half up". The phase truncation error can be removed very
27/// efficiently by dithering.
28///
29/// This PLL does not unwrap phase slips accumulated during (frequency) lock acquisition.
30/// This can and should be implemented elsewhere by unwrapping and scaling the input phase
31/// and un-scaling and wrapping output phase and frequency. This then affects dynamic range,
32/// gain, and noise accordingly.
33///
34/// The extension to I^3,I^2,I behavior to track chirps phase-accurately or to i64 data to
35/// increase resolution for extremely narrowband applications is obvious.
36///
37/// This PLL implements first order noise shaping to reduce quantization errors.
38#[derive(Copy, Clone, Default, Deserialize, Serialize)]
39pub struct PLL {
40    // last input phase
41    x: i32,
42    // last output phase
43    y0: i32,
44    // last output frequency
45    f0: i32,
46    // filtered frequency
47    f: i64,
48    // filtered output phase
49    y: i64,
50}
51
52impl PLL {
53    /// Update the PLL with a new phase sample. This needs to be called (sampled) periodically.
54    /// The signal's phase/frequency is reconstructed relative to the sampling period.
55    ///
56    /// Args:
57    /// * `x`: New input phase sample or None if a sample has been missed.
58    /// * `k`: Feedback gain.
59    ///
60    /// Returns:
61    /// A tuple of instantaneous phase and frequency estimates.
62    pub fn update(&mut self, x: Option<i32>, k: i32) {
63        if let Some(x) = x {
64            let dx = x.wrapping_sub(self.x);
65            self.x = x;
66            let df = dx.wrapping_sub((self.f >> 32) as i32) as i64 * k as i64;
67            self.f = self.f.wrapping_add(df);
68            self.y = self.y.wrapping_add(self.f);
69            self.f = self.f.wrapping_add(df);
70            let dy = x.wrapping_sub((self.y >> 32) as i32) as i64 * k as i64;
71            self.y = self.y.wrapping_add(dy);
72            let y = (self.y >> 32) as i32;
73            self.y = self.y.wrapping_add(dy);
74            self.f0 = y.wrapping_sub(self.y0);
75            self.y0 = y;
76        } else {
77            self.y = self.y.wrapping_add(self.f);
78            self.x = self.x.wrapping_add(self.f0);
79            self.y0 = self.y0.wrapping_add(self.f0);
80        }
81    }
82
83    /// Return the current phase estimate
84    pub fn phase(&self) -> i32 {
85        self.y0
86    }
87
88    /// Return the current frequency estimate
89    pub fn frequency(&self) -> i32 {
90        self.f0
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    #[test]
98    fn mini() {
99        let mut p = PLL::default();
100        let k = 1 << 24;
101        p.update(Some(0x10000), k);
102        assert_eq!(p.phase(), 0x1ff);
103        assert_eq!(p.frequency(), 0x1ff);
104    }
105
106    #[test]
107    fn converge() {
108        let mut p = PLL::default();
109        let k = 1 << 24;
110        let f0 = 0x71f63049_i32;
111        let n = 1 << 14;
112        let mut x = 0i32;
113        for i in 0..n {
114            x = x.wrapping_add(f0);
115            p.update(Some(x), k);
116            if i > n / 4 {
117                assert_eq!(p.frequency().wrapping_sub(f0).abs() <= 1, true);
118            }
119            if i > n / 2 {
120                assert_eq!(p.phase().wrapping_sub(x).abs() <= 1, true);
121            }
122        }
123    }
124}