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