From 80661a51fbcff7597ebf10f23923fbac258b9d76 Mon Sep 17 00:00:00 2001 From: Ryan Summers Date: Mon, 8 Jun 2020 18:20:10 +0200 Subject: [PATCH] Reimporting pounder-specific code --- Cargo.lock | 15 ++ Cargo.toml | 1 + ad9959/.gitignore | 2 + ad9959/Cargo.toml | 11 + ad9959/src/lib.rs | 350 ++++++++++++++++++++++++++ stabilizer/Cargo.toml | 3 + stabilizer/src/main.rs | 89 +++++++ stabilizer/src/pounder/attenuators.rs | 57 +++++ stabilizer/src/pounder/error.rs | 11 + stabilizer/src/pounder/mod.rs | 265 +++++++++++++++++++ stabilizer/src/pounder/rf_power.rs | 14 ++ stabilizer/src/pounder/types.rs | 16 ++ 12 files changed, 834 insertions(+) create mode 100644 ad9959/.gitignore create mode 100644 ad9959/Cargo.toml create mode 100644 ad9959/src/lib.rs create mode 100644 stabilizer/src/pounder/attenuators.rs create mode 100644 stabilizer/src/pounder/error.rs create mode 100644 stabilizer/src/pounder/mod.rs create mode 100644 stabilizer/src/pounder/rf_power.rs create mode 100644 stabilizer/src/pounder/types.rs diff --git a/Cargo.lock b/Cargo.lock index 3957ff6..bb74654 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,13 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "ad9959" +version = "0.1.0" +dependencies = [ + "bit_field", + "embedded-hal", +] + [[package]] name = "aligned" version = "0.3.2" @@ -46,6 +54,12 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "bit_field" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a165d606cf084741d4ac3a28fb6e9b1eb0bd31f6cd999098cfddb0b2ab381dc0" + [[package]] name = "bitflags" version = "1.2.1" @@ -427,6 +441,7 @@ dependencies = [ name = "stabilizer" version = "0.3.0" dependencies = [ + "ad9959", "asm-delay", "cortex-m", "cortex-m-log", diff --git a/Cargo.toml b/Cargo.toml index ac00002..05df738 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "stabilizer", "stm32h7xx-hal", + "ad9959", ] [profile.dev] diff --git a/ad9959/.gitignore b/ad9959/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/ad9959/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/ad9959/Cargo.toml b/ad9959/Cargo.toml new file mode 100644 index 0000000..f234470 --- /dev/null +++ b/ad9959/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "ad9959" +version = "0.1.0" +authors = ["Ryan Summers "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +embedded-hal = {version = "0.2.3", features = ["unproven"]} +bit_field = "0.10.0" diff --git a/ad9959/src/lib.rs b/ad9959/src/lib.rs new file mode 100644 index 0000000..5346bdc --- /dev/null +++ b/ad9959/src/lib.rs @@ -0,0 +1,350 @@ +#![no_std] + +use bit_field::BitField; +use embedded_hal::{ + digital::v2::OutputPin, + blocking::delay::DelayMs, +}; + +/// A device driver for the AD9959 direct digital synthesis (DDS) chip. +/// +/// This chip provides four independently controllable digital-to-analog output sinusoids with +/// configurable phase, amplitude, and frequency. All channels are inherently synchronized as they +/// are derived off a common system clock. +/// +/// The chip contains a configurable PLL and supports system clock frequencies up to 500 MHz. +/// +/// The chip supports a number of serial interfaces to improve data throughput, including normal, +/// dual, and quad SPI configurations. +pub struct Ad9959 { + interface: INTERFACE, + delay: DELAY, + reference_clock_frequency: u32, + system_clock_multiplier: u8, + io_update: UPDATE, +} + +pub trait Interface { + type Error; + + fn configure_mode(&mut self, mode: Mode) -> Result<(), Self::Error>; + + fn write(&mut self, addr: u8, data: &[u8]) -> Result<(), Self::Error>; + + fn read(&mut self, addr: u8, dest: &mut [u8]) -> Result<(), Self::Error>; +} + +#[derive(Copy, Clone, PartialEq)] +pub enum Mode { + SingleBitTwoWire = 0b00, + SingleBitThreeWire = 0b01, + TwoBitSerial = 0b10, + FourBitSerial = 0b11, +} + +/// The configuration registers within the AD9959 DDS device. The values of each register are +/// equivalent to the address. +pub enum Register { + CSR = 0x00, + FR1 = 0x01, + FR2 = 0x02, + CFR = 0x03, + CFTW0 = 0x04, + CPOW0 = 0x05, + ACR = 0x06, + LSRR = 0x07, + RDW = 0x08, + FDW = 0x09, + CW1 = 0x0a, + CW2 = 0x0b, + CW3 = 0x0c, + CW4 = 0x0d, + CW5 = 0x0e, + CW6 = 0x0f, + CW7 = 0x10, + CW8 = 0x11, + CW9 = 0x12, + CW10 = 0x13, + CW11 = 0x14, + CW12 = 0x15, + CW13 = 0x16, + CW14 = 0x17, + CW15 = 0x18, +} + +/// Specifies an output channel of the AD9959 DDS chip. +pub enum Channel { + One = 0, + Two = 1, + Three = 2, + Four = 3, +} + +/// Possible errors generated by the AD9959 driver. +#[derive(Debug)] +pub enum Error { + Interface(InterfaceE), + Bounds, + Pin, + Frequency, +} + +impl From for Error { + fn from(interface_error: InterfaceE) -> Self { + Error::Interface(interface_error) + } +} + +impl Ad9959 +where + INTERFACE: Interface, + DELAY: DelayMs, + UPDATE: OutputPin, + +{ + pub fn new(interface: INTERFACE, + reset_pin: &mut RST, + io_update: UPDATE, + delay: DELAY, + desired_mode: Mode, + clock_frequency: u32, + multiplier: u8) -> Result> + where + RST: OutputPin, + { + let mut ad9959 = Ad9959 { + interface: interface, + io_update: io_update, + delay: delay, + reference_clock_frequency: clock_frequency, + system_clock_multiplier: 1, + }; + + ad9959.io_update.set_low().or_else(|_| Err(Error::Pin))?; + + // Reset the AD9959 + reset_pin.set_high().or_else(|_| 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); + + reset_pin.set_low().or_else(|_| Err(Error::Pin))?; + + ad9959.interface.configure_mode(Mode::SingleBitTwoWire)?; + + // Program the interface configuration in the AD9959. Default to all channels enabled. + let mut csr: [u8; 1] = [0xF0]; + csr[0].set_bits(1..3, desired_mode as u8); + ad9959.interface.write(0, &csr)?; + + // Configure the interface to the desired mode. + ad9959.interface.configure_mode(Mode::FourBitSerial)?; + + // Latch the configuration registers to make them active. + ad9959.latch_configuration()?; + + ad9959.interface.configure_mode(desired_mode)?; + + // Set the clock frequency to configure the device as necessary. + ad9959.configure_system_clock(clock_frequency, multiplier)?; + Ok(ad9959) + } + + fn latch_configuration(&mut self) -> Result<(), Error> { + self.io_update.set_high().or_else(|_| 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_else(|_| Err(Error::Pin))?; + + Ok(()) + } + + /// Configure the internal system clock of the chip. + /// + /// Arguments: + /// * `reference_clock_frequency` - The reference clock frequency provided to the AD9959 core. + /// * `prescaler` - The frequency prescaler of the system clock. Must be 1 or 4-20. + /// + /// Returns: + /// The actual frequency configured for the internal system clock. + pub fn configure_system_clock(&mut self, + reference_clock_frequency: u32, + prescaler: u8) -> Result> + { + self.reference_clock_frequency = reference_clock_frequency; + + if prescaler != 1 && (prescaler > 20 || prescaler < 4) { + return Err(Error::Bounds); + } + + let frequency = prescaler as f64 * self.reference_clock_frequency as f64; + if frequency > 500_000_000.0f64 { + return Err(Error::Frequency); + } + + // TODO: Update / disable any enabled channels? + let mut fr1: [u8; 3] = [0, 0, 0]; + self.interface.read(Register::FR1 as u8, &mut fr1)?; + fr1[0].set_bits(2..=6, prescaler); + + let vco_range = frequency > 255e6; + fr1[0].set_bit(7, vco_range); + + self.interface.write(Register::FR1 as u8, &fr1)?; + self.system_clock_multiplier = prescaler; + + Ok(self.system_clock_frequency()) + } + + /// Perform a self-test of the communication interface. + /// + /// Note: + /// This modifies the existing channel enables. They are restored upon exit. + /// + /// Returns: + /// True if the self test succeeded. False otherwise. + pub fn self_test(&mut self) -> Result> { + let mut csr: [u8; 1] = [0]; + self.interface.read(Register::CSR as u8, &mut csr)?; + let old_csr = csr[0]; + + // Enable all channels. + csr[0].set_bits(4..8, 0xF); + self.interface.write(Register::CSR as u8, &csr)?; + + // Read back the enable. + csr[0] = 0; + self.interface.read(Register::CSR as u8, &mut csr)?; + if csr[0].get_bits(4..8) != 0xF { + return Ok(false); + } + + // Clear all channel enables. + csr[0].set_bits(4..8, 0x0); + self.interface.write(Register::CSR as u8, &csr)?; + + // Read back the enable. + csr[0] = 0xFF; + self.interface.read(Register::CSR as u8, &mut csr)?; + if csr[0].get_bits(4..8) != 0 { + return Ok(false); + } + + // Restore the CSR. + csr[0] = old_csr; + self.interface.write(Register::CSR as u8, &csr)?; + + Ok(true) + } + + 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.interface.read(Register::CSR as u8, &mut csr)?; + csr[0].set_bit(channel as usize + 4, true); + self.interface.write(Register::CSR as u8, &csr)?; + + Ok(()) + } + + /// Disable an output channel. + pub fn disable_channel(&mut self, channel: Channel) -> Result<(), Error> { + let mut csr: [u8; 1] = [0]; + self.interface.read(Register::CSR as u8, &mut csr)?; + csr[0].set_bit(channel as usize + 4, false); + self.interface.write(Register::CSR as u8, &csr)?; + + Ok(()) + } + + fn modify_channel(&mut self, channel: Channel, register: Register, data: &[u8]) -> Result<(), Error> { + let mut csr: [u8; 1] = [0]; + self.interface.read(Register::CSR as u8, &mut csr)?; + + let mut new_csr = csr; + new_csr[0].set_bits(4..8, 0); + new_csr[0].set_bit(4 + channel as usize, true); + + self.interface.write(Register::CSR as u8, &new_csr)?; + + self.interface.write(register as u8, &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.interface.write(Register::CSR as u8, &csr)?; + + Ok(()) + } + + /// Configure the phase of a specified channel. + /// + /// Arguments: + /// * `channel` - The channel to configure the frequency of. + /// * `phase_turns` - The desired phase offset in normalized turns. + /// + /// Returns: + /// The actual programmed phase offset of the channel in degrees. + pub fn set_phase(&mut self, channel: Channel, phase_turns: f32) -> Result> { + if phase_turns > 1.0 || phase_turns < 0.0 { + return Err(Error::Bounds); + } + + let phase_offset: u16 = (phase_turns * 1u32.wrapping_shl(14) as f32) as u16; + self.modify_channel(channel, Register::CPOW0, &phase_offset.to_be_bytes())?; + Ok((phase_offset as f32 / 1u32.wrapping_shl(14) as f32) * 360.0) + } + + /// Configure the amplitude of a specified channel. + /// + /// Arguments: + /// * `channel` - The channel to configure the frequency of. + /// * `amplitude` - A normalized amplitude setting [0, 1]. + /// + /// Returns: + /// The actual normalized amplitude of the channel relative to full-scale range. + pub fn set_amplitude(&mut self, channel: Channel, amplitude: f32) -> Result> { + if amplitude < 0.0 || amplitude > 1.0 { + return Err(Error::Bounds); + } + + let amplitude_control: u16 = (amplitude / 1u16.wrapping_shl(10) as f32) as u16; + let mut acr: [u8; 3] = [0, amplitude_control.to_be_bytes()[0], amplitude_control.to_be_bytes()[1]]; + + // 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. + acr[1].set_bit(4, amplitude_control >= 1u16.wrapping_shl(10)); + + self.modify_channel(channel, Register::ACR, &acr)?; + + Ok(amplitude_control as f32 / 1_u16.wrapping_shl(10) as f32) + } + + /// Configure the frequency of a specified channel. + /// + /// Arguments: + /// * `channel` - The channel to configure the frequency of. + /// * `frequency` - The desired output frequency in Hz. + /// + /// Returns: + /// The actual programmed frequency of the channel. + pub fn set_frequency(&mut self, channel: Channel, frequency: f64) -> Result> { + if frequency < 0.0 || frequency > self.system_clock_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. + let tuning_word: u32 = ((frequency as f64 / self.system_clock_frequency()) + * 1u64.wrapping_shl(32) as f64) as u32; + + self.modify_channel(channel, Register::CFTW0, &tuning_word.to_be_bytes())?; + Ok((tuning_word as f64 / 1u64.wrapping_shl(32) as f64) * self.system_clock_frequency()) + } +} diff --git a/stabilizer/Cargo.toml b/stabilizer/Cargo.toml index 711476b..ffbd1e4 100644 --- a/stabilizer/Cargo.toml +++ b/stabilizer/Cargo.toml @@ -49,6 +49,9 @@ version = "0.6" features = ["ethernet", "proto-ipv4", "socket-tcp", "proto-ipv6"] default-features = false +[dependencies.ad9959] +path = "../ad9959" + [dependencies.stm32h7-ethernet] git = "https://github.com/quartiq/stm32h7-ethernet.git" features = ["stm32h743v"] diff --git a/stabilizer/src/main.rs b/stabilizer/src/main.rs index 1b42929..afe6f44 100644 --- a/stabilizer/src/main.rs +++ b/stabilizer/src/main.rs @@ -28,6 +28,7 @@ extern crate panic_halt; extern crate log; // use core::sync::atomic::{AtomicU32, AtomicBool, Ordering}; +use asm_delay; use rtfm::cyccnt::{Instant, U32Ext}; use cortex_m_rt::exception; use cortex_m; @@ -48,6 +49,7 @@ use smoltcp as net; static mut DES_RING: ethernet::DesRing = ethernet::DesRing::new(); mod eth; +mod pounder; mod server; mod afe; @@ -129,6 +131,8 @@ const APP: () = { _eth_mac: ethernet::EthernetMAC, mac_addr: net::wire::EthernetAddress, + pounder: pounder::PounderDevices, + #[init([[0.; 5]; 2])] iir_state: [IIRState; 2], #[init([IIR { ba: [1., 0., 0., 0., 0.], y_offset: 0., y_min: -SCALE - 1., y_max: SCALE }; 2])] @@ -161,6 +165,8 @@ const APP: () = { clocks.rb.d2ccip1r.modify(|_, w| w.spi123sel().pll2_p().spi45sel().pll2_q()); + let mut delay = hal::delay::Delay::new(cp.SYST, clocks.clocks); + let gpioa = dp.GPIOA.split(&mut clocks.ahb4); let gpiob = dp.GPIOB.split(&mut clocks.ahb4); let gpioc = dp.GPIOC.split(&mut clocks.ahb4); @@ -274,6 +280,88 @@ const APP: () = { spi }; + let pounder_devices = { + let ad9959 = { + let qspi_interface = { + // Instantiate the QUADSPI pins and peripheral interface. + // TODO: Place these into a pins structure that is provided to the QSPI + // constructor. + let _qspi_clk = gpiob.pb2.into_alternate_af9(); + let _qspi_ncs = gpioc.pc11.into_alternate_af9(); + let _qspi_io0 = gpioe.pe7.into_alternate_af10(); + let _qspi_io1 = gpioe.pe8.into_alternate_af10(); + let _qspi_io2 = gpioe.pe9.into_alternate_af10(); + let _qspi_io3 = gpioe.pe10.into_alternate_af10(); + + let qspi = hal::qspi::Qspi::new(dp.QUADSPI, &mut clocks, 10.mhz()).unwrap(); + 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 asm_delay = { + let frequency_hz = clocks.clocks.c_ck().0; + asm_delay::AsmDelay::new(asm_delay::bitrate::Hertz (frequency_hz)) + }; + + ad9959::Ad9959::new(qspi_interface, + &mut reset_pin, + io_update, + asm_delay, + ad9959::Mode::FourBitSerial, + 100_000_000, + 5).unwrap() + }; + + let io_expander = { + let sda = gpiob.pb7.into_alternate_af4().set_open_drain(); + let scl = gpiob.pb8.into_alternate_af4().set_open_drain(); + let i2c1 = dp.I2C1.i2c((scl, sda), 100.khz(), &clocks); + mcp23017::MCP23017::default(i2c1).unwrap() + }; + + let spi = { + let spi_mosi = gpiod.pd7.into_alternate_af5(); + let spi_miso = gpioa.pa6.into_alternate_af5(); + let spi_sck = gpiog.pg11.into_alternate_af5(); + + let config = hal::spi::Config::new(hal::spi::Mode{ + polarity: hal::spi::Polarity::IdleHigh, + phase: hal::spi::Phase::CaptureOnSecondTransition, + }) + .frame_size(8); + + dp.SPI1.spi((spi_sck, spi_miso, spi_mosi), config, 25.mhz(), &clocks) + }; + + let adc1 = { + let mut adc = dp.ADC1.adc(&mut delay, &mut clocks); + adc.calibrate(); + + adc.enable() + }; + + let adc2 = { + let mut adc = dp.ADC2.adc(&mut delay, &mut clocks); + adc.calibrate(); + + adc.enable() + }; + + let adc1_in_p = gpiof.pf11.into_analog(); + let adc2_in_p = gpiof.pf14.into_analog(); + + pounder::PounderDevices::new(io_expander, + ad9959, + spi, + adc1, + adc2, + adc1_in_p, + adc2_in_p).unwrap() + }; + let mut fp_led_0 = gpiod.pd5.into_push_pull_output(); let mut fp_led_1 = gpiod.pd6.into_push_pull_output(); let mut fp_led_2 = gpiod.pd12.into_push_pull_output(); @@ -380,6 +468,7 @@ const APP: () = { _afe2: afe2, timer: timer2, + pounder: pounder_devices, eeprom_i2c: eeprom_i2c, net_interface: network_interface, diff --git a/stabilizer/src/pounder/attenuators.rs b/stabilizer/src/pounder/attenuators.rs new file mode 100644 index 0000000..d9785f8 --- /dev/null +++ b/stabilizer/src/pounder/attenuators.rs @@ -0,0 +1,57 @@ +use super::error::Error; +use super::DdsChannel; + +pub trait AttenuatorInterface { + fn modify(&mut self, attenuation: f32, channel: DdsChannel) -> Result { + if attenuation > 31.5 || attenuation < 0.0 { + return Err(Error::Bounds); + } + + // Calculate the attenuation code to program into the attenuator. The attenuator uses a + // code where the LSB is 0.5 dB. + let attenuation_code = (attenuation * 2.0) as u8; + + // Read all the channels, modify the channel of interest, and write all the channels back. + // This ensures the staging register and the output register are always in sync. + let mut channels = [0_u8; 4]; + self.read_all(&mut channels)?; + + // The lowest 2 bits of the 8-bit shift register on the attenuator are ignored. Shift the + // attenuator code into the upper 6 bits of the register value. Note that the attenuator + // treats inputs as active-low, so the code is inverted before writing. + channels[channel as usize] = !attenuation_code.wrapping_shl(2); + self.write_all(&channels)?; + + // Finally, latch the output of the updated channel to force it into an active state. + self.latch(channel)?; + + Ok(attenuation_code as f32 / 2.0) + } + + fn read(&mut self, channel: DdsChannel) -> 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.read_all(&mut channels)?; + self.write_all(&channels)?; + + // The attenuation code is stored in the upper 6 bits of the register, where each LSB + // represents 0.5 dB. The attenuator stores the code as active-low, so inverting the result + // (before the shift) has the affect of transforming the bits of interest (and the + // dont-care bits) into an active-high state and then masking off the don't care bits. If + // the shift occurs before the inversion, the upper 2 bits (which would then be don't + // care) would contain erroneous data. + let attenuation_code = (!channels[channel as usize]).wrapping_shr(2); + + // Convert the desired channel code into dB of attenuation. + Ok(attenuation_code as f32 / 2.0) + } + + fn reset(&mut self) -> Result<(), Error>; + + fn latch(&mut self, channel: DdsChannel) -> Result<(), Error>; + fn read_all(&mut self, channels: &mut [u8; 4]) -> Result<(), Error>; + fn write_all(&mut self, channels: &[u8; 4]) -> Result<(), Error>; +} diff --git a/stabilizer/src/pounder/error.rs b/stabilizer/src/pounder/error.rs new file mode 100644 index 0000000..b3cc596 --- /dev/null +++ b/stabilizer/src/pounder/error.rs @@ -0,0 +1,11 @@ +#[derive(Debug)] +pub enum Error { + Spi, + I2c, + DDS, + Qspi, + Bounds, + InvalidAddress, + InvalidChannel, + Adc, +} diff --git a/stabilizer/src/pounder/mod.rs b/stabilizer/src/pounder/mod.rs new file mode 100644 index 0000000..d29e3bd --- /dev/null +++ b/stabilizer/src/pounder/mod.rs @@ -0,0 +1,265 @@ +use mcp23017; +use ad9959; + +pub mod error; +pub mod attenuators; +mod rf_power; +pub mod types; + +use super::hal; + +use error::Error; +use attenuators::AttenuatorInterface; +use types::{DdsChannel, InputChannel}; +use rf_power::PowerMeasurementInterface; + +use embedded_hal::{ + blocking::spi::Transfer, + adc::OneShot +}; + +#[allow(dead_code)] +const OSC_EN_N_PIN: u8 = 8 + 7; + +const EXT_CLK_SEL_PIN: u8 = 8 + 6; + +const ATT_RST_N_PIN: u8 = 8 + 5; + +const ATT_LE0_PIN: u8 = 8 + 0; +const ATT_LE1_PIN: u8 = 8 + 1; +const ATT_LE2_PIN: u8 = 8 + 2; +const ATT_LE3_PIN: u8 = 8 + 3; + +pub struct QspiInterface { + pub qspi: hal::qspi::Qspi, + mode: ad9959::Mode, +} + +impl QspiInterface { + pub fn new(mut qspi: hal::qspi::Qspi) -> Result { + qspi.configure_mode(hal::qspi::QspiMode::FourBit).map_err(|_| Error::Qspi)?; + Ok(Self { qspi: qspi, mode: ad9959::Mode::SingleBitTwoWire }) + } +} + +impl ad9959::Interface for QspiInterface { + type Error = Error; + + fn configure_mode(&mut self, mode: ad9959::Mode) -> Result<(), Error> { + self.mode = mode; + + Ok(()) + } + + fn write(&mut self, addr: u8, data: &[u8]) -> Result<(), Error> { + if (addr & 0x80) != 0 { + return Err(Error::InvalidAddress); + } + + // The QSPI interface implementation always operates in 4-bit mode because the AD9959 uses + // IO3 as SYNC_IO in some output modes. In order for writes to be successful, SYNC_IO must + // be driven low. However, the QSPI peripheral forces IO3 high when operating in 1 or 2 bit + // modes. As a result, any writes while in single- or dual-bit modes has to instead write + // the data encoded into 4-bit QSPI data so that IO3 can be driven low. + match self.mode { + ad9959::Mode::SingleBitTwoWire => { + // Encode the data into a 4-bit QSPI pattern. + + // In 4-bit mode, we can send 2 bits of address and data per byte transfer. As + // such, we need at least 4x more bytes than the length of data. To avoid dynamic + // allocation, we assume the maximum transaction length for single-bit-two-wire is + // 2 bytes. + let mut encoded_data: [u8; 12] = [0; 12]; + + if (data.len() * 4) > (encoded_data.len() - 4) { + return Err(Error::Bounds); + } + + // Encode the address into the first 4 bytes. + for address_bit in 0..8 { + let offset: u8 = { + if address_bit % 2 == 0 { + 4 + } else { + 0 + } + }; + + if addr & address_bit != 0 { + encoded_data[(address_bit >> 1) as usize] |= 1 << offset; + } + } + + // Encode the data into the remaining bytes. + for byte_index in 0..data.len() { + let byte = data[byte_index]; + for address_bit in 0..8 { + let offset: u8 = { + if address_bit % 2 == 0 { + 4 + } else { + 0 + } + }; + + if byte & address_bit != 0 { + encoded_data[(byte_index + 1) * 4 + (address_bit >> 1) as usize] |= 1 << offset; + } + } + } + + let (encoded_address, encoded_payload) = { + let end_index = (1 + data.len()) * 4; + (encoded_data[0], &encoded_data[1..end_index]) + }; + + self.qspi.write(encoded_address, &encoded_payload).map_err(|_| Error::Qspi) + }, + ad9959::Mode::FourBitSerial => { + self.qspi.write(addr, &data).map_err(|_| Error::Qspi) + }, + _ => { + Err(Error::Qspi) + } + } + } + + fn read(&mut self, addr: u8, mut dest: &mut [u8]) -> Result<(), Error> { + if (addr & 0x80) != 0 { + return Err(Error::InvalidAddress); + } + + // It is not possible to read data from the AD9959 in single bit two wire mode because the + // QSPI interface assumes that data is always received on IO1. + if self.mode == ad9959::Mode::SingleBitTwoWire { + return Err(Error::Qspi); + } + + self.qspi.read(0x80_u8 | addr, &mut dest).map_err(|_| Error::Qspi) + } +} + +pub struct PounderDevices { + pub ad9959: ad9959::Ad9959>>, + mcp23017: mcp23017::MCP23017>, + attenuator_spi: hal::spi::Spi, + adc1: hal::adc::Adc, + adc2: hal::adc::Adc, + adc1_in_p: hal::gpio::gpiof::PF11, + adc2_in_p: hal::gpio::gpiof::PF14, +} + +impl PounderDevices +where + DELAY: embedded_hal::blocking::delay::DelayMs, +{ + pub fn new(mcp23017: mcp23017::MCP23017>, + ad9959: ad9959::Ad9959>>, + attenuator_spi: hal::spi::Spi, + adc1: hal::adc::Adc, + adc2: hal::adc::Adc, + adc1_in_p: hal::gpio::gpiof::PF11, + adc2_in_p: hal::gpio::gpiof::PF14, + ) -> Result { + let mut devices = Self { + mcp23017, + ad9959, + attenuator_spi, + adc1, + adc2, + adc1_in_p, + adc2_in_p, + }; + + // Configure power-on-default state for pounder. All LEDs are on, on-board oscillator + // selected, attenuators out of reset. + devices.mcp23017.write_gpio(mcp23017::Port::GPIOA, 0xF).map_err(|_| Error::I2c)?; + devices.mcp23017.write_gpio(mcp23017::Port::GPIOB, + 1_u8.wrapping_shl(5)).map_err(|_| Error::I2c)?; + devices.mcp23017.all_pin_mode(mcp23017::PinMode::OUTPUT).map_err(|_| Error::I2c)?; + + // Select the on-board clock with a 5x prescaler (500MHz). + devices.select_onboard_clock(5u8)?; + + Ok(devices) + } + + pub fn select_external_clock(&mut self, frequency: u32, 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(()) + } + + pub fn select_onboard_clock(&mut self, prescaler: u8) -> Result<(), Error> { + self.mcp23017.digital_write(EXT_CLK_SEL_PIN, false).map_err(|_| Error::I2c)?; + self.ad9959.configure_system_clock(100_000_000, prescaler).map_err(|_| Error::DDS)?; + + Ok(()) + } +} + +impl AttenuatorInterface for PounderDevices +{ + fn reset(&mut self) -> Result<(), Error> { + self.mcp23017.digital_write(ATT_RST_N_PIN, true).map_err(|_| Error::I2c)?; + // TODO: Measure the I2C transaction speed to the RST pin to ensure that the delay is + // sufficient. Document the delay here. + self.mcp23017.digital_write(ATT_RST_N_PIN, false).map_err(|_| Error::I2c)?; + + Ok(()) + } + + fn latch(&mut self, channel: DdsChannel) -> Result<(), Error> { + let pin = match channel { + DdsChannel::Zero => ATT_LE1_PIN, + DdsChannel::One => ATT_LE0_PIN, + DdsChannel::Two => ATT_LE3_PIN, + DdsChannel::Three => ATT_LE2_PIN, + }; + + self.mcp23017.digital_write(pin, true).map_err(|_| Error::I2c)?; + // TODO: Measure the I2C transaction speed to the RST pin to ensure that the delay is + // sufficient. Document the delay here. + self.mcp23017.digital_write(pin, false).map_err(|_| Error::I2c)?; + + Ok(()) + } + + fn read_all(&mut self, channels: &mut [u8; 4]) -> Result<(), Error> { + self.attenuator_spi.transfer(channels).map_err(|_| Error::Spi)?; + + Ok(()) + } + + fn write_all(&mut self, channels: &[u8; 4]) -> Result<(), Error> { + let mut result = [0_u8; 4]; + result.clone_from_slice(channels); + self.attenuator_spi.transfer(&mut result).map_err(|_| Error::Spi)?; + + Ok(()) + } +} + +impl PowerMeasurementInterface for PounderDevices { + fn sample_converter(&mut self, channel: InputChannel) -> Result { + let adc_scale = match channel { + InputChannel::Zero => { + let adc_reading: u32 = self.adc1.read(&mut self.adc1_in_p).map_err(|_| Error::Adc)?; + adc_reading as f32 / self.adc1.max_sample() as f32 + }, + InputChannel::One => { + let adc_reading: u32 = self.adc2.read(&mut self.adc2_in_p).map_err(|_| Error::Adc)?; + adc_reading as f32 / self.adc2.max_sample() as f32 + }, + }; + + // Convert analog percentage to voltage. + Ok(adc_scale * 3.3) + } +} diff --git a/stabilizer/src/pounder/rf_power.rs b/stabilizer/src/pounder/rf_power.rs new file mode 100644 index 0000000..dd7a3ec --- /dev/null +++ b/stabilizer/src/pounder/rf_power.rs @@ -0,0 +1,14 @@ +use super::Error; +use super::InputChannel; + +pub trait PowerMeasurementInterface { + fn sample_converter(&mut self, channel: InputChannel) -> Result; + + fn measure_power(&mut self, channel: InputChannel) -> Result { + let analog_measurement = self.sample_converter(channel)?; + + // The AD8363 with VSET connected to VOUT provides an output voltage of 51.7mV / dB at + // 100MHz. + Ok(analog_measurement / 0.0517) + } +} diff --git a/stabilizer/src/pounder/types.rs b/stabilizer/src/pounder/types.rs new file mode 100644 index 0000000..9938bfe --- /dev/null +++ b/stabilizer/src/pounder/types.rs @@ -0,0 +1,16 @@ + +#[allow(dead_code)] +#[derive(Debug, Copy, Clone)] +pub enum DdsChannel { + Zero, + One, + Two, + Three, +} + +#[allow(dead_code)] +#[derive(Debug, Copy, Clone)] +pub enum InputChannel { + Zero, + One, +}