Merge remote-tracking branch 'origin/master' into trig

* origin/master:
  Updating after review
  Fixing semantics
  Adding documentation
  Fixing buffer size
  Adding updated QSPI stream writer
  Increasing batch size
  Adding updates for QSPI streaming
  Adding WIP refactor
  Updating DDS control
  Removing pounder test, adding file
  Removing unused code
  Refactoring DDS output control
  Updating QSPI frequency
  Adding support for hardware IO_update
  Fixing merge issues
  Adding WIP
  Adding WIP HRTimer
  Adding WIP experimental code
  Adding WIP QSPI streaming
This commit is contained in:
Robert Jördens 2020-12-10 17:02:19 +01:00
commit 18ea94298b
6 changed files with 562 additions and 530 deletions

View File

@ -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<INTERFACE, DELAY, UPDATE> {
pub struct Ad9959<INTERFACE> {
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<PinE, INTERFACE, DELAY, UPDATE> Ad9959<INTERFACE, DELAY, UPDATE>
where
INTERFACE: Interface,
DELAY: DelayMs<u8>,
UPDATE: OutputPin<Error = PinE>,
{
impl<I: Interface> Ad9959<I> {
/// 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<RST>(
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<u8>,
desired_mode: Mode,
clock_frequency: f32,
multiplier: u8,
) -> Result<Self, Error>
where
RST: OutputPin,
{
) -> Result<Self, Error> {
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<f64, Error> {
) -> Result<f32, Error> {
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<bool, Error> {
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<f64, Error> {
frequency: f32,
) -> Result<f32, Error> {
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<f64, Error> {
pub fn get_frequency(&mut self, channel: Channel) -> Result<f32, Error> {
// 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<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': 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()

132
src/hrtimer.rs Normal file
View File

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

View File

@ -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<hal::stm32::I2C2>,
dds_output: Option<DdsOutput>,
// 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<ethernet::EthernetMAC>,
mac_addr: net::wire::EthernetAddress,
pounder: Option<pounder::PounderDevices<asm_delay::AsmDelay>>,
pounder: Option<pounder::PounderDevices>,
// 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(
let pounder_devices = pounder::PounderDevices::new(
io_expander,
ad9959,
&mut ad9959,
spi,
adc1,
adc2,
adc1_in_p,
adc2_in_p,
)
.unwrap(),
)
.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::<server::Status, ()>(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::<server::IirRequest, ()>(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::<(), ()>(())

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,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<ad9959::Channel> 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<DELAY> {
pub ad9959: ad9959::Ad9959<
QspiInterface,
DELAY,
hal::gpio::gpiog::PG7<hal::gpio::Output<hal::gpio::PushPull>>,
>,
pub struct PounderDevices {
mcp23017: mcp23017::MCP23017<hal::i2c::I2c<hal::stm32::I2C1>>,
attenuator_spi: hal::spi::Spi<hal::stm32::SPI1, hal::spi::Enabled, u8>,
adc1: hal::adc::Adc<hal::stm32::ADC1, hal::adc::Enabled>,
@ -242,10 +270,7 @@ pub struct PounderDevices<DELAY> {
adc2_in_p: hal::gpio::gpiof::PF14<hal::gpio::Analog>,
}
impl<DELAY> PounderDevices<DELAY>
where
DELAY: embedded_hal::blocking::delay::DelayMs<u8>,
{
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<hal::i2c::I2c<hal::stm32::I2C1>>,
ad9959: ad9959::Ad9959<
QspiInterface,
DELAY,
hal::gpio::gpiog::PG7<hal::gpio::Output<hal::gpio::PushPull>>,
>,
ad9959: &mut ad9959::Ad9959<QspiInterface>,
attenuator_spi: hal::spi::Spi<hal::stm32::SPI1, hal::spi::Enabled, u8>,
adc1: hal::adc::Adc<hal::stm32::ADC1, hal::adc::Enabled>,
adc2: hal::adc::Adc<hal::stm32::ADC2, hal::adc::Enabled>,
@ -270,7 +291,6 @@ where
) -> Result<Self, Error> {
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<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,
})
}
/// 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<InputChannelState, Error> {
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<DdsChannelState, Error> {
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<OutputChannelState, Error> {
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<DELAY> AttenuatorInterface for PounderDevices<DELAY> {
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<DELAY> AttenuatorInterface for PounderDevices<DELAY> {
}
}
impl<DELAY> PowerMeasurementInterface for PounderDevices<DELAY> {
impl PowerMeasurementInterface for PounderDevices {
/// Sample an ADC channel.
///
/// Args: