Merge #388
388: Feature/scan mode r=jordens a=ryan-summers This PR is to add visibility on design decisions for the scan mode implementation for #86 This PR: * Adds a signal generator for sinusoids, triangular waves, and square waves to both channels of `dual-iir` Testing: The new signal generator was scanned across 0-100% symmetry for all waveform types using frequencies of 500-1KHz. It was observed on an oscilloscope to contain nominal, well-formed outputs. Co-authored-by: Ryan Summers <ryan.summers@vertigo-designs.com>
This commit is contained in:
commit
929a7611d6
|
@ -29,7 +29,10 @@
|
||||||
#![no_std]
|
#![no_std]
|
||||||
#![no_main]
|
#![no_main]
|
||||||
|
|
||||||
use core::sync::atomic::{fence, Ordering};
|
use core::{
|
||||||
|
convert::TryInto,
|
||||||
|
sync::atomic::{fence, Ordering},
|
||||||
|
};
|
||||||
|
|
||||||
use mutex_trait::prelude::*;
|
use mutex_trait::prelude::*;
|
||||||
|
|
||||||
|
@ -42,6 +45,7 @@ use stabilizer::{
|
||||||
dac::{Dac0Output, Dac1Output, DacCode},
|
dac::{Dac0Output, Dac1Output, DacCode},
|
||||||
embedded_hal::digital::v2::InputPin,
|
embedded_hal::digital::v2::InputPin,
|
||||||
hal,
|
hal,
|
||||||
|
signal_generator::{self, SignalGenerator},
|
||||||
system_timer::SystemTimer,
|
system_timer::SystemTimer,
|
||||||
DigitalInput0, DigitalInput1, AFE0, AFE1,
|
DigitalInput0, DigitalInput1, AFE0, AFE1,
|
||||||
},
|
},
|
||||||
|
@ -119,6 +123,17 @@ pub struct Settings {
|
||||||
/// # Value
|
/// # Value
|
||||||
/// See [StreamTarget#miniconf]
|
/// See [StreamTarget#miniconf]
|
||||||
stream_target: StreamTarget,
|
stream_target: StreamTarget,
|
||||||
|
|
||||||
|
/// Specifies the config for signal generators to add on to DAC0/DAC1 outputs.
|
||||||
|
///
|
||||||
|
/// # Path
|
||||||
|
/// `signal_generator/<n>`
|
||||||
|
///
|
||||||
|
/// * <n> specifies which channel to configure. <n> := [0, 1]
|
||||||
|
///
|
||||||
|
/// # Value
|
||||||
|
/// See [signal_generator::BasicConfig#miniconf]
|
||||||
|
signal_generator: [signal_generator::BasicConfig; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Settings {
|
impl Default for Settings {
|
||||||
|
@ -139,6 +154,8 @@ impl Default for Settings {
|
||||||
// The default telemetry period in seconds.
|
// The default telemetry period in seconds.
|
||||||
telemetry_period: 10,
|
telemetry_period: 10,
|
||||||
|
|
||||||
|
signal_generator: [signal_generator::BasicConfig::default(); 2],
|
||||||
|
|
||||||
stream_target: StreamTarget::default(),
|
stream_target: StreamTarget::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -153,6 +170,7 @@ const APP: () = {
|
||||||
dacs: (Dac0Output, Dac1Output),
|
dacs: (Dac0Output, Dac1Output),
|
||||||
network: NetworkUsers<Settings, Telemetry>,
|
network: NetworkUsers<Settings, Telemetry>,
|
||||||
generator: BlockGenerator,
|
generator: BlockGenerator,
|
||||||
|
signal_generator: [SignalGenerator; 2],
|
||||||
|
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
telemetry: TelemetryBuffer,
|
telemetry: TelemetryBuffer,
|
||||||
|
@ -193,6 +211,8 @@ const APP: () = {
|
||||||
// Start sampling ADCs.
|
// Start sampling ADCs.
|
||||||
stabilizer.adc_dac_timer.start();
|
stabilizer.adc_dac_timer.start();
|
||||||
|
|
||||||
|
let settings = Settings::default();
|
||||||
|
|
||||||
init::LateResources {
|
init::LateResources {
|
||||||
afes: stabilizer.afes,
|
afes: stabilizer.afes,
|
||||||
adcs: stabilizer.adcs,
|
adcs: stabilizer.adcs,
|
||||||
|
@ -201,7 +221,15 @@ const APP: () = {
|
||||||
network,
|
network,
|
||||||
digital_inputs: stabilizer.digital_inputs,
|
digital_inputs: stabilizer.digital_inputs,
|
||||||
telemetry: TelemetryBuffer::default(),
|
telemetry: TelemetryBuffer::default(),
|
||||||
settings: Settings::default(),
|
settings,
|
||||||
|
signal_generator: [
|
||||||
|
SignalGenerator::new(
|
||||||
|
settings.signal_generator[0].try_into().unwrap(),
|
||||||
|
),
|
||||||
|
SignalGenerator::new(
|
||||||
|
settings.signal_generator[1].try_into().unwrap(),
|
||||||
|
),
|
||||||
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,7 +249,7 @@ const APP: () = {
|
||||||
///
|
///
|
||||||
/// Because the ADC and DAC operate at the same rate, these two constraints actually implement
|
/// Because the ADC and DAC operate at the same rate, these two constraints actually implement
|
||||||
/// the same time bounds, meeting one also means the other is also met.
|
/// the same time bounds, meeting one also means the other is also met.
|
||||||
#[task(binds=DMA1_STR4, resources=[adcs, digital_inputs, dacs, iir_state, settings, telemetry, generator], priority=2)]
|
#[task(binds=DMA1_STR4, resources=[adcs, digital_inputs, dacs, iir_state, settings, signal_generator, telemetry, generator], priority=2)]
|
||||||
#[inline(never)]
|
#[inline(never)]
|
||||||
#[link_section = ".itcm.process"]
|
#[link_section = ".itcm.process"]
|
||||||
fn process(mut c: process::Context) {
|
fn process(mut c: process::Context) {
|
||||||
|
@ -233,6 +261,7 @@ const APP: () = {
|
||||||
ref mut iir_state,
|
ref mut iir_state,
|
||||||
ref mut telemetry,
|
ref mut telemetry,
|
||||||
ref mut generator,
|
ref mut generator,
|
||||||
|
ref mut signal_generator,
|
||||||
} = c.resources;
|
} = c.resources;
|
||||||
|
|
||||||
let digital_inputs = [
|
let digital_inputs = [
|
||||||
|
@ -255,7 +284,8 @@ const APP: () = {
|
||||||
adc_samples[channel]
|
adc_samples[channel]
|
||||||
.iter()
|
.iter()
|
||||||
.zip(dac_samples[channel].iter_mut())
|
.zip(dac_samples[channel].iter_mut())
|
||||||
.map(|(ai, di)| {
|
.zip(&mut signal_generator[channel])
|
||||||
|
.map(|((ai, di), signal)| {
|
||||||
let x = f32::from(*ai as i16);
|
let x = f32::from(*ai as i16);
|
||||||
let y = settings.iir_ch[channel]
|
let y = settings.iir_ch[channel]
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -263,9 +293,13 @@ const APP: () = {
|
||||||
.fold(x, |yi, (ch, state)| {
|
.fold(x, |yi, (ch, state)| {
|
||||||
ch.update(state, yi, hold)
|
ch.update(state, yi, hold)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Note(unsafe): The filter limits must ensure that the value is in range.
|
// Note(unsafe): The filter limits must ensure that the value is in range.
|
||||||
// The truncation introduces 1/2 LSB distortion.
|
// The truncation introduces 1/2 LSB distortion.
|
||||||
let y: i16 = unsafe { y.to_int_unchecked() };
|
let y: i16 = unsafe { y.to_int_unchecked() };
|
||||||
|
|
||||||
|
let y = y.saturating_add(signal);
|
||||||
|
|
||||||
// Convert to DAC code
|
// Convert to DAC code
|
||||||
*di = DacCode::from(y).0;
|
*di = DacCode::from(y).0;
|
||||||
})
|
})
|
||||||
|
@ -300,7 +334,7 @@ const APP: () = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[task(priority = 1, resources=[network, afes, settings])]
|
#[task(priority = 1, resources=[network, afes, settings, signal_generator])]
|
||||||
fn settings_update(mut c: settings_update::Context) {
|
fn settings_update(mut c: settings_update::Context) {
|
||||||
// Update the IIR channels.
|
// Update the IIR channels.
|
||||||
let settings = c.resources.network.miniconf.settings();
|
let settings = c.resources.network.miniconf.settings();
|
||||||
|
@ -310,6 +344,22 @@ const APP: () = {
|
||||||
c.resources.afes.0.set_gain(settings.afe[0]);
|
c.resources.afes.0.set_gain(settings.afe[0]);
|
||||||
c.resources.afes.1.set_gain(settings.afe[1]);
|
c.resources.afes.1.set_gain(settings.afe[1]);
|
||||||
|
|
||||||
|
// Update the signal generators
|
||||||
|
for (i, &config) in settings.signal_generator.iter().enumerate() {
|
||||||
|
match config.try_into() {
|
||||||
|
Ok(config) => {
|
||||||
|
c.resources
|
||||||
|
.signal_generator
|
||||||
|
.lock(|generator| generator[i].update_waveform(config));
|
||||||
|
}
|
||||||
|
Err(err) => log::error!(
|
||||||
|
"Failed to update signal generation on DAC{}: {:?}",
|
||||||
|
i,
|
||||||
|
err
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let target = settings.stream_target.into();
|
let target = settings.stream_target.into();
|
||||||
c.resources.network.direct_stream(target);
|
c.resources.network.direct_stream(target);
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,10 @@
|
||||||
#![no_std]
|
#![no_std]
|
||||||
#![no_main]
|
#![no_main]
|
||||||
|
|
||||||
use core::sync::atomic::{fence, Ordering};
|
use core::{
|
||||||
|
convert::TryFrom,
|
||||||
|
sync::atomic::{fence, Ordering},
|
||||||
|
};
|
||||||
|
|
||||||
use mutex_trait::prelude::*;
|
use mutex_trait::prelude::*;
|
||||||
|
|
||||||
|
@ -40,10 +43,10 @@ use stabilizer::{
|
||||||
adc::{Adc0Input, Adc1Input, AdcCode},
|
adc::{Adc0Input, Adc1Input, AdcCode},
|
||||||
afe::Gain,
|
afe::Gain,
|
||||||
dac::{Dac0Output, Dac1Output, DacCode},
|
dac::{Dac0Output, Dac1Output, DacCode},
|
||||||
design_parameters,
|
|
||||||
embedded_hal::digital::v2::InputPin,
|
embedded_hal::digital::v2::InputPin,
|
||||||
hal,
|
hal,
|
||||||
input_stamper::InputStamper,
|
input_stamper::InputStamper,
|
||||||
|
signal_generator,
|
||||||
system_timer::SystemTimer,
|
system_timer::SystemTimer,
|
||||||
DigitalInput0, DigitalInput1, AFE0, AFE1,
|
DigitalInput0, DigitalInput1, AFE0, AFE1,
|
||||||
},
|
},
|
||||||
|
@ -56,13 +59,6 @@ use stabilizer::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// A constant sinusoid to send on the DAC output.
|
|
||||||
// Full-scale gives a +/- 10.24V amplitude waveform. Scale it down to give +/- 1V.
|
|
||||||
const ONE: i16 = ((1.0 / 10.24) * i16::MAX as f32) as _;
|
|
||||||
const SQRT2: i16 = (ONE as f32 * 0.707) as _;
|
|
||||||
const DAC_SEQUENCE: [i16; design_parameters::SAMPLE_BUFFER_SIZE] =
|
|
||||||
[ONE, SQRT2, 0, -SQRT2, -ONE, -SQRT2, 0, SQRT2];
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Deserialize, Miniconf)]
|
#[derive(Copy, Clone, Debug, Deserialize, Miniconf)]
|
||||||
enum Conf {
|
enum Conf {
|
||||||
/// Output the lockin magnitude.
|
/// Output the lockin magnitude.
|
||||||
|
@ -213,6 +209,7 @@ const APP: () = {
|
||||||
telemetry: TelemetryBuffer,
|
telemetry: TelemetryBuffer,
|
||||||
digital_inputs: (DigitalInput0, DigitalInput1),
|
digital_inputs: (DigitalInput0, DigitalInput1),
|
||||||
generator: BlockGenerator,
|
generator: BlockGenerator,
|
||||||
|
signal_generator: signal_generator::SignalGenerator,
|
||||||
|
|
||||||
timestamper: InputStamper,
|
timestamper: InputStamper,
|
||||||
pll: RPLL,
|
pll: RPLL,
|
||||||
|
@ -264,6 +261,23 @@ const APP: () = {
|
||||||
// Enable the timestamper.
|
// Enable the timestamper.
|
||||||
stabilizer.timestamper.start();
|
stabilizer.timestamper.start();
|
||||||
|
|
||||||
|
let signal_config = {
|
||||||
|
let frequency_tuning_word =
|
||||||
|
(1u64 << (32 - configuration::SAMPLE_BUFFER_SIZE_LOG2)) as u32;
|
||||||
|
|
||||||
|
signal_generator::Config {
|
||||||
|
// Same frequency as batch size.
|
||||||
|
frequency_tuning_word: [
|
||||||
|
frequency_tuning_word,
|
||||||
|
frequency_tuning_word,
|
||||||
|
],
|
||||||
|
// 1V Amplitude
|
||||||
|
amplitude: DacCode::try_from(1.0).unwrap().into(),
|
||||||
|
|
||||||
|
signal: signal_generator::Signal::Cosine,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
init::LateResources {
|
init::LateResources {
|
||||||
afes: stabilizer.afes,
|
afes: stabilizer.afes,
|
||||||
adcs: stabilizer.adcs,
|
adcs: stabilizer.adcs,
|
||||||
|
@ -272,6 +286,9 @@ const APP: () = {
|
||||||
digital_inputs: stabilizer.digital_inputs,
|
digital_inputs: stabilizer.digital_inputs,
|
||||||
timestamper: stabilizer.timestamper,
|
timestamper: stabilizer.timestamper,
|
||||||
telemetry: TelemetryBuffer::default(),
|
telemetry: TelemetryBuffer::default(),
|
||||||
|
signal_generator: signal_generator::SignalGenerator::new(
|
||||||
|
signal_config,
|
||||||
|
),
|
||||||
|
|
||||||
settings,
|
settings,
|
||||||
generator,
|
generator,
|
||||||
|
@ -288,7 +305,7 @@ const APP: () = {
|
||||||
/// This is an implementation of a externally (DI0) referenced PLL lockin on the ADC0 signal.
|
/// This is an implementation of a externally (DI0) referenced PLL lockin on the ADC0 signal.
|
||||||
/// It outputs either I/Q or power/phase on DAC0/DAC1. Data is normalized to full scale.
|
/// It outputs either I/Q or power/phase on DAC0/DAC1. Data is normalized to full scale.
|
||||||
/// PLL bandwidth, filter bandwidth, slope, and x/y or power/phase post-filters are available.
|
/// PLL bandwidth, filter bandwidth, slope, and x/y or power/phase post-filters are available.
|
||||||
#[task(binds=DMA1_STR4, resources=[adcs, dacs, lockin, timestamper, pll, settings, telemetry, generator], priority=2)]
|
#[task(binds=DMA1_STR4, resources=[adcs, dacs, lockin, timestamper, pll, settings, telemetry, generator, signal_generator], priority=2)]
|
||||||
#[inline(never)]
|
#[inline(never)]
|
||||||
#[link_section = ".itcm.process"]
|
#[link_section = ".itcm.process"]
|
||||||
fn process(mut c: process::Context) {
|
fn process(mut c: process::Context) {
|
||||||
|
@ -301,6 +318,7 @@ const APP: () = {
|
||||||
ref mut pll,
|
ref mut pll,
|
||||||
ref mut timestamper,
|
ref mut timestamper,
|
||||||
ref mut generator,
|
ref mut generator,
|
||||||
|
ref mut signal_generator,
|
||||||
} = c.resources;
|
} = c.resources;
|
||||||
|
|
||||||
let (reference_phase, reference_frequency) = match settings.lockin_mode
|
let (reference_phase, reference_frequency) = match settings.lockin_mode
|
||||||
|
@ -356,7 +374,7 @@ const APP: () = {
|
||||||
|
|
||||||
// Convert to DAC data.
|
// Convert to DAC data.
|
||||||
for (channel, samples) in dac_samples.iter_mut().enumerate() {
|
for (channel, samples) in dac_samples.iter_mut().enumerate() {
|
||||||
for (i, sample) in samples.iter_mut().enumerate() {
|
for sample in samples.iter_mut() {
|
||||||
let value = match settings.output_conf[channel] {
|
let value = match settings.output_conf[channel] {
|
||||||
Conf::Magnitude => output.abs_sqr() as i32 >> 16,
|
Conf::Magnitude => output.abs_sqr() as i32 >> 16,
|
||||||
Conf::Phase => output.arg() >> 16,
|
Conf::Phase => output.arg() >> 16,
|
||||||
|
@ -366,7 +384,10 @@ const APP: () = {
|
||||||
}
|
}
|
||||||
Conf::InPhase => output.re >> 16,
|
Conf::InPhase => output.re >> 16,
|
||||||
Conf::Quadrature => output.im >> 16,
|
Conf::Quadrature => output.im >> 16,
|
||||||
Conf::Modulation => DAC_SEQUENCE[i] as i32,
|
|
||||||
|
Conf::Modulation => {
|
||||||
|
signal_generator.next().unwrap() as i32
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
*sample = DacCode::from(value as i16).0;
|
*sample = DacCode::from(value as i16).0;
|
||||||
|
|
|
@ -83,18 +83,45 @@ use hal::dma::{
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
pub struct AdcCode(pub u16);
|
pub struct AdcCode(pub u16);
|
||||||
|
|
||||||
#[allow(clippy::from_over_into)]
|
impl From<u16> for AdcCode {
|
||||||
impl Into<f32> for AdcCode {
|
/// Construct an ADC code from a provided binary (ADC-formatted) code.
|
||||||
|
fn from(value: u16) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<i16> for AdcCode {
|
||||||
|
/// Construct an ADC code from the stabilizer-defined code (i16 full range).
|
||||||
|
fn from(value: i16) -> Self {
|
||||||
|
Self(value as u16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AdcCode> for i16 {
|
||||||
|
/// Get a stabilizer-defined code from the ADC code.
|
||||||
|
fn from(code: AdcCode) -> i16 {
|
||||||
|
code.0 as i16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AdcCode> for u16 {
|
||||||
|
/// Get an ADC-frmatted binary value from the code.
|
||||||
|
fn from(code: AdcCode) -> u16 {
|
||||||
|
code.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AdcCode> for f32 {
|
||||||
/// Convert raw ADC codes to/from voltage levels.
|
/// Convert raw ADC codes to/from voltage levels.
|
||||||
///
|
///
|
||||||
/// # Note
|
/// # Note
|
||||||
/// This does not account for the programmable gain amplifier at the signal input.
|
/// This does not account for the programmable gain amplifier at the signal input.
|
||||||
fn into(self) -> f32 {
|
fn from(code: AdcCode) -> f32 {
|
||||||
// The ADC has a differential input with a range of +/- 4.096 V and 16-bit resolution.
|
// The ADC has a differential input with a range of +/- 4.096 V and 16-bit resolution.
|
||||||
// The gain into the two inputs is 1/5.
|
// The gain into the two inputs is 1/5.
|
||||||
let adc_volts_per_lsb = 5.0 / 2.0 * 4.096 / (1u16 << 15) as f32;
|
let adc_volts_per_lsb = 5.0 / 2.0 * 4.096 / (1u16 << 15) as f32;
|
||||||
|
|
||||||
(self.0 as i16) as f32 * adc_volts_per_lsb
|
i16::from(code) as f32 * adc_volts_per_lsb
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,8 @@ use mutex_trait::Mutex;
|
||||||
use super::design_parameters::{SampleBuffer, SAMPLE_BUFFER_SIZE};
|
use super::design_parameters::{SampleBuffer, SAMPLE_BUFFER_SIZE};
|
||||||
use super::timers;
|
use super::timers;
|
||||||
|
|
||||||
|
use core::convert::TryFrom;
|
||||||
|
|
||||||
use hal::dma::{
|
use hal::dma::{
|
||||||
dma::{DMAReq, DmaConfig},
|
dma::{DMAReq, DmaConfig},
|
||||||
traits::TargetAddress,
|
traits::TargetAddress,
|
||||||
|
@ -75,14 +77,37 @@ static mut DAC_BUF: [[SampleBuffer; 2]; 2] = [[[0; SAMPLE_BUFFER_SIZE]; 2]; 2];
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
pub struct DacCode(pub u16);
|
pub struct DacCode(pub u16);
|
||||||
|
|
||||||
#[allow(clippy::from_over_into)]
|
impl TryFrom<f32> for DacCode {
|
||||||
impl Into<f32> for DacCode {
|
type Error = ();
|
||||||
fn into(self) -> f32 {
|
|
||||||
|
fn try_from(voltage: f32) -> Result<DacCode, ()> {
|
||||||
|
// The DAC output range in bipolar mode (including the external output op-amp) is +/- 4.096
|
||||||
|
// V with 16-bit resolution. The anti-aliasing filter has an additional gain of 2.5.
|
||||||
|
let dac_range = 4.096 * 2.5;
|
||||||
|
|
||||||
|
if voltage > dac_range || voltage < -1. * dac_range {
|
||||||
|
Err(())
|
||||||
|
} else {
|
||||||
|
Ok(DacCode::from(
|
||||||
|
(voltage * (i16::MAX as f32 / dac_range)) as i16,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DacCode> for f32 {
|
||||||
|
fn from(code: DacCode) -> f32 {
|
||||||
// The DAC output range in bipolar mode (including the external output op-amp) is +/- 4.096
|
// The DAC output range in bipolar mode (including the external output op-amp) is +/- 4.096
|
||||||
// V with 16-bit resolution. The anti-aliasing filter has an additional gain of 2.5.
|
// V with 16-bit resolution. The anti-aliasing filter has an additional gain of 2.5.
|
||||||
let dac_volts_per_lsb = 4.096 * 2.5 / (1u16 << 15) as f32;
|
let dac_volts_per_lsb = 4.096 * 2.5 / (1u16 << 15) as f32;
|
||||||
|
|
||||||
(self.0 as i16).wrapping_add(i16::MIN) as f32 * dac_volts_per_lsb
|
(code.0 as i16).wrapping_add(i16::MIN) as f32 * dac_volts_per_lsb
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DacCode> for i16 {
|
||||||
|
fn from(code: DacCode) -> i16 {
|
||||||
|
(code.0 as i16).wrapping_sub(i16::MIN)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,6 +118,13 @@ impl From<i16> for DacCode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<u16> for DacCode {
|
||||||
|
/// Create a dac code from the provided DAC output code.
|
||||||
|
fn from(value: u16) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
macro_rules! dac_output {
|
macro_rules! dac_output {
|
||||||
($name:ident, $index:literal, $data_stream:ident,
|
($name:ident, $index:literal, $data_stream:ident,
|
||||||
$spi:ident, $trigger_channel:ident, $dma_req:ident) => {
|
$spi:ident, $trigger_channel:ident, $dma_req:ident) => {
|
||||||
|
|
|
@ -10,6 +10,7 @@ pub mod design_parameters;
|
||||||
pub mod input_stamper;
|
pub mod input_stamper;
|
||||||
pub mod pounder;
|
pub mod pounder;
|
||||||
pub mod setup;
|
pub mod setup;
|
||||||
|
pub mod signal_generator;
|
||||||
pub mod system_timer;
|
pub mod system_timer;
|
||||||
|
|
||||||
mod eeprom;
|
mod eeprom;
|
||||||
|
|
|
@ -0,0 +1,170 @@
|
||||||
|
use crate::{configuration::ADC_SAMPLE_TICKS_LOG2, hardware::dac::DacCode};
|
||||||
|
use core::convert::{TryFrom, TryInto};
|
||||||
|
use miniconf::Miniconf;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
/// Types of signals that can be generated.
|
||||||
|
#[derive(Copy, Clone, Debug, Deserialize, Miniconf)]
|
||||||
|
pub enum Signal {
|
||||||
|
Cosine,
|
||||||
|
Square,
|
||||||
|
Triangle,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Basic configuration for a generated signal.
|
||||||
|
///
|
||||||
|
/// # Miniconf
|
||||||
|
/// `{"signal": <signal>, "frequency", 1000.0, "symmetry": 0.5, "amplitude": 1.0}`
|
||||||
|
///
|
||||||
|
/// Where `<signal>` may be any of [Signal] variants, `frequency` specifies the signal frequency
|
||||||
|
/// in Hertz, `symmetry` specifies the normalized signal symmetry which ranges from 0 - 1.0, and
|
||||||
|
/// `amplitude` specifies the signal amplitude in Volts.
|
||||||
|
#[derive(Copy, Clone, Debug, Miniconf, Deserialize)]
|
||||||
|
pub struct BasicConfig {
|
||||||
|
/// The signal type that should be generated. See [Signal] variants.
|
||||||
|
pub signal: Signal,
|
||||||
|
|
||||||
|
/// The frequency of the generated signal in Hertz.
|
||||||
|
pub frequency: f32,
|
||||||
|
|
||||||
|
/// The normalized symmetry of the signal. At 0% symmetry, the duration of the first half oscillation is minimal.
|
||||||
|
/// At 25% symmetry, the first half oscillation lasts for 25% of the signal period. For square wave output this
|
||||||
|
//// symmetry is the duty cycle.
|
||||||
|
pub symmetry: f32,
|
||||||
|
|
||||||
|
/// The amplitude of the output signal in volts.
|
||||||
|
pub amplitude: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BasicConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
frequency: 1.0e3,
|
||||||
|
symmetry: 0.5,
|
||||||
|
signal: Signal::Cosine,
|
||||||
|
amplitude: 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the errors that can occur when attempting to configure the signal generator.
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
/// The provided amplitude is out-of-range.
|
||||||
|
InvalidAmplitude,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<BasicConfig> for Config {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(config: BasicConfig) -> Result<Config, Error> {
|
||||||
|
// Calculate the frequency tuning words
|
||||||
|
let frequency_tuning_word: [u32; 2] = {
|
||||||
|
const LSB_PER_HERTZ: f32 =
|
||||||
|
(1u64 << (31 + ADC_SAMPLE_TICKS_LOG2)) as f32 / 100.0e6;
|
||||||
|
let ftw = config.frequency * LSB_PER_HERTZ;
|
||||||
|
|
||||||
|
if config.symmetry <= 0.0 {
|
||||||
|
[1u32 << 31, ftw as u32]
|
||||||
|
} else if config.symmetry >= 1.0 {
|
||||||
|
[ftw as u32, 1u32 << 31]
|
||||||
|
} else {
|
||||||
|
[
|
||||||
|
(ftw / config.symmetry) as u32,
|
||||||
|
(ftw / (1.0 - config.symmetry)) as u32,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Config {
|
||||||
|
amplitude: DacCode::try_from(config.amplitude)
|
||||||
|
.or(Err(Error::InvalidAmplitude))?
|
||||||
|
.into(),
|
||||||
|
signal: config.signal,
|
||||||
|
frequency_tuning_word,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub struct Config {
|
||||||
|
/// The type of signal being generated
|
||||||
|
pub signal: Signal,
|
||||||
|
|
||||||
|
/// 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_tuning_word: [u32; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SignalGenerator {
|
||||||
|
phase_accumulator: u32,
|
||||||
|
config: Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SignalGenerator {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
config: BasicConfig::default().try_into().unwrap(),
|
||||||
|
phase_accumulator: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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: Config) -> Self {
|
||||||
|
Self {
|
||||||
|
config,
|
||||||
|
phase_accumulator: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update waveform generation settings.
|
||||||
|
pub fn update_waveform(&mut self, new_config: Config) {
|
||||||
|
self.config = new_config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::iter::Iterator for SignalGenerator {
|
||||||
|
type Item = i16;
|
||||||
|
|
||||||
|
/// Get the next value in the generator sequence.
|
||||||
|
fn next(&mut self) -> Option<i16> {
|
||||||
|
self.phase_accumulator = self.phase_accumulator.wrapping_add(
|
||||||
|
if (self.phase_accumulator as i32).is_negative() {
|
||||||
|
self.config.frequency_tuning_word[0]
|
||||||
|
} else {
|
||||||
|
self.config.frequency_tuning_word[1]
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let phase = self.phase_accumulator as i32;
|
||||||
|
|
||||||
|
let amplitude: i16 = match self.config.signal {
|
||||||
|
Signal::Cosine => (dsp::cossin(phase).0 >> 16) as i16,
|
||||||
|
Signal::Square => {
|
||||||
|
if phase.is_negative() {
|
||||||
|
i16::MAX
|
||||||
|
} else {
|
||||||
|
-i16::MAX
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Signal::Triangle => i16::MAX - (phase.abs() >> 15) 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 + (1 << 14)) >> 15) as i16)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue