239 lines
6.7 KiB
Rust
239 lines
6.7 KiB
Rust
use crate::hardware::{dac::DacCode, design_parameters::ADC_SAMPLE_TICKS};
|
|
use miniconf::Miniconf;
|
|
use serde::Deserialize;
|
|
|
|
#[derive(Copy, Clone, Debug, Deserialize, Miniconf)]
|
|
pub enum Signal {
|
|
Cosine,
|
|
Square,
|
|
Triangle,
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, Miniconf, Deserialize)]
|
|
pub struct BasicConfig {
|
|
pub frequency: f32,
|
|
pub asymmetry: f32,
|
|
pub signal: Signal,
|
|
pub amplitude: f32,
|
|
}
|
|
|
|
impl Default for BasicConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
frequency: 1.0e3,
|
|
asymmetry: 0.0,
|
|
signal: Signal::Cosine,
|
|
amplitude: 0.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<BasicConfig> for Config {
|
|
fn from(config: BasicConfig) -> Config {
|
|
// Calculate the frequency tuning word
|
|
let frequency: u32 = {
|
|
let conversion_factor =
|
|
ADC_SAMPLE_TICKS as f32 / 100.0e6 * (1u64 << 32) as f32;
|
|
(config.frequency * conversion_factor) as u32
|
|
};
|
|
|
|
// Clamp amplitude and symmetry.
|
|
let amplitude = if config.amplitude > 10.24 {
|
|
10.24
|
|
} else if config.amplitude < 0.0 {
|
|
0.0
|
|
} else {
|
|
config.amplitude
|
|
};
|
|
|
|
let symmetry = {
|
|
let asymmetry = if config.asymmetry < -1.0 {
|
|
-1.0
|
|
} else if config.asymmetry > 1.0 {
|
|
1.0
|
|
} else {
|
|
config.asymmetry
|
|
};
|
|
|
|
(asymmetry * i32::MAX as f32) as i32
|
|
};
|
|
|
|
let signal_config = match config.signal {
|
|
Signal::Cosine => SignalConfig::Cosine,
|
|
Signal::Square => SignalConfig::Square { symmetry },
|
|
Signal::Triangle => {
|
|
let tuning_word = {
|
|
let segment_one_turns =
|
|
symmetry.wrapping_sub(i32::MIN) >> 16;
|
|
let segment_one_tw = if segment_one_turns > 0 {
|
|
u16::MAX as u32 / segment_one_turns as u32
|
|
} else {
|
|
0
|
|
};
|
|
|
|
let segment_two_turns =
|
|
i32::MAX.wrapping_sub(symmetry) >> 16;
|
|
let segment_two_tw = if segment_two_turns > 0 {
|
|
u16::MAX as u32 / segment_two_turns as u32
|
|
} else {
|
|
0
|
|
};
|
|
|
|
[segment_one_tw, segment_two_tw]
|
|
};
|
|
|
|
SignalConfig::Triangle {
|
|
symmetry,
|
|
tuning_word,
|
|
}
|
|
}
|
|
};
|
|
|
|
Config {
|
|
amplitude: DacCode::from(amplitude).into(),
|
|
signal: signal_config,
|
|
frequency,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug)]
|
|
pub struct Config {
|
|
/// The type of signal being generated
|
|
pub signal: SignalConfig,
|
|
|
|
/// The full-scale output code of the signal
|
|
pub amplitude: i16,
|
|
|
|
/// The frequency tuning word of the signal. Phase is incremented by this amount
|
|
pub frequency: u32,
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug)]
|
|
pub enum SignalConfig {
|
|
Cosine,
|
|
Square {
|
|
/// The phase symmetry cross-over of the waveform.
|
|
symmetry: i32,
|
|
},
|
|
Triangle {
|
|
/// The phase symmetry cross-over of the waveform.
|
|
symmetry: i32,
|
|
/// The amplitude added for each phase turn increment (different words for different
|
|
/// phases).
|
|
tuning_word: [u32; 2],
|
|
},
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct SignalGenerator {
|
|
phase_accumulator: u32,
|
|
config: Config,
|
|
pending_config: Option<Config>,
|
|
}
|
|
|
|
impl Default for SignalGenerator {
|
|
fn default() -> Self {
|
|
Self {
|
|
config: BasicConfig::default().into(),
|
|
phase_accumulator: 0,
|
|
pending_config: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl SignalGenerator {
|
|
/// Construct a new signal generator with some specific config.
|
|
///
|
|
/// # Args
|
|
/// * `config` - The config to use for generating signals.
|
|
///
|
|
/// # Returns
|
|
/// The generator
|
|
pub fn new(config: impl Into<Config>) -> Self {
|
|
Self {
|
|
config: config.into(),
|
|
pending_config: None,
|
|
phase_accumulator: 0,
|
|
}
|
|
}
|
|
|
|
// Increment the phase of the signal.
|
|
//
|
|
// # Note
|
|
// This handles automatically applying pending configurations on phase wrap.
|
|
//
|
|
// # Returns
|
|
// The new phase to use
|
|
fn increment(&mut self) -> i32 {
|
|
let (phase, overflow) = self
|
|
.phase_accumulator
|
|
.overflowing_add(self.config.frequency);
|
|
|
|
self.phase_accumulator = phase;
|
|
|
|
// Special case: If the FTW is specified as zero, we would otherwise never update the
|
|
// settings. Perform a check here for this corner case.
|
|
if overflow || self.config.frequency == 0 {
|
|
if let Some(config) = self.pending_config.take() {
|
|
self.config = config;
|
|
self.phase_accumulator = 0;
|
|
}
|
|
}
|
|
|
|
self.phase_accumulator as i32
|
|
}
|
|
|
|
/// Update waveform generation settings.
|
|
///
|
|
/// # Note
|
|
/// Changes will not take effect until the current waveform period elapses.
|
|
pub fn update_waveform(&mut self, new_config: impl Into<Config>) {
|
|
self.pending_config = Some(new_config.into());
|
|
}
|
|
}
|
|
|
|
impl core::iter::Iterator for SignalGenerator {
|
|
type Item = i16;
|
|
|
|
/// Get the next value in the generator sequence.
|
|
fn next(&mut self) -> Option<i16> {
|
|
let phase = self.increment();
|
|
|
|
let amplitude = match self.config.signal {
|
|
SignalConfig::Cosine => (dsp::cossin(phase).0 >> 16) as i16,
|
|
SignalConfig::Square { symmetry } => {
|
|
if phase < symmetry {
|
|
i16::MAX
|
|
} else {
|
|
i16::MIN
|
|
}
|
|
}
|
|
SignalConfig::Triangle {
|
|
symmetry,
|
|
tuning_word,
|
|
} => {
|
|
if phase < symmetry {
|
|
let segment_turns =
|
|
(phase.wrapping_sub(i32::MIN) >> 16) as u16;
|
|
i16::MIN.wrapping_add(
|
|
(tuning_word[0] * segment_turns as u32) as i16,
|
|
)
|
|
} else {
|
|
let segment_turns =
|
|
(phase.wrapping_sub(symmetry) >> 16) as u16;
|
|
i16::MAX.wrapping_sub(
|
|
(tuning_word[1] * segment_turns as u32) as i16,
|
|
)
|
|
}
|
|
}
|
|
};
|
|
|
|
// Calculate the final output result as an i16.
|
|
let result = amplitude as i32 * self.config.amplitude as i32;
|
|
|
|
// Note: We downshift by 15-bits to preserve only one of the sign bits.
|
|
Some((result >> 15) as i16)
|
|
}
|
|
}
|