Compare commits

...

19 Commits

18 changed files with 701 additions and 1338 deletions

344
Cargo.lock generated
View File

@ -9,18 +9,6 @@ dependencies = [
"as-slice",
]
[[package]]
name = "arraydeque"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0ffd3d69bd89910509a5d31d1f1353f38ccffdd116dd0099bbd6627f7bd8ad8"
[[package]]
name = "arrayvec"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8"
[[package]]
name = "as-slice"
version = "0.1.3"
@ -65,35 +53,12 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "built"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fa7899958f4aa3c40edc1b033d0e956763319e398924abb80a0034dda5bb198"
dependencies = [
"cargo-lock",
"git2",
"semver 0.10.0",
]
[[package]]
name = "byteorder"
version = "1.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
[[package]]
name = "cargo-lock"
version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8504b63dd1249fd1745b7b4ef9b6f7b107ddeb3c95370043c7dbcc38653a2679"
dependencies = [
"semver 0.9.0",
"serde",
"toml",
"url",
]
[[package]]
name = "cast"
version = "0.2.3"
@ -103,15 +68,6 @@ dependencies = [
"rustc_version",
]
[[package]]
name = "cc"
version = "1.0.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66120af515773fb005778dc07c261bd201ec8ce50bd6e7144c927753fe013381"
dependencies = [
"jobserver",
]
[[package]]
name = "cfg-if"
version = "0.1.10"
@ -228,31 +184,6 @@ dependencies = [
"syn",
]
[[package]]
name = "firmware"
version = "0.1.0"
dependencies = [
"arrayvec",
"cortex-m",
"cortex-m-log",
"cortex-m-rt",
"cortex-m-rtic",
"embedded-hal",
"embedded-nal",
"heapless",
"lazy_static",
"log",
"minimq",
"nb 1.0.0",
"nom",
"panic-halt",
"panic-itm",
"scpi",
"smoltcp",
"stm32h7xx-hal",
"uom 0.29.0",
]
[[package]]
name = "generic-array"
version = "0.12.3"
@ -281,19 +212,6 @@ dependencies = [
"version_check",
]
[[package]]
name = "git2"
version = "0.13.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86d97249f21e9542caeee9f8e1d150905cd875bf723f5ff771bdb4852eb83a24"
dependencies = [
"bitflags",
"libc",
"libgit2-sys",
"log",
"url",
]
[[package]]
name = "hash32"
version = "0.1.1"
@ -311,9 +229,9 @@ checksum = "00d63df3d41950fb462ed38308eea019113ad1508da725bbedcd0fa5a85ef5f7"
[[package]]
name = "heapless"
version = "0.5.5"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73a8a2391a3bc70b31f60e7a90daa5755a360559c0b6b9c5cfc0fee482362dc0"
checksum = "74911a68a1658cfcfb61bc0ccfbd536e3b6e906f8c2f7883ee50157e3e2184f1"
dependencies = [
"as-slice",
"generic-array 0.13.2",
@ -322,14 +240,25 @@ dependencies = [
]
[[package]]
name = "idna"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9"
name = "humpback-dds"
version = "0.1.0"
dependencies = [
"matches",
"unicode-bidi",
"unicode-normalization",
"cortex-m",
"cortex-m-log",
"cortex-m-rt",
"cortex-m-rtic",
"embedded-hal",
"embedded-nal",
"heapless",
"lazy_static",
"log",
"minimq",
"nb 1.0.0",
"nom",
"panic-halt",
"panic-itm",
"smoltcp",
"stm32h7xx-hal",
]
[[package]]
@ -342,15 +271,6 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "jobserver"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2"
dependencies = [
"libc",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
@ -360,52 +280,6 @@ dependencies = [
"spin",
]
[[package]]
name = "lexical-core"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db65c6da02e61f55dae90a0ae427b2a5f6b3e8db09f58d10efab23af92592616"
dependencies = [
"bitflags",
"cfg-if",
]
[[package]]
name = "libc"
version = "0.2.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "755456fae044e6fa1ebbbd1b3e902ae19e73097ed4ed87bb79934a867c007bc3"
[[package]]
name = "libgit2-sys"
version = "0.12.12+1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0100ae90655025134424939f1f60e27e879460d451dff6afedde4f8226cbebfc"
dependencies = [
"cc",
"libc",
"libz-sys",
"pkg-config",
]
[[package]]
name = "libm"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a"
[[package]]
name = "libz-sys"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af67924b8dd885cccea261866c8ce5b74d239d272e154053ff927dae839f5ae9"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "log"
version = "0.4.11"
@ -421,12 +295,6 @@ version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c75de51135344a4f8ed3cfe2720dc27736f7711989703a0b43aadf3753c55577"
[[package]]
name = "matches"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
[[package]]
name = "memchr"
version = "2.3.3"
@ -476,36 +344,6 @@ dependencies = [
"version_check",
]
[[package]]
name = "num-integer"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611"
dependencies = [
"autocfg",
]
[[package]]
name = "panic-halt"
version = "0.2.0"
@ -540,18 +378,6 @@ dependencies = [
"proc-macro-hack",
]
[[package]]
name = "percent-encoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pkg-config"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33"
[[package]]
name = "proc-macro-hack"
version = "0.5.18"
@ -605,31 +431,7 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
dependencies = [
"semver 0.9.0",
]
[[package]]
name = "scpi"
version = "0.3.4"
source = "git+https://github.com/occheung/scpi-rs?branch=issue-4#982099ea490bfef70455e995700411746c55ade0"
dependencies = [
"arraydeque",
"arrayvec",
"built",
"lexical-core",
"libm",
"scpi_derive",
"uom 0.28.0",
]
[[package]]
name = "scpi_derive"
version = "0.3.4"
source = "git+https://github.com/occheung/scpi-rs?branch=issue-4#982099ea490bfef70455e995700411746c55ade0"
dependencies = [
"proc-macro2",
"quote",
"syn",
"semver",
]
[[package]]
@ -637,16 +439,6 @@ name = "semver"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
dependencies = [
"semver-parser",
"serde",
]
[[package]]
name = "semver"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "394cec28fa623e00903caf7ba4fa6fb9a0e260280bb8cdbbba029611108a0190"
dependencies = [
"semver-parser",
]
@ -657,31 +449,10 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "serde"
version = "1.0.115"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e54c9a88f2da7238af84b5101443f0c0d0a3bbdc455e34a5c9497b1903ed55d5"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.115"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "609feed1d0a73cc36a0182a840a9b37b4a82f0b1150369f0536a9e3f2a31dc48"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "smoltcp"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fe46639fd2ec79eadf8fe719f237a7a0bd4dac5d957f1ca5bbdbc1c3c39e53a"
source = "git+https://github.com/smoltcp-rs/smoltcp.git#bdfa44270e9c59b3095b555cdf14601f7dc27794"
dependencies = [
"bitflags",
"byteorder",
@ -742,95 +513,24 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "tinyvec"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "238ce071d267c5710f9d31451efec16c5ee22de34df17cc05e56cbc92e967117"
[[package]]
name = "toml"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a"
dependencies = [
"serde",
]
[[package]]
name = "typenum"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33"
[[package]]
name = "unicode-bidi"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
dependencies = [
"matches",
]
[[package]]
name = "unicode-normalization"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fb19cf769fa8c6a80a162df694621ebeb4dafb606470b2b2fce0be40a98a977"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-xid"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]]
name = "uom"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "627142a1043c2d460613232ce4f7e322e756636e000c0f1d1f2e779cb431358a"
dependencies = [
"num-rational",
"num-traits",
"typenum",
]
[[package]]
name = "uom"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bb593f5252356bfb829112f8fca2d0982d48588d2d6bb5a92553b0dfc4c9aba"
dependencies = [
"num-traits",
"typenum",
]
[[package]]
name = "url"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb"
dependencies = [
"idna",
"matches",
"percent-encoding",
]
[[package]]
name = "vcell"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "876e32dcadfe563a4289e994f7cb391197f362b6315dc45e8ba4aa6f564a4b3c"
[[package]]
name = "vcpkg"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c"
[[package]]
name = "version_check"
version = "0.9.2"

View File

@ -2,7 +2,7 @@
authors = ["occheung"]
edition = "2018"
readme = "README.md"
name = "firmware"
name = "humpback-dds"
version = "0.1.0"
[dependencies]
@ -16,8 +16,7 @@ nb = "1.0.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" ] }
heapless = "0.5.6"
nom = { version = "5.1.2", default-features = false, features = [] }
# Logging and Panicking
@ -27,46 +26,8 @@ cortex-m-log = { version = "0.6.2", features = [ "itm", "log-integration" ] }
log = {version = "0.4.11"}
lazy_static = { version = "1.4.0", features = ["spin_no_std"] }
[dependencies.scpi]
git = "https://github.com/occheung/scpi-rs"
branch = "issue-4"
default-features = false
features = [ "build-info", "unit-frequency", "unit-angle" ]
[dependencies.uom]
version = "0.29.0"
default-features = false
features = [ "autoconvert", "f32", "f64", "si" ]
# Use below SCPI dependency when need to modify SCPI fork offline
# [dependencies.scpi]
# path = "../scpi-fork/scpi"
# default-features = false
# features = [ "build-info", "unit-frequency" ]
[[example]]
name = "ethernet"
[[example]]
name = "fpga_config"
[[example]]
name = "tcp_client"
[[example]]
name = "mqtt_client"
[[example]]
name = "mqtt_hello_world"
# Uncomment for the allocator example.
# alloc-cortex-m = "0.3.5"
# this lets you use `cargo fix`!
[[bin]]
name = "firmware"
test = false
bench = false
[patch.crates-io]
smoltcp = { git = "https://github.com/smoltcp-rs/smoltcp.git" }
[profile.release]
codegen-units = 1 # better optimizations

