diff --git a/ad9959/src/lib.rs b/ad9959/src/lib.rs index 446825f..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. /// @@ -13,12 +13,11 @@ 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 struct Ad9959 { interface: INTERFACE, - delay: DELAY, reference_clock_frequency: f32, system_clock_multiplier: u8, - io_update: UPDATE, + communication_mode: Mode, } /// A trait that allows a HAL to provide a means of communicating with the AD9959. @@ -73,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, @@ -90,12 +90,7 @@ pub enum Error { Frequency, } -impl Ad9959 -where - INTERFACE: Interface, - DELAY: DelayMs, - UPDATE: OutputPin, -{ +impl Ad9959 { /// Construct and initialize the DDS. /// /// Args: @@ -107,35 +102,31 @@ where /// * `clock_frequency` - The clock frequency of the reference clock input. /// * `multiplier` - The desired clock multiplier for the system clock. This multiplies /// `clock_frequency` to generate the system clock. - pub fn new( - interface: INTERFACE, - reset_pin: &mut RST, - io_update: UPDATE, - delay: DELAY, + pub fn new( + interface: I, + mut reset_pin: impl OutputPin, + io_update: &mut impl OutputPin, + delay: &mut impl DelayUs, desired_mode: Mode, clock_frequency: f32, multiplier: u8, - ) -> Result - where - RST: OutputPin, - { + ) -> Result { let mut ad9959 = Ad9959 { interface, - io_update, - delay, reference_clock_frequency: clock_frequency, system_clock_multiplier: 1, + communication_mode: desired_mode, }; - ad9959.io_update.set_low().or(Err(Error::Pin))?; + io_update.set_low().or(Err(Error::Pin))?; // Reset the AD9959 reset_pin.set_high().or(Err(Error::Pin))?; - // Delay for a clock cycle to allow the device to reset. - ad9959 - .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))?; @@ -149,14 +140,29 @@ where csr[0].set_bits(1..3, desired_mode as u8); ad9959.write(Register::CSR, &csr)?; - // Latch the configuration registers to make them active. - ad9959.latch_configuration()?; + // Latch the new interface configuration. + 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)?; @@ -181,18 +187,6 @@ where .or(Err(Error::Interface)) } - /// Latch the DDS configuration to ensure it is active on the output channels. - fn latch_configuration(&mut self) -> Result<(), Error> { - self.io_update.set_high().or(Err(Error::Pin))?; - // The SYNC_CLK is 1/4 the system clock frequency. The IO_UPDATE pin must be latched for one - // full SYNC_CLK pulse to register. For safety, we latch for 5 here. - self.delay - .delay_ms((5000.0 / self.system_clock_frequency()) as u8); - self.io_update.set_low().or(Err(Error::Pin))?; - - Ok(()) - } - /// Configure the internal system clock of the chip. /// /// Arguments: @@ -205,7 +199,7 @@ where &mut self, reference_clock_frequency: f32, multiplier: u8, - ) -> Result { + ) -> Result { self.reference_clock_frequency = reference_clock_frequency; if multiplier != 1 && !(4..=20).contains(&multiplier) { @@ -213,8 +207,8 @@ where } let frequency = - multiplier as f64 * self.reference_clock_frequency as f64; - if frequency > 500_000_000.0f64 { + multiplier as f32 * self.reference_clock_frequency as f32; + if frequency > 500_000_000.0f32 { return Err(Error::Frequency); } @@ -287,37 +281,9 @@ where } /// Get the current system clock frequency in Hz. - fn system_clock_frequency(&self) -> f64 { - self.system_clock_multiplier as f64 - * self.reference_clock_frequency as f64 - } - - /// 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)) + fn system_clock_frequency(&self) -> f32 { + self.system_clock_multiplier as f32 + * self.reference_clock_frequency as f32 } /// Update an output channel configuration register. @@ -334,22 +300,16 @@ where ) -> 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)?; - // Latch the configuration and restore the previous CSR. Note that the re-enable of the - // channel happens immediately, so the CSR update does not need to be latched. - self.latch_configuration()?; - self.write(Register::CSR, &csr)?; - Ok(()) } @@ -494,8 +454,8 @@ where pub fn set_frequency( &mut self, channel: Channel, - frequency: f64, - ) -> Result { + frequency: f32, + ) -> Result { if frequency < 0.0 || frequency > self.system_clock_frequency() { return Err(Error::Bounds); } @@ -503,15 +463,15 @@ where // 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 = - ((frequency as f64 / self.system_clock_frequency()) - * 1u64.wrapping_shl(32) as f64) as u32; + ((frequency as f32 / self.system_clock_frequency()) + * 1u64.wrapping_shl(32) as f32) as u32; self.modify_channel( channel, Register::CFTW0, &tuning_word.to_be_bytes(), )?; - Ok((tuning_word as f64 / 1u64.wrapping_shl(32) as f64) + Ok((tuning_word as f32 / 1u64.wrapping_shl(32) as f32) * self.system_clock_frequency()) } @@ -522,14 +482,135 @@ where /// /// Returns: /// The frequency of the channel in Hz. - pub fn get_frequency(&mut self, channel: Channel) -> Result { + pub fn get_frequency(&mut self, channel: Channel) -> Result { // Read the frequency tuning word for the channel. let mut tuning_word: [u8; 4] = [0; 4]; self.read_channel(channel, Register::CFTW0, &mut tuning_word)?; let tuning_word = u32::from_be_bytes(tuning_word); // Convert the tuning word into a frequency. - Ok(tuning_word as f64 * self.system_clock_frequency() - / (1u64 << 32) as f64) + Ok((tuning_word as f32 * self.system_clock_frequency()) + / (1u64 << 32) as f32) + } + + /// 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 a50856a..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': 31.5, - 'parameters': { - 'phase_offset': 0.5, - 'frequency': 100.0e6, - 'amplitude': 0.2, - '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 new file mode 100644 index 0000000..2831bbe --- /dev/null +++ b/src/hrtimer.rs @@ -0,0 +1,132 @@ +///! 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, + common: hal::stm32::HRTIM_COMMON, + + clocks: CoreClocks, +} + +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, + common_regs: hal::stm32::HRTIM_COMMON, + clocks: CoreClocks, + prec: rec::Hrtim, + ) -> Self { + prec.reset().enable(); + + Self { + master: master_regs, + timer: timer_regs, + common: common_regs, + clocks, + } + } + + /// 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, + set_duration: f32, + set_offset: f32, + ) { + // Disable the timer before configuration. + self.master.mcr.modify(|_, w| w.tecen().clear_bit()); + + // Configure the desired timer for single shot mode with set and reset of the specified + // channel at the desired durations. The HRTIM is on APB2 (D2 domain), and the kernel clock + // is the APB bus clock. + let minimum_duration = set_duration + set_offset; + + let source_frequency: u32 = self.clocks.timy_ker_ck().0; + let source_cycles = + (minimum_duration * source_frequency as f32) as u32 + 1; + + // 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 setting: u8 = if source_cycles < 0xFFDF { + 1 + } else if (source_cycles / 2) < 0xFFDF { + 2 + } else if (source_cycles / 4) < 0xFFDF { + 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(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) }); + + // Configure the set/reset signals. + // Set on compare with CMP1, reset upon reaching PER + match channel { + Channel::One => { + self.timer.sete1r.write(|w| w.cmp1().set_bit()); + self.timer.rste1r.write(|w| w.per().set_bit()); + self.common.oenr.write(|w| w.te1oen().set_bit()); + } + Channel::Two => { + self.timer.sete2r.write(|w| w.cmp1().set_bit()); + self.timer.rste2r.write(|w| w.per().set_bit()); + self.common.oenr.write(|w| w.te2oen().set_bit()); + } + } + + // Enable the timer now that it is configured. + 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 1ee6415..b37f740 100644 --- a/src/main.rs +++ b/src/main.rs @@ -71,6 +71,7 @@ mod afe; mod dac; mod design_parameters; mod eeprom; +mod hrtimer; mod pounder; mod sampling_timer; mod server; @@ -78,6 +79,7 @@ mod server; use adc::{Adc0Input, Adc1Input}; use dac::{Dac0Output, Dac1Output}; use dsp::iir; +use pounder::DdsOutput; #[cfg(not(feature = "semihosting"))] fn init_log() {} @@ -200,6 +202,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] @@ -211,7 +215,7 @@ const APP: () = { eth_mac: ethernet::phy::LAN8742A, mac_addr: net::wire::EthernetAddress, - pounder: Option>, + pounder: Option, // Format: iir_state[ch][cascade-no][coeff] #[init([[[0.; 5]; IIR_CASCADE_LENGTH]; 2])] @@ -259,7 +263,7 @@ const APP: () = { let gpiod = dp.GPIOD.split(ccdr.peripheral.GPIOD); let gpioe = dp.GPIOE.split(ccdr.peripheral.GPIOE); let gpiof = dp.GPIOF.split(ccdr.peripheral.GPIOF); - let gpiog = dp.GPIOG.split(ccdr.peripheral.GPIOG); + let mut gpiog = dp.GPIOG.split(ccdr.peripheral.GPIOG); let afe0 = { let a0_pin = gpiof.pf2.into_push_pull_output(); @@ -465,8 +469,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 = { @@ -502,33 +507,32 @@ const APP: () = { let qspi = hal::qspi::Qspi::bank2( dp.QUADSPI, qspi_pins, - 10.mhz(), + 40.mhz(), &ccdr.clocks, ccdr.peripheral.QSPI, ); + pounder::QspiInterface::new(qspi).unwrap() }; - let mut reset_pin = gpioa.pa0.into_push_pull_output(); - let io_update = gpiog.pg7.into_push_pull_output(); + let reset_pin = gpioa.pa0.into_push_pull_output(); + let mut io_update = gpiog.pg7.into_push_pull_output(); - let asm_delay = { - let frequency_hz = ccdr.clocks.c_ck().0; - asm_delay::AsmDelay::new(asm_delay::bitrate::Hertz( - frequency_hz, - )) - }; - - ad9959::Ad9959::new( + let ad9959 = ad9959::Ad9959::new( qspi_interface, - &mut reset_pin, - io_update, - asm_delay, + reset_pin, + &mut io_update, + &mut delay, ad9959::Mode::FourBitSerial, - 100_000_000f32, + 100_000_000_f32, 5, ) - .unwrap() + .unwrap(); + + // Return IO_Update + gpiog.pg7 = io_update.into_analog(); + + ad9959 }; let io_expander = { @@ -598,20 +602,64 @@ const APP: () = { let adc1_in_p = gpiof.pf11.into_analog(); let adc2_in_p = gpiof.pf14.into_analog(); - Some( - pounder::PounderDevices::new( - io_expander, - ad9959, - spi, - adc1, - adc2, - adc1_in_p, - adc2_in_p, - ) - .unwrap(), + let pounder_devices = pounder::PounderDevices::new( + io_expander, + &mut ad9959, + spi, + adc1, + adc2, + adc1_in_p, + adc2_in_p, ) + .unwrap(); + + let dds_output = { + let io_update_trigger = { + let _io_update = gpiog + .pg7 + .into_alternate_af2() + .set_speed(hal::gpio::Speed::VeryHigh); + + // 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, + ); + + // 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, + ); + + // 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_devices), Some(dds_output)) } else { - None + (None, None) }; let mut eeprom_i2c = { @@ -741,7 +789,7 @@ const APP: () = { adcs, dacs, - + dds_output, pounder: pounder_devices, eeprom_i2c, @@ -751,7 +799,7 @@ const APP: () = { } } - #[task(binds=DMA1_STR3, resources=[adcs, dacs, iir_state, iir_ch], priority=2)] + #[task(binds=DMA1_STR3, resources=[adcs, dacs, iir_state, iir_ch, dds_output], priority=2)] fn process(c: process::Context) { let adc_samples = [ c.resources.adcs.0.acquire_buffer(), @@ -777,6 +825,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); @@ -856,41 +916,7 @@ const APP: () = { Ok::(state) }), "stabilizer/afe0/gain": (|| c.resources.afes.0.get_gain()), - "stabilizer/afe1/gain": (|| c.resources.afes.1.get_gain()), - "pounder/in0": (|| { - match c.resources.pounder { - Some(pounder) => - pounder.get_input_channel_state(pounder::Channel::In0), - _ => Err(pounder::Error::Access), - } - }), - "pounder/in1": (|| { - match c.resources.pounder { - Some(pounder) => - pounder.get_input_channel_state(pounder::Channel::In1), - _ => Err(pounder::Error::Access), - } - }), - "pounder/out0": (|| { - match c.resources.pounder { - Some(pounder) => - pounder.get_output_channel_state(pounder::Channel::Out0), - _ => Err(pounder::Error::Access), - } - }), - "pounder/out1": (|| { - match c.resources.pounder { - Some(pounder) => - pounder.get_output_channel_state(pounder::Channel::Out1), - _ => Err(pounder::Error::Access), - } - }), - "pounder/dds/clock": (|| { - match c.resources.pounder { - Some(pounder) => pounder.get_dds_clock_config(), - _ => Err(pounder::Error::Access), - } - }) + "stabilizer/afe1/gain": (|| c.resources.afes.1.get_gain()) ], modifiable_attributes: [ @@ -938,40 +964,6 @@ const APP: () = { Ok::(req) }) }), - "pounder/in0": pounder::ChannelState, (|state| { - match c.resources.pounder { - Some(pounder) => - pounder.set_channel_state(pounder::Channel::In0, state), - _ => Err(pounder::Error::Access), - } - }), - "pounder/in1": pounder::ChannelState, (|state| { - match c.resources.pounder { - Some(pounder) => - pounder.set_channel_state(pounder::Channel::In1, state), - _ => Err(pounder::Error::Access), - } - }), - "pounder/out0": pounder::ChannelState, (|state| { - match c.resources.pounder { - Some(pounder) => - pounder.set_channel_state(pounder::Channel::Out0, state), - _ => Err(pounder::Error::Access), - } - }), - "pounder/out1": pounder::ChannelState, (|state| { - match c.resources.pounder { - Some(pounder) => - pounder.set_channel_state(pounder::Channel::Out1, state), - _ => Err(pounder::Error::Access), - } - }), - "pounder/dds/clock": pounder::DdsClockConfig, (|config| { - match c.resources.pounder { - Some(pounder) => pounder.configure_dds_clock(config), - _ => Err(pounder::Error::Access), - } - }), "stabilizer/afe0/gain": afe::Gain, (|gain| { c.resources.afes.0.set_gain(gain); Ok::<(), ()>(()) 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 8054243..c20d251 100644 --- a/src/pounder/mod.rs +++ b/src/pounder/mod.rs @@ -1,8 +1,11 @@ use serde::{Deserialize, Serialize}; mod attenuators; +mod dds_output; mod rf_power; +pub use dds_output::DdsOutput; + use super::hal; use attenuators::AttenuatorInterface; @@ -33,6 +36,7 @@ pub enum Error { } #[derive(Debug, Copy, Clone)] +#[allow(dead_code)] pub enum Channel { In0, In1, @@ -43,7 +47,7 @@ pub enum Channel { #[derive(Serialize, Deserialize, Copy, Clone, Debug)] pub struct DdsChannelState { pub phase_offset: f32, - pub frequency: f64, + pub frequency: f32, pub amplitude: f32, pub enabled: bool, } @@ -90,6 +94,7 @@ impl Into for Channel { pub struct QspiInterface { pub qspi: hal::qspi::Qspi, mode: ad9959::Mode, + streaming: bool, } impl QspiInterface { @@ -106,8 +111,31 @@ impl QspiInterface { Ok(Self { qspi, mode: ad9959::Mode::SingleBitTwoWire, + streaming: false, }) } + + pub fn start_stream(&mut self) -> Result<(), Error> { + if self.qspi.is_busy() { + return Err(Error::Qspi); + } + + // Configure QSPI for infinite transaction mode using only a data phase (no instruction or + // address). + let qspi_regs = unsafe { &*hal::stm32::QUADSPI::ptr() }; + qspi_regs.fcr.modify(|_, w| w.ctcf().set_bit()); + + unsafe { + qspi_regs.dlr.write(|w| w.dl().bits(0xFFFF_FFFF)); + qspi_regs.ccr.modify(|_, w| { + w.imode().bits(0).fmode().bits(0).admode().bits(0) + }); + } + + self.streaming = true; + + Ok(()) + } } impl ad9959::Interface for QspiInterface { @@ -205,13 +233,18 @@ impl ad9959::Interface for QspiInterface { .map_err(|_| Error::Qspi) } ad9959::Mode::FourBitSerial => { - self.qspi.write(addr, &data).map_err(|_| Error::Qspi) + if self.streaming { + Err(Error::Qspi) + } else { + self.qspi.write(addr, data).map_err(|_| Error::Qspi)?; + Ok(()) + } } _ => Err(Error::Qspi), } } - fn read(&mut self, addr: u8, mut dest: &mut [u8]) -> Result<(), Error> { + fn read(&mut self, addr: u8, dest: &mut [u8]) -> Result<(), Error> { if (addr & 0x80) != 0 { return Err(Error::InvalidAddress); } @@ -222,18 +255,13 @@ impl ad9959::Interface for QspiInterface { } self.qspi - .read(0x80_u8 | addr, &mut dest) + .read(0x80_u8 | addr, dest) .map_err(|_| Error::Qspi) } } /// A structure containing implementation for Pounder hardware. -pub struct PounderDevices { - pub ad9959: ad9959::Ad9959< - QspiInterface, - DELAY, - hal::gpio::gpiog::PG7>, - >, +pub struct PounderDevices { mcp23017: mcp23017::MCP23017>, attenuator_spi: hal::spi::Spi, adc1: hal::adc::Adc, @@ -242,10 +270,7 @@ pub struct PounderDevices { adc2_in_p: hal::gpio::gpiof::PF14, } -impl PounderDevices -where - DELAY: embedded_hal::blocking::delay::DelayMs, -{ +impl PounderDevices { /// Construct and initialize pounder-specific hardware. /// /// Args: @@ -257,11 +282,7 @@ where /// * `adc2_in_p` - The input channel for the RF power measurement on IN1. pub fn new( mcp23017: mcp23017::MCP23017>, - ad9959: ad9959::Ad9959< - QspiInterface, - DELAY, - hal::gpio::gpiog::PG7>, - >, + ad9959: &mut ad9959::Ad9959, attenuator_spi: hal::spi::Spi, adc1: hal::adc::Adc, adc2: hal::adc::Adc, @@ -270,7 +291,6 @@ where ) -> Result { let mut devices = Self { mcp23017, - ad9959, attenuator_spi, adc1, adc2, @@ -295,212 +315,19 @@ where .map_err(|_| Error::I2c)?; // Select the on-board clock with a 4x prescaler (400MHz). - devices.select_onboard_clock(4u8)?; + devices + .mcp23017 + .digital_write(EXT_CLK_SEL_PIN, false) + .map_err(|_| Error::I2c)?; + ad9959 + .configure_system_clock(100_000_000f32, 4) + .map_err(|_| Error::Dds)?; 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 - .digital_write(EXT_CLK_SEL_PIN, false) - .map_err(|_| Error::I2c)?; - self.ad9959 - .configure_system_clock(100_000_000f32, multiplier) - .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, - }) - } - - /// Get the state of a Pounder input channel. - /// - /// Args: - /// * `channel` - The pounder channel to get the state of. Must be an input channel - /// - /// Returns: - /// The read-back channel input state. - pub fn get_input_channel_state( - &mut self, - channel: Channel, - ) -> Result { - match channel { - Channel::In0 | Channel::In1 => { - let channel_state = self.get_dds_channel_state(channel)?; - - let attenuation = self.get_attenuation(channel)?; - let power = self.measure_power(channel)?; - - Ok(InputChannelState { - attenuation, - power, - mixer: channel_state, - }) - } - _ => Err(Error::InvalidChannel), - } - } - - /// Get the state of a DDS channel. - /// - /// Args: - /// * `channel` - The pounder channel to get the state of. - /// - /// Returns: - /// The read-back channel state. - fn get_dds_channel_state( - &mut self, - channel: Channel, - ) -> Result { - let frequency = self - .ad9959 - .get_frequency(channel.into()) - .map_err(|_| Error::Dds)?; - let phase_offset = self - .ad9959 - .get_phase(channel.into()) - .map_err(|_| Error::Dds)?; - let amplitude = self - .ad9959 - .get_amplitude(channel.into()) - .map_err(|_| Error::Dds)?; - let enabled = self - .ad9959 - .is_enabled(channel.into()) - .map_err(|_| Error::Dds)?; - - Ok(DdsChannelState { - phase_offset, - frequency, - amplitude, - enabled, - }) - } - - /// Get the state of a DDS output channel. - /// - /// Args: - /// * `channel` - The pounder channel to get the output state of. Must be an output channel. - /// - /// Returns: - /// The read-back output channel state. - pub fn get_output_channel_state( - &mut self, - channel: Channel, - ) -> Result { - match channel { - Channel::Out0 | Channel::Out1 => { - let channel_state = self.get_dds_channel_state(channel)?; - let attenuation = self.get_attenuation(channel)?; - - Ok(OutputChannelState { - attenuation, - channel: channel_state, - }) - } - _ => Err(Error::InvalidChannel), - } - } - - /// 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> { - self.ad9959 - .set_frequency(channel.into(), state.parameters.frequency) - .map_err(|_| Error::Dds)?; - self.ad9959 - .set_phase(channel.into(), state.parameters.phase_offset) - .map_err(|_| Error::Dds)?; - self.ad9959 - .set_amplitude(channel.into(), state.parameters.amplitude) - .map_err(|_| Error::Dds)?; - - if state.parameters.enabled { - self.ad9959 - .enable_channel(channel.into()) - .map_err(|_| Error::Dds)?; - } else { - self.ad9959 - .disable_channel(channel.into()) - .map_err(|_| Error::Dds)?; - } - - self.set_attenuation(channel, state.attenuation)?; - - Ok(()) - } } -impl AttenuatorInterface for PounderDevices { +impl AttenuatorInterface for PounderDevices { /// Reset all of the attenuators to a power-on default state. fn reset_attenuators(&mut self) -> Result<(), Error> { self.mcp23017 @@ -572,7 +399,7 @@ impl AttenuatorInterface for PounderDevices { } } -impl PowerMeasurementInterface for PounderDevices { +impl PowerMeasurementInterface for PounderDevices { /// Sample an ADC channel. /// /// Args: