dds: more clk ctrl; scpi: sys_clk ctrl

This commit is contained in:
occheung 2020-09-16 12:05:45 +08:00
parent 331d1ff86f
commit e1abf87351
8 changed files with 221 additions and 93 deletions

View File

@ -18,7 +18,7 @@ libm = "0.2.0"
embedded-nal = "0.1.0"
minimq = { git = "https://github.com/quartiq/minimq.git", branch = "master" }
heapless = "0.5.5"
arrayvec = { version = "0.5.1", default-features = false, features = ["array-sizes-33-128", "array-sizes-129-255"] }
arrayvec = { version = "0.5.1", default-features = false, features = [ "array-sizes-33-128", "array-sizes-129-255" ] }
# Logging and Panicking
panic-itm = "0.4.1"
@ -31,7 +31,7 @@ lazy_static = { version = "1.4.0", features = ["spin_no_std"] }
git = "https://github.com/occheung/scpi-rs"
branch = "issue-4"
default-features = false
features = [ "build-info", "unit-frequency" ]
features = [ "build-info", "unit-frequency", "unit-angle" ]
# Use below SCPI dependency when need to modify SCPI fork offline
# [dependencies.scpi]

View File

@ -47,13 +47,15 @@ use firmware::{
Channel1SwitchCommand,
Channel2SwitchCommand,
Channel3SwitchCommand,
Channel0SystemClockCommand,
Channel0AttenuationCommand,
Channel1AttenuationCommand,
Channel2AttenuationCommand,
Channel3AttenuationCommand,
ClockSourceCommand,
ClockDivisionCommand,
ProfileCommand
ProfileCommand,
Channel0Profile0Singletone
},
Urukul, scpi_root, recursive_scpi_tree, scpi_tree
};
@ -208,8 +210,7 @@ fn main() -> ! {
let switch = CPLD::new(spi, (cs0, cs1, cs2), io_update);
let parts = switch.split();
let mut urukul = Urukul::new(
parts.spi1, parts.spi2, parts.spi3, parts.spi4, parts.spi5, parts.spi6, parts.spi7,
[25_000_000.0, 25_000_000.0, 25_000_000.0, 25_000_000.0]
parts.spi1, parts.spi2, parts.spi3, parts.spi4, parts.spi5, parts.spi6, parts.spi7
);
// Setup ethernet pins

View File

@ -44,6 +44,7 @@ use firmware::{
ClockSourceCommand,
ClockDivisionCommand,
ProfileCommand,
Channel0Profile0Singletone
},
Urukul, scpi_root, recursive_scpi_tree, scpi_tree
};
@ -209,8 +210,7 @@ fn main() -> ! {
let parts = cpld.split();
let mut urukul = Urukul::new(
parts.spi1, parts.spi2, parts.spi3, parts.spi4, parts.spi5, parts.spi6, parts.spi7,
[25_000_000.0, 25_000_000.0, 25_000_000.0, 25_000_000.0]
parts.spi1, parts.spi2, parts.spi3, parts.spi4, parts.spi5, parts.spi6, parts.spi7
);
cp.SCB.invalidate_icache();

View File

@ -216,6 +216,22 @@ where
}
}
// Change sys_clk frequency, method to be determined
pub fn set_sys_clk_frequency(&mut self, f_sys_clk: f64) -> Result<(), Error<E>> {
// If f_sys_clk is exactly the same as f_ref_clk, then invoke enable_normal_ref_clk
if f_sys_clk == self.f_ref_clk {
self.enable_normal_ref_clk()
}
// Otherwise, if the requested sys_clk is half of ref_clk, invoke enable_divided_ref_clk
else if f_sys_clk == (self.f_ref_clk / 2.0) {
self.enable_divided_ref_clk()
}
// Finally, try enabling PLL
else {
self.enable_pll(f_sys_clk)
}
}
#[allow(non_snake_case)]
fn get_VCO_no(&mut self, f_sys_clk: f64, divider: u8) -> Result<u8, Error<E>> {
// Select a VCO
@ -310,8 +326,8 @@ where
/*
* Set a single tone profile
* Phase: Expressed in positive degree
* Frequency: Must be integer
* Phase: Expressed in positive degree, i.e. [0.0, 360.0)
* Frequency: Must be non-negative
* Amplitude: In a scale from 0 to 1, taking float
*/
pub fn set_single_tone_profile(&mut self, profile: u8, f_out: f64, phase_offset: f64, amp_scale_factor: f64) -> Result<(), Error<E>> {

View File

@ -72,6 +72,7 @@ pub struct Urukul<SPI> {
config_register: ConfigRegister<SPI>,
attenuator: Attenuator<SPI>,
dds: [DDS<SPI>; 4],
f_master_clk: f64,
}
impl<SPI, E> Urukul<SPI>
@ -82,17 +83,21 @@ where
* Master constructor for the entire Urukul device
* Supply 7 SPI channels to Urukul and 4 reference clock frequencies
*/
pub fn new(spi1: SPI, spi2: SPI, spi3: SPI, spi4: SPI, spi5: SPI, spi6: SPI, spi7: SPI, f_ref_clks: [f64; 4]) -> Self {
pub fn new(spi1: SPI, spi2: SPI, spi3: SPI, spi4: SPI, spi5: SPI, spi6: SPI, spi7: SPI) -> Self {
// Construct Urukul
Urukul {
config_register: ConfigRegister::new(spi1),
attenuator: Attenuator::new(spi2),
// Create 4 DDS instances with fixed 25MHz clock
// Counter-intuitive to assign urukul clock before having a urukul
dds: [
DDS::new(spi4, f_ref_clks[1]),
DDS::new(spi5, f_ref_clks[1]),
DDS::new(spi6, f_ref_clks[2]),
DDS::new(spi7, f_ref_clks[3]),
DDS::new(spi4, 25_000_000.0),
DDS::new(spi5, 25_000_000.0),
DDS::new(spi6, 25_000_000.0),
DDS::new(spi7, 25_000_000.0),
],
// Default clock selection: OSC, fixed 100MHz speed
f_master_clk: 100_000_000.0,
}
}
@ -128,10 +133,11 @@ where
for chip_no in 0..4 {
self.dds[chip_no].init()?;
}
// Clock tree reset. CPLD divides clock frequency by 4 by default.
// Clock tree reset. OSC clock source by default
self.f_master_clk = 100_000_000.0;
// CPLD divides clock frequency by 4 by default.
for chip_no in 0..4 {
self.dds[chip_no].set_ref_clk_frequency(25_000_000.0)?;
self.dds[chip_no].set_ref_clk_frequency(self.f_master_clk / 4.0)?;
}
Ok(())
}
@ -158,6 +164,8 @@ pub trait UrukulTraits {
fn set_clock_division(&mut self, division: u8) -> Result<(), Self::Error>;
fn set_channel_attenuation(&mut self, channel: u8, attenuation: f32) -> Result<(), Self::Error>;
fn set_profile(&mut self, profile: u8) -> Result<(), Self::Error>;
fn set_channel_single_tone_profile(&mut self, channel: u8, profile: u8, frequency: f64, phase: f64, amplitude: f64) -> Result<(), Self::Error>;
fn set_channel_sys_clk(&mut self, channel: u8, sys_clk: f64) -> Result<(), Self::Error>;
}
impl<SPI, E> UrukulTraits for Urukul<SPI>
@ -207,13 +215,16 @@ where
(CFGMask::CLK_SEL0, 1),
]),
}?;
// Save the new master clock frequency
self.f_master_clk = frequency;
// Calculate reference clock frequency after clock division from configuration register
let frequency = frequency / (self.config_register.get_configuration(CFGMask::DIV) as f64);
let f_ref_clk = self.f_master_clk / (self.config_register.get_configuration(CFGMask::DIV) as f64);
// Update all DDS chips on reference clock frequency
for dds_channel in 0..4 {
self.dds[dds_channel].set_ref_clk_frequency(frequency)?;
self.dds[dds_channel].set_ref_clk_frequency(f_ref_clk)?;
}
Ok(())
}
@ -230,7 +241,16 @@ where
(CFGMask::DIV, 3),
]),
_ => Err(Error::ParameterError),
}.map(|_| ())
}?;
// Calculate reference clock frequency after clock division from configuration register
let f_ref_clk = self.f_master_clk / (division as f64);
// Update all DDS chips on reference clock frequency
for dds_channel in 0..4 {
self.dds[dds_channel].set_ref_clk_frequency(f_ref_clk)?;
}
Ok(())
}
fn set_channel_attenuation(&mut self, channel: u8, attenuation: f32) -> Result<(), Self::Error> {
@ -242,4 +262,12 @@ where
(CFGMask::PROFILE, profile.into())
]).map(|_| ())
}
fn set_channel_single_tone_profile(&mut self, channel: u8, profile: u8, frequency: f64, phase: f64, amplitude: f64) -> Result<(), Self::Error> {
self.dds[usize::from(channel)].set_single_tone_profile(profile, frequency, phase, amplitude)
}
fn set_channel_sys_clk(&mut self, channel: u8, f_sys_clk: f64) -> Result<(), Self::Error> {
self.dds[usize::from(channel)].set_sys_clk_frequency(f_sys_clk)
}
}