10
build.rs Normal file
View File

@ -0,0 +1,10 @@
use std::process::Command;
fn main() {
Command::new("python3")
.arg("migen/fpga_config.py")
.spawn()
.expect("FPGA bitstream file cannot be built!");
println!("cargo:rerun-if-changed=migen/fpga_config.py")
}

View File

@ -222,9 +222,10 @@ fn main() -> ! {
client.network_stack.update(time);
}
// Process MQTT messages about Urukul/Control
let connection = client
.poll(|_client, topic, message, _properties| {
info!("On '{:?}', received: {:?}", topic, message);
info!("On {:?}, received: {:?}", topic, message);
// Why is topic a string while message is a slice?
mqtt_mux.process_mqtt(topic, message);
}).is_ok();

View File

@ -10,6 +10,9 @@ from migen.genlib.io import *
class UrukulConnector(Module):
def __init__(self, platform):
# Include extension
spi_mosi = [
("spi_mosi", 0, Pins("B16"), IOStandard("LVCMOS33"))
]
spi_cs = [
("spi_cs", 0, Pins("B13 B14 B15"), IOStandard("LVCMOS33"))
]
@ -20,6 +23,7 @@ class UrukulConnector(Module):
# Add extensions
platform.add_extension(spi_cs)
platform.add_extension(io_update)
platform.add_extension(spi_mosi)
# Request EEM I/O & SPI
eem0 = [
@ -34,12 +38,13 @@ class UrukulConnector(Module):
platform.request("eem0", 6)
]
spi = platform.request("spi")
spi_mosi = platform.request("spi_mosi")
spi_cs = platform.request("spi_cs")
led = platform.request("user_led")
io_update = platform.request("io_update")
assert len(spi.clk) == 1
assert len(spi.mosi) == 1
assert len(spi_mosi) == 1
assert len(spi.miso) == 1
assert len(spi_cs) == 3
assert len(io_update) == 1
@ -61,8 +66,8 @@ class UrukulConnector(Module):
eem0[0].p.eq(spi.clk),
eem0[0].n.eq(~spi.clk),
eem0[1].p.eq(spi.mosi),
eem0[1].n.eq(~spi.mosi),
eem0[1].p.eq(spi_mosi),
eem0[1].n.eq(~spi_mosi),
spi.miso.eq(~self.miso_n),

View File

@ -10,7 +10,7 @@ let
runOpenOcd = writeShellScriptBin "run-openocd" ''
openocd \
-f board/st_nucleo_h743zi.cfg \
-f openocd/openocd.cfg \
-c init &
sleep 1
'';
@ -49,12 +49,6 @@ let
&& flash-fpga-config
'';
verifyFPGAConfig = writeShellScriptBin "verify-fpga-config" ''
gdb -x gdb_config/fpga_verify.gdb
diff build/top.bin mem.bin
rm mem.bin
'';
resetFlash = writeShellScriptBin "reset-flash" ''
gdb -batch -x gdb_config/reset.gdb
echo "Reset is complete, please reset the openocd server."
@ -64,6 +58,10 @@ let
openocd -f openocd/openocd.cfg -f openocd/$1.cfg
'';
publishMqtt = writeShellScriptBin "publish-mqtt" ''
mosquitto_pub -h localhost -t $1 -m "$2" -d
'';
in
stdenv.mkDerivation {
name = "nix-shell";
@ -85,8 +83,8 @@ in
compileMigenScript
flashFPGAConfig
configureFPGA
verifyFPGAConfig
resetFlash
openocdFlash
publishMqtt
];
}

View File

@ -1,7 +1,7 @@
use embedded_hal::blocking::spi::Transfer;
use core::assert;
use crate::Error;
use crate::urukul::Error;
pub struct Attenuator<SPI> {
spi: SPI,

View File

@ -1,5 +1,5 @@
use embedded_hal::blocking::spi::Transfer;
use crate::Error;
use crate::urukul::Error;
use core::mem::size_of;
// Bitmasks for CFG
@ -75,7 +75,6 @@ where
/*
* Return selected configuration field
* TODO: Return result type instead for error checking
*/
pub fn get_configuration(&mut self, config_type: CFGMask) -> u8 {
config_type.get_filtered_content(self.data) as u8

View File

@ -1,4 +1,4 @@
use crate::Error;
use crate::urukul::Error;
use crate::spi_slave::Parts;
use embedded_hal::{

View File

@ -1,8 +1,9 @@
use embedded_hal::blocking::spi::Transfer;
use crate::Error;
use crate::urukul::Error;
use core::mem::size_of;
use core::convert::TryInto;
use arrayvec::ArrayVec;
use heapless::Vec;
use heapless::consts::*;
/*
* Bitmask for all configurations (Order: CFR3, CFR2, CFR1)
@ -64,6 +65,8 @@ construct_bitmask!(DDSCFRMask; u32;
const WRITE_MASK :u8 = 0x00;
const READ_MASK :u8 = 0x80;
static mut RAM_VEC: Vec<u8, U8192> = Vec(heapless::i::Vec::new());
#[derive(Clone, PartialEq)]
pub enum RAMDestination {
Frequency = 0,
@ -483,94 +486,167 @@ where
}
/*
* Configure a RAM mode profile, but with RAM data generated by a closure
* Configure a RAM mode profile, wrt supplied frequency data
* This will setup the static RAM_VEC by converting frequency to ftw
*/
pub fn set_ram_profile_with_closure<F>(&mut self, profile: u8, start_addr: u16,
ram_dst: RAMDestination, no_dwell_high: bool, zero_crossing: bool,
op_mode: RAMOperationMode, playback_rate: f64, f: F) -> Result<(), Error<E>>
where
F: FnOnce() -> ArrayVec::<[f64; 2048]>
{
// Check the legality of the profile setup
assert!(profile < 7);
assert!(start_addr < 1024);
let mut vec = f();
if (ram_dst != RAMDestination::Polar && ((vec.len() as u16) + start_addr) < 1024) ||
((((vec.len()/2) as u16) + start_addr) < 1024) {
return Err(Error::DDSRAMError);
}
// TODO: Convert argument into bytes for RAM
let mut byte_vec: ArrayVec<[u8; 8192]> = ArrayVec::new();
match ram_dst {
RAMDestination::Frequency => {
for freq in vec.into_iter() {
let ftw = self.frequency_to_ftw(freq);
byte_vec.push(((ftw >> 24) & 0xFF) as u8);
byte_vec.push(((ftw >> 16) & 0xFF) as u8);
byte_vec.push(((ftw >> 8) & 0xFF) as u8);
byte_vec.push(((ftw >> 0) & 0xFF) as u8);
}
}
RAMDestination::Phase => {
for deg in vec.into_iter() {
let pow = self.degree_to_pow(deg);
byte_vec.push(((pow >> 8) & 0xFF) as u8);
byte_vec.push(((pow >> 0) & 0xFF) as u8);
byte_vec.push(0);
byte_vec.push(0);
}
}
RAMDestination::Amplitude => {
for amp in vec.into_iter() {
let asf = self.amplitude_to_asf(amp);
byte_vec.push(((asf >> 8) & 0xFF) as u8);
byte_vec.push(((asf << 2) & 0xFC) as u8);
byte_vec.push(0);
byte_vec.push(0);
}
}
RAMDestination::Polar => {
// Alternate phase and amplitude
let mut phase = true;
for pol in vec.into_iter() {
if phase {
let pow = self.degree_to_pow(pol);
byte_vec.push(((pow >> 8) & 0xFF) as u8);
byte_vec.push(((pow >> 0) & 0xFF) as u8);
phase = false;
} else {
let asf = self.amplitude_to_asf(pol);
byte_vec.push(((asf >> 8) & 0xFF) as u8);
byte_vec.push(((asf << 2) & 0xFC) as u8);
phase = true;
}
}
if phase {
return Err(Error::DDSRAMError);
}
}
}
let data = byte_vec.as_slice();
self.set_ram_profile(profile, start_addr, start_addr + (((data.len()/4) - 1) as u16),
ram_dst, no_dwell_high, zero_crossing, op_mode, playback_rate, data)
}
/*
* Configure a RAM mode profile
* TODO: Possibly remove redundant end_addr parameter.
* This can be inferred by start_addr and data size.
*/
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]
pub unsafe fn set_frequency_ram_profile(&mut self, profile: u8, start_addr: u16, end_addr: u16,
no_dwell_high: bool, zero_crossing: bool, op_mode: RAMOperationMode, playback_rate: f64,
frequency_data: &[f64]
) -> Result<(), Error<E>> {
// Check the legality of the profile setup
assert!(profile < 7);
assert!(profile <= 7);
assert!(end_addr >= start_addr);
assert!(end_addr < 1024);
assert_eq!(data.len() as u16, (end_addr - start_addr + 1) * 4);
assert_eq!(frequency_data.len() as u16, end_addr - start_addr + 1);
// Clear RAM vector, and add address byte
RAM_VEC.clear();
RAM_VEC.push(0x16)
.map_err(|_| Error::DDSRAMError)?;
// Convert frequency data into bytes recognized by DDS
for freq in frequency_data.iter() {
let ftw = self.frequency_to_ftw(*freq);
RAM_VEC.push(((ftw >> 24) & 0xFF) as u8)
.map_err(|_| Error::DDSRAMError)?;
RAM_VEC.push(((ftw >> 16) & 0xFF) as u8)
.map_err(|_| Error::DDSRAMError)?;
RAM_VEC.push(((ftw >> 8) & 0xFF) as u8)
.map_err(|_| Error::DDSRAMError)?;
RAM_VEC.push(((ftw >> 0) & 0xFF) as u8)
.map_err(|_| Error::DDSRAMError)?;
}
self.set_ram_profile(profile, start_addr, end_addr, RAMDestination::Frequency,
no_dwell_high, zero_crossing, op_mode, playback_rate)
}
/*
* Configure a RAM mode profile, wrt supplied amplitude data
* This will setup the static RAM_VEC by converting amplitude to asf
*/
pub unsafe fn set_amplitude_ram_profile(&mut self, profile: u8, start_addr: u16, end_addr: u16,
no_dwell_high: bool, zero_crossing: bool, op_mode: RAMOperationMode, playback_rate: f64,
amplitude_data: &[f64]
) -> Result<(), Error<E>> {
// Check the legality of the profile setup
assert!(profile <= 7);
assert!(end_addr >= start_addr);
assert!(end_addr < 1024);
assert_eq!(amplitude_data.len() as u16, end_addr - start_addr + 1);
// Clear RAM vector, and add address byte
RAM_VEC.clear();
RAM_VEC.push(0x16)
.map_err(|_| Error::DDSRAMError)?;
// Convert amplitude data into bytes recognized by DDS
for amp in amplitude_data.iter() {
let asf = self.amplitude_to_asf(*amp);
RAM_VEC.push(((asf >> 8) & 0xFF) as u8)
.map_err(|_| Error::DDSRAMError)?;
RAM_VEC.push(((asf << 2) & 0xFC) as u8)
.map_err(|_| Error::DDSRAMError)?;
RAM_VEC.push(0)
.map_err(|_| Error::DDSRAMError)?;
RAM_VEC.push(0)
.map_err(|_| Error::DDSRAMError)?;
}
self.set_ram_profile(profile, start_addr, end_addr, RAMDestination::Amplitude,
no_dwell_high, zero_crossing, op_mode, playback_rate)
}
/*
* Configure a RAM mode profile, wrt supplied phase data
* This will setup the static RAM_VEC by converting phase to ftw
*/
pub unsafe fn set_phase_ram_profile(&mut self, profile: u8, start_addr: u16, end_addr: u16,
no_dwell_high: bool, zero_crossing: bool, op_mode: RAMOperationMode, playback_rate: f64,
phase_data: &[f64]
) -> Result<(), Error<E>> {
// Check the legality of the profile setup
assert!(profile <= 7);
assert!(end_addr >= start_addr);
assert!(end_addr < 1024);
assert_eq!(phase_data.len() as u16, end_addr - start_addr + 1);
// Clear RAM vector, and add address byte
RAM_VEC.clear();
RAM_VEC.push(0x16)
.map_err(|_| Error::DDSRAMError)?;
// Convert phase data into bytes recognized by DDS
for deg in phase_data.iter() {
let pow = self.degree_to_pow(*deg);
RAM_VEC.push(((pow >> 8) & 0xFF) as u8)
.map_err(|_| Error::DDSRAMError)?;
RAM_VEC.push(((pow >> 0) & 0xFF) as u8)
.map_err(|_| Error::DDSRAMError)?;
RAM_VEC.push(0)
.map_err(|_| Error::DDSRAMError)?;
RAM_VEC.push(0)
.map_err(|_| Error::DDSRAMError)?;
}
self.set_ram_profile(profile, start_addr, end_addr, RAMDestination::Phase,
no_dwell_high, zero_crossing, op_mode, playback_rate)
}
/*
* Configure a RAM mode profile, wrt supplied phase data
* This will setup the static RAM_VEC by converting phase to ftw
*/
pub unsafe fn set_polar_ram_profile(&mut self, profile: u8, start_addr: u16, end_addr: u16,
no_dwell_high: bool, zero_crossing: bool, op_mode: RAMOperationMode, playback_rate: f64,
polar_data: &[(f64, f64)]
) -> Result<(), Error<E>> {
// Check the legality of the profile setup
assert!(profile <= 7);
assert!(end_addr >= start_addr);
assert!(end_addr < 1024);
assert_eq!(polar_data.len() as u16, end_addr - start_addr + 1);
// Clear RAM vector, and add address byte
RAM_VEC.clear();
RAM_VEC.push(0x16)
.map_err(|_| Error::DDSRAMError)?;
// Convert amplitude data into bytes recognized by DDS
for (deg, amp) in polar_data.iter() {
let pow = self.degree_to_pow(*deg);
let asf = self.amplitude_to_asf(*amp);
RAM_VEC.push(((pow >> 8) & 0xFF) as u8)
.map_err(|_| Error::DDSRAMError)?;
RAM_VEC.push(((pow >> 0) & 0xFF) as u8)
.map_err(|_| Error::DDSRAMError)?;
RAM_VEC.push(((asf >> 8) & 0xFF) as u8)
.map_err(|_| Error::DDSRAMError)?;
RAM_VEC.push(((asf << 2) & 0xFC) as u8)
.map_err(|_| Error::DDSRAMError)?;
}
self.set_ram_profile(profile, start_addr, end_addr, RAMDestination::Phase,
no_dwell_high, zero_crossing, op_mode, playback_rate)
}
/*
* Configure a RAM mode profile, w.r.t static vector (RAM_VEC)
*/
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
) -> Result<(), Error<E>> {
// Check the legality of the profile setup
assert!(profile <= 7);
assert!(end_addr >= start_addr);
assert!(end_addr < 1024);
// assert_eq! RAM_VEC.len() as u16, ((end_addr - start_addr + 1) * 4) + 1);
// Calculate address step rate, and check legality
let step_rate = (self.f_sys_clk/(4.0 * playback_rate)) as u64;
@ -595,7 +671,9 @@ where
// Temporarily disable RAM mode while accessing into RAM
self.disable_ram_configuration()?;
self.write_ram(data)?;
unsafe {
self.write_ram()?;
}
// Properly configure start_addr and end_addr
self.enable_ram_configuration(ram_dst)
@ -622,14 +700,8 @@ where
}
// Write data in RAM
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)
unsafe fn write_ram(&mut self) -> Result<(), Error<E>> {
self.spi.transfer(&mut RAM_VEC)
.map(|_| ())
.map_err(Error::SPI)
}
@ -654,6 +726,17 @@ where
}
Ok(error_count)
}
// Setter function for f_sys_clk
// Warning: This does not setup the chip to generate this actual f_sys_clk
pub(crate) fn set_f_sys_clk(&mut self, f_sys_clk: f64) {
self.f_sys_clk = f_sys_clk;
}
// Getter function for f_sys_clk
pub fn get_f_sys_clk(&mut self) -> f64 {
self.f_sys_clk
}
}
// Strong check for bytes passed to a register

View File

