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

@ -31,7 +31,7 @@ lazy_static = { version = "1.4.0", features = ["spin_no_std"] }
git = "https://github.com/occheung/scpi-rs" git = "https://github.com/occheung/scpi-rs"
branch = "issue-4" branch = "issue-4"
default-features = false 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 # Use below SCPI dependency when need to modify SCPI fork offline
# [dependencies.scpi] # [dependencies.scpi]

View File

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

View File

@ -44,6 +44,7 @@ use firmware::{
ClockSourceCommand, ClockSourceCommand,
ClockDivisionCommand, ClockDivisionCommand,
ProfileCommand, ProfileCommand,
Channel0Profile0Singletone
}, },
Urukul, scpi_root, recursive_scpi_tree, scpi_tree Urukul, scpi_root, recursive_scpi_tree, scpi_tree
}; };
@ -209,8 +210,7 @@ fn main() -> ! {
let parts = cpld.split(); let parts = cpld.split();
let mut urukul = Urukul::new( let mut urukul = Urukul::new(
parts.spi1, parts.spi2, parts.spi3, parts.spi4, parts.spi5, parts.spi6, parts.spi7, 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]
); );
cp.SCB.invalidate_icache(); 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)] #[allow(non_snake_case)]
fn get_VCO_no(&mut self, f_sys_clk: f64, divider: u8) -> Result<u8, Error<E>> { fn get_VCO_no(&mut self, f_sys_clk: f64, divider: u8) -> Result<u8, Error<E>> {
// Select a VCO // Select a VCO
@ -310,8 +326,8 @@ where
/* /*
* Set a single tone profile * Set a single tone profile
* Phase: Expressed in positive degree * Phase: Expressed in positive degree, i.e. [0.0, 360.0)
* Frequency: Must be integer * Frequency: Must be non-negative
* Amplitude: In a scale from 0 to 1, taking float * 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>> { 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>, config_register: ConfigRegister<SPI>,
attenuator: Attenuator<SPI>, attenuator: Attenuator<SPI>,
dds: [DDS<SPI>; 4], dds: [DDS<SPI>; 4],
f_master_clk: f64,
} }
impl<SPI, E> Urukul<SPI> impl<SPI, E> Urukul<SPI>
@ -82,17 +83,21 @@ where
* Master constructor for the entire Urukul device * Master constructor for the entire Urukul device
* Supply 7 SPI channels to Urukul and 4 reference clock frequencies * 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 // Construct Urukul
Urukul { Urukul {
config_register: ConfigRegister::new(spi1), config_register: ConfigRegister::new(spi1),
attenuator: Attenuator::new(spi2), attenuator: Attenuator::new(spi2),
// Create 4 DDS instances with fixed 25MHz clock
// Counter-intuitive to assign urukul clock before having a urukul
dds: [ dds: [
DDS::new(spi4, f_ref_clks[1]), DDS::new(spi4, 25_000_000.0),
DDS::new(spi5, f_ref_clks[1]), DDS::new(spi5, 25_000_000.0),
DDS::new(spi6, f_ref_clks[2]), DDS::new(spi6, 25_000_000.0),
DDS::new(spi7, f_ref_clks[3]), 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 { for chip_no in 0..4 {
self.dds[chip_no].init()?; self.dds[chip_no].init()?;
} }
// Clock tree reset. OSC clock source by default
// Clock tree reset. CPLD divides clock frequency by 4 by default. self.f_master_clk = 100_000_000.0;
// CPLD divides clock frequency by 4 by default.
for chip_no in 0..4 { 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(()) Ok(())
} }
@ -158,6 +164,8 @@ pub trait UrukulTraits {
fn set_clock_division(&mut self, division: u8) -> Result<(), Self::Error>; 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_channel_attenuation(&mut self, channel: u8, attenuation: f32) -> Result<(), Self::Error>;
fn set_profile(&mut self, profile: u8) -> 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> impl<SPI, E> UrukulTraits for Urukul<SPI>
@ -208,12 +216,15 @@ where
]), ]),
}?; }?;
// Save the new master clock frequency
self.f_master_clk = frequency;
// Calculate reference clock frequency after clock division from configuration register // 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 // Update all DDS chips on reference clock frequency
for dds_channel in 0..4 { 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(()) Ok(())
} }
@ -230,7 +241,16 @@ where
(CFGMask::DIV, 3), (CFGMask::DIV, 3),
]), ]),
_ => Err(Error::ParameterError), _ => 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> { fn set_channel_attenuation(&mut self, channel: u8, attenuation: f32) -> Result<(), Self::Error> {
@ -242,4 +262,12 @@ where
(CFGMask::PROFILE, profile.into()) (CFGMask::PROFILE, profile.into())
]).map(|_| ()) ]).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 config = ConfigRegister::new(parts.spi1);
let mut att = Attenuator::new(parts.spi2); 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 // Reset all DDS, set CLK_SEL to 0
config.set_configurations(&mut [ config.set_configurations(&mut [
@ -135,75 +135,75 @@ fn main() -> ! {
(DDSCFRMask::READ_EFFECTIVE_FTW, 1), (DDSCFRMask::READ_EFFECTIVE_FTW, 1),
]).unwrap(); ]).unwrap();
dds0.enable_pll(1_000_000_000).unwrap(); dds0.set_sys_clk_frequency(1_000_000_000.0).unwrap();
// Attenuator // Attenuator
att.set_attenuation([ att.set_attenuation([
0.0, 31.5, 24.0, 0.0 5.0, 31.5, 24.0, 0.0
]).unwrap(); ]).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 [ config.set_configurations(&mut [
(CFGMask::PROFILE, 1), (CFGMask::PROFILE, 1),
]).unwrap(); ]).unwrap();
// Setup RAM configuration // // Setup RAM configuration
dds0.set_configurations(&mut [ // dds0.set_configurations(&mut [
(DDSCFRMask::RAM_ENABLE, 1), // (DDSCFRMask::RAM_ENABLE, 1),
(DDSCFRMask::RAM_PLAYBACK_DST, 2), // (DDSCFRMask::RAM_PLAYBACK_DST, 2),
]).unwrap(); // ]).unwrap();
// Configure RAM profile 0 // // Configure RAM profile 0
dds0.write_register(0x0E, &mut [ // dds0.write_register(0x0E, &mut [
0x00, // Open // 0x00, // Open
0x09, 0xC4, // Address step rate (2500) // 0x09, 0xC4, // Address step rate (2500)
0xFF, 0xC0, // End at address 1023 // 0xFF, 0xC0, // End at address 1023
0x00, 0x00, // Start at address 0 // 0x00, 0x00, // Start at address 0
0x04, // Recirculate mode // 0x04, // Recirculate mode
]).unwrap(); // ]).unwrap();
debug!("{:#X?}", dds0.read_register(0x0E, &mut[ // debug!("{:#X?}", dds0.read_register(0x0E, &mut[
0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00,
]).unwrap()); // ]).unwrap());
// Choose profile 0 // // Choose profile 0
config.set_configurations(&mut [ // config.set_configurations(&mut [
(CFGMask::PROFILE, 0), // (CFGMask::PROFILE, 0),
]).unwrap(); // ]).unwrap();
// Set RAM to be amplitudes, disable RAM momentarily // // Set RAM to be amplitudes, disable RAM momentarily
dds0.set_configurations(&mut [ // dds0.set_configurations(&mut [
(DDSCFRMask::RAM_PLAYBACK_DST, 0), // (DDSCFRMask::RAM_PLAYBACK_DST, 0),
(DDSCFRMask::RAM_ENABLE, 0), // (DDSCFRMask::RAM_ENABLE, 0),
]).unwrap(); // ]).unwrap();
let mut ram_data: [u8; ((1024 * 4) + 1)] = [0; (1024 * 4) + 1]; // let mut ram_data: [u8; ((1024 * 4) + 1)] = [0; (1024 * 4) + 1];
ram_data[0] = 0x16; // ram_data[0] = 0x16;
for index in 0..1024 { // for index in 0..1024 {
if index % 2 == 1 { // if index % 2 == 1 {
ram_data[(index * 4) + 1] = 0x3F; // ram_data[(index * 4) + 1] = 0x3F;
ram_data[(index * 4) + 2] = 0xFF; // ram_data[(index * 4) + 2] = 0xFF;
} else { // } else {
ram_data[(index * 4) + 1] = 0x00; // ram_data[(index * 4) + 1] = 0x00;
ram_data[(index * 4) + 2] = 0x00; // ram_data[(index * 4) + 2] = 0x00;
} // }
// ram_data[(index * 4) + 1] = ((index >> 2) & 0xFF) as u8; // // ram_data[(index * 4) + 1] = ((index >> 2) & 0xFF) as u8;
// ram_data[(index * 4) + 2] = ((index & 0x03) << 6) as u8; // // ram_data[(index * 4) + 2] = ((index & 0x03) << 6) as u8;
} // }
dds0.transfer(&mut ram_data).unwrap(); // dds0.transfer(&mut ram_data).unwrap();
config.set_configurations(&mut [ // config.set_configurations(&mut [
(CFGMask::PROFILE, 1), // (CFGMask::PROFILE, 1),
]).unwrap(); // ]).unwrap();
config.set_configurations(&mut [ // config.set_configurations(&mut [
(CFGMask::PROFILE, 0), // (CFGMask::PROFILE, 0),
]).unwrap(); // ]).unwrap();
dds0.set_configurations(&mut [ // dds0.set_configurations(&mut [
(DDSCFRMask::RAM_ENABLE, 1), // (DDSCFRMask::RAM_ENABLE, 1),
]).unwrap(); // ]).unwrap();
loop {} loop {}
} }

View File

@ -13,6 +13,7 @@ use scpi::{
}; };
use scpi::suffix::{Amplitude, Db}; use scpi::suffix::{Amplitude, Db};
use uom::si::frequency::{gigahertz, hertz, kilohertz, megahertz, Frequency}; 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 uom::si::{f32, f64};
use embedded_hal::blocking::spi::Transfer; use embedded_hal::blocking::spi::Transfer;
@ -116,7 +117,11 @@ macro_rules! scpi_tree {
scpi_root!( scpi_root!(
"CHANNEL0" => { "CHANNEL0" => {
"SWitch" => Channel0SwitchCommand, "SWitch" => Channel0SwitchCommand,
"Attenuation" => Channel0AttenuationCommand "Attenuation" => Channel0AttenuationCommand,
"SYSCLOCK" => Channel0SystemClockCommand,
"PROFILE0" => {
"SINGLEtone" => Channel0Profile0Singletone
}
}, },
"CHANNEL1" => { "CHANNEL1" => {
"SWitch" => Channel1SwitchCommand, "SWitch" => Channel1SwitchCommand,
@ -164,11 +169,16 @@ pub struct Channel2SwitchCommand {}
pub struct Channel3SwitchCommand {} pub struct Channel3SwitchCommand {}
pub struct ClockSourceCommand {} pub struct ClockSourceCommand {}
pub struct ClockDivisionCommand {} pub struct ClockDivisionCommand {}
pub struct Channel0SystemClockCommand {}
pub struct Channel0AttenuationCommand {} pub struct Channel0AttenuationCommand {}
pub struct Channel1AttenuationCommand {} pub struct Channel1AttenuationCommand {}
pub struct Channel2AttenuationCommand {} pub struct Channel2AttenuationCommand {}
pub struct Channel3AttenuationCommand {} pub struct Channel3AttenuationCommand {}
pub struct ProfileCommand {} pub struct ProfileCommand {}
pub struct Channel0Profile0Singletone {}
pub struct Channel0Profile0SingletoneFrequency {}
pub struct Channel0Profile0SingletonePhase {}
pub struct Channel0Profile0SingletoneAmplitude {}
impl<T: Device + UrukulTraits> Command<T> for Channel0SwitchCommand { impl<T: Device + UrukulTraits> Command<T> for Channel0SwitchCommand {
nquery!(); nquery!();
@ -253,7 +263,7 @@ impl<T:Device + UrukulTraits> Command<T> for ClockSourceCommand {
_ => Err(ErrorCode::IllegalParameterValue.into()), _ => Err(ErrorCode::IllegalParameterValue.into()),
}) })
})?; })?;
trace!("Received frequency: {:?}", frequency); trace!("Received master clock frequency: {:?}", frequency);
let clock_source = match s_str { let clock_source = match s_str {
source if source.eq_ignore_ascii_case("OSC") => { 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 { impl<T:Device + UrukulTraits> Command<T> for Channel0AttenuationCommand {
nquery!(); 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 * Implement "Device" trait from SCPI
* TODO: Implement mandatory commands * TODO: Implement mandatory commands

View File

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