Compare commits

...

10 Commits

Author SHA1 Message Date
25f8363e54 dds: add RAM mode 2020-09-18 12:25:39 +08:00
a5dbfdaf4f mqtt_mux: remove excessive paranthesis 2020-09-18 12:24:27 +08:00
a7073c419b reduce warning 2020-09-18 12:23:28 +08:00
72770d9276 mqtt_client: take out scpi 2020-09-17 17:05:09 +08:00
74f7b7ce61 mqtt_mux: init openocd 2020-09-17 17:02:41 +08:00
3758029a52 mqtt_mux: init 2020-09-17 17:02:01 +08:00
54e91c49e0 mf: using file name 2020-09-17 13:33:59 +08:00
e174961bff mf: init 2020-09-17 13:29:33 +08:00
537a375f2c nix: rewire configure-fpga command 2020-09-17 13:19:23 +08:00
c084da1848 migen: move add extension into build 2020-09-17 12:59:22 +08:00
15 changed files with 400 additions and 198 deletions

View File

@ -13,7 +13,6 @@ embedded-hal = "0.2.4"
stm32h7xx-hal = {version = "0.7.1", features = [ "stm32h743v", "rt", "unproven", "ethernet", "phy_lan8742a" ] }
smoltcp = { version = "0.6.0", default-features = false, features = [ "ethernet", "proto-ipv4", "proto-ipv6", "socket-tcp", "log" ] }
nb = "1.0.0"
libm = "0.2.0"
embedded-nal = "0.1.0"
minimq = { git = "https://github.com/quartiq/minimq.git", branch = "master" }
@ -33,6 +32,18 @@ branch = "issue-4"
default-features = false
features = [ "build-info", "unit-frequency", "unit-angle" ]
# [dependencies.uom]
# version = "0.29.0"
# default-features = false
# features = [
# "autoconvert",
# "usize", "u8", "u16", "u32", "u64",
# "isize", "i8", "i16", "i32", "i64",
# "f32", "f64",
# "si",
# "try-from"
# ]
# Use below SCPI dependency when need to modify SCPI fork offline
# [dependencies.scpi]
# path = "../scpi-fork/scpi"

9
Makefile Normal file
View File

@ -0,0 +1,9 @@
fpga_config: fpga_config.rs
openocd -f openocd/openocd.cfg -f openocd/fpga-config.cfg
fpga_config.rs: top.bin
cargo build --example fpga_config
top.bin:
python3 migen/fpga_config.py

View File

