diff --git a/ad9959/src/lib.rs b/ad9959/src/lib.rs index d77af62..d5de1ec 100644 --- a/ad9959/src/lib.rs +++ b/ad9959/src/lib.rs @@ -1,7 +1,7 @@ #![no_std] use bit_field::BitField; -use embedded_hal::{blocking::delay::DelayMs, digital::v2::OutputPin}; +use embedded_hal::{blocking::delay::DelayUs, digital::v2::OutputPin}; /// A device driver for the AD9959 direct digital synthesis (DDS) chip. /// @@ -14,7 +14,7 @@ use embedded_hal::{blocking::delay::DelayMs, digital::v2::OutputPin}; /// The chip supports a number of serial interfaces to improve data throughput, including normal, /// dual, and quad SPI configurations. pub struct Ad9959 { - pub interface: INTERFACE, + interface: INTERFACE, reference_clock_frequency: f32, system_clock_multiplier: u8, communication_mode: Mode, @@ -72,6 +72,7 @@ pub enum Register { } /// Specifies an output channel of the AD9959 DDS chip. +#[derive(Copy, Clone, PartialEq)] pub enum Channel { One = 0, Two = 1, @@ -103,9 +104,9 @@ impl Ad9959 { /// `clock_frequency` to generate the system clock. pub fn new( interface: I, - reset_pin: &mut impl OutputPin, + mut reset_pin: impl OutputPin, io_update: &mut impl OutputPin, - delay: &mut impl DelayMs, + delay: &mut impl DelayUs, desired_mode: Mode, clock_frequency: f32, multiplier: u8, @@ -117,13 +118,15 @@ impl Ad9959 { communication_mode: desired_mode, }; + io_update.set_low().or(Err(Error::Pin))?; + // Reset the AD9959 reset_pin.set_high().or(Err(Error::Pin))?; - io_update.set_low().or_else(|_| Err(Error::Pin))?; - - // Delay for a clock cycle to allow the device to reset. - delay.delay_ms((1000.0 / clock_frequency as f32) as u8); + // Delay for at least 1 SYNC_CLK period for the reset to occur. The SYNC_CLK is guaranteed + // to be at least 250KHz (1/4 of 1MHz minimum REF_CLK). We use 5uS instead of 4uS to + // guarantee conformance with datasheet requirements. + delay.delay_us(5); reset_pin.set_low().or(Err(Error::Pin))?; @@ -138,16 +141,28 @@ impl Ad9959 { ad9959.write(Register::CSR, &csr)?; // Latch the new interface configuration. - io_update.set_high().or_else(|_| Err(Error::Pin))?; - // Delay for a clock cycle to allow the device to reset. - delay.delay_ms(2 * (1000.0 / clock_frequency as f32) as u8); - io_update.set_low().or_else(|_| Err(Error::Pin))?; + io_update.set_high().or(Err(Error::Pin))?; + + // Delay for at least 1 SYNC_CLK period for the update to occur. The SYNC_CLK is guaranteed + // to be at least 250KHz (1/4 of 1MHz minimum REF_CLK). We use 5uS instead of 4uS to + // guarantee conformance with datasheet requirements. + delay.delay_us(5); + + io_update.set_low().or(Err(Error::Pin))?; ad9959 .interface .configure_mode(desired_mode) .or(Err(Error::Interface))?; + // Empirical evidence indicates a delay is necessary here for the IO update to become + // active. This is likely due to needing to wait at least 1 clock cycle of the DDS for the + // interface update to occur. + // Delay for at least 1 SYNC_CLK period for the update to occur. The SYNC_CLK is guaranteed + // to be at least 250KHz (1/4 of 1MHz minimum REF_CLK). We use 5uS instead of 4uS to + // guarantee conformance with datasheet requirements. + delay.delay_us(5); + // Read back the CSR to ensure it specifies the mode correctly. let mut updated_csr: [u8; 1] = [0]; ad9959.read(Register::CSR, &mut updated_csr)?; @@ -271,34 +286,6 @@ impl Ad9959 { * self.reference_clock_frequency as f32 } - /// Enable an output channel. - pub fn enable_channel(&mut self, channel: Channel) -> Result<(), Error> { - let mut csr: [u8; 1] = [0]; - self.read(Register::CSR, &mut csr)?; - csr[0].set_bit(channel as usize + 4, true); - self.write(Register::CSR, &csr)?; - - Ok(()) - } - - /// Disable an output channel. - pub fn disable_channel(&mut self, channel: Channel) -> Result<(), Error> { - let mut csr: [u8; 1] = [0]; - self.read(Register::CSR, &mut csr)?; - csr[0].set_bit(channel as usize + 4, false); - self.write(Register::CSR, &csr)?; - - Ok(()) - } - - /// Determine if an output channel is enabled. - pub fn is_enabled(&mut self, channel: Channel) -> Result { - let mut csr: [u8; 1] = [0; 1]; - self.read(Register::CSR, &mut csr)?; - - Ok(csr[0].get_bit(channel as usize + 4)) - } - /// Update an output channel configuration register. /// /// Args: @@ -313,14 +300,13 @@ impl Ad9959 { ) -> Result<(), Error> { // Disable all other outputs so that we can update the configuration register of only the // specified channel. - let mut csr: [u8; 1] = [0]; - self.read(Register::CSR, &mut csr)?; + let csr: u8 = *0x00_u8 + .set_bits(1..=2, self.communication_mode as u8) + .set_bit(4 + channel as usize, true); - let mut new_csr = csr; - new_csr[0].set_bits(4..8, 0); - new_csr[0].set_bit(4 + channel as usize, true); - - self.write(Register::CSR, &new_csr)?; + self.interface + .write(Register::CSR as u8, &[csr]) + .map_err(|_| Error::Interface)?; self.write(register, &data)?; @@ -507,51 +493,124 @@ impl Ad9959 { / (1u64 << 32) as f32) } - pub fn serialize_profile( - &self, - channel: Channel, - freq: f32, - turns: f32, - amplitude: f32, - ) -> Result<[u32; 4], Error> { - let csr: u8 = *0x00_u8 - .set_bits(1..=2, self.communication_mode as u8) - .set_bit(4 + channel as usize, true); - - // 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. - let tuning_word: u32 = ((freq * (1u64 << 32) as f32) - / self.system_clock_frequency()) - as u32; - - let phase_offset: u16 = (turns * (1 << 14) as f32) as u16 & 0x3FFFu16; - let pow: u32 = *0u32 - .set_bits(24..32, Register::CPOW0 as u32) - .set_bits(8..24, phase_offset as u32) - .set_bits(0..8, Register::CFTW0 as u32); - - // Enable the amplitude multiplier for the channel if required. The amplitude control has - // full-scale at 0x3FF (amplitude of 1), so the multiplier should be disabled whenever - // full-scale is used. - let amplitude_control: u16 = (amplitude * (1 << 10) as f32) as u16; - - let acr: u32 = *0u32 - .set_bits(24..32, Register::ACR as u32) - .set_bits(0..10, amplitude_control as u32 & 0x3FF) - .set_bit(12, amplitude_control < (1 << 10)); - - let serialized: [u32; 4] = [ - u32::from_le_bytes([ - Register::CSR as u8, - csr, - Register::CSR as u8, - csr, - ]), - acr.to_be(), - pow.to_be(), - tuning_word.to_be(), - ]; - - Ok(serialized) + /// Finalize DDS configuration + /// + /// # Note + /// This is intended for when the DDS profiles will be written as a stream of data to the DDS. + /// + /// # Returns + /// (I, config) where `I` is the interface to the DDS and `config` is the frozen `DdsConfig`. + pub fn freeze(self) -> (I, DdsConfig) { + let config = DdsConfig { + mode: self.communication_mode, + }; + (self.interface, config) + } +} + +/// The frozen DDS configuration. +pub struct DdsConfig { + mode: Mode, +} + +impl DdsConfig { + /// Create a serializer that can be used for generating a serialized DDS profile for writing to + /// a QSPI stream. + pub fn builder(&self) -> ProfileSerializer { + ProfileSerializer::new(self.mode) + } +} + +/// Represents a means of serializing a DDS profile for writing to a stream. +pub struct ProfileSerializer { + data: [u8; 16], + index: usize, + mode: Mode, +} + +impl ProfileSerializer { + /// Construct a new serializer. + /// + /// # Args + /// * `mode` - The communication mode of the DDS. + fn new(mode: Mode) -> Self { + Self { + mode, + data: [0; 16], + index: 0, + } + } + + /// Update a number of channels with the requested profile. + /// + /// # Args + /// * `channels` - A list of channels to apply the configuration to. + /// * `ftw` - If provided, indicates a frequency tuning word for the channels. + /// * `pow` - If provided, indicates a phase offset word for the channels. + /// * `acr` - If provided, indicates the amplitude control register for the channels. + pub fn update_channels( + &mut self, + channels: &[Channel], + ftw: Option, + pow: Option, + acr: Option, + ) { + let mut csr: u8 = *0u8.set_bits(1..3, self.mode as u8); + for channel in channels.iter() { + csr.set_bit(4 + *channel as usize, true); + } + + self.add_write(Register::CSR, &[csr]); + + if let Some(ftw) = ftw { + self.add_write(Register::CFTW0, &ftw.to_be_bytes()); + } + + if let Some(pow) = pow { + self.add_write(Register::CPOW0, &pow.to_be_bytes()); + } + + if let Some(acr) = acr { + self.add_write(Register::ACR, &acr.to_be_bytes()); + } + } + + /// Add a register write to the serialization data. + fn add_write(&mut self, register: Register, value: &[u8]) { + let data = &mut self.data[self.index..]; + data[0] = register as u8; + data[1..][..value.len()].copy_from_slice(value); + self.index += value.len() + 1; + } + + /// Get the serialized profile as a slice of 32-bit words. + /// + /// # Note + /// The serialized profile will be padded to the next 32-bit word boundary by adding dummy + /// writes to the CSR or LSRR registers. + /// + /// # Returns + /// A slice of `u32` words representing the serialized profile. + pub fn finalize<'a>(&'a mut self) -> &[u32] { + // Pad the buffer to 32-bit alignment by adding dummy writes to CSR and LSRR. + let padding = 4 - (self.index % 4); + match padding { + 0 => {} + 1 => { + // For a pad size of 1, we have to pad with 5 bytes to align things. + self.add_write(Register::CSR, &[(self.mode as u8) << 1]); + self.add_write(Register::LSRR, &[0, 0, 0]); + } + 2 => self.add_write(Register::CSR, &[(self.mode as u8) << 1]), + 3 => self.add_write(Register::LSRR, &[0, 0, 0]), + + _ => unreachable!(), + } + unsafe { + core::slice::from_raw_parts::<'a, u32>( + &self.data as *const _ as *const u32, + self.index / 4, + ) + } } } diff --git a/pounder_test.py b/pounder_test.py deleted file mode 100644 index 5d43dd3..0000000 --- a/pounder_test.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/python3 -""" -Description: Test Stabilizer communication and DDS configuration. - -Author: Ryan Summers -""" - -import socket -import json - -HOST = '10.0.16.99' -PORT = 1235 - -def do_request(s, request): - """ Perform a request with the Stabilizer. - - Args: - s: The socket to the stabilizer. - request: The request to transmit. - - Returns: - The received response object. - """ - # Transform the value field. - request['value'] = json.dumps(request['value'], separators=[',', ':']).replace('"', "'") - data = (json.dumps(request, separators=[',', ':']) + '\n').encode('ascii') - s.send(data) - - response = b'' - while not response.endswith(b'\n'): - response += s.recv(1024) - - # Decode the value array - response = json.loads(response.decode('ascii')) - response['value'] = response['value'].replace("'", '"') - response['value'] = json.loads(response['value']) - - return response - - -def read_attribute(s, attribute_name): - """ Read an attribute on the Stabilizer device. - - Args: - s: The socket to the stabilizer. - attribute_name: The name of the endpoint to write to (the attribute name). - - Returns: - The value of the attribute. May be a string or a dictionary. - """ - request = { - "req": "Read", - "attribute": attribute_name, - "value": "", - } - - response = do_request(s, request) - - if 'code' not in response or response['code'] != 200: - raise Exception(f'Failed to read {attribute_name}: {response}') - - return response['value'] - - -def write_attribute(s, attribute_name, attribute_value): - """ Write an attribute on the Stabilizer device. - - Args: - s: The socket to the stabilizer. - attribute_name: The name of the endpoint to write to (the attribute name). - attribute_value: The value to write to the attribute. May be a string or a dictionary. - """ - request = { - "req": "Write", - "attribute": attribute_name, - "value": attribute_value, - } - - response = do_request(s, request) - - if 'code' not in response or response['code'] != 200: - raise Exception(f'Failed to write {attribute_name}: {response}') - - -def main(): - """ Main program entry point. """ - with socket.socket() as s: - - # Connect to the stabilizer. - s.connect((HOST, PORT)) - - # A sample configuration for an output channel. - channel_config = { - 'attenuation': 0.0, - 'parameters': { - 'phase_offset': 0.5, - 'frequency': 100.0e6, - 'amplitude': 1.0, - 'enabled': True, - } - } - - # Configure OUT0 and read it back. - write_attribute(s, "pounder/out0", channel_config) - print('Pounder OUT0: ', read_attribute(s, "pounder/out0")) - - print('Pounder IN1: ', read_attribute(s, "pounder/in1")) - print('Pounder OUT1: ', read_attribute(s, "pounder/out1")) - -if __name__ == '__main__': - main() diff --git a/src/hrtimer.rs b/src/hrtimer.rs index 47ea5c2..2831bbe 100644 --- a/src/hrtimer.rs +++ b/src/hrtimer.rs @@ -1,11 +1,14 @@ +///! The HRTimer (High Resolution Timer) is used to generate IO_Update pulses to the Pounder DDS. use crate::hal; use hal::rcc::{rec, CoreClocks, ResetEnable}; +/// A HRTimer output channel. pub enum Channel { One, Two, } +/// The high resolution timer. Currently, only Timer E is supported. pub struct HighResTimerE { master: hal::stm32::HRTIM_MASTER, timer: hal::stm32::HRTIM_TIME, @@ -15,6 +18,7 @@ pub struct HighResTimerE { } impl HighResTimerE { + /// Construct a new high resolution timer for generating IO_update signals. pub fn new( timer_regs: hal::stm32::HRTIM_TIME, master_regs: hal::stm32::HRTIM_MASTER, @@ -32,6 +36,18 @@ impl HighResTimerE { } } + /// Configure the timer to operate in single-shot mode. + /// + /// # Note + /// This will configure the timer to generate a single pulse on an output channel. The timer + /// will only count up once and must be `trigger()`'d after / configured. + /// + /// The output will be asserted from `set_offset` to `set_offset` + `set_duration` in the count. + /// + /// # Args + /// * `channel` - The timer output channel to configure. + /// * `set_duration` - The duration that the output should be asserted for. + /// * `set_offset` - The first time at which the output should be asserted. pub fn configure_single_shot( &mut self, channel: Channel, @@ -52,28 +68,39 @@ impl HighResTimerE { // Determine the clock divider, which may be 1, 2, or 4. We will choose a clock divider that // allows us the highest resolution per tick, so lower dividers are favored. - let divider: u8 = if source_cycles < 0xFFDF { + let setting: u8 = if source_cycles < 0xFFDF { 1 } else if (source_cycles / 2) < 0xFFDF { 2 } else if (source_cycles / 4) < 0xFFDF { - 4 + 3 } else { panic!("Unattainable timing parameters!"); }; + let divider = 1 << (setting - 1); + // The period register must be greater than or equal to 3 cycles. let period = (source_cycles / divider as u32) as u16; assert!(period > 2); // We now have the prescaler and the period registers. Configure the timer. + // Note(unsafe): The prescaler is guaranteed to be greater than or equal to 4 (minimum + // allowed value) due to the addition. The setting is always 1, 2, or 3, which represents + // all valid values. self.timer .timecr - .modify(|_, w| unsafe { w.ck_pscx().bits(divider + 4) }); + .modify(|_, w| unsafe { w.ck_pscx().bits(setting + 4) }); + + // Note(unsafe): The period register is guaranteed to be a 16-bit value, which will fit in + // this register. self.timer.perer.write(|w| unsafe { w.perx().bits(period) }); // Configure the comparator 1 level. let offset = (set_offset * source_frequency as f32) as u16; + // Note(unsafe): The offset is always a 16-bit value, so is always valid for values >= 3, as + // specified by the datasheet. + assert!(offset >= 3); self.timer .cmp1er .write(|w| unsafe { w.cmp1x().bits(offset) }); @@ -97,6 +124,7 @@ impl HighResTimerE { self.master.mcr.modify(|_, w| w.tecen().set_bit()); } + /// Generate a single trigger of the timer to start the output pulse generation. pub fn trigger(&mut self) { // Generate a reset event to force the timer to start counting. self.common.cr2.write(|w| w.terst().set_bit()); diff --git a/src/main.rs b/src/main.rs index 2c07284..e798294 100644 --- a/src/main.rs +++ b/src/main.rs @@ -80,6 +80,7 @@ mod timers; use adc::{Adc0Input, Adc1Input}; use dac::{Dac0Output, Dac1Output}; use dsp::iir; +use pounder::DdsOutput; #[cfg(not(feature = "semihosting"))] fn init_log() {} @@ -202,6 +203,8 @@ const APP: () = { eeprom_i2c: hal::i2c::I2c, + dds_output: Option, + // Note: It appears that rustfmt generates a format that GDB cannot recognize, which // results in GDB breakpoints being set improperly. #[rustfmt::skip] @@ -487,8 +490,9 @@ const APP: () = { // Measure the Pounder PGOOD output to detect if pounder is present on Stabilizer. let pounder_pgood = gpiob.pb13.into_pull_down_input(); delay.delay_ms(2u8); - let pounder_devices = if pounder_pgood.is_high().unwrap() { - let ad9959 = { + let (pounder_devices, dds_output) = if pounder_pgood.is_high().unwrap() + { + let mut ad9959 = { let qspi_interface = { // Instantiate the QUADSPI pins and peripheral interface. let qspi_pins = { @@ -532,12 +536,12 @@ const APP: () = { pounder::QspiInterface::new(qspi).unwrap() }; - let mut reset_pin = gpioa.pa0.into_push_pull_output(); + let reset_pin = gpioa.pa0.into_push_pull_output(); let mut io_update = gpiog.pg7.into_push_pull_output(); let ad9959 = ad9959::Ad9959::new( qspi_interface, - &mut reset_pin, + reset_pin, &mut io_update, &mut delay, ad9959::Mode::FourBitSerial, @@ -619,54 +623,64 @@ const APP: () = { let adc1_in_p = gpiof.pf11.into_analog(); let adc2_in_p = gpiof.pf14.into_analog(); - let io_update_trigger = { - let _io_update = gpiog - .pg7 - .into_alternate_af2() - .set_speed(hal::gpio::Speed::VeryHigh); + let pounder_devices = pounder::PounderDevices::new( + io_expander, + &mut ad9959, + spi, + adc1, + adc2, + adc1_in_p, + adc2_in_p, + ) + .unwrap(); - // Configure the IO_Update signal for the DDS. - let mut hrtimer = hrtimer::HighResTimerE::new( - dp.HRTIM_TIME, - dp.HRTIM_MASTER, - dp.HRTIM_COMMON, - ccdr.clocks, - ccdr.peripheral.HRTIM, - ); + let dds_output = { + let io_update_trigger = { + let _io_update = gpiog + .pg7 + .into_alternate_af2() + .set_speed(hal::gpio::Speed::VeryHigh); - // IO_Update should be latched for 50ns after the QSPI profile write. Profile writes - // are always 16 bytes, with 2 cycles required per byte, coming out to a total of 32 - // QSPI clock cycles. The QSPI is configured for 40MHz, so this comes out to an - // offset of 800nS. We use 900ns to be safe - note that the timer is triggered after - // the QSPI write, which can take approximately 120nS, so there is additional - // margin. - hrtimer.configure_single_shot( - hrtimer::Channel::Two, - 50_e-9, - 900_e-9, - ); + // Configure the IO_Update signal for the DDS. + let mut hrtimer = hrtimer::HighResTimerE::new( + dp.HRTIM_TIME, + dp.HRTIM_MASTER, + dp.HRTIM_COMMON, + ccdr.clocks, + ccdr.peripheral.HRTIM, + ); - // Ensure that we have enough time for an IO-update every sample. - assert!(1.0 / (1000 * SAMPLE_FREQUENCY_KHZ) as f32 > 900_e-9); + // IO_Update should be latched for 4 SYNC_CLK cycles after the QSPI profile + // write. With pounder SYNC_CLK running at 100MHz (1/4 of the pounder reference + // clock of 400MHz), this corresponds to 40ns. To accomodate rounding errors, we + // use 50ns instead. + // + // Profile writes are always 16 bytes, with 2 cycles required per byte, coming + // out to a total of 32 QSPI clock cycles. The QSPI is configured for 40MHz, so + // this comes out to an offset of 800nS. We use 900ns to be safe - note that the + // timer is triggered after the QSPI write, which can take approximately 120nS, + // so there is additional margin. + hrtimer.configure_single_shot( + hrtimer::Channel::Two, + 50_e-9, + 900_e-9, + ); - hrtimer + // Ensure that we have enough time for an IO-update every sample. + assert!( + 1.0 / (1000 * SAMPLE_FREQUENCY_KHZ) as f32 > 900_e-9 + ); + + hrtimer + }; + + let (qspi, config) = ad9959.freeze(); + DdsOutput::new(qspi, io_update_trigger, config) }; - Some( - pounder::PounderDevices::new( - io_expander, - ad9959, - io_update_trigger, - spi, - adc1, - adc2, - adc1_in_p, - adc2_in_p, - ) - .unwrap(), - ) + (Some(pounder_devices), Some(dds_output)) } else { - None + (None, None) }; let mut eeprom_i2c = { @@ -780,7 +794,6 @@ const APP: () = { }; cp.SCB.enable_icache(); - //cp.SCB.enable_dcache(&mut cp.CPUID); // info!("Version {} {}", build_info::PKG_VERSION, build_info::GIT_VERSION.unwrap()); // info!("Built on {}", build_info::BUILT_TIME_UTC); @@ -808,7 +821,7 @@ const APP: () = { adcs, dacs, input_stamper, - + dds_output, pounder: pounder_devices, eeprom_i2c, @@ -818,7 +831,7 @@ const APP: () = { } } - #[task(binds=DMA1_STR3, resources=[adcs, dacs, iir_state, iir_ch, input_stamper], priority=2)] + #[task(binds=DMA1_STR3, resources=[adcs, dacs, iir_state, iir_ch, dds_output, input_stamper], priority=2)] fn process(c: process::Context) { let adc_samples = [ c.resources.adcs.0.acquire_buffer(), @@ -846,6 +859,18 @@ const APP: () = { dac_samples[channel][sample] = y as u16 ^ 0x8000; } } + + if let Some(dds_output) = c.resources.dds_output { + let builder = dds_output.builder().update_channels( + &[pounder::Channel::Out0.into()], + Some(u32::MAX / 4), + None, + None, + ); + + builder.write_profile(); + } + let [dac0, dac1] = dac_samples; c.resources.dacs.0.release_buffer(dac0); c.resources.dacs.1.release_buffer(dac1); diff --git a/src/pounder/dds_output.rs b/src/pounder/dds_output.rs new file mode 100644 index 0000000..418eb65 --- /dev/null +++ b/src/pounder/dds_output.rs @@ -0,0 +1,111 @@ +///! The DdsOutput is used as an output stream to the pounder DDS. +use super::QspiInterface; +use crate::hrtimer::HighResTimerE; +use ad9959::{Channel, DdsConfig, ProfileSerializer}; +use stm32h7xx_hal as hal; + +/// The DDS profile update stream. +pub struct DdsOutput { + _qspi: QspiInterface, + io_update_trigger: HighResTimerE, + config: DdsConfig, +} + +impl DdsOutput { + /// Construct a new DDS output stream. + /// + /// # Note + /// It is assumed that the QSPI stream and the IO_Update trigger timer have been configured in a + /// way such that the profile has sufficient time to be written before the IO_Update signal is + /// generated. + /// + /// # Args + /// * `qspi` - The QSPI interface to the run the stream on. + /// * `io_update_trigger` - The HighResTimerE used to generate IO_Update pulses. + /// * `dds_config` - The frozen DDS configuration. + pub fn new( + mut qspi: QspiInterface, + io_update_trigger: HighResTimerE, + dds_config: DdsConfig, + ) -> Self { + qspi.start_stream().unwrap(); + Self { + config: dds_config, + _qspi: qspi, + io_update_trigger, + } + } + + /// Get a builder for serializing a Pounder DDS profile. + pub fn builder(&mut self) -> ProfileBuilder { + let builder = self.config.builder(); + ProfileBuilder { + dds_stream: self, + serializer: builder, + } + } + + /// Write a profile to the stream. + /// + /// # Note: + /// If a profile of more than 4 words is provided, it is possible that the QSPI interface will + /// stall execution. + /// + /// # Args + /// * `profile` - The serialized DDS profile to write. + fn write_profile(&mut self, profile: &[u32]) { + // Note(unsafe): We own the QSPI interface, so it is safe to access the registers in a raw + // fashion. + let regs = unsafe { &*hal::stm32::QUADSPI::ptr() }; + + if regs.sr.read().flevel() != 0 { + warn!("QSPI stalling") + } + + for word in profile.iter() { + // Note(unsafe): We are writing to the SPI TX FIFO in a raw manner for performance. This + // is safe because we know the data register is a valid address to write to. + unsafe { + core::ptr::write_volatile( + ®s.dr as *const _ as *mut u32, + *word, + ); + } + } + + // Trigger the IO_update signal generating timer to asynchronous create the IO_Update pulse. + self.io_update_trigger.trigger(); + } +} + +/// A temporary builder for serializing and writing profiles. +pub struct ProfileBuilder<'a> { + dds_stream: &'a mut DdsOutput, + serializer: ProfileSerializer, +} + +impl<'a> ProfileBuilder<'a> { + /// Update a number of channels with the provided configuration + /// + /// # Args + /// * `channels` - A list of channels to apply the configuration to. + /// * `ftw` - If provided, indicates a frequency tuning word for the channels. + /// * `pow` - If provided, indicates a phase offset word for the channels. + /// * `acr` - If provided, indicates the amplitude control register for the channels. + pub fn update_channels( + mut self, + channels: &[Channel], + ftw: Option, + pow: Option, + acr: Option, + ) -> Self { + self.serializer.update_channels(channels, ftw, pow, acr); + self + } + + /// Write the profile to the DDS asynchronously. + pub fn write_profile(mut self) { + let profile = self.serializer.finalize(); + self.dds_stream.write_profile(profile); + } +} diff --git a/src/pounder/mod.rs b/src/pounder/mod.rs index 255851a..c20d251 100644 --- a/src/pounder/mod.rs +++ b/src/pounder/mod.rs @@ -1,10 +1,12 @@ use serde::{Deserialize, Serialize}; mod attenuators; +mod dds_output; mod rf_power; +pub use dds_output::DdsOutput; + use super::hal; -use super::hrtimer::HighResTimerE; use attenuators::AttenuatorInterface; use rf_power::PowerMeasurementInterface; @@ -34,6 +36,7 @@ pub enum Error { } #[derive(Debug, Copy, Clone)] +#[allow(dead_code)] pub enum Channel { In0, In1, @@ -133,34 +136,6 @@ impl QspiInterface { Ok(()) } - - pub fn write_profile(&mut self, data: [u32; 4]) -> Result<(), Error> { - if self.streaming == false { - return Err(Error::Qspi); - } - - let qspi_regs = unsafe { &*hal::stm32::QUADSPI::ptr() }; - unsafe { - core::ptr::write_volatile( - &qspi_regs.dr as *const _ as *mut u32, - data[0], - ); - core::ptr::write_volatile( - &qspi_regs.dr as *const _ as *mut u32, - data[1], - ); - core::ptr::write_volatile( - &qspi_regs.dr as *const _ as *mut u32, - data[2], - ); - core::ptr::write_volatile( - &qspi_regs.dr as *const _ as *mut u32, - data[3], - ); - } - - Ok(()) - } } impl ad9959::Interface for QspiInterface { @@ -287,8 +262,6 @@ impl ad9959::Interface for QspiInterface { /// A structure containing implementation for Pounder hardware. pub struct PounderDevices { - pub ad9959: ad9959::Ad9959, - pub io_update_trigger: HighResTimerE, mcp23017: mcp23017::MCP23017>, attenuator_spi: hal::spi::Spi, adc1: hal::adc::Adc, @@ -303,15 +276,13 @@ impl PounderDevices { /// Args: /// * `ad9959` - The DDS driver for the pounder hardware. /// * `attenuator_spi` - A SPI interface to control digital attenuators. - /// * `io_update_timer` - The HRTimer with the IO_update signal connected to the output. /// * `adc1` - The ADC1 peripheral for measuring power. /// * `adc2` - The ADC2 peripheral for measuring power. /// * `adc1_in_p` - The input channel for the RF power measurement on IN0. /// * `adc2_in_p` - The input channel for the RF power measurement on IN1. pub fn new( mcp23017: mcp23017::MCP23017>, - ad9959: ad9959::Ad9959, - io_update_trigger: HighResTimerE, + ad9959: &mut ad9959::Ad9959, attenuator_spi: hal::spi::Spi, adc1: hal::adc::Adc, adc2: hal::adc::Adc, @@ -320,8 +291,6 @@ impl PounderDevices { ) -> Result { let mut devices = Self { mcp23017, - io_update_trigger, - ad9959, attenuator_spi, adc1, adc2, @@ -346,114 +315,15 @@ impl PounderDevices { .map_err(|_| Error::I2c)?; // Select the on-board clock with a 4x prescaler (400MHz). - devices.select_onboard_clock(4u8)?; - - // Run the DDS in stream-only mode (no read support). - devices.ad9959.interface.start_stream().unwrap(); - - Ok(devices) - } - - /// Select the an external for the DDS reference clock source. - /// - /// Args: - /// * `frequency` - The frequency of the external clock source. - /// * `multiplier` - The multiplier of the reference clock to use in the DDS. - fn select_external_clock( - &mut self, - frequency: f32, - prescaler: u8, - ) -> Result<(), Error> { - self.mcp23017 - .digital_write(EXT_CLK_SEL_PIN, true) - .map_err(|_| Error::I2c)?; - self.ad9959 - .configure_system_clock(frequency, prescaler) - .map_err(|_| Error::Dds)?; - - Ok(()) - } - - /// Select the onboard oscillator for the DDS reference clock source. - /// - /// Args: - /// * `multiplier` - The multiplier of the reference clock to use in the DDS. - fn select_onboard_clock(&mut self, multiplier: u8) -> Result<(), Error> { - self.mcp23017 + devices + .mcp23017 .digital_write(EXT_CLK_SEL_PIN, false) .map_err(|_| Error::I2c)?; - self.ad9959 - .configure_system_clock(100_000_000f32, multiplier) + ad9959 + .configure_system_clock(100_000_000f32, 4) .map_err(|_| Error::Dds)?; - Ok(()) - } - - /// Configure the Pounder DDS clock. - /// - /// Args: - /// * `config` - The configuration of the DDS clock desired. - pub fn configure_dds_clock( - &mut self, - config: DdsClockConfig, - ) -> Result<(), Error> { - if config.external_clock { - self.select_external_clock( - config.reference_clock, - config.multiplier, - ) - } else { - self.select_onboard_clock(config.multiplier) - } - } - - /// Get the pounder DDS clock configuration - /// - /// Returns: - /// The current pounder DDS clock configuration. - pub fn get_dds_clock_config(&mut self) -> Result { - let external_clock = self - .mcp23017 - .digital_read(EXT_CLK_SEL_PIN) - .map_err(|_| Error::I2c)?; - let multiplier = self - .ad9959 - .get_reference_clock_multiplier() - .map_err(|_| Error::Dds)?; - let reference_clock = self.ad9959.get_reference_clock_frequency(); - - Ok(DdsClockConfig { - multiplier, - reference_clock, - external_clock, - }) - } - - /// Configure a DDS channel. - /// - /// Args: - /// * `channel` - The pounder channel to configure. - /// * `state` - The state to configure the channel for. - pub fn set_channel_state( - &mut self, - channel: Channel, - state: ChannelState, - ) -> Result<(), Error> { - let profile = self - .ad9959 - .serialize_profile( - channel.into(), - state.parameters.frequency, - state.parameters.phase_offset, - state.parameters.amplitude, - ) - .map_err(|_| Error::Dds)?; - self.ad9959.interface.write_profile(profile).unwrap(); - self.io_update_trigger.trigger(); - - self.set_attenuation(channel, state.attenuation)?; - - Ok(()) + Ok(devices) } }