diff --git a/mcu-contrib/default.nix b/mcu-contrib/default.nix index c0d5fec..cac0e9e 100644 --- a/mcu-contrib/default.nix +++ b/mcu-contrib/default.nix @@ -64,6 +64,7 @@ in cargoDepsName = "stabilizer"; src = ; patchPhase = '' + patch -p1 < ${./pounder-725.diff} substituteInPlace src/net/mod.rs \ --replace "[10, 34, 16, 10];" \ "[192, 168, 1, 10];" # or other default MQTT broker address diff --git a/mcu-contrib/pounder-725.diff b/mcu-contrib/pounder-725.diff new file mode 100644 index 0000000..e5779cc --- /dev/null +++ b/mcu-contrib/pounder-725.diff @@ -0,0 +1,819 @@ +diff --git a/ad9959/src/lib.rs b/ad9959/src/lib.rs +index b15f2a5bd..4e83bcb2f 100644 +--- a/ad9959/src/lib.rs ++++ b/ad9959/src/lib.rs +@@ -2,8 +2,24 @@ + + use bit_field::BitField; + use bitflags::bitflags; ++use core::ops::Range; + use embedded_hal::{blocking::delay::DelayUs, digital::v2::OutputPin}; + ++/// The minimum reference clock input frequency with REFCLK multiplier disabled. ++const MIN_REFCLK_FREQUENCY: f32 = 1e6; ++/// The minimum reference clock input frequency with REFCLK multiplier enabled. ++const MIN_MULTIPLIED_REFCLK_FREQUENCY: f32 = 10e6; ++/// The system clock frequency range with high gain configured for the internal VCO. ++const HIGH_GAIN_VCO_RANGE: Range = Range { ++ start: 255e6, ++ end: 500e6, ++}; ++/// The system clock frequency range with low gain configured for the internal VCO. ++const LOW_GAIN_VCO_RANGE: Range = Range { ++ start: 100e6, ++ end: 160e6, ++}; ++ + /// A device driver for the AD9959 direct digital synthesis (DDS) chip. + /// + /// This chip provides four independently controllable digital-to-analog output sinusoids with +@@ -216,23 +232,17 @@ impl Ad9959 { + reference_clock_frequency: f32, + multiplier: u8, + ) -> Result { ++ let frequency = ++ validate_clocking(reference_clock_frequency, multiplier)?; + self.reference_clock_frequency = reference_clock_frequency; + +- if multiplier != 1 && !(4..=20).contains(&multiplier) { +- return Err(Error::Bounds); +- } +- +- let frequency = multiplier as f32 * self.reference_clock_frequency; +- if frequency > 500_000_000.0f32 { +- return Err(Error::Frequency); +- } +- + // TODO: Update / disable any enabled channels? + let mut fr1: [u8; 3] = [0, 0, 0]; + self.read(Register::FR1, &mut fr1)?; + fr1[0].set_bits(2..=6, multiplier); + +- let vco_range = frequency > 255e6; ++ let vco_range = HIGH_GAIN_VCO_RANGE.contains(&frequency) ++ || frequency == HIGH_GAIN_VCO_RANGE.end; + fr1[0].set_bit(7, vco_range); + + self.write(Register::FR1, &fr1)?; +@@ -363,9 +373,7 @@ impl Ad9959 { + channel: Channel, + phase_turns: f32, + ) -> Result { +- let phase_offset: u16 = +- (phase_turns * (1 << 14) as f32) as u16 & 0x3FFFu16; +- ++ let phase_offset = phase_to_pow(phase_turns)?; + self.modify_channel( + channel, + Register::CPOW0, +@@ -511,6 +519,108 @@ impl Ad9959 { + } + } + ++/// Validate the internal system clock configuration of the chip. ++/// ++/// Arguments: ++/// * `reference_clock_frequency` - The reference clock frequency provided to the AD9959 core. ++/// * `multiplier` - The frequency multiplier of the system clock. Must be 1 or 4-20. ++/// ++/// Returns: ++/// The system clock frequency to be configured. ++pub fn validate_clocking( ++ reference_clock_frequency: f32, ++ multiplier: u8, ++) -> Result { ++ // The REFCLK frequency must be at least 1 MHz with REFCLK multiplier disabled. ++ if reference_clock_frequency < MIN_REFCLK_FREQUENCY { ++ return Err(Error::Bounds); ++ } ++ // If the REFCLK multiplier is enabled, the multiplier (FR1[22:18]) must be between 4 to 20. ++ // Alternatively, the clock multiplier can be disabled. The multiplication factor is 1. ++ if multiplier != 1 && !(4..=20).contains(&multiplier) { ++ return Err(Error::Bounds); ++ } ++ // If the REFCLK multiplier is enabled, the REFCLK frequency must be at least 10 MHz. ++ if multiplier != 1 ++ && reference_clock_frequency < MIN_MULTIPLIED_REFCLK_FREQUENCY ++ { ++ return Err(Error::Bounds); ++ } ++ let frequency = multiplier as f32 * reference_clock_frequency; ++ // SYSCLK frequency between 255 MHz and 500 MHz (inclusive) is valid with high range VCO ++ if HIGH_GAIN_VCO_RANGE.contains(&frequency) ++ || frequency == HIGH_GAIN_VCO_RANGE.end ++ { ++ return Ok(frequency); ++ } ++ ++ // SYSCLK frequency between 100 MHz and 160 MHz (inclusive) is valid with low range VCO ++ if LOW_GAIN_VCO_RANGE.contains(&frequency) ++ || frequency == LOW_GAIN_VCO_RANGE.end ++ { ++ return Ok(frequency); ++ } ++ ++ // When the REFCLK multiplier is disabled, SYSCLK frequency can go below 100 MHz ++ if multiplier == 1 && (0.0..=LOW_GAIN_VCO_RANGE.start).contains(&frequency) ++ { ++ return Ok(frequency); ++ } ++ ++ Err(Error::Frequency) ++} ++ ++/// Convert and validate frequency into frequency tuning word. ++/// ++/// Arguments: ++/// * `dds_frequency` - The DDS frequency to be converted and validated. ++/// * `system_clock_frequency` - The system clock frequency of the AD9959 core. ++/// ++/// Returns: ++/// The corresponding frequency tuning word. ++pub fn frequency_to_ftw( ++ dds_frequency: f32, ++ system_clock_frequency: f32, ++) -> Result { ++ // Output frequency should not exceed the Nyquist's frequency. ++ if !(0.0..=(system_clock_frequency / 2.0)).contains(&dds_frequency) { ++ return Err(Error::Bounds); ++ } ++ // The function for channel frequency is `f_out = FTW * f_s / 2^32`, where FTW is the ++ // frequency tuning word and f_s is the system clock rate. ++ Ok(((dds_frequency / system_clock_frequency) * (1u64 << 32) as f32) as u32) ++} ++ ++/// Convert phase into phase offset word. ++/// ++/// Arguments: ++/// * `phase_turns` - The normalized number of phase turns of a DDS channel. ++/// ++/// Returns: ++/// The corresponding phase offset word. ++pub fn phase_to_pow(phase_turns: f32) -> Result { ++ Ok((phase_turns * (1 << 14) as f32) as u16 & 0x3FFFu16) ++} ++ ++/// Convert amplitude into amplitude control register values. ++/// ++/// Arguments: ++/// * `amplitude` - The normalized amplitude of a DDS channel. ++/// ++/// Returns: ++/// The corresponding value in the amplitude control register. ++pub fn amplitude_to_acr(amplitude: f32) -> Result { ++ if !(0.0..=1.0).contains(&litude) { ++ return Err(Error::Bounds); ++ } ++ ++ let acr: u32 = *0u32 ++ .set_bits(0..=9, ((amplitude * (1 << 10) as f32) as u32) & 0x3FF) ++ .set_bit(12, amplitude != 1.0); ++ ++ Ok(acr as u32) ++} ++ + /// Represents a means of serializing a DDS profile for writing to a stream. + pub struct ProfileSerializer { + // heapless::Vec, especially its extend_from_slice() is slow +@@ -566,6 +676,39 @@ impl ProfileSerializer { + } + } + ++ /// Update the system clock configuration. ++ /// ++ /// # Args ++ /// * `reference_clock_frequency` - The reference clock frequency provided to the AD9959 core. ++ /// * `multiplier` - The frequency multiplier of the system clock. Must be 1 or 4-20. ++ /// ++ /// # Limitations ++ /// The correctness of the FR1 register setting code rely on FR1\[0:17\] staying 0. ++ pub fn set_system_clock( ++ &mut self, ++ reference_clock_frequency: f32, ++ multiplier: u8, ++ ) -> Result { ++ let frequency = reference_clock_frequency * multiplier as f32; ++ ++ // The enabled channel will be updated after clock reconfig ++ let mut fr1 = [0u8; 3]; ++ ++ // The ad9959 crate does not modify FR1[0:17]. These bits keep their default value. ++ // These bits by default are 0. ++ // Reading the register then update is not possible to implement in a serializer, where ++ // many QSPI writes are performed in burst. Switching between read and write requires ++ // breaking the QSPI indirect write mode and switch into the QSPI indirect read mode. ++ fr1[0].set_bits(2..=6, multiplier); ++ ++ // Frequencies within the VCO forbidden range (160e6, 255e6) are already rejected. ++ let vco_range = HIGH_GAIN_VCO_RANGE.contains(&frequency); ++ fr1[0].set_bit(7, vco_range); ++ ++ self.add_write(Register::FR1, &fr1); ++ Ok(frequency) ++ } ++ + /// Add a register write to the serialization data. + fn add_write(&mut self, register: Register, value: &[u8]) { + let data = &mut self.data[self.index..]; +diff --git a/src/bin/dual-iir.rs b/src/bin/dual-iir.rs +index c7df3f603..dadd0523c 100644 +--- a/src/bin/dual-iir.rs ++++ b/src/bin/dual-iir.rs +@@ -44,6 +44,8 @@ use stabilizer::{ + afe::Gain, + dac::{Dac0Output, Dac1Output, DacCode}, + hal, ++ pounder::{ClockConfig, PounderConfig}, ++ setup::PounderDevices as Pounder, + signal_generator::{self, SignalGenerator}, + timers::SamplingTimer, + DigitalInput0, DigitalInput1, SystemTimer, Systick, AFE0, AFE1, +@@ -145,6 +147,16 @@ pub struct Settings { + /// See [signal_generator::BasicConfig#miniconf] + #[miniconf(defer)] + signal_generator: miniconf::Array, ++ ++ /// Specifies the config for pounder DDS clock configuration, DDS channels & attenuations ++ /// ++ /// # Path ++ /// `pounder` ++ /// ++ /// # Value ++ /// See [PounderConfig#miniconf] ++ #[miniconf(defer)] ++ pounder: miniconf::Option, + } + + impl Default for Settings { +@@ -171,6 +183,8 @@ impl Default for Settings { + .into(), + + stream_target: StreamTarget::default(), ++ ++ pounder: None.into(), + } + } + } +@@ -189,6 +203,7 @@ mod app { + settings: Settings, + telemetry: TelemetryBuffer, + signal_generator: [SignalGenerator; 2], ++ pounder: Option, + } + + #[local] +@@ -199,6 +214,7 @@ mod app { + adcs: (Adc0Input, Adc1Input), + dacs: (Dac0Output, Dac1Output), + iir_state: [[iir::Vec5; IIR_CASCADE_LENGTH]; 2], ++ dds_clock_state: Option, + generator: FrameGenerator, + cpu_temp_sensor: stabilizer::hardware::cpu_temp_sensor::CpuTempSensor, + } +@@ -208,7 +224,7 @@ mod app { + let clock = SystemTimer::new(|| monotonics::now().ticks() as u32); + + // Configure the microcontroller +- let (stabilizer, _pounder) = hardware::setup::setup( ++ let (stabilizer, pounder) = hardware::setup::setup( + c.core, + c.device, + clock, +@@ -216,6 +232,13 @@ mod app { + SAMPLE_TICKS, + ); + ++ let dds_clock_state = pounder.as_ref().map(|_| ClockConfig::default()); ++ ++ let mut settings = Settings::default(); ++ if pounder.is_some() { ++ settings.pounder.replace(PounderConfig::default()); ++ } ++ + let mut network = NetworkUsers::new( + stabilizer.net.stack, + stabilizer.net.phy, +@@ -226,13 +249,12 @@ mod app { + .unwrap_or("10.34.16.1") + .parse() + .unwrap(), ++ settings, + ); + + let generator = network + .configure_streaming(StreamFormat::AdcDacData, BATCH_SIZE as _); + +- let settings = Settings::default(); +- + let shared = Shared { + network, + settings, +@@ -249,6 +271,7 @@ mod app { + .unwrap(), + ), + ], ++ pounder, + }; + + let mut local = Local { +@@ -258,6 +281,7 @@ mod app { + adcs: stabilizer.adcs, + dacs: stabilizer.dacs, + iir_state: [[[0.; 5]; IIR_CASCADE_LENGTH]; 2], ++ dds_clock_state, + generator, + cpu_temp_sensor: stabilizer.temperature_sensor, + }; +@@ -407,7 +431,7 @@ mod app { + } + } + +- #[task(priority = 1, local=[afes], shared=[network, settings, signal_generator])] ++ #[task(priority = 1, local=[afes, dds_clock_state], shared=[network, settings, signal_generator, pounder])] + fn settings_update(mut c: settings_update::Context) { + let settings = c.shared.network.lock(|net| *net.miniconf.settings()); + c.shared.settings.lock(|current| *current = settings); +@@ -431,15 +455,28 @@ mod app { + } + } + ++ // Update Pounder configurations ++ c.shared.pounder.lock(|pounder| { ++ if let Some(pounder) = pounder { ++ let pounder_settings = settings.pounder.as_ref().unwrap(); ++ let mut clocking = c.local.dds_clock_state.unwrap(); ++ pounder.update_dds(*pounder_settings, &mut clocking); ++ } ++ }); ++ + let target = settings.stream_target.into(); + c.shared.network.lock(|net| net.direct_stream(target)); + } + +- #[task(priority = 1, shared=[network, settings, telemetry], local=[cpu_temp_sensor])] ++ #[task(priority = 1, shared=[network, settings, telemetry, pounder], local=[cpu_temp_sensor])] + fn telemetry(mut c: telemetry::Context) { + let telemetry: TelemetryBuffer = + c.shared.telemetry.lock(|telemetry| *telemetry); + ++ let pounder_telemetry = c.shared.pounder.lock(|pounder| { ++ pounder.as_mut().map(|pounder| pounder.get_telemetry()) ++ }); ++ + let (gains, telemetry_period) = c + .shared + .settings +@@ -450,6 +487,7 @@ mod app { + gains[0], + gains[1], + c.local.cpu_temp_sensor.get_temperature().unwrap(), ++ pounder_telemetry, + )) + }); + +diff --git a/src/bin/lockin.rs b/src/bin/lockin.rs +index 6896df7ff..1e92a0f75 100644 +--- a/src/bin/lockin.rs ++++ b/src/bin/lockin.rs +@@ -254,6 +254,8 @@ mod app { + SAMPLE_TICKS, + ); + ++ let settings = Settings::default(); ++ + let mut network = NetworkUsers::new( + stabilizer.net.stack, + stabilizer.net.phy, +@@ -264,6 +266,7 @@ mod app { + .unwrap_or("10.34.16.1") + .parse() + .unwrap(), ++ settings, + ); + + let generator = network +@@ -272,7 +275,7 @@ mod app { + let shared = Shared { + network, + telemetry: TelemetryBuffer::default(), +- settings: Settings::default(), ++ settings, + }; + + let signal_config = signal_generator::Config { +@@ -499,6 +502,7 @@ mod app { + gains[0], + gains[1], + c.local.cpu_temp_sensor.get_temperature().unwrap(), ++ None, + )) + }); + +diff --git a/src/hardware/pounder/attenuators.rs b/src/hardware/pounder/attenuators.rs +index ef22814d4..a5d220dfb 100644 +--- a/src/hardware/pounder/attenuators.rs ++++ b/src/hardware/pounder/attenuators.rs +@@ -54,10 +54,9 @@ pub trait AttenuatorInterface { + fn get_attenuation(&mut self, channel: Channel) -> Result { + let mut channels = [0_u8; 4]; + +- // Reading the data always shifts data out of the staging registers, so we perform a +- // duplicate write-back to ensure the staging register is always equal to the output +- // register. +- self.transfer_attenuators(&mut channels)?; ++ // Reading the data always shifts data out of the staging registers, so a duplicate ++ // write-back will be performed to ensure the staging register is always equal to the ++ // output register. + self.transfer_attenuators(&mut channels)?; + + // The attenuation code is stored in the upper 6 bits of the register, where each LSB +@@ -68,6 +67,9 @@ pub trait AttenuatorInterface { + // care) would contain erroneous data. + let attenuation_code = (!channels[channel as usize]) >> 2; + ++ // The write-back transfer is performed. Staging register is now restored. ++ self.transfer_attenuators(&mut channels)?; ++ + // Convert the desired channel code into dB of attenuation. + Ok(attenuation_code as f32 / 2.0) + } +diff --git a/src/hardware/pounder/dds_output.rs b/src/hardware/pounder/dds_output.rs +index 9af23a9aa..559855fcc 100644 +--- a/src/hardware/pounder/dds_output.rs ++++ b/src/hardware/pounder/dds_output.rs +@@ -55,7 +55,7 @@ + use log::warn; + use stm32h7xx_hal as hal; + +-use super::{hrtimer::HighResTimerE, QspiInterface}; ++use super::{hrtimer::HighResTimerE, Profile, QspiInterface}; + use ad9959::{Channel, Mode, ProfileSerializer}; + + /// The DDS profile update stream. +@@ -157,6 +157,46 @@ impl<'a> ProfileBuilder<'a> { + self + } + ++ /// Update a number of channels with fully defined profile settings. ++ /// ++ /// # Args ++ /// * `channels` - A set of channels to apply the configuration to. ++ /// * `profile` - The complete DDS profile, which defines the frequency tuning word, ++ /// amplitude control register & the phase offset word of the channels. ++ /// # Note ++ /// The ACR should be stored in the 3 LSB of the word. ++ /// If amplitude scaling is to be used, the "Amplitude multiplier enable" bit must be set. ++ #[inline] ++ pub fn update_channels_with_profile( ++ &mut self, ++ channels: Channel, ++ profile: Profile, ++ ) -> &mut Self { ++ self.serializer.update_channels( ++ channels, ++ Some(profile.frequency_tuning_word), ++ Some(profile.phase_offset), ++ Some(profile.amplitude_control), ++ ); ++ self ++ } ++ ++ /// Update the system clock configuration. ++ /// ++ /// # Args ++ /// * `reference_clock_frequency` - The reference clock frequency provided to the AD9959 core. ++ /// * `multiplier` - The frequency multiplier of the system clock. Must be 1 or 4-20. ++ #[inline] ++ pub fn set_system_clock( ++ &mut self, ++ reference_clock_frequency: f32, ++ multiplier: u8, ++ ) -> Result<&mut Self, ad9959::Error> { ++ self.serializer ++ .set_system_clock(reference_clock_frequency, multiplier)?; ++ Ok(self) ++ } ++ + /// Write the profile to the DDS asynchronously. + #[allow(dead_code)] + #[inline] +diff --git a/src/hardware/pounder/mod.rs b/src/hardware/pounder/mod.rs +index d6dd654fc..1e436f785 100644 +--- a/src/hardware/pounder/mod.rs ++++ b/src/hardware/pounder/mod.rs +@@ -1,10 +1,17 @@ + use self::attenuators::AttenuatorInterface; + + use super::hal; +-use crate::hardware::{shared_adc::AdcChannel, I2c1Proxy}; ++use crate::hardware::{setup, shared_adc::AdcChannel, I2c1Proxy}; ++use crate::net::telemetry::PounderTelemetry; ++use ad9959::{ ++ amplitude_to_acr, frequency_to_ftw, phase_to_pow, validate_clocking, ++}; + use embedded_hal::blocking::spi::Transfer; + use enum_iterator::Sequence; ++use miniconf::Miniconf; ++use rf_power::PowerMeasurementInterface; + use serde::{Deserialize, Serialize}; ++use stm32h7xx_hal::time::MegaHertz; + + pub mod attenuators; + pub mod dds_output; +@@ -91,40 +98,98 @@ impl From for GpioPin { + } + } + +-#[derive(Serialize, Deserialize, Copy, Clone, Debug)] +-pub struct DdsChannelState { +- pub phase_offset: f32, ++#[derive(Serialize, Deserialize, Copy, Clone, Debug, Miniconf)] ++pub struct DdsChannelConfig { + pub frequency: f32, ++ pub phase_offset: f32, + pub amplitude: f32, +- pub enabled: bool, + } + +-#[derive(Serialize, Deserialize, Copy, Clone, Debug)] +-pub struct ChannelState { +- pub parameters: DdsChannelState, +- pub attenuation: f32, ++impl Default for DdsChannelConfig { ++ fn default() -> Self { ++ Self { ++ frequency: 0.0, ++ phase_offset: 0.0, ++ amplitude: 0.0, ++ } ++ } + } + +-#[derive(Serialize, Deserialize, Copy, Clone, Debug)] +-pub struct InputChannelState { +- pub attenuation: f32, +- pub power: f32, +- pub mixer: DdsChannelState, ++/// Represents a fully defined DDS profile, with parameters expressed in machine units ++pub struct Profile { ++ /// A 32-bits representation of DDS frequency in relation to the system clock frequency. ++ /// This value corresponds to the AD9959 CFTW0 register, which specifies the frequency ++ /// of DDS channels. ++ pub frequency_tuning_word: u32, ++ /// The DDS phase offset. It corresponds to the AD9959 CPOW0 register, which specifies ++ /// the phase offset of DDS channels. ++ pub phase_offset: u16, ++ /// Control amplitudes of DDS channels. It corresponds to the AD9959 ACR register, which ++ /// controls the amplitude scaling factor of DDS channels. ++ pub amplitude_control: u32, ++} ++ ++impl TryFrom<(ClockConfig, ChannelConfig)> for Profile { ++ type Error = ad9959::Error; ++ ++ fn try_from( ++ (clocking, channel): (ClockConfig, ChannelConfig), ++ ) -> Result { ++ let system_clock_frequency = ++ clocking.reference_clock * clocking.multiplier as f32; ++ Ok(Profile { ++ frequency_tuning_word: frequency_to_ftw( ++ channel.dds.frequency, ++ system_clock_frequency, ++ )?, ++ phase_offset: phase_to_pow(channel.dds.phase_offset)?, ++ amplitude_control: amplitude_to_acr(channel.dds.amplitude)?, ++ }) ++ } + } + +-#[derive(Serialize, Deserialize, Copy, Clone, Debug)] +-pub struct OutputChannelState { ++#[derive(Serialize, Deserialize, Copy, Clone, Debug, Miniconf)] ++pub struct ChannelConfig { ++ #[miniconf(defer)] ++ pub dds: DdsChannelConfig, + pub attenuation: f32, +- pub channel: DdsChannelState, + } + +-#[derive(Serialize, Deserialize, Copy, Clone, Debug)] +-pub struct DdsClockConfig { ++impl Default for ChannelConfig { ++ fn default() -> Self { ++ ChannelConfig { ++ dds: DdsChannelConfig::default(), ++ attenuation: 31.5, ++ } ++ } ++} ++ ++#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Miniconf)] ++pub struct ClockConfig { + pub multiplier: u8, + pub reference_clock: f32, + pub external_clock: bool, + } + ++impl Default for ClockConfig { ++ fn default() -> Self { ++ Self { ++ multiplier: 5, ++ reference_clock: MegaHertz::MHz(100).to_Hz() as f32, ++ external_clock: false, ++ } ++ } ++} ++ ++#[derive(Copy, Clone, Debug, Default, Miniconf)] ++pub struct PounderConfig { ++ pub clock: ClockConfig, ++ #[miniconf(defer)] ++ pub in_channel: miniconf::Array, ++ #[miniconf(defer)] ++ pub out_channel: miniconf::Array, ++} ++ + impl From for ad9959::Channel { + /// Translate pounder channels to DDS output channels. + fn from(other: Channel) -> Self { +@@ -489,3 +554,77 @@ impl rf_power::PowerMeasurementInterface for PounderDevices { + Ok(adc_scale * 2.048) + } + } ++ ++impl setup::PounderDevices { ++ pub fn update_dds( ++ &mut self, ++ settings: PounderConfig, ++ clocking: &mut ClockConfig, ++ ) { ++ if *clocking != settings.clock { ++ match validate_clocking( ++ settings.clock.reference_clock, ++ settings.clock.multiplier, ++ ) { ++ Ok(_frequency) => { ++ self.pounder ++ .set_ext_clk(settings.clock.external_clock) ++ .unwrap(); ++ ++ self.dds_output ++ .builder() ++ .set_system_clock( ++ settings.clock.reference_clock, ++ settings.clock.multiplier, ++ ) ++ .unwrap() ++ .write(); ++ ++ *clocking = settings.clock; ++ } ++ Err(err) => { ++ log::error!("Invalid AD9959 clocking parameters: {:?}", err) ++ } ++ } ++ } ++ ++ for (channel_config, pounder_channel) in settings ++ .in_channel ++ .iter() ++ .chain(settings.out_channel.iter()) ++ .zip([Channel::In0, Channel::In1, Channel::Out0, Channel::Out1]) ++ { ++ match Profile::try_from((*clocking, *channel_config)) { ++ Ok(dds_profile) => { ++ self.dds_output ++ .builder() ++ .update_channels_with_profile( ++ pounder_channel.into(), ++ dds_profile, ++ ) ++ .write(); ++ ++ if let Err(err) = self.pounder.set_attenuation( ++ pounder_channel, ++ channel_config.attenuation, ++ ) { ++ log::error!("Invalid attenuation settings: {:?}", err) ++ } ++ } ++ Err(err) => { ++ log::error!("Invalid AD9959 profile settings: {:?}", err) ++ } ++ } ++ } ++ } ++ ++ pub fn get_telemetry(&mut self) -> PounderTelemetry { ++ PounderTelemetry { ++ temperature: self.pounder.lm75.read_temperature().unwrap(), ++ input_power: [ ++ self.pounder.measure_power(Channel::In0).unwrap(), ++ self.pounder.measure_power(Channel::In1).unwrap(), ++ ], ++ } ++ } ++} +diff --git a/src/net/mod.rs b/src/net/mod.rs +index f6372381b..9a88008f7 100644 +--- a/src/net/mod.rs ++++ b/src/net/mod.rs +@@ -43,7 +43,7 @@ pub enum NetworkState { + } + + /// A structure of Stabilizer's default network users. +-pub struct NetworkUsers { ++pub struct NetworkUsers { + pub miniconf: miniconf::MqttClient, + pub processor: NetworkProcessor, + stream: DataStream, +@@ -53,7 +53,7 @@ pub struct NetworkUsers { + + impl NetworkUsers + where +- S: Default + Miniconf + Clone, ++ S: Miniconf + Clone, + T: Serialize, + { + /// Construct Stabilizer's default network users. +@@ -65,6 +65,7 @@ where + /// * `app` - The name of the application. + /// * `mac` - The MAC address of the network. + /// * `broker` - The IP address of the MQTT broker to use. ++ /// * `settings` - The initial settings value + /// + /// # Returns + /// A new struct of network users. +@@ -75,6 +76,7 @@ where + app: &str, + mac: smoltcp_nal::smoltcp::wire::EthernetAddress, + broker: IpAddr, ++ settings: S, + ) -> Self { + let stack_manager = + cortex_m::singleton!(: NetworkManager = NetworkManager::new(stack)) +@@ -91,7 +93,7 @@ where + &prefix, + broker, + clock, +- S::default(), ++ settings, + ) + .unwrap(); + +diff --git a/src/net/telemetry.rs b/src/net/telemetry.rs +index c6e071f17..e35347037 100644 +--- a/src/net/telemetry.rs ++++ b/src/net/telemetry.rs +@@ -58,6 +58,23 @@ pub struct Telemetry { + + /// The CPU temperature in degrees Celsius. + pub cpu_temp: f32, ++ ++ /// Measurements related to Pounder ++ pub pounder: Option, ++} ++ ++/// The structure that holds the telemetry related to Pounder. ++/// ++/// # Note ++/// This structure should be generated on-demand by the buffer when required to minimize conversion ++/// overhead. ++#[derive(Copy, Clone, Serialize)] ++pub struct PounderTelemetry { ++ /// The Pounder temperature in degrees Celsius ++ pub temperature: f32, ++ ++ /// The detected RF power into IN channels ++ pub input_power: [f32; 2], + } + + impl Default for TelemetryBuffer { +@@ -77,10 +94,17 @@ impl TelemetryBuffer { + /// * `afe0` - The current AFE configuration for channel 0. + /// * `afe1` - The current AFE configuration for channel 1. + /// * `cpu_temp` - The current CPU temperature. ++ /// * `pounder` - The current Pounder telemetry. + /// + /// # Returns + /// The finalized telemetry structure that can be serialized and reported. +- pub fn finalize(self, afe0: Gain, afe1: Gain, cpu_temp: f32) -> Telemetry { ++ pub fn finalize( ++ self, ++ afe0: Gain, ++ afe1: Gain, ++ cpu_temp: f32, ++ pounder: Option, ++ ) -> Telemetry { + let in0_volts = Into::::into(self.adcs[0]) / afe0.as_multiplier(); + let in1_volts = Into::::into(self.adcs[1]) / afe1.as_multiplier(); + +@@ -89,6 +113,7 @@ impl TelemetryBuffer { + adcs: [in0_volts, in1_volts], + dacs: [self.dacs[0].into(), self.dacs[1].into()], + digital_inputs: self.digital_inputs, ++ pounder, + } + } + }