use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy)]
pub struct ZeroCrossingEstimator<S: ControlScalar> {
pub frequency_hz: S,
prev: S,
time_since_crossing: S,
period: S,
pub min_hz: S,
pub max_hz: S,
}
impl<S: ControlScalar> ZeroCrossingEstimator<S> {
pub fn new(nominal_hz: S) -> Self {
Self {
frequency_hz: nominal_hz,
prev: S::ZERO,
time_since_crossing: S::ZERO,
period: S::ONE / nominal_hz,
min_hz: S::from_f64(40.0),
max_hz: S::from_f64(70.0),
}
}
pub fn update(&mut self, sample: S, dt: S) -> S {
self.time_since_crossing += dt;
let rising = self.prev < S::ZERO && sample >= S::ZERO;
if rising && self.time_since_crossing > dt {
let t = self.time_since_crossing;
let f = S::ONE / t;
if f >= self.min_hz && f <= self.max_hz {
self.period = t;
self.frequency_hz = f;
}
self.time_since_crossing = S::ZERO;
}
self.prev = sample;
self.frequency_hz
}
pub fn reset(&mut self, nominal_hz: S) {
self.frequency_hz = nominal_hz;
self.prev = S::ZERO;
self.time_since_crossing = S::ZERO;
self.period = S::ONE / nominal_hz;
}
}
#[derive(Debug, Clone, Copy)]
pub struct FrequencyPll<S: ControlScalar> {
pub omega: S,
pub theta: S,
pub amplitude: S,
pub kp: S,
pub ki: S,
int: S,
pub omega_nom: S,
amp_lpf: S,
amp_alpha: S,
}
impl<S: ControlScalar> FrequencyPll<S> {
pub fn new(omega_nom: S, kp: S, ki: S) -> Self {
Self {
omega: omega_nom,
theta: S::ZERO,
amplitude: S::ONE,
kp,
ki,
int: S::ZERO,
omega_nom,
amp_lpf: S::ONE,
amp_alpha: S::from_f64(0.99),
}
}
pub fn update(&mut self, sample: S, dt: S) -> (S, S, S) {
let sin_t = self.theta.sin();
let cos_t = self.theta.cos();
let inst_amp =
(sample * sample + (sample * sin_t / (cos_t.abs() + S::from_f64(0.01))).powi(2)).sqrt();
self.amp_lpf = self.amp_alpha * self.amp_lpf + (S::ONE - self.amp_alpha) * inst_amp;
self.amplitude = self.amp_lpf + S::from_f64(1e-6);
let err = sample * cos_t;
self.int += self.ki * err * dt;
let delta_omega = self.kp * err + self.int;
self.omega = self.omega_nom + delta_omega;
self.theta += self.omega * dt;
let pi = S::PI;
let two_pi = S::TWO * pi;
while self.theta > pi {
self.theta -= two_pi;
}
while self.theta < -pi {
self.theta += two_pi;
}
let two_pi_f64 = S::TWO * S::PI;
(self.omega / two_pi_f64, self.theta, self.amplitude)
}
pub fn reset(&mut self) {
self.omega = self.omega_nom;
self.theta = S::ZERO;
self.int = S::ZERO;
self.amp_lpf = S::ONE;
}
}
#[derive(Debug, Clone, Copy)]
pub struct InstantaneousFrequency<S: ControlScalar> {
xi_prev: S,
xq_prev: S,
delay: [S; 4],
delay_idx: usize,
pub frequency_hz: S,
lpf: S,
pub lpf_alpha: S,
}
impl<S: ControlScalar> InstantaneousFrequency<S> {
pub fn new(nominal_hz: S, lpf_alpha: S) -> Self {
Self {
xi_prev: S::ZERO,
xq_prev: S::ZERO,
delay: [S::ZERO; 4],
delay_idx: 0,
frequency_hz: nominal_hz,
lpf: nominal_hz,
lpf_alpha,
}
}
pub fn update(&mut self, sample: S, dt: S) -> S {
let xq = self.delay[self.delay_idx];
self.delay[self.delay_idx] = sample;
self.delay_idx = (self.delay_idx + 1) % 4;
let xi = sample;
let cross = xi * self.xq_prev - xq * self.xi_prev;
let energy = xi * xi + xq * xq;
if energy > S::from_f64(1e-6) {
let two_pi = S::TWO * S::PI;
let f_inst = cross / (two_pi * dt * energy);
self.lpf = self.lpf_alpha * self.lpf + (S::ONE - self.lpf_alpha) * f_inst;
self.frequency_hz = self.lpf;
}
self.xi_prev = xi;
self.xq_prev = xq;
self.frequency_hz
}
pub fn reset(&mut self, nominal_hz: S) {
self.delay = [S::ZERO; 4];
self.delay_idx = 0;
self.xi_prev = S::ZERO;
self.xq_prev = S::ZERO;
self.lpf = nominal_hz;
self.frequency_hz = nominal_hz;
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::f64::consts::PI;
#[test]
fn zero_crossing_detects_50hz() {
let dt = 1e-4;
let f_true = 50.0f64;
let omega = 2.0 * PI * f_true;
let mut est = ZeroCrossingEstimator::new(50.0_f64);
for k in 0..2000 {
let t = k as f64 * dt;
let s = (omega * t).sin();
est.update(s, dt);
}
let err = (est.frequency_hz - f_true).abs();
assert!(err < 1.0, "freq={:.3} Hz, err={err:.3}", est.frequency_hz);
}
#[test]
fn zero_crossing_60hz() {
let dt = 1e-4;
let f_true = 60.0f64;
let omega = 2.0 * PI * f_true;
let mut est = ZeroCrossingEstimator::new(60.0_f64);
for k in 0..2000 {
let t = k as f64 * dt;
let s = (omega * t).sin();
est.update(s, dt);
}
let err = (est.frequency_hz - f_true).abs();
assert!(err < 1.0, "freq={:.3} Hz, err={err:.3}", est.frequency_hz);
}
#[test]
fn pll_frequency_locks() {
let f_true = 50.0f64;
let omega_nom = 2.0 * PI * f_true;
let dt = 1e-4;
let mut pll = FrequencyPll::new(omega_nom, 100.0_f64, 2000.0_f64);
let mut theta_in = 0.0f64;
for _ in 0..8000 {
let s = theta_in.sin();
pll.update(s, dt);
theta_in += omega_nom * dt;
if theta_in > PI {
theta_in -= 2.0 * PI;
}
}
let freq_err = (pll.omega / (2.0 * PI) - f_true).abs();
assert!(freq_err < 2.0, "freq_err={freq_err:.4} Hz");
}
#[test]
fn pll_reset_clears_state() {
let mut pll = FrequencyPll::new(314.16_f64, 50.0, 500.0);
for _ in 0..100 {
pll.update(1.0, 1e-4);
}
pll.reset();
assert_eq!(pll.theta, 0.0);
assert_eq!(pll.int, 0.0);
}
}