Merge branch 'feature/qspi-stream' into feature/digital-input-stamp

This commit is contained in:
Ryan Summers 2020-12-07 18:49:35 +01:00
commit 35b4287fe1
6 changed files with 377 additions and 395 deletions

View File

@ -1,7 +1,7 @@
#![no_std] #![no_std]
use bit_field::BitField; 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. /// 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, /// The chip supports a number of serial interfaces to improve data throughput, including normal,
/// dual, and quad SPI configurations. /// dual, and quad SPI configurations.
pub struct Ad9959<INTERFACE> { pub struct Ad9959<INTERFACE> {
pub interface: INTERFACE, interface: INTERFACE,
reference_clock_frequency: f32, reference_clock_frequency: f32,
system_clock_multiplier: u8, system_clock_multiplier: u8,
communication_mode: Mode, communication_mode: Mode,
@ -72,6 +72,7 @@ pub enum Register {
} }
/// Specifies an output channel of the AD9959 DDS chip. /// Specifies an output channel of the AD9959 DDS chip.
#[derive(Copy, Clone, PartialEq)]
pub enum Channel { pub enum Channel {
One = 0, One = 0,
Two = 1, Two = 1,
@ -103,9 +104,9 @@ impl<I: Interface> Ad9959<I> {
/// `clock_frequency` to generate the system clock. /// `clock_frequency` to generate the system clock.
pub fn new( pub fn new(
interface: I, interface: I,
reset_pin: &mut impl OutputPin, mut reset_pin: impl OutputPin,
io_update: &mut impl OutputPin, io_update: &mut impl OutputPin,
delay: &mut impl DelayMs<u8>, delay: &mut impl DelayUs<u8>,
desired_mode: Mode, desired_mode: Mode,
clock_frequency: f32, clock_frequency: f32,
multiplier: u8, multiplier: u8,
@ -117,13 +118,15 @@ impl<I: Interface> Ad9959<I> {
communication_mode: desired_mode, communication_mode: desired_mode,
}; };
io_update.set_low().or(Err(Error::Pin))?;
// Reset the AD9959 // Reset the AD9959
reset_pin.set_high().or(Err(Error::Pin))?; reset_pin.set_high().or(Err(Error::Pin))?;
io_update.set_low().or_else(|_| Err(Error::Pin))?; // 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
// Delay for a clock cycle to allow the device to reset. // guarantee conformance with datasheet requirements.
delay.delay_ms((1000.0 / clock_frequency as f32) as u8); delay.delay_us(5);
reset_pin.set_low().or(Err(Error::Pin))?; reset_pin.set_low().or(Err(Error::Pin))?;
@ -138,16 +141,28 @@ impl<I: Interface> Ad9959<I> {
ad9959.write(Register::CSR, &csr)?; ad9959.write(Register::CSR, &csr)?;
// Latch the new interface configuration. // Latch the new interface configuration.
io_update.set_high().or_else(|_| Err(Error::Pin))?; io_update.set_high().or(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); // Delay for at least 1 SYNC_CLK period for the update to occur. The SYNC_CLK is guaranteed
io_update.set_low().or_else(|_| Err(Error::Pin))?; // 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 ad9959
.interface .interface
.configure_mode(desired_mode) .configure_mode(desired_mode)
.or(Err(Error::Interface))?; .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. // Read back the CSR to ensure it specifies the mode correctly.
let mut updated_csr: [u8; 1] = [0]; let mut updated_csr: [u8; 1] = [0];
ad9959.read(Register::CSR, &mut updated_csr)?; ad9959.read(Register::CSR, &mut updated_csr)?;
@ -271,34 +286,6 @@ impl<I: Interface> Ad9959<I> {
* self.reference_clock_frequency as f32 * 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<bool, Error> {
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. /// Update an output channel configuration register.
/// ///
/// Args: /// Args:
@ -313,14 +300,13 @@ impl<I: Interface> Ad9959<I> {
) -> Result<(), Error> { ) -> Result<(), Error> {
// Disable all other outputs so that we can update the configuration register of only the // Disable all other outputs so that we can update the configuration register of only the
// specified channel. // specified channel.
let mut csr: [u8; 1] = [0]; let csr: u8 = *0x00_u8
self.read(Register::CSR, &mut csr)?; .set_bits(1..=2, self.communication_mode as u8)
.set_bit(4 + channel as usize, true);
let mut new_csr = csr; self.interface
new_csr[0].set_bits(4..8, 0); .write(Register::CSR as u8, &[csr])
new_csr[0].set_bit(4 + channel as usize, true); .map_err(|_| Error::Interface)?;
self.write(Register::CSR, &new_csr)?;
self.write(register, &data)?; self.write(register, &data)?;
@ -507,51 +493,124 @@ impl<I: Interface> Ad9959<I> {
/ (1u64 << 32) as f32) / (1u64 << 32) as f32)
} }
pub fn serialize_profile( /// Finalize DDS configuration
&self, ///
channel: Channel, /// # Note
freq: f32, /// This is intended for when the DDS profiles will be written as a stream of data to the DDS.
turns: f32, ///
amplitude: f32, /// # Returns
) -> Result<[u32; 4], Error> { /// (I, config) where `I` is the interface to the DDS and `config` is the frozen `DdsConfig`.
let csr: u8 = *0x00_u8 pub fn freeze(self) -> (I, DdsConfig) {
.set_bits(1..=2, self.communication_mode as u8) let config = DdsConfig {
.set_bit(4 + channel as usize, true); mode: self.communication_mode,
};
// The function for channel frequency is `f_out = FTW * f_s / 2^32`, where FTW is the (self.interface, config)
// 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; /// The frozen DDS configuration.
pub struct DdsConfig {
let phase_offset: u16 = (turns * (1 << 14) as f32) as u16 & 0x3FFFu16; mode: Mode,
let pow: u32 = *0u32 }
.set_bits(24..32, Register::CPOW0 as u32)
.set_bits(8..24, phase_offset as u32) impl DdsConfig {
.set_bits(0..8, Register::CFTW0 as u32); /// Create a serializer that can be used for generating a serialized DDS profile for writing to
/// a QSPI stream.
// Enable the amplitude multiplier for the channel if required. The amplitude control has pub fn builder(&self) -> ProfileSerializer {
// full-scale at 0x3FF (amplitude of 1), so the multiplier should be disabled whenever ProfileSerializer::new(self.mode)
// full-scale is used. }
let amplitude_control: u16 = (amplitude * (1 << 10) as f32) as u16; }
let acr: u32 = *0u32 /// Represents a means of serializing a DDS profile for writing to a stream.
.set_bits(24..32, Register::ACR as u32) pub struct ProfileSerializer {
.set_bits(0..10, amplitude_control as u32 & 0x3FF) data: [u8; 16],
.set_bit(12, amplitude_control < (1 << 10)); index: usize,
mode: Mode,
let serialized: [u32; 4] = [ }
u32::from_le_bytes([
Register::CSR as u8, impl ProfileSerializer {
csr, /// Construct a new serializer.
Register::CSR as u8, ///
csr, /// # Args
]), /// * `mode` - The communication mode of the DDS.
acr.to_be(), fn new(mode: Mode) -> Self {
pow.to_be(), Self {
tuning_word.to_be(), mode,
]; data: [0; 16],
index: 0,
Ok(serialized) }
}
/// 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<u32>,
pow: Option<u16>,
acr: Option<u16>,
) {
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,
)
}
} }
} }

View File

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

View File

@ -1,11 +1,14 @@
///! The HRTimer (High Resolution Timer) is used to generate IO_Update pulses to the Pounder DDS.
use crate::hal; use crate::hal;
use hal::rcc::{rec, CoreClocks, ResetEnable}; use hal::rcc::{rec, CoreClocks, ResetEnable};
/// A HRTimer output channel.
pub enum Channel { pub enum Channel {
One, One,
Two, Two,
} }
/// The high resolution timer. Currently, only Timer E is supported.
pub struct HighResTimerE { pub struct HighResTimerE {
master: hal::stm32::HRTIM_MASTER, master: hal::stm32::HRTIM_MASTER,
timer: hal::stm32::HRTIM_TIME, timer: hal::stm32::HRTIM_TIME,
@ -15,6 +18,7 @@ pub struct HighResTimerE {
} }
impl HighResTimerE { impl HighResTimerE {
/// Construct a new high resolution timer for generating IO_update signals.
pub fn new( pub fn new(
timer_regs: hal::stm32::HRTIM_TIME, timer_regs: hal::stm32::HRTIM_TIME,
master_regs: hal::stm32::HRTIM_MASTER, 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( pub fn configure_single_shot(
&mut self, &mut self,
channel: Channel, 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 // 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. // 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 1
} else if (source_cycles / 2) < 0xFFDF { } else if (source_cycles / 2) < 0xFFDF {
2 2
} else if (source_cycles / 4) < 0xFFDF { } else if (source_cycles / 4) < 0xFFDF {
4 3
} else { } else {
panic!("Unattainable timing parameters!"); panic!("Unattainable timing parameters!");
}; };
let divider = 1 << (setting - 1);
// The period register must be greater than or equal to 3 cycles. // The period register must be greater than or equal to 3 cycles.
let period = (source_cycles / divider as u32) as u16; let period = (source_cycles / divider as u32) as u16;
assert!(period > 2); assert!(period > 2);
// We now have the prescaler and the period registers. Configure the timer. // 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 self.timer
.timecr .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) }); self.timer.perer.write(|w| unsafe { w.perx().bits(period) });
// Configure the comparator 1 level. // Configure the comparator 1 level.
let offset = (set_offset * source_frequency as f32) as u16; 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 self.timer
.cmp1er .cmp1er
.write(|w| unsafe { w.cmp1x().bits(offset) }); .write(|w| unsafe { w.cmp1x().bits(offset) });
@ -97,6 +124,7 @@ impl HighResTimerE {
self.master.mcr.modify(|_, w| w.tecen().set_bit()); 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) { pub fn trigger(&mut self) {
// Generate a reset event to force the timer to start counting. // Generate a reset event to force the timer to start counting.
self.common.cr2.write(|w| w.terst().set_bit()); self.common.cr2.write(|w| w.terst().set_bit());

View File

@ -80,6 +80,7 @@ mod timers;
use adc::{Adc0Input, Adc1Input}; use adc::{Adc0Input, Adc1Input};
use dac::{Dac0Output, Dac1Output}; use dac::{Dac0Output, Dac1Output};
use dsp::iir; use dsp::iir;
use pounder::DdsOutput;
#[cfg(not(feature = "semihosting"))] #[cfg(not(feature = "semihosting"))]
fn init_log() {} fn init_log() {}
@ -202,6 +203,8 @@ const APP: () = {
eeprom_i2c: hal::i2c::I2c<hal::stm32::I2C2>, eeprom_i2c: hal::i2c::I2c<hal::stm32::I2C2>,
dds_output: Option<DdsOutput>,
// Note: It appears that rustfmt generates a format that GDB cannot recognize, which // Note: It appears that rustfmt generates a format that GDB cannot recognize, which
// results in GDB breakpoints being set improperly. // results in GDB breakpoints being set improperly.
#[rustfmt::skip] #[rustfmt::skip]
@ -487,8 +490,9 @@ const APP: () = {
// Measure the Pounder PGOOD output to detect if pounder is present on Stabilizer. // Measure the Pounder PGOOD output to detect if pounder is present on Stabilizer.
let pounder_pgood = gpiob.pb13.into_pull_down_input(); let pounder_pgood = gpiob.pb13.into_pull_down_input();
delay.delay_ms(2u8); delay.delay_ms(2u8);
let pounder_devices = if pounder_pgood.is_high().unwrap() { let (pounder_devices, dds_output) = if pounder_pgood.is_high().unwrap()
let ad9959 = { {
let mut ad9959 = {
let qspi_interface = { let qspi_interface = {
// Instantiate the QUADSPI pins and peripheral interface. // Instantiate the QUADSPI pins and peripheral interface.
let qspi_pins = { let qspi_pins = {
@ -532,12 +536,12 @@ const APP: () = {
pounder::QspiInterface::new(qspi).unwrap() 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 mut io_update = gpiog.pg7.into_push_pull_output();
let ad9959 = ad9959::Ad9959::new( let ad9959 = ad9959::Ad9959::new(
qspi_interface, qspi_interface,
&mut reset_pin, reset_pin,
&mut io_update, &mut io_update,
&mut delay, &mut delay,
ad9959::Mode::FourBitSerial, ad9959::Mode::FourBitSerial,
@ -619,54 +623,64 @@ const APP: () = {
let adc1_in_p = gpiof.pf11.into_analog(); let adc1_in_p = gpiof.pf11.into_analog();
let adc2_in_p = gpiof.pf14.into_analog(); let adc2_in_p = gpiof.pf14.into_analog();
let io_update_trigger = { let pounder_devices = pounder::PounderDevices::new(
let _io_update = gpiog io_expander,
.pg7 &mut ad9959,
.into_alternate_af2() spi,
.set_speed(hal::gpio::Speed::VeryHigh); adc1,
adc2,
adc1_in_p,
adc2_in_p,
)
.unwrap();
// Configure the IO_Update signal for the DDS. let dds_output = {
let mut hrtimer = hrtimer::HighResTimerE::new( let io_update_trigger = {
dp.HRTIM_TIME, let _io_update = gpiog
dp.HRTIM_MASTER, .pg7
dp.HRTIM_COMMON, .into_alternate_af2()
ccdr.clocks, .set_speed(hal::gpio::Speed::VeryHigh);
ccdr.peripheral.HRTIM,
);
// IO_Update should be latched for 50ns after the QSPI profile write. Profile writes // Configure the IO_Update signal for the DDS.
// are always 16 bytes, with 2 cycles required per byte, coming out to a total of 32 let mut hrtimer = hrtimer::HighResTimerE::new(
// QSPI clock cycles. The QSPI is configured for 40MHz, so this comes out to an dp.HRTIM_TIME,
// offset of 800nS. We use 900ns to be safe - note that the timer is triggered after dp.HRTIM_MASTER,
// the QSPI write, which can take approximately 120nS, so there is additional dp.HRTIM_COMMON,
// margin. ccdr.clocks,
hrtimer.configure_single_shot( ccdr.peripheral.HRTIM,
hrtimer::Channel::Two, );
50_e-9,
900_e-9,
);
// Ensure that we have enough time for an IO-update every sample. // IO_Update should be latched for 4 SYNC_CLK cycles after the QSPI profile
assert!(1.0 / (1000 * SAMPLE_FREQUENCY_KHZ) as f32 > 900_e-9); // 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( (Some(pounder_devices), Some(dds_output))
pounder::PounderDevices::new(
io_expander,
ad9959,
io_update_trigger,
spi,
adc1,
adc2,
adc1_in_p,
adc2_in_p,
)
.unwrap(),
)
} else { } else {
None (None, None)
}; };
let mut eeprom_i2c = { let mut eeprom_i2c = {
@ -780,7 +794,6 @@ const APP: () = {
}; };
cp.SCB.enable_icache(); cp.SCB.enable_icache();
//cp.SCB.enable_dcache(&mut cp.CPUID);
// info!("Version {} {}", build_info::PKG_VERSION, build_info::GIT_VERSION.unwrap()); // info!("Version {} {}", build_info::PKG_VERSION, build_info::GIT_VERSION.unwrap());
// info!("Built on {}", build_info::BUILT_TIME_UTC); // info!("Built on {}", build_info::BUILT_TIME_UTC);
@ -808,7 +821,7 @@ const APP: () = {
adcs, adcs,
dacs, dacs,
input_stamper, input_stamper,
dds_output,
pounder: pounder_devices, pounder: pounder_devices,
eeprom_i2c, 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) { fn process(c: process::Context) {
let adc_samples = [ let adc_samples = [
c.resources.adcs.0.acquire_buffer(), c.resources.adcs.0.acquire_buffer(),
@ -846,6 +859,18 @@ const APP: () = {
dac_samples[channel][sample] = y as u16 ^ 0x8000; 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; let [dac0, dac1] = dac_samples;
c.resources.dacs.0.release_buffer(dac0); c.resources.dacs.0.release_buffer(dac0);
c.resources.dacs.1.release_buffer(dac1); c.resources.dacs.1.release_buffer(dac1);

111
src/pounder/dds_output.rs Normal file
View File

@ -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(
&regs.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<u32>,
pow: Option<u16>,
acr: Option<u16>,
) -> 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);
}
}

View File

@ -1,10 +1,12 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
mod attenuators; mod attenuators;
mod dds_output;
mod rf_power; mod rf_power;
pub use dds_output::DdsOutput;
use super::hal; use super::hal;
use super::hrtimer::HighResTimerE;
use attenuators::AttenuatorInterface; use attenuators::AttenuatorInterface;
use rf_power::PowerMeasurementInterface; use rf_power::PowerMeasurementInterface;
@ -34,6 +36,7 @@ pub enum Error {
} }
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
#[allow(dead_code)]
pub enum Channel { pub enum Channel {
In0, In0,
In1, In1,
@ -133,34 +136,6 @@ impl QspiInterface {
Ok(()) 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 { impl ad9959::Interface for QspiInterface {
@ -287,8 +262,6 @@ impl ad9959::Interface for QspiInterface {
/// A structure containing implementation for Pounder hardware. /// A structure containing implementation for Pounder hardware.
pub struct PounderDevices { pub struct PounderDevices {
pub ad9959: ad9959::Ad9959<QspiInterface>,
pub io_update_trigger: HighResTimerE,
mcp23017: mcp23017::MCP23017<hal::i2c::I2c<hal::stm32::I2C1>>, mcp23017: mcp23017::MCP23017<hal::i2c::I2c<hal::stm32::I2C1>>,
attenuator_spi: hal::spi::Spi<hal::stm32::SPI1, hal::spi::Enabled, u8>, attenuator_spi: hal::spi::Spi<hal::stm32::SPI1, hal::spi::Enabled, u8>,
adc1: hal::adc::Adc<hal::stm32::ADC1, hal::adc::Enabled>, adc1: hal::adc::Adc<hal::stm32::ADC1, hal::adc::Enabled>,
@ -303,15 +276,13 @@ impl PounderDevices {
/// Args: /// Args:
/// * `ad9959` - The DDS driver for the pounder hardware. /// * `ad9959` - The DDS driver for the pounder hardware.
/// * `attenuator_spi` - A SPI interface to control digital attenuators. /// * `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. /// * `adc1` - The ADC1 peripheral for measuring power.
/// * `adc2` - The ADC2 peripheral for measuring power. /// * `adc2` - The ADC2 peripheral for measuring power.
/// * `adc1_in_p` - The input channel for the RF power measurement on IN0. /// * `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. /// * `adc2_in_p` - The input channel for the RF power measurement on IN1.
pub fn new( pub fn new(
mcp23017: mcp23017::MCP23017<hal::i2c::I2c<hal::stm32::I2C1>>, mcp23017: mcp23017::MCP23017<hal::i2c::I2c<hal::stm32::I2C1>>,
ad9959: ad9959::Ad9959<QspiInterface>, ad9959: &mut ad9959::Ad9959<QspiInterface>,
io_update_trigger: HighResTimerE,
attenuator_spi: hal::spi::Spi<hal::stm32::SPI1, hal::spi::Enabled, u8>, attenuator_spi: hal::spi::Spi<hal::stm32::SPI1, hal::spi::Enabled, u8>,
adc1: hal::adc::Adc<hal::stm32::ADC1, hal::adc::Enabled>, adc1: hal::adc::Adc<hal::stm32::ADC1, hal::adc::Enabled>,
adc2: hal::adc::Adc<hal::stm32::ADC2, hal::adc::Enabled>, adc2: hal::adc::Adc<hal::stm32::ADC2, hal::adc::Enabled>,
@ -320,8 +291,6 @@ impl PounderDevices {
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let mut devices = Self { let mut devices = Self {
mcp23017, mcp23017,
io_update_trigger,
ad9959,
attenuator_spi, attenuator_spi,
adc1, adc1,
adc2, adc2,
@ -346,114 +315,15 @@ impl PounderDevices {
.map_err(|_| Error::I2c)?; .map_err(|_| Error::I2c)?;
// Select the on-board clock with a 4x prescaler (400MHz). // Select the on-board clock with a 4x prescaler (400MHz).
devices.select_onboard_clock(4u8)?; devices
.mcp23017
// 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
.digital_write(EXT_CLK_SEL_PIN, false) .digital_write(EXT_CLK_SEL_PIN, false)
.map_err(|_| Error::I2c)?; .map_err(|_| Error::I2c)?;
self.ad9959 ad9959
.configure_system_clock(100_000_000f32, multiplier) .configure_system_clock(100_000_000f32, 4)
.map_err(|_| Error::Dds)?; .map_err(|_| Error::Dds)?;
Ok(()) Ok(devices)
}
/// 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<DdsClockConfig, Error> {
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(())
} }
} }