@ -3,20 +3,12 @@
extern crate log;
use log::debug;
use stm32h7xx_hal::hal::digital::v2::{
InputPin,
OutputPin,
};
use stm32h7xx_hal::{gpio::Speed, pac, prelude::*, spi};
use stm32h7xx_hal::{pac, prelude::*, spi};
use cortex_m;
use cortex_m::asm::nop;
use cortex_m_rt::entry;
use core::ptr;
use nb::block;
use firmware::flash::flash_ice40_fpga;
#[path = "util/logger.rs"]
@ -42,7 +34,7 @@ fn main() -> ! {
logger::enable_itm(&dp.DBGMCU, &mut cp.DCB, &mut cp.ITM);
}
let mut delay = cp.SYST.delay(ccdr.clocks);
let delay = cp.SYST.delay(ccdr.clocks);
let gpioa = dp.GPIOA.split(ccdr.peripheral.GPIOA);
let gpiob = dp.GPIOB.split(ccdr.peripheral.GPIOB);
@ -61,14 +53,14 @@ fn main() -> ! {
let fpga_sdi = gpiob.pb5.into_alternate_af5();
// Setup SPI_SS_B and CRESET_B
let mut fpga_ss = gpioa.pa4.into_push_pull_output();
let mut fpga_creset = gpiof.pf3.into_open_drain_output();
let fpga_ss = gpioa.pa4.into_push_pull_output();
let fpga_creset = gpiof.pf3.into_open_drain_output();
// Setup CDONE
let fpga_cdone = gpiod.pd15.into_pull_up_input();
// Setup SPI interface
let mut fpga_cfg_spi = dp.SPI1.spi(
let fpga_cfg_spi = dp.SPI1.spi(
(fpga_sck, fpga_sdo, fpga_sdi),
spi::MODE_3,
12.mhz(),

View File

@ -1,74 +1,31 @@
#![no_std]
#![no_main]
#[macro_use]
extern crate log;
use log::{info, warn};
use smoltcp as net;
use stm32h7xx_hal::ethernet;
use stm32h7xx_hal::{gpio::Speed, prelude::*, spi, pac};
use embedded_hal::{
blocking::spi::Transfer,
digital::v2::OutputPin,
};
use heapless::{consts, String};
use heapless::consts;
use cortex_m;
use cortex_m_rt::{
entry,
exception,
};
use cortex_m_rt::entry;
use rtic::cyccnt::{Instant, U32Ext};
use minimq::{
embedded_nal::{IpAddr, Ipv4Addr, TcpStack, SocketAddr, Mode},
embedded_nal::{IpAddr, Ipv4Addr, TcpStack},
MqttClient, QoS
};
use firmware::nal_tcp_client::{NetworkStack, NetStorage, NetworkInterface};
use firmware::nal_tcp_client::{NetworkStack, NetStorage};
use firmware::{
cpld::{
CPLD,
},
scpi::{
HelloWorldCommand,
Channel0SwitchCommand,
Channel1SwitchCommand,
Channel2SwitchCommand,
Channel3SwitchCommand,
Channel0AttenuationCommand,
Channel1AttenuationCommand,
Channel2AttenuationCommand,
Channel3AttenuationCommand,
ClockSourceCommand,
ClockDivisionCommand,
ProfileCommand,
Channel0Profile0Singletone
},
Urukul, scpi_root, recursive_scpi_tree, scpi_tree
};
use firmware::translation::MqttScpiTranslator;
use scpi::prelude::*;
use scpi::ieee488::commands::*;
use scpi::scpi::commands::*;
use scpi::{
ieee488_cls,
ieee488_ese,
ieee488_esr,
ieee488_idn,
ieee488_opc,
ieee488_rst,
ieee488_sre,
ieee488_stb,
ieee488_tst,
ieee488_wai,
scpi_crate_version,
scpi_status,
scpi_system,
};
use scpi::Context;
use firmware::Urukul;
use firmware::mqtt_mux::MqttMux;
#[path = "util/logger.rs"]
mod logger;
@ -129,7 +86,7 @@ fn main() -> ! {
logger::enable_itm(&dp.DBGMCU, &mut cp.DCB, &mut cp.ITM);
}
logger::init();
let mut delay = cp.SYST.delay(ccdr.clocks);
// let mut delay = cp.SYST.delay(ccdr.clocks);
let gpioa = dp.GPIOA.split(ccdr.peripheral.GPIOA);
let gpiob = dp.GPIOB.split(ccdr.peripheral.GPIOB);
@ -209,23 +166,16 @@ fn main() -> ! {
let cpld = CPLD::new(spi, (cs0, cs1, cs2), io_update);
let parts = cpld.split();
let mut urukul = Urukul::new(
let urukul = Urukul::new(
parts.spi1, parts.spi2, parts.spi3, parts.spi4, parts.spi5, parts.spi6, parts.spi7
);
cp.SCB.invalidate_icache();
cp.SCB.enable_icache();
// Define SCPI tree
let tree = scpi_tree!();
// Device was declared in prior
let mut errors = ArrayErrorQueue::<[Error; 10]>::new();
let mut context = Context::new(&mut urukul, &mut errors, tree);
//Response bytebuffer
let mut buf = ArrayVecFormatter::<[u8; 256]>::new();
// SCPI configs END
// TODO: Remove this simple test
let mut mqtt_mux = MqttMux::new(urukul);
info!("{:?}", mqtt_mux.handle_command("Urukul/Control/Channel0/Switch", "on".as_bytes()));
// Time unit in ms
let mut time: u32 = 0;
@ -274,10 +224,7 @@ fn main() -> ! {
topic => {
info!("On '{:?}', received: {:?}", topic, message);
// Why is topic a string while message is a slice?
context.run_with_mqtt(topic,
core::str::from_utf8(message).unwrap(),
&mut buf)
.unwrap();
mqtt_mux.handle_command(topic, message).unwrap();
},
}).is_ok();

View File

@ -1,24 +1,26 @@
# Import built in I/O, Connectors & Platform template for Humpback
from migen.build.platforms.sinara import humpback
# Import migen platform for Lattice Products
from migen.build.lattice import LatticePlatform
# Import migen pin record structure
from migen.build.generic_platform import *
from migen.fhdl.module import Module
from migen.fhdl.specials import Instance
from migen.genlib.io import *
from migen.build.lattice.common import LatticeiCE40DifferentialInputImpl
from migen.genlib.io import DifferentialInput
spi_cs = [
("spi_cs", 0, Pins("B13 B14 B15"), IOStandard("LVCMOS33"))
]
io_update = [
("io_update", 0, Pins("A11"), IOStandard("LVCMOS33"))
]
class UrukulConnector(Module):
def __init__(self, platform):
# Include extension
spi_cs = [
("spi_cs", 0, Pins("B13 B14 B15"), IOStandard("LVCMOS33"))
]
io_update = [
("io_update", 0, Pins("A11"), IOStandard("LVCMOS33"))
]
# Add extensions
platform.add_extension(spi_cs)
platform.add_extension(io_update)
# Request EEM I/O & SPI
eem0 = [
platform.request("eem0", 0),
@ -82,6 +84,4 @@ class UrukulConnector(Module):
if __name__ == "__main__":
platform = humpback.Platform()
platform.add_extension(spi_cs)
platform.add_extension(io_update)
platform.build(UrukulConnector(platform))

View File

@ -2,4 +2,5 @@ init
reset init
halt
flash write_image erase target/thumbv7em-none-eabihf/debug/examples/ethernet
reset run
shutdown

View File

@ -2,4 +2,5 @@ init
reset init
halt
flash write_image erase target/thumbv7em-none-eabihf/debug/examples/fpga_config
reset run
shutdown

6
openocd/mqtt-client.cfg Normal file
View File

@ -0,0 +1,6 @@
init
reset init
halt
flash write_image erase target/thumbv7em-none-eabihf/debug/examples/mqtt_client
reset run
shutdown

View File

@ -40,14 +40,13 @@ let
'';
flashFPGAConfig = writeShellScriptBin "flash-fpga-config" ''
set-gdb-config-file "fpga_config" && cargo run --example fpga_config
openocd-flash fpga-config
'';
configureFPGA = writeShellScriptBin "configure-fpga" ''
nc -zv localhost 3333 \
&& compile-migen-script \
&& flash-fpga-config \
|| echo "Please run OpenOcd first."
compile-migen-script \
&& cargo build --example fpga_config \
&& flash-fpga-config
'';
verifyFPGAConfig = writeShellScriptBin "verify-fpga-config" ''

View File

@ -42,6 +42,7 @@ macro_rules! construct_bitmask {
assert!(arg < (2 << self.get_width()));
(arg << (self.get_shift() % ((size_of::<$unsigned_type>() as u8) * 8)))
}
#[allow(dead_code)]
pub(crate) fn set_data_by_arg(self, data: &mut $unsigned_type, arg: $unsigned_type) {
// Clear bits in field, then insert shifted argument
*data &= (!self.get_bitmask());

View File

@ -1,7 +1,8 @@
use embedded_hal::blocking::spi::Transfer;
use crate::Error;
use core::mem::size_of;
use libm::round;
use core::convert::TryInto;
use arrayvec::ArrayVec;
/*
* Bitmask for all configurations (Order: CFR3, CFR2, CFR1)
@ -63,6 +64,23 @@ construct_bitmask!(DDSCFRMask; u32;
const WRITE_MASK :u8 = 0x00;
const READ_MASK :u8 = 0x80;
#[derive(Clone)]
pub enum RAMDestination {
Frequency = 0,
Phase = 1,
Amplitude = 2,
Polar = 3,
}
#[derive(Clone)]
pub enum RAMOperationMode {
DirectSwitch = 0,
RampUp = 1,
BidirectionalRamp = 2,
ContinuousBidirectionalRamp = 3,
ContinuousRecirculate = 4,
}
pub struct DDS<SPI> {
spi: SPI,
f_ref_clk: f64,
@ -143,7 +161,7 @@ where
// Get a divider
let divider = (f_sys_clk / self.f_ref_clk) as u64;
// Reject extreme divider values. However, accept no frequency division
if ((divider > 127 || divider < 12) && divider != 1) {
if (divider > 127 || divider < 12) && divider != 1 {
// panic!("Invalid divider value for PLL!");
return Err(Error::DDSCLKError);
}
@ -345,16 +363,10 @@ where
} else {
((resolutions[2] as f64) * amp_scale_factor) as u16
};
// Setup configuration registers before writing single tone register
self.set_configurations(&mut [
(DDSCFRMask::RAM_ENABLE, 0),
(DDSCFRMask::DIGITAL_RAMP_ENABLE, 0),
(DDSCFRMask::OSK_ENABLE, 0),
(DDSCFRMask::PARALLEL_DATA_PORT_ENABLE, 0),
])?;
self.set_configurations(&mut [
(DDSCFRMask::EN_AMP_SCALE_SINGLE_TONE_PRO, 1),
])?;
self.enable_single_tone_configuration()?;
// Transfer single tone profile data
self.write_register(0x0E + profile, &mut [
((asf >> 8 ) & 0xFF) as u8,
@ -376,15 +388,7 @@ where
pub fn set_single_tone_profile_frequency(&mut self, profile: u8, f_out: f64) -> Result<(), Error<E>> {
// Setup configuration registers before writing single tone register
self.set_configurations(&mut [
(DDSCFRMask::RAM_ENABLE, 0),
(DDSCFRMask::DIGITAL_RAMP_ENABLE, 0),
(DDSCFRMask::OSK_ENABLE, 0),
(DDSCFRMask::PARALLEL_DATA_PORT_ENABLE, 0),
])?;
self.set_configurations(&mut [
(DDSCFRMask::EN_AMP_SCALE_SINGLE_TONE_PRO, 1),
])?;
self.enable_single_tone_configuration()?;
// Calculate frequency tuning work (FTW)
let f_res: u64 = 1 << 32;
@ -412,15 +416,7 @@ where
pub fn set_single_tone_profile_phase(&mut self, profile: u8, phase_offset: f64) -> Result<(), Error<E>> {
// Setup configuration registers before writing single tone register
self.set_configurations(&mut [
(DDSCFRMask::RAM_ENABLE, 0),
(DDSCFRMask::DIGITAL_RAMP_ENABLE, 0),
(DDSCFRMask::OSK_ENABLE, 0),
(DDSCFRMask::PARALLEL_DATA_PORT_ENABLE, 0),
])?;
self.set_configurations(&mut [
(DDSCFRMask::EN_AMP_SCALE_SINGLE_TONE_PRO, 1),
])?;
self.enable_single_tone_configuration()?;
// Calculate phase offset work (POW)
let phase_res: u64 = 1 << 16;
@ -446,15 +442,7 @@ where
pub fn set_single_tone_profile_amplitude(&mut self, profile: u8, amp_scale_factor: f64) -> Result<(), Error<E>> {
// Setup configuration registers before writing single tone register
self.set_configurations(&mut [
(DDSCFRMask::RAM_ENABLE, 0),
(DDSCFRMask::DIGITAL_RAMP_ENABLE, 0),
(DDSCFRMask::OSK_ENABLE, 0),
(DDSCFRMask::PARALLEL_DATA_PORT_ENABLE, 0),
])?;
self.set_configurations(&mut [
(DDSCFRMask::EN_AMP_SCALE_SINGLE_TONE_PRO, 1),
])?;
self.enable_single_tone_configuration()?;
// Calculate amplitude_scale_factor (ASF)
let amp_res: u64 = 1 << 14;
@ -476,6 +464,96 @@ where
self.write_register(0x0E + profile, &mut register)
}
// Helper function to switch into single tone mode
// Need to setup configuration registers before writing single tone register
fn enable_single_tone_configuration(&mut self) -> Result<(), Error<E>> {
self.set_configurations(&mut [
(DDSCFRMask::RAM_ENABLE, 0),
(DDSCFRMask::DIGITAL_RAMP_ENABLE, 0),
(DDSCFRMask::OSK_ENABLE, 0),
(DDSCFRMask::PARALLEL_DATA_PORT_ENABLE, 0),
])?;
self.set_configurations(&mut [
(DDSCFRMask::EN_AMP_SCALE_SINGLE_TONE_PRO, 1),
])
}
// Helper function to switch into RAM mode
// Need to setup configuration registers before writing into RAM profile register
fn enable_ram_configuration(&mut self, ram_dst: RAMDestination) -> Result<(), Error<E>> {
self.set_configurations(&mut [
(DDSCFRMask::RAM_ENABLE, 1),
(DDSCFRMask::RAM_PLAYBACK_DST, ram_dst as u32),
])
}
// Helper function to switch out of RAM mode
// Need to setup configuration registers before writing into RAM profile register
fn disable_ram_configuration(&mut self) -> Result<(), Error<E>> {
self.set_configurations(&mut [
(DDSCFRMask::RAM_ENABLE, 0),
])
}
/*
* Configure a RAM mode profile
*
*/
pub fn set_ram_profile(&mut self, profile: u8, start_addr: u16, end_addr: u16,
ram_dst: RAMDestination, no_dwell_high: bool, zero_crossing: bool,
op_mode: RAMOperationMode, playback_rate: f64, data: &[u8]
) -> Result<(), Error<E>> {
// Check the legality of this setup
assert!(profile < 7);
assert!(end_addr >= start_addr);
assert!(end_addr < 1024);
assert_eq!(data.len() as u16, (end_addr - start_addr + 1) * 4);
// Calculate address step rate, and check legality
let step_rate = (self.f_sys_clk/(4.0 * playback_rate)) as u64;
if (step_rate == 0 || step_rate > 0xFFFF) {
return Err(Error::ParameterError);
}
// Before setting up RAM, disable RAM_ENABLE
self.enable_ram_configuration(ram_dst.clone())?;
// Write a RAM profile, but include all data in RAM
self.write_register(0x0E + profile, &mut [
0x00,
((step_rate >> 8) & 0xFF).try_into().unwrap(),
((step_rate >> 0) & 0xFF).try_into().unwrap(),
((end_addr >> 2) & 0xFF).try_into().unwrap(),
((end_addr & 0x3) << 6).try_into().unwrap(),
((start_addr >> 2) & 0xFF).try_into().unwrap(),
((start_addr & 0x3) << 6).try_into().unwrap(),
((no_dwell_high as u8) << 5) | ((zero_crossing as u8) << 3) | (op_mode as u8)
])?;
// Temporarily disable RAM mode while accessing into RAM
self.disable_ram_configuration();
self.write_ram(data)?;
// Properly configure start_addr and end_addr
self.enable_ram_configuration(ram_dst)
}
// Helper function to write data in RAM
// Need address range for data size check
fn write_ram(&mut self, data: &[u8]) -> Result<(), Error<E>> {
let mut vec: ArrayVec<[u8; 8192]> = ArrayVec::new();
vec.try_push(0x16)
.map_err(|_| Error::DDSRAMError)?;
vec.try_extend_from_slice(data)
.map_err(|_| Error::DDSRAMError)?;
let mut data_slice = vec.as_mut_slice();
self.spi.transfer(&mut data_slice)
.map(|_| ())
.map_err(Error::SPI)
}
/*
* Test method for DDS.
@ -497,9 +575,9 @@ where
}
Ok(error_count)
}
}
// Strong check for bytes passed to a register
macro_rules! impl_register_io {
($($reg_addr: expr, $reg_byte_size: expr),+) => {
impl<SPI, E> DDS<SPI>
@ -572,6 +650,4 @@ impl_register_io!(
0x13, 8,
0x14, 8,
0x15, 8
// RAM works in other way
// 0x16, 4
);

View File

@ -1,17 +1,9 @@
use embedded_hal::{
digital::v2::{OutputPin, InputPin},
blocking::spi::Transfer,
blocking::delay::{DelayMs, DelayUs},
blocking::delay::DelayUs,
};
use cortex_m;
use cortex_m::asm::nop;
use cortex_m_rt::entry;
use core::ptr;
use nb::block;
use log::{warn, debug};
use log::info;
#[derive(Debug)]
pub enum FPGAFlashError {
@ -91,10 +83,10 @@ pub fn flash_ice40_fpga<SPI: Transfer<u8>,
_ => return Err(FPGAFlashError::ResetStatusError),
};
debug!("Configuration successful!");
info!("Configuration successful!");
// Send at least another 49 clock cycles to activate IO pins (choosing same 13 bytes)
spi.transfer(&mut dummy_13_bytes).map_err(|_| FPGAFlashError::SPICommunicationError)?;
debug!("User I/O pins activated.");
info!("User I/O pins activated.");
Ok(())
}

View File

@ -1,29 +1,14 @@
#![no_std]
#![feature(generic_associated_types)]
#![feature(str_strip)]
extern crate embedded_hal;
use embedded_hal::{
digital::v2::OutputPin,
blocking::spi::Transfer,
};
use core::{
cell,
marker::PhantomData,
};
use cortex_m;
#[macro_use]
pub mod bitmask_macro;
pub mod spi_slave;
use crate::spi_slave::{
Parts,
SPISlave,
};
pub mod cpld;
use crate::cpld::CPLD;
use crate::cpld::DoOnGetRefMutData;
pub mod config_register;
use crate::config_register::ConfigRegister;
@ -36,11 +21,12 @@ use crate::attenuator::Attenuator;
pub mod dds;
use crate::dds::DDS;
pub mod scpi;
// pub mod scpi;
pub mod translation;
pub mod nal_tcp_client;
pub mod flash;
pub mod mqtt_mux;
/*
* Enum for structuring error
@ -55,7 +41,9 @@ pub enum Error<E> {
DDSError,
ConfigRegisterError,
DDSCLKError,
DDSRAMError,
ParameterError,
MqttCommandError,
}
#[derive(Debug)]
@ -156,28 +144,12 @@ where
}
}
pub trait UrukulTraits {
type Error;
fn get_channel_switch_status(&mut self, channel: u32) -> Result<bool, Self::Error>;
fn set_channel_switch(&mut self, channel: u32, status: bool) -> Result<(), Self::Error>;
fn set_clock_source(&mut self, source: ClockSource, frequency: f64) -> 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_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_single_tone_profile_frequency(&mut self, channel: u8, profile: u8, frequency: f64)-> Result<(), Self::Error>;
fn set_channel_single_tone_profile_phase(&mut self, channel: u8, profile: u8, phase: f64)-> Result<(), Self::Error>;
fn set_channel_single_tone_profile_amplitude(&mut self, channel: u8, profile: u8, 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> Urukul<SPI>
where
SPI: Transfer<u8, Error = E>
{
type Error = Error<E>;
fn get_channel_switch_status(&mut self, channel: u32) -> Result<bool, Self::Error> {
fn get_channel_switch_status(&mut self, channel: u32) -> Result<bool, Error<E>> {
if channel < 4 {
self.config_register.get_status(StatusMask::RF_SW).map(|val| (val & (1 << channel)) != 0)
} else {
@ -185,7 +157,7 @@ where
}
}
fn set_channel_switch(&mut self, channel: u32, status: bool) -> Result<(), Self::Error> {
fn set_channel_switch(&mut self, channel: u32, status: bool) -> Result<(), Error<E>> {
if channel < 4 {
let prev = u32::from(self.config_register.get_status(StatusMask::RF_SW)?);
let next = {
@ -203,7 +175,7 @@ where
}
}
fn set_clock_source(&mut self, source: ClockSource, frequency: f64) -> Result<(), Self::Error> {
fn set_clock_source(&mut self, source: ClockSource, frequency: f64) -> Result<(), Error<E>> {
// Change clock source through configuration register
match source {
ClockSource::OSC => self.config_register.set_configurations(&mut [
@ -232,7 +204,7 @@ where
Ok(())
}
fn set_clock_division(&mut self, division: u8) -> Result<(), Self::Error> {
fn set_clock_division(&mut self, division: u8) -> Result<(), Error<E>> {
match division {
1 => self.config_register.set_configurations(&mut [
(CFGMask::DIV, 1),
@ -256,33 +228,33 @@ where
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<(), Error<E>> {
self.attenuator.set_channel_attenuation(channel, attenuation)
}
fn set_profile(&mut self, profile: u8) -> Result<(), Self::Error> {
fn set_profile(&mut self, profile: u8) -> Result<(), Error<E>> {
self.config_register.set_configurations(&mut [
(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> {
fn set_channel_single_tone_profile(&mut self, channel: u8, profile: u8, frequency: f64, phase: f64, amplitude: f64) -> Result<(), Error<E>> {
self.dds[usize::from(channel)].set_single_tone_profile(profile, frequency, phase, amplitude)
}
fn set_channel_single_tone_profile_frequency(&mut self, channel: u8, profile: u8, frequency: f64)-> Result<(), Self::Error> {
fn set_channel_single_tone_profile_frequency(&mut self, channel: u8, profile: u8, frequency: f64)-> Result<(), Error<E>> {
self.dds[usize::from(channel)].set_single_tone_profile_frequency(profile, frequency)
}
fn set_channel_single_tone_profile_phase(&mut self, channel: u8, profile: u8, phase: f64)-> Result<(), Self::Error> {
fn set_channel_single_tone_profile_phase(&mut self, channel: u8, profile: u8, phase: f64)-> Result<(), Error<E>> {
self.dds[usize::from(channel)].set_single_tone_profile_phase(profile, phase)
}
fn set_channel_single_tone_profile_amplitude(&mut self, channel: u8, profile: u8, amplitude: f64)-> Result<(), Self::Error> {
fn set_channel_single_tone_profile_amplitude(&mut self, channel: u8, profile: u8, amplitude: f64)-> Result<(), Error<E>> {
self.dds[usize::from(channel)].set_single_tone_profile_amplitude(profile, amplitude)
}
fn set_channel_sys_clk(&mut self, channel: u8, f_sys_clk: f64) -> Result<(), Self::Error> {
fn set_channel_sys_clk(&mut self, channel: u8, f_sys_clk: f64) -> Result<(), Error<E>> {
self.dds[usize::from(channel)].set_sys_clk_frequency(f_sys_clk)
}
}

197
src/mqtt_mux.rs Normal file
View File

@ -0,0 +1,197 @@
use log::info;
use embedded_hal::blocking::spi::Transfer;
use core::convert::TryInto;
use crate::ClockSource as UrukulClockSource;
use crate::ClockSource::*;
use crate::Urukul;
use crate::Error;
#[derive(Debug)]
pub enum MqttCommandType {
// Urukul/Control/Clock/Source
ClockSource(UrukulClockSource),
// Urukul/Control/Clock/Division
ClockDivision(u8),
// Urukul/Control/ChannelX/Switch
Switch(u8, bool),
// Urukul/Control/ChannelX/Attenuation
Attenuation(u8, f32),
// Urukul/Control/ChannelX/SystemClock
SystemClock(u8, f64),
// Urukul/Control/ChannelX/ProfileY/Frequency
SingleToneFrequency(u8, u8, f64),
// Urukul/Control/ChannelX/ProfileY/Amplitude
SingleToneAmplitude(u8, u8, f64),
// Urukul/Control/ChannelX/ProfileY/Phase
SingleTonePhase(u8, u8, f64),
}
use crate::mqtt_mux::MqttCommandType::*;
pub struct MqttMux<SPI> {
urukul: Urukul<SPI>
}
impl<SPI, E> MqttMux<SPI> where SPI: Transfer<u8, Error = E> {
pub fn new(urukul: Urukul<SPI>) -> Self {
MqttMux {
urukul
}
}
pub fn handle_command(&mut self, topic: &str, message: &[u8]) -> Result<(), Error<E>> {
let command = self.parse(topic, message)?;
self.execute(command)
}
// MQTT command are not case tolerant
// If the command differs by case, space or delimiter, it is a wrong command
// A starting forward slash ("/") is acceptable, as per MQTT standard
// Topic should contain the appropriate command header
// Message should provide the parameter
fn parse(&mut self, topic: &str, message: &[u8]) -> Result<MqttCommandType, Error<E>> {
let mut assigned_channel = false;
let mut assigned_profile = false;
let mut channel :u8 = 0;
let mut profile :u8 = 0;
// Verify that the topic must start with Urukul/Control/ or /Urukul/Control/
let mut header = topic.strip_prefix("/Urukul/Control/")
.or_else(|| topic.strip_prefix("Urukul/Control/"))
.ok_or(Error::MqttCommandError)?;
loop {
match header {
// The topic has a channel subtopic
_ if header.starts_with("Channel") => {
// MQTT command should only mention channel once appropriately
// Channel must be referred before profile,
// as a channel is broader than a profile
if assigned_channel || assigned_profile {
return Err(Error::MqttCommandError);
}
// Remove the "Channel" part of the subtopic
header = header.strip_prefix("Channel")
.ok_or(Error::MqttCommandError)?;
// Remove the channel number at the end of the subtopic
// But store the channel as a char, so it can be removed easily
let numeric_char :char = header.chars()
.next()
.ok_or(Error::MqttCommandError)?;
// Record the channel number
channel = numeric_char.to_digit(10)
.ok_or(Error::MqttCommandError)?
.try_into()
.unwrap();
assigned_channel = true;
header = header.strip_prefix(numeric_char)
.ok_or(Error::MqttCommandError)?;
// Remove forward slash ("/")
header = header.strip_prefix("/")
.ok_or(Error::MqttCommandError)?;
},
_ if header.starts_with("Profile") => {
// MQTT command should only mention profile once appropriately
if assigned_profile {
return Err(Error::MqttCommandError);
}
// Remove the "Profile" part of the subtopic
header = header.strip_prefix("Profile")
.ok_or(Error::MqttCommandError)?;
// Remove the profile number at the end of the subtopic
// But store the profile as a char, so it can be removed easily
let numeric_char :char = header.chars()
.next()
.ok_or(Error::MqttCommandError)?;
// Record the channel number
profile = numeric_char.to_digit(10)
.ok_or(Error::MqttCommandError)?
.try_into()
.unwrap();
assigned_profile = true;
header = header.strip_prefix(numeric_char)
.ok_or(Error::MqttCommandError)?;
// Remove forward slash ("/")
header = header.strip_prefix("/")
.ok_or(Error::MqttCommandError)?;
},
"Clock/Source" => {
// Clock/Source refers to the Urukul clock source
// It should be common for all channels and profiles
if assigned_channel || assigned_profile {
return Err(Error::MqttCommandError);
}
let source_string = core::str::from_utf8(message).unwrap();
return match source_string {
_ if source_string.eq_ignore_ascii_case("OSC") => {
Ok(ClockSource(OSC))
},
_ if source_string.eq_ignore_ascii_case("SMA") => {
Ok(ClockSource(SMA))
},
_ if source_string.eq_ignore_ascii_case("MMCX") => {
Ok(ClockSource(MMCX))
},
_ => Err(Error::MqttCommandError),
};
}
"Clock/Division" => {
// Clock/Division refers to the Urukul clock division
// It should be common for all channels and profiles
if assigned_channel || assigned_profile {
return Err(Error::MqttCommandError);
}
let division = u8::from_str_radix(core::str::from_utf8(message).unwrap(), 10)
.map_or_else(
|_| Err(Error::MqttCommandError),
|div| if div == 1 || div == 2 || div == 4 {
Ok(div)
} else {
Err(Error::MqttCommandError)
})?;
return Ok(ClockDivision(division));
}
"Switch" => {
// Switch is a channel specific topic
if !(assigned_channel && !assigned_profile) {
return Err(Error::MqttCommandError);
}
let switch_string = core::str::from_utf8(message).unwrap();
return match switch_string {
_ if switch_string.eq_ignore_ascii_case("on") => {
Ok(Switch(channel, true))
},
_ if switch_string.eq_ignore_ascii_case("off") => {
Ok(Switch(channel, false))
},
_ => Err(Error::MqttCommandError),
};
},
// TODO: Cover all commands
_ => return Err(Error::MqttCommandError),
};
}
}
// TODO: Implement this
// Only need to sort the command enum
// Obviously. This is what a MUX does
fn execute(&mut self, command_type: MqttCommandType) -> Result<(), Error<E>> {
info!("{:?}", command_type);
match command_type {
Switch(channel, status) => self.urukul.set_channel_switch(channel as u32, status),
_ => Ok(())
}
}
}

View File

@ -2,8 +2,6 @@ use embedded_hal::{
blocking::spi::Transfer,
digital::v2::OutputPin,
};
use core::marker::PhantomData;
use crate::cpld::CPLD;
use crate::Error;