Compare commits
10 Commits
e834e6fcea
...
25f8363e54
Author | SHA1 | Date | |
---|---|---|---|
25f8363e54 | |||
a5dbfdaf4f | |||
a7073c419b | |||
72770d9276 | |||
74f7b7ce61 | |||
3758029a52 | |||
54e91c49e0 | |||
e174961bff | |||
537a375f2c | |||
c084da1848 |
13
Cargo.toml
13
Cargo.toml
@ -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
9
Makefile
Normal 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
|
||||
|
@ -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(),
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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))
|
||||
|
@ -2,4 +2,5 @@ init
|
||||
reset init
|
||||
halt
|
||||
flash write_image erase target/thumbv7em-none-eabihf/debug/examples/ethernet
|
||||
reset run
|
||||
shutdown
|
@ -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
6
openocd/mqtt-client.cfg
Normal file
@ -0,0 +1,6 @@
|
||||
init
|
||||
reset init
|
||||
halt
|
||||
flash write_image erase target/thumbv7em-none-eabihf/debug/examples/mqtt_client
|
||||
reset run
|
||||
shutdown
|
@ -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" ''
|
||||
|
@ -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());
|
||||
|
158
src/dds.rs
158
src/dds.rs
@ -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
|
||||
);
|
||||
|
16
src/flash.rs
16
src/flash.rs
@ -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(())
|
||||
|
||||
}
|
64
src/lib.rs
64
src/lib.rs
@ -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
197
src/mqtt_mux.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
}
|
@ -2,8 +2,6 @@ use embedded_hal::{
|
||||
blocking::spi::Transfer,
|
||||
digital::v2::OutputPin,
|
||||
};
|
||||
use core::marker::PhantomData;
|
||||
|
||||
use crate::cpld::CPLD;
|
||||
use crate::Error;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user