use std::f64::consts::SQRT_2;
pub const A2: f64 = SQRT_2;
pub const A3: f64 = 1.1;
pub const B3: f64 = 2.4;
pub fn natural_frequency(order: u8, bn_hz: f64) -> Option<f64> {
if !(bn_hz > 0.0 && bn_hz.is_finite()) {
return None;
}
match order {
1 => Some(bn_hz / 0.25),
2 => Some(bn_hz / 0.53),
3 => Some(bn_hz / 0.7845),
_ => None,
}
}
pub fn second_order_noise_bandwidth(w0: f64, zeta: f64) -> f64 {
w0 * (1.0 + 4.0 * zeta * zeta) / (8.0 * zeta)
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
struct Integrator {
y: f64,
x_prev: f64,
}
impl Integrator {
fn step(&mut self, x: f64, t: f64) -> f64 {
self.y += 0.5 * t * (x + self.x_prev);
self.x_prev = x;
self.y
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct LoopFilter {
phase: Option<(u8, f64)>,
freq: Option<(u8, f64)>,
acc: Integrator,
vel: Integrator,
}
impl LoopFilter {
pub fn new(phase: Option<(u8, f64)>, freq: Option<(u8, f64)>) -> Result<Self, String> {
let phase = match phase {
Some((o, bn)) => Some((
o,
natural_frequency(o, bn).ok_or_else(|| {
format!("phase loop needs order 1..=3 and Bn > 0 (got {o}, {bn})")
})?,
)),
None => None,
};
let freq = match freq {
Some((o, bn)) if o == 1 || o == 2 => Some((
o,
natural_frequency(o, bn)
.ok_or_else(|| format!("frequency loop needs Bn > 0 (got {bn})"))?,
)),
Some((o, _)) => return Err(format!("frequency loop order must be 1 or 2 (got {o})")),
None => None,
};
if phase.is_none() && freq.is_none() {
return Err("a loop filter needs a phase path, a frequency path, or both".into());
}
Ok(Self {
phase,
freq,
acc: Integrator::default(),
vel: Integrator::default(),
})
}
pub fn pll(order: u8, bn_hz: f64) -> Result<Self, String> {
Self::new(Some((order, bn_hz)), None)
}
pub fn fll(order: u8, bn_hz: f64) -> Result<Self, String> {
Self::new(None, Some((order, bn_hz)))
}
pub fn phase_natural_frequency(&self) -> Option<f64> {
self.phase.map(|p| p.1)
}
pub fn freq_natural_frequency(&self) -> Option<f64> {
self.freq.map(|p| p.1)
}
pub fn phase_gains(&self) -> (f64, f64, f64) {
match self.phase {
Some((1, w)) => (w, 0.0, 0.0),
Some((2, w)) => (A2 * w, w * w, 0.0),
Some((_, w)) => (B3 * w, A3 * w * w, w * w * w),
None => (0.0, 0.0, 0.0),
}
}
pub fn freq_gains(&self) -> (f64, f64) {
match self.freq {
Some((1, w)) => (w, 0.0),
Some((_, w)) => (A2 * w, w * w),
None => (0.0, 0.0),
}
}
pub fn update(&mut self, phase_err: f64, freq_err: f64, t_s: f64) -> f64 {
let (k1, k2, k3) = self.phase_gains();
let (f1, f2) = self.freq_gains();
let acc_in = k3 * phase_err + f2 * freq_err;
let acc = if k3 != 0.0 || f2 != 0.0 {
self.acc.step(acc_in, t_s)
} else {
0.0
};
let vel_in = acc + k2 * phase_err + f1 * freq_err;
let vel = if k2 != 0.0 || f1 != 0.0 || k3 != 0.0 || f2 != 0.0 {
self.vel.step(vel_in, t_s)
} else {
0.0
};
vel + k1 * phase_err
}
pub fn reset(&mut self) {
self.acc = Integrator::default();
self.vel = Integrator::default();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ward_table_natural_frequencies() {
assert!((natural_frequency(1, 10.0).unwrap() - 40.0).abs() < 1e-12);
assert!((natural_frequency(2, 5.3).unwrap() - 10.0).abs() < 1e-12);
assert!((natural_frequency(3, 7.845).unwrap() - 10.0).abs() < 1e-12);
assert!(natural_frequency(4, 1.0).is_none());
assert!(natural_frequency(2, 0.0).is_none());
}
#[test]
fn second_order_closed_form_gives_ward_0_53() {
let r = second_order_noise_bandwidth(1.0, 1.0 / SQRT_2);
assert!((r - 0.53).abs() < 5e-4, "{r}");
}
#[test]
fn rejects_bad_orders() {
assert!(LoopFilter::new(None, None).is_err());
assert!(LoopFilter::fll(3, 1.0).is_err());
assert!(LoopFilter::pll(0, 1.0).is_err());
}
}