use crate::dsp::pll_filter::PllFilter;
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct Nco {
phase: f64,
frequency: f64,
pll_filter: PllFilter,
}
impl Nco {
pub fn new(frequency_hz: f64, sample_rate: f64) -> Self {
let frequency = frequency_hz / sample_rate;
Self {
phase: 0.0,
frequency,
pll_filter: PllFilter::from_bandwidth(0.1 * sample_rate, sample_rate), }
}
pub fn set_pll_bandwidth(&mut self, bandwidth_hz: f64, sample_rate: f64) {
self.pll_filter.set_bandwidth(bandwidth_hz, sample_rate);
}
pub fn set_pll_gains(&mut self, alpha: f64, beta: f64) {
self.pll_filter.set_gains(alpha, beta);
}
pub fn get_phase_radians(&self) -> f64 {
self.phase * 2.0 * PI
}
pub fn get_phase(&self) -> f64 {
self.phase
}
pub fn set_phase(&mut self, phase: f64) {
self.phase = phase - phase.floor();
}
pub fn set_phase_radians(&mut self, phase_rad: f64) {
self.set_phase(phase_rad / (2.0 * PI));
}
pub fn get_frequency_hz(&self, sample_rate: f64) -> f64 {
self.frequency * sample_rate
}
pub fn get_frequency(&self) -> f64 {
self.frequency
}
pub fn set_frequency_hz(&mut self, frequency_hz: f64, sample_rate: f64) {
self.frequency = frequency_hz / sample_rate;
}
pub fn set_frequency(&mut self, frequency: f64) {
self.frequency = frequency;
}
pub fn adjust_frequency(&mut self, delta: f64) {
self.frequency += delta;
}
pub fn adjust_phase(&mut self, delta: f64) {
self.phase += delta;
self.phase -= self.phase.floor();
}
pub fn get_complex(&self) -> (f32, f32) {
let theta = 2.0 * PI * self.phase;
(theta.cos() as f32, theta.sin() as f32)
}
pub fn sincos(&self) -> (f32, f32) {
let theta = 2.0 * PI * self.phase;
(theta.sin() as f32, theta.cos() as f32)
}
pub fn step(&mut self) {
self.phase += self.frequency;
if self.phase >= 1.0 {
self.phase -= 1.0;
} else if self.phase < 0.0 {
self.phase += 1.0;
}
}
pub fn mix_down(&self, input: f32) -> (f32, f32) {
let theta = 2.0 * PI * self.phase;
let cos_theta = theta.cos() as f32;
let sin_theta = theta.sin() as f32;
(input * cos_theta, -input * sin_theta)
}
pub fn mix_down_complex(&self, i: f32, q: f32) -> (f32, f32) {
let theta = 2.0 * PI * self.phase;
let cos_theta = theta.cos() as f32;
let sin_theta = theta.sin() as f32;
(i * cos_theta + q * sin_theta, q * cos_theta - i * sin_theta)
}
pub fn mix_up(&self, input: f32) -> (f32, f32) {
let theta = 2.0 * PI * self.phase;
let cos_theta = theta.cos() as f32;
let sin_theta = theta.sin() as f32;
(input * cos_theta, input * sin_theta)
}
pub fn pll_step(&mut self, phase_error: f64) {
let error_cycles = phase_error / (2.0 * PI);
let (freq_adj_cycles, phase_adj_cycles) = self.pll_filter.update_cycles(error_cycles);
self.adjust_frequency(freq_adj_cycles);
self.adjust_phase(phase_adj_cycles);
}
pub fn pll_step_cycles(&mut self, phase_error_cycles: f64) {
let (freq_adj_cycles, phase_adj_cycles) = self.pll_filter.update_cycles(phase_error_cycles);
self.adjust_frequency(freq_adj_cycles);
self.adjust_phase(phase_adj_cycles);
}
pub fn reset(&mut self) {
self.phase = 0.0;
}
pub fn reset_full(&mut self) {
self.phase = 0.0;
self.frequency = 0.0;
}
}
impl Default for Nco {
fn default() -> Self {
Self {
phase: 0.0,
frequency: 0.0,
pll_filter: PllFilter::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
#[test]
fn test_nco_creation() {
let nco = Nco::new(57000.0, 171000.0);
assert!((nco.get_frequency() - 1.0 / 3.0).abs() < 1e-10);
assert_eq!(nco.get_phase(), 0.0);
}
#[test]
fn test_nco_step() {
let mut nco = Nco::new(57000.0, 171000.0);
nco.step();
assert!((nco.get_phase() - 1.0 / 3.0).abs() < 1e-10);
nco.step();
assert!((nco.get_phase() - 2.0 / 3.0).abs() < 1e-10);
nco.step();
assert!(nco.get_phase() < 1e-10);
}
#[test]
fn test_nco_get_complex() {
let nco = Nco::new(57000.0, 171000.0);
let (cos_val, sin_val) = nco.get_complex();
assert!((cos_val - 1.0).abs() < 1e-6);
assert!(sin_val.abs() < 1e-6);
}
#[test]
fn test_nco_mix_down() {
let nco = Nco::new(57000.0, 171000.0);
let (i, q) = nco.mix_down(1.0);
assert!((i - 1.0).abs() < 1e-6);
assert!(q.abs() < 1e-6);
}
#[test]
fn test_pll_bandwidth() {
let mut nco = Nco::new(57000.0, 171000.0);
nco.set_pll_bandwidth(0.03, 171000.0);
let expected_alpha = 0.03 / 171000.0;
let phase_error = 1.0; let freq_before = nco.get_frequency();
nco.pll_step(phase_error);
let freq_after = nco.get_frequency();
let error_cycles = phase_error / (2.0 * std::f64::consts::PI);
let expected_freq_change = expected_alpha * error_cycles;
assert!((freq_after - freq_before - expected_freq_change).abs() < 1e-15);
}
#[test]
fn test_pll_tracking() {
let sample_rate = 171000.0;
let mut nco_tx = Nco::new(57010.0, sample_rate); let mut nco_rx = Nco::new(57000.0, sample_rate); nco_rx.set_pll_bandwidth(10.0, sample_rate);
for _ in 0..10000 {
let tx_phase = nco_tx.get_phase_radians();
let rx_phase = nco_rx.get_phase_radians();
let mut phase_error = tx_phase - rx_phase;
while phase_error > PI {
phase_error -= 2.0 * PI;
}
while phase_error < -PI {
phase_error += 2.0 * PI;
}
nco_rx.pll_step(phase_error);
nco_tx.step();
nco_rx.step();
}
let freq_error =
(nco_tx.get_frequency_hz(sample_rate) - nco_rx.get_frequency_hz(sample_rate)).abs();
assert!(
freq_error < 1.0,
"Frequency error {} Hz too large",
freq_error
);
}
#[test]
fn test_phase_wrapping() {
let mut nco = Nco::new(100000.0, 100000.0);
for _ in 0..10 {
nco.step();
assert!(nco.get_phase() >= 0.0);
assert!(nco.get_phase() < 1.0);
}
}
#[test]
fn test_negative_frequency() {
let mut nco = Nco::new(-10000.0, 100000.0);
nco.step();
assert!(nco.get_phase() >= 0.0);
assert!(nco.get_phase() < 1.0);
assert!((nco.get_phase() - 0.9).abs() < 1e-10);
}
}