@ -3,7 +3,6 @@ use embedded_hal::{
blocking::spi::Transfer,
blocking::delay::DelayUs,
};
use log::info;
#[derive(Debug)]
pub enum FPGAFlashError {
@ -12,13 +11,15 @@ pub enum FPGAFlashError {
ResetStatusError,
}
const DATA: &'static [u8] = include_bytes!("../build/top.bin");
// A public method to flash iCE40 FPGA on Humpback
pub fn flash_ice40_fpga<SPI: Transfer<u8>,
SS: OutputPin,
RST: OutputPin,
DELAY: DelayUs<u32>,
DONE: InputPin>
(mut spi: SPI, mut ss: SS, mut creset: RST, cdone: DONE, mut delay: DELAY, data: &[u8]) -> Result<(), FPGAFlashError>
(mut spi: SPI, mut ss: SS, mut creset: RST, cdone: DONE, mut delay: DELAY) -> Result<(), FPGAFlashError>
{
// Data buffer setup
let mut dummy_byte :[u8; 1] = [0x00];
@ -62,7 +63,7 @@ pub fn flash_ice40_fpga<SPI: Transfer<u8>,
.map_err(|_| FPGAFlashError::NegotiationError)?;
// Send the whole image without interruption
for byte in data.into_iter() {
for byte in DATA.into_iter() {
let mut single_byte_slice = [*byte];
spi.transfer(&mut single_byte_slice)
.map_err(|_| FPGAFlashError::SPICommunicationError)?;
@ -83,10 +84,8 @@ pub fn flash_ice40_fpga<SPI: Transfer<u8>,
_ => return Err(FPGAFlashError::ResetStatusError),
};
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)?;
info!("User I/O pins activated.");
Ok(())
}

78
src/logger.rs Normal file
View File

@ -0,0 +1,78 @@
// Enables ITM
pub unsafe fn enable_itm(
dbgmcu: &stm32h7xx_hal::stm32::DBGMCU,
dcb: &mut cortex_m::peripheral::DCB,
itm: &mut cortex_m::peripheral::ITM
) {
// ARMv7-M DEMCR: Set TRCENA. Enables DWT and ITM units
//unsafe { *(0xE000_EDFC as *mut u32) |= 1 << 24 };
dcb.enable_trace();
// Ensure debug blocks are clocked before interacting with them
dbgmcu.cr.modify(|_, w| {
w.d1dbgcken()
.set_bit()
.d3dbgcken()
.set_bit()
.traceclken()
.set_bit()
.dbgsleep_d1()
.set_bit()
});
// SWO: Unlock
*(0x5c00_3fb0 as *mut u32) = 0xC5ACCE55;
// SWTF: Unlock
*(0x5c00_4fb0 as *mut u32) = 0xC5ACCE55;
// SWO CODR Register: Set SWO speed
*(0x5c00_3010 as *mut _) = 200;
// SWO SPPR Register:
// 1 = Manchester
// 2 = NRZ
*(0x5c00_30f0 as *mut _) = 2;
// SWTF Trace Funnel: Enable for CM7
*(0x5c00_4000 as *mut u32) |= 1;
// ITM: Unlock
itm.lar.write(0xC5ACCE55);
// ITM Trace Enable Register: Enable lower 8 stimulus ports
itm.ter[0].write(1);
// ITM Trace Control Register: Enable ITM
itm.tcr.write(
(0b000001 << 16) | // TraceBusID
(1 << 3) | // enable SWO output
(1 << 0), // enable the ITM
);
}
use panic_itm as _;
use lazy_static::lazy_static;
use log::LevelFilter;
pub use cortex_m_log::log::Logger;
use cortex_m_log::{
destination::Itm as ItmDest,
printer::itm::InterruptSync,
modes::InterruptFree,
printer::itm::ItmSync
};
lazy_static! {
static ref LOGGER: Logger<ItmSync<InterruptFree>> = Logger {
level: LevelFilter::Trace,
inner: unsafe {
InterruptSync::new(
ItmDest::new(cortex_m::Peripherals::steal().ITM)
)
},
};
}
pub fn init() {
cortex_m_log::log::init(&LOGGER).unwrap();
}

View File

@ -1,39 +1,71 @@
#![no_main]
#![no_std]
#[macro_use]
extern crate log;
use log::{trace, debug, info, warn};
use stm32h7xx_hal::hal::digital::v2::{
InputPin,
OutputPin,
};
#![feature(str_strip)]
use log::{ trace, debug, info, warn };
use stm32h7xx_hal::hal::digital::v2::InputPin;
use stm32h7xx_hal::gpio::Speed;
use stm32h7xx_hal::{pac, prelude::*, spi};
use stm32h7xx_hal::ethernet;
use smoltcp as net;
use minimq::{
embedded_nal::{IpAddr, Ipv4Addr, TcpStack},
MqttClient, QoS
};
use cortex_m;
use cortex_m_rt::entry;
use rtic::cyccnt::{Instant, U32Ext};
use firmware;
use firmware::{
attenuator::Attenuator,
config_register::{
ConfigRegister,
CFGMask,
StatusMask,
},
dds::{
DDS,
DDSCFRMask,
},
cpld::{
CPLD,
}
use heapless::Vec;
use heapless::consts;
#[macro_use]
pub mod bitmask_macro;
pub mod spi_slave;
pub mod cpld;
use crate::cpld::CPLD;
pub mod config_register;
pub mod attenuator;
pub mod dds;
pub mod nal_tcp_client;
use crate::nal_tcp_client::{ NetStorage, NetworkStack };
pub mod flash;
use crate::flash::flash_ice40_fpga;
pub mod mqtt_mux;
use crate::mqtt_mux::MqttMux;
pub mod urukul;
use crate::urukul::Urukul;
mod logger;
static mut NET_STORE: NetStorage = NetStorage {
// Placeholder for the real IP address, which is initialized at runtime.
ip_addrs: [net::wire::IpCidr::Ipv6(
net::wire::Ipv6Cidr::SOLICITED_NODE_PREFIX,
)],
neighbor_cache: [None; 8],
routes_cache: [None; 8],
};
#[path = "../examples/util/logger.rs"]
mod logger;
#[link_section = ".sram3.eth"]
static mut DES_RING: ethernet::DesRing = ethernet::DesRing::new();
macro_rules! add_socket {
($sockets:ident, $tx_storage:ident, $rx_storage:ident) => {
let mut $rx_storage = [0; 4096];
let mut $tx_storage = [0; 4096];
let tcp_socket = {
let tx_buffer = net::socket::TcpSocketBuffer::new(&mut $tx_storage[..]);
let rx_buffer = net::socket::TcpSocketBuffer::new(&mut $rx_storage[..]);
net::socket::TcpSocket::new(tx_buffer, rx_buffer)
};
let _handle = $sockets.add(tcp_socket);
};
}
#[entry]
fn main() -> ! {
@ -41,6 +73,16 @@ fn main() -> ! {
let mut cp = cortex_m::Peripherals::take().unwrap();
let dp = pac::Peripherals::take().unwrap();
unsafe {
logger::enable_itm(&dp.DBGMCU, &mut cp.DCB, &mut cp.ITM);
}
logger::init();
// Enable SRAM3 for the descriptor ring.
dp.RCC.ahb2enr.modify(|_, w| w.sram3en().set_bit());
// // Reset RCC clock
// dp.RCC.rsr.write(|w| w.rmvf().set_bit());
let pwr = dp.PWR.constrain();
let vos = pwr.freeze();
@ -48,16 +90,17 @@ fn main() -> ! {
let ccdr = rcc
.use_hse(8.mhz())
.sys_ck(400.mhz())
.hclk(200.mhz())
.pll1_q_ck(48.mhz())
.pll1_r_ck(400.mhz())
.freeze(vos, &dp.SYSCFG);
unsafe {
logger::enable_itm(&dp.DBGMCU, &mut cp.DCB, &mut cp.ITM);
}
logger::init();
let delay = cp.SYST.delay(ccdr.clocks);
let mut delay = cp.SYST.delay(ccdr.clocks);
cp.SCB.invalidate_icache();
cp.SCB.enable_icache();
cp.DWT.enable_cycle_counter();
let gpioa = dp.GPIOA.split(ccdr.peripheral.GPIOA);
let gpiob = dp.GPIOB.split(ccdr.peripheral.GPIOB);
@ -65,29 +108,91 @@ fn main() -> ! {
let gpiod = dp.GPIOD.split(ccdr.peripheral.GPIOD);
let gpioe = dp.GPIOE.split(ccdr.peripheral.GPIOE);
let gpiof = dp.GPIOF.split(ccdr.peripheral.GPIOF);
let gpiog = dp.GPIOG.split(ccdr.peripheral.GPIOG);
// Setup CDONE for checking
// Note: ITM doesn't work beyond this, due to a pin conflict between:
// - FPGA_SPI: SCK (af5)
// - ST_LINK SWO (af0)
// Both demands PB3
trace!("Flashing configuration bitstream to iCE40 HX8K on Humpback.");
// Using SPI_1 alternate functions (af5)
let fpga_sck = gpiob.pb3.into_alternate_af5();
let fpga_sdo = gpiob.pb4.into_alternate_af5();
let fpga_sdi = gpiob.pb5.into_alternate_af5();
// Setup SPI_SS_B and CRESET_B
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();
match fpga_cdone.is_high() {
Ok(true) => info!("FPGA is ready."),
Ok(_) => info!("FPGA is in reset state."),
Err(_) => info!("Error: Cannot read C_DONE"),
};
// Setup SPI interface
let fpga_cfg_spi = dp.SPI1.spi(
(fpga_sck, fpga_sdo, fpga_sdi),
spi::MODE_3,
12.mhz(),
ccdr.peripheral.SPI1,
&ccdr.clocks,
);
flash_ice40_fpga(fpga_cfg_spi, fpga_ss, fpga_creset, fpga_cdone, delay).unwrap();
// Configure ethernet IO
{
let _rmii_refclk = gpioa.pa1.into_alternate_af11().set_speed(Speed::VeryHigh);
let _rmii_mdio = gpioa.pa2.into_alternate_af11().set_speed(Speed::VeryHigh);
let _rmii_mdc = gpioc.pc1.into_alternate_af11().set_speed(Speed::VeryHigh);
let _rmii_crs_dv = gpioa.pa7.into_alternate_af11().set_speed(Speed::VeryHigh);
let _rmii_rxd0 = gpioc.pc4.into_alternate_af11().set_speed(Speed::VeryHigh);
let _rmii_rxd1 = gpioc.pc5.into_alternate_af11().set_speed(Speed::VeryHigh);
let _rmii_tx_en = gpiog.pg11.into_alternate_af11().set_speed(Speed::VeryHigh);
let _rmii_txd0 = gpiog.pg13.into_alternate_af11().set_speed(Speed::VeryHigh);
let _rmii_txd1 = gpiob.pb13.into_alternate_af11().set_speed(Speed::VeryHigh);
}
// Configure ethernet
let mac_addr = net::wire::EthernetAddress([0xAC, 0x6F, 0x7A, 0xDE, 0xD6, 0xC8]);
let (eth_dma, mut eth_mac) = unsafe {
ethernet::new_unchecked(
dp.ETHERNET_MAC,
dp.ETHERNET_MTL,
dp.ETHERNET_DMA,
&mut DES_RING,
mac_addr.clone(),
)
};
unsafe { ethernet::enable_interrupt() }
let store = unsafe { &mut NET_STORE };
store.ip_addrs[0] = net::wire::IpCidr::new(net::wire::IpAddress::v4(192, 168, 1, 200), 24);
let neighbor_cache = net::iface::NeighborCache::new(&mut store.neighbor_cache[..]);
let mut routes = net::iface::Routes::new(&mut store.routes_cache[..]);
let default_v4_gw = net::wire::Ipv4Address::new(192, 168, 1, 1);
routes.add_default_ipv4_route(default_v4_gw).unwrap();
let mut net_interface = net::iface::EthernetInterfaceBuilder::new(eth_dma)
.ethernet_addr(mac_addr)
.neighbor_cache(neighbor_cache)
.ip_addrs(&mut store.ip_addrs[..])
.routes(routes)
.finalize();
/*
* Using SPI1, AF5
* SCLK -> PA5
* MOSI -> PB5
* MISO -> PA6
* Using SPI6
* SCLK -> PA5 (af8)
* MOSI -> PG14 (af5)
* MISO -> PA6 (af8)
* CS -> 0: PB12, 1: PA15, 2: PC7
*/
let sclk = gpioa.pa5.into_alternate_af5();
let mosi = gpiob.pb5.into_alternate_af5();
let miso = gpioa.pa6.into_alternate_af5();
let sclk = gpioa.pa5.into_alternate_af8().set_speed(Speed::VeryHigh);
let mosi = gpiog.pg14.into_alternate_af5().set_speed(Speed::VeryHigh);
let miso = gpioa.pa6.into_alternate_af8().set_speed(Speed::VeryHigh);
let (cs0, cs1, cs2) = (
gpiob.pb12.into_push_pull_output(),
gpioa.pa15.into_push_pull_output(),
@ -99,112 +204,94 @@ fn main() -> ! {
*/
let io_update = gpiob.pb15.into_push_pull_output();
let spi = dp.SPI1.spi(
let spi = dp.SPI6.spi(
(sclk, miso, mosi),
spi::MODE_0,
3.mhz(),
ccdr.peripheral.SPI1,
10.mhz(),
ccdr.peripheral.SPI6,
&ccdr.clocks,
);
let switch = CPLD::new(spi, (cs0, cs1, cs2), io_update);
let parts = switch.split();
let mut config = ConfigRegister::new(parts.spi1);
let mut att = Attenuator::new(parts.spi2);
let mut dds0 = DDS::new(parts.spi4, 25_000_000.0);
let mut urukul = Urukul::new(
parts.spi1, parts.spi2, parts.spi3, parts.spi4, parts.spi5, parts.spi6, parts.spi7
);
// Reset all DDS, set CLK_SEL to 0
config.set_configurations(&mut [
(CFGMask::RST, 1),
(CFGMask::IO_RST, 1),
(CFGMask::IO_UPDATE, 0)
]).unwrap();
urukul.reset().unwrap();
// info!("Test value: {}", urukul.test().unwrap());
config.set_configurations(&mut [
(CFGMask::IO_RST, 0),
(CFGMask::RST, 0),
(CFGMask::RF_SW, 13),
(CFGMask::DIV, 3)
]).unwrap();
let mut mqtt_mux = MqttMux::new(urukul);
dds0.init().unwrap();
// Time unit in ms
let mut time: u32 = 0;
dds0.set_configurations(&mut [
(DDSCFRMask::PDCLK_ENABLE, 0),
(DDSCFRMask::READ_EFFECTIVE_FTW, 1),
]).unwrap();
// Cycle counter for 1 ms
// This effectively provides a conversion from rtic unit to ms
let mut next_ms = Instant::now();
next_ms += 400_000.cycles();
dds0.set_sys_clk_frequency(1_000_000_000.0).unwrap();
let mut socket_set_entries: [_; 8] = Default::default();
let mut sockets = net::socket::SocketSet::new(&mut socket_set_entries[..]);
add_socket!(sockets, rx_storage, tx_storage);
// Attenuator
att.set_attenuation([
5.0, 31.5, 24.0, 0.0
]).unwrap();
let tcp_stack = NetworkStack::new(&mut net_interface, sockets);
dds0.set_single_tone_profile(1, 10_000_000.0, 0.0, 0.5).unwrap();
config.set_configurations(&mut [
(CFGMask::PROFILE, 1),
]).unwrap();
// Case dealt: Ethernet connection break down, neither side has timeout
// Limitation: Timeout inequality will cause TCP socket state to desync
// Probably fixed in latest smoltcp commit
let mut client = MqttClient::<consts::U256, _>::new(
IpAddr::V4(Ipv4Addr::new(192, 168, 1, 125)),
"Urukul",
tcp_stack,
)
.unwrap();
// // Setup RAM configuration
// dds0.set_configurations(&mut [
// (DDSCFRMask::RAM_ENABLE, 1),
// (DDSCFRMask::RAM_PLAYBACK_DST, 2),
// ]).unwrap();
let mut tick = false;
let mut has_subscribed = false;
// // 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();
loop {
// Update time accumulator in ms
// Tick once every ms
if Instant::now() > next_ms {
tick = true;
time += 1;
next_ms += 400_000.cycles();
}
// debug!("{:#X?}", dds0.read_register(0x0E, &mut[
// 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00,
// ]).unwrap());
// eth Poll if necessary
// Do not poll if eth link is down
if tick && client.network_stack.update_delay(time) == 0 && eth_mac.phy_poll_link() {
client.network_stack.update(time);
}
// // Choose profile 0
// config.set_configurations(&mut [
// (CFGMask::PROFILE, 0),
// ]).unwrap();
// Process MQTT messages about Urukul/Control
let connection = client
.poll(|_client, topic, message, _properties| {
// info!("On {:?}, received: {:?}", topic, message);
// Why is topic a string while message is a slice?
mqtt_mux.process_mqtt(topic, message).is_ok();
}).is_ok();
// // Set RAM to be amplitudes, disable RAM momentarily
// dds0.set_configurations(&mut [
// (DDSCFRMask::RAM_PLAYBACK_DST, 0),
// (DDSCFRMask::RAM_ENABLE, 0),
// ]).unwrap();
if connection && !has_subscribed && tick {
match client.subscribe("Urukul/Control/#", &[]) {
Ok(()) => has_subscribed = true,
Err(minimq::Error::NotReady) => {},
_e => {},
};
}
// 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();
if connection && tick && (time % 3000) == 0 {
client.publish("Urukul/Channel1/Switch", mqtt_mux
.get_switch_status_message(1)
.unwrap()
.as_bytes(), QoS::AtMostOnce, &[])
.unwrap();
}
// config.set_configurations(&mut [
// (CFGMask::PROFILE, 1),
// ]).unwrap();
// config.set_configurations(&mut [
// (CFGMask::PROFILE, 0),
// ]).unwrap();
// dds0.set_configurations(&mut [
// (DDSCFRMask::RAM_ENABLE, 1),
// ]).unwrap();
loop {}
// Reset tick flag
tick = false;
}
}

View File

@ -1,24 +1,18 @@
use log::info;
use nom::IResult;
use nom::combinator::{value, map, map_res, not, opt, all_consuming};
use nom::sequence::{terminated, preceded, pair, delimited, tuple};
use nom::bytes::complete::{take, tag, tag_no_case, take_while};
use nom::combinator::{value, map, map_res, opt, all_consuming};
use nom::sequence::{terminated, preceded, pair};
use nom::bytes::complete::{tag, tag_no_case, take_while};
use nom::character::complete::digit1;
use nom::character::is_space;
use nom::branch::alt;
use nom::branch::{permutation, alt};
use nom::number::complete::{float, double};
use uom::si::f64::Frequency;
use uom::si::frequency::{hertz, kilohertz, megahertz, gigahertz};
use arrayvec::ArrayVec;
use embedded_hal::blocking::spi::Transfer;
use core::convert::TryInto;
use crate::ClockSource as UrukulClockSource;
use crate::ClockSource::*;
use crate::Urukul;
use crate::Error;
use crate::urukul::ClockSource as UrukulClockSource;
use crate::urukul::Urukul;
use crate::urukul::Error;
#[derive(Debug, Clone)]
pub enum MqttTopic {
@ -284,24 +278,24 @@ impl<SPI, E> MqttMux<SPI> where SPI: Transfer<u8, Error = E> {
MqttCommand::Profile(prof) => self.urukul.set_profile(prof),
}
}
pub fn get_switch_status_message(&mut self, channel: u8) -> Result<&str, Error<E>> {
self.urukul.get_channel_switch_status(channel.into()).map(
|stat| if stat {
"on"
} else {
"off"
})
}
}
// Topic separator parser
fn topic_separator<'a>(topic: &'a str) -> IResult<&'a str, ()> {
value((), tag("/"))(topic)
}
// Message separator parser
// Read message parameter separator (optional comma and whitespace)
fn message_separator(message: &[u8]) -> IResult<&[u8], ()> {
value(
(),
preceded(
whitespace,
preceded(
tag("/"),
whitespace
)
)
preceded(
opt(
tag(",")
),
whitespace
)(message)
}
@ -401,63 +395,48 @@ fn clock_division_message(message: &[u8]) -> IResult<&[u8], MqttCommand> {
}
// Parser for one-command master clock setup message
// Possible improvements: Chop off redundant braces and quotes
// Allow optional parameters/permutation of parameters
fn clock_message(message: &[u8]) -> IResult<&[u8], MqttCommand> {
all_consuming(
map(
delimited(
tag("{"),
tuple((
permutation((
preceded(
tag_no_case("source:"),
preceded(
whitespace,
preceded(
tag("\"source\":"),
preceded(
whitespace,
terminated(
alt((
value(UrukulClockSource::OSC, tag_no_case("OSC")),
value(UrukulClockSource::MMCX, tag_no_case("MMCX")),
value(UrukulClockSource::SMA, tag_no_case("SMA"))
)),
tag(",")
)
)
)
),
preceded(
whitespace,
preceded(
tag("\"frequency\":"),
preceded(
whitespace,
terminated(
read_frequency,
tag(",")
)
)
)
),
preceded(
whitespace,
preceded(
tag("\"division\":"),
preceded(
whitespace,
terminated(
map_res(
digit1,
|div: &[u8]| u8::from_str_radix(core::str::from_utf8(div).unwrap(), 10)
),
whitespace
)
)
terminated(
alt((
value(UrukulClockSource::OSC, tag_no_case("OSC")),
value(UrukulClockSource::MMCX, tag_no_case("MMCX")),
value(UrukulClockSource::SMA, tag_no_case("SMA"))
)),
message_separator
)
)
)),
tag("}")
),
),
preceded(
tag_no_case("frequency:"),
preceded(
whitespace,
terminated(
read_frequency,
message_separator
)
)
),
preceded(
tag_no_case("division:"),
preceded(
whitespace,
terminated(
map_res(
digit1,
|div: &[u8]| u8::from_str_radix(core::str::from_utf8(div).unwrap(), 10)
),
message_separator
)
)
)
)),
|(src, freq, div): (UrukulClockSource, f64, u8)| MqttCommand::Clock(src, freq, div)
)
)(message)
@ -512,68 +491,50 @@ fn singletone_phase_message(channel: u8, profile: u8, message: &[u8]) -> IResult
}
// Parser for one-command singletone profile Command
// Using JSON like command structure
// Possible improvements: Chop off redundant braces and quotes
// Allow optional parameters/permutation of parameters
fn singletone_message(channel: u8, profile: u8, message: &[u8]) -> IResult<&[u8], MqttCommand> {
all_consuming(
map(
tuple((
permutation((
preceded(
tag("{"),
tag_no_case("frequency:"),
preceded(
whitespace,
preceded(
tag("\"frequency\":"),
preceded(
whitespace,
read_frequency
)
terminated(
read_frequency,
message_separator
)
)
),
preceded(
tag(","),
tag_no_case("phase:"),
preceded(
whitespace,
preceded(
tag("\"amplitude\":"),
terminated(
double,
preceded(
whitespace,
double
)
)
)
),
preceded(
tag(","),
preceded(
whitespace,
preceded(
tag("\"phase\":"),
preceded(
whitespace,
terminated(
double,
opt(
preceded(
opt(
preceded(
whitespace,
tag_no_case("deg")
)
),
preceded(
whitespace,
tag("}")
)
whitespace,
tag_no_case("deg")
)
)
),
message_separator
)
)
)
),
preceded(
tag_no_case("amplitude:"),
preceded(
whitespace,
terminated(
double,
message_separator
)
)
)
)),
|(freq, ampl, phase): (f64, f64, f64)| MqttCommand::Singletone(channel, profile, freq, phase, ampl)
|(freq, phase, ampl): (f64, f64, f64)| MqttCommand::Singletone(channel, profile, freq, phase, ampl)
)
)(message)
}

View File

@ -1,510 +0,0 @@
use scpi::error::Result;
use scpi::expression::numeric_list;
use scpi::expression::numeric_list::NumericList;
use scpi::format::{Arbitrary, Character};
use scpi::prelude::*;
use scpi::NumericValues;
use core::convert::{TryFrom, TryInto};
use core::str;
use scpi::{
nquery,
qonly,
};
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;
use crate::{
Urukul,
UrukulTraits,
Error as UrukulError,
ClockSource,
};
use log::{trace, debug, info, warn};
#[macro_export]
macro_rules! recursive_scpi_tree {
// Handle optional headers (end-node)
([$header_name: expr] => $handler: ident) => {
Node {
name: str::as_bytes($header_name),
handler: Some(&$handler{}),
sub: &[],
optional: true,
}
};
// Handle non-optinal header (end-node)
($header_name: expr => $handler: ident) => {
Node {
name: str::as_bytes($header_name),
handler: Some(&$handler{}),
sub: &[],
optional: false,
}
};
// Handle optional header with sub-commands
([$header_name: expr] => {$($($rest: tt)=>*),*}) => {
Node {
name: str::as_bytes($header_name),
handler: None,
sub: &[
$(
recursive_scpi_tree!($($rest)=>*),
)*
],
optional: true,
}
};
// Handle non-optional header with sub-commands
($header_name: expr => {$($($rest: tt)=>*),*}) => {
Node {
name: str::as_bytes($header_name),
handler: None,
sub: &[
$(
recursive_scpi_tree!($($rest)=>*),
)*
],
optional: false,
}
};
}
#[macro_export]
macro_rules! scpi_root {
($($($node: tt)=>*),*) => {
&Node {
name: b"ROOT",
optional: false,
handler: None,
sub: &[
// Create default IEEE488 mandated commands
ieee488_cls!(),
ieee488_ese!(),
ieee488_esr!(),
ieee488_idn!(b"manufacturer", b"model", b"serial", b"0.1.2"),
ieee488_opc!(),
ieee488_rst!(),
ieee488_sre!(),
ieee488_stb!(),
ieee488_tst!(),
ieee488_wai!(),
// Create default SCPI mandated STATus subsystem
scpi_status!(),
// Create default SCPI mandated SYSTem subsystem
scpi_system!(),
//
scpi_crate_version!(),
$(
recursive_scpi_tree!($($node)=>*),
)*
]
}
};
}
#[macro_export]
macro_rules! scpi_tree {
() => {
scpi_root!(
"CHANNEL0" => {
"SWitch" => Channel0SwitchCommand,
"ATTenuation" => Channel0AttenuationCommand,
"SYSCLOCK" => Channel0SystemClockCommand,
"PROFILE0" => {
"SINGLEtone" => {
"FREQuency" => Channel0Profile0SingletoneFrequencyCommand,
"PHASE" => Channel0Profile0SingletonePhaseCommand,
"AMPlitude" => Channel0Profile0SingletoneAmplitudeCommand,
["Setup"] => Channel0Profile0SingletoneCommand
}
}
},
"CHANNEL1" => {
"SWitch" => Channel1SwitchCommand,
"ATTenuation" => Channel1AttenuationCommand
},
"CHANNEL2" => {
"SWitch" => Channel2SwitchCommand,
"ATTenuation" => Channel2AttenuationCommand
},
"CHANNEL3" => {
"SWitch" => Channel3SwitchCommand,
"ATTenuation" => Channel3AttenuationCommand
},
"CLOCK" => {
"SOURCE" => ClockSourceCommand,
"DIVision" => ClockDivisionCommand
},
"PROFILE" => ProfileCommand,
["EXAMple"] => {
"HELLO" => {
"WORLD" => HelloWorldCommand
}
}
);
};
}
pub struct HelloWorldCommand {}
impl<T: Device> Command<T> for HelloWorldCommand {
qonly!();
fn query(
&self,
_context: &mut Context<T>,
_args: &mut Tokenizer,
response: &mut ResponseUnit,
) -> Result<()> {
response.data(b"Hello world" as &[u8]).finish()
}
}
pub struct Channel0SwitchCommand {}
pub struct Channel1SwitchCommand {}
pub struct Channel2SwitchCommand {}
pub struct Channel3SwitchCommand {}
macro_rules! impl_channel_switch_command {
($($channel: literal => $command_struct: ty),*) => {
$(
impl<T: Device + UrukulTraits> Command<T> for $command_struct {
nquery!();
fn event(&self, context: &mut Context<T>, args: &mut Tokenizer) -> Result<()> {
let next_state: bool = args.next_data(true)?
.map_or(
context.device.get_channel_switch_status($channel)
.map(|current| !current)
.map_err(|_| Error::new(ErrorCode::HardwareError)),
|token| token.try_into()
)?;
context.device.set_channel_switch($channel, next_state).map_err(|_| Error::new(ErrorCode::HardwareError))
}
}
)*
};
}
impl_channel_switch_command!(
0 => Channel0SwitchCommand,
1 => Channel1SwitchCommand,
2 => Channel2SwitchCommand,
3 => Channel3SwitchCommand
);
pub struct ClockSourceCommand {}
pub struct ClockDivisionCommand {}
pub struct Channel0SystemClockCommand {}
pub struct ProfileCommand {}
pub struct Channel0Profile0SingletoneCommand {}
pub struct Channel0Profile0SingletoneFrequencyCommand {}
pub struct Channel0Profile0SingletonePhaseCommand {}
pub struct Channel0Profile0SingletoneAmplitudeCommand {}
// Handle CLOCK:SOURCE command, setup the proper source for the system clock
// Leave clock division to CLOCK:DIVision command
impl<T:Device + UrukulTraits> Command<T> for ClockSourceCommand {
nquery!();
fn event(&self, context: &mut Context<T>, args: &mut Tokenizer) -> Result<()> {
// next_data() fucntion call can never return CharacterProgramData, could be an oversight
let s: &[u8] = match args.next_data(false)? {
Some(Token::CharacterProgramData(s)) => s,
_ => return Err(ErrorCode::IllegalParameterValue.into()),
};
let s_str: &str = str::from_utf8(s)
.map_err(|_| ErrorCode::CharacterDataError)?;
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 master clock frequency: {:?}", frequency);
let clock_source = match s_str {
source if source.eq_ignore_ascii_case("OSC") => {
// If clock source is OSC, it must be 100MHz (not configurable)
if frequency.get::<megahertz>() != 100.0 {
warn!("Clock selection failed! OSC must be 100 MHz");
return Err(ErrorCode::IllegalParameterValue.into());
}
ClockSource::OSC
},
source if source.eq_ignore_ascii_case("MMCX") => {
// TODO: Implement frequency check for MMCX
ClockSource::MMCX
},
source if source.eq_ignore_ascii_case("SMA") => {
// TODO: Implement frequency check for SMA
ClockSource::SMA
},
_ => {
warn!("Clock selection failed! Argument error!");
return Err(ErrorCode::IllegalParameterValue.into());
},
};
trace!("Changing clock source to {:?} at {:?}", clock_source, frequency);
context.device.set_clock_source(clock_source, frequency.get::<hertz>())
.map_err(|_| Error::new(ErrorCode::HardwareError))
}
}
impl<T:Device + UrukulTraits> Command<T> for ClockDivisionCommand {
nquery!();
fn event(&self, context: &mut Context<T>, args: &mut Tokenizer) -> Result<()> {
let div :f32 = args.next_data(false)?
.map_or(Err(Error::new(ErrorCode::IllegalParameterValue)),
|token| token.try_into())?;
trace!("Received master clock division factor: {}", div);
if div == 1.0 || div == 2.0 || div == 4.0 {
debug!("Set master clock division as {}", div);
context.device.set_clock_division(div as u8)
.map_err(|_| Error::new(ErrorCode::HardwareError))
} else {
Err(Error::new(ErrorCode::IllegalParameterValue))
}
}
}
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))
}
}
pub struct Channel0AttenuationCommand {}
pub struct Channel1AttenuationCommand {}
pub struct Channel2AttenuationCommand {}
pub struct Channel3AttenuationCommand {}
macro_rules! impl_channel_attenuation_command {
($($channel: literal => $command_struct: ty),*) => {
$(
impl<T:Device + UrukulTraits> Command<T> for $command_struct {
nquery!();
fn event(&self, context: &mut Context<T>, args: &mut Tokenizer) -> Result<()> {
let attenuation: f32 = args.next_data(false)?
.map_or(Err(Error::new(ErrorCode::IllegalParameterValue)),
|token| token.try_into())?;
trace!("Received channel {} attenuation input: {}", $channel, attenuation);
context.device.set_channel_attenuation($channel, attenuation)
.map_err(|_| Error::new(ErrorCode::HardwareError))
}
}
)*
};
}
impl_channel_attenuation_command!(
0 => Channel0AttenuationCommand,
1 => Channel1AttenuationCommand,
2 => Channel2AttenuationCommand,
3 => Channel3AttenuationCommand
);
impl<T:Device + UrukulTraits> Command<T> for ProfileCommand {
nquery!();
fn event(&self, context: &mut Context<T>, args: &mut Tokenizer) -> Result<()> {
let profile :f32 = args.next_data(false)?
.map_or(Err(Error::new(ErrorCode::IllegalParameterValue)),
|token| token.try_into())?;
if ((profile as u8) as f32) != profile {
return Err(Error::new(ErrorCode::IllegalParameterValue));
}
trace!("Selected Profile :{}", profile);
let profile = profile as u8;
if profile >= 8 {
Err(Error::new(ErrorCode::IllegalParameterValue))
} else {
context.device.set_profile(profile)
.map_err(|_| Error::new(ErrorCode::HardwareError))
}
}
}
impl<T:Device + UrukulTraits> Command<T> for Channel0Profile0SingletoneCommand {
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());
}
trace!("Set up a single tone on channel 0, profile 0");
context.device.set_channel_single_tone_profile(0, 0, frequency.get::<hertz>(), phase.get::<degree>(), amplitude)
.map_err(|_| Error::new(ErrorCode::HardwareError))
}
}
impl<T:Device + UrukulTraits> Command<T> for Channel0Profile0SingletoneFrequencyCommand {
// TODO: Implement query for publishing
nquery!();
// Param: frequency
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());
}
context.device.set_channel_single_tone_profile_frequency(0, 0, frequency.get::<hertz>())
.map_err(|_| Error::new(ErrorCode::HardwareError))
}
}
impl<T:Device + UrukulTraits> Command<T> for Channel0Profile0SingletonePhaseCommand {
// TODO: Implement query for publishing
nquery!();
// Param: frequency
fn event(&self, context: &mut Context<T>, args: &mut Tokenizer) -> Result<()> {
// 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());
}
context.device.set_channel_single_tone_profile_phase(0, 0, phase.get::<degree>())
.map_err(|_| Error::new(ErrorCode::HardwareError))
}
}
impl<T:Device + UrukulTraits> Command<T> for Channel0Profile0SingletoneAmplitudeCommand {
// TODO: Implement query for publishing
nquery!();
// Param: frequency
fn event(&self, context: &mut Context<T>, args: &mut Tokenizer) -> Result<()> {
// 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());
}
context.device.set_channel_single_tone_profile_amplitude(0, 0, amplitude)
.map_err(|_| Error::new(ErrorCode::HardwareError))
}
}
/*
* Implement "Device" trait from SCPI
* TODO: Implement mandatory commands
*/
impl<SPI, E> Device for Urukul<SPI>
where
SPI: Transfer<u8, Error = E>
{
fn cls(&mut self) -> Result<()> {
Ok(())
}
fn rst(&mut self) -> Result<()> {
match self.reset() {
Ok(_) => Ok(()),
Err(_) => Err(Error::new(ErrorCode::HardwareError))
}
}
fn tst(&mut self) -> Result<()> {
match self.test() {
Ok(0) => Ok(()),
Ok(_) => Err(Error::new(ErrorCode::SelfTestFailed)),
Err(_) => Err(Error::new(ErrorCode::SelfTestFailed)),
}
}
}