View File

@ -112,7 +112,7 @@ fn main() -> ! {
let mut config = ConfigRegister::new(parts.spi1);
let mut att = Attenuator::new(parts.spi2);
let mut dds0 = DDS::new(parts.spi4, 25_000_000);
let mut dds0 = DDS::new(parts.spi4, 25_000_000.0);
// Reset all DDS, set CLK_SEL to 0
config.set_configurations(&mut [
@ -135,75 +135,75 @@ fn main() -> ! {
(DDSCFRMask::READ_EFFECTIVE_FTW, 1),
]).unwrap();
dds0.enable_pll(1_000_000_000).unwrap();
dds0.set_sys_clk_frequency(1_000_000_000.0).unwrap();
// Attenuator
att.set_attenuation([
0.0, 31.5, 24.0, 0.0
5.0, 31.5, 24.0, 0.0
]).unwrap();
dds0.set_single_tone_profile(1, 10_000_000, 0.0, 0.5).unwrap();
dds0.set_single_tone_profile(1, 10_000_000.0, 0.0, 0.5).unwrap();
config.set_configurations(&mut [
(CFGMask::PROFILE, 1),
]).unwrap();
// Setup RAM configuration
dds0.set_configurations(&mut [
(DDSCFRMask::RAM_ENABLE, 1),
(DDSCFRMask::RAM_PLAYBACK_DST, 2),
]).unwrap();
// // Setup RAM configuration
// dds0.set_configurations(&mut [
// (DDSCFRMask::RAM_ENABLE, 1),
// (DDSCFRMask::RAM_PLAYBACK_DST, 2),
// ]).unwrap();
// Configure RAM profile 0
dds0.write_register(0x0E, &mut [
0x00, // Open
0x09, 0xC4, // Address step rate (2500)
0xFF, 0xC0, // End at address 1023
0x00, 0x00, // Start at address 0
0x04, // Recirculate mode
]).unwrap();
// // Configure RAM profile 0
// dds0.write_register(0x0E, &mut [
// 0x00, // Open
// 0x09, 0xC4, // Address step rate (2500)
// 0xFF, 0xC0, // End at address 1023
// 0x00, 0x00, // Start at address 0
// 0x04, // Recirculate mode
// ]).unwrap();
debug!("{:#X?}", dds0.read_register(0x0E, &mut[
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
]).unwrap());
// debug!("{:#X?}", dds0.read_register(0x0E, &mut[
// 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00,
// ]).unwrap());
// Choose profile 0
config.set_configurations(&mut [
(CFGMask::PROFILE, 0),
]).unwrap();
// // Choose profile 0
// config.set_configurations(&mut [
// (CFGMask::PROFILE, 0),
// ]).unwrap();
// Set RAM to be amplitudes, disable RAM momentarily
dds0.set_configurations(&mut [
(DDSCFRMask::RAM_PLAYBACK_DST, 0),
(DDSCFRMask::RAM_ENABLE, 0),
]).unwrap();
// // Set RAM to be amplitudes, disable RAM momentarily
// dds0.set_configurations(&mut [
// (DDSCFRMask::RAM_PLAYBACK_DST, 0),
// (DDSCFRMask::RAM_ENABLE, 0),
// ]).unwrap();
let mut ram_data: [u8; ((1024 * 4) + 1)] = [0; (1024 * 4) + 1];
ram_data[0] = 0x16;
for index in 0..1024 {
if index % 2 == 1 {
ram_data[(index * 4) + 1] = 0x3F;
ram_data[(index * 4) + 2] = 0xFF;
} else {
ram_data[(index * 4) + 1] = 0x00;
ram_data[(index * 4) + 2] = 0x00;
}
// ram_data[(index * 4) + 1] = ((index >> 2) & 0xFF) as u8;
// ram_data[(index * 4) + 2] = ((index & 0x03) << 6) as u8;
}
dds0.transfer(&mut ram_data).unwrap();
// let mut ram_data: [u8; ((1024 * 4) + 1)] = [0; (1024 * 4) + 1];
// ram_data[0] = 0x16;
// for index in 0..1024 {
// if index % 2 == 1 {
// ram_data[(index * 4) + 1] = 0x3F;
// ram_data[(index * 4) + 2] = 0xFF;
// } else {
// ram_data[(index * 4) + 1] = 0x00;
// ram_data[(index * 4) + 2] = 0x00;
// }
// // ram_data[(index * 4) + 1] = ((index >> 2) & 0xFF) as u8;
// // ram_data[(index * 4) + 2] = ((index & 0x03) << 6) as u8;
// }
// dds0.transfer(&mut ram_data).unwrap();
config.set_configurations(&mut [
(CFGMask::PROFILE, 1),
]).unwrap();
// config.set_configurations(&mut [
// (CFGMask::PROFILE, 1),
// ]).unwrap();
config.set_configurations(&mut [
(CFGMask::PROFILE, 0),
]).unwrap();
// config.set_configurations(&mut [
// (CFGMask::PROFILE, 0),
// ]).unwrap();
dds0.set_configurations(&mut [
(DDSCFRMask::RAM_ENABLE, 1),
]).unwrap();
// dds0.set_configurations(&mut [
// (DDSCFRMask::RAM_ENABLE, 1),
// ]).unwrap();
loop {}
}

View File

@ -13,6 +13,7 @@ use scpi::{
};
use scpi::suffix::{Amplitude, Db};
use uom::si::frequency::{gigahertz, hertz, kilohertz, megahertz, Frequency};
use uom::si::angle::{degree, gon, minute as aminute, radian, revolution, Angle};
use uom::si::{f32, f64};
use embedded_hal::blocking::spi::Transfer;
@ -116,7 +117,11 @@ macro_rules! scpi_tree {
scpi_root!(
"CHANNEL0" => {
"SWitch" => Channel0SwitchCommand,
"Attenuation" => Channel0AttenuationCommand
"Attenuation" => Channel0AttenuationCommand,
"SYSCLOCK" => Channel0SystemClockCommand,
"PROFILE0" => {
"SINGLEtone" => Channel0Profile0Singletone
}
},
"CHANNEL1" => {
"SWitch" => Channel1SwitchCommand,
@ -164,11 +169,16 @@ pub struct Channel2SwitchCommand {}
pub struct Channel3SwitchCommand {}
pub struct ClockSourceCommand {}
pub struct ClockDivisionCommand {}
pub struct Channel0SystemClockCommand {}
pub struct Channel0AttenuationCommand {}
pub struct Channel1AttenuationCommand {}
pub struct Channel2AttenuationCommand {}
pub struct Channel3AttenuationCommand {}
pub struct ProfileCommand {}
pub struct Channel0Profile0Singletone {}
pub struct Channel0Profile0SingletoneFrequency {}
pub struct Channel0Profile0SingletonePhase {}
pub struct Channel0Profile0SingletoneAmplitude {}
impl<T: Device + UrukulTraits> Command<T> for Channel0SwitchCommand {
nquery!();
@ -253,7 +263,7 @@ impl<T:Device + UrukulTraits> Command<T> for ClockSourceCommand {
_ => Err(ErrorCode::IllegalParameterValue.into()),
})
})?;
trace!("Received frequency: {:?}", frequency);
trace!("Received master clock frequency: {:?}", frequency);
let clock_source = match s_str {
source if source.eq_ignore_ascii_case("OSC") => {
@ -302,6 +312,28 @@ impl<T:Device + UrukulTraits> Command<T> for ClockDivisionCommand {
}
}
impl<T:Device + UrukulTraits> Command<T> for Channel0SystemClockCommand {
nquery!();
// Param: <frequency>
// The exact method of generating this frequency is auto-decided
// The process is delegated to individual DDS chip
fn event(&self, context: &mut Context<T>, args: &mut Tokenizer) -> Result<()> {
let frequency: f64::Frequency = args.next_data(true)?
.map_or(Ok(f64::Frequency::new::<hertz>(0.0)), |t| {
t.numeric(|s| match s {
NumericValues::Default => Ok(f64::Frequency::new::<hertz>(0.0)),
_ => Err(ErrorCode::IllegalParameterValue.into()),
})
})?;
trace!("Received channel 0 system clock frequency: {:?}", frequency);
// Setup sys_clk through urukul interface
context.device.set_channel_sys_clk(0, frequency.get::<hertz>()).map_err(|_| Error::new(ErrorCode::IllegalParameterValue))
}
}
impl<T:Device + UrukulTraits> Command<T> for Channel0AttenuationCommand {
nquery!();
@ -376,6 +408,57 @@ impl<T:Device + UrukulTraits> Command<T> for ProfileCommand {
}
}
impl<T:Device + UrukulTraits> Command<T> for Channel0Profile0Singletone {
nquery!();
// Params: frequency, phase, amplitude (all mandatory)
fn event(&self, context: &mut Context<T>, args: &mut Tokenizer) -> Result<()> {
// Read output frequency
let frequency: f64::Frequency = args.next_data(false)?
.map_or(Err(Error::new(ErrorCode::MissingParameter)), |t| {
t.numeric(|s| match s {
NumericValues::Default => Ok(f64::Frequency::new::<hertz>(0.0)),
_ => Err(ErrorCode::IllegalParameterValue.into()),
})
})?;
trace!("Received channel 0 profile 0 output single tone frequency: {:?}", frequency);
// Handle negative frequency
if frequency.get::<hertz>() < 0.0 {
return Err(ErrorCode::DataOutOfRange.into());
}
// Read phase offset
let phase: f64::Angle = args.next_data(false)?
.map_or(Err(Error::new(ErrorCode::MissingParameter)), |t| {
t.numeric(
|s| match s {
NumericValues::Default => Ok(f64::Angle::new::<degree>(0.0)),
_ => Err(ErrorCode::IllegalParameterValue.into()),
})
})?;
trace!("Received channel 0 profile 0 output single tone phase offset: {:?}", phase);
// Handle out-of-bound phase offset
if phase.get::<degree>() < 0.0 || phase.get::<degree>() >= 360.0 {
return Err(ErrorCode::DataOutOfRange.into());
}
// Read amplitude offset
let amplitude: f64 = args.next_data(false)?
.map_or(Err(Error::new(ErrorCode::MissingParameter)),
|token| token.try_into())?;
trace!("Received channel 0 profile 0 output single tone amplitude offset: {:?}", amplitude);
// Handle out-of-bound phase offset
if amplitude < 0.0 || amplitude > 1.0 {
return Err(ErrorCode::DataOutOfRange.into());
}
// TODO: Setup single tone on DDS
context.device.set_channel_single_tone_profile(0, 0, frequency.get::<hertz>(), phase.get::<degree>(), amplitude)
.map_err(|_| Error::new(ErrorCode::HardwareError))
}
}
/*
* Implement "Device" trait from SCPI
* TODO: Implement mandatory commands

View File

@ -5,8 +5,8 @@ use log::{trace, info};
use arrayvec::{ArrayVec};
pub trait MqttScpiTranslator {
// Convert an MQTT publish message into SCPI compatible command
// The argument part/ MQTT message must follow SCPI standard for parameter formatting
// Unwrap an MQTT publish message into SCPI compatible command
// The command part/ MQTT message must follow SCPI standard for parameter formatting
fn run_with_mqtt<FMT: Formatter>(&mut self, topic: &str, args: &str, response: &mut FMT) -> Result<()>;
}
@ -20,27 +20,27 @@ impl<'a, T: Device> MqttScpiTranslator for Context<'a, T> {
return Ok(());
}
let command_topic = topic.strip_prefix("Urukul/Control/")
.unwrap_or("");
// let command_topic = topic.strip_prefix("Urukul/Control")
// .unwrap_or("");
// Create a fixed-size buffer to handle slice operation
let mut buffer = ArrayVec::<[u8; 1024]>::new();
// Copy MQTT topic, convert it into SCPI header format
for i in command_topic.chars() {
if i == '/' {
// The topic separator is colon(':') in SCPI, and slash('/') in MQTT
buffer.try_push(b':')
.map_err(|_| ErrorCode::OutOfMemory)?;
} else {
buffer.try_push(i as u8)
.map_err(|_| ErrorCode::OutOfMemory)?;
}
}
// // Copy MQTT topic, convert it into SCPI header format
// for i in command_topic.chars() {
// if i == '/' {
// // The topic separator is colon(':') in SCPI, and slash('/') in MQTT
// buffer.try_push(b':')
// .map_err(|_| ErrorCode::OutOfMemory)?;
// } else {
// buffer.try_push(i as u8)
// .map_err(|_| ErrorCode::OutOfMemory)?;
// }
// }
// Place a space bar between header and parameter
buffer.try_push(b' ')
.map_err(|_| ErrorCode::OutOfMemory)?;
// // Place a space bar between header and parameter
// buffer.try_push(b' ')
// .map_err(|_| ErrorCode::OutOfMemory)?;
// Copy the arguments into the buffer
for i in args.chars() {