diff --git a/src/bin/dual-iir.rs b/src/bin/dual-iir.rs index d0aca33..39cdefe 100644 --- a/src/bin/dual-iir.rs +++ b/src/bin/dual-iir.rs @@ -29,7 +29,10 @@ #![no_std] #![no_main] -use core::sync::atomic::{fence, Ordering}; +use core::{ + convert::TryInto, + sync::atomic::{fence, Ordering}, +}; use mutex_trait::prelude::*; @@ -42,6 +45,7 @@ use stabilizer::{ dac::{Dac0Output, Dac1Output, DacCode}, embedded_hal::digital::v2::InputPin, hal, + signal_generator::{self, SignalGenerator}, system_timer::SystemTimer, DigitalInput0, DigitalInput1, AFE0, AFE1, }, @@ -119,6 +123,17 @@ pub struct Settings { /// # Value /// See [StreamTarget#miniconf] stream_target: StreamTarget, + + /// Specifies the config for signal generators to add on to DAC0/DAC1 outputs. + /// + /// # Path + /// `signal_generator/` + /// + /// * specifies which channel to configure. := [0, 1] + /// + /// # Value + /// See [signal_generator::BasicConfig#miniconf] + signal_generator: [signal_generator::BasicConfig; 2], } impl Default for Settings { @@ -139,6 +154,8 @@ impl Default for Settings { // The default telemetry period in seconds. telemetry_period: 10, + signal_generator: [signal_generator::BasicConfig::default(); 2], + stream_target: StreamTarget::default(), } } @@ -153,6 +170,7 @@ const APP: () = { dacs: (Dac0Output, Dac1Output), network: NetworkUsers, generator: BlockGenerator, + signal_generator: [SignalGenerator; 2], settings: Settings, telemetry: TelemetryBuffer, @@ -193,6 +211,8 @@ const APP: () = { // Start sampling ADCs. stabilizer.adc_dac_timer.start(); + let settings = Settings::default(); + init::LateResources { afes: stabilizer.afes, adcs: stabilizer.adcs, @@ -201,7 +221,15 @@ const APP: () = { network, digital_inputs: stabilizer.digital_inputs, 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 /// 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)] #[link_section = ".itcm.process"] fn process(mut c: process::Context) { @@ -233,6 +261,7 @@ const APP: () = { ref mut iir_state, ref mut telemetry, ref mut generator, + ref mut signal_generator, } = c.resources; let digital_inputs = [ @@ -255,7 +284,8 @@ const APP: () = { adc_samples[channel] .iter() .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 y = settings.iir_ch[channel] .iter() @@ -263,9 +293,13 @@ const APP: () = { .fold(x, |yi, (ch, state)| { ch.update(state, yi, hold) }); + // Note(unsafe): The filter limits must ensure that the value is in range. // The truncation introduces 1/2 LSB distortion. let y: i16 = unsafe { y.to_int_unchecked() }; + + let y = y.saturating_add(signal); + // Convert to DAC code *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) { // Update the IIR channels. 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.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(); c.resources.network.direct_stream(target); } diff --git a/src/bin/lockin.rs b/src/bin/lockin.rs index 33332d4..af7a747 100644 --- a/src/bin/lockin.rs +++ b/src/bin/lockin.rs @@ -28,7 +28,10 @@ #![no_std] #![no_main] -use core::sync::atomic::{fence, Ordering}; +use core::{ + convert::TryFrom, + sync::atomic::{fence, Ordering}, +}; use mutex_trait::prelude::*; @@ -40,10 +43,10 @@ use stabilizer::{ adc::{Adc0Input, Adc1Input, AdcCode}, afe::Gain, dac::{Dac0Output, Dac1Output, DacCode}, - design_parameters, embedded_hal::digital::v2::InputPin, hal, input_stamper::InputStamper, + signal_generator, system_timer::SystemTimer, 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)] enum Conf { /// Output the lockin magnitude. @@ -213,6 +209,7 @@ const APP: () = { telemetry: TelemetryBuffer, digital_inputs: (DigitalInput0, DigitalInput1), generator: BlockGenerator, + signal_generator: signal_generator::SignalGenerator, timestamper: InputStamper, pll: RPLL, @@ -264,6 +261,23 @@ const APP: () = { // Enable the timestamper. 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 { afes: stabilizer.afes, adcs: stabilizer.adcs, @@ -272,6 +286,9 @@ const APP: () = { digital_inputs: stabilizer.digital_inputs, timestamper: stabilizer.timestamper, telemetry: TelemetryBuffer::default(), + signal_generator: signal_generator::SignalGenerator::new( + signal_config, + ), settings, generator, @@ -288,7 +305,7 @@ const APP: () = { /// 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. /// 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)] #[link_section = ".itcm.process"] fn process(mut c: process::Context) { @@ -301,6 +318,7 @@ const APP: () = { ref mut pll, ref mut timestamper, ref mut generator, + ref mut signal_generator, } = c.resources; let (reference_phase, reference_frequency) = match settings.lockin_mode @@ -356,7 +374,7 @@ const APP: () = { // Convert to DAC data. 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] { Conf::Magnitude => output.abs_sqr() as i32 >> 16, Conf::Phase => output.arg() >> 16, @@ -366,7 +384,10 @@ const APP: () = { } Conf::InPhase => output.re >> 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; diff --git a/src/hardware/adc.rs b/src/hardware/adc.rs index 9b1a833..fd131ee 100644 --- a/src/hardware/adc.rs +++ b/src/hardware/adc.rs @@ -83,18 +83,45 @@ use hal::dma::{ #[derive(Copy, Clone)] pub struct AdcCode(pub u16); -#[allow(clippy::from_over_into)] -impl Into for AdcCode { +impl From for AdcCode { + /// Construct an ADC code from a provided binary (ADC-formatted) code. + fn from(value: u16) -> Self { + Self(value) + } +} + +impl From 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 for i16 { + /// Get a stabilizer-defined code from the ADC code. + fn from(code: AdcCode) -> i16 { + code.0 as i16 + } +} + +impl From for u16 { + /// Get an ADC-frmatted binary value from the code. + fn from(code: AdcCode) -> u16 { + code.0 + } +} + +impl From for f32 { /// Convert raw ADC codes to/from voltage levels. /// /// # Note /// 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 gain into the two inputs is 1/5. 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 } } diff --git a/src/hardware/dac.rs b/src/hardware/dac.rs index 80913cd..b5391c6 100644 --- a/src/hardware/dac.rs +++ b/src/hardware/dac.rs @@ -57,6 +57,8 @@ use mutex_trait::Mutex; use super::design_parameters::{SampleBuffer, SAMPLE_BUFFER_SIZE}; use super::timers; +use core::convert::TryFrom; + use hal::dma::{ dma::{DMAReq, DmaConfig}, traits::TargetAddress, @@ -75,14 +77,37 @@ static mut DAC_BUF: [[SampleBuffer; 2]; 2] = [[[0; SAMPLE_BUFFER_SIZE]; 2]; 2]; #[derive(Copy, Clone)] pub struct DacCode(pub u16); -#[allow(clippy::from_over_into)] -impl Into for DacCode { - fn into(self) -> f32 { +impl TryFrom for DacCode { + type Error = (); + + fn try_from(voltage: f32) -> Result { + // 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 for f32 { + fn from(code: DacCode) -> f32 { // 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_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 for i16 { + fn from(code: DacCode) -> i16 { + (code.0 as i16).wrapping_sub(i16::MIN) } } @@ -93,6 +118,13 @@ impl From for DacCode { } } +impl From for DacCode { + /// Create a dac code from the provided DAC output code. + fn from(value: u16) -> Self { + Self(value) + } +} + macro_rules! dac_output { ($name:ident, $index:literal, $data_stream:ident, $spi:ident, $trigger_channel:ident, $dma_req:ident) => { diff --git a/src/hardware/mod.rs b/src/hardware/mod.rs index 422f7b9..07f1b3d 100644 --- a/src/hardware/mod.rs +++ b/src/hardware/mod.rs @@ -10,6 +10,7 @@ pub mod design_parameters; pub mod input_stamper; pub mod pounder; pub mod setup; +pub mod signal_generator; pub mod system_timer; mod eeprom; diff --git a/src/hardware/signal_generator.rs b/src/hardware/signal_generator.rs new file mode 100644 index 0000000..2eade09 --- /dev/null +++ b/src/hardware/signal_generator.rs @@ -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": , "frequency", 1000.0, "symmetry": 0.5, "amplitude": 1.0}` +/// +/// Where `` 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 for Config { + type Error = Error; + + fn try_from(config: BasicConfig) -> Result { + // 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 { + 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) + } +}