From 986e7cc45790eec28e29ed387b722f4c8466e772 Mon Sep 17 00:00:00 2001 From: Ryan Summers Date: Mon, 21 Jun 2021 16:59:38 +0200 Subject: [PATCH] Adding initial take at scan mode signal generation --- src/bin/lockin.rs | 28 +++-- src/hardware/mod.rs | 1 + src/hardware/signal_generator.rs | 178 +++++++++++++++++++++++++++++++ 3 files changed, 197 insertions(+), 10 deletions(-) create mode 100644 src/hardware/signal_generator.rs diff --git a/src/bin/lockin.rs b/src/bin/lockin.rs index bc1d39a..24a18be 100644 --- a/src/bin/lockin.rs +++ b/src/bin/lockin.rs @@ -16,6 +16,7 @@ use stabilizer::{ embedded_hal::digital::v2::InputPin, hal, input_stamper::InputStamper, + signal_generator, system_timer::SystemTimer, DigitalInput0, DigitalInput1, AFE0, AFE1, }, @@ -28,13 +29,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 { Magnitude, @@ -102,6 +96,7 @@ const APP: () = { telemetry: TelemetryBuffer, digital_inputs: (DigitalInput0, DigitalInput1), generator: BlockGenerator, + signal_generator: signal_generator::Generator, timestamper: InputStamper, pll: RPLL, @@ -161,6 +156,14 @@ const APP: () = { digital_inputs: stabilizer.digital_inputs, timestamper: stabilizer.timestamper, telemetry: TelemetryBuffer::default(), + signal_generator: signal_generator::Generator::new( + signal_generator::Config { + period: design_parameters::SAMPLE_BUFFER_SIZE as u32, + symmetry: 0.5, + amplitude: 1.0, + signal: signal_generator::Signal::Triangle, + }, + ), settings, generator, @@ -177,7 +180,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) { @@ -190,6 +193,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 @@ -246,7 +250,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, @@ -256,7 +260,11 @@ const APP: () = { } Conf::InPhase => output.re >> 16, Conf::Quadrature => output.im >> 16, - Conf::Modulation => DAC_SEQUENCE[i] as i32, + + // Note: Because the signal generator has a period equal to one batch size, + // it's okay to only update it when outputting the modulation waveform, as + // it will perfectly wrap back to zero phase for each batch. + Conf::Modulation => signal_generator.next() as i32, }; *sample = DacCode::from(value as i16).0; 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..52aa341 --- /dev/null +++ b/src/hardware/signal_generator.rs @@ -0,0 +1,178 @@ +#[derive(Copy, Clone, Debug)] +pub enum Signal { + Sine, + Square, + Triangle, +} + +#[derive(Copy, Clone, Debug)] +pub struct Config { + // TODO: Should period be specified in Hz? + pub period: u32, + pub symmetry: f32, + pub signal: Signal, + pub amplitude: f32, +} + +impl Default for Config { + fn default() -> Self { + Self { + period: 10, + symmetry: 0.5, + signal: Signal::Sine, + amplitude: 1.0, + } + } +} + +impl Into for Config { + fn into(self) -> InternalConf { + // Clamp amplitude and symmetry. + let amplitude = if self.amplitude > 10.24 { + 10.24 + } else { + self.amplitude + }; + + let symmetry = if self.symmetry < 0.0 { + 0.0 + } else if self.symmetry > 1.0 { + 1.0 + } else { + self.symmetry + }; + + InternalConf { + signal: self.signal, + period: self.period, + amplitude: ((amplitude / 10.24) * i16::MAX as f32) as i16, + phase_symmetry: ((symmetry - 0.5) * i32::MAX as f32) as i32, + } + } +} + +#[derive(Copy, Clone, Debug)] +struct InternalConf { + period: u32, + signal: Signal, + amplitude: i16, + + // The 32-bit representation of the phase symmetry. That is, with a 50% symmetry, this is equal + // to 0. + phase_symmetry: i32, +} + +#[derive(Debug)] +pub struct Generator { + index: u32, + config: InternalConf, + pending_config: Option, +} + +impl Default for Generator { + fn default() -> Self { + Self { + config: Config::default().into(), + index: 0, + pending_config: None, + } + } +} + +impl Generator { + /// 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: config.into(), + pending_config: None, + index: 0, + } + } + + /// Generate a sequence of new values. + /// + /// # Args + /// * `samples` - The location to store generated values into. + pub fn generate(&mut self, samples: &mut [i16]) { + for sample in samples.iter_mut() { + *sample = self.next(); + } + } + + /// Get the next value in the generator sequence. + pub fn next(&mut self) -> i16 { + // When phase wraps, apply any new settings. + if self.pending_config.is_some() && self.index == 0 { + self.config = self.pending_config.take().unwrap(); + } + + let phase_step = u32::MAX / self.config.period; + + // Note: We allow phase to silently wrap here intentionally, as it will wrap to negative. + // This is acceptable with phase, since it is perfectly periodic. + let phase = (self.index * phase_step) as i32; + + let amplitude = match self.config.signal { + Signal::Sine => (dsp::cossin(phase).1 >> 16) as i16, + Signal::Square => { + if phase < self.config.phase_symmetry { + i16::MAX + } else { + i16::MIN + } + } + Signal::Triangle => { + if phase < self.config.phase_symmetry { + let rise_period: u32 = + (self.config.phase_symmetry - i32::MIN) as u32 + / phase_step; + + if rise_period == 0 { + i16::MIN + } else { + i16::MIN + + (self.index * u16::MAX as u32 / rise_period) + as i16 + } + } else { + let fall_period: u32 = (i32::MAX as u32 + - self.config.phase_symmetry as u32) + / phase_step; + let index: u32 = (phase - self.config.phase_symmetry) + as u32 + / phase_step; + + if fall_period == 0 { + i16::MAX + } else { + i16::MAX + - (index * u16::MAX as u32 / fall_period) as i16 + } + } + } + }; + + // Update the current index. + self.index = (self.index + 1) % self.config.period; + + // 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. + (result >> 15) as i16 + } + + /// Update waveform generation settings. + /// + /// # Note + /// Changes will not take effect until the current waveform period elapses. + pub fn update_waveform(&mut self, new_config: Config) { + self.pending_config = Some(new_config.into()); + } +}