From b73a4d9e5974d53db921759bf3370bbe8b9921eb Mon Sep 17 00:00:00 2001 From: Ryan Summers Date: Fri, 7 May 2021 13:50:34 +0200 Subject: [PATCH] Adding adc/dac code conversion utilities --- src/bin/dual-iir.rs | 11 ++++++----- src/bin/lockin.rs | 13 +++++++------ src/hardware/adc.rs | 22 ++++++++++++++++++++++ src/hardware/dac.rs | 33 +++++++++++++++++++++++++++++++++ src/hardware/mod.rs | 4 ++-- src/net/telemetry.rs | 42 ++++++++++-------------------------------- 6 files changed, 80 insertions(+), 45 deletions(-) diff --git a/src/bin/dual-iir.rs b/src/bin/dual-iir.rs index e952ae6..2820184 100644 --- a/src/bin/dual-iir.rs +++ b/src/bin/dual-iir.rs @@ -9,8 +9,8 @@ use serde::Deserialize; use dsp::iir; use hardware::{ - Adc0Input, Adc1Input, AfeGain, Dac0Output, Dac1Output, DigitalInput0, - DigitalInput1, InputPin, SystemTimer, AFE0, AFE1, + Adc0Input, Adc1Input, AdcSample, AfeGain, Dac0Output, Dac1Output, DacCode, + DigitalInput0, DigitalInput1, InputPin, SystemTimer, AFE0, AFE1, }; use net::{NetworkUsers, Telemetry, TelemetryBuffer, UpdateState}; @@ -154,15 +154,16 @@ const APP: () = { // The truncation introduces 1/2 LSB distortion. let y = unsafe { y.to_int_unchecked::() }; // Convert to DAC code - dac_samples[channel][sample] = y as u16 ^ 0x8000; + dac_samples[channel][sample] = DacCode::from(y).0; } } // Update telemetry measurements. c.resources.telemetry.adcs = - [adc_samples[0][0] as i16, adc_samples[1][0] as i16]; + [AdcSample(adc_samples[0][0]), AdcSample(adc_samples[1][0])]; - c.resources.telemetry.dacs = [dac_samples[0][0], dac_samples[1][0]]; + c.resources.telemetry.dacs = + [DacCode(dac_samples[0][0]), DacCode(dac_samples[1][0])]; c.resources.telemetry.digital_inputs = digital_inputs; } diff --git a/src/bin/lockin.rs b/src/bin/lockin.rs index 73c3a92..f4f86d7 100644 --- a/src/bin/lockin.rs +++ b/src/bin/lockin.rs @@ -12,9 +12,9 @@ use dsp::{Accu, Complex, ComplexExt, Lockin, RPLL}; use stabilizer::net; use stabilizer::hardware::{ - design_parameters, setup, Adc0Input, Adc1Input, AfeGain, Dac0Output, - Dac1Output, DigitalInput0, DigitalInput1, InputStamper, SystemTimer, AFE0, - AFE1, + design_parameters, setup, Adc0Input, Adc1Input, AdcSample, AfeGain, + Dac0Output, Dac1Output, DacCode, DigitalInput0, DigitalInput1, + InputStamper, SystemTimer, AFE0, AFE1, }; use miniconf::Miniconf; @@ -234,15 +234,16 @@ const APP: () = { Conf::Modulation => DAC_SEQUENCE[i] as i32, }; - *sample = value as u16 ^ 0x8000; + *sample = DacCode::from(value as i16).0; } } // Update telemetry measurements. c.resources.telemetry.adcs = - [adc_samples[0][0] as i16, adc_samples[1][0] as i16]; + [AdcSample(adc_samples[0][0]), AdcSample(adc_samples[1][0])]; - c.resources.telemetry.dacs = [dac_samples[0][0], dac_samples[1][0]]; + c.resources.telemetry.dacs = + [DacCode(dac_samples[0][0]), DacCode(dac_samples[1][0])]; } #[idle(resources=[network], spawn=[settings_update])] diff --git a/src/hardware/adc.rs b/src/hardware/adc.rs index 3f2fa5d..081d801 100644 --- a/src/hardware/adc.rs +++ b/src/hardware/adc.rs @@ -84,6 +84,28 @@ use hal::dma::{ MemoryToPeripheral, PeripheralToMemory, Transfer, }; +/// A type representing an ADC sample. +#[derive(Copy, Clone)] +pub struct AdcSample(pub u16); + +impl Into for AdcSample { + /// 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 { + // The input voltage is measured by the ADC across a dynamic scale of +/- 4.096 V with a + // dynamic range across signed integers. Additionally, the signal is fully differential, so + // the differential voltage is measured at the ADC. Thus, the single-ended signal is + // measured at the input is half of the ADC-reported measurement. As a pre-filter, the + // input signal has a fixed gain of 1/5 through a static input active filter. + let adc_volts_per_lsb = 5.0 / 2.0 * 4.096 / i16::MAX as f32; + + (self.0 as i16) as f32 * adc_volts_per_lsb + } +} + // The following data is written by the timer ADC sample trigger into the SPI CR1 to start the // transfer. Data in AXI SRAM is not initialized on boot, so the contents are random. This value is // initialized during setup. diff --git a/src/hardware/dac.rs b/src/hardware/dac.rs index d41ae8c..e4e5947 100644 --- a/src/hardware/dac.rs +++ b/src/hardware/dac.rs @@ -69,6 +69,39 @@ use hal::dma::{ static mut DAC_BUF: [[[u16; SAMPLE_BUFFER_SIZE]; 3]; 2] = [[[0; SAMPLE_BUFFER_SIZE]; 3]; 2]; +/// Custom type for referencing DAC output codes. +/// The internal integer is the raw code written to the DAC output register. +#[derive(Copy, Clone)] +pub struct DacCode(pub u16); + +impl Into for DacCode { + fn into(self) -> f32 { + // The output voltage is generated by the DAC with an output range of +/- 4.096 V. This + // signal then passes through a 2.5x gain stage. Note that the DAC operates using unsigned + // integers, and u16::MAX / 2 is considered zero voltage output. Thus, the dynamic range of + // the output stage is +/- 10.24 V. At a DAC code of zero, there is an output of -10.24 V, + // and at a max DAC code, there is an output of 10.24 V. + // + // Note: The MAX code corresponding to +VREF is not programmable, as it is 1 bit larger + // than full-scale. + let dac_volts_per_lsb = 10.24 * 2.0 / (u16::MAX as u32 + 1) as f32; + + (self.0 as f32) * dac_volts_per_lsb - 10.24 + } +} + +impl From for DacCode { + /// Generate a DAC code from a 16-bit signed value. + /// + /// # Note + /// This is provided as a means to convert between the DACs internal 16-bit, unsigned register + /// and a 2s-complement integer that is convenient when using the DAC in the bipolar + /// configuration. + fn from(value: i16) -> Self { + Self(value as u16 ^ 0x8000) + } +} + 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 a439642..a741cb3 100644 --- a/src/hardware/mod.rs +++ b/src/hardware/mod.rs @@ -19,10 +19,10 @@ pub mod pounder; mod system_timer; mod timers; -pub use adc::{Adc0Input, Adc1Input}; +pub use adc::{Adc0Input, Adc1Input, AdcSample}; pub use afe::Gain as AfeGain; pub use cycle_counter::CycleCounter; -pub use dac::{Dac0Output, Dac1Output}; +pub use dac::{Dac0Output, Dac1Output, DacCode}; pub use digital_input_stamper::InputStamper; pub use pounder::DdsOutput; pub use system_timer::SystemTimer; diff --git a/src/net/telemetry.rs b/src/net/telemetry.rs index cf14f4f..974e0eb 100644 --- a/src/net/telemetry.rs +++ b/src/net/telemetry.rs @@ -15,7 +15,9 @@ use minimq::QoS; use serde::Serialize; use super::NetworkReference; -use crate::hardware::{design_parameters::MQTT_BROKER, AfeGain}; +use crate::hardware::{ + design_parameters::MQTT_BROKER, AdcSample, AfeGain, DacCode, +}; /// The telemetry client for reporting telemetry data over MQTT. pub struct TelemetryClient { @@ -33,9 +35,9 @@ pub struct TelemetryClient { #[derive(Copy, Clone)] pub struct TelemetryBuffer { /// The latest input sample on ADC0/ADC1. - pub adcs: [i16; 2], + pub adcs: [AdcSample; 2], /// The latest output code on DAC0/DAC1. - pub dacs: [u16; 2], + pub dacs: [DacCode; 2], /// The latest digital input states during processing. pub digital_inputs: [bool; 2], } @@ -55,8 +57,8 @@ pub struct Telemetry { impl Default for TelemetryBuffer { fn default() -> Self { Self { - adcs: [0, 0], - dacs: [0, 0], + adcs: [AdcSample(0), AdcSample(0)], + dacs: [DacCode(0), DacCode(0)], digital_inputs: [false, false], } } @@ -72,36 +74,12 @@ impl TelemetryBuffer { /// # Returns /// The finalized telemetry structure that can be serialized and reported. pub fn finalize(self, afe0: AfeGain, afe1: AfeGain) -> Telemetry { - // The input voltage is measured by the ADC across a dynamic scale of +/- 4.096 V with a - // dynamic range across signed integers. Additionally, the signal is fully differential, so - // the differential voltage is measured at the ADC. Thus, the single-ended signal is - // measured at the input is half of the ADC-reported measurement. As a pre-filter, the - // input signal has a fixed gain of 1/5 through a static input active filter. Finally, at - // the very front-end of the signal, there's an analog input multiplier that is - // configurable by the user. - let adc_volts_per_lsb = 5.0 * 4.096 / 2.0 / i16::MAX as f32; - let in0_volts = - (adc_volts_per_lsb * self.adcs[0] as f32) / afe0.as_multiplier(); - let in1_volts = - (adc_volts_per_lsb * self.adcs[1] as f32) / afe1.as_multiplier(); - - // The output voltage is generated by the DAC with an output range of +/- 4.096 V. This - // signal then passes through a 2.5x gain stage. Note that the DAC operates using unsigned - // integers, and u16::MAX / 2 is considered zero voltage output. Thus, the dynamic range of - // the output stage is +/- 10.24 V. At a DAC code of zero, there is an output of -10.24 V, - // and at a max DAC code, there is an output of 10.24 V. - // - // Note: The MAX code corresponding to +VREF is not programmable, as it is 1 bit larger - // than full-scale. - let dac_volts_per_lsb = 10.24 * 2.0 / (u16::MAX as u32 + 1) as f32; - let dac_offset = -10.24; - - let out0_volts = dac_volts_per_lsb * self.dacs[0] as f32 + dac_offset; - let out1_volts = dac_volts_per_lsb * self.dacs[1] as f32 + dac_offset; + let in0_volts = Into::::into(self.adcs[0]) / afe0.as_multiplier(); + let in1_volts = Into::::into(self.adcs[1]) / afe1.as_multiplier(); Telemetry { adcs: [in0_volts, in1_volts], - dacs: [out0_volts, out1_volts], + dacs: [self.dacs[0].into(), self.dacs[1].into()], digital_inputs: self.digital_inputs, } }