View File

@ -3,7 +3,7 @@ use embedded_hal::{
digital::v2::OutputPin,
};
use crate::cpld::CPLD;
use crate::Error;
use crate::urukul::Error;
pub struct SPISlave<'a, SPI, CS0, CS1, CS2, GPIO> (
// SPI device to be multiplexed
@ -29,7 +29,7 @@ impl<'a, SPI, CS0, CS1, CS2, GPIO> Parts<'a, SPI, CS0, CS1, CS2, GPIO> {
Parts {
spi1: SPISlave(&cpld, 1, false),
spi2: SPISlave(&cpld, 2, false),
spi3: SPISlave(&cpld, 3, true),
spi3: SPISlave(&cpld, 3, false),
spi4: SPISlave(&cpld, 4, true),
spi5: SPISlave(&cpld, 5, true),
spi6: SPISlave(&cpld, 6, true),

View File

@ -1,56 +0,0 @@
use scpi::prelude::*;
use scpi::Context;
use scpi::error::Result;
use log::{trace, info};
use arrayvec::{ArrayVec};
pub trait MqttScpiTranslator {
// 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<()>;
}
impl<'a, T: Device> MqttScpiTranslator for Context<'a, T> {
fn run_with_mqtt<FMT>(&mut self, topic: &str, args: &str, response: &mut FMT) -> Result<()>
where
FMT: Formatter,
{
if !topic.starts_with("Urukul/Control") {
info!("Received a publish, but not for control! Topic: {}", topic);
return Ok(());
}
// 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)?;
// }
// }
// // 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() {
buffer.try_push(i as u8)
.map_err(|_| ErrorCode::OutOfMemory)?;
}
// Pass the message to SCPI processing unit
trace!("Translated MQTT message into SCPI. Translated command: {}",
core::str::from_utf8(buffer.as_slice()).unwrap());
self.run(buffer.as_slice(), response)
}
}

View File

@ -1,33 +1,14 @@
#![no_std]
#![feature(str_strip)]
extern crate embedded_hal;
use embedded_hal::{
blocking::spi::Transfer,
};
#[macro_use]
pub mod bitmask_macro;
pub mod spi_slave;
pub mod cpld;
pub mod config_register;
use crate::config_register::ConfigRegister;
use crate::config_register::CFGMask;
use crate::config_register::StatusMask;
pub mod attenuator;
use crate::attenuator::Attenuator;
pub mod dds;
use crate::dds::DDS;
// pub mod scpi;
pub mod translation;
pub mod nal_tcp_client;
pub mod flash;
pub mod mqtt_mux;
/*
* Enum for structuring error
*/
@ -60,6 +41,7 @@ pub enum ClockSource {
pub struct Urukul<SPI> {
config_register: ConfigRegister<SPI>,
attenuator: Attenuator<SPI>,
multi_dds: DDS<SPI>,
dds: [DDS<SPI>; 4],
f_master_clk: f64,
}
@ -77,7 +59,9 @@ where
Urukul {
config_register: ConfigRegister::new(spi1),
attenuator: Attenuator::new(spi2),
// Create 4 DDS instances with fixed 25MHz clock
// Create a multi-channel DDS with predefined 25MHz clock
multi_dds: DDS::new(spi3, 25_000_000.0),
// Create 4 DDS instances with predefined 25MHz clock
// Counter-intuitive to assign urukul clock before having a urukul
dds: [
DDS::new(spi4, 25_000_000.0),
@ -85,7 +69,7 @@ where
DDS::new(spi6, 25_000_000.0),
DDS::new(spi7, 25_000_000.0),
],
// Default clock selection: OSC, fixed 100MHz speed
// Default clock selection: OSC, predefined 100MHz speed
f_master_clk: 100_000_000.0,
}
}
@ -150,7 +134,7 @@ where
SPI: Transfer<u8, Error = E>
{
fn get_channel_switch_status(&mut self, channel: u32) -> Result<bool, Error<E>> {
pub 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 {
@ -158,7 +142,7 @@ where
}
}
fn set_channel_switch(&mut self, channel: u32, status: bool) -> Result<(), Error<E>> {
pub 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 = {
@ -176,7 +160,7 @@ where
}
}
fn set_clock(&mut self, source: ClockSource, frequency: f64, division: u8) -> Result<(), Error<E>> {
pub fn set_clock(&mut self, source: ClockSource, frequency: f64, division: u8) -> Result<(), Error<E>> {
// Change clock source through configuration register
self.set_clock_source(source)?;
@ -187,7 +171,7 @@ where
self.set_clock_division(division)
}
fn set_clock_source(&mut self, source: ClockSource) -> Result<(), Error<E>> {
pub fn set_clock_source(&mut self, source: ClockSource) -> Result<(), Error<E>> {
// Change clock source through configuration register
match source {
ClockSource::OSC => self.config_register.set_configurations(&mut [
@ -204,7 +188,7 @@ where
}.map(|_| ())
}
fn set_clock_frequency(&mut self, frequency: f64) -> Result<(), Error<E>> {
pub fn set_clock_frequency(&mut self, frequency: f64) -> Result<(), Error<E>> {
// Update master clock frequency
self.f_master_clk = frequency;
@ -212,7 +196,7 @@ where
self.set_dds_ref_clk()
}
fn set_clock_division(&mut self, division: u8) -> Result<(), Error<E>> {
pub fn set_clock_division(&mut self, division: u8) -> Result<(), Error<E>> {
match division {
1 => self.config_register.set_configurations(&mut [
(CFGMask::DIV, 1),
@ -231,7 +215,7 @@ where
fn set_dds_ref_clk(&mut self) -> Result<(), Error<E>> {
// Calculate reference clock frequency after clock division from configuration register
let f_ref_clk = self.f_master_clk / (self.config_register.get_configuration(CFGMask::DIV) as f64);
let f_ref_clk = self.f_master_clk / (self.get_master_clock_division() as f64);
// Update all DDS chips on reference clock frequency
for dds_channel in 0..4 {
@ -240,14 +224,23 @@ where
Ok(())
}
fn set_channel_attenuation(&mut self, channel: u8, attenuation: f32) -> Result<(), Error<E>> {
fn get_master_clock_division(&mut self) -> u8 {
match self.config_register.get_configuration(CFGMask::DIV) {
0 | 3 => 4,
1 => 1,
2 => 2,
_ => panic!("Divisor out of range, when reading configuration register (CPLD)."),
}
}
pub fn set_channel_attenuation(&mut self, channel: u8, attenuation: f32) -> Result<(), Error<E>> {
if channel >= 4 || attenuation < 0.0 || attenuation > 31.5 {
return Err(Error::ParameterError);
}
self.attenuator.set_channel_attenuation(channel, attenuation)
}
fn set_profile(&mut self, profile: u8) -> Result<(), Error<E>> {
pub fn set_profile(&mut self, profile: u8) -> Result<(), Error<E>> {
if profile >= 8 {
return Err(Error::ParameterError);
}
@ -256,7 +249,7 @@ where
]).map(|_| ())
}
fn set_channel_single_tone_profile(&mut self, channel: u8, profile: u8, frequency: f64, phase: f64, amplitude: f64) -> Result<(), Error<E>> {
pub fn set_channel_single_tone_profile(&mut self, channel: u8, profile: u8, frequency: f64, phase: f64, amplitude: f64) -> Result<(), Error<E>> {
if channel >= 4 || profile >= 8 || frequency < 0.0 || phase >= 360.0 ||
phase < 0.0 || amplitude < 0.0 || amplitude > 1.0 {
return Err(Error::ParameterError);
@ -264,28 +257,82 @@ where
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<(), Error<E>> {
pub fn set_channel_single_tone_profile_frequency(&mut self, channel: u8, profile: u8, frequency: f64)-> Result<(), Error<E>> {
if channel >= 4 || profile >= 8 || frequency < 0.0 {
return Err(Error::ParameterError);
}
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<(), Error<E>> {
pub fn set_channel_single_tone_profile_phase(&mut self, channel: u8, profile: u8, phase: f64)-> Result<(), Error<E>> {
if channel >= 4 || profile >= 8 || phase >= 360.0 || phase < 0.0 {
return Err(Error::ParameterError);
}
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<(), Error<E>> {
pub fn set_channel_single_tone_profile_amplitude(&mut self, channel: u8, profile: u8, amplitude: f64)-> Result<(), Error<E>> {
if channel >= 4 || profile >= 8 || amplitude < 0.0 || amplitude > 1.0 {
return Err(Error::ParameterError);
}
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<(), Error<E>> {
self.dds[usize::from(channel)].set_sys_clk_frequency(f_sys_clk)
pub 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).map(|_| ())
}
// Multi-dds channel functions
// Do not allow reading of DDS registers
// Make sure only 1 SPI transaction is compelted per function call
// Setup NU_MASK in configuration register
// This selects the DDS channels that will be covered by multi_channel DDS (spi3)
// Note: If a channel is masked, io_update must be completed through configuration register (IO_UPDATE bit-field)
// Implication: Deselect such channel if individual communication is needed.
pub fn set_multi_channel_coverage(&mut self, channel: u8) -> Result<(), Error<E>> {
self.config_register.set_configurations(&mut [
(CFGMask::MASK_NU, channel.into())
]).map(|_| ())
}
// Difference from individual single tone setup function:
// - Remove the need of passing channel
// All selected channels must share the same f_sys_clk
pub fn set_multi_channel_single_tone_profile(&mut self, profile: u8, frequency: f64, phase: f64, amplitude: f64) -> Result<(), Error<E>> {
if profile >= 8 || frequency < 0.0 || phase >= 360.0 ||
phase < 0.0 || amplitude < 0.0 || amplitude > 1.0 {
return Err(Error::ParameterError);
}
// Check f_sys_clk of all selected channels
let selected_channels = self.config_register.get_configuration(CFGMask::MASK_NU);
let mut found_a_selected_channel = false;
let mut reported_f_sys_clk: f64 = 0.0;
for channel_bit in 0..4 {
if (selected_channels & (1 << (channel_bit as u8))) != 0 {
if !found_a_selected_channel {
found_a_selected_channel = true;
reported_f_sys_clk = self.dds[channel_bit].get_f_sys_clk();
} else if reported_f_sys_clk != self.dds[channel_bit].get_f_sys_clk() {
return Err(Error::DDSError);
}
}
}
self.multi_dds.set_sys_clk_frequency(reported_f_sys_clk);
self.multi_dds.set_single_tone_profile(profile, frequency, phase, amplitude)?;
self.invoke_io_update()?;
Ok(())
}
// Generate a pulse for io_update bit in configuration register
// This acts like io_update in CPLD struct, but for multi-dds channel
fn invoke_io_update(&mut self) -> Result<(), Error<E>> {
self.config_register.set_configurations(&mut [
(CFGMask::IO_UPDATE, 1)
])?;
self.config_register.set_configurations(&mut [
(CFGMask::IO_UPDATE, 0)
]).map(|_| ())
}
}