From 245ac9c1b9e17ec2d17a8dbf506fcab337d6a303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Thu, 20 Nov 2025 12:13:35 +0000 Subject: [PATCH 01/28] mpll: init --- .github/workflows/ci.yml | 1 + .github/workflows/release.yml | 1 + CHANGELOG.md | 1 + Cargo.lock | 156 +++++----- Cargo.toml | 1 + book/src/SUMMARY.md | 1 + book/src/overview.md | 1 + src/bin/mpll.rs | 542 ++++++++++++++++++++++++++++++++++ stream/src/lib.rs | 3 + 9 files changed, 625 insertions(+), 82 deletions(-) create mode 100644 src/bin/mpll.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a0ef72b7..7e92d18f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,6 +65,7 @@ jobs: - run: cargo run --bin dual-iir --target x86_64-unknown-linux-gnu - run: cargo run --bin lockin --target x86_64-unknown-linux-gnu - run: cargo run --bin dds --target x86_64-unknown-linux-gnu + - run: cargo run --bin mpll --target x86_64-unknown-linux-gnu doc: runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9ab9b8f07..b92ca51e0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,6 +27,7 @@ jobs: target/*/release/lockin target/*/release/fls target/*/release/dds + target/*/release/mpll - id: create_release uses: actions/create-release@v1 env: diff --git a/CHANGELOG.md b/CHANGELOG.md index 513bd67fb..1cf6576c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). * Support for "cooked" PID biquad and standard biquad filter prototypes from `idsp` * Defaulting to `s` optimization for debug and release * `fls`: application for fiber length stabilization and phase/frequency measurement/control +* `mpll`: dispersive PLL ### Changed diff --git a/Cargo.lock b/Cargo.lock index 5dcf2b34b..5784832f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,7 +77,7 @@ dependencies = [ "arbitrary-int", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -124,7 +124,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -141,9 +141,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.41" +version = "1.2.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" dependencies = [ "find-msvc-tools", "jobserver", @@ -196,7 +196,7 @@ checksum = "e37549a379a9e0e6e576fd208ee60394ccb8be963889eebba3ffe0980364f472" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -226,7 +226,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -237,7 +237,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -248,7 +248,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -382,9 +382,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "fnv" @@ -403,9 +403,9 @@ dependencies = [ [[package]] name = "fugit" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17186ad64927d5ac8f02c1e77ccefa08ccd9eaa314d5a4772278aa204a22f7e7" +checksum = "4e639847d312d9a82d2e75b0edcc1e934efcc64e6cb7aa94f0b1fbec0bc231d6" dependencies = [ "gcd", ] @@ -531,9 +531,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -544,9 +544,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -557,11 +557,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -572,42 +571,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -645,8 +640,6 @@ dependencies = [ [[package]] name = "idsp" version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16d1e6135ee1f9e22ef8c3ade3f851c8af40f4bb472c5c7739fd7254035fff2e" dependencies = [ "miniconf", "num-complex 0.4.6", @@ -708,9 +701,9 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libz-sys" -version = "1.1.22" +version = "1.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" +checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" dependencies = [ "cc", "libc", @@ -720,9 +713,9 @@ dependencies = [ [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lm75" @@ -809,7 +802,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -966,7 +959,7 @@ checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -1055,9 +1048,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -1081,23 +1074,23 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] name = "proc-macro2" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e0f6df8eaa422d97d72edcd152e1451618fed47fabbdbd5a8864167b1d4aff7" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -1140,7 +1133,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -1183,7 +1176,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -1265,9 +1258,9 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "schemars" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" dependencies = [ "dyn-clone", "ref-cast", @@ -1361,7 +1354,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -1380,9 +1373,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.15.1" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04" +checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1" dependencies = [ "serde_core", "serde_with_macros", @@ -1390,14 +1383,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.15.1" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955" +checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -1650,7 +1643,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -1666,9 +1659,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.108" +version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", @@ -1695,7 +1688,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -1735,7 +1728,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -1746,14 +1739,14 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -1773,9 +1766,9 @@ checksum = "e87a2ed6b42ec5e28cc3b94c09982969e9227600b2e3dcbc1db927a84c06bd69" [[package]] name = "unicode-ident" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "url" @@ -1883,9 +1876,9 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "yafnv" @@ -1898,11 +1891,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -1910,13 +1902,13 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", "synstructure", ] @@ -1937,15 +1929,15 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", "synstructure", ] [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -1954,9 +1946,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -1965,11 +1957,11 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] diff --git a/Cargo.toml b/Cargo.toml index b0c7f57a2..dd3d4ab10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -156,3 +156,4 @@ lto = true [patch.crates-io] serde-reflection = { git = "https://github.com/quartiq/serde-reflection.git", branch = "pub-ser-de" } +idsp = { path = "../idsp" } diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 1ec2e92ff..2d45ed4d2 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -7,3 +7,4 @@ - [Application: Lockin](./firmware/lockin/index.html) - [Application: Urukul DDS](./firmware/dds/index.html) - [Application: Fiber Length Stabilization](./firmware/fls/index.html) +- [Application: Dispersive PLL](./firmware/mpll/index.html) diff --git a/book/src/overview.md b/book/src/overview.md index 343d1fd6c..d56853132 100644 --- a/book/src/overview.md +++ b/book/src/overview.md @@ -38,6 +38,7 @@ information. | [`lockin`](firmware/lockin/index.html) | Lockin amplifier support various various reference sources | | [`dds`](firmware/dds/index.html) | Urukul-AD9912 control over MQTT | | [`fls`](firmware/fls/index.html) | Fiber length stabilizer/Phase-Frequency Metrology and control | +| [`mpll`](firmware/mpll/index.html) | Dispersive PLL | ## Library Documentation diff --git a/src/bin/mpll.rs b/src/bin/mpll.rs new file mode 100644 index 000000000..272322cbc --- /dev/null +++ b/src/bin/mpll.rs @@ -0,0 +1,542 @@ +#![cfg_attr(target_os = "none", no_std)] +#![cfg_attr(target_os = "none", no_main)] + +use core::sync::atomic::{Ordering, fence}; + +use fugit::ExtU32; +use idsp::iir::{Process, Sos, SosClamp, State as SosState, StatefulRef}; +use miniconf::Tree; +use rtic_monotonics::Monotonic; + +use serde::Serialize; +use stabilizer::{ + convert::{DacCode, Gain}, + statistics, +}; + +use platform::{AppSettings, NetSettings}; + +const BATCH_SIZE_LOG2: u32 = 3; +const BATCH_SIZE: usize = 1 << BATCH_SIZE_LOG2; + +// The logarithm of the number of 100MHz timer ticks between each sample. This corresponds with a +// sampling period of 2^7 = 128 ticks. At 100MHz, 10ns per tick, this corresponds to a sampling +// period of 1.28 uS or 781.25 KHz. +const SAMPLE_TICKS_LOG2: u32 = 7; +const SAMPLE_TICKS: u32 = 1 << SAMPLE_TICKS_LOG2; + +#[derive(Clone, Debug, Tree, Default)] +#[tree(meta(doc, typename))] +pub struct Settings { + mpll: App, + net: NetSettings, +} + +impl AppSettings for Settings { + fn new(net: NetSettings) -> Self { + Self { + net, + mpll: App::default(), + } + } + + fn net(&self) -> &NetSettings { + &self.net + } +} + +impl serial_settings::Settings for Settings { + fn reset(&mut self) { + *self = Self { + mpll: App::default(), + net: NetSettings::new(self.net.mac), + } + } +} + +const LP: usize = 4; + +#[derive(Debug, Clone, Default)] +struct MpllState { + /// Lowpass state + lp: [[SosState; LP]; 2], + /// Phase clamp + /// + /// Makes the phase wrap monotonic by clamping it. + /// Aids capture/pull-in. + clamp: idsp::Clamp, + /// PID state + iir: SosState, + /// Current output phase + phase: i32, + /// Current LO samples for downconverting the next batch + lo: [[i32; BATCH_SIZE]; 2], +} + +#[derive(Debug, Clone, Tree)] +#[tree(meta(doc, typename))] +struct Mpll { + /// Lowpass + /// + /// * `2*LP` order second order section + /// * after demodulation before decimation + lp: [Sos<30>; LP], + /// Input phase offset + phase: i32, + /// PID IIR filter + /// + /// Do not use the iir offset as phase offset. + /// Includes frequency limits. + iir: SosClamp<30>, + /// Output amplitude scale + amp: i32, +} + +impl Default for Mpll { + fn default() -> Self { + // Cheby2, -100 dB @ 0.014, scaled + let mut lp = [ + [ + [0.0079552215, -0.0143694552, 0.0079552215], + [1., -1.9232316595, 0.9247726472], + ], + [ + [0.0589409927, -0.1164117092, 0.0589409927], + [1., -1.9376814971, 0.9391517732], + ], + [ + [0.1238200795, -0.2462564036, 0.1238200795], + [1., -1.9603512995, 0.9617350549], + ], + [ + [0.1661807159, -0.3310256507, 0.1661807159], + [1., -1.9856932107, 0.9870289918], + ], + ]; + // add 2*2 gain to compensate mix and reject + for i in [0, 3] { + for i in lp[i][0].iter_mut() { + *i *= 2.0; + } + } + + let mut pid = idsp::iir::PidBuilder::default(); + pid.order(idsp::iir::Order::I); + pid.period(1.0 / BATCH_SIZE as f32); + pid.gain(idsp::iir::Action::P, -6e-3); // fs/turn + pid.gain(idsp::iir::Action::I, -8e-4); // fs/turn/ts = 1/turn + pid.gain(idsp::iir::Action::D, -5e-3); // fs/turn*ts = turn + pid.limit(idsp::iir::Action::D, -0.3); + let mut iir: SosClamp<_> = pid.build().into(); + iir.min = (-0.3 * (1u64 << 32) as f32) as _; + iir.max = (-0.005 * (1u64 << 32) as f32) as _; + + Self { + lp: lp.map(|c| Sos::from(&c)), + phase: (0.0 * (1u64 << 32) as f32) as _, + iir, + amp: (0.1 * (1u64 << 31) as f32) as _, // 1 V + } + } +} + +impl Mpll { + /// Ingest and process a batch of input samples and output new modulation + pub fn process( + &self, + state: &mut MpllState, + x: &[u16; BATCH_SIZE], + y: &mut [u16; BATCH_SIZE], + ) -> [i32; 2] { + // scratch + let mut m = [[0; BATCH_SIZE]; 2]; + // mix + let [m0, m1] = m.each_mut(); + for (x, (m, y)) in x.iter().zip( + m0.iter_mut() + .zip(m1) + .zip(state.lo[0].iter().zip(&state.lo[1])), + ) { + let x = (*x as i16 as i32) << 16; + *m.0 = (((x as i64) * *y.0 as i64) >> 32) as i32; + *m.1 = (((x as i64) * *y.1 as i64) >> 32) as i32; + } + // lowpass + let [s0, s1] = state.lp.each_mut(); + for ((s0, s1), lp) in s0.iter_mut().zip(s1).zip(&self.lp) { + StatefulRef(lp, s0).process_in_place(&mut m[0]); + StatefulRef(lp, s1).process_in_place(&mut m[1]); + } + // decimate + let m = [m[0][BATCH_SIZE - 1], m[1][BATCH_SIZE - 1]]; + // phase + // need full atan2 to support any phase offset + let p = idsp::atan2(m[1], m[0]); + // phase offset before clamp to maximize working range + // clamp + let c = state.clamp.update(p.wrapping_add(self.phase)); + // pid + let f = StatefulRef(&self.iir, &mut state.iir).process(c); + // modulate + let [y0, y1] = state.lo.each_mut(); + state.phase = y0.iter_mut().zip(y1).zip(y).fold( + state.phase, + |mut p, ((y0, y1), yo)| { + p = p.wrapping_add(f); + (*y0, *y1) = idsp::cossin(p); + let y = + ((*y0 as i64 * self.amp as i64) >> (31 + 31 - 15)) as i16; + *yo = DacCode::from(y).0; + p + }, + ); + m + } +} + +#[derive(Clone, Debug, Tree)] +#[tree(meta(doc, typename))] +pub struct App { + ch: [Mpll; 2], + + /// Specifies the telemetry output period in seconds. + telemetry_period: f32, + + /// Specifies the target for data streaming. + #[tree(with=miniconf::leaf)] + stream: stream::Target, +} + +impl Default for App { + fn default() -> Self { + Self { + telemetry_period: 10., + stream: Default::default(), + ch: Default::default(), + } + } +} + +/// Stream data format. +#[derive( + Clone, Copy, Debug, Default, Serialize, bytemuck::Zeroable, bytemuck::Pod, +)] +#[repr(C)] +pub struct Stream { + demod: [i32; 2], + frequency: i32, +} + +/// Channel Telemetry +#[derive(Default, Clone)] +pub struct TelemetryState { + power: statistics::State, + phase: statistics::State, + frequency: statistics::State, +} + +#[derive(Default, Clone, Serialize)] +pub struct TelemetryCooked { + power: statistics::ScaledStatistics, + phase: statistics::ScaledStatistics, + frequency: statistics::ScaledStatistics, +} + +impl From for TelemetryCooked { + fn from(t: TelemetryState) -> Self { + Self { + phase: t.phase.get_scaled(1.0 / (1u64 << 32) as f32), + power: t.power.get_scaled(2.0 / (1u64 << 32) as f32), + frequency: t.frequency.get_scaled(1.0 / (1u64 << 32) as f32), + } + } +} + +#[derive(Default, Clone, Serialize)] +pub struct Telemetry { + ch: [TelemetryCooked; 2], + cpu_temp: f32, +} + +#[cfg(not(target_os = "none"))] +fn main() { + use miniconf::{json::to_json_value, json_schema::TreeJsonSchema}; + let s = Settings::default(); + println!( + "{}", + serde_json::to_string_pretty(&to_json_value(&s).unwrap()).unwrap() + ); + let mut schema = TreeJsonSchema::new(Some(&s)).unwrap(); + schema + .root + .insert("title".to_string(), "Stabilizer mpll".into()); + println!("{}", serde_json::to_string_pretty(&schema.root).unwrap()); +} + +#[cfg(target_os = "none")] +#[rtic::app(device = stabilizer::hardware::hal::stm32, peripherals = true, dispatchers=[DCMI, JPEG, SDMMC])] +mod app { + use super::*; + use stabilizer::hardware::{ + self, SerialTerminal, SystemTimer, Systick, UsbDevice, + adc::{Adc0Input, Adc1Input}, + dac::{Dac0Output, Dac1Output}, + hal, + net::{NetworkState, NetworkUsers}, + timers::SamplingTimer, + }; + use stream::FrameGenerator; + + #[shared] + struct Shared { + usb: UsbDevice, + network: NetworkUsers, + settings: Settings, + active_settings: App, + telemetry: [TelemetryState; 2], + } + + #[local] + struct Local { + usb_terminal: SerialTerminal, + sampling_timer: SamplingTimer, + adcs: (Adc0Input, Adc1Input), + dacs: (Dac0Output, Dac1Output), + state: [MpllState; 2], + generator: FrameGenerator, + cpu_temp_sensor: stabilizer::hardware::cpu_temp_sensor::CpuTempSensor, + } + + #[init] + fn init(c: init::Context) -> (Shared, Local) { + let clock = SystemTimer::new(|| Systick::now().ticks()); + + // Configure the microcontroller + let (mut carrier, _mezzanine, _eem) = hardware::setup::setup::( + c.core, + c.device, + clock, + BATCH_SIZE, + SAMPLE_TICKS, + ); + + carrier.afes[0].set_gain(Gain::G10); + carrier.afes[1].set_gain(Gain::G10); + + let mut network = NetworkUsers::new( + carrier.network_devices.stack, + carrier.network_devices.phy, + clock, + env!("CARGO_BIN_NAME"), + &carrier.settings.net, + carrier.metadata, + ); + + let generator = network.configure_streaming(stream::Format::Mpll); + + let shared = Shared { + network, + usb: carrier.usb, + telemetry: Default::default(), + active_settings: carrier.settings.mpll.clone(), + settings: carrier.settings, + }; + + let mut local = Local { + usb_terminal: carrier.usb_serial, + sampling_timer: carrier.sampling_timer, + adcs: carrier.adcs, + dacs: carrier.dacs, + cpu_temp_sensor: carrier.temperature_sensor, + state: Default::default(), + generator, + }; + + // Enable ADC/DAC events + local.adcs.0.start(); + local.adcs.1.start(); + local.dacs.0.start(); + local.dacs.1.start(); + + // Spawn a settings and telemetry update for default settings. + settings_update::spawn().unwrap(); + telemetry::spawn().unwrap(); + ethernet_link::spawn().unwrap(); + start::spawn().unwrap(); + usb::spawn().unwrap(); + + (shared, local) + } + + #[task(priority = 1, local=[sampling_timer])] + async fn start(c: start::Context) { + Systick::delay(100.millis()).await; + // Start sampling ADCs and DACs. + c.local.sampling_timer.start(); + } + + /// Main DSP processing routine. + /// + /// See `dual-iir` for general notes on processing time and timing. + /// + /// This is an implementation of a externally (DI0) referenced PLL lockin on the ADC0 signal. + /// It outputs either I/Q or power/phase on DAC0/DAC1. Data is normalized to full scale. + /// PLL bandwidth, filter bandwidth, slope, and x/y or power/phase post-filters are available. + #[task(binds=DMA1_STR4, shared=[active_settings, telemetry], local=[adcs, dacs, state, generator], priority=3)] + #[unsafe(link_section = ".itcm.process")] + fn process(c: process::Context) { + let process::SharedResources { + active_settings, + telemetry, + .. + } = c.shared; + + let process::LocalResources { + adcs: (adc0, adc1), + dacs: (dac0, dac1), + state, + generator, + .. + } = c.local; + + (active_settings, telemetry).lock(|settings, telemetry| { + (adc0, adc1, dac0, dac1).lock(|adc0, adc1, dac0, dac1| { + // Preserve instruction and data ordering w.r.t. DMA flag access. + fence(Ordering::SeqCst); + let adc: [&[u16; BATCH_SIZE]; 2] = [ + (**adc0).try_into().unwrap(), + (**adc1).try_into().unwrap(), + ]; + let mut dac: [&mut [u16; BATCH_SIZE]; 2] = + [(*dac0).try_into().unwrap(), (*dac1).try_into().unwrap()]; + + let mut stream = [Stream::default(); 2]; + + for (((mpll, state), (a, d)), (stream, tele)) in settings + .ch + .iter() + .zip(state.into_iter()) + .zip(adc.iter().zip(dac.iter_mut())) + .zip(stream.iter_mut().zip(telemetry)) + .take(1) + { + stream.demod = mpll.process(state, *a, *d); + stream.frequency = state.iir.xy[2]; + tele.frequency.update(state.iir.xy[2]); + tele.phase.update(state.iir.xy[0]); + tele.power.update( + (((stream.demod[0] as i64 * stream.demod[0] as i64) + + (stream.demod[1] as i64 + * stream.demod[1] as i64)) + >> 32) as i32, + ); + } + + const N: usize = core::mem::size_of::<[Stream; 2]>(); + generator.add(|buf| { + buf[..N].copy_from_slice(bytemuck::cast_slice(&stream)); + N + }); + + // Preserve instruction and data ordering w.r.t. DMA flag access. + fence(Ordering::SeqCst); + }); + }); + } + + #[idle(shared=[settings, network, usb])] + fn idle(mut c: idle::Context) -> ! { + loop { + match (&mut c.shared.network, &mut c.shared.settings) + .lock(|net, settings| net.update(&mut settings.mpll)) + { + NetworkState::SettingsChanged => { + settings_update::spawn().unwrap() + } + NetworkState::Updated => {} + NetworkState::NoChange => { + // We can't sleep if USB is not in suspend. + if c.shared.usb.lock(|usb| { + usb.state() + == usb_device::device::UsbDeviceState::Suspend + }) { + cortex_m::asm::wfi(); + } + } + } + } + } + + #[task(priority = 1, shared=[network, settings, active_settings])] + async fn settings_update(mut c: settings_update::Context) { + c.shared.settings.lock(|settings| { + c.shared + .network + .lock(|net| net.direct_stream(settings.mpll.stream)); + + c.shared + .active_settings + .lock(|current| *current = settings.mpll.clone()); + }); + } + + #[task(priority = 1, local=[cpu_temp_sensor], shared=[network, settings, telemetry])] + async fn telemetry(mut c: telemetry::Context) -> ! { + loop { + let tele = Telemetry { + ch: c + .shared + .telemetry + .lock(|t| core::mem::take(t)) + .map(Into::into), + cpu_temp: c + .local + .cpu_temp_sensor + .get_temperature() + .unwrap_or_default(), + }; + c.shared.network.lock(|net| { + net.telemetry.publish_telemetry("/telemetry", &tele) + }); + + let telemetry_period = + c.shared.settings.lock(|s| s.mpll.telemetry_period); + Systick::delay(((telemetry_period * 1000.0) 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| { + usb.poll(&mut [c + .local + .usb_terminal + .interface_mut() + .inner_mut()]); + }); + + c.shared.settings.lock(|settings| { + if c.local.usb_terminal.poll(settings).unwrap() { + settings_update::spawn().unwrap() + } + }); + + Systick::delay(10.millis()).await; + } + } + + #[task(priority = 1, shared=[network])] + async fn ethernet_link(mut c: ethernet_link::Context) -> ! { + loop { + c.shared.network.lock(|net| net.processor.handle_link()); + Systick::delay(1.secs()).await; + } + } + + #[task(binds = ETH, priority = 1)] + fn eth(_: eth::Context) { + unsafe { hal::ethernet::interrupt_handler() } + } +} diff --git a/stream/src/lib.rs b/stream/src/lib.rs index f7548e9ae..55f60212d 100644 --- a/stream/src/lib.rs +++ b/stream/src/lib.rs @@ -92,6 +92,9 @@ pub enum Format { /// Thermostat-EEM data. See `thermostat-eem` repo and application. ThermostatEem = 3, + + /// MPLL data + Mpll = 4, } #[cfg(target_arch = "arm")] From 388c53f6120822738d771021dec8adf2049f1e68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Thu, 20 Nov 2025 17:08:40 +0000 Subject: [PATCH 02/28] net: print app name --- src/bin/mpll.rs | 33 ++++++++++++--------------------- src/hardware/net.rs | 3 +++ 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/bin/mpll.rs b/src/bin/mpll.rs index 272322cbc..a2e50a0ba 100644 --- a/src/bin/mpll.rs +++ b/src/bin/mpll.rs @@ -10,7 +10,7 @@ use rtic_monotonics::Monotonic; use serde::Serialize; use stabilizer::{ - convert::{DacCode, Gain}, + convert::{Gain}, statistics, }; @@ -123,10 +123,10 @@ impl Default for Mpll { let mut pid = idsp::iir::PidBuilder::default(); pid.order(idsp::iir::Order::I); pid.period(1.0 / BATCH_SIZE as f32); - pid.gain(idsp::iir::Action::P, -6e-3); // fs/turn - pid.gain(idsp::iir::Action::I, -8e-4); // fs/turn/ts = 1/turn - pid.gain(idsp::iir::Action::D, -5e-3); // fs/turn*ts = turn - pid.limit(idsp::iir::Action::D, -0.3); + pid.gain(idsp::iir::Action::P, -2e-3); // fs/turn + pid.gain(idsp::iir::Action::I, -1e-4); // fs/turn/ts = 1/turn + //pid.gain(idsp::iir::Action::D, -5e-3); // fs/turn*ts = turn + //pid.limit(idsp::iir::Action::D, -0.3); let mut iir: SosClamp<_> = pid.build().into(); iir.min = (-0.3 * (1u64 << 32) as f32) as _; iir.max = (-0.005 * (1u64 << 32) as f32) as _; @@ -186,7 +186,7 @@ impl Mpll { (*y0, *y1) = idsp::cossin(p); let y = ((*y0 as i64 * self.amp as i64) >> (31 + 31 - 15)) as i16; - *yo = DacCode::from(y).0; + *yo = y.wrapping_add(i16::MIN) as u16; p }, ); @@ -358,7 +358,6 @@ mod app { local.dacs.0.start(); local.dacs.1.start(); - // Spawn a settings and telemetry update for default settings. settings_update::spawn().unwrap(); telemetry::spawn().unwrap(); ethernet_link::spawn().unwrap(); @@ -375,13 +374,6 @@ mod app { c.local.sampling_timer.start(); } - /// Main DSP processing routine. - /// - /// See `dual-iir` for general notes on processing time and timing. - /// - /// This is an implementation of a externally (DI0) referenced PLL lockin on the ADC0 signal. - /// It outputs either I/Q or power/phase on DAC0/DAC1. Data is normalized to full scale. - /// PLL bandwidth, filter bandwidth, slope, and x/y or power/phase post-filters are available. #[task(binds=DMA1_STR4, shared=[active_settings, telemetry], local=[adcs, dacs, state, generator], priority=3)] #[unsafe(link_section = ".itcm.process")] fn process(c: process::Context) { @@ -410,17 +402,17 @@ mod app { let mut dac: [&mut [u16; BATCH_SIZE]; 2] = [(*dac0).try_into().unwrap(), (*dac1).try_into().unwrap()]; - let mut stream = [Stream::default(); 2]; + let mut streams = [Stream::default(); 2]; for (((mpll, state), (a, d)), (stream, tele)) in settings .ch .iter() .zip(state.into_iter()) .zip(adc.iter().zip(dac.iter_mut())) - .zip(stream.iter_mut().zip(telemetry)) + .zip(streams.iter_mut().zip(telemetry)) .take(1) { - stream.demod = mpll.process(state, *a, *d); + stream.demod = mpll.process(state, a, d); stream.frequency = state.iir.xy[2]; tele.frequency.update(state.iir.xy[2]); tele.phase.update(state.iir.xy[0]); @@ -434,7 +426,7 @@ mod app { const N: usize = core::mem::size_of::<[Stream; 2]>(); generator.add(|buf| { - buf[..N].copy_from_slice(bytemuck::cast_slice(&stream)); + buf[..N].copy_from_slice(bytemuck::cast_slice(&streams)); N }); @@ -499,9 +491,8 @@ mod app { net.telemetry.publish_telemetry("/telemetry", &tele) }); - let telemetry_period = - c.shared.settings.lock(|s| s.mpll.telemetry_period); - Systick::delay(((telemetry_period * 1000.0) as u32).millis()).await; + let delay = c.shared.settings.lock(|s| s.mpll.telemetry_period); + Systick::delay(((delay * 1000.0) as u32).millis()).await; } } diff --git a/src/hardware/net.rs b/src/hardware/net.rs index 1edfcca2c..f55a9dafe 100644 --- a/src/hardware/net.rs +++ b/src/hardware/net.rs @@ -6,6 +6,7 @@ //! streaming over raw UDP/TCP sockets. This module encompasses the main processing routines //! related to Stabilizer networking operations. use heapless; +use log::info; use miniconf; use crate::hardware::{SystemTimer, hal::ethernet}; @@ -107,6 +108,8 @@ where net_settings: &NetSettings, metadata: &'static ApplicationMetadata, ) -> Self { + info!("Application: {}", app); + let stack_manager = cortex_m::singleton!(: NetworkManager = NetworkManager::new(stack)) .unwrap(); From 141956066703ab7fc83333ad4565eb6364588c0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Sat, 22 Nov 2025 12:56:47 +0000 Subject: [PATCH 03/28] mpll: working --- src/bin/mpll.rs | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/bin/mpll.rs b/src/bin/mpll.rs index a2e50a0ba..dabed9910 100644 --- a/src/bin/mpll.rs +++ b/src/bin/mpll.rs @@ -9,10 +9,7 @@ use miniconf::Tree; use rtic_monotonics::Monotonic; use serde::Serialize; -use stabilizer::{ - convert::{Gain}, - statistics, -}; +use stabilizer::{convert::Gain, statistics}; use platform::{AppSettings, NetSettings}; @@ -123,13 +120,13 @@ impl Default for Mpll { let mut pid = idsp::iir::PidBuilder::default(); pid.order(idsp::iir::Order::I); pid.period(1.0 / BATCH_SIZE as f32); - pid.gain(idsp::iir::Action::P, -2e-3); // fs/turn - pid.gain(idsp::iir::Action::I, -1e-4); // fs/turn/ts = 1/turn - //pid.gain(idsp::iir::Action::D, -5e-3); // fs/turn*ts = turn - //pid.limit(idsp::iir::Action::D, -0.3); + pid.gain(idsp::iir::Action::P, -6e-3); // fs/turn + pid.gain(idsp::iir::Action::I, -8e-4); // fs/turn/ts = 1/turn + pid.gain(idsp::iir::Action::D, -5e-3); // fs/turn*ts = turn + pid.limit(idsp::iir::Action::D, -0.3); let mut iir: SosClamp<_> = pid.build().into(); - iir.min = (-0.3 * (1u64 << 32) as f32) as _; - iir.max = (-0.005 * (1u64 << 32) as f32) as _; + iir.max = (0.3 * (1u64 << 32) as f32) as _; + iir.min = (0.005 * (1u64 << 32) as f32) as _; Self { lp: lp.map(|c| Sos::from(&c)), @@ -157,6 +154,7 @@ impl Mpll { .zip(m1) .zip(state.lo[0].iter().zip(&state.lo[1])), ) { + // ADC encoding let x = (*x as i16 as i32) << 16; *m.0 = (((x as i64) * *y.0 as i64) >> 32) as i32; *m.1 = (((x as i64) * *y.1 as i64) >> 32) as i32; @@ -186,7 +184,7 @@ impl Mpll { (*y0, *y1) = idsp::cossin(p); let y = ((*y0 as i64 * self.amp as i64) >> (31 + 31 - 15)) as i16; - *yo = y.wrapping_add(i16::MIN) as u16; + *yo = y.wrapping_add(i16::MIN) as u16; // DAC encoding p }, ); @@ -223,7 +221,8 @@ impl Default for App { )] #[repr(C)] pub struct Stream { - demod: [i32; 2], + raw: [u16; BATCH_SIZE], + demod: [i16; 2], frequency: i32, } @@ -247,7 +246,9 @@ impl From for TelemetryCooked { Self { phase: t.phase.get_scaled(1.0 / (1u64 << 32) as f32), power: t.power.get_scaled(2.0 / (1u64 << 32) as f32), - frequency: t.frequency.get_scaled(1.0 / (1u64 << 32) as f32), + frequency: t + .frequency + .get_scaled(100e6 / SAMPLE_TICKS as f32 / (1u64 << 32) as f32), } } } @@ -411,16 +412,17 @@ mod app { .zip(adc.iter().zip(dac.iter_mut())) .zip(streams.iter_mut().zip(telemetry)) .take(1) + // TODO: relax { - stream.demod = mpll.process(state, a, d); + stream.raw = (*a).clone(); + let demod = mpll.process(state, a, d); + stream.demod = demod.map(|d| (d >> 16) as _); stream.frequency = state.iir.xy[2]; tele.frequency.update(state.iir.xy[2]); tele.phase.update(state.iir.xy[0]); tele.power.update( - (((stream.demod[0] as i64 * stream.demod[0] as i64) - + (stream.demod[1] as i64 - * stream.demod[1] as i64)) - >> 32) as i32, + stream.demod[0] as i32 * stream.demod[0] as i32 + + stream.demod[1] as i32 * stream.demod[1] as i32, ); } From a50de5be835d5b55f746b4d0e3b9f53b8eab653a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Sat, 22 Nov 2025 20:24:45 +0000 Subject: [PATCH 04/28] mpll: reference and relative --- src/bin/mpll.rs | 174 +++++++++++++++++++++++++----------------------- 1 file changed, 90 insertions(+), 84 deletions(-) diff --git a/src/bin/mpll.rs b/src/bin/mpll.rs index dabed9910..eb6b623a4 100644 --- a/src/bin/mpll.rs +++ b/src/bin/mpll.rs @@ -16,9 +16,7 @@ use platform::{AppSettings, NetSettings}; const BATCH_SIZE_LOG2: u32 = 3; const BATCH_SIZE: usize = 1 << BATCH_SIZE_LOG2; -// The logarithm of the number of 100MHz timer ticks between each sample. This corresponds with a -// sampling period of 2^7 = 128 ticks. At 100MHz, 10ns per tick, this corresponds to a sampling -// period of 1.28 uS or 781.25 KHz. +// 100 MHz timer, 128 divider: period of 1.28 µs, ~781.25 KHz. const SAMPLE_TICKS_LOG2: u32 = 7; const SAMPLE_TICKS: u32 = 1 << SAMPLE_TICKS_LOG2; @@ -54,9 +52,9 @@ impl serial_settings::Settings for Settings { const LP: usize = 4; #[derive(Debug, Clone, Default)] -struct MpllState { +pub struct MpllState { /// Lowpass state - lp: [[SosState; LP]; 2], + lp: [[SosState; LP]; 4], /// Phase clamp /// /// Makes the phase wrap monotonic by clamping it. @@ -72,7 +70,7 @@ struct MpllState { #[derive(Debug, Clone, Tree)] #[tree(meta(doc, typename))] -struct Mpll { +pub struct Mpll { /// Lowpass /// /// * `2*LP` order second order section @@ -86,7 +84,13 @@ struct Mpll { /// Includes frequency limits. iir: SosClamp<30>, /// Output amplitude scale - amp: i32, + amp: [i32; 2], +} + +impl Mpll { + fn new(config: &Self) -> Self { + config.clone() + } } impl Default for Mpll { @@ -132,7 +136,7 @@ impl Default for Mpll { lp: lp.map(|c| Sos::from(&c)), phase: (0.0 * (1u64 << 32) as f32) as _, iir, - amp: (0.1 * (1u64 << 31) as f32) as _, // 1 V + amp: [(0.1 * (1u64 << 31) as f32) as _; 2], // 1 V } } } @@ -142,34 +146,48 @@ impl Mpll { pub fn process( &self, state: &mut MpllState, - x: &[u16; BATCH_SIZE], - y: &mut [u16; BATCH_SIZE], - ) -> [i32; 2] { + x: &[&[u16; BATCH_SIZE]; 2], + y: &mut [&mut [u16; BATCH_SIZE]; 2], + ) -> Stream { // scratch - let mut m = [[0; BATCH_SIZE]; 2]; + let mut m = [[0; BATCH_SIZE]; 4]; // mix - let [m0, m1] = m.each_mut(); - for (x, (m, y)) in x.iter().zip( - m0.iter_mut() - .zip(m1) + let [m0i, m0q, m1i, m1q] = m.each_mut(); + for (x, (m, y)) in x[0].iter().zip(x[1].iter()).zip( + m0i.iter_mut() + .zip(m0q) + .zip(m1i.iter_mut().zip(m1q)) .zip(state.lo[0].iter().zip(&state.lo[1])), ) { // ADC encoding - let x = (*x as i16 as i32) << 16; - *m.0 = (((x as i64) * *y.0 as i64) >> 32) as i32; - *m.1 = (((x as i64) * *y.1 as i64) >> 32) as i32; + let x0 = (*x.0 as i16 as i32) << 16; + *m.0.0 = (((x0 as i64) * *y.0 as i64) >> 32) as i32; + *m.0.1 = (((x0 as i64) * *y.1 as i64) >> 32) as i32; + let x1 = (*x.1 as i16 as i32) << 16; + *m.1.0 = (((x1 as i64) * *y.0 as i64) >> 32) as i32; + *m.1.1 = (((x1 as i64) * *y.1 as i64) >> 32) as i32; } // lowpass - let [s0, s1] = state.lp.each_mut(); - for ((s0, s1), lp) in s0.iter_mut().zip(s1).zip(&self.lp) { - StatefulRef(lp, s0).process_in_place(&mut m[0]); - StatefulRef(lp, s1).process_in_place(&mut m[1]); + let [s0i, s0q, s1i, s1q] = state.lp.each_mut(); + for (s, lp) in s0i + .iter_mut() + .zip(s0q) + .zip(s1i.iter_mut().zip(s1q)) + .zip(&self.lp) + { + StatefulRef(lp, s.0.0).process_in_place(&mut m[0]); + StatefulRef(lp, s.0.1).process_in_place(&mut m[1]); + StatefulRef(lp, s.1.0).process_in_place(&mut m[2]); + StatefulRef(lp, s.1.1).process_in_place(&mut m[3]); } // decimate - let m = [m[0][BATCH_SIZE - 1], m[1][BATCH_SIZE - 1]]; + let m = [ + [m[0][BATCH_SIZE - 1], m[1][BATCH_SIZE - 1]], + [m[2][BATCH_SIZE - 1], m[3][BATCH_SIZE - 1]], + ]; // phase - // need full atan2 to support any phase offset - let p = idsp::atan2(m[1], m[0]); + // need full atan2 or addtl osc to support any phase offset + let p = idsp::atan2(m[0][1], m[0][0]); // phase offset before clamp to maximize working range // clamp let c = state.clamp.update(p.wrapping_add(self.phase)); @@ -177,25 +195,33 @@ impl Mpll { let f = StatefulRef(&self.iir, &mut state.iir).process(c); // modulate let [y0, y1] = state.lo.each_mut(); - state.phase = y0.iter_mut().zip(y1).zip(y).fold( - state.phase, - |mut p, ((y0, y1), yo)| { + let [yo0, yo1] = y; + state.phase = y0 + .iter_mut() + .zip(y1) + .zip((*yo0).iter_mut().zip((*yo1).iter_mut())) + .fold(state.phase, |mut p, (y, yo)| { p = p.wrapping_add(f); - (*y0, *y1) = idsp::cossin(p); - let y = - ((*y0 as i64 * self.amp as i64) >> (31 + 31 - 15)) as i16; - *yo = y.wrapping_add(i16::MIN) as u16; // DAC encoding + (*y.0, *y.1) = idsp::cossin(p); + *yo.0 = (((*y.0 as i64 * self.amp[0] as i64) >> (31 + 31 - 15)) + as i16) + .wrapping_add(i16::MIN) as u16; // DAC encoding + *yo.1 = (((*y.1 as i64 * self.amp[1] as i64) >> (31 + 31 - 15)) + as i16) + .wrapping_add(i16::MIN) as u16; // DAC encoding p - }, - ); - m + }); + Stream { + demod: m, + frequency: state.iir.xy[2], + } } } #[derive(Clone, Debug, Tree)] #[tree(meta(doc, typename))] pub struct App { - ch: [Mpll; 2], + mpll: Mpll, /// Specifies the telemetry output period in seconds. telemetry_period: f32, @@ -210,7 +236,7 @@ impl Default for App { Self { telemetry_period: 10., stream: Default::default(), - ch: Default::default(), + mpll: Default::default(), } } } @@ -221,31 +247,30 @@ impl Default for App { )] #[repr(C)] pub struct Stream { - raw: [u16; BATCH_SIZE], - demod: [i16; 2], + demod: [[i32; 2]; 2], frequency: i32, } /// Channel Telemetry #[derive(Default, Clone)] pub struct TelemetryState { - power: statistics::State, - phase: statistics::State, + demod: [[statistics::State; 2]; 2], frequency: statistics::State, } #[derive(Default, Clone, Serialize)] pub struct TelemetryCooked { - power: statistics::ScaledStatistics, - phase: statistics::ScaledStatistics, + demod: [[statistics::ScaledStatistics; 2]; 2], frequency: statistics::ScaledStatistics, } impl From for TelemetryCooked { fn from(t: TelemetryState) -> Self { Self { - phase: t.phase.get_scaled(1.0 / (1u64 << 32) as f32), - power: t.power.get_scaled(2.0 / (1u64 << 32) as f32), + // G10, 1 V fs + demod: t + .demod + .map(|d| d.map(|p| p.get_scaled(1.0 / (1u64 << 31) as f32))), frequency: t .frequency .get_scaled(100e6 / SAMPLE_TICKS as f32 / (1u64 << 32) as f32), @@ -255,7 +280,7 @@ impl From for TelemetryCooked { #[derive(Default, Clone, Serialize)] pub struct Telemetry { - ch: [TelemetryCooked; 2], + mpll: TelemetryCooked, cpu_temp: f32, } @@ -293,8 +318,8 @@ mod app { usb: UsbDevice, network: NetworkUsers, settings: Settings, - active_settings: App, - telemetry: [TelemetryState; 2], + active_settings: Mpll, + telemetry: TelemetryState, } #[local] @@ -303,7 +328,7 @@ mod app { sampling_timer: SamplingTimer, adcs: (Adc0Input, Adc1Input), dacs: (Dac0Output, Dac1Output), - state: [MpllState; 2], + state: MpllState, generator: FrameGenerator, cpu_temp_sensor: stabilizer::hardware::cpu_temp_sensor::CpuTempSensor, } @@ -339,7 +364,7 @@ mod app { network, usb: carrier.usb, telemetry: Default::default(), - active_settings: carrier.settings.mpll.clone(), + active_settings: carrier.settings.mpll.mpll.clone(), settings: carrier.settings, }; @@ -403,32 +428,19 @@ mod app { let mut dac: [&mut [u16; BATCH_SIZE]; 2] = [(*dac0).try_into().unwrap(), (*dac1).try_into().unwrap()]; - let mut streams = [Stream::default(); 2]; - - for (((mpll, state), (a, d)), (stream, tele)) in settings - .ch - .iter() - .zip(state.into_iter()) - .zip(adc.iter().zip(dac.iter_mut())) - .zip(streams.iter_mut().zip(telemetry)) - .take(1) - // TODO: relax - { - stream.raw = (*a).clone(); - let demod = mpll.process(state, a, d); - stream.demod = demod.map(|d| (d >> 16) as _); - stream.frequency = state.iir.xy[2]; - tele.frequency.update(state.iir.xy[2]); - tele.phase.update(state.iir.xy[0]); - tele.power.update( - stream.demod[0] as i32 * stream.demod[0] as i32 - + stream.demod[1] as i32 * stream.demod[1] as i32, - ); - } + let stream = settings.process(state, &adc, &mut dac); + telemetry.frequency.update(stream.frequency); - const N: usize = core::mem::size_of::<[Stream; 2]>(); + telemetry.demod[0][0].update(stream.demod[0][0]); + telemetry.demod[0][1].update(stream.demod[0][1]); + telemetry.demod[1][0].update(stream.demod[1][0]); + telemetry.demod[1][1].update(stream.demod[1][1]); + + const N: usize = core::mem::size_of::(); generator.add(|buf| { - buf[..N].copy_from_slice(bytemuck::cast_slice(&streams)); + buf[..N].copy_from_slice(bytemuck::cast_slice( + bytemuck::bytes_of(&stream), + )); N }); @@ -467,10 +479,8 @@ mod app { c.shared .network .lock(|net| net.direct_stream(settings.mpll.stream)); - - c.shared - .active_settings - .lock(|current| *current = settings.mpll.clone()); + let new = Mpll::new(&settings.mpll.mpll); + c.shared.active_settings.lock(|current| *current = new); }); } @@ -478,11 +488,7 @@ mod app { async fn telemetry(mut c: telemetry::Context) -> ! { loop { let tele = Telemetry { - ch: c - .shared - .telemetry - .lock(|t| core::mem::take(t)) - .map(Into::into), + mpll: c.shared.telemetry.lock(|t| core::mem::take(t)).into(), cpu_temp: c .local .cpu_temp_sensor From 40affdb592958160a739184409adc083ba43cf16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Mon, 24 Nov 2025 10:47:52 +0000 Subject: [PATCH 05/28] mpll: remove clamp --- src/bin/mpll.rs | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/src/bin/mpll.rs b/src/bin/mpll.rs index eb6b623a4..469748638 100644 --- a/src/bin/mpll.rs +++ b/src/bin/mpll.rs @@ -55,11 +55,6 @@ const LP: usize = 4; pub struct MpllState { /// Lowpass state lp: [[SosState; LP]; 4], - /// Phase clamp - /// - /// Makes the phase wrap monotonic by clamping it. - /// Aids capture/pull-in. - clamp: idsp::Clamp, /// PID state iir: SosState, /// Current output phase @@ -114,8 +109,8 @@ impl Default for Mpll { [1., -1.9856932107, 0.9870289918], ], ]; - // add 2*2 gain to compensate mix and reject - for i in [0, 3] { + // add 2 gain to compensate mix shift + for i in [0] { for i in lp[i][0].iter_mut() { *i *= 2.0; } @@ -136,7 +131,7 @@ impl Default for Mpll { lp: lp.map(|c| Sos::from(&c)), phase: (0.0 * (1u64 << 32) as f32) as _, iir, - amp: [(0.1 * (1u64 << 31) as f32) as _; 2], // 1 V + amp: [(0.09 * (1u64 << 31) as f32) as _; 2], // ~0.9 V } } } @@ -153,19 +148,17 @@ impl Mpll { let mut m = [[0; BATCH_SIZE]; 4]; // mix let [m0i, m0q, m1i, m1q] = m.each_mut(); - for (x, (m, y)) in x[0].iter().zip(x[1].iter()).zip( + for (x, (m, lo)) in x[0].iter().zip(x[1].iter()).zip( m0i.iter_mut() .zip(m0q) .zip(m1i.iter_mut().zip(m1q)) .zip(state.lo[0].iter().zip(&state.lo[1])), ) { - // ADC encoding - let x0 = (*x.0 as i16 as i32) << 16; - *m.0.0 = (((x0 as i64) * *y.0 as i64) >> 32) as i32; - *m.0.1 = (((x0 as i64) * *y.1 as i64) >> 32) as i32; - let x1 = (*x.1 as i16 as i32) << 16; - *m.1.0 = (((x1 as i64) * *y.0 as i64) >> 32) as i32; - *m.1.1 = (((x1 as i64) * *y.1 as i64) >> 32) as i32; + // ADC encoding, mix + *m.0.0 = ((*x.0 as i16 as i64 * *lo.0 as i64) >> 16) as i32; + *m.0.1 = ((*x.0 as i16 as i64 * *lo.1 as i64) >> 16) as i32; + *m.1.0 = ((*x.1 as i16 as i64 * *lo.0 as i64) >> 16) as i32; + *m.1.1 = ((*x.1 as i16 as i64 * *lo.1 as i64) >> 16) as i32; } // lowpass let [s0i, s0q, s1i, s1q] = state.lp.each_mut(); @@ -189,8 +182,7 @@ impl Mpll { // need full atan2 or addtl osc to support any phase offset let p = idsp::atan2(m[0][1], m[0][0]); // phase offset before clamp to maximize working range - // clamp - let c = state.clamp.update(p.wrapping_add(self.phase)); + let c = p.wrapping_add(self.phase); // pid let f = StatefulRef(&self.iir, &mut state.iir).process(c); // modulate From feb57bac946f35ef2fe94310470a051a9d852d29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Tue, 25 Nov 2025 10:31:00 +0000 Subject: [PATCH 06/28] mpll: new filter --- src/bin/mpll.rs | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/bin/mpll.rs b/src/bin/mpll.rs index 469748638..06c7cec65 100644 --- a/src/bin/mpll.rs +++ b/src/bin/mpll.rs @@ -55,12 +55,18 @@ const LP: usize = 4; pub struct MpllState { /// Lowpass state lp: [[SosState; LP]; 4], + // /// Phase clamp + // /// + // /// Makes the phase wrap monotonic by clamping it. + // /// Aids capture/pull-in. + // clamp: idsp::Clamp, /// PID state iir: SosState, /// Current output phase phase: i32, /// Current LO samples for downconverting the next batch lo: [[i32; BATCH_SIZE]; 2], + // TODO: investigate matched LO delay: 20 samples } #[derive(Debug, Clone, Tree)] @@ -71,8 +77,8 @@ pub struct Mpll { /// * `2*LP` order second order section /// * after demodulation before decimation lp: [Sos<30>; LP], - /// Input phase offset - phase: i32, + // /// Input phase offset + // phase: i32, /// PID IIR filter /// /// Do not use the iir offset as phase offset. @@ -90,23 +96,23 @@ impl Mpll { impl Default for Mpll { fn default() -> Self { - // Cheby2, -100 dB @ 0.014, scaled + // Cheby2, -100 dB @ 0.012, scaled let mut lp = [ [ - [0.0079552215, -0.0143694552, 0.0079552215], - [1., -1.9232316595, 0.9247726472], + [0.0080039091, -0.0141593181, 0.0080039091], + [1., -1.9159128964, 0.9177613957], ], [ - [0.0589409927, -0.1164117092, 0.0589409927], - [1., -1.9376814971, 0.9391517732], + [0.0588374268, -0.1159099406, 0.0588374268], + [1., -1.93166913, 0.9334340431], ], [ - [0.1238200795, -0.2462564036, 0.1238200795], - [1., -1.9603512995, 0.9617350549], + [0.1236517783, -0.2456406737, 0.1236517783], + [1., -1.9564304454, 0.9580933293], ], [ - [0.1661807159, -0.3310256507, 0.1661807159], - [1., -1.9856932107, 0.9870289918], + [0.1661327109, -0.3306582132, 0.1661327109], + [1., -1.9841698138, 0.9857770223], ], ]; // add 2 gain to compensate mix shift @@ -120,16 +126,16 @@ impl Default for Mpll { pid.order(idsp::iir::Order::I); pid.period(1.0 / BATCH_SIZE as f32); pid.gain(idsp::iir::Action::P, -6e-3); // fs/turn - pid.gain(idsp::iir::Action::I, -8e-4); // fs/turn/ts = 1/turn - pid.gain(idsp::iir::Action::D, -5e-3); // fs/turn*ts = turn - pid.limit(idsp::iir::Action::D, -0.3); + pid.gain(idsp::iir::Action::I, -5e-4); // fs/turn/ts = 1/turn + pid.gain(idsp::iir::Action::D, -4e-3); // fs/turn*ts = turn + pid.limit(idsp::iir::Action::D, -0.2); let mut iir: SosClamp<_> = pid.build().into(); iir.max = (0.3 * (1u64 << 32) as f32) as _; iir.min = (0.005 * (1u64 << 32) as f32) as _; Self { lp: lp.map(|c| Sos::from(&c)), - phase: (0.0 * (1u64 << 32) as f32) as _, + // phase: (0.0 * (1u64 << 32) as f32) as _, iir, amp: [(0.09 * (1u64 << 31) as f32) as _; 2], // ~0.9 V } @@ -182,9 +188,9 @@ impl Mpll { // need full atan2 or addtl osc to support any phase offset let p = idsp::atan2(m[0][1], m[0][0]); // phase offset before clamp to maximize working range - let c = p.wrapping_add(self.phase); + // let c = state.clamp.update(p.wrapping_add(self.phase)); // pid - let f = StatefulRef(&self.iir, &mut state.iir).process(c); + let f = StatefulRef(&self.iir, &mut state.iir).process(p); // modulate let [y0, y1] = state.lo.each_mut(); let [yo0, yo1] = y; @@ -234,9 +240,7 @@ impl Default for App { } /// Stream data format. -#[derive( - Clone, Copy, Debug, Default, Serialize, bytemuck::Zeroable, bytemuck::Pod, -)] +#[derive(Clone, Copy, Debug, Default, bytemuck::Zeroable, bytemuck::Pod)] #[repr(C)] pub struct Stream { demod: [[i32; 2]; 2], From 6fc123b6e20c08b38fe7c7438467add989c6b212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Tue, 25 Nov 2025 23:29:01 +0000 Subject: [PATCH 07/28] mpll: cancel lo phase --- src/bin/mpll.rs | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/bin/mpll.rs b/src/bin/mpll.rs index 06c7cec65..f07519b7c 100644 --- a/src/bin/mpll.rs +++ b/src/bin/mpll.rs @@ -96,23 +96,23 @@ impl Mpll { impl Default for Mpll { fn default() -> Self { - // Cheby2, -100 dB @ 0.012, scaled + // Cheby2, -3 dB @ 0.005*1.28, -120 dB @ 0.017*1.28, Chebychev norm scaled, quantized let mut lp = [ [ - [0.0080039091, -0.0141593181, 0.0080039091], - [1., -1.9159128964, 0.9177613957], + [0.0045134034, -0.0073064622, 0.0045134034], + [1., -1.91874131, 0.9204616547], ], [ - [0.0588374268, -0.1159099406, 0.0588374268], - [1., -1.93166913, 0.9334340431], + [0.0327912588, -0.0639008116, 0.0327912588], + [1., -1.9324034546, 0.9340851586], ], [ - [0.1236517783, -0.2456406737, 0.1236517783], - [1., -1.9564304454, 0.9580933293], + [0.0708566532, -0.140079312, 0.0708566532], + [1., -1.9555726107, 0.9572066069], ], [ - [0.1661327109, -0.3306582132, 0.1661327109], - [1., -1.9841698138, 0.9857770223], + [0.0971013568, -0.1925907861, 0.0971013568], + [1., -1.9835639875, 0.9851759151], ], ]; // add 2 gain to compensate mix shift @@ -189,6 +189,8 @@ impl Mpll { let p = idsp::atan2(m[0][1], m[0][0]); // phase offset before clamp to maximize working range // let c = state.clamp.update(p.wrapping_add(self.phase)); + // Delay correction + let p = p.wrapping_sub(state.iir.xy[2].wrapping_mul(10)); // pid let f = StatefulRef(&self.iir, &mut state.iir).process(p); // modulate @@ -211,6 +213,7 @@ impl Mpll { }); Stream { demod: m, + phase: state.iir.xy[0], frequency: state.iir.xy[2], } } @@ -244,6 +247,7 @@ impl Default for App { #[repr(C)] pub struct Stream { demod: [[i32; 2]; 2], + phase: i32, frequency: i32, } @@ -251,12 +255,14 @@ pub struct Stream { #[derive(Default, Clone)] pub struct TelemetryState { demod: [[statistics::State; 2]; 2], + phase: statistics::State, frequency: statistics::State, } #[derive(Default, Clone, Serialize)] pub struct TelemetryCooked { demod: [[statistics::ScaledStatistics; 2]; 2], + phase: statistics::ScaledStatistics, frequency: statistics::ScaledStatistics, } @@ -267,6 +273,7 @@ impl From for TelemetryCooked { demod: t .demod .map(|d| d.map(|p| p.get_scaled(1.0 / (1u64 << 31) as f32))), + phase: t.phase.get_scaled(1.0 / (1u64 << 32) as f32), frequency: t .frequency .get_scaled(100e6 / SAMPLE_TICKS as f32 / (1u64 << 32) as f32), @@ -425,6 +432,7 @@ mod app { [(*dac0).try_into().unwrap(), (*dac1).try_into().unwrap()]; let stream = settings.process(state, &adc, &mut dac); + telemetry.phase.update(stream.phase); telemetry.frequency.update(stream.frequency); telemetry.demod[0][0].update(stream.demod[0][0]); From 3c62b9b04691b61936e24b40ada72280be995709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Wed, 3 Dec 2025 15:17:50 +0000 Subject: [PATCH 08/28] mpll: new idsp --- Cargo.lock | 1 + src/bin/mpll.rs | 25 +++++++++++++------------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5784832f6..d11dc4183 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -641,6 +641,7 @@ dependencies = [ name = "idsp" version = "0.19.0" dependencies = [ + "bytemuck", "miniconf", "num-complex 0.4.6", "num-traits", diff --git a/src/bin/mpll.rs b/src/bin/mpll.rs index f07519b7c..efcc03af6 100644 --- a/src/bin/mpll.rs +++ b/src/bin/mpll.rs @@ -4,7 +4,7 @@ use core::sync::atomic::{Ordering, fence}; use fugit::ExtU32; -use idsp::iir::{Process, Sos, SosClamp, State as SosState, StatefulRef}; +use idsp::iir::{Process, Sos, SosClamp, SosState, StatefulRef}; use miniconf::Tree; use rtic_monotonics::Monotonic; @@ -77,8 +77,8 @@ pub struct Mpll { /// * `2*LP` order second order section /// * after demodulation before decimation lp: [Sos<30>; LP], - // /// Input phase offset - // phase: i32, + /// Input phase offset + phase: i32, /// PID IIR filter /// /// Do not use the iir offset as phase offset. @@ -135,7 +135,7 @@ impl Default for Mpll { Self { lp: lp.map(|c| Sos::from(&c)), - // phase: (0.0 * (1u64 << 32) as f32) as _, + phase: (0.0 * (1u64 << 32) as f32) as _, iir, amp: [(0.09 * (1u64 << 31) as f32) as _; 2], // ~0.9 V } @@ -174,10 +174,10 @@ impl Mpll { .zip(s1i.iter_mut().zip(s1q)) .zip(&self.lp) { - StatefulRef(lp, s.0.0).process_in_place(&mut m[0]); - StatefulRef(lp, s.0.1).process_in_place(&mut m[1]); - StatefulRef(lp, s.1.0).process_in_place(&mut m[2]); - StatefulRef(lp, s.1.1).process_in_place(&mut m[3]); + StatefulRef::new(lp, s.0.0).process_in_place(&mut m[0]); + StatefulRef::new(lp, s.0.1).process_in_place(&mut m[1]); + StatefulRef::new(lp, s.1.0).process_in_place(&mut m[2]); + StatefulRef::new(lp, s.1.1).process_in_place(&mut m[3]); } // decimate let m = [ @@ -187,12 +187,13 @@ impl Mpll { // phase // need full atan2 or addtl osc to support any phase offset let p = idsp::atan2(m[0][1], m[0][0]); - // phase offset before clamp to maximize working range - // let c = state.clamp.update(p.wrapping_add(self.phase)); // Delay correction - let p = p.wrapping_sub(state.iir.xy[2].wrapping_mul(10)); + let p = p + .wrapping_add(self.phase) + .wrapping_sub(state.iir.xy[2].wrapping_mul(10)); + // let p = state.clamp.update(p); // pid - let f = StatefulRef(&self.iir, &mut state.iir).process(p); + let f = StatefulRef::new(&self.iir, &mut state.iir).process(p); // modulate let [y0, y1] = state.lo.each_mut(); let [yo0, yo1] = y; From f2d1227f9837ae22135f616f5f0e84c0aca8e707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Tue, 16 Dec 2025 00:27:59 +0000 Subject: [PATCH 09/28] mpll: idsp changes --- Cargo.lock | 1 - src/bin/mpll.rs | 181 ++++++++++++++++++++++++------------------------ 2 files changed, 90 insertions(+), 92 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d11dc4183..5784832f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -641,7 +641,6 @@ dependencies = [ name = "idsp" version = "0.19.0" dependencies = [ - "bytemuck", "miniconf", "num-complex 0.4.6", "num-traits", diff --git a/src/bin/mpll.rs b/src/bin/mpll.rs index efcc03af6..7c59ab9a6 100644 --- a/src/bin/mpll.rs +++ b/src/bin/mpll.rs @@ -4,7 +4,10 @@ use core::sync::atomic::{Ordering, fence}; use fugit::ExtU32; -use idsp::iir::{Process, Sos, SosClamp, SosState, StatefulRef}; +use idsp::{ + iir::{SosClamp, SosState, Wdf, WdfState}, + process::{Inplace, Pair, Parallel, Process, Split}, +}; use miniconf::Tree; use rtic_monotonics::Monotonic; @@ -49,17 +52,17 @@ impl serial_settings::Settings for Settings { } } -const LP: usize = 4; - #[derive(Debug, Clone, Default)] pub struct MpllState { /// Lowpass state - lp: [[SosState; LP]; 4], - // /// Phase clamp - // /// - // /// Makes the phase wrap monotonic by clamping it. - // /// Aids capture/pull-in. - // clamp: idsp::Clamp, + lp: [(((), ([WdfState<2>; 2], (WdfState<2>, WdfState<1>))), ()); 4], + /// Phase clamp + /// + /// Makes the phase wrap monotonic by clamping it. + /// Aids capture/pull-in with external modulation. + /// + /// TODO: Remove this for modulation drive. + clamp: idsp::Clamp, /// PID state iir: SosState, /// Current output phase @@ -73,10 +76,8 @@ pub struct MpllState { #[tree(meta(doc, typename))] pub struct Mpll { /// Lowpass - /// - /// * `2*LP` order second order section - /// * after demodulation before decimation - lp: [Sos<30>; LP], + #[tree(skip)] + lp: Pair<[Wdf<2, 0xad>; 2], (Wdf<2, 0xad>, Wdf<1, 0xa>), i32>, /// Input phase offset phase: i32, /// PID IIR filter @@ -96,45 +97,46 @@ impl Mpll { impl Default for Mpll { fn default() -> Self { - // Cheby2, -3 dB @ 0.005*1.28, -120 dB @ 0.017*1.28, Chebychev norm scaled, quantized - let mut lp = [ - [ - [0.0045134034, -0.0073064622, 0.0045134034], - [1., -1.91874131, 0.9204616547], - ], - [ - [0.0327912588, -0.0639008116, 0.0327912588], - [1., -1.9324034546, 0.9340851586], - ], - [ - [0.0708566532, -0.140079312, 0.0708566532], - [1., -1.9555726107, 0.9572066069], - ], - [ - [0.0971013568, -0.1925907861, 0.0971013568], - [1., -1.9835639875, 0.9851759151], - ], - ]; - // add 2 gain to compensate mix shift - for i in [0] { - for i in lp[i][0].iter_mut() { - *i *= 2.0; - } - } + // 7th order Cheby2 as WDF-CA, -3 dB @ 0.005*1.28, -112 dB @ 0.018*1.28 + let lp = Pair::new(( + ( + Default::default(), + Parallel(( + [ + Wdf::quantize(&[ + -0.9866183703960676, + 0.9995042973541263, + ]) + .unwrap(), + Wdf::quantize(&[-0.94357710177202, 0.9994723555364557]) + .unwrap(), + ], + ( + Wdf::quantize(&[ + -0.9619459355859967, + 0.9994905727027024, + ]) + .unwrap(), + Wdf::quantize(&[0.9677764552414969]).unwrap(), + ), + )), + ), + Default::default(), + )); let mut pid = idsp::iir::PidBuilder::default(); pid.order(idsp::iir::Order::I); pid.period(1.0 / BATCH_SIZE as f32); - pid.gain(idsp::iir::Action::P, -6e-3); // fs/turn - pid.gain(idsp::iir::Action::I, -5e-4); // fs/turn/ts = 1/turn + pid.gain(idsp::iir::Action::P, -5e-3); // fs/turn + pid.gain(idsp::iir::Action::I, -4e-4); // fs/turn/ts = 1/turn pid.gain(idsp::iir::Action::D, -4e-3); // fs/turn*ts = turn - pid.limit(idsp::iir::Action::D, -0.2); + //pid.limit(idsp::iir::Action::D, -0.2); let mut iir: SosClamp<_> = pid.build().into(); iir.max = (0.3 * (1u64 << 32) as f32) as _; iir.min = (0.005 * (1u64 << 32) as f32) as _; Self { - lp: lp.map(|c| Sos::from(&c)), + lp, phase: (0.0 * (1u64 << 32) as f32) as _, iir, amp: [(0.09 * (1u64 << 31) as f32) as _; 2], // ~0.9 V @@ -149,51 +151,44 @@ impl Mpll { state: &mut MpllState, x: &[&[u16; BATCH_SIZE]; 2], y: &mut [&mut [u16; BATCH_SIZE]; 2], - ) -> Stream { + ) -> (Stream, i32) { // scratch - let mut m = [[0; BATCH_SIZE]; 4]; + let mut mix = [[0; BATCH_SIZE]; 4]; + let [m00, m01, m10, m11] = mix.each_mut(); // mix - let [m0i, m0q, m1i, m1q] = m.each_mut(); - for (x, (m, lo)) in x[0].iter().zip(x[1].iter()).zip( - m0i.iter_mut() - .zip(m0q) - .zip(m1i.iter_mut().zip(m1q)) + for (x, (mix, lo)) in x[0].iter().zip(x[1].iter()).zip( + m00.iter_mut() + .zip(m01.iter_mut()) + .zip(m10.iter_mut().zip(m11.iter_mut())) .zip(state.lo[0].iter().zip(&state.lo[1])), ) { // ADC encoding, mix - *m.0.0 = ((*x.0 as i16 as i64 * *lo.0 as i64) >> 16) as i32; - *m.0.1 = ((*x.0 as i16 as i64 * *lo.1 as i64) >> 16) as i32; - *m.1.0 = ((*x.1 as i16 as i64 * *lo.0 as i64) >> 16) as i32; - *m.1.1 = ((*x.1 as i16 as i64 * *lo.1 as i64) >> 16) as i32; + *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 - let [s0i, s0q, s1i, s1q] = state.lp.each_mut(); - for (s, lp) in s0i - .iter_mut() - .zip(s0q) - .zip(s1i.iter_mut().zip(s1q)) - .zip(&self.lp) - { - StatefulRef::new(lp, s.0.0).process_in_place(&mut m[0]); - StatefulRef::new(lp, s.0.1).process_in_place(&mut m[1]); - StatefulRef::new(lp, s.1.0).process_in_place(&mut m[2]); - StatefulRef::new(lp, s.1.1).process_in_place(&mut m[3]); + for (mix, state) in mix.iter_mut().zip(state.lp.iter_mut()) { + Split::new(&self.lp, state).inplace(mix); } // decimate - let m = [ - [m[0][BATCH_SIZE - 1], m[1][BATCH_SIZE - 1]], - [m[2][BATCH_SIZE - 1], m[3][BATCH_SIZE - 1]], + let demod = [ + mix[0][BATCH_SIZE - 1], + mix[1][BATCH_SIZE - 1], + mix[2][BATCH_SIZE - 1], + mix[3][BATCH_SIZE - 1], ]; // phase // need full atan2 or addtl osc to support any phase offset - let p = idsp::atan2(m[0][1], m[0][0]); + let p = idsp::atan2(demod[1], demod[0]); // Delay correction let p = p .wrapping_add(self.phase) .wrapping_sub(state.iir.xy[2].wrapping_mul(10)); - // let p = state.clamp.update(p); + let p = state.clamp.process(p); // pid - let f = StatefulRef::new(&self.iir, &mut state.iir).process(p); + let f = Split::new(&self.iir, &mut state.iir).process(p); // modulate let [y0, y1] = state.lo.each_mut(); let [yo0, yo1] = y; @@ -212,14 +207,26 @@ impl Mpll { .wrapping_add(i16::MIN) as u16; // DAC encoding p }); - Stream { - demod: m, - phase: state.iir.xy[0], - frequency: state.iir.xy[2], - } + ( + Stream { + demod, + phase_in: p, + phase_out: state.phase, + }, + f, + ) } } +/// Stream data format. +#[derive(Clone, Copy, Debug, Default, bytemuck::Zeroable, bytemuck::Pod)] +#[repr(C)] +pub struct Stream { + demod: [i32; 4], + phase_in: i32, + phase_out: i32, +} + #[derive(Clone, Debug, Tree)] #[tree(meta(doc, typename))] pub struct App { @@ -243,15 +250,6 @@ impl Default for App { } } -/// Stream data format. -#[derive(Clone, Copy, Debug, Default, bytemuck::Zeroable, bytemuck::Pod)] -#[repr(C)] -pub struct Stream { - demod: [[i32; 2]; 2], - phase: i32, - frequency: i32, -} - /// Channel Telemetry #[derive(Default, Clone)] pub struct TelemetryState { @@ -432,14 +430,15 @@ mod app { let mut dac: [&mut [u16; BATCH_SIZE]; 2] = [(*dac0).try_into().unwrap(), (*dac1).try_into().unwrap()]; - let stream = settings.process(state, &adc, &mut dac); - telemetry.phase.update(stream.phase); - telemetry.frequency.update(stream.frequency); + let (stream, frequency) = + settings.process(state, &adc, &mut dac); + telemetry.phase.update(stream.phase_in); + telemetry.frequency.update(frequency); - telemetry.demod[0][0].update(stream.demod[0][0]); - telemetry.demod[0][1].update(stream.demod[0][1]); - telemetry.demod[1][0].update(stream.demod[1][0]); - telemetry.demod[1][1].update(stream.demod[1][1]); + telemetry.demod[0][0].update(stream.demod[0]); + telemetry.demod[0][1].update(stream.demod[1]); + telemetry.demod[1][0].update(stream.demod[2]); + telemetry.demod[1][1].update(stream.demod[3]); const N: usize = core::mem::size_of::(); generator.add(|buf| { From c2ac191d156300aab9bbafa492059e51f08d36d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Tue, 16 Dec 2025 09:39:01 +0000 Subject: [PATCH 10/28] fls: port --- src/bin/fls.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/bin/fls.rs b/src/bin/fls.rs index 8867cd229..db9ac481e 100644 --- a/src/bin/fls.rs +++ b/src/bin/fls.rs @@ -81,6 +81,7 @@ use ad9959::Acr; use arbitrary_int::{u14, u24}; use idsp::{ Accu, Complex, ComplexExt, Filter, Lockin, Lowpass, PLL, Unwrapper, iir, + process::{Process, Split}, }; use miniconf::Tree; use platform::NetSettings; @@ -303,8 +304,6 @@ mod validate_att { } } -type LockinLowpass = Lowpass<2>; - #[derive(Clone, Debug, Tree)] struct ChannelSettings { /// Input (demodulation) DDS settings @@ -338,7 +337,7 @@ struct ChannelSettings { /// # Default /// `lockin_k = [0x200_0000, -0x2000_0000]` #[tree(with=miniconf::leaf)] - lockin_k: ::Config, + lockin_k: [i32; 2], /// Minimum demodulated signal power to enable feedback. /// Note that this is RMS and that the signal peak must not clip. /// @@ -658,7 +657,7 @@ pub struct CookedTelemetry { #[derive(Clone, Default)] pub struct ChannelState { - lockin: Lockin, + lockin: Lockin>, x0: i32, t0: i32, t: i64, @@ -863,7 +862,7 @@ mod app { .lock(|state, settings, dds_output, telemetry| { // Reconstruct frequency and phase using a lowpass that is aware of phase and frequency // wraps. - pll.update(timestamp, settings.pll_k); + Split::new(&settings.pll_k, pll).process(timestamp); // TODO: implement clear stream[0].pll = pll.frequency() as _; stream[1].pll = pll.phase() as _; @@ -906,7 +905,7 @@ mod app { // 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 = state.lockin.update_iq( + let y = state.lockin.process( // 3 bit headroom for coeff sum minus one bit gain for filter (*a as i16 as i32) << 14, *p, @@ -989,7 +988,7 @@ mod app { state.y = state.y.wrapping_add( dphase as i64 * settings.phase_scale[0][0] as i64, ); - state.unwrapper.update( + state.unwrapper.process( ((state.y >> settings.phase_scale[0][1]) as i32) .wrapping_add( (state.t >> settings.phase_scale[1][1]) From a40fe2fd5e06329ada7fed6cad7de64f2feebb1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Wed, 17 Dec 2025 00:25:25 +0000 Subject: [PATCH 11/28] deps: bump --- Cargo.lock | 93 +++++++++++++++++++++++++++--------------------------- 1 file changed, 47 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5784832f6..80d5dee5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,7 +77,7 @@ dependencies = [ "arbitrary-int", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -124,7 +124,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -141,9 +141,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.46" +version = "1.2.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" dependencies = [ "find-msvc-tools", "jobserver", @@ -196,7 +196,7 @@ checksum = "e37549a379a9e0e6e576fd208ee60394ccb8be963889eebba3ffe0980364f472" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -226,7 +226,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -237,7 +237,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -248,7 +248,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -454,9 +454,9 @@ dependencies = [ [[package]] name = "git2" -version = "0.20.2" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110" +checksum = "3e2b37e2f62729cdada11f0e6b3b6fe383c69c29fc619e391223e12856af308c" dependencies = [ "bitflags 2.10.0", "libc", @@ -494,9 +494,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heapless" @@ -577,9 +577,9 @@ checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ "icu_collections", "icu_locale_core", @@ -591,9 +591,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" @@ -651,9 +651,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", "hashbrown", @@ -677,15 +677,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.177" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libgit2-sys" -version = "0.18.2+1.9.1" +version = "0.18.3+1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c42fe03df2bd3c53a3a9c7317ad91d80c81cd1fb0caec8d7cc4cd2bfa10c222" +checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" dependencies = [ "cc", "libc", @@ -737,9 +737,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "managed" @@ -802,7 +802,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -959,7 +959,7 @@ checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1074,7 +1074,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1133,7 +1133,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1176,7 +1176,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1327,12 +1327,13 @@ dependencies = [ [[package]] name = "serde-reflection" -version = "0.5.0" -source = "git+https://github.com/quartiq/serde-reflection.git?branch=pub-ser-de#2910c9c3422ee7b7fe2bffe7e1f929778b217607" +version = "0.5.1" +source = "git+https://github.com/quartiq/serde-reflection.git?branch=pub-ser-de#337a07a9ae56e4139b005a4e2b5a9c3865ea13e8" dependencies = [ "erased-discriminant", "once_cell", "serde", + "serde_json", "thiserror 1.0.69", "typeid", ] @@ -1354,7 +1355,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1373,9 +1374,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.16.0" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" dependencies = [ "serde_core", "serde_with_macros", @@ -1383,14 +1384,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.16.0" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1643,7 +1644,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1659,9 +1660,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.110" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -1688,7 +1689,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1728,7 +1729,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1739,7 +1740,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1908,7 +1909,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", "synstructure", ] @@ -1929,7 +1930,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", "synstructure", ] @@ -1963,5 +1964,5 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] From 0c2aa3c4e25df997f7a13e8e96f191c7567c2884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Sat, 3 Jan 2026 23:51:17 +0000 Subject: [PATCH 12/28] dsp-process --- Cargo.lock | 15 +++++++++++++++ Cargo.toml | 2 ++ signal_generator/src/lib.rs | 4 +++- src/bin/mpll.rs | 24 ++++++++++++------------ 4 files changed, 32 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 80d5dee5d..301ec3f6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -251,6 +251,18 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "dsp-fixedpoint" +version = "0.1.0" +dependencies = [ + "num-traits", + "serde", +] + +[[package]] +name = "dsp-process" +version = "0.1.0" + [[package]] name = "dyn-clone" version = "1.0.20" @@ -641,6 +653,8 @@ dependencies = [ name = "idsp" version = "0.19.0" dependencies = [ + "dsp-fixedpoint", + "dsp-process", "miniconf", "num-complex 0.4.6", "num-traits", @@ -1518,6 +1532,7 @@ dependencies = [ "bytemuck", "cortex-m", "cortex-m-rt", + "dsp-process", "embedded-hal 0.2.7", "embedded-hal 1.0.0", "embedded-hal-bus", diff --git a/Cargo.toml b/Cargo.toml index dd3d4ab10..b52293375 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ rtic-monotonics = { version = "2.0", features = ["cortex-m-systick"] } num_enum = { version = "0.7.3", default-features = false } paste = "1" idsp = "0.19.0" +dsp-process = "0.1.0" ad9959 = { path = "ad9959", version = "0.3.0" } serial_settings = { version = "0.2", path = "serial_settings" } mcp230xx = "1.0" @@ -157,3 +158,4 @@ lto = true [patch.crates-io] serde-reflection = { git = "https://github.com/quartiq/serde-reflection.git", branch = "pub-ser-de" } idsp = { path = "../idsp" } +dsp-process = { path = "../idsp/dsp-process" } diff --git a/signal_generator/src/lib.rs b/signal_generator/src/lib.rs index c876048b4..00746f36c 100644 --- a/signal_generator/src/lib.rs +++ b/signal_generator/src/lib.rs @@ -162,7 +162,9 @@ impl Iterator for Source { #[inline] fn next(&mut self) -> Option { let (s, a) = match self { - Self::SweptSine { sweep, amp } => (sweep.next().map(|c| c.im), amp), + Self::SweptSine { sweep, amp } => { + (sweep.next().map(|c| c.im()), amp) + } Self::Periodic { accu, signal, amp } => { (accu.next().map(|p| signal.map(p)), amp) } diff --git a/src/bin/mpll.rs b/src/bin/mpll.rs index 7c59ab9a6..a272f298e 100644 --- a/src/bin/mpll.rs +++ b/src/bin/mpll.rs @@ -3,11 +3,11 @@ use core::sync::atomic::{Ordering, fence}; -use fugit::ExtU32; -use idsp::{ - iir::{SosClamp, SosState, Wdf, WdfState}, - process::{Inplace, Pair, Parallel, Process, Split}, +use dsp_process::{ + Add, Identity, Inplace, Pair, Parallel, Process, Split, Unsplit, }; +use fugit::ExtU32; +use idsp::iir::{SosClamp, SosState, Wdf, WdfState}; use miniconf::Tree; use rtic_monotonics::Monotonic; @@ -84,6 +84,7 @@ pub struct Mpll { /// /// Do not use the iir offset as phase offset. /// Includes frequency limits. + #[tree(skip)] iir: SosClamp<30>, /// Output amplitude scale amp: [i32; 2], @@ -100,7 +101,7 @@ impl Default for Mpll { // 7th order Cheby2 as WDF-CA, -3 dB @ 0.005*1.28, -112 dB @ 0.018*1.28 let lp = Pair::new(( ( - Default::default(), + Unsplit(&Identity), Parallel(( [ Wdf::quantize(&[ @@ -121,7 +122,7 @@ impl Default for Mpll { ), )), ), - Default::default(), + Unsplit(&Add), )); let mut pid = idsp::iir::PidBuilder::default(); @@ -130,7 +131,7 @@ impl Default for Mpll { pid.gain(idsp::iir::Action::P, -5e-3); // fs/turn pid.gain(idsp::iir::Action::I, -4e-4); // fs/turn/ts = 1/turn pid.gain(idsp::iir::Action::D, -4e-3); // fs/turn*ts = turn - //pid.limit(idsp::iir::Action::D, -0.2); + pid.limit(idsp::iir::Action::D, -0.2); let mut iir: SosClamp<_> = pid.build().into(); iir.max = (0.3 * (1u64 << 32) as f32) as _; iir.min = (0.005 * (1u64 << 32) as f32) as _; @@ -149,8 +150,8 @@ impl Mpll { pub fn process( &self, state: &mut MpllState, - x: &[&[u16; BATCH_SIZE]; 2], - y: &mut [&mut [u16; BATCH_SIZE]; 2], + x: [&[u16; BATCH_SIZE]; 2], + y: [&mut [u16; BATCH_SIZE]; 2], ) -> (Stream, i32) { // scratch let mut mix = [[0; BATCH_SIZE]; 4]; @@ -427,11 +428,10 @@ mod app { (**adc0).try_into().unwrap(), (**adc1).try_into().unwrap(), ]; - let mut dac: [&mut [u16; BATCH_SIZE]; 2] = + let dac: [&mut [u16; BATCH_SIZE]; 2] = [(*dac0).try_into().unwrap(), (*dac1).try_into().unwrap()]; - let (stream, frequency) = - settings.process(state, &adc, &mut dac); + let (stream, frequency) = settings.process(state, adc, dac); telemetry.phase.update(stream.phase_in); telemetry.frequency.update(frequency); From 330b7b4c7802786e575616cb8107e14fdfc451d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Tue, 6 Jan 2026 10:25:32 +0000 Subject: [PATCH 13/28] deps: update --- Cargo.lock | 97 +++++++++++++++++++++++++++++------------------------- 1 file changed, 52 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 301ec3f6d..3470fec05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,7 +77,7 @@ dependencies = [ "arbitrary-int", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -124,7 +124,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -141,9 +141,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.49" +version = "1.2.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" dependencies = [ "find-msvc-tools", "jobserver", @@ -196,7 +196,7 @@ checksum = "e37549a379a9e0e6e576fd208ee60394ccb8be963889eebba3ffe0980364f472" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -226,7 +226,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -237,7 +237,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -248,7 +248,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -394,9 +394,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" [[package]] name = "fnv" @@ -675,9 +675,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jobserver" @@ -691,9 +691,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.178" +version = "0.2.179" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" [[package]] name = "libgit2-sys" @@ -816,7 +816,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -973,7 +973,7 @@ checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -1045,9 +1045,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" [[package]] name = "postcard" @@ -1088,23 +1088,23 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] @@ -1147,7 +1147,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -1190,7 +1190,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -1266,15 +1266,15 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "schemars" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" +checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" dependencies = [ "dyn-clone", "ref-cast", @@ -1369,21 +1369,21 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" dependencies = [ "indexmap", "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -1405,7 +1405,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -1532,6 +1532,7 @@ dependencies = [ "bytemuck", "cortex-m", "cortex-m-rt", + "dsp-fixedpoint", "dsp-process", "embedded-hal 0.2.7", "embedded-hal 1.0.0", @@ -1659,7 +1660,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -1675,9 +1676,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.111" +version = "2.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "678faa00651c9eb72dd2020cbdf275d92eccb2400d568e419efdd64838145cb4" dependencies = [ "proc-macro2", "quote", @@ -1704,7 +1705,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -1744,7 +1745,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -1755,7 +1756,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] [[package]] @@ -1788,9 +1789,9 @@ checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -1924,7 +1925,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", "synstructure", ] @@ -1945,7 +1946,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", "synstructure", ] @@ -1979,5 +1980,11 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.113", ] + +[[package]] +name = "zmij" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" From 117828d64369ac54580a4cff53e668416e676f8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Tue, 6 Jan 2026 10:25:40 +0000 Subject: [PATCH 14/28] deps: add fixepoint --- Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index b52293375..aa75ab267 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,7 @@ num_enum = { version = "0.7.3", default-features = false } paste = "1" idsp = "0.19.0" dsp-process = "0.1.0" +dsp-fixedpoint = "0.1.0" ad9959 = { path = "ad9959", version = "0.3.0" } serial_settings = { version = "0.2", path = "serial_settings" } mcp230xx = "1.0" @@ -159,3 +160,4 @@ lto = true serde-reflection = { git = "https://github.com/quartiq/serde-reflection.git", branch = "pub-ser-de" } idsp = { path = "../idsp" } dsp-process = { path = "../idsp/dsp-process" } +dsp-fixedpoint = { path = "../idsp/dsp-fixedpoint" } From 7d2e067a1a8a4913ad3169dae761886cbb6d6877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Tue, 6 Jan 2026 20:51:43 +0000 Subject: [PATCH 15/28] fls, lockin: port --- Cargo.lock | 1 + src/bin/fls.rs | 30 +++++++++++++++--------------- src/bin/lockin.rs | 43 ++++++++++++++++++++++++------------------- 3 files changed, 40 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3470fec05..1164d673f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -653,6 +653,7 @@ dependencies = [ name = "idsp" version = "0.19.0" dependencies = [ + "bytemuck", "dsp-fixedpoint", "dsp-process", "miniconf", diff --git a/src/bin/fls.rs b/src/bin/fls.rs index db9ac481e..66d218dba 100644 --- a/src/bin/fls.rs +++ b/src/bin/fls.rs @@ -79,9 +79,10 @@ use ad9959::Acr; use arbitrary_int::{u14, u24}; +use dsp_process::{Process, SplitProcess}; use idsp::{ - Accu, Complex, ComplexExt, Filter, Lockin, Lowpass, PLL, Unwrapper, iir, - process::{Process, Split}, + Accu, Complex, ComplexExt, Lockin, Lowpass, LowpassState, PLL, Unwrapper, + iir, }; use miniconf::Tree; use platform::NetSettings; @@ -336,8 +337,8 @@ struct ChannelSettings { /// /// # Default /// `lockin_k = [0x200_0000, -0x2000_0000]` - #[tree(with=miniconf::leaf)] - lockin_k: [i32; 2], + #[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. /// @@ -415,7 +416,7 @@ impl Default for ChannelSettings { att: 6.0, phase: u14::new(0), }, - lockin_k: [-(i32::MIN >> 6), i32::MIN >> 2], + lockin_k: Lockin(Lowpass([-(i32::MIN >> 6), i32::MIN >> 2])), amp: u24::new(0), min_power: -24, clear: true, @@ -657,7 +658,7 @@ pub struct CookedTelemetry { #[derive(Clone, Default)] pub struct ChannelState { - lockin: Lockin>, + lockin: [LowpassState<2>; 2], x0: i32, t0: i32, t: i64, @@ -862,7 +863,7 @@ mod app { .lock(|state, settings, dds_output, telemetry| { // Reconstruct frequency and phase using a lowpass that is aware of phase and frequency // wraps. - Split::new(&settings.pll_k, pll).process(timestamp); + settings.pll_k.process(pll, timestamp); // TODO: implement clear stream[0].pll = pll.frequency() as _; stream[1].pll = pll.phase() as _; @@ -905,14 +906,13 @@ mod app { // 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 = state.lockin.process( + 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, - &settings.lockin_k, + ((*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; + *d = DacCode::from((y.im() >> 13) as i16).0; y }) // Add more zeros at fs/2, fs/4, and fs/8 by rectangular window. @@ -996,11 +996,11 @@ mod app { ), ); - stream.phase = bytemuck::cast(state.unwrapper.y()); - telemetry.phase = state.unwrapper.y(); + stream.phase = bytemuck::cast(state.unwrapper.y); + telemetry.phase = state.unwrapper.y; let phase_err = state .unwrapper - .y() + .y .clamp(-i32::MAX as _, i32::MAX as _) as _; diff --git a/src/bin/lockin.rs b/src/bin/lockin.rs index 8894ec19a..7e9da8496 100644 --- a/src/bin/lockin.rs +++ b/src/bin/lockin.rs @@ -33,8 +33,9 @@ use core::{ sync::atomic::{Ordering, fence}, }; +use dsp_process::SplitProcess; use fugit::ExtU32; -use idsp::{Accu, Complex, ComplexExt, Filter, Lowpass, RPLL, Repeat}; +use idsp::{Accu, Complex, ComplexExt, Lowpass, RPLL, RPLLConfig}; use miniconf::{Leaf, Tree}; use rtic_monotonics::Monotonic; use serde::{Deserialize, Serialize}; @@ -122,11 +123,12 @@ pub struct Lockin { /// Specifis the PLL time constant. /// /// The PLL time constant exponent (1-31). - pll_tc: [u32; 2], + #[tree(with=miniconf::leaf)] + pll_tc: RPLLConfig, /// Specifies the lockin lowpass gains. - #[tree(with=miniconf::leaf)] - lockin_k: as Filter>::Config, + #[tree(skip)] // TODO + lockin_k: idsp::Lockin>, /// Specifies which harmonic to use for the lockin. /// @@ -157,9 +159,13 @@ impl Default for Lockin { lockin_mode: LockinMode::External, - pll_tc: [21, 21], // frequency and phase settling time (log2 counter cycles) + pll_tc: RPLLConfig { + dt2: (SAMPLE_TICKS_LOG2 + BATCH_SIZE_LOG2) as _, + shift_frequency: 21, + shift_phase: 21, + }, // frequency and phase settling time (log2 counter cycles) - lockin_k: [0x8_0000, -0x400_0000], // lockin lowpass gains + lockin_k: idsp::Lockin(Lowpass([0x8_0000, -0x400_0000])), // lockin lowpass gains lockin_harmonic: -1, // Harmonic index of the LO: -1 to _de_modulate the fundamental (complex conjugate) lockin_phase: 0, // Demodulation LO phase offset @@ -191,6 +197,7 @@ fn main() { #[rtic::app(device = stabilizer::hardware::hal::stm32, peripherals = true, dispatchers=[DCMI, JPEG, SDMMC])] mod app { use super::*; + use idsp::LowpassState; use stabilizer::{ hardware::{ self, DigitalInput0, DigitalInput1, Pgia, SerialTerminal, @@ -225,7 +232,7 @@ mod app { adcs: (Adc0Input, Adc1Input), dacs: (Dac0Output, Dac1Output), pll: RPLL, - lockin: idsp::Lockin>>, + lockin: [LowpassState<2>; 2], source: idsp::AccuOsc>, generator: FrameGenerator, cpu_temp_sensor: stabilizer::hardware::cpu_temp_sensor::CpuTempSensor, @@ -274,8 +281,8 @@ mod app { timestamper: stabilizer.input_stamper, cpu_temp_sensor: stabilizer.temperature_sensor, - pll: RPLL::new(SAMPLE_TICKS_LOG2 + BATCH_SIZE_LOG2), - lockin: idsp::Lockin::default(), + pll: RPLL::default(), + lockin: Default::default(), source: idsp::AccuOsc::new(iter::repeat( 1i64 << (64 - BATCH_SIZE_LOG2), )), @@ -345,11 +352,9 @@ mod app { LockinMode::External => { let timestamp = timestamper.latest_timestamp().unwrap_or(None); // Ignore data from timer capture overflows. - let (pll_phase, pll_frequency) = pll.update( - timestamp.map(|t| t as i32), - settings.pll_tc[0], - settings.pll_tc[1], - ); + let (pll_phase, pll_frequency) = settings + .pll_tc + .process(pll, timestamp.map(|t| t as i32)); (pll_phase, (pll_frequency >> BATCH_SIZE_LOG2) as i32) } LockinMode::Internal => { @@ -378,7 +383,7 @@ mod app { // Convert to signed, MSB align the ADC sample, update the Lockin (demodulate, filter) .map(|(&sample, phase)| { let s = (sample as i16 as i32) << 16; - lockin.update(s, phase, &settings.lockin_k) + settings.lockin_k.process(lockin, (s, phase)) }) // Decimate .last() @@ -389,16 +394,16 @@ mod app { for (channel, samples) in dac_samples.iter_mut().enumerate() { for sample in samples.iter_mut() { let value = match *settings.output_conf[channel] { - Conf::Magnitude => output.abs_sqr() as i32 >> 16, + Conf::Magnitude => output.norm_sqr() as i32 >> 16, Conf::Phase => output.arg() >> 16, Conf::LogPower => output.log2() << 8, Conf::ReferenceFrequency => { reference_frequency >> 16 } - Conf::InPhase => output.re >> 16, - Conf::Quadrature => output.im >> 16, + Conf::InPhase => output.re() >> 16, + Conf::Quadrature => output.im() >> 16, - Conf::Modulation => source.next().unwrap().re, + Conf::Modulation => source.next().unwrap().re(), }; *sample = DacCode::from(value as i16).0; From b4511a1d15358376acf948d0491d03128dd4732d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Wed, 7 Jan 2026 19:49:39 +0000 Subject: [PATCH 16/28] idsp: sos full fixedpoint --- src/bin/fls.rs | 8 ++++---- src/bin/mpll.rs | 9 +++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/bin/fls.rs b/src/bin/fls.rs index 66d218dba..0ec20988d 100644 --- a/src/bin/fls.rs +++ b/src/bin/fls.rs @@ -295,12 +295,12 @@ mod validate_att { ) -> Result<(), SerdeError> { let mut att = *value; leaf::deserialize_by_key(&mut att, keys, de)?; - if !stabilizer::convert::att_is_valid(att) { - Err(ValueError::Access("Attenuation out of range (0..=31.5 dB)") - .into()) - } else { + if stabilizer::convert::att_is_valid(att) { *value = att; Ok(()) + } else { + Err(ValueError::Access("Attenuation out of range (0..=31.5 dB)") + .into()) } } } diff --git a/src/bin/mpll.rs b/src/bin/mpll.rs index a272f298e..0669eec60 100644 --- a/src/bin/mpll.rs +++ b/src/bin/mpll.rs @@ -3,11 +3,12 @@ use core::sync::atomic::{Ordering, fence}; +use dsp_fixedpoint::Q32; use dsp_process::{ Add, Identity, Inplace, Pair, Parallel, Process, Split, Unsplit, }; use fugit::ExtU32; -use idsp::iir::{SosClamp, SosState, Wdf, WdfState}; +use idsp::iir::{DirectForm1, SosClamp, Wdf, WdfState}; use miniconf::Tree; use rtic_monotonics::Monotonic; @@ -64,7 +65,7 @@ pub struct MpllState { /// TODO: Remove this for modulation drive. clamp: idsp::Clamp, /// PID state - iir: SosState, + iir: DirectForm1, /// Current output phase phase: i32, /// Current LO samples for downconverting the next batch @@ -85,7 +86,7 @@ pub struct Mpll { /// Do not use the iir offset as phase offset. /// Includes frequency limits. #[tree(skip)] - iir: SosClamp<30>, + iir: SosClamp, i32>, /// Output amplitude scale amp: [i32; 2], } @@ -132,7 +133,7 @@ impl Default for Mpll { pid.gain(idsp::iir::Action::I, -4e-4); // fs/turn/ts = 1/turn pid.gain(idsp::iir::Action::D, -4e-3); // fs/turn*ts = turn pid.limit(idsp::iir::Action::D, -0.2); - let mut iir: SosClamp<_> = pid.build().into(); + let mut iir: SosClamp<_, _> = pid.build().into(); iir.max = (0.3 * (1u64 << 32) as f32) as _; iir.min = (0.005 * (1u64 << 32) as f32) as _; From 09af44eed35e1f8fb30c93766ed3d7f714084e5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Thu, 8 Jan 2026 16:20:08 +0000 Subject: [PATCH 17/28] mpll: port --- src/bin/mpll.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/bin/mpll.rs b/src/bin/mpll.rs index 0669eec60..05afe11f7 100644 --- a/src/bin/mpll.rs +++ b/src/bin/mpll.rs @@ -8,7 +8,7 @@ use dsp_process::{ Add, Identity, Inplace, Pair, Parallel, Process, Split, Unsplit, }; use fugit::ExtU32; -use idsp::iir::{DirectForm1, SosClamp, Wdf, WdfState}; +use idsp::iir::{DirectForm1, BiquadClamp, Wdf, WdfState}; use miniconf::Tree; use rtic_monotonics::Monotonic; @@ -86,7 +86,7 @@ pub struct Mpll { /// Do not use the iir offset as phase offset. /// Includes frequency limits. #[tree(skip)] - iir: SosClamp, i32>, + iir: BiquadClamp, i32>, /// Output amplitude scale amp: [i32; 2], } @@ -126,14 +126,14 @@ impl Default for Mpll { Unsplit(&Add), )); - let mut pid = idsp::iir::PidBuilder::default(); - pid.order(idsp::iir::Order::I); - pid.period(1.0 / BATCH_SIZE as f32); - pid.gain(idsp::iir::Action::P, -5e-3); // fs/turn - pid.gain(idsp::iir::Action::I, -4e-4); // fs/turn/ts = 1/turn - pid.gain(idsp::iir::Action::D, -4e-3); // fs/turn*ts = turn - pid.limit(idsp::iir::Action::D, -0.2); - let mut iir: SosClamp<_, _> = pid.build().into(); + let mut iir: BiquadClamp<_, _> = idsp::iir::PidBuilder::default() + .order(idsp::iir::Order::I) + .period(1.0 / BATCH_SIZE as f32) + .gain(idsp::iir::Action::P, -5e-3) // fs/turn + .gain(idsp::iir::Action::I, -4e-4) // fs/turn/ts = 1/turn + .gain(idsp::iir::Action::D, -4e-3) // fs/turn*ts = turn + .limit(idsp::iir::Action::D, -0.2) + .into(); iir.max = (0.3 * (1u64 << 32) as f32) as _; iir.min = (0.005 * (1u64 << 32) as f32) as _; From 979ca86a43f737a2ce12fc878d8eb4683205f961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Thu, 8 Jan 2026 16:20:51 +0000 Subject: [PATCH 18/28] dual-iir: port --- src/bin/dual-iir.rs | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/bin/dual-iir.rs b/src/bin/dual-iir.rs index 64ffc19b1..0ee1ee34f 100644 --- a/src/bin/dual-iir.rs +++ b/src/bin/dual-iir.rs @@ -30,6 +30,7 @@ use miniconf::Tree; +use dsp_process::SplitProcess; use idsp::iir; use platform::{AppSettings, NetSettings}; @@ -90,9 +91,9 @@ pub struct BiquadRepr { impl Default for BiquadRepr { fn default() -> Self { - let mut i = iir::Biquad::IDENTITY; - i.set_min(-i16::MAX as _); - i.set_max(i16::MAX as _); + let mut i = iir::BiquadClamp::from(iir::Biquad::IDENTITY); + i.min = -i16::MAX as _; + i.max = i16::MAX as _; Self { _typ: (), repr: iir::BiquadRepr::Raw(i), @@ -147,11 +148,7 @@ impl Channel { state: Default::default(), run: self.run, biquad: self.biquad.each_ref().map(|biquad| { - biquad.repr.build::( - SAMPLE_PERIOD, - 1.0, - DacCode::LSB_PER_VOLT, - ) + biquad.repr.build(SAMPLE_PERIOD, 1.0, DacCode::LSB_PER_VOLT) }), }) } @@ -189,8 +186,8 @@ impl Default for DualIir { #[derive(Clone, Debug)] pub struct Active { run: Run, - biquad: [iir::Biquad; IIR_CASCADE_LENGTH], - state: [[f32; 4]; IIR_CASCADE_LENGTH], + biquad: [iir::BiquadClamp; IIR_CASCADE_LENGTH], + state: [iir::DirectForm1; IIR_CASCADE_LENGTH], source: Source, } @@ -386,12 +383,11 @@ mod app { .iter() .zip(active.state.iter_mut()) .fold(x, |y, (ch, state)| { - let filter = if active.run.run(di) { - ch + if active.run.run(di) { + ch.process(state, y) } else { - &iir::Biquad::HOLD - }; - filter.update(state, y) + iir::Biquad::::HOLD.process(state, y) + } }); // Note(unsafe): The filter limits must ensure that the value is in range. @@ -477,7 +473,7 @@ mod app { ( ch.run, ch.biquad.each_ref().map(|b| { - b.repr.build::( + b.repr.build( SAMPLE_PERIOD, 1.0, DacCode::LSB_PER_VOLT, From 019a3e46ae2733724b1483caeeaa52739fca0166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Thu, 8 Jan 2026 16:56:55 +0000 Subject: [PATCH 19/28] port to new biquad --- src/bin/dual-iir.rs | 6 +-- src/bin/fls.rs | 119 ++++++++++++++++++++++++-------------------- src/bin/mpll.rs | 2 +- 3 files changed, 68 insertions(+), 59 deletions(-) diff --git a/src/bin/dual-iir.rs b/src/bin/dual-iir.rs index 0ee1ee34f..17f4edd60 100644 --- a/src/bin/dual-iir.rs +++ b/src/bin/dual-iir.rs @@ -473,11 +473,7 @@ mod app { ( ch.run, ch.biquad.each_ref().map(|b| { - b.repr.build( - SAMPLE_PERIOD, - 1.0, - DacCode::LSB_PER_VOLT, - ) + b.repr.build(SAMPLE_PERIOD, 1.0, DacCode::LSB_PER_VOLT) }), ) }); diff --git a/src/bin/fls.rs b/src/bin/fls.rs index 0ec20988d..288b95857 100644 --- a/src/bin/fls.rs +++ b/src/bin/fls.rs @@ -79,12 +79,16 @@ use ad9959::Acr; use arbitrary_int::{u14, u24}; +use dsp_fixedpoint::{Const, Q32}; use dsp_process::{Process, SplitProcess}; use idsp::{ Accu, Complex, ComplexExt, Lockin, Lowpass, LowpassState, PLL, Unwrapper, - iir, + iir::{ + Biquad, BiquadClamp, BiquadRepr, DirectForm1, DirectForm1Dither, Pid, + }, }; use miniconf::Tree; +use num_traits::AsPrimitive; use platform::NetSettings; use serde::{Deserialize, Serialize}; use stabilizer::{ @@ -142,12 +146,14 @@ const PHASE_SCALE_SHIFT: u32 = 12; const F_DEMOD: u32 = 0x5200_0000; #[derive(Clone, Debug, Tree)] -pub struct BiquadRepr +pub struct BiquadReprTree where - T: idsp::Coefficient - + num_traits::AsPrimitive - + num_traits::AsPrimitive, - f32: num_traits::AsPrimitive, + T: 'static + Const + Copy, + Y: 'static + Copy, + f32: AsPrimitive + AsPrimitive, + BiquadClamp: Default, + BiquadRepr: Default, + Pid: Default + Into>, { // Order matters /// Biquad representation type @@ -155,13 +161,13 @@ where _typ: (), /// Biquad parameters /// Biquad representation subtree access - repr: iir::BiquadRepr, + 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: iir::Biquad, + iir: BiquadClamp, #[tree(skip)] period: f32, #[tree(skip)] @@ -171,46 +177,51 @@ where } mod biquad_update { - use super::BiquadRepr; + use super::BiquadReprTree; + use dsp_fixedpoint::Const; + use idsp::iir::{BiquadClamp, BiquadRepr, Pid}; 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: &BiquadRepr, + pub fn serialize_by_key( + _value: &BiquadReprTree, keys: impl Keys, ser: S, ) -> Result> where S: Serializer, - T: idsp::Coefficient - + num_traits::AsPrimitive - + num_traits::AsPrimitive, - f32: num_traits::AsPrimitive, + T: 'static + Const + Copy, + Y: 'static + Copy, + f32: AsPrimitive + AsPrimitive, + BiquadRepr: Default, + BiquadClamp: Default, + Pid: Into>, { leaf::serialize_by_key(&(), keys, ser) } - pub fn deserialize_by_key<'de, D, T>( - value: &mut BiquadRepr, + pub fn deserialize_by_key<'de, D, T, Y>( + value: &mut BiquadReprTree, keys: impl Keys, de: D, ) -> Result<(), SerdeError> where D: Deserializer<'de>, - T: idsp::Coefficient - + num_traits::AsPrimitive - + num_traits::AsPrimitive, - f32: num_traits::AsPrimitive, + T: 'static + Const + Copy, + Y: 'static + Copy, + f32: AsPrimitive + AsPrimitive, + BiquadRepr: Default, + BiquadClamp: Default, + Pid: Into>, { leaf::deserialize_by_key(&mut (), keys, de)?; value.iir = - value - .repr - .build::(value.period, value.b_scale, value.y_scale); + value.repr.build(value.period, value.b_scale, value.y_scale); Ok(()) } @@ -227,19 +238,21 @@ mod biquad_update { } } -impl Default for BiquadRepr +impl Default for BiquadReprTree where - T: idsp::Coefficient - + num_traits::AsPrimitive - + num_traits::AsPrimitive, - f32: num_traits::AsPrimitive, + T: 'static + Const + Copy, + Y: 'static + Copy, + f32: AsPrimitive + AsPrimitive, + BiquadClamp: Default, + Pid: Into>, + BiquadRepr: Default, { fn default() -> Self { Self { _typ: (), - repr: iir::BiquadRepr::Raw(iir::Biquad::IDENTITY), + repr: BiquadRepr::Raw(Biquad::IDENTITY.into()), _update: (), - iir: iir::Biquad::IDENTITY, + iir: Biquad::IDENTITY.into(), period: 1.0, b_scale: 1.0, y_scale: 1.0, @@ -371,7 +384,7 @@ struct ChannelSettings { /// /// # Default /// A proportional gain=-1 filter. - iir: BiquadRepr, + 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. @@ -389,7 +402,7 @@ struct ChannelSettings { /// /// # Default /// No feedback - iir_amp: BiquadRepr, + iir_amp: BiquadReprTree, } const DDS_LSB_PER_HZ: f32 = (1i64 << 32) as f32 @@ -397,14 +410,14 @@ const DDS_LSB_PER_HZ: f32 = (1i64 << 32) as f32 impl Default for ChannelSettings { fn default() -> Self { - let mut iir_prop = iir::Biquad::IDENTITY; - iir_prop.ba_mut()[0] *= -1; - iir_prop.set_min(-0x4_0000); - iir_prop.set_max(0x4_0000); - let mut iir_amp = iir::Biquad::default(); - iir_amp.set_u(0x3ff as _); - iir_amp.set_min(0.0); - iir_amp.set_max(0x3ff as _); + 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, @@ -421,9 +434,9 @@ impl Default for ChannelSettings { min_power: -24, clear: true, phase_scale: [[1, 16], [0, 0]], - iir: BiquadRepr { - repr: iir::BiquadRepr::Raw(iir_prop.clone()), - iir: iir_prop, + iir: BiquadReprTree { + repr: BiquadRepr::Raw(iir_prop.clone()), + iir: iir_prop.clone(), period: stabilizer::design_parameters::TIMER_PERIOD * (SAMPLE_TICKS * BATCH_SIZE as u32) as f32, y_scale: DDS_LSB_PER_HZ, @@ -431,12 +444,12 @@ impl Default for ChannelSettings { }, pow_gain: 0, hold_en: false, - iir_amp: BiquadRepr { - repr: iir::BiquadRepr::Raw(iir_amp.clone()), + iir_amp: BiquadReprTree { + repr: BiquadRepr::Raw(iir_amp.clone()), iir: iir_amp.clone(), period: 10e-3, - b_scale: iir_amp.max(), - y_scale: iir_amp.max(), + b_scale: iir_amp.max, + y_scale: iir_amp.max, ..Default::default() }, }; @@ -664,8 +677,8 @@ pub struct ChannelState { t: i64, y: i64, unwrapper: Unwrapper, - iir: [i32; 5], - iir_amp: [f32; 4], + iir: DirectForm1Dither, + iir_amp: DirectForm1, hold: bool, } @@ -1007,7 +1020,7 @@ mod app { stats.update(phase_err); // TODO; unchecked_shr let delta_ftw = - settings.iir.iir.update(&mut state.iir, phase_err); + settings.iir.iir.process(&mut state.iir, phase_err); let delta_pow = ((phase_err >> 16) .wrapping_mul(settings.pow_gain as _) >> 16) as _; @@ -1150,10 +1163,10 @@ mod app { .zip(y.iter_mut()) { *y = if c.hold { - iir::Biquad::HOLD.update(&mut c.iir_amp, *x); + Biquad::::HOLD.process(&mut c.iir_amp, *x); 0 } else { - s.iir_amp.iir.update(&mut c.iir_amp, *x) as u16 + s.iir_amp.iir.process(&mut c.iir_amp, *x) as u16 }; } }); diff --git a/src/bin/mpll.rs b/src/bin/mpll.rs index 05afe11f7..62cd6d484 100644 --- a/src/bin/mpll.rs +++ b/src/bin/mpll.rs @@ -8,7 +8,7 @@ use dsp_process::{ Add, Identity, Inplace, Pair, Parallel, Process, Split, Unsplit, }; use fugit::ExtU32; -use idsp::iir::{DirectForm1, BiquadClamp, Wdf, WdfState}; +use idsp::iir::{BiquadClamp, DirectForm1, Wdf, WdfState}; use miniconf::Tree; use rtic_monotonics::Monotonic; From 2658cd93ffe0d27c1d3dfac84399a1b83c9f0856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 9 Jan 2026 16:45:54 +0000 Subject: [PATCH 20/28] mpll: factor --- src/bin/mpll.rs | 186 +----------------------------------------------- src/lib.rs | 2 + src/mpll.rs | 184 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 187 insertions(+), 185 deletions(-) create mode 100644 src/mpll.rs diff --git a/src/bin/mpll.rs b/src/bin/mpll.rs index 62cd6d484..13280e729 100644 --- a/src/bin/mpll.rs +++ b/src/bin/mpll.rs @@ -3,23 +3,15 @@ use core::sync::atomic::{Ordering, fence}; -use dsp_fixedpoint::Q32; -use dsp_process::{ - Add, Identity, Inplace, Pair, Parallel, Process, Split, Unsplit, -}; use fugit::ExtU32; -use idsp::iir::{BiquadClamp, DirectForm1, Wdf, WdfState}; use miniconf::Tree; use rtic_monotonics::Monotonic; use serde::Serialize; -use stabilizer::{convert::Gain, statistics}; +use stabilizer::{convert::Gain, mpll::*, statistics}; use platform::{AppSettings, NetSettings}; -const BATCH_SIZE_LOG2: u32 = 3; -const BATCH_SIZE: usize = 1 << BATCH_SIZE_LOG2; - // 100 MHz timer, 128 divider: period of 1.28 µs, ~781.25 KHz. const SAMPLE_TICKS_LOG2: u32 = 7; const SAMPLE_TICKS: u32 = 1 << SAMPLE_TICKS_LOG2; @@ -53,182 +45,6 @@ impl serial_settings::Settings for Settings { } } -#[derive(Debug, Clone, Default)] -pub struct MpllState { - /// Lowpass state - lp: [(((), ([WdfState<2>; 2], (WdfState<2>, WdfState<1>))), ()); 4], - /// Phase clamp - /// - /// Makes the phase wrap monotonic by clamping it. - /// Aids capture/pull-in with external modulation. - /// - /// TODO: Remove this for modulation drive. - clamp: idsp::Clamp, - /// PID state - iir: DirectForm1, - /// Current output phase - phase: i32, - /// Current LO samples for downconverting the next batch - lo: [[i32; BATCH_SIZE]; 2], - // TODO: investigate matched LO delay: 20 samples -} - -#[derive(Debug, Clone, Tree)] -#[tree(meta(doc, typename))] -pub struct Mpll { - /// Lowpass - #[tree(skip)] - lp: Pair<[Wdf<2, 0xad>; 2], (Wdf<2, 0xad>, Wdf<1, 0xa>), i32>, - /// Input phase offset - phase: i32, - /// PID IIR filter - /// - /// Do not use the iir offset as phase offset. - /// Includes frequency limits. - #[tree(skip)] - iir: BiquadClamp, i32>, - /// Output amplitude scale - amp: [i32; 2], -} - -impl Mpll { - fn new(config: &Self) -> Self { - config.clone() - } -} - -impl Default for Mpll { - fn default() -> Self { - // 7th order Cheby2 as WDF-CA, -3 dB @ 0.005*1.28, -112 dB @ 0.018*1.28 - let lp = Pair::new(( - ( - Unsplit(&Identity), - Parallel(( - [ - Wdf::quantize(&[ - -0.9866183703960676, - 0.9995042973541263, - ]) - .unwrap(), - Wdf::quantize(&[-0.94357710177202, 0.9994723555364557]) - .unwrap(), - ], - ( - Wdf::quantize(&[ - -0.9619459355859967, - 0.9994905727027024, - ]) - .unwrap(), - Wdf::quantize(&[0.9677764552414969]).unwrap(), - ), - )), - ), - Unsplit(&Add), - )); - - let mut iir: BiquadClamp<_, _> = idsp::iir::PidBuilder::default() - .order(idsp::iir::Order::I) - .period(1.0 / BATCH_SIZE as f32) - .gain(idsp::iir::Action::P, -5e-3) // fs/turn - .gain(idsp::iir::Action::I, -4e-4) // fs/turn/ts = 1/turn - .gain(idsp::iir::Action::D, -4e-3) // fs/turn*ts = turn - .limit(idsp::iir::Action::D, -0.2) - .into(); - iir.max = (0.3 * (1u64 << 32) as f32) as _; - iir.min = (0.005 * (1u64 << 32) as f32) as _; - - Self { - lp, - phase: (0.0 * (1u64 << 32) as f32) as _, - iir, - amp: [(0.09 * (1u64 << 31) as f32) as _; 2], // ~0.9 V - } - } -} - -impl Mpll { - /// Ingest and process a batch of input samples and output new modulation - pub fn process( - &self, - state: &mut MpllState, - x: [&[u16; BATCH_SIZE]; 2], - y: [&mut [u16; BATCH_SIZE]; 2], - ) -> (Stream, i32) { - // scratch - let mut mix = [[0; BATCH_SIZE]; 4]; - let [m00, m01, m10, m11] = mix.each_mut(); - // mix - for (x, (mix, lo)) in x[0].iter().zip(x[1].iter()).zip( - m00.iter_mut() - .zip(m01.iter_mut()) - .zip(m10.iter_mut().zip(m11.iter_mut())) - .zip(state.lo[0].iter().zip(&state.lo[1])), - ) { - // ADC encoding, mix - *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 - for (mix, state) in mix.iter_mut().zip(state.lp.iter_mut()) { - Split::new(&self.lp, state).inplace(mix); - } - // decimate - let demod = [ - mix[0][BATCH_SIZE - 1], - mix[1][BATCH_SIZE - 1], - mix[2][BATCH_SIZE - 1], - mix[3][BATCH_SIZE - 1], - ]; - // phase - // need full atan2 or addtl osc to support any phase offset - let p = idsp::atan2(demod[1], demod[0]); - // Delay correction - let p = p - .wrapping_add(self.phase) - .wrapping_sub(state.iir.xy[2].wrapping_mul(10)); - let p = state.clamp.process(p); - // pid - let f = Split::new(&self.iir, &mut state.iir).process(p); - // modulate - let [y0, y1] = state.lo.each_mut(); - let [yo0, yo1] = y; - state.phase = y0 - .iter_mut() - .zip(y1) - .zip((*yo0).iter_mut().zip((*yo1).iter_mut())) - .fold(state.phase, |mut p, (y, yo)| { - p = p.wrapping_add(f); - (*y.0, *y.1) = idsp::cossin(p); - *yo.0 = (((*y.0 as i64 * self.amp[0] as i64) >> (31 + 31 - 15)) - as i16) - .wrapping_add(i16::MIN) as u16; // DAC encoding - *yo.1 = (((*y.1 as i64 * self.amp[1] as i64) >> (31 + 31 - 15)) - as i16) - .wrapping_add(i16::MIN) as u16; // DAC encoding - p - }); - ( - Stream { - demod, - phase_in: p, - phase_out: state.phase, - }, - f, - ) - } -} - -/// Stream data format. -#[derive(Clone, Copy, Debug, Default, bytemuck::Zeroable, bytemuck::Pod)] -#[repr(C)] -pub struct Stream { - demod: [i32; 4], - phase_in: i32, - phase_out: i32, -} - #[derive(Clone, Debug, Tree)] #[tree(meta(doc, typename))] pub struct App { diff --git a/src/lib.rs b/src/lib.rs index db32d99b0..1f06f266b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,3 +11,5 @@ pub mod telemetry; pub mod convert; pub mod statistics; + +pub mod mpll; diff --git a/src/mpll.rs b/src/mpll.rs new file mode 100644 index 000000000..c420b8afb --- /dev/null +++ b/src/mpll.rs @@ -0,0 +1,184 @@ +use dsp_fixedpoint::Q32; +use dsp_process::{ + Add, Identity, Inplace, Pair, Parallel, Process, Split, Unsplit, +}; +use idsp::iir::{BiquadClamp, DirectForm1, Wdf, WdfState}; +use miniconf::Tree; + +pub const BATCH_SIZE: usize = 8; + +#[derive(Debug, Clone, Default)] +pub struct MpllState { + /// Lowpass state + lp: [(((), ([WdfState<2>; 2], (WdfState<2>, WdfState<1>))), ()); 4], + /// Phase clamp + /// + /// Makes the phase wrap monotonic by clamping it. + /// Aids capture/pull-in with external modulation. + /// + /// TODO: Remove this for modulation drive. + clamp: idsp::Clamp, + /// PID state + pub iir: DirectForm1, + /// Current output phase + phase: i32, + /// Current LO samples for downconverting the next batch + lo: [[i32; BATCH_SIZE]; 2], + // TODO: investigate matched LO delay: 20 samples +} + +#[derive(Debug, Clone, Tree)] +#[tree(meta(doc, typename))] +pub struct Mpll { + /// Lowpass + #[tree(skip)] + lp: Pair<[Wdf<2, 0xad>; 2], (Wdf<2, 0xad>, Wdf<1, 0xa>), i32>, + /// Input phase offset + phase: i32, + /// PID IIR filter + /// + /// Do not use the iir offset as phase offset. + /// Includes frequency limits. + #[tree(skip)] + iir: BiquadClamp, i32>, + /// Output amplitude scale + amp: [i32; 2], +} + +impl Mpll { + pub fn new(config: &Self) -> Self { + config.clone() + } +} + +impl Default for Mpll { + fn default() -> Self { + // 7th order Cheby2 as WDF-CA, -3 dB @ 0.005*1.28, -112 dB @ 0.018*1.28 + let lp = Pair::new(( + ( + Unsplit(&Identity), + Parallel(( + [ + Wdf::quantize(&[ + -0.9866183703960676, + 0.9995042973541263, + ]) + .unwrap(), + Wdf::quantize(&[-0.94357710177202, 0.9994723555364557]) + .unwrap(), + ], + ( + Wdf::quantize(&[ + -0.9619459355859967, + 0.9994905727027024, + ]) + .unwrap(), + Wdf::quantize(&[0.9677764552414969]).unwrap(), + ), + )), + ), + Unsplit(&Add), + )); + + let mut iir: BiquadClamp<_, _> = idsp::iir::PidBuilder::default() + .order(idsp::iir::Order::I) + .period(1.0 / BATCH_SIZE as f32) + .gain(idsp::iir::Action::P, -5e-3) // fs/turn + .gain(idsp::iir::Action::I, -4e-4) // fs/turn/ts = 1/turn + .gain(idsp::iir::Action::D, -4e-3) // fs/turn*ts = turn + .limit(idsp::iir::Action::D, -0.2) + .into(); + iir.max = (0.3 * (1u64 << 32) as f32) as _; + iir.min = (0.005 * (1u64 << 32) as f32) as _; + + Self { + lp, + phase: (0.0 * (1u64 << 32) as f32) as _, + iir, + amp: [(0.09 * (1u64 << 31) as f32) as _; 2], // ~0.9 V + } + } +} + +impl Mpll { + /// Ingest and process a batch of input samples and output new modulation + pub fn process( + &self, + state: &mut MpllState, + x: [&[u16; BATCH_SIZE]; 2], + y: [&mut [u16; BATCH_SIZE]; 2], + ) -> (Stream, i32) { + // scratch + let mut mix = [[0; BATCH_SIZE]; 4]; + let [m00, m01, m10, m11] = mix.each_mut(); + // mix + for (x, (mix, lo)) in x[0].iter().zip(x[1].iter()).zip( + m00.iter_mut() + .zip(m01.iter_mut()) + .zip(m10.iter_mut().zip(m11.iter_mut())) + .zip(state.lo[0].iter().zip(&state.lo[1])), + ) { + // ADC encoding, mix + *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 + for (mix, state) in mix.iter_mut().zip(state.lp.iter_mut()) { + Split::new(&self.lp, state).inplace(mix); + } + // decimate + let demod = [ + mix[0][BATCH_SIZE - 1], + mix[1][BATCH_SIZE - 1], + mix[2][BATCH_SIZE - 1], + mix[3][BATCH_SIZE - 1], + ]; + // phase + // need full atan2 or addtl osc to support any phase offset + let p = idsp::atan2(demod[1], demod[0]); + // Delay correction + let p = p + .wrapping_add(self.phase) + .wrapping_sub(state.iir.xy[2].wrapping_mul(10)); + let p = state.clamp.process(p); + // pid + let f = Split::new(&self.iir, &mut state.iir).process(p); + // modulate + let [y0, y1] = state.lo.each_mut(); + let [yo0, yo1] = y; + state.phase = y0 + .iter_mut() + .zip(y1) + .zip((*yo0).iter_mut().zip((*yo1).iter_mut())) + .fold(state.phase, |mut p, (y, yo)| { + p = p.wrapping_add(f); + (*y.0, *y.1) = idsp::cossin(p); + *yo.0 = (((*y.0 as i64 * self.amp[0] as i64) >> (31 + 31 - 15)) + as i16) + .wrapping_add(i16::MIN) as u16; // DAC encoding + *yo.1 = (((*y.1 as i64 * self.amp[1] as i64) >> (31 + 31 - 15)) + as i16) + .wrapping_add(i16::MIN) as u16; // DAC encoding + p + }); + ( + Stream { + demod, + phase_in: p, + phase_out: state.phase, + }, + f, + ) + } +} + +/// Stream data format. +#[derive(Clone, Copy, Debug, Default, bytemuck::Zeroable, bytemuck::Pod)] +#[repr(C)] +pub struct Stream { + pub demod: [i32; 4], + pub phase_in: i32, // after clamping and offset + pub phase_out: i32, // derivative is output frequency +} From cbf04dfffffb87f445a8fc9037e3a27ffaf22a0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 9 Jan 2026 16:46:20 +0000 Subject: [PATCH 21/28] deps/ci: fix schema build --- .github/workflows/ci.yml | 9 +++--- Cargo.lock | 64 +++++++++++++++++++++++++++++++++------- Cargo.toml | 3 +- 3 files changed, 61 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e92d18f0..f58f238f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,10 +62,11 @@ jobs: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable # https://github.com/rust-lang/cargo/issues/13051 - - run: cargo run --bin dual-iir --target x86_64-unknown-linux-gnu - - run: cargo run --bin lockin --target x86_64-unknown-linux-gnu - - run: cargo run --bin dds --target x86_64-unknown-linux-gnu - - run: cargo run --bin mpll --target x86_64-unknown-linux-gnu + - run: cargo run --bin dual-iir --target host-tuple + - run: cargo run --bin lockin --target host-tuple + - run: cargo run --bin dds --target host-tuple + - run: cargo run --bin mpll --target host-tuple + - run: cargo run --bin fls --target host-tuple doc: runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index 1164d673f..34290d7a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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.113", ] +[[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.113", +] + [[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.113", +] + +[[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.113", ] @@ -535,6 +569,17 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "heapless" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed" +dependencies = [ + "hash32 0.3.1", + "serde_core", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.5.0" @@ -793,10 +838,10 @@ dependencies = [ [[package]] name = "miniconf" version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111e5168eaece53a023515c386ebe4581e382a21cb91f1134c3a2efcddb9bf71" +source = "git+https://github.com/quartiq/miniconf.git#6bb2b28188e5803677c75edbcc830e32be288334" dependencies = [ "heapless 0.8.0", + "heapless 0.9.2", "itoa", "miniconf_derive", "postcard", @@ -811,10 +856,9 @@ dependencies = [ [[package]] name = "miniconf_derive" version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9826a0808d011e3217d93fd84ac670d75782d1f5962438b53a3f580f4dc14016" +source = "git+https://github.com/quartiq/miniconf.git#6bb2b28188e5803677c75edbcc830e32be288334" dependencies = [ - "darling", + "darling 0.23.0", "proc-macro2", "quote", "syn 2.0.113", @@ -1343,7 +1387,7 @@ dependencies = [ [[package]] name = "serde-reflection" version = "0.5.1" -source = "git+https://github.com/quartiq/serde-reflection.git?branch=pub-ser-de#337a07a9ae56e4139b005a4e2b5a9c3865ea13e8" +source = "git+https://github.com/zefchain/serde-reflection.git#b19be2ccd00a4956c4283b82661b9a0995b11518" dependencies = [ "erased-discriminant", "once_cell", @@ -1403,7 +1447,7 @@ version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" dependencies = [ - "darling", + "darling 0.21.3", "proc-macro2", "quote", "syn 2.0.113", diff --git a/Cargo.toml b/Cargo.toml index aa75ab267..58880e086 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -157,7 +157,8 @@ debug = true lto = true [patch.crates-io] -serde-reflection = { git = "https://github.com/quartiq/serde-reflection.git", branch = "pub-ser-de" } +miniconf = { git = "https://github.com/quartiq/miniconf.git" } +serde-reflection = { git = "https://github.com/zefchain/serde-reflection.git" } idsp = { path = "../idsp" } dsp-process = { path = "../idsp/dsp-process" } dsp-fixedpoint = { path = "../idsp/dsp-fixedpoint" } From d1436ed052f7fcb7a022bb540436c33863841924 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 9 Jan 2026 17:31:12 +0000 Subject: [PATCH 22/28] idsp: trait move --- src/bin/fls.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/bin/fls.rs b/src/bin/fls.rs index 288b95857..fd57fac5f 100644 --- a/src/bin/fls.rs +++ b/src/bin/fls.rs @@ -79,8 +79,9 @@ use ad9959::Acr; use arbitrary_int::{u14, u24}; -use dsp_fixedpoint::{Const, Q32}; +use dsp_fixedpoint::Q32; use dsp_process::{Process, SplitProcess}; +use idsp::Const; use idsp::{ Accu, Complex, ComplexExt, Lockin, Lowpass, LowpassState, PLL, Unwrapper, iir::{ @@ -178,7 +179,7 @@ where mod biquad_update { use super::BiquadReprTree; - use dsp_fixedpoint::Const; + use idsp::Const; use idsp::iir::{BiquadClamp, BiquadRepr, Pid}; use miniconf::{Keys, SerdeError, leaf}; pub use miniconf::{ From ab78cca6d34f884fc4e4f5641ae45c8fc4f946ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 9 Jan 2026 20:40:27 +0000 Subject: [PATCH 23/28] accu: wrapping --- src/bin/fls.rs | 17 ++++++++++++----- src/bin/lockin.rs | 9 +++++++-- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/bin/fls.rs b/src/bin/fls.rs index fd57fac5f..6e40b3e65 100644 --- a/src/bin/fls.rs +++ b/src/bin/fls.rs @@ -702,7 +702,10 @@ fn main() { #[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 core::{ + num::Wrapping, + sync::atomic::{Ordering, fence}, + }; use fugit::ExtU32 as _; use rtic_monotonics::Monotonic; @@ -887,11 +890,15 @@ mod app { let mut demod = [Complex::::default(); BATCH_SIZE]; // 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::new( - (pll.phase() << BATCH_SIZE_LOG2) - .wrapping_mul(settings.lockin_freq as _), - pll.frequency().wrapping_mul(settings.lockin_freq as _), + Wrapping( + (pll.phase() << BATCH_SIZE_LOG2) + .wrapping_mul(settings.lockin_freq as _), + ), + Wrapping( + pll.frequency().wrapping_mul(settings.lockin_freq as _), + ), )) { - *d = Complex::from_angle(p); + *d = Complex::from_angle(p.0); } (adc0, adc1, dac0, dac1).lock(|adc0, adc1, dac0, dac1| { diff --git a/src/bin/lockin.rs b/src/bin/lockin.rs index 7e9da8496..dd8358f35 100644 --- a/src/bin/lockin.rs +++ b/src/bin/lockin.rs @@ -196,6 +196,8 @@ fn main() { #[cfg(target_os = "none")] #[rtic::app(device = stabilizer::hardware::hal::stm32, peripherals = true, dispatchers=[DCMI, JPEG, SDMMC])] mod app { + use core::num::Wrapping; + use super::*; use idsp::LowpassState; use stabilizer::{ @@ -379,11 +381,14 @@ mod app { let output: Complex = adc_samples[0] .iter() // Zip in the LO phase. - .zip(Accu::new(sample_phase, sample_frequency)) + .zip(Accu::new( + Wrapping(sample_phase), + Wrapping(sample_frequency), + )) // Convert to signed, MSB align the ADC sample, update the Lockin (demodulate, filter) .map(|(&sample, phase)| { let s = (sample as i16 as i32) << 16; - settings.lockin_k.process(lockin, (s, phase)) + settings.lockin_k.process(lockin, (s, phase.0)) }) // Decimate .last() From 9ec84532cc70c4664ece82608c3a1e219ff365d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Mon, 12 Jan 2026 15:24:52 +0000 Subject: [PATCH 24/28] idsp: port, [wip] --- src/bin/dual-iir.rs | 4 +- src/bin/fls.rs | 115 ++++++++++++++++++++++---------------------- src/bin/lockin.rs | 10 ++-- src/mpll.rs | 19 +++++--- 4 files changed, 76 insertions(+), 72 deletions(-) diff --git a/src/bin/dual-iir.rs b/src/bin/dual-iir.rs index 17f4edd60..82371f911 100644 --- a/src/bin/dual-iir.rs +++ b/src/bin/dual-iir.rs @@ -86,7 +86,7 @@ pub struct BiquadRepr { /// Biquad parameters #[tree(rename="typ", typ="&str", with=miniconf::str_leaf, defer=self.repr)] _typ: (), - repr: iir::BiquadRepr, + repr: iir::repr::BiquadRepr, } impl Default for BiquadRepr { @@ -96,7 +96,7 @@ impl Default for BiquadRepr { i.max = i16::MAX as _; Self { _typ: (), - repr: iir::BiquadRepr::Raw(i), + repr: iir::repr::BiquadRepr::Raw(i), } } } diff --git a/src/bin/fls.rs b/src/bin/fls.rs index 6e40b3e65..769b9569e 100644 --- a/src/bin/fls.rs +++ b/src/bin/fls.rs @@ -77,15 +77,17 @@ //! See [stream]. To view and analyze noise spectra the graphical application //! [`stabilizer-stream`](https://github.com/quartiq/stabilizer-stream) can be used. +use core::num::Wrapping as W; + use ad9959::Acr; use arbitrary_int::{u14, u24}; -use dsp_fixedpoint::Q32; +use dsp_fixedpoint::{Q32, W32}; use dsp_process::{Process, SplitProcess}; -use idsp::Const; use idsp::{ - Accu, Complex, ComplexExt, Lockin, Lowpass, LowpassState, PLL, Unwrapper, + Clamp, Complex, Lockin, Lowpass, LowpassState, PLL, Unwrapper, iir::{ - Biquad, BiquadClamp, BiquadRepr, DirectForm1, DirectForm1Dither, Pid, + Biquad, BiquadClamp, DirectForm1, DirectForm1Dither, pid::Pid, + repr::BiquadRepr, }, }; use miniconf::Tree; @@ -149,7 +151,7 @@ const F_DEMOD: u32 = 0x5200_0000; #[derive(Clone, Debug, Tree)] pub struct BiquadReprTree where - T: 'static + Const + Copy, + T: 'static + Clamp + Copy, Y: 'static + Copy, f32: AsPrimitive + AsPrimitive, BiquadClamp: Default, @@ -179,8 +181,8 @@ where mod biquad_update { use super::BiquadReprTree; - use idsp::Const; - use idsp::iir::{BiquadClamp, BiquadRepr, Pid}; + use idsp::Clamp; + use idsp::iir::{BiquadClamp, pid::Pid, repr::BiquadRepr}; use miniconf::{Keys, SerdeError, leaf}; pub use miniconf::{ deny::{mut_any_by_key, ref_any_by_key}, @@ -196,7 +198,7 @@ mod biquad_update { ) -> Result> where S: Serializer, - T: 'static + Const + Copy, + T: 'static + Clamp + Copy, Y: 'static + Copy, f32: AsPrimitive + AsPrimitive, BiquadRepr: Default, @@ -213,7 +215,7 @@ mod biquad_update { ) -> Result<(), SerdeError> where D: Deserializer<'de>, - T: 'static + Const + Copy, + T: 'static + Clamp + Copy, Y: 'static + Copy, f32: AsPrimitive + AsPrimitive, BiquadRepr: Default, @@ -241,7 +243,7 @@ mod biquad_update { impl Default for BiquadReprTree where - T: 'static + Const + Copy, + T: 'static + Clamp + Copy, Y: 'static + Copy, f32: AsPrimitive + AsPrimitive, BiquadClamp: Default, @@ -537,7 +539,8 @@ pub struct Fls { /// /// # Default /// `/pll_k = 0x4_0000` corresponds to to a time constant of about 0.4 s. - pll_k: i32, + #[tree(with=miniconf::leaf)] + pll_k: W32<32>, /// Telemetry output period in seconds /// /// # Default @@ -557,7 +560,7 @@ impl Default for Fls { ch: Default::default(), ext_clk: false, lockin_freq: 0x40, - pll_k: 0x4_0000, + pll_k: W32::new(W(0x4_0000)), telemetry_period: 10, stream: Default::default(), } @@ -673,10 +676,10 @@ pub struct CookedTelemetry { #[derive(Clone, Default)] pub struct ChannelState { lockin: [LowpassState<2>; 2], - x0: i32, - t0: i32, - t: i64, - y: i64, + x0: W, + t0: W, + t: W, + y: W, unwrapper: Unwrapper, iir: DirectForm1Dither, iir_amp: DirectForm1, @@ -702,11 +705,9 @@ fn main() { #[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::{ - num::Wrapping, - sync::atomic::{Ordering, fence}, - }; + use core::sync::atomic::{Ordering, fence}; use fugit::ExtU32 as _; + use num_traits::ConstZero; use rtic_monotonics::Monotonic; use stabilizer::hardware::{ @@ -869,7 +870,7 @@ mod app { let timestamp = timestamper .latest_timestamp() .unwrap_or(None) - .map(|t| ((t as u32) << 16) as i32); + .map(|t| W(((t as u32) << 16) as i32)); ( c.shared.state, @@ -880,25 +881,20 @@ mod app { .lock(|state, settings, dds_output, telemetry| { // Reconstruct frequency and phase using a lowpass that is aware of phase and frequency // wraps. - settings.pll_k.process(pll, timestamp); + let mut accu = settings.pll_k.process(pll, timestamp); // TODO: implement clear - stream[0].pll = pll.frequency() as _; - stream[1].pll = pll.phase() as _; + stream[0].pll = accu.step.0 as _; + stream[1].pll = accu.state.0 as _; telemetry.pll_time = - telemetry.pll_time.wrapping_add(pll.frequency() as _); + telemetry.pll_time.wrapping_add(pll.frequency().0 as _); - let mut demod = [Complex::::default(); BATCH_SIZE]; + 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::new( - Wrapping( - (pll.phase() << BATCH_SIZE_LOG2) - .wrapping_mul(settings.lockin_freq as _), - ), - Wrapping( - pll.frequency().wrapping_mul(settings.lockin_freq as _), - ), - )) { - *d = Complex::from_angle(p.0); + for (d, p) in demod.iter_mut().zip(accu) { + *d = Complex::from_angle(p); } (adc0, adc1, dac0, dac1).lock(|adc0, adc1, dac0, dac1| { @@ -947,8 +943,8 @@ mod app { let di = [digital_inputs.0.is_high(), digital_inputs.1.is_high()]; // TODO: pll.frequency()? - let time = pll.phase() & (-1 << PHASE_SCALE_SHIFT); - let dtime = time.wrapping_sub(state[0].t0) >> PHASE_SCALE_SHIFT; + 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; @@ -972,8 +968,8 @@ mod app { if settings.clear { state.unwrapper = Unwrapper::default(); - state.t = 0; - state.y = 0; + state.t = W(0); + state.y = W(0); settings.clear = false; } @@ -993,37 +989,38 @@ mod app { (0, stream.delta_pow) } else { let phase = stream.demod.arg(); - let dphase = phase.wrapping_sub(state.x0); + 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.wrapping_add(1 << 30) < 0 { + if dphase + W(1 << 30) < W::ZERO { telemetry.slips = telemetry.slips.wrapping_add(1); } // Scale, offset and unwrap phase - state.t = state.t.wrapping_add( - dtime as i64 * settings.phase_scale[1][0] as i64, - ); - state.y = state.y.wrapping_add( - dphase as i64 * settings.phase_scale[0][0] as i64, - ); + 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 i32) + ((state.y >> settings.phase_scale[0][1] as usize).0 + as i32) .wrapping_add( - (state.t >> settings.phase_scale[1][1]) + (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 = state - .unwrapper - .y - .clamp(-i32::MAX as _, i32::MAX as _) - as _; + let phase_err = Clamp::clamp( + state.unwrapper.y, + -i32::MAX as _, + i32::MAX as _, + ) as _; stats.update(phase_err); // TODO; unchecked_shr @@ -1054,9 +1051,11 @@ mod app { )), Some( Acr::DEFAULT - .with_asf(u10::new( - telemetry.mod_amp.clamp(0, 0x3ff), - )) + .with_asf(u10::new(Clamp::clamp( + telemetry.mod_amp, + 0, + 0x3ff, + ))) .with_multiplier(true), ), ); diff --git a/src/bin/lockin.rs b/src/bin/lockin.rs index dd8358f35..f743086dd 100644 --- a/src/bin/lockin.rs +++ b/src/bin/lockin.rs @@ -35,7 +35,7 @@ use core::{ use dsp_process::SplitProcess; use fugit::ExtU32; -use idsp::{Accu, Complex, ComplexExt, Lowpass, RPLL, RPLLConfig}; +use idsp::{Accu, Complex, Lowpass, RPLL, RPLLConfig}; use miniconf::{Leaf, Tree}; use rtic_monotonics::Monotonic; use serde::{Deserialize, Serialize}; @@ -388,7 +388,7 @@ mod app { // Convert to signed, MSB align the ADC sample, update the Lockin (demodulate, filter) .map(|(&sample, phase)| { let s = (sample as i16 as i32) << 16; - settings.lockin_k.process(lockin, (s, phase.0)) + settings.lockin_k.process(lockin, (s, phase)) }) // Decimate .last() @@ -399,8 +399,10 @@ mod app { for (channel, samples) in dac_samples.iter_mut().enumerate() { for sample in samples.iter_mut() { let value = match *settings.output_conf[channel] { - Conf::Magnitude => output.norm_sqr() as i32 >> 16, - Conf::Phase => output.arg() >> 16, + Conf::Magnitude => { + output.norm_sqr().inner as i32 >> 16 + } + Conf::Phase => output.arg().0 >> 16, Conf::LogPower => output.log2() << 8, Conf::ReferenceFrequency => { reference_frequency >> 16 diff --git a/src/mpll.rs b/src/mpll.rs index c420b8afb..cf206de72 100644 --- a/src/mpll.rs +++ b/src/mpll.rs @@ -2,7 +2,10 @@ use dsp_fixedpoint::Q32; use dsp_process::{ Add, Identity, Inplace, Pair, Parallel, Process, Split, Unsplit, }; -use idsp::iir::{BiquadClamp, DirectForm1, Wdf, WdfState}; +use idsp::iir::{ + BiquadClamp, DirectForm1, + wdf::{Wdf, WdfState}, +}; use miniconf::Tree; pub const BATCH_SIZE: usize = 8; @@ -17,7 +20,7 @@ pub struct MpllState { /// Aids capture/pull-in with external modulation. /// /// TODO: Remove this for modulation drive. - clamp: idsp::Clamp, + clamp: idsp::ClampWrap, /// PID state pub iir: DirectForm1, /// Current output phase @@ -80,13 +83,13 @@ impl Default for Mpll { Unsplit(&Add), )); - let mut iir: BiquadClamp<_, _> = idsp::iir::PidBuilder::default() - .order(idsp::iir::Order::I) + let mut iir: BiquadClamp<_, _> = idsp::iir::pid::Builder::default() + .order(idsp::iir::pid::Order::I) .period(1.0 / BATCH_SIZE as f32) - .gain(idsp::iir::Action::P, -5e-3) // fs/turn - .gain(idsp::iir::Action::I, -4e-4) // fs/turn/ts = 1/turn - .gain(idsp::iir::Action::D, -4e-3) // fs/turn*ts = turn - .limit(idsp::iir::Action::D, -0.2) + .gain(idsp::iir::pid::Action::P, -5e-3) // fs/turn + .gain(idsp::iir::pid::Action::I, -4e-4) // fs/turn/ts = 1/turn + .gain(idsp::iir::pid::Action::D, -4e-3) // fs/turn*ts = turn + .limit(idsp::iir::pid::Action::D, -0.2) .into(); iir.max = (0.3 * (1u64 << 32) as f32) as _; iir.min = (0.005 * (1u64 << 32) as f32) as _; From 9446a5cf5b641ff2ea8bc13b26ea8a55d84a49aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Mon, 12 Jan 2026 22:14:29 +0000 Subject: [PATCH 25/28] idsp: rpll port --- src/bin/lockin.rs | 62 ++++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/src/bin/lockin.rs b/src/bin/lockin.rs index f743086dd..d053b720e 100644 --- a/src/bin/lockin.rs +++ b/src/bin/lockin.rs @@ -30,6 +30,7 @@ use core::{ iter, mem::MaybeUninit, + num::Wrapping, sync::atomic::{Ordering, fence}, }; @@ -133,13 +134,15 @@ pub struct Lockin { /// Specifies which harmonic to use for the lockin. /// /// Harmonic index of the LO. -1 to _de_modulate the fundamental (complex conjugate) - lockin_harmonic: i32, + #[tree(with=miniconf::leaf)] + lockin_harmonic: Wrapping, /// Specifies the LO phase offset. /// /// Demodulation LO phase offset. Units are in terms of i32, where [i32::MIN] is equivalent to /// -pi and [i32::MAX] is equivalent to +pi. - lockin_phase: i32, + #[tree(with=miniconf::leaf)] + lockin_phase: Wrapping, /// Specifies DAC output mode. output_conf: [Leaf; 2], @@ -166,8 +169,8 @@ impl Default for Lockin { }, // frequency and phase settling time (log2 counter cycles) lockin_k: idsp::Lockin(Lowpass([0x8_0000, -0x400_0000])), // lockin lowpass gains - lockin_harmonic: -1, // Harmonic index of the LO: -1 to _de_modulate the fundamental (complex conjugate) - lockin_phase: 0, // Demodulation LO phase offset + lockin_harmonic: Wrapping(-1), // Harmonic index of the LO: -1 to _de_modulate the fundamental (complex conjugate) + lockin_phase: Wrapping(0), // Demodulation LO phase offset output_conf: [Leaf(Conf::InPhase), Leaf(Conf::Quadrature)], // The default telemetry period in seconds. @@ -196,9 +199,8 @@ fn main() { #[cfg(target_os = "none")] #[rtic::app(device = stabilizer::hardware::hal::stm32, peripherals = true, dispatchers=[DCMI, JPEG, SDMMC])] mod app { - use core::num::Wrapping; - use super::*; + use core::num::Wrapping; use idsp::LowpassState; use stabilizer::{ hardware::{ @@ -349,27 +351,30 @@ mod app { } = c.local; (active_settings, telemetry).lock(|settings, telemetry| { - let (reference_phase, reference_frequency) = - match settings.lockin_mode { - LockinMode::External => { - let timestamp = - timestamper.latest_timestamp().unwrap_or(None); // Ignore data from timer capture overflows. - let (pll_phase, pll_frequency) = settings - .pll_tc - .process(pll, timestamp.map(|t| t as i32)); - (pll_phase, (pll_frequency >> BATCH_SIZE_LOG2) as i32) - } - LockinMode::Internal => { - // Reference phase and frequency are known. - (1i32 << 30, 1i32 << (32 - BATCH_SIZE_LOG2)) - } - }; + let (reference_phase, reference_frequency) = match settings + .lockin_mode + { + LockinMode::External => { + let timestamp = + timestamper.latest_timestamp().unwrap_or(None); // Ignore data from timer capture overflows. + let accu = settings + .pll_tc + .process(pll, timestamp.map(|t| Wrapping(t as i32))); + (accu.state, (accu.step >> BATCH_SIZE_LOG2 as usize)) + } + LockinMode::Internal => { + // Reference phase and frequency are known. + ( + Wrapping(1i32 << 30), + Wrapping(1i32 << (32 - BATCH_SIZE_LOG2)), + ) + } + }; let sample_frequency = - reference_frequency.wrapping_mul(settings.lockin_harmonic); - let sample_phase = settings.lockin_phase.wrapping_add( - reference_phase.wrapping_mul(settings.lockin_harmonic), - ); + reference_frequency * settings.lockin_harmonic; + let sample_phase = settings.lockin_phase + + reference_phase * settings.lockin_harmonic; (adc0, adc1, dac0, dac1).lock(|adc0, adc1, dac0, dac1| { let adc_samples = [adc0, adc1]; @@ -381,10 +386,7 @@ mod app { let output: Complex = adc_samples[0] .iter() // Zip in the LO phase. - .zip(Accu::new( - Wrapping(sample_phase), - Wrapping(sample_frequency), - )) + .zip(Accu::new(sample_phase, sample_frequency)) // Convert to signed, MSB align the ADC sample, update the Lockin (demodulate, filter) .map(|(&sample, phase)| { let s = (sample as i16 as i32) << 16; @@ -405,7 +407,7 @@ mod app { Conf::Phase => output.arg().0 >> 16, Conf::LogPower => output.log2() << 8, Conf::ReferenceFrequency => { - reference_frequency >> 16 + reference_frequency.0 >> 16 } Conf::InPhase => output.re() >> 16, Conf::Quadrature => output.im() >> 16, From ae4e8154992cda5ad54dc4d8f3d9ac9f501c2143 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Tue, 13 Jan 2026 11:27:32 +0000 Subject: [PATCH 26/28] mpll: imports --- src/mpll.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/mpll.rs b/src/mpll.rs index cf206de72..a18553661 100644 --- a/src/mpll.rs +++ b/src/mpll.rs @@ -3,7 +3,7 @@ use dsp_process::{ Add, Identity, Inplace, Pair, Parallel, Process, Split, Unsplit, }; use idsp::iir::{ - BiquadClamp, DirectForm1, + BiquadClamp, DirectForm1, pid, wdf::{Wdf, WdfState}, }; use miniconf::Tree; @@ -83,13 +83,13 @@ impl Default for Mpll { Unsplit(&Add), )); - let mut iir: BiquadClamp<_, _> = idsp::iir::pid::Builder::default() - .order(idsp::iir::pid::Order::I) + let mut iir: BiquadClamp<_, _> = pid::Builder::default() + .order(pid::Order::I) .period(1.0 / BATCH_SIZE as f32) - .gain(idsp::iir::pid::Action::P, -5e-3) // fs/turn - .gain(idsp::iir::pid::Action::I, -4e-4) // fs/turn/ts = 1/turn - .gain(idsp::iir::pid::Action::D, -4e-3) // fs/turn*ts = turn - .limit(idsp::iir::pid::Action::D, -0.2) + .gain(pid::Action::P, -5e-3) // fs/turn + .gain(pid::Action::I, -4e-4) // fs/turn/ts = 1/turn + .gain(pid::Action::D, -4e-3) // fs/turn*ts = turn + .limit(pid::Action::D, -0.2) .into(); iir.max = (0.3 * (1u64 << 32) as f32) as _; iir.min = (0.005 * (1u64 << 32) as f32) as _; From 6f473661b33d33ef806741dca99283c367c6075f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Tue, 13 Jan 2026 16:07:32 +0000 Subject: [PATCH 27/28] workspace dependencies --- Cargo.lock | 8 +++++++- Cargo.toml | 32 +++++++++++++++++++++----------- ad9912/Cargo.toml | 4 ++-- ad9959/Cargo.toml | 2 +- encoded_pin/Cargo.toml | 2 +- platform/Cargo.toml | 4 ++-- serial_settings/Cargo.toml | 4 ++-- signal_generator/Cargo.toml | 6 +++--- stream/Cargo.toml | 2 +- urukul/Cargo.toml | 8 +++----- 10 files changed, 43 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 34290d7a9..51e5f5c4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -288,6 +288,8 @@ 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", @@ -296,6 +298,8 @@ dependencies = [ [[package]] name = "dsp-process" version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f999350286aca82b7fb79f05318f2abe0967b1f068d7a24c573c02369d1153f5" [[package]] name = "dyn-clone" @@ -696,7 +700,9 @@ dependencies = [ [[package]] name = "idsp" -version = "0.19.0" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f514ce6181d9bf2385f0a0ba7df1d63ea4c561ac7b86067345a9e73fba5c15f7" dependencies = [ "bytemuck", "dsp-fixedpoint", diff --git a/Cargo.toml b/Cargo.toml index 58880e086..e6eeae7c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,20 @@ members = [ "stream", "platform", ] +[workspace.dependencies] +arbitrary-int = { version = "1.3.0", features = ["serde", "hint"] } +thiserror = { version = "2.0.11", default-features = false } +idsp = "0.20.0" +dsp-process = "0.1.0" +dsp-fixedpoint = "0.1.0" +# Keep this synced with the miniconf version in py/setup.py +miniconf = { version = "0.20", features = [ + "json-core", + "derive", + "postcard", + "heapless", +] } +heapless = { version = "0.8", features = ["serde"] } [dependencies] panic-persist = { version = "0.3", features = ["utf8", "custom-panic-handler"] } @@ -50,13 +64,13 @@ log = { version = "0.4", features = [ "release_max_level_info", ] } serde = { version = "1.0", features = ["derive"], default-features = false } -heapless = { version = "0.8", features = ["serde"] } +heapless.workspace = true rtic-monotonics = { version = "2.0", features = ["cortex-m-systick"] } num_enum = { version = "0.7.3", default-features = false } paste = "1" -idsp = "0.19.0" -dsp-process = "0.1.0" -dsp-fixedpoint = "0.1.0" +idsp.workspace = true +dsp-process.workspace = true +dsp-fixedpoint.workspace = true ad9959 = { path = "ad9959", version = "0.3.0" } serial_settings = { version = "0.2", path = "serial_settings" } mcp230xx = "1.0" @@ -68,13 +82,12 @@ shared-bus = { version = "0.3", features = ["cortex-m"] } lm75 = "0.2" usb-device = "0.3.2" usbd-serial = "0.2" -# Keep this synced with the miniconf version in py/setup.py -miniconf = { version = "0.20", features = ["json-core", "derive", "postcard"] } +miniconf.workspace = true miniconf_mqtt = { version = "0.20" } tca9539 = "0.2" bitbybit = "1.3.3" -arbitrary-int = { version = "1.3.0", features = ["serde", "hint"] } -thiserror = { version = "2.0.11", default-features = false } +arbitrary-int.workspace = true +thiserror.workspace = true embedded-hal-compat = "0.13.0" embedded-hal-02 = { package = "embedded-hal", version = "0.2.7", features = [ "unproven", @@ -159,6 +172,3 @@ lto = true [patch.crates-io] miniconf = { git = "https://github.com/quartiq/miniconf.git" } serde-reflection = { git = "https://github.com/zefchain/serde-reflection.git" } -idsp = { path = "../idsp" } -dsp-process = { path = "../idsp/dsp-process" } -dsp-fixedpoint = { path = "../idsp/dsp-fixedpoint" } diff --git a/ad9912/Cargo.toml b/ad9912/Cargo.toml index 01e91c947..36485ed09 100644 --- a/ad9912/Cargo.toml +++ b/ad9912/Cargo.toml @@ -10,8 +10,8 @@ categories = ["embedded", "no-std"] keywords = ["spi"] [dependencies] -arbitrary-int = "1.3.0" -thiserror = { version = "2.0.11", default-features = false } +arbitrary-int.workspace = true +thiserror.workspace = true num-traits = { version = "0.2.19", default-features = false } embedded-hal = "1.0" bitbybit = "1.3.3" diff --git a/ad9959/Cargo.toml b/ad9959/Cargo.toml index 85bac4259..aad80caa7 100644 --- a/ad9959/Cargo.toml +++ b/ad9959/Cargo.toml @@ -11,4 +11,4 @@ description = "AD9959 4-channel DDS SPI driver" embedded-hal = {version = "0.2.7", features = ["unproven"]} bytemuck = "1.21.0" bitbybit = "1.3.3" -arbitrary-int = { version = "1.3.0", features = ["serde"] } +arbitrary-int.workspace = true diff --git a/encoded_pin/Cargo.toml b/encoded_pin/Cargo.toml index c7d895557..6002e9032 100644 --- a/encoded_pin/Cargo.toml +++ b/encoded_pin/Cargo.toml @@ -12,5 +12,5 @@ categories = ["embedded", "no-std"] keywords = ["spi"] [dependencies] -arbitrary-int = { version = "1.3.0", features = ["serde"] } +arbitrary-int.workspace = true embedded-hal = { version = "1.0" } diff --git a/platform/Cargo.toml b/platform/Cargo.toml index 9b35e8dd5..9e2e1bbb2 100644 --- a/platform/Cargo.toml +++ b/platform/Cargo.toml @@ -10,9 +10,9 @@ categories = ["embedded", "no-std"] keywords = [] [dependencies] -miniconf = { version = "0.20", features = ["derive", "heapless", "postcard"] } +miniconf.workspace = true serde = { version = "1.0", features = ["derive"], default-features = false } -heapless = { version = "0.8", features = ["serde"] } +heapless.workspace = true log = { version = "0.4" } cortex-m = { version = "0.7.7" } sequential-storage = "5" diff --git a/serial_settings/Cargo.toml b/serial_settings/Cargo.toml index 70ace07be..90d282d60 100644 --- a/serial_settings/Cargo.toml +++ b/serial_settings/Cargo.toml @@ -17,9 +17,9 @@ keywords = ["serial", "settings", "cli", "management", "async"] maintenance = { status = "actively-developed" } [dependencies] -miniconf = { version = "0.20", features = ["json-core", "postcard"] } +miniconf.workspace = true menu = { version = "0.6.1", features = ["echo"] } -heapless = "0.8" +heapless.workspace = true embedded-io = "0.6" yafnv = "3.0" postcard = { version = "1", default-features = false } diff --git a/signal_generator/Cargo.toml b/signal_generator/Cargo.toml index 57ff9043e..a3073aaba 100644 --- a/signal_generator/Cargo.toml +++ b/signal_generator/Cargo.toml @@ -14,7 +14,7 @@ keywords = [] [dependencies] rand_xorshift = "0.4.0" rand_core = "0.9.0" -thiserror = { version = "2.0.11", default-features = false } -idsp = "0.19.0" -miniconf = { version = "0.20", features = ["derive"] } serde = { version = "1.0", features = ["derive"], default-features = false } +thiserror.workspace = true +idsp.workspace = true +miniconf.workspace = true diff --git a/stream/Cargo.toml b/stream/Cargo.toml index 663fbab74..b43dd014d 100644 --- a/stream/Cargo.toml +++ b/stream/Cargo.toml @@ -11,7 +11,7 @@ keywords = [] [dependencies] serde = { version = "1.0", features = ["derive"], default-features = false } -heapless = { version = "0.8", features = ["serde"] } +heapless.workspace = true num_enum = { version = "0.7.3", default-features = false } serde_with = { version = "3.12", default-features = false, features = [ "macros", diff --git a/urukul/Cargo.toml b/urukul/Cargo.toml index 8e3cc49b2..cb01e52d3 100644 --- a/urukul/Cargo.toml +++ b/urukul/Cargo.toml @@ -5,16 +5,14 @@ license.workspace = true repository.workspace = true version = "0.1.1" description = "Sinara Urukul quad 1 GHz DDS driver" -authors = [ - "Robert Jördens ", -] +authors = ["Robert Jördens "] categories = ["embedded", "no-std"] keywords = ["spi"] [dependencies] encoded_pin = { version = "0.1", path = "../encoded_pin" } -arbitrary-int = { version = "1.3.0", features = ["serde"] } -thiserror = { version = "2.0.11", default-features = false } +arbitrary-int.workspace = true +thiserror.workspace = true num-traits = { version = "0.2.19", default-features = false } embedded-hal = { version = "1.0" } embedded-hal-bus = "0.3.0" From 1dcc406580bb77e0c5981aab17bf196b6fe470ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Tue, 13 Jan 2026 16:13:14 +0000 Subject: [PATCH 28/28] ci: fls schema unbuildable --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f58f238f7..28ea2d7c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,7 +66,7 @@ jobs: - run: cargo run --bin lockin --target host-tuple - run: cargo run --bin dds --target host-tuple - run: cargo run --bin mpll --target host-tuple - - run: cargo run --bin fls --target host-tuple + # - run: cargo run --bin fls --target host-tuple doc: runs-on: ubuntu-latest