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>
master
bors[bot] 2021-07-19 14:37:11 +00:00 committed by GitHub
commit 929a7611d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 326 additions and 25 deletions

View File

@ -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/<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 {
@ -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<Settings, Telemetry>,
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);
}

View File

@ -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;

View File

@ -83,18 +83,45 @@ use hal::dma::{
#[derive(Copy, Clone)]
pub struct AdcCode(pub u16);
#[allow(clippy::from_over_into)]
impl Into<f32> for AdcCode {
impl From<u16> 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.
///
/// # 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
}
}

View File

@ -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<f32> for DacCode {
fn into(self) -> f32 {
impl TryFrom<f32> for DacCode {
type Error = ();
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
// 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<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 {
($name:ident, $index:literal, $data_stream:ident,
$spi:ident, $trigger_channel:ident, $dma_req:ident) => {

View File

@ -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;

View File

@ -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)
}
}