Merge pull request #186 from vertigo-designs/feature/qspi-stream
Adding support for Pounder DDS QSPI profile stream
This commit is contained in:
commit
85b8f12e5c
@ -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.
|
||||||
///
|
///
|
||||||
@ -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,
|
/// 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, DELAY, UPDATE> {
|
pub struct Ad9959<INTERFACE> {
|
||||||
interface: INTERFACE,
|
interface: INTERFACE,
|
||||||
delay: DELAY,
|
|
||||||
reference_clock_frequency: f32,
|
reference_clock_frequency: f32,
|
||||||
system_clock_multiplier: u8,
|
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.
|
/// 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.
|
/// 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,
|
||||||
@ -90,12 +90,7 @@ pub enum Error {
|
|||||||
Frequency,
|
Frequency,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<PinE, INTERFACE, DELAY, UPDATE> Ad9959<INTERFACE, DELAY, UPDATE>
|
impl<I: Interface> Ad9959<I> {
|
||||||
where
|
|
||||||
INTERFACE: Interface,
|
|
||||||
DELAY: DelayMs<u8>,
|
|
||||||
UPDATE: OutputPin<Error = PinE>,
|
|
||||||
{
|
|
||||||
/// Construct and initialize the DDS.
|
/// Construct and initialize the DDS.
|
||||||
///
|
///
|
||||||
/// Args:
|
/// Args:
|
||||||
@ -107,35 +102,31 @@ where
|
|||||||
/// * `clock_frequency` - The clock frequency of the reference clock input.
|
/// * `clock_frequency` - The clock frequency of the reference clock input.
|
||||||
/// * `multiplier` - The desired clock multiplier for the system clock. This multiplies
|
/// * `multiplier` - The desired clock multiplier for the system clock. This multiplies
|
||||||
/// `clock_frequency` to generate the system clock.
|
/// `clock_frequency` to generate the system clock.
|
||||||
pub fn new<RST>(
|
pub fn new(
|
||||||
interface: INTERFACE,
|
interface: I,
|
||||||
reset_pin: &mut RST,
|
mut reset_pin: impl OutputPin,
|
||||||
io_update: UPDATE,
|
io_update: &mut impl OutputPin,
|
||||||
delay: DELAY,
|
delay: &mut impl DelayUs<u8>,
|
||||||
desired_mode: Mode,
|
desired_mode: Mode,
|
||||||
clock_frequency: f32,
|
clock_frequency: f32,
|
||||||
multiplier: u8,
|
multiplier: u8,
|
||||||
) -> Result<Self, Error>
|
) -> Result<Self, Error> {
|
||||||
where
|
|
||||||
RST: OutputPin,
|
|
||||||
{
|
|
||||||
let mut ad9959 = Ad9959 {
|
let mut ad9959 = Ad9959 {
|
||||||
interface,
|
interface,
|
||||||
io_update,
|
|
||||||
delay,
|
|
||||||
reference_clock_frequency: clock_frequency,
|
reference_clock_frequency: clock_frequency,
|
||||||
system_clock_multiplier: 1,
|
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 the AD9959
|
||||||
reset_pin.set_high().or(Err(Error::Pin))?;
|
reset_pin.set_high().or(Err(Error::Pin))?;
|
||||||
|
|
||||||
// Delay for a clock cycle to allow the device to reset.
|
// Delay for at least 1 SYNC_CLK period for the reset to occur. The SYNC_CLK is guaranteed
|
||||||
ad9959
|
// to be at least 250KHz (1/4 of 1MHz minimum REF_CLK). We use 5uS instead of 4uS to
|
||||||
.delay
|
// guarantee conformance with datasheet requirements.
|
||||||
.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))?;
|
||||||
|
|
||||||
@ -149,14 +140,29 @@ where
|
|||||||
csr[0].set_bits(1..3, desired_mode as u8);
|
csr[0].set_bits(1..3, desired_mode as u8);
|
||||||
ad9959.write(Register::CSR, &csr)?;
|
ad9959.write(Register::CSR, &csr)?;
|
||||||
|
|
||||||
// Latch the configuration registers to make them active.
|
// Latch the new interface configuration.
|
||||||
ad9959.latch_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
|
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)?;
|
||||||
@ -181,18 +187,6 @@ where
|
|||||||
.or(Err(Error::Interface))
|
.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.
|
/// Configure the internal system clock of the chip.
|
||||||
///
|
///
|
||||||
/// Arguments:
|
/// Arguments:
|
||||||
@ -205,7 +199,7 @@ where
|
|||||||
&mut self,
|
&mut self,
|
||||||
reference_clock_frequency: f32,
|
reference_clock_frequency: f32,
|
||||||
multiplier: u8,
|
multiplier: u8,
|
||||||
) -> Result<f64, Error> {
|
) -> Result<f32, Error> {
|
||||||
self.reference_clock_frequency = reference_clock_frequency;
|
self.reference_clock_frequency = reference_clock_frequency;
|
||||||
|
|
||||||
if multiplier != 1 && !(4..=20).contains(&multiplier) {
|
if multiplier != 1 && !(4..=20).contains(&multiplier) {
|
||||||
@ -213,8 +207,8 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
let frequency =
|
let frequency =
|
||||||
multiplier as f64 * self.reference_clock_frequency as f64;
|
multiplier as f32 * self.reference_clock_frequency as f32;
|
||||||
if frequency > 500_000_000.0f64 {
|
if frequency > 500_000_000.0f32 {
|
||||||
return Err(Error::Frequency);
|
return Err(Error::Frequency);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -287,37 +281,9 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current system clock frequency in Hz.
|
/// Get the current system clock frequency in Hz.
|
||||||
fn system_clock_frequency(&self) -> f64 {
|
fn system_clock_frequency(&self) -> f32 {
|
||||||
self.system_clock_multiplier as f64
|
self.system_clock_multiplier as f32
|
||||||
* self.reference_clock_frequency as f64
|
* 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.
|
||||||
@ -334,22 +300,16 @@ where
|
|||||||
) -> 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)?;
|
||||||
|
|
||||||
// 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -494,8 +454,8 @@ where
|
|||||||
pub fn set_frequency(
|
pub fn set_frequency(
|
||||||
&mut self,
|
&mut self,
|
||||||
channel: Channel,
|
channel: Channel,
|
||||||
frequency: f64,
|
frequency: f32,
|
||||||
) -> Result<f64, Error> {
|
) -> Result<f32, Error> {
|
||||||
if frequency < 0.0 || frequency > self.system_clock_frequency() {
|
if frequency < 0.0 || frequency > self.system_clock_frequency() {
|
||||||
return Err(Error::Bounds);
|
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
|
// 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.
|
// frequency tuning word and f_s is the system clock rate.
|
||||||
let tuning_word: u32 =
|
let tuning_word: u32 =
|
||||||
((frequency as f64 / self.system_clock_frequency())
|
((frequency as f32 / self.system_clock_frequency())
|
||||||
* 1u64.wrapping_shl(32) as f64) as u32;
|
* 1u64.wrapping_shl(32) as f32) as u32;
|
||||||
|
|
||||||
self.modify_channel(
|
self.modify_channel(
|
||||||
channel,
|
channel,
|
||||||
Register::CFTW0,
|
Register::CFTW0,
|
||||||
&tuning_word.to_be_bytes(),
|
&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())
|
* self.system_clock_frequency())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -522,14 +482,135 @@ where
|
|||||||
///
|
///
|
||||||
/// Returns:
|
/// Returns:
|
||||||
/// The frequency of the channel in Hz.
|
/// 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.
|
// Read the frequency tuning word for the channel.
|
||||||
let mut tuning_word: [u8; 4] = [0; 4];
|
let mut tuning_word: [u8; 4] = [0; 4];
|
||||||
self.read_channel(channel, Register::CFTW0, &mut tuning_word)?;
|
self.read_channel(channel, Register::CFTW0, &mut tuning_word)?;
|
||||||
let tuning_word = u32::from_be_bytes(tuning_word);
|
let tuning_word = u32::from_be_bytes(tuning_word);
|
||||||
|
|
||||||
// Convert the tuning word into a frequency.
|
// Convert the tuning word into a frequency.
|
||||||
Ok(tuning_word as f64 * self.system_clock_frequency()
|
Ok((tuning_word as f32 * self.system_clock_frequency())
|
||||||
/ (1u64 << 32) as f64)
|
/ (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,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
111
pounder_test.py
111
pounder_test.py
@ -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
132
src/hrtimer.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
186
src/main.rs
186
src/main.rs
@ -71,6 +71,7 @@ mod afe;
|
|||||||
mod dac;
|
mod dac;
|
||||||
mod design_parameters;
|
mod design_parameters;
|
||||||
mod eeprom;
|
mod eeprom;
|
||||||
|
mod hrtimer;
|
||||||
mod pounder;
|
mod pounder;
|
||||||
mod sampling_timer;
|
mod sampling_timer;
|
||||||
mod server;
|
mod server;
|
||||||
@ -78,6 +79,7 @@ mod server;
|
|||||||
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() {}
|
||||||
@ -200,6 +202,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]
|
||||||
@ -211,7 +215,7 @@ const APP: () = {
|
|||||||
eth_mac: ethernet::phy::LAN8742A<ethernet::EthernetMAC>,
|
eth_mac: ethernet::phy::LAN8742A<ethernet::EthernetMAC>,
|
||||||
mac_addr: net::wire::EthernetAddress,
|
mac_addr: net::wire::EthernetAddress,
|
||||||
|
|
||||||
pounder: Option<pounder::PounderDevices<asm_delay::AsmDelay>>,
|
pounder: Option<pounder::PounderDevices>,
|
||||||
|
|
||||||
// Format: iir_state[ch][cascade-no][coeff]
|
// Format: iir_state[ch][cascade-no][coeff]
|
||||||
#[init([[[0.; 5]; IIR_CASCADE_LENGTH]; 2])]
|
#[init([[[0.; 5]; IIR_CASCADE_LENGTH]; 2])]
|
||||||
@ -259,7 +263,7 @@ const APP: () = {
|
|||||||
let gpiod = dp.GPIOD.split(ccdr.peripheral.GPIOD);
|
let gpiod = dp.GPIOD.split(ccdr.peripheral.GPIOD);
|
||||||
let gpioe = dp.GPIOE.split(ccdr.peripheral.GPIOE);
|
let gpioe = dp.GPIOE.split(ccdr.peripheral.GPIOE);
|
||||||
let gpiof = dp.GPIOF.split(ccdr.peripheral.GPIOF);
|
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 afe0 = {
|
||||||
let a0_pin = gpiof.pf2.into_push_pull_output();
|
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.
|
// 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 = {
|
||||||
@ -502,33 +507,32 @@ const APP: () = {
|
|||||||
let qspi = hal::qspi::Qspi::bank2(
|
let qspi = hal::qspi::Qspi::bank2(
|
||||||
dp.QUADSPI,
|
dp.QUADSPI,
|
||||||
qspi_pins,
|
qspi_pins,
|
||||||
10.mhz(),
|
40.mhz(),
|
||||||
&ccdr.clocks,
|
&ccdr.clocks,
|
||||||
ccdr.peripheral.QSPI,
|
ccdr.peripheral.QSPI,
|
||||||
);
|
);
|
||||||
|
|
||||||
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 io_update = gpiog.pg7.into_push_pull_output();
|
let mut io_update = gpiog.pg7.into_push_pull_output();
|
||||||
|
|
||||||
let asm_delay = {
|
let ad9959 = ad9959::Ad9959::new(
|
||||||
let frequency_hz = ccdr.clocks.c_ck().0;
|
|
||||||
asm_delay::AsmDelay::new(asm_delay::bitrate::Hertz(
|
|
||||||
frequency_hz,
|
|
||||||
))
|
|
||||||
};
|
|
||||||
|
|
||||||
ad9959::Ad9959::new(
|
|
||||||
qspi_interface,
|
qspi_interface,
|
||||||
&mut reset_pin,
|
reset_pin,
|
||||||
io_update,
|
&mut io_update,
|
||||||
asm_delay,
|
&mut delay,
|
||||||
ad9959::Mode::FourBitSerial,
|
ad9959::Mode::FourBitSerial,
|
||||||
100_000_000f32,
|
100_000_000_f32,
|
||||||
5,
|
5,
|
||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap();
|
||||||
|
|
||||||
|
// Return IO_Update
|
||||||
|
gpiog.pg7 = io_update.into_analog();
|
||||||
|
|
||||||
|
ad9959
|
||||||
};
|
};
|
||||||
|
|
||||||
let io_expander = {
|
let io_expander = {
|
||||||
@ -598,20 +602,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();
|
||||||
|
|
||||||
Some(
|
let pounder_devices = pounder::PounderDevices::new(
|
||||||
pounder::PounderDevices::new(
|
|
||||||
io_expander,
|
io_expander,
|
||||||
ad9959,
|
&mut ad9959,
|
||||||
spi,
|
spi,
|
||||||
adc1,
|
adc1,
|
||||||
adc2,
|
adc2,
|
||||||
adc1_in_p,
|
adc1_in_p,
|
||||||
adc2_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 {
|
} else {
|
||||||
None
|
(None, None)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut eeprom_i2c = {
|
let mut eeprom_i2c = {
|
||||||
@ -741,7 +789,7 @@ const APP: () = {
|
|||||||
|
|
||||||
adcs,
|
adcs,
|
||||||
dacs,
|
dacs,
|
||||||
|
dds_output,
|
||||||
pounder: pounder_devices,
|
pounder: pounder_devices,
|
||||||
|
|
||||||
eeprom_i2c,
|
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) {
|
fn process(c: process::Context) {
|
||||||
let adc_samples = [
|
let adc_samples = [
|
||||||
c.resources.adcs.0.acquire_buffer(),
|
c.resources.adcs.0.acquire_buffer(),
|
||||||
@ -777,6 +825,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);
|
||||||
@ -856,41 +916,7 @@ const APP: () = {
|
|||||||
Ok::<server::Status, ()>(state)
|
Ok::<server::Status, ()>(state)
|
||||||
}),
|
}),
|
||||||
"stabilizer/afe0/gain": (|| c.resources.afes.0.get_gain()),
|
"stabilizer/afe0/gain": (|| c.resources.afes.0.get_gain()),
|
||||||
"stabilizer/afe1/gain": (|| c.resources.afes.1.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),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
],
|
],
|
||||||
|
|
||||||
modifiable_attributes: [
|
modifiable_attributes: [
|
||||||
@ -938,40 +964,6 @@ const APP: () = {
|
|||||||
Ok::<server::IirRequest, ()>(req)
|
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| {
|
"stabilizer/afe0/gain": afe::Gain, (|gain| {
|
||||||
c.resources.afes.0.set_gain(gain);
|
c.resources.afes.0.set_gain(gain);
|
||||||
Ok::<(), ()>(())
|
Ok::<(), ()>(())
|
||||||
|
111
src/pounder/dds_output.rs
Normal file
111
src/pounder/dds_output.rs
Normal 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(
|
||||||
|
®s.dr as *const _ as *mut u32,
|
||||||
|
*word,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger the IO_update signal generating timer to asynchronous create the IO_Update pulse.
|
||||||
|
self.io_update_trigger.trigger();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A temporary builder for serializing and writing profiles.
|
||||||
|
pub struct ProfileBuilder<'a> {
|
||||||
|
dds_stream: &'a mut DdsOutput,
|
||||||
|
serializer: ProfileSerializer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ProfileBuilder<'a> {
|
||||||
|
/// Update a number of channels with the provided configuration
|
||||||
|
///
|
||||||
|
/// # Args
|
||||||
|
/// * `channels` - A list of channels to apply the configuration to.
|
||||||
|
/// * `ftw` - If provided, indicates a frequency tuning word for the channels.
|
||||||
|
/// * `pow` - If provided, indicates a phase offset word for the channels.
|
||||||
|
/// * `acr` - If provided, indicates the amplitude control register for the channels.
|
||||||
|
pub fn update_channels(
|
||||||
|
mut self,
|
||||||
|
channels: &[Channel],
|
||||||
|
ftw: Option<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);
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,11 @@
|
|||||||
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 attenuators::AttenuatorInterface;
|
use attenuators::AttenuatorInterface;
|
||||||
@ -33,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,
|
||||||
@ -43,7 +47,7 @@ pub enum Channel {
|
|||||||
#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
|
||||||
pub struct DdsChannelState {
|
pub struct DdsChannelState {
|
||||||
pub phase_offset: f32,
|
pub phase_offset: f32,
|
||||||
pub frequency: f64,
|
pub frequency: f32,
|
||||||
pub amplitude: f32,
|
pub amplitude: f32,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
}
|
}
|
||||||
@ -90,6 +94,7 @@ impl Into<ad9959::Channel> for Channel {
|
|||||||
pub struct QspiInterface {
|
pub struct QspiInterface {
|
||||||
pub qspi: hal::qspi::Qspi,
|
pub qspi: hal::qspi::Qspi,
|
||||||
mode: ad9959::Mode,
|
mode: ad9959::Mode,
|
||||||
|
streaming: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl QspiInterface {
|
impl QspiInterface {
|
||||||
@ -106,8 +111,31 @@ impl QspiInterface {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
qspi,
|
qspi,
|
||||||
mode: ad9959::Mode::SingleBitTwoWire,
|
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 {
|
impl ad9959::Interface for QspiInterface {
|
||||||
@ -205,13 +233,18 @@ impl ad9959::Interface for QspiInterface {
|
|||||||
.map_err(|_| Error::Qspi)
|
.map_err(|_| Error::Qspi)
|
||||||
}
|
}
|
||||||
ad9959::Mode::FourBitSerial => {
|
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),
|
_ => 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 {
|
if (addr & 0x80) != 0 {
|
||||||
return Err(Error::InvalidAddress);
|
return Err(Error::InvalidAddress);
|
||||||
}
|
}
|
||||||
@ -222,18 +255,13 @@ impl ad9959::Interface for QspiInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.qspi
|
self.qspi
|
||||||
.read(0x80_u8 | addr, &mut dest)
|
.read(0x80_u8 | addr, dest)
|
||||||
.map_err(|_| Error::Qspi)
|
.map_err(|_| Error::Qspi)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A structure containing implementation for Pounder hardware.
|
/// A structure containing implementation for Pounder hardware.
|
||||||
pub struct PounderDevices<DELAY> {
|
pub struct PounderDevices {
|
||||||
pub ad9959: ad9959::Ad9959<
|
|
||||||
QspiInterface,
|
|
||||||
DELAY,
|
|
||||||
hal::gpio::gpiog::PG7<hal::gpio::Output<hal::gpio::PushPull>>,
|
|
||||||
>,
|
|
||||||
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>,
|
||||||
@ -242,10 +270,7 @@ pub struct PounderDevices<DELAY> {
|
|||||||
adc2_in_p: hal::gpio::gpiof::PF14<hal::gpio::Analog>,
|
adc2_in_p: hal::gpio::gpiof::PF14<hal::gpio::Analog>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<DELAY> PounderDevices<DELAY>
|
impl PounderDevices {
|
||||||
where
|
|
||||||
DELAY: embedded_hal::blocking::delay::DelayMs<u8>,
|
|
||||||
{
|
|
||||||
/// Construct and initialize pounder-specific hardware.
|
/// Construct and initialize pounder-specific hardware.
|
||||||
///
|
///
|
||||||
/// Args:
|
/// Args:
|
||||||
@ -257,11 +282,7 @@ where
|
|||||||
/// * `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<
|
ad9959: &mut ad9959::Ad9959<QspiInterface>,
|
||||||
QspiInterface,
|
|
||||||
DELAY,
|
|
||||||
hal::gpio::gpiog::PG7<hal::gpio::Output<hal::gpio::PushPull>>,
|
|
||||||
>,
|
|
||||||
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>,
|
||||||
@ -270,7 +291,6 @@ where
|
|||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
let mut devices = Self {
|
let mut devices = Self {
|
||||||
mcp23017,
|
mcp23017,
|
||||||
ad9959,
|
|
||||||
attenuator_spi,
|
attenuator_spi,
|
||||||
adc1,
|
adc1,
|
||||||
adc2,
|
adc2,
|
||||||
@ -295,212 +315,19 @@ where
|
|||||||
.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
|
||||||
|
.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)
|
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.
|
/// Reset all of the attenuators to a power-on default state.
|
||||||
fn reset_attenuators(&mut self) -> Result<(), Error> {
|
fn reset_attenuators(&mut self) -> Result<(), Error> {
|
||||||
self.mcp23017
|
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.
|
/// Sample an ADC channel.
|
||||||
///
|
///
|
||||||
/// Args:
|
/// Args:
|
||||||
|
Loading…
Reference in New Issue
Block a user