From 233c6619ff5a09147c5843ba58fbe7237e4afe0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Thu, 12 Feb 2026 12:54:25 +0000 Subject: [PATCH 01/19] deps: bump --- Cargo.lock | 87 +++++++++++++++++++++++++++--------------------------- 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f445a7501a..b3116de30b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,7 +77,7 @@ dependencies = [ "arbitrary-int", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -124,7 +124,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -196,7 +196,7 @@ checksum = "e37549a379a9e0e6e576fd208ee60394ccb8be963889eebba3ffe0980364f472" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -236,7 +236,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -249,7 +249,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -260,7 +260,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -271,7 +271,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core 0.23.0", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -282,7 +282,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -504,9 +504,9 @@ dependencies = [ [[package]] name = "git2" -version = "0.20.3" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2b37e2f62729cdada11f0e6b3b6fe383c69c29fc619e391223e12856af308c" +checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" dependencies = [ "bitflags 2.10.0", "libc", @@ -517,9 +517,9 @@ dependencies = [ [[package]] name = "grounded" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917d82402c7eb9755fdd87d52117701dae9e413a6abb309fac2a13af693b6080" +checksum = "5a7c71ebd5d467418b46639b622912cd0338ce59766bd19130bffcbf9ac6df2c" dependencies = [ "portable-atomic", ] @@ -743,9 +743,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.180" +version = "0.2.181" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" [[package]] name = "libgit2-sys" @@ -827,9 +827,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "menu" @@ -844,7 +844,7 @@ dependencies = [ [[package]] name = "miniconf" version = "0.20.0" -source = "git+https://github.com/quartiq/miniconf.git#c635b2c326e940d914ed99b23075523f9ff761a6" +source = "git+https://github.com/quartiq/miniconf.git#3dbf5251eec67d3892b6aed562b560753ea03246" dependencies = [ "heapless 0.8.0", "heapless 0.9.2", @@ -862,12 +862,12 @@ dependencies = [ [[package]] name = "miniconf_derive" version = "0.20.0" -source = "git+https://github.com/quartiq/miniconf.git#c635b2c326e940d914ed99b23075523f9ff761a6" +source = "git+https://github.com/quartiq/miniconf.git#3dbf5251eec67d3892b6aed562b560753ea03246" dependencies = [ "darling 0.23.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -1024,7 +1024,7 @@ checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -1139,7 +1139,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -1198,7 +1198,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -1241,7 +1241,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -1317,15 +1317,15 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "schemars" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", "ref-cast", @@ -1393,7 +1393,8 @@ dependencies = [ [[package]] name = "serde-reflection" version = "0.5.2" -source = "git+https://github.com/zefchain/serde-reflection.git#b8702f1a984557ee02b69f45a6ee4506f541c29d" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68fb2363ca88876b3e16442b02dde5305646fd50df297c79a4b54fc5f5cf51d4" dependencies = [ "erased-discriminant", "once_cell", @@ -1420,7 +1421,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -1456,7 +1457,7 @@ dependencies = [ "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -1711,7 +1712,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -1727,9 +1728,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.114" +version = "2.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12" dependencies = [ "proc-macro2", "quote", @@ -1756,7 +1757,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -1796,7 +1797,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -1807,7 +1808,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -1834,9 +1835,9 @@ checksum = "e87a2ed6b42ec5e28cc3b94c09982969e9227600b2e3dcbc1db927a84c06bd69" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" [[package]] name = "url" @@ -1976,7 +1977,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", "synstructure", ] @@ -1997,7 +1998,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", "synstructure", ] @@ -2031,11 +2032,11 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] name = "zmij" -version = "1.0.18" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1966f8ac2c1f76987d69a74d0e0f929241c10e78136434e3be70ff7f58f64214" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" From 773da8afc52c087e7e712ae4a320b530b96e33bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Thu, 12 Feb 2026 12:55:24 +0000 Subject: [PATCH 02/19] ad9959: avoid some unsafe, use nyquist for frequency --- ad9959/src/lib.rs | 2 +- src/hardware/pounder/dds_output.rs | 8 ++++---- src/hardware/pounder/mod.rs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ad9959/src/lib.rs b/ad9959/src/lib.rs index 552444da21..4d89b58658 100644 --- a/ad9959/src/lib.rs +++ b/ad9959/src/lib.rs @@ -521,7 +521,7 @@ impl ProfileSerializer { pub fn push( &mut self, channels: Channel, - ftw: Option, + ftw: Option, pow: Option, acr: Option, ) { diff --git a/src/hardware/pounder/dds_output.rs b/src/hardware/pounder/dds_output.rs index 9e7e743da3..89247e5502 100644 --- a/src/hardware/pounder/dds_output.rs +++ b/src/hardware/pounder/dds_output.rs @@ -54,12 +54,12 @@ //! buffer to avoid the software overhead of much of the serialization process. use log::warn; -use super::{QspiInterface, hal, hrtimer::HighResTimerE}; +use super::{QspiInterface, hrtimer::HighResTimerE}; use ad9959::{Mode, ProfileSerializer}; /// The DDS profile update stream. pub struct DdsOutput { - _qspi: QspiInterface, + qspi: QspiInterface, io_update_trigger: HighResTimerE, mode: Mode, } @@ -84,7 +84,7 @@ impl DdsOutput { qspi.start_stream().unwrap(); Self { mode, - _qspi: qspi, + qspi, io_update_trigger, } } @@ -107,7 +107,7 @@ impl DdsOutput { pub fn write(&mut self, mut profile: ProfileSerializer) { // Note(unsafe): We own the QSPI interface, so it is safe to access the registers in a raw // fashion. - let regs = unsafe { &*hal::stm32::QUADSPI::ptr() }; + let regs = self.qspi.qspi.inner_mut(); // Warn if the fifo is still at least half full. if regs.sr.read().flevel().bits() >= 16 { diff --git a/src/hardware/pounder/mod.rs b/src/hardware/pounder/mod.rs index ea53d1ccb1..8a4960a334 100644 --- a/src/hardware/pounder/mod.rs +++ b/src/hardware/pounder/mod.rs @@ -196,7 +196,7 @@ impl QspiInterface { // Configure QSPI for infinite transaction mode using only a data phase (no instruction or // address). - let qspi_regs = unsafe { &*hal::stm32::QUADSPI::ptr() }; + let qspi_regs = self.qspi.inner_mut(); qspi_regs.fcr.modify(|_, w| w.ctcf().set_bit()); unsafe { From 9ab8ebb3f11b8be916eac454ab84a7bcf3747aa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Thu, 12 Feb 2026 13:28:22 +0000 Subject: [PATCH 03/19] deps: workspace arbitrary-int --- Cargo.toml | 4 +++- ad9912/Cargo.toml | 2 +- ad9959/Cargo.toml | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2e60f7d407..a76aac74b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,8 +40,10 @@ members = [ "stream", "platform", ] + [workspace.dependencies] arbitrary-int = { version = "1.3.0", features = ["serde", "hint"] } +bitbybit = "1.4" thiserror = { version = "2.0.11", default-features = false } idsp = "0.21.0" dsp-process = "0.2.0" @@ -85,7 +87,7 @@ usbd-serial = "0.2" miniconf.workspace = true miniconf_mqtt = { version = "0.20" } tca9539 = "0.2" -bitbybit = "1.3.3" +bitbybit.workspace = true arbitrary-int.workspace = true thiserror.workspace = true embedded-hal-compat = "0.13.0" diff --git a/ad9912/Cargo.toml b/ad9912/Cargo.toml index 36485ed096..749e08d685 100644 --- a/ad9912/Cargo.toml +++ b/ad9912/Cargo.toml @@ -14,4 +14,4 @@ arbitrary-int.workspace = true thiserror.workspace = true num-traits = { version = "0.2.19", default-features = false } embedded-hal = "1.0" -bitbybit = "1.3.3" +bitbybit.workspace = true diff --git a/ad9959/Cargo.toml b/ad9959/Cargo.toml index aad80caa7a..b1b0cec3ca 100644 --- a/ad9959/Cargo.toml +++ b/ad9959/Cargo.toml @@ -10,5 +10,5 @@ description = "AD9959 4-channel DDS SPI driver" [dependencies] embedded-hal = {version = "0.2.7", features = ["unproven"]} bytemuck = "1.21.0" -bitbybit = "1.3.3" +bitbybit.workspace = true arbitrary-int.workspace = true From 7dbcde295e3b491b51c665ac06643e65de5c0247 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Thu, 12 Feb 2026 14:27:44 +0000 Subject: [PATCH 04/19] deps: miniconf 0.20.1 --- Cargo.lock | 52 ++++++++++------------------------------------------ Cargo.toml | 5 +---- 2 files changed, 11 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b3116de30b..48a1d438cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -211,18 +211,8 @@ version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ - "darling_core 0.21.3", - "darling_macro 0.21.3", -] - -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", + "darling_core", + "darling_macro", ] [[package]] @@ -239,37 +229,13 @@ dependencies = [ "syn 2.0.115", ] -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.115", -] - [[package]] name = "darling_macro" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "darling_core 0.21.3", - "quote", - "syn 2.0.115", -] - -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core 0.23.0", + "darling_core", "quote", "syn 2.0.115", ] @@ -843,8 +809,9 @@ dependencies = [ [[package]] name = "miniconf" -version = "0.20.0" -source = "git+https://github.com/quartiq/miniconf.git#3dbf5251eec67d3892b6aed562b560753ea03246" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c2f61ae3c5e0361464f6ae2d5a68d95f8f0701004eab98812ee94c0d9ea180" dependencies = [ "heapless 0.8.0", "heapless 0.9.2", @@ -862,9 +829,10 @@ dependencies = [ [[package]] name = "miniconf_derive" version = "0.20.0" -source = "git+https://github.com/quartiq/miniconf.git#3dbf5251eec67d3892b6aed562b560753ea03246" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9826a0808d011e3217d93fd84ac670d75782d1f5962438b53a3f580f4dc14016" dependencies = [ - "darling 0.23.0", + "darling", "proc-macro2", "quote", "syn 2.0.115", @@ -1454,7 +1422,7 @@ version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" dependencies = [ - "darling 0.21.3", + "darling", "proc-macro2", "quote", "syn 2.0.115", diff --git a/Cargo.toml b/Cargo.toml index a76aac74b7..76037df17d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,7 @@ idsp = "0.21.0" dsp-process = "0.2.0" dsp-fixedpoint = "0.1.0" # Keep this synced with the miniconf version in py/setup.py -miniconf = { version = "0.20", features = [ +miniconf = { version = "0.20.1", features = [ "json-core", "derive", "postcard", @@ -170,6 +170,3 @@ codegen-units = 1 opt-level = "s" debug = true lto = true - -[patch.crates-io] -miniconf = { git = "https://github.com/quartiq/miniconf.git" } From f00df9329481156c380dc5d85eab26b0bd189cbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Mon, 16 Feb 2026 21:42:54 +0000 Subject: [PATCH 05/19] local idsp --- Cargo.lock | 6 ------ Cargo.toml | 5 +++++ src/mpll.rs | 16 ++++++---------- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 48a1d438cc..66071f39bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -254,8 +254,6 @@ dependencies = [ [[package]] name = "dsp-fixedpoint" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e6691b4edb40c0a1bd948fa524482e3f36f690d782fcabdc39b5d52f6dcf8" dependencies = [ "num-traits", "serde", @@ -264,8 +262,6 @@ dependencies = [ [[package]] name = "dsp-process" version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625475d67fbdf3f4e5b317a3c2d7e83b1e36a8bc07bd637495542f54f18b5f18" [[package]] name = "dyn-clone" @@ -667,8 +663,6 @@ dependencies = [ [[package]] name = "idsp" version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa950bdb624200b55f2feb897c76e787ccd89f607b9be724622cdb7619744db4" dependencies = [ "bytemuck", "dsp-fixedpoint", diff --git a/Cargo.toml b/Cargo.toml index 76037df17d..c3321d8a79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -170,3 +170,8 @@ codegen-units = 1 opt-level = "s" debug = true lto = true + +[patch.crates-io] +idsp = { path = "../idsp" } +dsp-process = { path = "../idsp/dsp-process" } +dsp-fixedpoint = { path = "../idsp/dsp-fixedpoint" } diff --git a/src/mpll.rs b/src/mpll.rs index d8c195101a..5a257c051b 100644 --- a/src/mpll.rs +++ b/src/mpll.rs @@ -43,22 +43,18 @@ pub struct MpllState { impl MpllState { pub fn set_frequency(&mut self, f: f32) { - self.iir.xy[2] = (f * UNITS.y.recip()) as _; - self.iir.xy[3] = self.iir.xy[2]; + self.iir.set_y((f * UNITS.y.recip()) as _); } } /// Runtime active settings #[derive(Debug, Clone)] pub struct Mpll { - lp: Lowpass, + lp: Pair<[Wdf<2, 0xad>; 2], (Wdf<2, 0xad>, Wdf<1, 0xa>), i32>, iir: BiquadClamp, i32>, amplitude: [Q32<16>; 2], } -#[derive(Debug, Clone)] -struct Lowpass(Pair<[Wdf<2, 0xad>; 2], (Wdf<2, 0xad>, Wdf<1, 0xa>), i32>); - #[derive(Debug, Clone, miniconf::Tree)] #[tree(meta(doc, typename))] pub struct MpllConfig { @@ -125,7 +121,7 @@ impl Default for MpllConfig { impl MpllConfig { pub fn build(&self) -> Mpll { Mpll { - lp: Lowpass(Pair::new(( + lp: Pair::new(( ( (), Parallel(( @@ -140,7 +136,7 @@ impl MpllConfig { )), ), (), - ))), + )), iir: self.iir.build(&UNITS), amplitude: self .amplitude @@ -175,7 +171,7 @@ impl Mpll { } // lowpass, 1 bit gain for (mix, state) in mix.iter_mut().zip(state.lp.iter_mut()) { - self.lp.0.inplace(state, mix); + self.lp.inplace(state, mix); } // decimate let demod = [ @@ -186,7 +182,7 @@ impl Mpll { // need full atan2, rotation, or addtl osc to support any phase offset let mut phase = Wrapping(idsp::atan2(demod[0][1], demod[0][0])); // Delay correction - phase += Wrapping(-10) * Wrapping(state.iir.xy[2]); + phase += Wrapping(-10) * Wrapping(state.iir.y0()); // PID let frequency = Wrapping(self.iir.process(&mut state.iir, phase.0)); // modulate From 0bbfdbbf24a0e4abc5e16257d9d5485f27a0a4fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Tue, 17 Feb 2026 14:33:31 +0000 Subject: [PATCH 06/19] statistics: emit ptp --- src/statistics.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/statistics.rs b/src/statistics.rs index 3d3eaf7fd7..bb8cf3d303 100644 --- a/src/statistics.rs +++ b/src/statistics.rs @@ -47,8 +47,7 @@ impl State { let mut stat = Statistics { mean: 0, var: 0, - max: self.max, - min: self.min, + ptp: self.max - self.min, }; if self.count != 0 { let mean = self.m1 / self.count as i64; @@ -62,8 +61,7 @@ impl State { let mut stat = ScaledStatistics { mean: 0., std: 0., - max: self.max as f32 * scale, - min: self.min as f32 * scale, + ptp: (self.max - self.min) as f32 * scale, }; if self.count != 0 { let c = 1. / self.count as f64; @@ -78,16 +76,14 @@ impl State { #[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize)] pub struct Statistics { - pub min: i32, - pub max: i32, pub mean: i32, pub var: u64, + pub ptp: i32, } #[derive(Copy, Clone, PartialEq, Debug, Default, Serialize)] pub struct ScaledStatistics { - pub min: f32, - pub max: f32, pub mean: f32, pub std: f32, + pub ptp: f32, } From dd06b42621631c8b514a0e808de4a50aa830d592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Tue, 17 Feb 2026 15:16:21 +0000 Subject: [PATCH 07/19] pounder: rf power detectors read normalized --- src/hardware/pounder/mod.rs | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/hardware/pounder/mod.rs b/src/hardware/pounder/mod.rs index 8a4960a334..340736c208 100644 --- a/src/hardware/pounder/mod.rs +++ b/src/hardware/pounder/mod.rs @@ -468,13 +468,29 @@ impl PounderDevices { Ok(devices) } + /// Sample a auxiliary ADC channel and return the raw sample value. + pub fn sample_aux_adc_raw( + &mut self, + channel: Channel, + ) -> Result { + match channel { + Channel::In0 => self.aux_adc.0.read_raw().or(Err(Error::Adc)), + Channel::In1 => self.aux_adc.1.read_raw().or(Err(Error::Adc)), + _ => return Err(Error::InvalidChannel), + } + } + /// Sample one of the two auxiliary ADC channels associated with the respective RF input channel. pub fn sample_aux_adc(&mut self, channel: Channel) -> Result { let adc_scale = match channel { - Channel::In0 => self.aux_adc.0.read_normalized().unwrap(), - Channel::In1 => self.aux_adc.1.read_normalized().unwrap(), + Channel::In0 => { + self.aux_adc.0.read_normalized().or(Err(Error::Adc)) + } + Channel::In1 => { + self.aux_adc.1.read_normalized().or(Err(Error::Adc)) + } _ => return Err(Error::InvalidChannel), - }; + }?; // Convert analog percentage to voltage. Note that the ADC uses an external 2.048V analog // reference. @@ -623,10 +639,10 @@ impl PounderDevices { /// The sampled voltage of the specified channel. fn sample_converter(&mut self, channel: Channel) -> Result { let adc_scale = match channel { - Channel::In0 => self.pwr.0.read_normalized().unwrap(), - Channel::In1 => self.pwr.1.read_normalized().unwrap(), + Channel::In0 => self.pwr.0.read_normalized().or(Err(Error::Adc)), + Channel::In1 => self.pwr.1.read_normalized().or(Err(Error::Adc)), _ => return Err(Error::InvalidChannel), - }; + }?; // Convert analog percentage to voltage. Note that the ADC uses an external 2.048V analog // reference. From c418e3ebb7f353affe7d71b7ca70339666c14941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Tue, 17 Feb 2026 20:59:42 +0000 Subject: [PATCH 08/19] ad9959: wrapping --- ad9959/src/lib.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ad9959/src/lib.rs b/ad9959/src/lib.rs index 4d89b58658..f649b7cc96 100644 --- a/ad9959/src/lib.rs +++ b/ad9959/src/lib.rs @@ -1,5 +1,7 @@ #![no_std] +use core::num::Wrapping; + use arbitrary_int::{Number, u2, u3, u4, u5, u10, u14, u24}; use bitbybit::{bitenum, bitfield}; use embedded_hal::{blocking::delay::DelayUs, digital::v2::OutputPin}; @@ -521,8 +523,8 @@ impl ProfileSerializer { pub fn push( &mut self, channels: Channel, - ftw: Option, - pow: Option, + ftw: Option>, + pow: Option>, // a-i v2: i14 acr: Option, ) { self.push_write( @@ -534,10 +536,10 @@ impl ProfileSerializer { .to_be_bytes(), ); if let Some(ftw) = ftw { - self.push_write(Address::CFTW0, &ftw.to_be_bytes()); + self.push_write(Address::CFTW0, &ftw.0.to_be_bytes()); } if let Some(pow) = pow { - self.push_write(Address::CPOW0, &pow.value().to_be_bytes()); + self.push_write(Address::CPOW0, &pow.0.value().to_be_bytes()); } if let Some(acr) = acr { self.push_write(Address::ACR, &acr.raw_value().to_be_bytes()); From 15da0e870ab09c5215b8283f0e2442e55f37ec39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Wed, 18 Feb 2026 15:17:49 +0000 Subject: [PATCH 09/19] statistics: saturate --- src/statistics.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/statistics.rs b/src/statistics.rs index bb8cf3d303..f40a195262 100644 --- a/src/statistics.rs +++ b/src/statistics.rs @@ -35,7 +35,7 @@ impl State { if self.count == 0 { self.x0 = x; } - let x64 = (x - self.x0) as i64; + let x64 = x.saturating_sub(self.x0) as i64; self.count += 1; self.m1 += x64; self.m2 += (x64 * x64) as u64; @@ -47,11 +47,11 @@ impl State { let mut stat = Statistics { mean: 0, var: 0, - ptp: self.max - self.min, + ptp: self.max.saturating_sub(self.min), }; if self.count != 0 { let mean = self.m1 / self.count as i64; - stat.mean = mean as i32 + self.x0; + stat.mean = (mean as i32).saturating_add(self.x0); stat.var = (self.m2 / self.count as u64) - (mean * mean) as u64; } stat @@ -61,7 +61,7 @@ impl State { let mut stat = ScaledStatistics { mean: 0., std: 0., - ptp: (self.max - self.min) as f32 * scale, + ptp: self.max.saturating_sub(self.min) as f32 * scale, }; if self.count != 0 { let c = 1. / self.count as f64; From 3122dc63a534c1db397ca107c5a90955bc70cdd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Sat, 21 Feb 2026 08:18:08 +0000 Subject: [PATCH 10/19] fls: redesign * split DSP flow * instead of DDS-time LO use fixed CPU-time LO and apply correction on phase later * prep for better lowpass (WIP) --- Cargo.toml | 1 + src/bin/fls.rs | 1256 +++++++++++++-------------------------------- src/fls.rs | 307 +++++++++++ src/lib.rs | 2 + src/mpll.rs | 4 +- stream/src/lib.rs | 5 +- 6 files changed, 684 insertions(+), 891 deletions(-) create mode 100644 src/fls.rs diff --git a/Cargo.toml b/Cargo.toml index c3321d8a79..7d1735b09f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,6 +100,7 @@ urukul = { version = "0.1.1", path = "urukul" } ad9912 = { version = "0.1.1", path = "ad9912" } bytemuck = { version = "1.21.0", features = [ "zeroable_maybe_uninit", + "min_const_generics", "derive", ] } num-complex = { version = "0.4", features = [ diff --git a/src/bin/fls.rs b/src/bin/fls.rs index 9a2daedd87..584ad68154 100644 --- a/src/bin/fls.rs +++ b/src/bin/fls.rs @@ -80,255 +80,107 @@ use core::num::Wrapping as W; use ad9959::Acr; -use arbitrary_int::{u14, u24}; -use dsp_fixedpoint::{Q32, W32}; -use dsp_process::{Process, SplitProcess}; -use idsp::{ - Build, Clamp, Complex, Lockin, Lowpass, LowpassState, PLL, Unwrapper, - iir::{ - Biquad, BiquadClamp, DirectForm1, DirectForm1Dither, - pid::{Pid, Units}, - repr::BiquadRepr, - }, -}; +use arbitrary_int::u24; +use dsp_fixedpoint::W32; +use dsp_process::SplitProcess; +use idsp::{Complex, PLL, Unwrapper}; use miniconf::Tree; -use num_traits::AsPrimitive; -use platform::NetSettings; -use serde::{Deserialize, Serialize}; -use stabilizer::{ - convert::{DacCode, Gain}, - statistics, -}; - -/// Sample and batch period configuration. -/// Note that both `SAMPLE_TICKS_LOG2` and `BATCH_SIZE_LOG2` are implicitly used in the -/// lockin harmonic computation below. Do not change them without accounting for that. -const SAMPLE_TICKS_LOG2: u32 = 7; -const BATCH_SIZE_LOG2: usize = 3; - -/// ADC and DAC sample rate in timer cycles. One timer cycle at 100 MHz is 10 ns. -const SAMPLE_TICKS: u32 = 1 << SAMPLE_TICKS_LOG2; // 1.28 µs - -/// ADC/DAC Samples per batch. The [app::process] routine is invoked once per batch period -/// and has access to the two (both channels) filled buffers of ADC samples from the -/// previous batch period and to the two to-be-filled buffers of DAC samples that will -/// be emitted in the next batch period. -const BATCH_SIZE: usize = 1 << BATCH_SIZE_LOG2; - -// Delta FTW between the two DDS: DF -// Timestamp counter wrap period in DDS clock cycles: -// 1 << (2 (dds SYNC prescaler) + 2 (timer prescaler) + 16 (timer counter width)) -// Lockin demodulation period in DDS clock cycles: (1 << 32) / DF -// Counter capture period in samples (also batch size): 1 << 3 -// -// DDS clock interval t_dds = 2 ns -// Lockin period -// t_lo = 1/(f_b - f_a) = (1 << 32)*t_dds/DF -// SYNC interval -// t_sync = t_dds*psc_dds*psc_tim -// CPU timer clock interval: -// t_cpu = 10 ns -// Batch interval: -// t_batch = t_cpu*128*8 -// Timestamper increment: -// dt_sync = t_batch/t_sync = t_cpu*128*8/(t_dds*4*4) = t_cpu/t_dds*64 -// Sample interval -// t_sample = t_batch/n_batch = dt_sync*t_sync/n_batch = dt_sync*t_dds*2 -// Sample phase increment -// dp_sample = t_sample/t_lo*(1 << 32) = dt_sync*2*DF -// Ratio between sample phase increment and timestamper increment -// harmonic_sample = dp_sample/dt_sync = DF << 1 - -// Scaling factor (harmonic) to convert PLL frequency to lockin LO frequency. -const MULT_SHIFT: u32 = 2 + 2 + 14 - BATCH_SIZE_LOG2 as u32; - -// Phase scale for fine phase offset, such that 1 is one DDS LSB. -const PHASE_SCALE_SHIFT: u32 = 12; - -// Default modulation/demodulation frequency for characterization. -// High CTZ has fewest DDS phase truncation spurs. Near 160 MHz. -const F_DEMOD: u32 = 0x5200_0000; +use platform::{AppSettings, NetSettings}; +use serde::Serialize; +use stabilizer::{convert::Gain, fls, statistics}; -#[derive(Clone, Debug, Tree)] -pub struct BiquadReprTree -where - T: 'static + Clamp + Copy, - Y: 'static + Copy, - f32: AsPrimitive + AsPrimitive, - BiquadClamp: Default, - BiquadRepr: Default, - Pid: Default + Build, Context = Units>, -{ - // Order matters - /// Biquad representation type - #[tree(rename="typ", typ="&str", with=miniconf::str_leaf, defer=self.repr)] - _typ: (), - /// Biquad parameters - /// Biquad representation subtree access - repr: BiquadRepr, - /// Update trigger. TODO: Needs explicit trigger for serial-settings - #[tree(rename="update", with=biquad_update, defer=*self)] - _update: (), - /// Built raw IIR - #[tree(skip)] - iir: BiquadClamp, - #[tree(skip)] - units: Units, +#[derive(Clone, Debug, Tree, Default)] +#[tree(meta(doc, typename))] +pub struct Settings { + fls: Fls, + net: NetSettings, } -mod biquad_update { - use super::BiquadReprTree; - use idsp::iir::pid::Units; - use idsp::iir::{BiquadClamp, pid::Pid, repr::BiquadRepr}; - use idsp::{Build, Clamp}; - use miniconf::{Keys, SerdeError, leaf}; - pub use miniconf::{ - deny::{mut_any_by_key, ref_any_by_key}, - leaf::SCHEMA, - }; - use num_traits::AsPrimitive; - use serde::{Deserialize, Deserializer, Serializer}; - - pub fn serialize_by_key( - _value: &BiquadReprTree, - keys: impl Keys, - ser: S, - ) -> Result> - where - S: Serializer, - T: 'static + Clamp + Copy, - Y: 'static + Copy, - f32: AsPrimitive + AsPrimitive, - BiquadRepr: Default, - BiquadClamp: Default, - Pid: Build, Context = Units>, - { - leaf::serialize_by_key(&(), keys, ser) +impl AppSettings for Settings { + fn new(net: NetSettings) -> Self { + Self { + net, + fls: Default::default(), + } } - pub fn deserialize_by_key<'de, D, T, Y>( - value: &mut BiquadReprTree, - keys: impl Keys, - de: D, - ) -> Result<(), SerdeError> - where - D: Deserializer<'de>, - T: 'static + Clamp + Copy, - Y: 'static + Copy, - f32: AsPrimitive + AsPrimitive, - BiquadRepr: Default, - BiquadClamp: Default, - Pid: Build, Context = Units>, - { - leaf::deserialize_by_key(&mut (), keys, de)?; - value.iir = value.repr.build(&value.units); - Ok(()) + fn net(&self) -> &NetSettings { + &self.net } +} - #[allow(clippy::extra_unused_type_parameters)] - pub fn probe_by_key<'de, T, D>( - keys: impl Keys, - de: D, - ) -> Result<(), SerdeError> - where - T: Deserialize<'de>, - D: Deserializer<'de>, - { - leaf::probe_by_key::<'_, T, _>(keys, de) +impl serial_settings::Settings for Settings { + fn reset(&mut self) { + *self = Self { + fls: Default::default(), + net: NetSettings::new(self.net.mac), + }; } } -impl Default for BiquadReprTree -where - T: 'static + Clamp + Copy, - Y: 'static + Copy, - f32: AsPrimitive + AsPrimitive, - BiquadClamp: Default, - Pid: Build, Context = Units>, - BiquadRepr: Default, -{ +/// Settings structure for the application. +/// All fields in this structure are available through MQTT and can be configured at runtime. +#[derive(Clone, Debug, Tree)] +#[tree(meta(doc, typename))] +pub struct Fls { + /// External 100 MHz reference + ext_clk: bool, + + /// Channel-specific settings. + channel: [Channel; 2], + + /// Lockin demodulation oscillator PLL bandwidth. + /// This PLL reconstructs the DDS SYNC clock output on the CPU clock timescale. + /// + /// TODO: settle pll and lockin settings into design after confirming optimal choice + pll: f32, + + /// Telemetry output period in seconds. + telemetry_period: f32, + + /// Specifies the target for data streaming. + #[tree(with=miniconf::leaf)] + stream: stream::Target, + + /// Activate settings + /// + /// If `true` each `channel` settings change immediately results + /// in activation. + activate: bool, +} + +impl Default for Fls { fn default() -> Self { Self { - _typ: (), - repr: BiquadRepr::Raw(Biquad::IDENTITY.into()), - _update: (), - iir: Biquad::IDENTITY.into(), - units: Default::default(), + ext_clk: false, + channel: Default::default(), + pll: 2.0, + telemetry_period: 10., + stream: Default::default(), + activate: true, } } } -#[derive(Clone, Debug, Deserialize, Serialize, Tree)] -struct DdsSettings { - /// RF output (modulation) or input (demodulation) offset frequency tuning word. +#[derive(Clone, Debug, miniconf::Tree)] +struct Dds { + /// Frequency tuning word (i32) /// The DDS sample clock is nominally 500 MHz. - /// - /// # Value - /// Modulation/demodulation frequency tuning word (32 bit). - /// Range [0, 0xffff_ffff] - /// - /// # Default - /// A `0x5200_0000` tuning word corresponds to close to 160 MHz. - freq: u32, - /// Modulation/demodulation RF attenuation. - /// - /// # Value - /// Attenuation in dB, Range [0, 31.5] - /// - /// # Default - /// 6 dB output attenuation, 31.5 dB input attenuation - #[tree(with=validate_att)] - att: f32, - /// Modulation/demodulation phase offset. - /// - /// # Value - /// Phase offset in machine units (16 bit). - /// - /// # Default - /// 0 - #[tree(with=miniconf::leaf)] - phase: u14, -} + frequency: W, -mod validate_att { - use miniconf::ValueError; - pub use miniconf::{ - Keys, SerdeError, - deny::mut_any_by_key, - leaf::{self, SCHEMA, probe_by_key, ref_any_by_key, serialize_by_key}, - }; - use serde::Deserializer; - - pub fn deserialize_by_key<'de, D: Deserializer<'de>>( - value: &mut f32, - keys: impl Keys, - de: D, - ) -> Result<(), SerdeError> { - let mut att = *value; - leaf::deserialize_by_key(&mut att, keys, de)?; - if stabilizer::convert::att_is_valid(att) { - *value = att; - Ok(()) - } else { - Err(ValueError::Access("Attenuation out of range (0..=31.5 dB)") - .into()) - } - } + /// Attenuation (dB) + /// Range [0, 31.5] + attenuation: f32, } #[derive(Clone, Debug, Tree)] -struct ChannelSettings { +struct Channel { /// Input (demodulation) DDS settings - /// Feedback to stabilize the RF input phase is applied to the RF output - /// on top of the output frequency and phase. /// - /// For the demodulation this is the total (DDS **minus** Lockin, i.e. lower sideband) - /// demodulation frequency. If the modulation AOM is passed twice at +1 order, - /// `input/freq` should be twice `output/freq`. - input: DdsSettings, - /// Output (modulation) DDS settings - output: DdsSettings, - /// Demodulation amplitude control register. + /// The demodulation frequency must include the second LO offset + demodulate: Dds, + + /// Demodulation LO DDS amplitude control register. /// /// # Value /// AD9959 amplitude control register (24 bits, see datasheet) @@ -336,354 +188,144 @@ struct ChannelSettings { /// # Default /// 0 for full scale amplitude and multiplier disable #[tree(with=miniconf::leaf)] - amp: u24, - /// Lockin lowpass time constant. The lowpass is a cascade of one second order IIR - /// filters, 12 dB/octave. - /// This needs to be high enough to suppress the unwanted demodulation components - /// and harmonics but as low as possible to maximize bandwidth. Many demodulation - /// components and harmonics are also suppressed by the zeros of the batch size - /// moving average FIR filter and judicious choice of `lockin_freq`. - /// - /// TODO: settle pll and lockin settings into design after confirming optimal choice - /// - /// # Default - /// `lockin_k = [0x200_0000, -0x2000_0000]` - #[tree(skip)] // TODO - lockin_k: Lockin>, - /// Minimum demodulated signal power to enable feedback. - /// Note that this is RMS and that the signal peak must not clip. - /// - /// # Value - /// `log2` of the signal power relative to full scale. Range: `[-63..0]` - /// - /// # Default - /// `min_power = -24` corresponding to about -69 dBFS. - min_power: i32, - /// Clear the phase unwrap tracking counters once. - /// To make this setting edge-sensitive, after setting it to `true`, - /// it must be reset to `false` by the user before setting any other settings. - clear: bool, - /// Scaling factor of the unwrapped phase and fine rational offset. - /// The phase scaling is located after the phase unwrapping before the feedback - /// IIR filter. - /// - /// FIXME: doc rational offset - /// - /// # Value - /// `[[phase_factor, phase_shr], [time_factor, time_shr]]` - /// - /// # Default - /// `phase_scale = [[1, 16], [0, 0]]`: - /// clamped range: ±33 k turn (tracked range is ±2 G turn) - /// quantization: 1.5 µ turn, 0.1 Hz - #[tree(with=phase_scale, defer=*self)] - phase_scale: [[i32; 2]; 2], - /// Feedback IIR filter settings. The filter input is phase, the output is frequency. - /// - /// # Default - /// A proportional gain=-1 filter. - iir: BiquadReprTree, i32>, - /// Phase offset feedback gain. - /// Phase feedback is a proportional bypass of the unwrapper, the IIR - /// (including its input and output scaling) and the frequency feedback path. - /// The phase offset gain is `pow_gain/(1 << 13) rad/rad`. - /// - /// # Value - /// Integer scaled phase feedback gain. Range: `[-0x2000, 0x2000]` + demodulate_acr: u24, + + /// AFE gain + #[tree(with=miniconf::leaf)] + afe_gain: Gain, + + /// Output (modulation) DDS settings /// - /// # Default - /// 0 for no phase feedback - pow_gain: i16, - /// Allow digital input to hold + /// Feedback to stabilize the RF input phase is applied to the RF output + /// on top of the output frequency + modulate: Dds, + + /// Allow digital input channel to clear phase/frequency IIR and hold amplitude IIR hold_en: bool, - /// Amplitude IIR filter. The filter input is squared magnitude, the output is DDS amplitude. - /// - /// # Default - /// No feedback - iir_amp: BiquadReprTree, -} -const DDS_LSB_PER_HZ: f32 = (1i64 << 32) as f32 - / stabilizer::design_parameters::DDS_SYSTEM_CLK.to_Hz() as f32; + /// Channel DSP components + dsp: fls::Channel, +} -impl Default for ChannelSettings { +impl Default for Channel { fn default() -> Self { - let mut iir_prop = BiquadClamp::from(Biquad::IDENTITY); - iir_prop.coeff.ba[0] *= -1; - iir_prop.min = -0x4_0000; - iir_prop.max = 0x4_0000; - let mut iir_amp = BiquadClamp::default(); - iir_amp.u = 0x3ff as _; - iir_amp.min = 0.0; - iir_amp.max = 0x3ff as _; - let mut s = Self { - input: DdsSettings { - freq: F_DEMOD, - att: 31.5, - phase: u14::new(0), - }, - output: DdsSettings { - freq: F_DEMOD, - att: 6.0, - phase: u14::new(0), + // Default modulation/demodulation frequency for characterization. + // High CTZ has fewest DDS phase truncation spurs. Near 160 MHz. + const F_DEMOD: W = W(0x5200_0000); + + Self { + demodulate: Dds { + frequency: F_DEMOD + W(0x20_0000), // LSB + attenuation: 31.5, }, - lockin_k: Lockin(Lowpass([-(i32::MIN >> 6), i32::MIN >> 2])), - amp: u24::new(0), - min_power: -24, - clear: true, - phase_scale: [[1, 16], [0, 0]], - iir: BiquadReprTree { - repr: BiquadRepr::Raw(iir_prop.clone()), - iir: iir_prop.clone(), - units: Units { - t: stabilizer::design_parameters::TIMER_PERIOD - * (SAMPLE_TICKS * BATCH_SIZE as u32) as f32, - y: DDS_LSB_PER_HZ.recip(), - x: 1.0, - }, - ..Default::default() + modulate: Dds { + frequency: F_DEMOD, + attenuation: 6.0, }, - pow_gain: 0, + demodulate_acr: u24::new(0), hold_en: false, - iir_amp: BiquadReprTree { - repr: BiquadRepr::Raw(iir_amp.clone()), - iir: iir_amp.clone(), - units: Units { - t: 10e-3, - x: iir_amp.max.recip(), - y: iir_amp.max.recip(), - }, - ..Default::default() - }, - }; - s.update_phase_scale(); - s - } -} - -mod phase_scale { - use super::ChannelSettings; - pub use miniconf::{ - Keys, SerdeError, - deny::{mut_any_by_key, ref_any_by_key}, - leaf::{self, SCHEMA}, - }; - use serde::{Deserialize, Deserializer, Serializer}; - - pub fn serialize_by_key( - value: &ChannelSettings, - keys: impl Keys, - ser: S, - ) -> Result> { - leaf::serialize_by_key(&value.phase_scale, keys, ser) - } - - pub fn deserialize_by_key<'de, D: Deserializer<'de>>( - value: &mut ChannelSettings, - keys: impl Keys, - de: D, - ) -> Result<(), SerdeError> { - leaf::deserialize_by_key(&mut value.phase_scale, keys, de)?; - value.update_phase_scale(); - Ok(()) - } - - pub fn probe_by_key<'de, T: Deserialize<'de>, D: Deserializer<'de>>( - keys: impl Keys, - de: D, - ) -> Result<(), SerdeError> { - leaf::probe_by_key::<'de, T, _>(keys, de) - } -} - -impl ChannelSettings { - fn update_phase_scale(&mut self) { - // Units: [x] = turns, [y] = Hz - // TODO: verify - self.iir.units.x = - ((self.phase_scale[0][0] << (32 - self.phase_scale[0][1])) as f32) - .recip(); + afe_gain: Gain::G5, + dsp: Default::default(), + } } } -/// Settings structure for the application. -/// All fields in this structure are available through MQTT and can be configured at runtime. -#[derive(Clone, Debug, Tree)] -pub struct Fls { - /// Channel-specific settings. - ch: [ChannelSettings; 2], - /// External reference - /// - /// # Value - /// `true` for external 100 MHz reference input selected, - /// `false` for internal 100 MHz XO enabled and selected - /// - /// # Default - /// `false` - ext_clk: bool, - /// Lockin local oscillator frequency tuning word. Common to both demodulation/input - /// channels. - /// - /// The demodulation DDS frequency tuning word - /// is `/ch/+/input/freq + lockin_freq*0x8000` (lower sideband). - /// - /// TODO: settle pll and lockin settings into design after confirming optimal choice - /// - /// # Default - /// `0x40` corresponding to 244 kHz. 5/8 Nyquist. - lockin_freq: u32, - /// Lockin demodulation oscillator PLL bandwidth. - /// This PLL reconstructs the DDS SYNC clock output on the CPU clock timescale. - /// - /// TODO: settle pll and lockin settings into design after confirming optimal choice - /// - /// # Default - /// `/pll_k = 0x4_0000` corresponds to to a time constant of about 0.4 s. - #[tree(with=miniconf::leaf)] - pll_k: W32<32>, - /// Telemetry output period in seconds - /// - /// # Default - /// 2 second interval - telemetry_period: u16, - /// Target for data streaming - /// - /// # Default - /// Streaming disabled - #[tree(with=miniconf::leaf)] - stream: stream::Target, +#[derive(Debug, Clone)] +struct ChannelConfig { + dsp: fls::ChannelConfig, + hold_en: bool, + modulate_frequency: W, } -impl Default for Fls { - fn default() -> Self { - Self { - ch: Default::default(), - ext_clk: false, - lockin_freq: 0x40, - pll_k: W32::new(W(0x4_0000)), - telemetry_period: 10, - stream: Default::default(), - } - } +#[derive(Debug, Clone)] +pub struct Config { + channel: [ChannelConfig; 2], + pll: W32<32>, } -#[derive(Clone, Debug, Tree, Default)] -pub struct Settings { - pub fls: Fls, - - pub net: NetSettings, +#[derive(Default, Debug, Clone)] +pub struct State { + channel: [fls::ChannelState; 2], + pll: PLL, + pll_time: Unwrapper, + phase: [statistics::State; 2], + power: [statistics::State; 2], } -impl platform::AppSettings for Settings { - fn new(net: NetSettings) -> Self { - Self { - net, - fls: Default::default(), +impl Fls { + fn build(&self) -> Config { + Config { + channel: self.channel.each_ref().map(|c| ChannelConfig { + dsp: c.dsp.build(), + hold_en: c.hold_en, + modulate_frequency: c.modulate.frequency, + }), + pll: W32::new(W(0x4_0000)), // TODO } } - - fn net(&self) -> &NetSettings { - &self.net - } } -impl serial_settings::Settings for Settings { - fn reset(&mut self) { - *self = Self { - fls: Default::default(), - net: NetSettings::new(self.net.mac), - } - } +/// Stream data format. +#[derive(Clone, Copy, Debug, Default, bytemuck::Zeroable, bytemuck::Pod)] +#[repr(C)] +pub struct Stream { + pll: W, + ch: [StreamChannel; 2], } -/// Stream data format. -#[derive( - Clone, Copy, Debug, Default, Serialize, bytemuck::Zeroable, bytemuck::Pod, -)] +#[derive(Clone, Copy, Debug, Default, bytemuck::Zeroable, bytemuck::Pod)] #[repr(C)] -struct Stream { - /// Demodulated signal. `-1 << 31` corresponds to negative full scale. - demod: Complex, - /// Current number of phase wraps. In units of turns. - phase: [i32; 2], +pub struct StreamChannel { + /// Demodulated signal. + pub demod: Complex, + /// Current raw phase including wraps and pll correction. + pub phase: [u32; 2], /// Current frequency tuning word added to the configured modulation /// offset `mod_freq`. - delta_ftw: i32, - /// Current phase offset word applied to the modulation DDS. - delta_pow: i16, - /// Modulation DDS amplitude word - mod_amp: u16, - /// PLL time - pll: u32, + pub delta_ftw: W, } -/// Channel Telemetry +/// Channel Telemetry, all undersampled and not AA filtered #[derive(Default, Clone, Serialize)] struct ChannelTelemetry { - /// Current phase. Offset and scaled. - phase: i64, - /// Power estimate, `|demod|²` re full scale. - power_log: i32, - /// - // power: i32, - /// Auxiliary front panel ADC input values, undersmpled + /// Current phase. + phase_raw: i64, + /// Demodulated phase statistics in turns + phase: statistics::ScaledStatistics, + /// Demodulated power (amplitude squared) statistics re full scale + power: statistics::ScaledStatistics, + /// Auxiliary front panel ADC input values, V aux_adc: f32, - mod_amp: u16, - /// Number of sampler where digital input signal was high. - holds: u32, + /// Modulation amplitude re full scale + mod_amp: f32, + /// Number of cycles where digital input held amplitude and + /// cleared the frequency servo. + holds: W, /// Number of potential phase slips where the absolute /// phase difference between successive samples is larger than π/2. - slips: u32, - /// Counter for the number of samples with low power. - blanks: u32, -} - -#[derive(Default, Clone)] -pub struct Telemetry { - pll_time: i64, - ch: [ChannelTelemetry; 2], - stats: [statistics::State; 2], + slips: W, + /// Counter for the number of samples with low power where the + /// frequency servo was cleared. + blanks: W, + /// Total RF power in dBm as reported by the RF detector. + rf_power: f32, } /// Telemetry structure. /// This structure is published via MQTT at the `telemetry_interval` configured in /// [Settings]. -/// There is no dedicated AA filtering for telemetry data (except for `stats`), -/// it is just decimated by the telemetry interval. Use streaming for full -/// bandwidth data. #[derive(Default, Clone, Serialize)] pub struct CookedTelemetry { /// PLL time - /// DDS PLL time as seen by CPU (sample) clock. + /// DDS PLL time as seen by CPU (sample) clock. Units of `1ns / (1 <<11)`. /// Settles increments of approximately `0x140_0000`. pll_time: i64, - /// Statistics of scaled (settings.phase_scale) phase including wraps. - /// Phase statistics state. Each message corresponds to the statistics of the - /// phase data since the last message. - phase: [statistics::ScaledStatistics; 2], - /// RF power in dBm as reported by the RF detector and ADC. Functionality - /// limited. - rf_power: [f32; 2], - /// Raw (binary) channel telemetry, mostly "stateful" - raw: [ChannelTelemetry; 2], - /// Channel frequency estimate (PI counter between telemetry messages) - /// TODO: deprecate - ch_freq: [f64; 2], + /// Channel telemetry, mostly "stateful" and undersampled + channel: [ChannelTelemetry; 2], /// Pounder board temperature - temp: f32, -} - -#[derive(Clone, Default)] -pub struct ChannelState { - lockin: [LowpassState<2>; 2], - x0: W, - t0: W, - t: W, - y: W, - unwrapper: Unwrapper, - iir: DirectForm1Dither, - iir_amp: DirectForm1, - hold: bool, + pounder_temp: f32, + /// CPU temperature, + cpu_temp: f32, } #[cfg(not(target_os = "none"))] @@ -704,25 +346,20 @@ fn main() { #[cfg(target_os = "none")] #[cfg_attr(target_os = "none", rtic::app(device = stabilizer::hardware::hal::stm32, peripherals = true, dispatchers=[DCMI, JPEG, LTDC, SDMMC]))] mod app { - use arbitrary_int::u10; use core::sync::atomic::{Ordering, fence}; + use dsp_fixedpoint::P32; + use dsp_process::Process; use fugit::ExtU32 as _; - use num_traits::ConstZero; use rtic_monotonics::Monotonic; use stabilizer::hardware::{ - self, - DigitalInput0, - DigitalInput1, - SerialTerminal, - SystemTimer, - Systick, - UsbDevice, + self, DigitalInput0, DigitalInput1, Pgia, SerialTerminal, SystemTimer, + Systick, UsbDevice, adc::{Adc0Input, Adc1Input}, + cpu_temp_sensor::CpuTempSensor, dac::{Dac0Output, Dac1Output}, hal, net::{NetworkState, NetworkUsers}, - // afe::Gain, pounder::{ Channel, PounderDevices, dds_output::DdsOutput, timestamp::Timestamper, @@ -738,12 +375,11 @@ mod app { struct Shared { usb: UsbDevice, network: NetworkUsers, - active_settings: Fls, settings: Settings, - telemetry: Telemetry, + config: Config, + state: State, dds_output: DdsOutput, pounder: PounderDevices, - state: [ChannelState; 2], } #[local] @@ -755,9 +391,8 @@ mod app { dacs: (Dac0Output, Dac1Output), generator: FrameGenerator, timestamper: Timestamper, - stream: [Stream; 2], - tele_state: [i64; 3], - pll: PLL, + afes: [Pgia; 2], + cpu_temp_sensor: CpuTempSensor, } #[init] @@ -769,8 +404,8 @@ mod app { c.core, c.device, clock, - BATCH_SIZE, - SAMPLE_TICKS, + fls::BATCH_SIZE, + 1 << fls::SAMPLE_TICKS_E, ); let mut network = NetworkUsers::new( @@ -782,11 +417,7 @@ mod app { carrier.metadata, ); - let generator = network.configure_streaming(stream::Format::Fls); - - // ADC0 full scale 5V - carrier.afes[0].set_gain(Gain::G2); - carrier.afes[1].set_gain(Gain::G2); + let generator = network.configure_streaming(stream::Format::Fls2); let hardware::setup::Mezzanine::Pounder(mut pounder) = mezzanine else { panic!("Missing Pounder Mezzanine"); @@ -802,8 +433,7 @@ mod app { let shared = Shared { usb: carrier.usb, network, - telemetry: Telemetry::default(), - active_settings: carrier.settings.fls.clone(), + config: carrier.settings.fls.build(), settings: carrier.settings, dds_output: pounder.dds_output, pounder: pounder.pounder, @@ -818,9 +448,8 @@ mod app { dacs: carrier.dacs, generator, timestamper: pounder.timestamper, - stream: Default::default(), - tele_state: [0; 3], - pll: PLL::default(), + afes: carrier.afes, + cpu_temp_sensor: carrier.temperature_sensor, }; settings_update::spawn().unwrap(); @@ -845,7 +474,7 @@ mod app { /// /// This is an implementation of fiber length stabilization using super-heterodyne /// (pounder + lockin) and digital feedback to a DDS. - #[task(binds = DMA1_STR4, local=[timestamper, adcs, dacs, generator, digital_inputs, stream, pll], shared = [active_settings, state, telemetry, dds_output], priority = 3)] + #[task(binds = DMA1_STR4, local=[timestamper, adcs, dacs, generator, digital_inputs], shared = [config, state, dds_output], priority = 3)] #[unsafe(link_section = ".itcm.process")] fn process(c: process::Context) { let process::LocalResources { @@ -854,220 +483,102 @@ mod app { digital_inputs, timestamper, generator, - stream, - pll, .. } = c.local; - // A counter running at a fourth of the DDS SYNC interval is captured by - // the overflow of a timer synchronized to the sampling timer (locked to the - // CPU clock and the other CPU timer clocks). - // Captured timestamps are about 0x140 counts apart between batches. - // They determine the phase and period of the DDS clock (driving the counter) - // in terms of the CPU clock (driving the capture). - // Discard double captures (overcaptures) and extrapolate. - // Extrapolate on no capture (undercapture). - let timestamp = timestamper - .latest_timestamp() - .unwrap_or(None) - .map(|t| W(((t as u32) << 16) as i32)); - - ( - c.shared.state, - c.shared.active_settings, - c.shared.dds_output, - c.shared.telemetry, - ) - .lock(|state, settings, dds_output, telemetry| { - // Reconstruct frequency and phase using a lowpass that is aware of phase and frequency - // wraps. - let mut accu = settings.pll_k.process(pll, timestamp); - // TODO: implement clear - stream[0].pll = accu.step.0 as _; - stream[1].pll = accu.state.0 as _; - telemetry.pll_time = - telemetry.pll_time.wrapping_add(pll.frequency().0 as _); - - let mut demod = [Complex::>::default(); BATCH_SIZE]; - accu.state <<= BATCH_SIZE_LOG2 as usize; - accu.state *= W(settings.lockin_freq as i32); - accu.step *= W(settings.lockin_freq as i32); - // TODO: fixed lockin_freq, const 5/16 frequency table (80 entries), then rotate each by pll phase - for (d, p) in demod.iter_mut().zip(accu) { - *d = Complex::from_angle(p); - } - - (adc0, adc1, dac0, dac1).lock(|adc0, adc1, dac0, dac1| { - fence(Ordering::SeqCst); - let adc: [&[u16; BATCH_SIZE]; 2] = [ - (**adc0).try_into().unwrap(), - (**adc1).try_into().unwrap(), - ]; - let dac: [&mut [u16; BATCH_SIZE]; 2] = [ - (*dac0).try_into().unwrap(), - (*dac1).try_into().unwrap(), - ]; - // Perform lockin demodulation of the ADC samples in the batch. - for ((((adc, dac), state), settings), stream) in adc - .into_iter() - .zip(dac.into_iter()) - .zip(state.iter_mut()) - .zip(settings.ch.iter()) - .zip(stream.iter_mut()) - { - stream.demod = adc - .iter() - .zip(dac.iter_mut()) - .zip(demod.iter()) - .map(|((a, d), p)| { - // Demodulate the ADC sample `a0` with the sample's phase `p` and - // filter it with the lowpass. - // zero(s) at fs/2 (Nyquist) by lowpass - let y = settings.lockin_k.process( - &mut state.lockin, - // 3 bit headroom for coeff sum minus one bit gain for filter - ((*a as i16 as i32) << 14, *p), - ); - // Convert quadrature demodulated output to DAC data for monitoring - *d = DacCode::from((y.im() >> 13) as i16).0; - y - }) - // Add more zeros at fs/2, fs/4, and fs/8 by rectangular window. - // Sum up all demodulated samples in the batch. Corresponds to a boxcar - // averager with sinc frequency response. The first 15 lockin harmonics end up - // in zeros of the filter. - .sum(); - } - fence(Ordering::SeqCst); - }); - let di = - [digital_inputs.0.is_high(), digital_inputs.1.is_high()]; - // TODO: pll.frequency()? - let time = pll.phase() & W(-1 << PHASE_SCALE_SHIFT); - let dtime = (time - state[0].t0) >> PHASE_SCALE_SHIFT as usize; - state[0].t0 = time; - state[1].t0 = time; + (c.shared.state, c.shared.config, c.shared.dds_output).lock( + |state, config, dds_output| { + let demod = + (adc0, adc1, dac0, dac1).lock(|adc0, adc1, dac0, dac1| { + fence(Ordering::SeqCst); + let demod = [ + config.channel[0] + .dsp + .demodulate::<{ fls::BATCH_SIZE }>( + &mut state.channel[0], + (**adc0).try_into().unwrap(), + (*dac0).try_into().unwrap(), + ), + config.channel[1] + .dsp + .demodulate::<{ fls::BATCH_SIZE }>( + &mut state.channel[1], + (**adc1).try_into().unwrap(), + (*dac1).try_into().unwrap(), + ), + ]; + fence(Ordering::SeqCst); + demod + }); + + // A counter running at a fourth of the DDS SYNC interval is captured by + // the overflow of a timer synchronized to the sampling timer (locked to the + // CPU clock and the other CPU timer clocks). + // Captured timestamps are about 0x140 counts apart between batches + // for default parameters. + // They determine the phase and period of the DDS clock (driving the counter) + // in terms of the CPU clock (driving the capture). + // Discard double captures (overcaptures) and extrapolate. + // Extrapolate on no capture (undercapture). + let timestamp = timestamper.latest_timestamp().unwrap_or(None); + let mut _accu = config.pll.process( + &mut state.pll, + timestamp.map(|t| W((t as i32) << 16)), + ); + let phase = state.pll.phase(); let mut builder = dds_output.builder(); - for ( - (((((idx, di), settings), state), telemetry), stream), - stats, - ) in [Channel::Out0, Channel::Out1] - .into_iter() - .zip(di) - .zip(settings.ch.iter_mut()) - .zip(state.iter_mut()) - .zip(telemetry.ch.iter_mut()) - .zip(stream.iter_mut()) - .zip(telemetry.stats.iter_mut()) + let hold = + [digital_inputs.0.is_high(), digital_inputs.1.is_high()]; + for (idx, ((demod, hold), (config, state))) in + [Channel::Out0, Channel::Out1].into_iter().zip( + demod.into_iter().zip(hold).zip( + config.channel.iter().zip(state.channel.iter_mut()), + ), + ) { - state.hold = settings.hold_en && di; - if state.hold { - telemetry.holds = telemetry.holds.wrapping_add(1); - } - - if settings.clear { - state.unwrapper = Unwrapper::default(); - state.t = W(0); - state.y = W(0); - settings.clear = false; - } - - let power = stream.demod.log2(); - telemetry.power_log = power; - let blank = power < settings.min_power; - - if blank { - telemetry.blanks = telemetry.blanks.wrapping_add(1); - } - - // Perform unwrapping, phase scaling, IIR filtering and FTW scaling. - let (delta_ftw, delta_pow) = if blank || state.hold { - // TODO: Unclear whether feeding zero error into the IIR or holding its output - // is more correct. Also unclear what the frequency and phase should do. - // telemetry.ch[0].dphase = 0; - (0, stream.delta_pow) - } else { - let phase = stream.demod.arg(); - let dphase = phase - state.x0; - state.x0 = phase; - - // |dphi| > pi/2 indicates a possible undetected phase slip. - // Neither a necessary nor a sufficient condition though. - if dphase + W(1 << 30) < W::ZERO { - telemetry.slips = telemetry.slips.wrapping_add(1); - } - - // Scale, offset and unwrap phase - state.t += - dtime.0 as i64 * settings.phase_scale[1][0] as i64; - state.y += - dphase.0 as i64 * settings.phase_scale[0][0] as i64; - state.unwrapper.process( - ((state.y >> settings.phase_scale[0][1] as usize).0 - as i32) - .wrapping_add( - (state.t - >> settings.phase_scale[1][1] as usize) - .0 - as i32, - ), - ); - - stream.phase = bytemuck::cast(state.unwrapper.y); - telemetry.phase = state.unwrapper.y; - let phase_err = Clamp::clamp( - state.unwrapper.y, - -i32::MAX as _, - i32::MAX as _, - ) as _; - - stats.update(phase_err); - // TODO; unchecked_shr - let delta_ftw = - settings.iir.iir.process(&mut state.iir, phase_err); - let delta_pow = ((phase_err >> 16) - .wrapping_mul(settings.pow_gain as _) - >> 16) as _; - (delta_ftw, delta_pow) - }; - stream.delta_ftw = delta_ftw; - stream.delta_pow = delta_pow; // note the u14 wrap below - - // let power = (((stream.demod.re as i64).pow(2) - // + (stream.demod.im as i64).pow(2)) - // >> 32) as i32; - + state.hold = hold && config.hold_en; + config.dsp.update(state, phase, demod); builder.push( idx.into(), - Some(settings.output.freq.wrapping_add(delta_ftw as _)), - Some(u14::new( - settings - .output - .phase - .value() - .wrapping_add(delta_pow as _) - & u14::MASK, - )), + Some( + config.modulate_frequency + W(state.phase.xy.y0()), + ), + None, Some( Acr::DEFAULT - .with_asf(u10::new(Clamp::clamp( - telemetry.mod_amp, - 0, - 0x3ff, - ))) - .with_multiplier(true), + .with_multiplier(true) + .with_asf(state.mod_amp), ), ); } dds_output.write(builder); - const N: usize = core::mem::size_of::<[Stream; 2]>(); + state.pll_time.process(state.pll.phase().0); + state.phase[0].update(state.channel[0].phase.xy.x0()); + state.phase[1].update(state.channel[1].phase.xy.x0()); + state.power[0].update(state.channel[0].power.inner as _); + state.power[1].update(state.channel[1].power.inner as _); + generator.add(|buf| { - buf[..N].copy_from_slice(bytemuck::cast_slice(stream)); + const N: usize = core::mem::size_of::(); + buf[..N].copy_from_slice(bytemuck::cast_slice( + bytemuck::bytes_of(&Stream { + pll: state.pll.phase(), + ch: core::array::from_fn(|i| { + let s = &state.channel[i]; + StreamChannel { + demod: demod[i], + phase: bytemuck::cast(s.unwrap.y), + delta_ftw: W(s.phase.xy.y0()), + } + }), + }), + )); N }); - }); + }, + ); } #[idle(shared=[network, usb, settings])] @@ -1093,200 +604,169 @@ mod app { } } - #[task(priority = 1, shared=[network, settings, dds_output, pounder, active_settings])] + #[task(priority = 1, shared=[network, settings, dds_output, pounder, config], local=[afes])] async fn settings_update(mut c: settings_update::Context) { c.shared.settings.lock(|settings| { + c.local.afes[0].set_gain(settings.fls.channel[0].afe_gain); + c.local.afes[1].set_gain(settings.fls.channel[1].afe_gain); + c.shared.pounder.lock(|p| { for (ch, att) in [ - (Channel::In0, settings.fls.ch[0].input.att), - (Channel::Out0, settings.fls.ch[0].output.att), - (Channel::In1, settings.fls.ch[1].input.att), - (Channel::Out1, settings.fls.ch[1].output.att), - ] - .into_iter() - { - p.set_attenuation(ch, att).unwrap(); + ( + Channel::In0, + settings.fls.channel[0].demodulate.attenuation, + ), + ( + Channel::Out0, + settings.fls.channel[0].modulate.attenuation, + ), + ( + Channel::In1, + settings.fls.channel[1].demodulate.attenuation, + ), + ( + Channel::Out1, + settings.fls.channel[1].modulate.attenuation, + ), + ] { + if p.set_attenuation(ch, att).is_err() { + log::warn!("invalid attenuation"); + } } p.set_ext_clk(settings.fls.ext_clk).unwrap(); }); - c.shared.dds_output.lock(|dds_output| { let mut builder = dds_output.builder(); builder.push( Channel::In0.into(), - Some( - settings.fls.ch[0].input.freq.wrapping_add( - settings.fls.lockin_freq << MULT_SHIFT, - ), - ), - Some(settings.fls.ch[0].input.phase), - Some(Acr::new_with_raw_value(settings.fls.ch[0].amp)), + Some(settings.fls.channel[0].demodulate.frequency), + None, + Some(Acr::new_with_raw_value( + settings.fls.channel[0].demodulate_acr, + )), ); builder.push( Channel::In1.into(), - Some( - settings.fls.ch[1].input.freq.wrapping_add( - settings.fls.lockin_freq << MULT_SHIFT, - ), - ), - Some(settings.fls.ch[1].input.phase), - Some(Acr::new_with_raw_value(settings.fls.ch[1].amp)), + Some(settings.fls.channel[1].demodulate.frequency), + None, + Some(Acr::new_with_raw_value( + settings.fls.channel[1].demodulate_acr, + )), ); dds_output.write(builder); }); c.shared .network .lock(|net| net.direct_stream(settings.fls.stream)); - c.shared - .active_settings - .lock(|current| *current = settings.fls.clone()); + + if settings.fls.activate { + let cfg = settings.fls.build(); + c.shared.config.lock(|config| *config = cfg); + } }); } - #[task(priority = 1, shared=[pounder, telemetry, settings, state])] + #[task(priority = 1, shared=[pounder, config, state])] async fn aux_adc(mut c: aux_adc::Context) -> ! { loop { let aux_adc::SharedResources { - settings, + config, state, - telemetry, pounder, .. } = &mut c.shared; let x = pounder.lock(|p| { [ - p.sample_aux_adc(Channel::In0).unwrap(), - p.sample_aux_adc(Channel::In1).unwrap(), + p.sample_aux_adc_raw(Channel::In0).unwrap(), + p.sample_aux_adc_raw(Channel::In1).unwrap(), ] }); - let mut y = [0; 2]; - (settings, state).lock(|s, c| { - for (((s, c), x), y) in s - .fls - .ch - .iter() - .zip(c.iter_mut()) - .zip(x.iter()) - .zip(y.iter_mut()) - { - *y = if c.hold { - Biquad::::HOLD.process(&mut c.iir_amp, *x); - 0 - } else { - s.iir_amp.iir.process(&mut c.iir_amp, *x) as u16 - }; - } - }); - telemetry.lock(|t| { - t.ch[0].aux_adc = x[0]; - t.ch[0].mod_amp = y[0]; - t.ch[1].aux_adc = x[1]; - t.ch[1].mod_amp = y[1]; + (config, state).lock(|c, s| { + c.channel[0].dsp.power(&mut s.channel[0], x[0]); + c.channel[1].dsp.power(&mut s.channel[1], x[1]); }); Systick::delay(10.millis()).await; } } - #[task(priority = 1, local=[tele_state], shared=[network, settings, telemetry, pounder])] + #[task(priority = 1, local=[cpu_temp_sensor], shared=[network, settings, state, pounder])] async fn telemetry(mut c: telemetry::Context) -> ! { loop { - let (raw, stats) = c - .shared - .telemetry - .lock(|t| (t.clone(), core::mem::take(&mut t.stats))); - - let (phase_scale, freq) = c.shared.settings.lock(|s| { + let cpu_temp = c.local.cpu_temp_sensor.get_temperature().unwrap(); + let (meas, pounder_temp) = c.shared.pounder.lock(|p| { ( - [s.fls.ch[0].phase_scale, s.fls.ch[1].phase_scale], - [s.fls.ch[0].input.freq, s.fls.ch[1].input.freq], + [ + p.measure_power(Channel::In0).unwrap(), + p.measure_power(Channel::In1).unwrap(), + ], + p.temperature().unwrap(), ) }); - let scales = [ - [ - (1i64 << phase_scale[0][0][1]) as f64 - / phase_scale[0][0][0] as f64, - phase_scale[0][1][0] as f64 - / ((1 << PHASE_SCALE_SHIFT) as f64 - * (1i64 << phase_scale[0][1][1]) as f64), - ], - [ - (1i64 << phase_scale[1][0][1]) as f64 - / phase_scale[1][0][0] as f64, - phase_scale[1][1][0] as f64 - / ((1 << PHASE_SCALE_SHIFT) as f64 - * (1i64 << phase_scale[1][1][1]) as f64), - ], - ]; - const FDDS: f64 = 1.0 / DDS_LSB_PER_HZ as f64; - const FLI: f64 = 0x1000 as f64 * FDDS; - let pll_tau = - 1.0 / raw.pll_time.wrapping_sub(c.local.tele_state[2]) as f64; - *c.local.tele_state = - [raw.ch[0].phase, raw.ch[1].phase, raw.pll_time]; - - let mut tele = CookedTelemetry { - pll_time: raw.pll_time, - phase: [ - stats[0] - .get_scaled(scales[0][0] as f32 / (1i64 << 32) as f32), - stats[1] - .get_scaled(scales[1][0] as f32 / (1i64 << 32) as f32), - ], - ch_freq: [ - freq[0] as f64 * FDDS - + (raw.ch[0].phase.wrapping_sub(c.local.tele_state[0]) - as f64 - * pll_tau - - scales[0][1]) - * scales[0][0] - * FLI, - freq[1] as f64 * FDDS - + (raw.ch[1].phase.wrapping_sub(c.local.tele_state[1]) - as f64 - * pll_tau - - scales[1][1]) - * scales[1][0] - * FLI, - ], - raw: raw.ch, - ..Default::default() - }; + let (pll_time, phase, power, mut channel) = + c.shared.state.lock(|s| { + ( + s.pll_time.y, + core::mem::take(&mut s.phase), + core::mem::take(&mut s.power), + core::array::from_fn(|i| { + let s = &s.channel[i]; + ChannelTelemetry { + phase_raw: s.unwrap.y, + aux_adc: s.amplitude.x0() as f32 + * fls::AMPLITUDE_UNITS.x, + mod_amp: s.amplitude.y0() as f32 + * fls::AMPLITUDE_UNITS.y, + holds: s.holds, + slips: s.slips, + blanks: s.blanks, + rf_power: meas[i], + ..Default::default() + } + }), + ) + }); + for ((phase, power), channel) in + phase.into_iter().zip(power).zip(channel.iter_mut()) + { + channel.phase = phase.get_scaled(fls::PHASE_UNITS.x); + channel.power = power.get_scaled(P32::<28>::DELTA); + } - c.shared.pounder.lock(|p| { - tele.rf_power = [ - p.measure_power(Channel::In0).unwrap(), - p.measure_power(Channel::In1).unwrap(), - ]; - tele.temp = p.temperature().unwrap(); - }); c.shared.network.lock(|net| { - net.telemetry.publish_telemetry("/telemetry", &tele) + net.telemetry.publish_telemetry( + "/telemetry", + &CookedTelemetry { + pll_time, + channel, + pounder_temp, + cpu_temp, + }, + ) }); let telemetry_period = c.shared.settings.lock(|s| s.fls.telemetry_period); - // Schedule the telemetry task in the future. - Systick::delay((telemetry_period as u32).secs()).await; + Systick::delay(((telemetry_period * 1e3) as u32).millis()).await; } } #[task(priority = 1, shared=[usb, settings], local=[usb_terminal])] async fn usb(mut c: usb::Context) -> ! { loop { - // Handle the USB serial terminal. - c.shared.usb.lock(|usb| { + if c.shared.usb.lock(|usb| { usb.poll(&mut [c .local .usb_terminal .interface_mut() - .inner_mut()]); - }); - - c.shared - .settings - .lock(|settings| c.local.usb_terminal.poll(settings).unwrap()); - - // Schedule to run this task every 10 milliseconds. + .inner_mut()]) + }) && c.shared.settings.lock(|settings| { + c.local.usb_terminal.poll(settings).unwrap_or_else(|_| { + log::warn!("USB error"); + false + }) + }) { + settings_update::spawn().unwrap(); + } Systick::delay(10.millis()).await; } } diff --git a/src/fls.rs b/src/fls.rs new file mode 100644 index 0000000000..6f644620dc --- /dev/null +++ b/src/fls.rs @@ -0,0 +1,307 @@ +use core::num::Wrapping; + +use arbitrary_int::{Number, u4, u10}; +use dsp_fixedpoint::{P32, Q32}; +use dsp_process::{ + Add, Identity, Pair, Parallel, Process, SplitProcess, Unsplit, +}; +use idsp::{ + ClampWrap, Complex, Unwrapper, + iir::{ + Biquad, BiquadClamp, DirectForm1, DirectForm1Dither, + pid::{Gains, Order, Pid, Units}, + repr::BiquadRepr, + wdf::{Wdf, WdfState}, + }, +}; +use num_traits::Float; + +pub const BATCH_SIZE: usize = 8; +pub const SAMPLE_TICKS_E: usize = 7; + +// signal: p +// demod: t'f/N +// t': fixstate +// -> p + t'f/N +// -> lp(p + t'f/N) + (t - t')*f/N +// q: pll time +// t: true sample time +// t = q * (PLL_M << PLL_E) +const PLL_M: Wrapping = Wrapping(5); // 5 ns +const PLL_E: usize = + 16 // PLL input align + + SAMPLE_TICKS_E // sample timer divider + + 1 // 10ns timer clock: 5 ns M, 1 E + - 1 // 2 ns DDS sample clock + - 2 // SYNC divider + - 2 // ETR prescaler +; +// in q32: (t - t')*PLL_M/N << 32 = (q << (32 - PLL_E - N_LOG2)) - (PLL_M*t' << (32 - N_LOG2)) + +const _: () = { + assert!(0x140_0000 / BATCH_SIZE as i32 == PLL_M.0 << PLL_E); +}; + +const N_E: usize = 4; + +/// Fixed LO LUT mixer +#[derive(Debug, Clone)] +struct FixLo { + lo: [Q32<32>; N], + f: usize, + pi_half: usize, +} + +#[derive(Debug, Clone, Default)] +pub struct FixState { + i: usize, +} + +impl FixLo { + pub fn new(f: usize, p: f32) -> Self { + Self { + f, + lo: core::array::from_fn(|i| { + Q32::from_f64( + (0.5 - 1e-5) + * ((p as f64 + (i * f) as f64) + * const { core::f64::consts::TAU / N as f64 }) + .cos(), + ) + }), + pi_half: (0..N).find(|i| i * f % N == N / 4).unwrap(), + } + } +} + +impl SplitProcess, FixState> for FixLo { + fn process(&self, state: &mut FixState, x: i32) -> Complex { + state.i = state.i.wrapping_add(1); + let y = Complex([ + x * self.lo[state.i % N], + x * self.lo[state.i.wrapping_sub(self.pi_half) % N], + ]); + y + } +} + +const WRAP_BITS: u32 = 13; + +pub const PHASE_UNITS: Units = Units { + // 100 MHz timer, 128 divider, BATCH_SIZE 8: period of 10.24 µs, ~98 KHz + t: (BATCH_SIZE << SAMPLE_TICKS_E) as f32 * 10e-9, + x: 1.0 / (PLL_M.0 << (32 - WRAP_BITS)) as f32, + // 500 MHz DDS clock + y: 500e6 / (1u64 << 32) as f32, +}; + +pub const AMPLITUDE_UNITS: Units = Units { + t: 10e-3, + x: 2.048 / (1 << 16) as f32, + y: 1.0 / i32::MAX as f32, +}; + +#[derive(Debug, Clone, miniconf::Tree)] +#[tree(meta(doc, typename))] +pub struct Channel { + /// LO frequency tuning word in 1/16 Fs + #[tree(with=miniconf::leaf)] + lo_frequency: u4, + + /// LO phase offsets in turns + lo_phase: f32, + + /// Lowpass filter poles (7th order wave digital filter allpass pair) + #[tree(with=miniconf::leaf)] + lowpass: (([f64; 2], [f64; 2]), ([f64; 2], [f64; 1])), + + /// Minimum demodulated signal power to enable feedback (re full scale) + min_power: f32, + + /// Active filter representation + #[tree(rename="phase_repr", typ="&str", with=miniconf::str_leaf, defer=self.phase)] + _phase_repr: (), // before repr + /// Phase-to-frequency filter (units: Hz/turn, s) + phase: BiquadRepr, i32>, + + /// Active filter representation + #[tree(rename="amplitude_repr", typ="&str", with=miniconf::str_leaf, defer=self.amplitude)] + _amplitude_repr: (), // before repr + /// Amplitude IIR filter (units: full scale amplitude/Volt, s) + amplitude: BiquadRepr, i32>, +} + +impl Default for Channel { + fn default() -> Self { + Self { + lo_frequency: u4::new(5), + lo_phase: 0.0, + lowpass: ( + ( + [-0.982905335393602, 0.9991878840149784], + [-0.9283527198560211, 0.9991355621411774], + ), + ( + [-0.9515541301687671, 0.9991654023997083], + [0.9589369885243411], + ), + ), + min_power: 1e-6, + _phase_repr: (), + phase: BiquadRepr::Pid(Pid { + min: -20e3, + max: 20e3, + setpoint: 0.0, + order: Order::I, + gain: Gains::new([0.0, -1e6, -1e3, 0.0, 0.0]), + ..Default::default() + }), + _amplitude_repr: (), + amplitude: BiquadRepr::Pid(Pid { + min: 1.0, + max: 1.0, + ..Default::default() + }), + } + } +} + +#[derive(Debug, Clone)] +pub struct ChannelConfig { + lo: FixLo<16>, + lowpass: Pair<[Wdf<2, 0xad>; 2], (Wdf<2, 0xad>, Wdf<1, 0xa>), i32>, + phase: BiquadClamp, i32>, + amplitude: BiquadClamp, i32>, + min_power: P32<28>, +} + +impl Channel { + pub fn build(&self) -> ChannelConfig { + const { assert!(u4::MAX.value() == (1 << N_E) as u8 - 1) }; + ChannelConfig { + lo: FixLo::new(self.lo_frequency.value() as _, self.lo_phase), + lowpass: Pair::new(( + ( + (), + Parallel(( + [ + Wdf::quantize(&self.lowpass.0.0).unwrap(), + Wdf::quantize(&self.lowpass.0.1).unwrap(), + ], + ( + Wdf::quantize(&self.lowpass.1.0).unwrap(), + Wdf::quantize(&self.lowpass.1.1).unwrap(), + ), + )), + ), + (), + )), + min_power: P32::from_f32(self.min_power), + phase: self.phase.build(&PHASE_UNITS), + amplitude: self.amplitude.build(&LITUDE_UNITS), + } + } +} + +#[derive(Debug, Clone, Default)] +pub struct ChannelState { + lo: FixState, + lowpass: Complex<( + ( + Unsplit, + ([WdfState<2>; 2], (WdfState<2>, WdfState<1>)), + ), + Unsplit, + )>, + pub unwrap: Unwrapper, + pub clamp: ClampWrap>, + pub phase: DirectForm1Dither, + pub amplitude: DirectForm1, + pub hold: bool, + pub holds: Wrapping, + pub blanks: Wrapping, + pub slips: Wrapping, + pub mod_amp: u10, + pub power: P32<28>, +} + +impl ChannelConfig { + #[inline(always)] + pub fn demodulate( + &self, + state: &mut ChannelState, + x: &[u16; B], + y: &mut [u16; B], + ) -> Complex { + x.iter() + .zip(y.iter_mut()) + .map(|(x, y)| { + // bit are peak amplitude + // x: 15 bit, 16 asl + // lo: 31 bit, 32 asr: 1 bit loss + let m = + self.lo.process(&mut state.lo, (*x as i16 as i32) << 16); + // m: 29 bit 0f plus 29 bit 2f + // lp: 1 bit DC gain + let demod = Complex([ + self.lowpass.process(&mut state.lowpass[0], m[0]), + self.lowpass.process(&mut state.lowpass[1], m[1]), + ]); + // demod: 30 bit + // output in phase (before CPU clock phase correction) + *y = ((demod.re() >> 15) as i16).wrapping_add(i16::MIN) as u16; + demod + }) + .last() + .unwrap() + } + + #[inline(always)] + pub fn update( + &self, + state: &mut ChannelState, + t: Wrapping, + demod: Complex, + ) { + let d2 = demod.0.map(|x| x as i64 * x as i64); + // d2: 30 + 30 bit + state.power = P32::new(((d2[0] + d2[1]) >> 32) as _); + // power: 28 bit + let blank = state.power < self.min_power; + if blank { + state.blanks += Wrapping(1); + } + if state.hold { + state.holds += Wrapping(1); + } + if blank | state.hold { + state.unwrap = Default::default(); + state.clamp = Default::default(); + state.phase = Default::default(); + } else { + let phase = demod.arg(); + let phase = PLL_M * phase + + Wrapping(self.lo.f as i32) + * ((t << (32 - PLL_E - N_E)) + - (PLL_M << (32 - N_E)) * Wrapping(state.lo.i as i32)); + let dphase = Wrapping(state.unwrap.process(phase.0)); + // |delta phase| > pi/2 indicates a possible undetected phase slip. + // Neither a necessary nor a sufficient condition though. + if dphase + Wrapping(1 << 30) < Wrapping(0) { + state.slips += Wrapping(1); + } + let phase = Wrapping((state.unwrap.y >> WRAP_BITS) as i32); + let phase = state.clamp.process(phase); + self.phase.process(&mut state.phase, phase.0); + } + } + + pub fn power(&self, state: &mut ChannelState, x: u32) { + let y0 = if state.hold { + Biquad::>::HOLD.process(&mut state.amplitude, x as _) + } else { + self.amplitude.process(&mut state.amplitude, x as _) + }; + state.mod_amp = u10::new((y0 >> 21) as u16); + } +} diff --git a/src/lib.rs b/src/lib.rs index 1f06f266b4..dc39103fbd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,3 +13,5 @@ pub mod convert; pub mod statistics; pub mod mpll; + +pub mod fls; diff --git a/src/mpll.rs b/src/mpll.rs index 5a257c051b..1b648886bd 100644 --- a/src/mpll.rs +++ b/src/mpll.rs @@ -163,13 +163,13 @@ impl Mpll { .zip(m10.iter_mut().zip(m11)) .zip(state.lo[0].iter().zip(state.lo[1].iter())), ) { - // ADC encoding, mix, 1 bit loss (headroom) + // ADC encoding, mix, 1 bit loss, 30 bit amplitude *mix.0.0 = ((*x.0 as i16 as i64 * *lo.0 as i64) >> 16) as i32; *mix.0.1 = ((*x.0 as i16 as i64 * *lo.1 as i64) >> 16) as i32; *mix.1.0 = ((*x.1 as i16 as i64 * *lo.0 as i64) >> 16) as i32; *mix.1.1 = ((*x.1 as i16 as i64 * *lo.1 as i64) >> 16) as i32; } - // lowpass, 1 bit gain + // lowpass, 1 bit DC gain, 1 bit loss to 2f for (mix, state) in mix.iter_mut().zip(state.lp.iter_mut()) { self.lp.inplace(state, mix); } diff --git a/stream/src/lib.rs b/stream/src/lib.rs index 55f60212de..5f79b015fa 100644 --- a/stream/src/lib.rs +++ b/stream/src/lib.rs @@ -55,7 +55,7 @@ impl Serialize for Target { &self, serializer: S, ) -> Result { - let mut display: String<30> = String::new(); + let mut display: String<21> = String::new(); write!(&mut display, "{}", self.0).unwrap(); serializer.serialize_str(&display) } @@ -95,6 +95,9 @@ pub enum Format { /// MPLL data Mpll = 4, + + /// FLS2 + Fls2 = 5, } #[cfg(target_arch = "arm")] From eec5c6c1ffcfed7d624cdcdbaf5f0229236e5e3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Wed, 25 Feb 2026 11:10:56 +0000 Subject: [PATCH 11/19] fls: new pll --- src/bin/fls.rs | 34 ++++++++++++++++++---------------- src/fls.rs | 14 +++++++++----- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/bin/fls.rs b/src/bin/fls.rs index 584ad68154..2d908d45fd 100644 --- a/src/bin/fls.rs +++ b/src/bin/fls.rs @@ -81,9 +81,8 @@ use core::num::Wrapping as W; use ad9959::Acr; use arbitrary_int::u24; -use dsp_fixedpoint::W32; use dsp_process::SplitProcess; -use idsp::{Complex, PLL, Unwrapper}; +use idsp::{Complex, PLL, PLLState, Unwrapper}; use miniconf::Tree; use platform::{AppSettings, NetSettings}; use serde::Serialize; @@ -129,11 +128,12 @@ pub struct Fls { /// Channel-specific settings. channel: [Channel; 2], - /// Lockin demodulation oscillator PLL bandwidth. + /// Lockin demodulation oscillator PLL crossover in Hz. /// This PLL reconstructs the DDS SYNC clock output on the CPU clock timescale. - /// - /// TODO: settle pll and lockin settings into design after confirming optimal choice - pll: f32, + pll_bw: f32, + + /// PLL lead-lag pole-zero ratio + pll_split: f32, /// Telemetry output period in seconds. telemetry_period: f32, @@ -154,7 +154,8 @@ impl Default for Fls { Self { ext_clk: false, channel: Default::default(), - pll: 2.0, + pll_bw: 1e-4 / 10.24e-6, + pll_split: 4.0, telemetry_period: 10., stream: Default::default(), activate: true, @@ -240,13 +241,13 @@ struct ChannelConfig { #[derive(Debug, Clone)] pub struct Config { channel: [ChannelConfig; 2], - pll: W32<32>, + pll: PLL, } #[derive(Default, Debug, Clone)] pub struct State { channel: [fls::ChannelState; 2], - pll: PLL, + pll: PLLState, pll_time: Unwrapper, phase: [statistics::State; 2], power: [statistics::State; 2], @@ -260,7 +261,7 @@ impl Fls { hold_en: c.hold_en, modulate_frequency: c.modulate.frequency, }), - pll: W32::new(W(0x4_0000)), // TODO + pll: PLL::from_bandwidth(self.pll_bw * 10.24e-6, self.pll_split), } } } @@ -520,12 +521,13 @@ mod app { // in terms of the CPU clock (driving the capture). // Discard double captures (overcaptures) and extrapolate. // Extrapolate on no capture (undercapture). - let timestamp = timestamper.latest_timestamp().unwrap_or(None); - let mut _accu = config.pll.process( - &mut state.pll, - timestamp.map(|t| W((t as i32) << 16)), - ); - let phase = state.pll.phase(); + let t = if let Ok(Some(t)) = timestamper.latest_timestamp() { + W((t as i32) << 16) + } else { + log::warn!("timestamp under/over"); + state.pll.clamp.x0 - state.pll.frequency() + }; + let phase = -config.pll.process(&mut state.pll, t); let mut builder = dds_output.builder(); let hold = diff --git a/src/fls.rs b/src/fls.rs index 6f644620dc..4194a5ead5 100644 --- a/src/fls.rs +++ b/src/fls.rs @@ -69,7 +69,7 @@ impl FixLo { .cos(), ) }), - pi_half: (0..N).find(|i| i * f % N == N / 4).unwrap(), + pi_half: (0..N).find(|i| i * f % N == N / 4).unwrap_or_default(), } } } @@ -185,12 +185,16 @@ impl Channel { (), Parallel(( [ - Wdf::quantize(&self.lowpass.0.0).unwrap(), - Wdf::quantize(&self.lowpass.0.1).unwrap(), + Wdf::quantize(&self.lowpass.0.0) + .unwrap_or_default(), + Wdf::quantize(&self.lowpass.0.1) + .unwrap_or_default(), ], ( - Wdf::quantize(&self.lowpass.1.0).unwrap(), - Wdf::quantize(&self.lowpass.1.1).unwrap(), + Wdf::quantize(&self.lowpass.1.0) + .unwrap_or_default(), + Wdf::quantize(&self.lowpass.1.1) + .unwrap_or_default(), ), )), ), From 5c67a6a86936788ba4d3756e2d3f154596cf6107 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Thu, 26 Feb 2026 16:14:51 +0000 Subject: [PATCH 12/19] pounder: set all attenuators --- src/hardware/pounder/mod.rs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/hardware/pounder/mod.rs b/src/hardware/pounder/mod.rs index 340736c208..204adf72a0 100644 --- a/src/hardware/pounder/mod.rs +++ b/src/hardware/pounder/mod.rs @@ -97,7 +97,7 @@ impl From for Error { /// The numerical value (discriminant) of the Channel enum is the index in the attenuator shift /// register as well as the attenuator latch enable signal index on the GPIO extender. -#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, strum::EnumIter)] #[allow(dead_code)] pub enum Channel { In0 = 0, @@ -476,7 +476,7 @@ impl PounderDevices { match channel { Channel::In0 => self.aux_adc.0.read_raw().or(Err(Error::Adc)), Channel::In1 => self.aux_adc.1.read_raw().or(Err(Error::Adc)), - _ => return Err(Error::InvalidChannel), + _ => Err(Error::InvalidChannel), } } @@ -564,6 +564,26 @@ impl PounderDevices { Ok(()) } + /// Set all attemuators + pub fn set_attenuation_all( + &mut self, + attenuation: [f32; 4], + ) -> Result<[f32; 4], Error> { + let mut codes = [0; 4]; + for (c, a) in codes.iter_mut().zip(attenuation) { + if !crate::convert::att_is_valid(a) { + return Err(Error::Bounds); + } + *c = !(((a * 2.0) as u8) << 2); + } + self.transfer_attenuators(&mut codes)?; + + for c in Channel::iter() { + self.latch_attenuator(c)?; + } + Ok(codes.map(|c| (!c >> 2) as f32 / 2.0)) + } + /// Set the attenuation of a single channel. /// /// Args: From 3dcce2f83615e0e46b74bebf6915ed895509559c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Thu, 26 Feb 2026 16:17:37 +0000 Subject: [PATCH 13/19] fls: cleanup code, add adc/acr to stream --- src/bin/fls.rs | 232 +++++++++++++++++++++++++----------------------- src/bin/mpll.rs | 2 +- src/fls.rs | 52 ++++++----- src/mpll.rs | 20 +++-- 4 files changed, 160 insertions(+), 146 deletions(-) diff --git a/src/bin/fls.rs b/src/bin/fls.rs index 2d908d45fd..552702f367 100644 --- a/src/bin/fls.rs +++ b/src/bin/fls.rs @@ -56,7 +56,7 @@ //! See [idsp::Lowpass] //! * Full rate baseband demodulated data (quadrature only) on DAC0 //! * Lowpass filtering with a batch-size boxcar FIR filter (zeros at n/4 Nyquist) -//! * Computation of signal power and phase. See [idsp::ComplexExt]. +//! * Computation of signal power and phase. //! * Fractional rescaling (`phase_scale`) and unwrapping of the phase with 32 bit turn range. //! * Scaling and clamping. //! * Filtering by a second order (biquad) IIR filter (supporting e.g. II, I, P @@ -70,7 +70,7 @@ //! * Modulation output at Pounder OUT0 //! //! # Telemetry -//! Data is regularly published via MQTT. See [Telemetry]. +//! Data is regularly published via MQTT. See [CookedTelemetry]. //! //! # Streaming //! Full-rate ADC and DAC data is available via configurable UDP data streaming. @@ -79,13 +79,16 @@ use core::num::Wrapping as W; -use ad9959::Acr; -use arbitrary_int::u24; -use dsp_process::SplitProcess; +use arbitrary_int::{u10, u24}; +use core::sync::atomic::{Ordering, fence}; +use dsp_fixedpoint::P32; +use dsp_process::{Process, SplitProcess}; use idsp::{Complex, PLL, PLLState, Unwrapper}; use miniconf::Tree; -use platform::{AppSettings, NetSettings}; use serde::Serialize; + +use ad9959::Acr; +use platform::{AppSettings, NetSettings}; use stabilizer::{convert::Gain, fls, statistics}; #[derive(Clone, Debug, Tree, Default)] @@ -130,7 +133,7 @@ pub struct Fls { /// Lockin demodulation oscillator PLL crossover in Hz. /// This PLL reconstructs the DDS SYNC clock output on the CPU clock timescale. - pll_bw: f32, + pll_bandwidth: f32, /// PLL lead-lag pole-zero ratio pll_split: f32, @@ -144,8 +147,9 @@ pub struct Fls { /// Activate settings /// - /// If `true` each `channel` settings change immediately results - /// in activation. + /// If `true` each settings change immediately results in activation. + /// If `false`, activation is suppressed. + /// Use this to synchronize changes. activate: bool, } @@ -154,7 +158,7 @@ impl Default for Fls { Self { ext_clk: false, channel: Default::default(), - pll_bw: 1e-4 / 10.24e-6, + pll_bandwidth: 1e-4 / 10.24e-6, pll_split: 4.0, telemetry_period: 10., stream: Default::default(), @@ -244,13 +248,51 @@ pub struct Config { pll: PLL, } +#[derive(Default, Debug, Clone)] +pub struct ChannelState { + dsp: fls::ChannelState, + phase: statistics::State, + power: statistics::State, +} + +impl ChannelState { + fn update(&mut self) { + self.phase.update(self.dsp.phase.xy.x0()); + self.power.update(self.dsp.power.inner as _); + } + + fn stream(&self) -> StreamChannel { + StreamChannel { + demod: self.dsp.demod, + phase: bytemuck::cast(self.dsp.unwrap.y), + delta_ftw: W(self.dsp.phase.xy.y0()), + aux_adc: self.dsp.amplitude.x0() as _, + mod_amp: (self.dsp.amplitude.y0() >> 21) as _, + } + } +} + #[derive(Default, Debug, Clone)] pub struct State { - channel: [fls::ChannelState; 2], + channel: [ChannelState; 2], pll: PLLState, pll_time: Unwrapper, - phase: [statistics::State; 2], - power: [statistics::State; 2], +} + +impl State { + fn update(&mut self) { + self.pll_time.process(self.pll.phase().0); + for c in self.channel.iter_mut() { + c.update(); + } + } + + fn stream(&self) -> Stream { + Stream { + pll: self.pll.phase(), + channel: self.channel.each_ref().map(|s| s.stream()), + } + } } impl Fls { @@ -261,7 +303,10 @@ impl Fls { hold_en: c.hold_en, modulate_frequency: c.modulate.frequency, }), - pll: PLL::from_bandwidth(self.pll_bw * 10.24e-6, self.pll_split), + pll: PLL::from_bandwidth( + self.pll_bandwidth * 10.24e-6, + self.pll_split, + ), } } } @@ -269,21 +314,24 @@ impl Fls { /// Stream data format. #[derive(Clone, Copy, Debug, Default, bytemuck::Zeroable, bytemuck::Pod)] #[repr(C)] -pub struct Stream { +struct Stream { pll: W, - ch: [StreamChannel; 2], + channel: [StreamChannel; 2], } #[derive(Clone, Copy, Debug, Default, bytemuck::Zeroable, bytemuck::Pod)] #[repr(C)] -pub struct StreamChannel { +struct StreamChannel { /// Demodulated signal. - pub demod: Complex, + demod: Complex, /// Current raw phase including wraps and pll correction. - pub phase: [u32; 2], + phase: [u32; 2], /// Current frequency tuning word added to the configured modulation /// offset `mod_freq`. - pub delta_ftw: W, + delta_ftw: W, + /// AUX ADC sample + aux_adc: u16, + mod_amp: u16, } /// Channel Telemetry, all undersampled and not AA filtered @@ -347,9 +395,6 @@ fn main() { #[cfg(target_os = "none")] #[cfg_attr(target_os = "none", rtic::app(device = stabilizer::hardware::hal::stm32, peripherals = true, dispatchers=[DCMI, JPEG, LTDC, SDMMC]))] mod app { - use core::sync::atomic::{Ordering, fence}; - use dsp_fixedpoint::P32; - use dsp_process::Process; use fugit::ExtU32 as _; use rtic_monotonics::Monotonic; @@ -489,28 +534,20 @@ mod app { (c.shared.state, c.shared.config, c.shared.dds_output).lock( |state, config, dds_output| { - let demod = - (adc0, adc1, dac0, dac1).lock(|adc0, adc1, dac0, dac1| { - fence(Ordering::SeqCst); - let demod = [ - config.channel[0] - .dsp - .demodulate::<{ fls::BATCH_SIZE }>( - &mut state.channel[0], - (**adc0).try_into().unwrap(), - (*dac0).try_into().unwrap(), - ), - config.channel[1] - .dsp - .demodulate::<{ fls::BATCH_SIZE }>( - &mut state.channel[1], - (**adc1).try_into().unwrap(), - (*dac1).try_into().unwrap(), - ), - ]; - fence(Ordering::SeqCst); - demod - }); + (adc0, adc1, dac0, dac1).lock(|adc0, adc1, dac0, dac1| { + fence(Ordering::SeqCst); + config.channel[0].dsp.demodulate::<{ fls::BATCH_SIZE }>( + &mut state.channel[0].dsp, + (**adc0).try_into().unwrap(), + (*dac0).try_into().unwrap(), + ); + config.channel[1].dsp.demodulate::<{ fls::BATCH_SIZE }>( + &mut state.channel[1].dsp, + (**adc1).try_into().unwrap(), + (*dac1).try_into().unwrap(), + ); + fence(Ordering::SeqCst); + }); // A counter running at a fourth of the DDS SYNC interval is captured by // the overflow of a timer synchronized to the sampling timer (locked to the @@ -532,50 +569,31 @@ mod app { let mut builder = dds_output.builder(); let hold = [digital_inputs.0.is_high(), digital_inputs.1.is_high()]; - for (idx, ((demod, hold), (config, state))) in - [Channel::Out0, Channel::Out1].into_iter().zip( - demod.into_iter().zip(hold).zip( - config.channel.iter().zip(state.channel.iter_mut()), - ), + for ((idx, hold), (config, state)) in + [Channel::Out0, Channel::Out1].into_iter().zip(hold).zip( + config.channel.iter().zip(state.channel.iter_mut()), ) { - state.hold = hold && config.hold_en; - config.dsp.update(state, phase, demod); + state.dsp.hold = hold && config.hold_en; + config.dsp.update(&mut state.dsp, phase); builder.push( idx.into(), Some( - config.modulate_frequency + W(state.phase.xy.y0()), + config.modulate_frequency + + W(state.dsp.phase.xy.y0()), ), None, - Some( - Acr::DEFAULT - .with_multiplier(true) - .with_asf(state.mod_amp), - ), + Some(Acr::DEFAULT.with_multiplier(true).with_asf( + u10::new((state.dsp.amplitude.y0() >> 21) as _), + )), ); } dds_output.write(builder); - - state.pll_time.process(state.pll.phase().0); - state.phase[0].update(state.channel[0].phase.xy.x0()); - state.phase[1].update(state.channel[1].phase.xy.x0()); - state.power[0].update(state.channel[0].power.inner as _); - state.power[1].update(state.channel[1].power.inner as _); - + state.update(); generator.add(|buf| { const N: usize = core::mem::size_of::(); buf[..N].copy_from_slice(bytemuck::cast_slice( - bytemuck::bytes_of(&Stream { - pll: state.pll.phase(), - ch: core::array::from_fn(|i| { - let s = &state.channel[i]; - StreamChannel { - demod: demod[i], - phase: bytemuck::cast(s.unwrap.y), - delta_ftw: W(s.phase.xy.y0()), - } - }), - }), + bytemuck::bytes_of(&state.stream()), )); N }); @@ -684,8 +702,8 @@ mod app { ] }); (config, state).lock(|c, s| { - c.channel[0].dsp.power(&mut s.channel[0], x[0]); - c.channel[1].dsp.power(&mut s.channel[1], x[1]); + c.channel[0].dsp.power(&mut s.channel[0].dsp, x[0]); + c.channel[1].dsp.power(&mut s.channel[1].dsp, x[1]); }); Systick::delay(10.millis()).await; } @@ -694,45 +712,39 @@ mod app { #[task(priority = 1, local=[cpu_temp_sensor], shared=[network, settings, state, pounder])] async fn telemetry(mut c: telemetry::Context) -> ! { loop { - let cpu_temp = c.local.cpu_temp_sensor.get_temperature().unwrap(); - let (meas, pounder_temp) = c.shared.pounder.lock(|p| { + let (pll_time, channel) = c.shared.state.lock(|s| { ( - [ - p.measure_power(Channel::In0).unwrap(), - p.measure_power(Channel::In1).unwrap(), - ], - p.temperature().unwrap(), - ) - }); - let (pll_time, phase, power, mut channel) = - c.shared.state.lock(|s| { - ( - s.pll_time.y, - core::mem::take(&mut s.phase), - core::mem::take(&mut s.power), - core::array::from_fn(|i| { - let s = &s.channel[i]; + s.pll_time.y, + s.channel.each_mut().map(|s| { + ( + core::mem::take(&mut s.phase), + core::mem::take(&mut s.power), ChannelTelemetry { - phase_raw: s.unwrap.y, - aux_adc: s.amplitude.x0() as f32 + phase_raw: s.dsp.unwrap.y, + aux_adc: s.dsp.amplitude.x0() as f32 * fls::AMPLITUDE_UNITS.x, - mod_amp: s.amplitude.y0() as f32 + mod_amp: s.dsp.amplitude.y0() as f32 * fls::AMPLITUDE_UNITS.y, - holds: s.holds, - slips: s.slips, - blanks: s.blanks, - rf_power: meas[i], + holds: s.dsp.holds, + slips: s.dsp.slips, + blanks: s.dsp.blanks, ..Default::default() - } - }), - ) - }); - for ((phase, power), channel) in - phase.into_iter().zip(power).zip(channel.iter_mut()) - { + }, + ) + }), + ) + }); + let mut channel = channel.map(|(phase, power, mut channel)| { channel.phase = phase.get_scaled(fls::PHASE_UNITS.x); channel.power = power.get_scaled(P32::<28>::DELTA); - } + channel + }); + let cpu_temp = c.local.cpu_temp_sensor.get_temperature().unwrap(); + let pounder_temp = c.shared.pounder.lock(|p| { + channel[0].rf_power = p.measure_power(Channel::In0).unwrap(); + channel[1].rf_power = p.measure_power(Channel::In0).unwrap(); + p.temperature().unwrap() + }); c.shared.network.lock(|net| { net.telemetry.publish_telemetry( diff --git a/src/bin/mpll.rs b/src/bin/mpll.rs index da37b75da8..7880ff6fa3 100644 --- a/src/bin/mpll.rs +++ b/src/bin/mpll.rs @@ -318,7 +318,7 @@ mod app { async fn telemetry(mut c: telemetry::Context) -> ! { loop { let tele = Telemetry { - mpll: c.shared.telemetry.lock(|t| core::mem::take(t)).into(), + mpll: c.shared.telemetry.lock(core::mem::take).into(), cpu_temp: c .local .cpu_temp_sensor diff --git a/src/fls.rs b/src/fls.rs index 4194a5ead5..c9b526a9a6 100644 --- a/src/fls.rs +++ b/src/fls.rs @@ -1,6 +1,6 @@ use core::num::Wrapping; -use arbitrary_int::{Number, u4, u10}; +use arbitrary_int::{Number, u4}; use dsp_fixedpoint::{P32, Q32}; use dsp_process::{ Add, Identity, Pair, Parallel, Process, SplitProcess, Unsplit, @@ -69,7 +69,7 @@ impl FixLo { .cos(), ) }), - pi_half: (0..N).find(|i| i * f % N == N / 4).unwrap_or_default(), + pi_half: (0..N).find(|i| (i * f) % N == N / 4).unwrap_or_default(), } } } @@ -77,11 +77,10 @@ impl FixLo { impl SplitProcess, FixState> for FixLo { fn process(&self, state: &mut FixState, x: i32) -> Complex { state.i = state.i.wrapping_add(1); - let y = Complex([ + Complex([ x * self.lo[state.i % N], x * self.lo[state.i.wrapping_sub(self.pi_half) % N], - ]); - y + ]) } } @@ -101,6 +100,8 @@ pub const AMPLITUDE_UNITS: Units = Units { y: 1.0 / i32::MAX as f32, }; +type Lp = (([f64; 2], [f64; 2]), ([f64; 2], [f64; 1])); + #[derive(Debug, Clone, miniconf::Tree)] #[tree(meta(doc, typename))] pub struct Channel { @@ -113,7 +114,7 @@ pub struct Channel { /// Lowpass filter poles (7th order wave digital filter allpass pair) #[tree(with=miniconf::leaf)] - lowpass: (([f64; 2], [f64; 2]), ([f64; 2], [f64; 1])), + lowpass: Lp, /// Minimum demodulated signal power to enable feedback (re full scale) min_power: f32, @@ -207,16 +208,19 @@ impl Channel { } } +type LpState = ( + ( + Unsplit, + ([WdfState<2>; 2], (WdfState<2>, WdfState<1>)), + ), + Unsplit, +); + #[derive(Debug, Clone, Default)] pub struct ChannelState { lo: FixState, - lowpass: Complex<( - ( - Unsplit, - ([WdfState<2>; 2], (WdfState<2>, WdfState<1>)), - ), - Unsplit, - )>, + lowpass: Complex, + pub demod: Complex, pub unwrap: Unwrapper, pub clamp: ClampWrap>, pub phase: DirectForm1Dither, @@ -225,7 +229,6 @@ pub struct ChannelState { pub holds: Wrapping, pub blanks: Wrapping, pub slips: Wrapping, - pub mod_amp: u10, pub power: P32<28>, } @@ -236,8 +239,9 @@ impl ChannelConfig { state: &mut ChannelState, x: &[u16; B], y: &mut [u16; B], - ) -> Complex { - x.iter() + ) { + state.demod = x + .iter() .zip(y.iter_mut()) .map(|(x, y)| { // bit are peak amplitude @@ -257,17 +261,12 @@ impl ChannelConfig { demod }) .last() - .unwrap() + .unwrap(); } #[inline(always)] - pub fn update( - &self, - state: &mut ChannelState, - t: Wrapping, - demod: Complex, - ) { - let d2 = demod.0.map(|x| x as i64 * x as i64); + pub fn update(&self, state: &mut ChannelState, t: Wrapping) { + let d2 = state.demod.0.map(|x| x as i64 * x as i64); // d2: 30 + 30 bit state.power = P32::new(((d2[0] + d2[1]) >> 32) as _); // power: 28 bit @@ -283,7 +282,7 @@ impl ChannelConfig { state.clamp = Default::default(); state.phase = Default::default(); } else { - let phase = demod.arg(); + let phase = state.demod.arg(); let phase = PLL_M * phase + Wrapping(self.lo.f as i32) * ((t << (32 - PLL_E - N_E)) @@ -301,11 +300,10 @@ impl ChannelConfig { } pub fn power(&self, state: &mut ChannelState, x: u32) { - let y0 = if state.hold { + if state.hold { Biquad::>::HOLD.process(&mut state.amplitude, x as _) } else { self.amplitude.process(&mut state.amplitude, x as _) }; - state.mod_amp = u10::new((y0 >> 21) as u16); } } diff --git a/src/mpll.rs b/src/mpll.rs index 1b648886bd..dd2b515ec4 100644 --- a/src/mpll.rs +++ b/src/mpll.rs @@ -23,16 +23,18 @@ pub const UNITS: pid::Units = pid::Units { y: HZ_PER_LSB, }; +type LpState = ( + ( + Unsplit, + ([WdfState<2>; 2], (WdfState<2>, WdfState<1>)), + ), + Unsplit, +); + #[derive(Debug, Clone, Default)] pub struct MpllState { /// Lowpass state - lp: [( - ( - Unsplit, - ([WdfState<2>; 2], (WdfState<2>, WdfState<1>)), - ), - Unsplit, - ); 4], + lp: [LpState; 4], /// PID state iir: DirectForm1, /// Current modulation phase @@ -55,12 +57,14 @@ pub struct Mpll { amplitude: [Q32<16>; 2], } +type Lp = (([f64; 2], [f64; 2]), ([f64; 2], [f64; 1])); + #[derive(Debug, Clone, miniconf::Tree)] #[tree(meta(doc, typename))] pub struct MpllConfig { /// Lowpass filter poles (7th order wave digital filter allpass pair) #[tree(with=miniconf::leaf)] - lp: (([f64; 2], [f64; 2]), ([f64; 2], [f64; 1])), + lp: Lp, /// Filter representation #[tree(rename="repr", typ="&str", with=miniconf::str_leaf, defer=self.iir)] _repr: (), // before iir From bfd4e5e3cff3e6592c67301079fb8d5efd8894eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Thu, 26 Feb 2026 17:42:34 +0000 Subject: [PATCH 14/19] deps: bump --- Cargo.lock | 93 +++++++++++++++++++++++++----------------------------- 1 file changed, 43 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 66071f39bc..600a5b7359 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,7 +77,7 @@ dependencies = [ "arbitrary-int", "proc-macro2", "quote", - "syn 2.0.115", + "syn 2.0.117", ] [[package]] @@ -94,9 +94,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "built" @@ -124,7 +124,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.115", + "syn 2.0.117", ] [[package]] @@ -141,9 +141,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.55" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "jobserver", @@ -196,7 +196,7 @@ checksum = "e37549a379a9e0e6e576fd208ee60394ccb8be963889eebba3ffe0980364f472" dependencies = [ "proc-macro2", "quote", - "syn 2.0.115", + "syn 2.0.117", ] [[package]] @@ -226,7 +226,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.115", + "syn 2.0.117", ] [[package]] @@ -237,7 +237,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.115", + "syn 2.0.117", ] [[package]] @@ -248,7 +248,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.115", + "syn 2.0.117", ] [[package]] @@ -424,26 +424,25 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-task", "pin-project-lite", - "pin-utils", ] [[package]] @@ -470,7 +469,7 @@ version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", "libgit2-sys", "log", @@ -703,9 +702,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.181" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libgit2-sys" @@ -727,9 +726,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libz-sys" -version = "1.1.23" +version = "1.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" +checksum = "4735e9cbde5aac84a5ce588f6b23a90b9b0b528f6c5a8db8a4aff300463a0839" dependencies = [ "cc", "libc", @@ -829,7 +828,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.115", + "syn 2.0.117", ] [[package]] @@ -986,7 +985,7 @@ checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.115", + "syn 2.0.117", ] [[package]] @@ -1022,12 +1021,6 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "pkg-config" version = "0.3.32" @@ -1101,7 +1094,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.115", + "syn 2.0.117", ] [[package]] @@ -1160,7 +1153,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.115", + "syn 2.0.117", ] [[package]] @@ -1203,7 +1196,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.115", + "syn 2.0.117", ] [[package]] @@ -1383,7 +1376,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.115", + "syn 2.0.117", ] [[package]] @@ -1402,9 +1395,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.16.1" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" dependencies = [ "serde_core", "serde_with_macros", @@ -1412,14 +1405,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.16.1" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.115", + "syn 2.0.117", ] [[package]] @@ -1674,7 +1667,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.115", + "syn 2.0.117", ] [[package]] @@ -1690,9 +1683,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.115" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -1719,7 +1712,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.115", + "syn 2.0.117", ] [[package]] @@ -1759,7 +1752,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.115", + "syn 2.0.117", ] [[package]] @@ -1770,7 +1763,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.115", + "syn 2.0.117", ] [[package]] @@ -1797,9 +1790,9 @@ checksum = "e87a2ed6b42ec5e28cc3b94c09982969e9227600b2e3dcbc1db927a84c06bd69" [[package]] name = "unicode-ident" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "url" @@ -1939,7 +1932,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.115", + "syn 2.0.117", "synstructure", ] @@ -1960,7 +1953,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.115", + "syn 2.0.117", "synstructure", ] @@ -1994,7 +1987,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.115", + "syn 2.0.117", ] [[package]] From 6ebfd54f6454e98d4b082896e3792b1bc082056d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 27 Feb 2026 09:19:27 +0000 Subject: [PATCH 15/19] py: filter multicast, register --- py/stabilizer/stream.py | 90 ++++++++++++++++++++++++++++++----------- 1 file changed, 66 insertions(+), 24 deletions(-) diff --git a/py/stabilizer/stream.py b/py/stabilizer/stream.py index 93c3c5121b..c796552f09 100644 --- a/py/stabilizer/stream.py +++ b/py/stabilizer/stream.py @@ -37,6 +37,32 @@ def get_local_ip(remote): sock.close() +class Frame: + """Stream frame constisting of a header and multiple data batches""" + + # The magic header half-word at the start of each packet. + magic = 0x057B + header_fmt = struct.Struct(" Date: Thu, 5 Mar 2026 14:56:54 +0000 Subject: [PATCH 16/19] mpll/fls: use biquads instead of wdf --- src/fls.rs | 123 ++++++++++++++++++++++------------------------------ src/mpll.rs | 96 ++++++++++++++++------------------------ 2 files changed, 90 insertions(+), 129 deletions(-) diff --git a/src/fls.rs b/src/fls.rs index c9b526a9a6..f886d21fc7 100644 --- a/src/fls.rs +++ b/src/fls.rs @@ -2,16 +2,13 @@ use core::num::Wrapping; use arbitrary_int::{Number, u4}; use dsp_fixedpoint::{P32, Q32}; -use dsp_process::{ - Add, Identity, Pair, Parallel, Process, SplitProcess, Unsplit, -}; +use dsp_process::{Process, SplitProcess}; use idsp::{ ClampWrap, Complex, Unwrapper, iir::{ - Biquad, BiquadClamp, DirectForm1, DirectForm1Dither, + Biquad, BiquadClamp, Cascade, DirectForm1, DirectForm1Dither, pid::{Gains, Order, Pid, Units}, repr::BiquadRepr, - wdf::{Wdf, WdfState}, }, }; use num_traits::Float; @@ -100,8 +97,6 @@ pub const AMPLITUDE_UNITS: Units = Units { y: 1.0 / i32::MAX as f32, }; -type Lp = (([f64; 2], [f64; 2]), ([f64; 2], [f64; 1])); - #[derive(Debug, Clone, miniconf::Tree)] #[tree(meta(doc, typename))] pub struct Channel { @@ -112,9 +107,9 @@ pub struct Channel { /// LO phase offsets in turns lo_phase: f32, - /// Lowpass filter poles (7th order wave digital filter allpass pair) + /// Lowpass filter (6th order biquad) #[tree(with=miniconf::leaf)] - lowpass: Lp, + lowpass: [[f64; 5]; 3], /// Minimum demodulated signal power to enable feedback (re full scale) min_power: f32, @@ -137,16 +132,31 @@ impl Default for Channel { Self { lo_frequency: u4::new(5), lo_phase: 0.0, - lowpass: ( - ( - [-0.982905335393602, 0.9991878840149784], - [-0.9283527198560211, 0.9991355621411774], - ), - ( - [-0.9515541301687671, 0.9991654023997083], - [0.9589369885243411], - ), - ), + // 6th order transitional Guassian to 6 dB, zeros at 1,2,3 * 5/16 + // 3dB+13τ @22kHz, 36 dB @100kHz, gain 2 + lowpass: [ + [ + 0.020708473, + 0.029286202, + 0.020708473, + 1.672374985, + -0.707726415, + ], + [ + 0.037786974, + 0.028920897, + 0.037786974, + 1.643345831, + -0.747840887, + ], + [ + 1.366993065, + -2.525873828, + 1.366993065, + 1.6675967, + -0.875708817, + ], + ], min_power: 1e-6, _phase_repr: (), phase: BiquadRepr::Pid(Pid { @@ -170,7 +180,7 @@ impl Default for Channel { #[derive(Debug, Clone)] pub struct ChannelConfig { lo: FixLo<16>, - lowpass: Pair<[Wdf<2, 0xad>; 2], (Wdf<2, 0xad>, Wdf<1, 0xa>), i32>, + lowpass: Cascade<[Biquad>; 3]>, phase: BiquadClamp, i32>, amplitude: BiquadClamp, i32>, min_power: P32<28>, @@ -181,26 +191,7 @@ impl Channel { const { assert!(u4::MAX.value() == (1 << N_E) as u8 - 1) }; ChannelConfig { lo: FixLo::new(self.lo_frequency.value() as _, self.lo_phase), - lowpass: Pair::new(( - ( - (), - Parallel(( - [ - Wdf::quantize(&self.lowpass.0.0) - .unwrap_or_default(), - Wdf::quantize(&self.lowpass.0.1) - .unwrap_or_default(), - ], - ( - Wdf::quantize(&self.lowpass.1.0) - .unwrap_or_default(), - Wdf::quantize(&self.lowpass.1.1) - .unwrap_or_default(), - ), - )), - ), - (), - )), + lowpass: Cascade(self.lowpass.each_ref().map(|c| Biquad::from(*c))), min_power: P32::from_f32(self.min_power), phase: self.phase.build(&PHASE_UNITS), amplitude: self.amplitude.build(&LITUDE_UNITS), @@ -208,18 +199,10 @@ impl Channel { } } -type LpState = ( - ( - Unsplit, - ([WdfState<2>; 2], (WdfState<2>, WdfState<1>)), - ), - Unsplit, -); - #[derive(Debug, Clone, Default)] pub struct ChannelState { lo: FixState, - lowpass: Complex, + lowpass: Complex<[DirectForm1; 3]>, pub demod: Complex, pub unwrap: Unwrapper, pub clamp: ClampWrap>, @@ -240,28 +223,26 @@ impl ChannelConfig { x: &[u16; B], y: &mut [u16; B], ) { - state.demod = x - .iter() - .zip(y.iter_mut()) - .map(|(x, y)| { - // bit are peak amplitude - // x: 15 bit, 16 asl - // lo: 31 bit, 32 asr: 1 bit loss - let m = - self.lo.process(&mut state.lo, (*x as i16 as i32) << 16); - // m: 29 bit 0f plus 29 bit 2f - // lp: 1 bit DC gain - let demod = Complex([ - self.lowpass.process(&mut state.lowpass[0], m[0]), - self.lowpass.process(&mut state.lowpass[1], m[1]), - ]); - // demod: 30 bit - // output in phase (before CPU clock phase correction) - *y = ((demod.re() >> 15) as i16).wrapping_add(i16::MIN) as u16; - demod - }) - .last() - .unwrap(); + // bit are peak amplitude + // x: 15 bit, 16 asl + // lo: 31 bit, 32 asr: 1 bit loss + let mut m = + x.map(|x| self.lo.process(&mut state.lo, (x as i16 as i32) << 16)); + // m: 29 bit 0f plus 29 bit 2f + // lp: 1 bit DC gain + let [si, sq] = state.lowpass.each_mut(); + for (c, s) in self.lowpass.0.iter().zip(si.iter_mut().zip(sq)) { + for m in m.iter_mut() { + m[0] = c.process(s.0, m[0]); + m[1] = c.process(s.1, m[1]); + } + // demod: 30 bit + } + for (y, m) in y.iter_mut().zip(m) { + // output in phase (before CPU clock phase correction) + *y = ((m.re() >> 15) as i16).wrapping_add(i16::MIN) as u16; + } + state.demod = m[B - 1]; } #[inline(always)] diff --git a/src/mpll.rs b/src/mpll.rs index dd2b515ec4..b06b408ec0 100644 --- a/src/mpll.rs +++ b/src/mpll.rs @@ -1,13 +1,10 @@ use core::num::Wrapping; use dsp_fixedpoint::Q32; -use dsp_process::{ - Add, Identity, Pair, Parallel, SplitInplace, SplitProcess, Unsplit, -}; +use dsp_process::{SplitInplace, SplitProcess}; use idsp::iir::{ - BiquadClamp, DirectForm1, pid, + Biquad, BiquadClamp, Cascade, DirectForm, DirectForm1, pid, repr::BiquadRepr, - wdf::{Wdf, WdfState}, }; pub const BATCH_SIZE: usize = 8; @@ -23,18 +20,10 @@ pub const UNITS: pid::Units = pid::Units { y: HZ_PER_LSB, }; -type LpState = ( - ( - Unsplit, - ([WdfState<2>; 2], (WdfState<2>, WdfState<1>)), - ), - Unsplit, -); - #[derive(Debug, Clone, Default)] pub struct MpllState { /// Lowpass state - lp: [LpState; 4], + lp: [DirectForm; 2], /// PID state iir: DirectForm1, /// Current modulation phase @@ -52,19 +41,17 @@ impl MpllState { /// Runtime active settings #[derive(Debug, Clone)] pub struct Mpll { - lp: Pair<[Wdf<2, 0xad>; 2], (Wdf<2, 0xad>, Wdf<1, 0xa>), i32>, + lp: Cascade<[Biquad>; 4]>, iir: BiquadClamp, i32>, amplitude: [Q32<16>; 2], } -type Lp = (([f64; 2], [f64; 2]), ([f64; 2], [f64; 1])); - #[derive(Debug, Clone, miniconf::Tree)] #[tree(meta(doc, typename))] pub struct MpllConfig { - /// Lowpass filter poles (7th order wave digital filter allpass pair) + /// Lowpass filter coefficients (8th order biquad) #[tree(with=miniconf::leaf)] - lp: Lp, + lp: [[f64; 5]; 4], /// Filter representation #[tree(rename="repr", typ="&str", with=miniconf::str_leaf, defer=self.iir)] _repr: (), // before iir @@ -79,29 +66,37 @@ pub struct MpllConfig { impl Default for MpllConfig { fn default() -> Self { - // 7th order Cheby2 as WDF-CA, -3 dB @ 0.005*1.28, -112 dB @ 0.018*1.28 - let lp = ( - ( - [-0.982905335393602, 0.9991878840149784], - [-0.9283527198560211, 0.9991355621411774], - ), - ( - [-0.9515541301687671, 0.9991654023997083], - [0.9589369885243411], - ), - ); - // 7th order Cheby2 as WDF-CA, -3 dB @ 0.002*1.28, -112 dB @ 0.008*1.28 - let _lp = ( - ( - [-0.9931253245194313, 0.9998700465308578], - [-0.9707043427084593, 0.9998616710293764], - ), - ( - [-0.9803312351720791, 0.9998664477981001], - [0.9833717980454499], - ), - ); - + // 8th order Gaussian to 12dB, 3dB+153τ @3.3kHz, 108dB @20kHz, gain 2 + let lp = [ + [ + 7.092673331e-05 * 2.0, + 1.418534666e-04 * 2.0, + 7.092673331e-05 * 2.0, + 1.971263763e+00, + -9.715472944e-01, + ], + [ + 2.185730264e-04, + 4.371460527e-04, + 2.185730264e-04, + 1.972048940e+00, + -9.729234576e-01, + ], + [ + 4.830453545e-04, + 9.660916403e-04, + 4.830453545e-04, + 1.975378300e+00, + -9.773108205e-01, + ], + [ + 7.561957464e-04, + 1.512391493e-03, + 7.561957464e-04, + 1.986543883e+00, + -9.895690493e-01, + ], + ]; let mut pid = pid::Pid::default(); pid.order = pid::Order::I; pid.gain.value[pid::Action::P as usize] = -3e3; // Hz/turn @@ -125,22 +120,7 @@ impl Default for MpllConfig { impl MpllConfig { pub fn build(&self) -> Mpll { Mpll { - lp: Pair::new(( - ( - (), - Parallel(( - [ - Wdf::quantize(&self.lp.0.0).unwrap(), - Wdf::quantize(&self.lp.0.1).unwrap(), - ], - ( - Wdf::quantize(&self.lp.1.0).unwrap(), - Wdf::quantize(&self.lp.1.1).unwrap(), - ), - )), - ), - (), - )), + lp: Cascade(self.lp.each_ref().map(|c| Biquad::from(*c))), iir: self.iir.build(&UNITS), amplitude: self .amplitude From c5c7a75c4336b78de6dbc83e07c94916f300e2a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Thu, 26 Mar 2026 14:51:02 +0000 Subject: [PATCH 17/19] mpll: fwhm --- py/stabilizer/mpll.py | 14 ++++++++------ src/fls.rs | 32 +++++++++++++++++--------------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/py/stabilizer/mpll.py b/py/stabilizer/mpll.py index 49c3cbbe13..ef76dc1929 100644 --- a/py/stabilizer/mpll.py +++ b/py/stabilizer/mpll.py @@ -143,18 +143,19 @@ async def main(): f = np.absolute(g) gf = g * f b = np.array([gf * s, gf / s, -f, -1j * f]) - b = np.linalg.pinv(np.hstack((b.real, b.imag)).T) @ -np.hstack( - (gf.real, gf.imag) + b, _res, _rank, _sing = np.linalg.lstsq( + np.hstack((b.real, b.imag)).T, + -np.hstack((gf.real, gf.imag)), + rcond=None, ) q = b[2] + 1j * b[3] g1 = q / (s * b[0] + b[1] / s + 1) angle = np.angle(q) / (2 * np.pi) fmean = np.sqrt(b[1] / b[0]) / (2 * np.pi * t) - width = (1 - np.sqrt(4 * b[0] * b[1] + 1)) / (2 * b[0]) / ( - 2 * np.pi * t - ) - fmean + bb = 4 * b[0] * b[1] + width = (1 - np.sqrt(bb + 1) + np.sqrt(bb)) / (2 * np.pi * t * b[0]) _logger.warning( - "ch%i IQ resonance %g V, %g kHz, %g turns, %g kHz half 1/sqrt(2) width", + "ch%i IQ resonance %g V, %g kHz, %g turns, %g kHz FWHM", ch, np.absolute(q), fmean / 1e3, @@ -168,6 +169,7 @@ async def main(): ax[ch, 0].semilogx(f, g.imag) ax[ch, 0].semilogx(f, g1.real) ax[ch, 0].semilogx(f, g1.imag) + ax[ch, 0].semilogx(f, np.absolute(g1)) ax[ch, 0].grid() ax[ch, 1].plot(g.real, g.imag) ax[ch, 1].plot(g1.real, g1.imag) diff --git a/src/fls.rs b/src/fls.rs index f886d21fc7..7972450594 100644 --- a/src/fls.rs +++ b/src/fls.rs @@ -26,7 +26,7 @@ pub const SAMPLE_TICKS_E: usize = 7; // t = q * (PLL_M << PLL_E) const PLL_M: Wrapping = Wrapping(5); // 5 ns const PLL_E: usize = - 16 // PLL input align + 16 // PLL input align ASL + SAMPLE_TICKS_E // sample timer divider + 1 // 10ns timer clock: 5 ns M, 1 E - 1 // 2 ns DDS sample clock @@ -43,18 +43,18 @@ const N_E: usize = 4; /// Fixed LO LUT mixer #[derive(Debug, Clone)] -struct FixLo { +struct FixMix { lo: [Q32<32>; N], f: usize, pi_half: usize, } #[derive(Debug, Clone, Default)] -pub struct FixState { +struct FixMixState { i: usize, } -impl FixLo { +impl FixMix { pub fn new(f: usize, p: f32) -> Self { Self { f, @@ -71,8 +71,10 @@ impl FixLo { } } -impl SplitProcess, FixState> for FixLo { - fn process(&self, state: &mut FixState, x: i32) -> Complex { +impl SplitProcess, FixMixState> + for FixMix +{ + fn process(&self, state: &mut FixMixState, x: i32) -> Complex { state.i = state.i.wrapping_add(1); Complex([ x * self.lo[state.i % N], @@ -93,7 +95,7 @@ pub const PHASE_UNITS: Units = Units { pub const AMPLITUDE_UNITS: Units = Units { t: 10e-3, - x: 2.048 / (1 << 16) as f32, + x: 2.048 / (1 << 16) as f32, // 2.048 V per 16 bit y: 1.0 / i32::MAX as f32, }; @@ -116,7 +118,7 @@ pub struct Channel { /// Active filter representation #[tree(rename="phase_repr", typ="&str", with=miniconf::str_leaf, defer=self.phase)] - _phase_repr: (), // before repr + _phase_repr: (), // before repr for setup from iter /// Phase-to-frequency filter (units: Hz/turn, s) phase: BiquadRepr, i32>, @@ -179,7 +181,7 @@ impl Default for Channel { #[derive(Debug, Clone)] pub struct ChannelConfig { - lo: FixLo<16>, + lo: FixMix<16>, lowpass: Cascade<[Biquad>; 3]>, phase: BiquadClamp, i32>, amplitude: BiquadClamp, i32>, @@ -190,7 +192,7 @@ impl Channel { pub fn build(&self) -> ChannelConfig { const { assert!(u4::MAX.value() == (1 << N_E) as u8 - 1) }; ChannelConfig { - lo: FixLo::new(self.lo_frequency.value() as _, self.lo_phase), + lo: FixMix::new(self.lo_frequency.value() as _, self.lo_phase), lowpass: Cascade(self.lowpass.each_ref().map(|c| Biquad::from(*c))), min_power: P32::from_f32(self.min_power), phase: self.phase.build(&PHASE_UNITS), @@ -201,7 +203,7 @@ impl Channel { #[derive(Debug, Clone, Default)] pub struct ChannelState { - lo: FixState, + lo: FixMixState, lowpass: Complex<[DirectForm1; 3]>, pub demod: Complex, pub unwrap: Unwrapper, @@ -238,11 +240,11 @@ impl ChannelConfig { } // demod: 30 bit } + state.demod = m[B - 1]; for (y, m) in y.iter_mut().zip(m) { - // output in phase (before CPU clock phase correction) + // output in phase (before CPU clock phase correction from PLL) *y = ((m.re() >> 15) as i16).wrapping_add(i16::MIN) as u16; } - state.demod = m[B - 1]; } #[inline(always)] @@ -269,8 +271,8 @@ impl ChannelConfig { * ((t << (32 - PLL_E - N_E)) - (PLL_M << (32 - N_E)) * Wrapping(state.lo.i as i32)); let dphase = Wrapping(state.unwrap.process(phase.0)); - // |delta phase| > pi/2 indicates a possible undetected phase slip. - // Neither a necessary nor a sufficient condition though. + // |delta phase| > pi/2 indicates a possible undetected phase slip + // Neither a necessary nor a sufficient condition though if dphase + Wrapping(1 << 30) < Wrapping(0) { state.slips += Wrapping(1); } From 5c47c78464840c5109ff3bf119cedd9b12000efe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Thu, 26 Mar 2026 14:53:35 +0000 Subject: [PATCH 18/19] deps: bump, idsp: git --- Cargo.lock | 91 ++++++++++++++++++++++++++++++++++++++---------------- Cargo.toml | 6 ++-- 2 files changed, 67 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 600a5b7359..30f262a7b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,9 +141,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.56" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "jobserver", @@ -211,8 +211,18 @@ version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] @@ -229,13 +239,37 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + [[package]] name = "darling_macro" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "darling_core", + "darling_core 0.21.3", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", "quote", "syn 2.0.117", ] @@ -254,6 +288,7 @@ dependencies = [ [[package]] name = "dsp-fixedpoint" version = "0.1.0" +source = "git+https://github.com/quartiq/idsp.git#d3f21570d8fd9a5047d527b355472c4689f4068d" dependencies = [ "num-traits", "serde", @@ -262,6 +297,7 @@ dependencies = [ [[package]] name = "dsp-process" version = "0.2.0" +source = "git+https://github.com/quartiq/idsp.git#d3f21570d8fd9a5047d527b355472c4689f4068d" [[package]] name = "dyn-clone" @@ -662,6 +698,7 @@ dependencies = [ [[package]] name = "idsp" version = "0.21.0" +source = "git+https://github.com/quartiq/idsp.git#d3f21570d8fd9a5047d527b355472c4689f4068d" dependencies = [ "bytemuck", "dsp-fixedpoint", @@ -686,9 +723,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jobserver" @@ -702,9 +739,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.182" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libgit2-sys" @@ -726,9 +763,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libz-sys" -version = "1.1.24" +version = "1.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4735e9cbde5aac84a5ce588f6b23a90b9b0b528f6c5a8db8a4aff300463a0839" +checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1" dependencies = [ "cc", "libc", @@ -825,7 +862,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9826a0808d011e3217d93fd84ac670d75782d1f5962438b53a3f580f4dc14016" dependencies = [ - "darling", + "darling 0.21.3", "proc-macro2", "quote", "syn 2.0.117", @@ -969,9 +1006,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ "num_enum_derive", "rustversion", @@ -979,9 +1016,9 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ "proc-macro2", "quote", @@ -990,9 +1027,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "panic-persist" @@ -1017,9 +1054,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" @@ -1108,9 +1145,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -1395,9 +1432,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.17.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" dependencies = [ "serde_core", "serde_with_macros", @@ -1405,11 +1442,11 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.17.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ - "darling", + "darling 0.23.0", "proc-macro2", "quote", "syn 2.0.117", diff --git a/Cargo.toml b/Cargo.toml index 7d1735b09f..c4d13a67bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -173,6 +173,6 @@ debug = true lto = true [patch.crates-io] -idsp = { path = "../idsp" } -dsp-process = { path = "../idsp/dsp-process" } -dsp-fixedpoint = { path = "../idsp/dsp-fixedpoint" } +idsp = { git = "https://github.com/quartiq/idsp.git" } +dsp-process = { git = "https://github.com/quartiq/idsp.git" } +dsp-fixedpoint = { git = "https://github.com/quartiq/idsp.git" } From a52c8f28c79f87451acd9070d1f0e919d7c70d1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Thu, 26 Mar 2026 15:22:08 +0000 Subject: [PATCH 19/19] py: lint --- py/stabilizer/stream.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/py/stabilizer/stream.py b/py/stabilizer/stream.py index c796552f09..44cfb133c1 100644 --- a/py/stabilizer/stream.py +++ b/py/stabilizer/stream.py @@ -47,8 +47,9 @@ class Frame: parsers = {} @classmethod - def register(cls, format): - cls.parsers[format.format_id] = format + def register(cls, fmt): + """Register a format""" + cls.parsers[fmt.format_id] = fmt @classmethod def parse(cls, data):