Adding adc/dac code conversion utilities
This commit is contained in:
parent
d68fa87fec
commit
b73a4d9e59
@ -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::<i16>() };
|
||||
// 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;
|
||||
}
|
||||
|
@ -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])]
|
||||
|
@ -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<f32> 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.
|
||||
|
@ -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<f32> 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<i16> 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) => {
|
||||
|
@ -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;
|
||||
|
@ -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<T: Serialize> {
|
||||
@ -33,9 +35,9 @@ pub struct TelemetryClient<T: Serialize> {
|
||||
#[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::<f32>::into(self.adcs[0]) / afe0.as_multiplier();
|
||||
let in1_volts = Into::<f32>::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,
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user