use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy)]
pub struct Pll<S: ControlScalar> {
pub kp: S,
pub ki: S,
pub omega_nominal: S,
theta_est: S,
omega_est: S,
integrator: S,
}
impl<S: ControlScalar> Pll<S> {
pub fn new(omega_nominal: S, kp: S, ki: S) -> Self {
Self {
kp,
ki,
omega_nominal,
theta_est: S::ZERO,
omega_est: omega_nominal,
integrator: S::ZERO,
}
}
pub fn update(&mut self, v_alpha: S, v_beta: S, dt: S) -> S {
let cos_t = self.theta_est.cos();
let sin_t = self.theta_est.sin();
let v_q = -v_alpha * sin_t + v_beta * cos_t;
self.integrator += v_q * dt;
let omega_correction = self.kp * v_q + self.ki * self.integrator;
self.omega_est = self.omega_nominal + omega_correction;
self.theta_est += self.omega_est * dt;
let pi = S::PI;
let two_pi = S::TWO * pi;
if self.theta_est > pi {
self.theta_est -= two_pi;
} else if self.theta_est < -pi {
self.theta_est += two_pi;
}
self.theta_est
}
pub fn theta(&self) -> S {
self.theta_est
}
pub fn omega(&self) -> S {
self.omega_est
}
pub fn phase_error(&self) -> S {
self.integrator }
pub fn reset(&mut self) {
self.theta_est = S::ZERO;
self.omega_est = self.omega_nominal;
self.integrator = S::ZERO;
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::f64::consts::PI;
#[test]
fn locks_onto_50hz_signal() {
let omega = 2.0 * PI * 50.0_f64; let mut pll = Pll::new(omega, 200.0, 2000.0);
let dt = 1e-4_f64;
for step in 0..10000 {
let t = step as f64 * dt;
let v_alpha = (omega * t).cos();
let v_beta = (omega * t).sin();
pll.update(v_alpha, v_beta, dt);
}
let t_end = 10000.0 * dt;
let theta_true = (omega * t_end) % (2.0 * PI);
let theta_true_wrapped = if theta_true > PI {
theta_true - 2.0 * PI
} else {
theta_true
};
let err = (pll.theta() - theta_true_wrapped).abs();
let err_wrapped = err.min((err - 2.0 * PI).abs());
assert!(
err_wrapped < 0.1,
"phase error={:.4} rad, theta_est={:.4}, theta_true={:.4}",
err_wrapped,
pll.theta(),
theta_true_wrapped
);
}
#[test]
fn estimated_frequency_converges() {
let omega = 2.0 * PI * 50.0_f64;
let mut pll = Pll::new(omega, 200.0, 2000.0);
let dt = 1e-4_f64;
for step in 0..10000 {
let t = step as f64 * dt;
let v_alpha = (omega * t).cos();
let v_beta = (omega * t).sin();
pll.update(v_alpha, v_beta, dt);
}
let omega_err = (pll.omega() - omega).abs();
assert!(omega_err < 10.0, "omega_err={:.2} rad/s", omega_err);
}
#[test]
fn reset_clears_state() {
let omega = 2.0 * PI * 50.0_f64;
let mut pll = Pll::new(omega, 200.0, 2000.0);
let dt = 1e-4_f64;
for step in 0..1000 {
let t = step as f64 * dt;
pll.update((omega * t).cos(), (omega * t).sin(), dt);
}
pll.reset();
assert_eq!(pll.theta(), 0.0);
assert_eq!(pll.omega(), omega);
}
#[test]
fn zero_input_stays_at_nominal() {
let omega = 100.0_f64;
let mut pll = Pll::new(omega, 10.0, 100.0);
let dt = 0.001;
for _ in 0..100 {
pll.update(0.0, 0.0, dt);
}
assert!((pll.omega() - omega).abs() < 1.0);
}
}