diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e61cb6979..789c9c9de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,7 +53,7 @@ jobs: strategy: matrix: # keep MSRV in sync in ci.yaml and Cargo.toml - toolchain: [stable, '1.65.0'] + toolchain: [stable, '1.66.1'] features: [''] continue-on-error: [false] include: diff --git a/.gitignore b/.gitignore index 5bb0577cc..f06f206d6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target /dsp/target vpy/ +.DS_Store \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a31b4639d..9120037fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [Unreleased](https://github.com/quartiq/stabilizer/compare/v0.9.0...main) + +### Added +* Serial terminal is available on USB for settings configurations +* Reboot to DFU support added via the serial terminal for remote bootloading +* The `meta` topic now contains metadata about the compiler, firmware, and hardware similar to +Booster +* Panic information is now persisted after reboot and available via telemetry and the USB serial +console. + +### Changed +* Broker is no longer configured at compile time, but is maintained in device memory +* MSRV bumped to v1.66 +* The IIR (biquad) filter used for PID action has changed its serialization format. + See also the `iir_coefficients` Python CLI implementation. +* Bumped MSRV 1.66.0 -> 1.66.1 + ## [0.9.0](https://github.com/quartiq/stabilizer/compare/v0.8.1...v0.9.0) ### Fixed @@ -21,7 +38,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). * `hitl/streaming.py` no longer requires a prefix * Streaming now supports UDP multicast addresses -## [v0.8.1](https://github.com/quartiq/stabilizer/compare/v0.8.0...v0.8.1) - 2022-11-14) +## [v0.8.1](https://github.com/quartiq/stabilizer/compare/v0.8.0...v0.8.1) - 2022-11-14 * Fixed the python package dependencies diff --git a/Cargo.lock b/Cargo.lock index 0589da15d..3d6a2e76b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7,7 +7,7 @@ name = "ad9959" version = "0.2.1" dependencies = [ "bit_field", - "bitflags 2.4.1", + "bitflags 2.4.2", "bytemuck", "embedded-hal", ] @@ -33,15 +33,6 @@ dependencies = [ "stable_deref_trait", ] -[[package]] -name = "atomic-polyfill" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ff7eb3f316534d83a8a2c3d1674ace8a5a71198eba31e2e2b597833f699b28" -dependencies = [ - "critical-section", -] - [[package]] name = "atomic-polyfill" version = "1.0.3" @@ -98,15 +89,24 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + +[[package]] +name = "built" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d17f4d6e4dc36d1a02fbedc2753a096848e7c1b0772f7654eab8e2c927dd53" +dependencies = [ + "git2", +] [[package]] name = "bytemuck" -version = "1.14.0" +version = "1.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" +checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f" [[package]] name = "byteorder" @@ -120,12 +120,27 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "cc" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9fa1897e4325be0d68d48df6aa1a71ac2ed4d27723887e7754192705350730" +dependencies = [ + "libc", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cobs" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" + [[package]] name = "cortex-m" version = "0.6.7" @@ -181,7 +196,7 @@ dependencies = [ "bare-metal 1.0.0", "cortex-m 0.7.7", "cortex-m-rtic-macros", - "heapless", + "heapless 0.7.17", "rtic-core", "rtic-monotonic", "version_check", @@ -237,16 +252,16 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "447416d161ba378782c13e82b11b267d6d2104b4913679a7c5640e7e94f96ea7" dependencies = [ - "heapless", + "heapless 0.7.17", "nb 1.1.0", "no-std-net", ] [[package]] name = "embedded-storage" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "156d7a2fdd98ebbf9ae579cbceca3058cff946e13f8e17b90e3511db0508c723" +checksum = "a21dea9854beb860f3062d10228ce9b976da520a73474aed3171ec276bc0c032" [[package]] name = "embedded-time" @@ -259,22 +274,31 @@ dependencies = [ [[package]] name = "enum-iterator" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7add3873b5dd076766ee79c8e406ad1a472c385476b9e38849f8eec24f1be689" +checksum = "9fd242f399be1da0a5354aa462d57b4ab2b4ee0683cc552f7c007d2d12d36e94" dependencies = [ "enum-iterator-derive", ] [[package]] name = "enum-iterator-derive" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eecf8589574ce9b895052fa12d69af7a233f99e6107f5cb8dd1044f2a17bfdcb" +checksum = "03cdc46ec28bd728e67540c528013c6a10eb69a02eb31078a1bda695438cbfb8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.50", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", ] [[package]] @@ -320,6 +344,19 @@ dependencies = [ "version_check", ] +[[package]] +name = "git2" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b3ba52851e73b46a4c3df1d89343741112003f0f6f13beb0dfac9e457c3fdcd" +dependencies = [ + "bitflags 2.4.2", + "libc", + "libgit2-sys", + "log", + "url", +] + [[package]] name = "hash32" version = "0.2.1" @@ -329,6 +366,15 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -337,25 +383,45 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "heapless" -version = "0.7.16" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db04bc24a18b9ea980628ecf00e6c0264f3c1426dac36c00cb49b6fbad8b0743" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" dependencies = [ - "atomic-polyfill 0.1.11", - "hash32", + "atomic-polyfill", + "hash32 0.2.1", "rustc_version 0.4.0", "serde", "spin", "stable_deref_trait", ] +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32 0.3.1", + "stable_deref_trait", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idsp" -version = "0.13.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "454770ee82223cd2580e0cef04b4e5dbe5b60f11600e3cb7e910aaf0d62c99ce" +checksum = "99b7ec7ce4a9fa0c4801301dc2dfc836bc98ea06e8e7f04372c86cfcd171a80e" dependencies = [ - "num-complex 0.4.4", + "num-complex 0.4.5", "num-traits", "serde", ] @@ -372,9 +438,27 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "libc" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "libgit2-sys" +version = "0.16.2+1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] [[package]] name = "libm" @@ -382,6 +466,18 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +[[package]] +name = "libz-sys" +version = "1.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037731f5d3aaa87a5675e895b63ddff1a87624bc29f77004ea829809654e48f6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "lm75" version = "0.2.0" @@ -425,6 +521,12 @@ dependencies = [ "paste", ] +[[package]] +name = "menu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5208bd660042c7760f40d960ba0b1a9dc7a9c90775bea4c4637c3b666d2b53d" + [[package]] name = "miniconf" version = "0.9.0" @@ -432,7 +534,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9df2d7bdba3acb28460c347b21e1e88d869f2716ebe060eb6a79f7b76b57de72" dependencies = [ "embedded-io", - "heapless", + "heapless 0.7.17", "itoa", "log", "miniconf_derive", @@ -450,7 +552,7 @@ checksum = "89f46d25f40e41f552d76b8eb9e225fe493ebf978a5c3f42b7599e45cfe6b4e3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.50", ] [[package]] @@ -462,8 +564,8 @@ dependencies = [ "bit_field", "embedded-nal", "embedded-time", - "heapless", - "num_enum 0.7.1", + "heapless 0.7.17", + "num_enum 0.7.2", "serde", "smlang", "varint-rs", @@ -535,9 +637,9 @@ dependencies = [ [[package]] name = "num-complex" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +checksum = "23c6602fda94a57c990fe0df199a035d83576b496aa29f4e634a8ac6004e68a6" dependencies = [ "num-traits", "serde", @@ -545,19 +647,18 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", "num-traits", ] [[package]] name = "num-iter" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" dependencies = [ "autocfg", "num-integer", @@ -577,9 +678,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", "libm", @@ -596,11 +697,20 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683751d591e6d81200c39fb0d1032608b77724f34114db54f571ff1317b337c0" +checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" dependencies = [ - "num_enum_derive 0.7.1", + "num_enum_derive 0.6.1", +] + +[[package]] +name = "num_enum" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" +dependencies = [ + "num_enum_derive 0.7.2", ] [[package]] @@ -616,13 +726,33 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c11e44798ad209ccdd91fc192f0526a369a01234f7373e1b141c96d7cee4f0e" +checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.50", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.50", +] + +[[package]] +name = "panic-persist" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32bb382689ecd2c4d2d4df9fd56700ba8d43b7b31cca11018cc0e6f8aef39fd5" +dependencies = [ + "cortex-m 0.7.7", ] [[package]] @@ -631,6 +761,35 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "portable-atomic" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + +[[package]] +name = "postcard" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55c51ee6c0db07e68448e336cf8ea4131a620edefebf9893e759b2d793420f8" +dependencies = [ + "cobs", + "heapless 0.7.17", + "serde", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -657,18 +816,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -747,14 +906,14 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.20", + "semver 1.0.22", ] [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "scopeguard" @@ -773,9 +932,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" [[package]] name = "semver-parser" @@ -783,11 +942,20 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +[[package]] +name = "sequential-storage" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc9294c2c7c725835fa69d5a144a29078159360bdc53593e5e977abf8d200f46" +dependencies = [ + "embedded-storage", +] + [[package]] name = "serde" -version = "1.0.190" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] @@ -798,32 +966,40 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c9e1ab533c0bc414c34920ec7e5f097101d126ed5eac1a1aac711222e0bbb33" dependencies = [ - "heapless", + "heapless 0.7.17", "ryu", "serde", ] [[package]] name = "serde_derive" -version = "1.0.190" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.50", +] + +[[package]] +name = "serial-settings" +version = "0.1.0" +dependencies = [ + "embedded-io", + "heapless 0.8.0", + "menu", + "miniconf", ] [[package]] name = "shared-bus" -version = "0.2.5" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f8438a40b91c8b9531c664e9680c55b92bd78cd6809a8b45b4512b1e5765f2" +checksum = "78b60428415b23ed3f0e3abc776e10e548cf2cbb4288e73d5d181a02b5a90b95" dependencies = [ - "atomic-polyfill 0.1.11", "cortex-m 0.6.7", "embedded-hal", - "nb 0.1.3", ] [[package]] @@ -832,7 +1008,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6b8d3f0e34309c22ca4a9a27d24fa493e31573485f3493802b75b9d706756a6" dependencies = [ - "atomic-polyfill 1.0.3", + "atomic-polyfill", "cortex-m 0.7.7", "embedded-hal", "nb 1.1.0", @@ -867,7 +1043,7 @@ dependencies = [ "bitflags 1.3.2", "byteorder", "cfg-if", - "heapless", + "heapless 0.7.17", "managed", ] @@ -879,9 +1055,9 @@ checksum = "6dd2ed2f8e7643a170506863ed0f52ad1dc5762abdcff27de825dde14fc8025f" dependencies = [ "embedded-nal", "embedded-time", - "heapless", + "heapless 0.7.17", "nanorand", - "shared-bus 0.2.5", + "shared-bus 0.2.2", "smoltcp", ] @@ -900,13 +1076,17 @@ version = "0.9.0" dependencies = [ "ad9959", "bbqueue", + "bit_field", + "built", "cortex-m 0.7.7", "cortex-m-rt", "cortex-m-rtic", "embedded-hal", + "embedded-io", + "embedded-storage", "enum-iterator", "fugit", - "heapless", + "heapless 0.7.17", "idsp", "lm75", "log", @@ -915,19 +1095,24 @@ dependencies = [ "minimq", "mono-clock", "mutex-trait", - "num_enum 0.7.1", + "num_enum 0.7.2", + "panic-persist", "paste", + "postcard", "rand_core", "rand_xorshift", "rtt-logger", "rtt-target", + "sequential-storage", "serde", "serde-json-core", + "serial-settings", "shared-bus 0.3.1", "smoltcp-nal", "spin", "stm32h7xx-hal", "systick-monotonic", + "tca9539", "usb-device", "usbd-serial", ] @@ -953,8 +1138,7 @@ dependencies = [ [[package]] name = "stm32h7xx-hal" version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08bcfbdbe4458133f2fd55994a5c4f1b4bf28084f0218e93cdbc19d7c70219f" +source = "git+https://github.com/stm32-rs/stm32h7xx-hal.git?branch=master#03b8dcf0fd5dcce324e557830838eff729cfeecb" dependencies = [ "bare-metal 1.0.0", "cast", @@ -984,9 +1168,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.38" +version = "2.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb" dependencies = [ "proc-macro2", "quote", @@ -995,9 +1179,9 @@ dependencies = [ [[package]] name = "synopsys-usb-otg" -version = "0.3.2" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678f3707a7b1fd4863023292c42f73c6bab0e9b0096f41ae612d1af0ff221b45" +checksum = "e948d523b316939545d8b21a48c27aef150ce25321b9f95ff7978647a806a6fe" dependencies = [ "cortex-m 0.7.7", "embedded-hal", @@ -1016,6 +1200,32 @@ dependencies = [ "rtic-monotonic", ] +[[package]] +name = "tca9539" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05458b00a3a73c5b64c0de8f2a5182f6d51eb1aeb54c638e585092d26fc9a971" +dependencies = [ + "bit_field", + "embedded-hal", + "num_enum 0.5.11", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "typenum" version = "1.17.0" @@ -1028,26 +1238,57 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e87a2ed6b42ec5e28cc3b94c09982969e9227600b2e3dcbc1db927a84c06bd69" +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "usb-device" -version = "0.2.9" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f6cc3adc849b5292b4075fc0d5fdcf2f24866e88e336dd27a8943090a520508" +checksum = "e73e438f527e567fb3982f2370967821fab4f5aea84c42e218a211dd2002b6a2" +dependencies = [ + "heapless 0.7.17", + "num_enum 0.6.1", + "portable-atomic", +] [[package]] name = "usbd-serial" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db75519b86287f12dcf0d171c7cf4ecc839149fe9f3b720ac4cfce52959e1dfe" +version = "0.2.0" +source = "git+https://github.com/rust-embedded-community/usbd-serial?rev=096742c1c480f6f63c1a936a3c23ede7993c624d#096742c1c480f6f63c1a936a3c23ede7993c624d" dependencies = [ "embedded-hal", - "nb 0.1.3", + "embedded-io", + "nb 1.1.0", "usb-device", ] @@ -1063,6 +1304,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index e1f95e4da..f7e29d4f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,8 @@ readme = "README.md" documentation = "https://docs.rs/stabilizer/" edition = "2021" # keep MSRV in sync in ci.yaml and Cargo.toml -rust-version = "1.65" +rust-version = "1.66.1" +build = "build.rs" exclude = [ ".gitignore", "doc/", @@ -31,9 +32,13 @@ features = [] default-target = "thumbv7em-none-eabihf" [workspace] -members = ["ad9959"] +members = ["ad9959", "serial-settings"] [dependencies] +panic-persist = { version = "0.3", features = ["utf8", "custom-panic-handler"] } +sequential-storage = "0.6" +embedded-io = "0.6" +embedded-storage = "0.3" cortex-m = { version = "0.7.7", features = ["inline-asm", "critical-section-single-core"] } cortex-m-rt = { version = "0.7", features = ["device"] } log = { version = "0.4", features = ["max_level_trace", "release_max_level_info"] } @@ -43,10 +48,11 @@ serde-json-core = "0.5" heapless = { version = "0.7.16", features = ["serde"] } cortex-m-rtic = "1.0" embedded-hal = "0.2.7" -num_enum = { version = "0.7.1", default-features = false } +num_enum = { version = "0.7.2", default-features = false } paste = "1" -idsp = "0.13" +idsp = "0.15.0" ad9959 = { path = "ad9959", version = "0.2.1" } +serial-settings = {path = "serial-settings"} mcp230xx = "1.0" mutex-trait = "0.2" fugit = "0.3" @@ -60,21 +66,32 @@ enum-iterator = "1.4.1" rand_xorshift = "0.3.0" rand_core = "0.6.4" minimq = "0.8.0" -# patch with https://github.com/rust-embedded-community/usb-device/pull/129 -usb-device = "0.2.9" -usbd-serial = "0.1.1" +usb-device = "0.3.0" +usbd-serial = "0.2" # Keep this synced with the miniconf version in py/setup.py miniconf = "0.9.0" +tca9539 = "0.2" smoltcp-nal = { version = "0.4.1", features = ["shared-stack"]} bbqueue = "0.5" +postcard = "1" +bit_field = "0.10.2" + +[build-dependencies] +built = { version = "0.7", features = ["git2"], default-features = false } [dependencies.stm32h7xx-hal] version = "0.15.1" +git = "https://github.com/stm32-rs/stm32h7xx-hal.git" +branch = "master" features = ["stm32h743v", "rt", "ethernet", "xspi", "usb_hs"] +[patch.crates-io.usbd-serial] +git = "https://github.com/rust-embedded-community/usbd-serial" +rev = "096742c1c480f6f63c1a936a3c23ede7993c624d" + [features] -nightly = [ ] -pounder_v1_0 = [ ] +nightly = [] +pounder_v1_0 = [] [profile.dev] codegen-units = 1 diff --git a/README.md b/README.md index 75249db43..ec5d1c682 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,19 @@ ## Applications Check out the [Documentation](https://quartiq.de/stabilizer) for more information on usage, -configuration, and development. +configuration, and development. + +For OITG-specific information, see below ## Hardware [![Stabilizer](https://github.com/sinara-hw/Stabilizer/wiki/Stabilizer_v1.0_top_small.jpg)](https://github.com/sinara-hw/Stabilizer) [![Pounder](https://user-images.githubusercontent.com/1338946/125936814-3664aa2d-a530-4c85-9393-999a7173424e.png)](https://github.com/sinara-hw/Pounder/wiki) + +## Notable changes over upstream +* Added `fnc.rs` binary for fibre noise cancellation based on `dual-iir.rs`, along with a corresponding script to push messages specific to an fnc application. Relies on Pounder +* Changed default MQTT broker to `10.255.6.4`. It does not need to be flashed separately unless using a different broker. + +### Active branches +Many stabilizer applications in the group rely on branches not up to date with `main`. It might prove useful to keep a track of them here for maintenance. diff --git a/ad9959/Cargo.toml b/ad9959/Cargo.toml index f843430f3..596ec9cff 100644 --- a/ad9959/Cargo.toml +++ b/ad9959/Cargo.toml @@ -13,4 +13,4 @@ documentation = "https://docs.rs/ad9959/" embedded-hal = {version = "0.2.7", features = ["unproven"]} bit_field = "0.10.2" bytemuck = "1.14.0" -bitflags = "2.4.1" +bitflags = "2.4.2" diff --git a/ad9959/src/lib.rs b/ad9959/src/lib.rs index b15f2a5bd..025f7d4f6 100644 --- a/ad9959/src/lib.rs +++ b/ad9959/src/lib.rs @@ -126,7 +126,9 @@ impl Ad9959 { io_update.set_low().or(Err(Error::Pin))?; - // Reset the AD9959 + // Reset the AD9959 (Pounder v1.1 and earlier) + // On Pounder v1.2 and later the reset has been done through the GPIO extender in + // PounderDevices before. reset_pin.set_high().or(Err(Error::Pin))?; // Delay for at least 1 SYNC_CLK period for the reset to occur. The SYNC_CLK is guaranteed diff --git a/book/src/setup.md b/book/src/setup.md index e00d26bac..0ea4c2092 100644 --- a/book/src/setup.md +++ b/book/src/setup.md @@ -40,10 +40,8 @@ Stabilizer requires an MQTT broker that supports MQTTv5. The MQTT broker is used to distribute and exchange elemetry data and to view/change application settings. The broker must be reachable by both the host-side applications used to interact with the application on Stabilizer and by the application running on Stabilizer. -Determine the IPv4 address of the broker as seen from the network Stabilizer is -connected to. The broker IP address must be stable. It will be used later -during firmware build. -The broker must be reachable on port 1883 on that IP address. +The broker must be reachable on port 1883 on that IP address - it may either be an IP address or a +fully qualified domain name. Firewalls between Stabilizer and the broker may need to be configured to allow connections from Stabilizer to that port and IP address. @@ -83,34 +81,31 @@ docker run -p 1883:1883 --name mosquitto -v ${pwd}/mosquitto.conf:/mosquitto/con git clone https://github.com/quartiq/stabilizer cd stabilizer ``` -5. Build firmware specifying the MQTT broker IP. Replace `10.34.16.1` by the - stable and reachable broker IPv4 address determined above. +5. Build firmware ```bash # Bash - BROKER="10.34.16.1" cargo build --release + cargo build --release # Powershell - # Note: This sets the broker for all future builds as well. - $env:BROKER='10.34.16.1'; cargo build --release + cargo build --release ``` 6. Extract the application binary (substitute `dual-iir` below with the desired application name) ```bash # Bash - BROKER="10.34.16.1" cargo objcopy --release --bin dual-iir -- -O binary dual-iir.bin + cargo objcopy --release --bin dual-iir -- -O binary dual-iir.bin # Powershell - $env:BROKER='10.34.16.1'; cargo objcopy --release --bin dual-iir -- -O binary dual-iir.bin + cargo objcopy --release --bin dual-iir -- -O binary dual-iir.bin ``` ## Flashing Firmware can be loaded onto stabilizer using **one** of the three following methods. -> **Note:** All methods require access to the circuit board. Pulling the device from a -> crate always requires power removal as there are sensitive leads and components on -> both sides of the board that may come into contact with adjacent front panels. -> Every access to the board also requires proper ESD precautions. Never -> hot-plug the device or the probe. +> **Note:** Most methods below require access to the circuit board. Pulling the device from a crate +> always requires power removal as there are sensitive leads and components on both sides of the +> board that may come into contact with adjacent front panels. Every access to the board also +> requires proper ESD precautions. Never hot-plug the device or the probe. ### ST-Link virtual mass storage @@ -128,15 +123,24 @@ and applying power again. ### DFU Upload -If an SWD/JTAG probe is not available, -you can flash firmware using only a micro USB cable -plugged in to the front of Stabilizer, a DFU utility, and a jumper to activate -the bootloader. +If an SWD/JTAG probe is not available, you can flash firmware using only a micro USB cable plugged +in to the front of Stabilizer, and a DFU utility. + +> **Note:** If there is already newer firmware running on Stabilizer that supports the USB serial +> interface, there is no need to remove Stabilizer from the crate or disconnect any existing +> connectors/power supplies or to jumper the BOOT0 pin. Instead, open the serial port on Stabilizer +> and request it to enter DFU mode: +> ```bash +> python -m serial +> > platform dfu +> ``` +> +> After the device is in DFU mode, use the `dfu-util` command specified in the instructions below, +> and the DFU firmware update will be complete. 1. Install the DFU USB tool [`dfu-util`](http://dfu-util.sourceforge.net) 1. Remove power -1. Then carefully remove the module from the crate to gain - access to the board +1. Then carefully remove the module from the crate to gain acccess to the board 1. Short JC2/BOOT with the jumper 1. Connect your computer to the Micro USB connector below/left of the RJ45 connector on the front panel @@ -144,7 +148,7 @@ the bootloader. 1. Then power it 1. Perform the Device Firmware Upgrade (DFU) ```bash - dfu-util -a 0 -s 0x08000000:leave -D dual-iir.bin + dfu-util -a 0 -s 0x08000000:leave -R -D dual-iir.bin ``` 1. To keep the device from entering the bootloader remove power, pull the board from the crate, remove the JC2/BOOT jumper, insert the module @@ -163,16 +167,29 @@ described [above](#st-link-virtual-mass-storage). 2. Build and run firmware on the device ```bash # Bash - BROKER="10.34.16.1" cargo run --release --bin dual-iir + cargo run --release --bin dual-iir # Powershell - $Env:BROKER='10.34.16.1'; cargo run --release --bin dual-iir + cargo run --release --bin dual-iir ``` When using debug (non `--release`) mode, decrease the sampling frequency significantly. The added error checking code and missing optimizations may lead to the application missing timer deadlines and panicing. +## Set the MQTT broker + +The MQTT broker can be configured via the USB port on Stabilizer's front. Connect a USB cable and +open up the serial port in a serial terminal of your choice. `pyserial` provides a simple, +easy-to-use terminal emulator: +```sh +python -m serial +``` + +Once you have opened the port, you can use the provided menu to update the MQTT broker address. The +address can be an IP address or a domain name. Once the broker has been updated, power cycle +stabilizer to have the new broker address take effect. + ## Verify MQTT connection Once your MQTT broker and Stabilizer are both running, verify that the application @@ -190,7 +207,8 @@ Broker. ![MQTT Explorer Configuration](assets/mqtt-explorer.png) -> **Note:** In MQTT explorer, use the same broker address that you used when building the firmware. +> **Note:** In MQTT explorer, use the same broker address that you set in the Stabilizer serial +> terminal. In addition to the `alive` status, telemetry messages are published at regular intervals when Stabilizer has connected to the broker. Once you observe incoming telemetry, diff --git a/build.rs b/build.rs index e71f6a660..f57ef7a69 100644 --- a/build.rs +++ b/build.rs @@ -1,3 +1,5 @@ fn main() { + built::write_built_file() + .expect("Failed to acquire build-time information"); println!("cargo:rerun-if-changed=memory.x"); } diff --git a/hitl/loopback.py b/hitl/loopback.py index 3ebaa6600..e0a9b941b 100644 --- a/hitl/loopback.py +++ b/hitl/loopback.py @@ -27,10 +27,10 @@ def static_iir_output(output_voltage): """ machine_units = voltage_to_machine_units(output_voltage) return { - 'y_min': machine_units, - 'y_max': machine_units, - 'y_offset': 0, 'ba': [1, 0, 0, 0, 0], + 'u': 0, + 'min': machine_units, + 'max': machine_units, } diff --git a/hitl/run.sh b/hitl/run.sh index 1f17896c0..214b464d7 100755 --- a/hitl/run.sh +++ b/hitl/run.sh @@ -20,9 +20,10 @@ python3 -m venv --system-site-packages vpy # Install Miniconf utilities for configuring stabilizer. python3 -m pip install -e py -cargo flash --chip STM32H743ZITx --elf target/thumbv7em-none-eabihf/release/dual-iir --probe 0483:3754:004C003D3137510D33333639 +probe-rs download --chip STM32H743ZITx --log-file /dev/null --probe 0483:3754:004C003D3137510D33333639 target/thumbv7em-none-eabihf/release/dual-iir +probe-rs reset --chip STM32H743ZITx --log-file /dev/null --probe 0483:3754:004C003D3137510D33333639 --connect-under-reset -# Sleep to allow flashing, booting, DHCP, MQTT +# Sleep to allow booting, DHCP, ARP, MQTT etc sleep 30 # Test pinging Stabilizer. This exercises that: diff --git a/memory.x b/memory.x index 02afe9eba..dfa174802 100644 --- a/memory.x +++ b/memory.x @@ -1,7 +1,7 @@ MEMORY { ITCM (rwx) : ORIGIN = 0x00000000, LENGTH = 64K - RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K + RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 127K AXISRAM (rwx) : ORIGIN = 0x24000000, LENGTH = 512K SRAM1 (rwx) : ORIGIN = 0x30000000, LENGTH = 128K SRAM2 (rwx) : ORIGIN = 0x30020000, LENGTH = 128K @@ -10,8 +10,17 @@ MEMORY RAM_B (rwx) : ORIGIN = 0x38800000, LENGTH = 4K FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K FLASH1 (rx) : ORIGIN = 0x08100000, LENGTH = 1024K + PERSISTENT_RAM: ORIGIN = 0x2001FC00, LENGTH = 1K } +/* + * Persistent memory has a u32 bootflag at the beginning and then the remainder is used for + * persisting panic information between boots. + */ +_bootflag = ORIGIN(PERSISTENT_RAM); +_panic_dump_start = ORIGIN(PERSISTENT_RAM) + 4; +_panic_dump_end = ORIGIN(PERSISTENT_RAM) + LENGTH(PERSISTENT_RAM) - 4; + SECTIONS { .axisram (NOLOAD) : ALIGN(8) { *(.axisram .axisram.*); diff --git a/py/stabilizer/fnc_pid_controller.py b/py/stabilizer/fnc_pid_controller.py new file mode 100644 index 000000000..4688e8aa4 --- /dev/null +++ b/py/stabilizer/fnc_pid_controller.py @@ -0,0 +1,213 @@ +#!/usr/bin/python3 +""" +Authors: + Étienne Wodey, Leibniz University Hannover, Institute of Quantum Optics + Ryan Summers, Vertigo Designs + Robert Jördens, QUARTIQ + +Description: Algorithms to generate biquad (second order IIR) coefficients. +""" +import argparse +import asyncio +import logging + +from math import pi, inf + +import miniconf +import json + +import stabilizer + +logger = logging.getLogger(__name__) + +# Disable pylint warnings about a0, b1 etc +#pylint: disable=invalid-name + +default_args = { + "Kp": 0, + "Ki": 0, + "Kd": 0, + "aom_frequency": 80e6, + "attn_out": 0.5, + "attn_in": 0.5, + "sample_period": stabilizer.SAMPLE_PERIOD, + "x_offset": 0, + "y_min": stabilizer.DAC_FULL_SCALE, + "y_max": stabilizer.DAC_FULL_SCALE, + "y_offset": 0, + "Ki_limit": inf, + "Kd_limit": inf, + "Kii": 0, + "Kii_limit": inf, + "Kdd": 0, + "Kdd_limit": inf, +} + + +def pid_coefficients(args): + """ + Calculate PID IIR filter coefficients. + + Note: + Calculations coefficient largely taken using the derivations in + page 9 of https://arxiv.org/pdf/1508.06319.pdf + + PII/PID coefficient equations are taken from the PID-IIR primer + written by Robert Jördens at https://hackmd.io/IACbwcOTSt6Adj3_F9bKuw + """ + + # Determine filter order + if args.Kii != 0: + assert (args.Kdd, args.Kd, args.Kdd_limit, args.Kd_limit) == \ + (0, 0, float('inf'), float('inf')), \ + "IIR filters I^2 and D or D^2 gain/limit are unsupported" + order = 2 + elif args.Ki != 0: + assert (args.Kdd, args.Kdd_limit) == (0, float('inf')), \ + "IIR filters with I and D^2 gain/limit are unsupported" + order = 1 + else: + order = 0 + + kernels = [ + [1, 0, 0], + [1, -1, 0], + [1, -2, 1] + ] + + gains = [args.Kii, args.Ki, args.Kp, args.Kd, args.Kdd] + limits = [args.Kii/args.Kii_limit, args.Ki/args.Ki_limit, + 1, args.Kd/args.Kd_limit, args.Kdd/args.Kdd_limit] + w = 2*pi*args.sample_period + b = [sum(gains[2 - order + i] * w**(order - i) * kernels[i][j] + for i in range(3)) for j in range(3)] + + a = [sum(limits[2 - order + i] * w**(order - i) * kernels[i][j] + for i in range(3)) for j in range(3)] + b = [i/a[0] for i in b] + a = [i/a[0] for i in a] + assert a[0] == 1 + + return b + [-ai for ai in a[1:]] + + +def _main(): + parser = argparse.ArgumentParser( + description="Configure Stabilizer dual-iir filter parameters." + "Note: This script assumes an AFE input gain of 1.") + parser.add_argument('-v', '--verbose', action='count', default=0, + help='Increase logging verbosity') + parser.add_argument("--broker", "-b", type=str, default="10.255.6.4", + help="The MQTT broker to use to communicate with " + "Stabilizer. Default: (%(default)s)") + parser.add_argument("--address", "-a", type=str, default="+", help="MAC address of the Stabilizer board") + parser.add_argument("--channel", "-c", type=int, choices=[0, 1], + required=True, help="The Stabilizer channel to configure.") + + parser.add_argument("--Kp", type=float, default=0, + help="Proportional (P) gain") + parser.add_argument("--Ki", type=float, default=0, + help="Integrator (I) gain") + parser.add_argument("--Kd", type=float, default=0, + help="Derivative (D) gain") + parser.add_argument("--aom-frequency", "-f", type=float, default=80e6, + help="Aom centre frequency (Hz) ") + parser.add_argument("--attn-out", type=float, default=0.5, + help="Output attenuation (dB) ") + parser.add_argument("--attn-in", type=float, default=16.5, + help="Input attenuation (dB) ") + + parser.add_argument("--sample-period", type=float, default= 2**9 * 10e-9, + help="Sample period in seconds. ") + + parser.add_argument("--prefix", "-p", type=str, + default="dt/sinara/fnc", + help="The Stabilizer device prefix path in MQTT, " + "wildcards allowed as long as the match is unique " + "Default: (%(default)s)") + parser.add_argument("--no-discover", "-d", action="store_true", + help="Do not discover Stabilizer device prefix.") + + parser.add_argument("--x-offset", type=float, default=0, + help="The channel input offset (V)") + parser.add_argument("--y-min", type=float, default=-stabilizer.DAC_FULL_SCALE, + help="The channel minimum output (V)") + parser.add_argument("--y-max", type=float, default=stabilizer.DAC_FULL_SCALE, + help="The channel maximum output (V)") + parser.add_argument("--y-offset", type=float, default=0, + help="The channel output offset (V)") + + parser.add_argument("--Kii", type=float, default=0, + help="Double Integrator (I^2) gain") + parser.add_argument("--Kii_limit", type=float, default=inf, + help="Integral gain limit") + parser.add_argument("--Ki_limit", type=float, default=inf, + help="Integral gain limit") + parser.add_argument("--Kd_limit", type=float, default=inf, + help="Derivative gain limit") + parser.add_argument("--Kdd", type=float, default=0, + help="Double Derivative (D^2) gain") + parser.add_argument("--Kdd_limit", type=float, default=inf, + help="Derivative gain limit") + + args = parser.parse_args() + + device_path = "{}/{}".format(args.prefix, args.address) + + print(device_path) + + logging.basicConfig( + format='%(asctime)s [%(levelname)s] %(name)s: %(message)s', + level=logging.WARN - 10*args.verbose) + + # Calculate the IIR coefficients for the filter. + coefficients = pid_coefficients(args) + + # The feed-forward gain of the IIR filter is the summation + # of the "b" components of the filter. + forward_gain = sum(coefficients[:3]) + if forward_gain == 0 and args.x_offset != 0: + logger.warning("Filter has no DC gain but x_offset is non-zero") + + if not (0.5 <= args.attn_out <= 31.5): + logger.warning("Output attenuation out of range, setting to default 0.5 dB") + args.attn_out = 0.5 + if not (0.5 <= args.attn_in <= 31.5): + logger.warning("Input attenuation out of range, setting to default 0.5 dB") + args.attn_in = 0.5 + + if args.no_discover: + prefix = device_path + else: + devices = asyncio.run(miniconf.discover(args.broker, device_path)) + if not devices: + raise ValueError("No prefixes discovered.") + if len(devices) > 1: + raise ValueError(f"Multiple prefixes discovered ({devices})." + "Please specify a more specific --prefix") + prefix = devices.pop() + logger.info("Automatically using detected device prefix: %s", prefix) + + async def configure(): + logger.info("Connecting to broker") + interface = await miniconf.Miniconf.create(prefix, args.broker) + + # Set the filter coefficients. + # Note: In the future, we will need to Handle higher-order cascades. + await interface.set(f"/iir_ch/{args.channel}/0", { + "ba": coefficients, + "u": stabilizer.voltage_to_machine_units( + args.y_offset + forward_gain * args.x_offset), + "min": stabilizer.voltage_to_machine_units(args.y_min), + "max": stabilizer.voltage_to_machine_units(args.y_max), + }, retain=True) + await interface.set(f"/aom_centre_f", args.aom_frequency, retain=True) + + await interface.set(f"/output_attenuation", args.attn_out, retain=True) + await interface.set(f"/input_attenuation", args.attn_in, retain=True) + + asyncio.run(configure()) + + +if __name__ == "__main__": + _main() diff --git a/py/stabilizer/iir_coefficients.py b/py/stabilizer/iir_coefficients.py index 7f1c8bc8a..ebbbec8a1 100644 --- a/py/stabilizer/iir_coefficients.py +++ b/py/stabilizer/iir_coefficients.py @@ -225,14 +225,14 @@ def _main(): "Note: This script assumes an AFE input gain of 1.") parser.add_argument('-v', '--verbose', action='count', default=0, help='Increase logging verbosity') - parser.add_argument("--broker", "-b", type=str, default="mqtt", + parser.add_argument("--broker", "-b", type=str, default="10.255.6.4", help="The MQTT broker to use to communicate with " - "Stabilizer (%(default)s)") + "Stabilizer. Default: (%(default)s)") parser.add_argument("--prefix", "-p", type=str, default="dt/sinara/dual-iir/+", help="The Stabilizer device prefix in MQTT, " "wildcards allowed as long as the match is unique " - "(%(default)s)") + "Default: (%(default)s)") parser.add_argument("--no-discover", "-d", action="store_true", help="Do not discover Stabilizer device prefix.") @@ -240,7 +240,8 @@ def _main(): required=True, help="The filter channel to configure.") parser.add_argument("--sample-period", type=float, default=stabilizer.SAMPLE_PERIOD, - help="Sample period in seconds (%(default)s s)") + help="Sample period in seconds. " + "Default: (%(default)s s)") parser.add_argument("--x-offset", type=float, default=0, help="The channel input offset (%(default)s V)") @@ -252,6 +253,12 @@ def _main(): help="The channel maximum output (%(default)s V)") parser.add_argument("--y-offset", type=float, default=0, help="The channel output offset (%(default)s V)") + parser.add_argument("--aom-frequency", "-f", type=float, default=80e3, + help="Aom centre frequency (%(default)s Hz) ") + parser.add_argument("--attn-out", type=float, default=0.5, + help="Output attenuation (%(default)s dB) ") + parser.add_argument("--attn-in", type=float, default=0.5, + help="Input attenuation (%(default)s dB) ") # Next, add subparsers and their arguments. subparsers = parser.add_subparsers( @@ -280,6 +287,13 @@ def _main(): if forward_gain == 0 and args.x_offset != 0: logger.warning("Filter has no DC gain but x_offset is non-zero") + if not (0.5 <= args.attn_out <= 31.5): + logger.warning("Output attenuation out of range, setting to default 0.5 dB") + args.attn_out = 0.5 + if not (0.5 <= args.attn_in <= 31.5): + logger.warning("Input attenuation out of range, setting to default 0.5 dB") + args.attn_in = 0.5 + if args.no_discover: prefix = args.prefix else: @@ -300,11 +314,15 @@ async def configure(): # Note: In the future, we will need to Handle higher-order cascades. await interface.set(f"/iir_ch/{args.channel}/0", { "ba": coefficients, - "y_min": stabilizer.voltage_to_machine_units(args.y_min), - "y_max": stabilizer.voltage_to_machine_units(args.y_max), - "y_offset": stabilizer.voltage_to_machine_units( - args.y_offset + forward_gain * args.x_offset) - }) + "u": stabilizer.voltage_to_machine_units( + args.y_offset + forward_gain * args.x_offset), + "min": stabilizer.voltage_to_machine_units(args.y_min), + "max": stabilizer.voltage_to_machine_units(args.y_max), + }, retain=True) + await interface.set(f"/aom_centre_f", args.aom_frequency, retain=True) + + await interface.set(f"/output_attenuation", args.attn_out, retain=True) + await interface.set(f"/input_attenuation", args.attn_in, retain=True) asyncio.run(configure()) diff --git a/serial-settings/Cargo.toml b/serial-settings/Cargo.toml new file mode 100644 index 000000000..a88ac82a3 --- /dev/null +++ b/serial-settings/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "serial-settings" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +miniconf = "0.9" +menu = {version = "0.4", features = ["echo"]} +heapless = "0.8" +embedded-io = "0.6" diff --git a/serial-settings/src/interface.rs b/serial-settings/src/interface.rs new file mode 100644 index 000000000..167833203 --- /dev/null +++ b/serial-settings/src/interface.rs @@ -0,0 +1,66 @@ +/// Wrapper type for a "best effort" serial interface. +/// +/// # Note +/// Overflows of the output are silently ignored. +pub struct BestEffortInterface(T); + +impl BestEffortInterface +where + T: embedded_io::Write + + embedded_io::WriteReady + + embedded_io::Read + + embedded_io::ReadReady, +{ + /// Construct an interface where overflows and errors when writing on the output are silently + /// ignored. + pub fn new(interface: T) -> Self { + Self(interface) + } + + /// Get access to the inner (wrapped) interface + pub fn inner(&self) -> &T { + &self.0 + } + + /// Get mutable access to the inner (wrapped) interface + pub fn inner_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +impl core::fmt::Write for BestEffortInterface +where + T: embedded_io::Write + embedded_io::WriteReady, +{ + fn write_str(&mut self, s: &str) -> core::fmt::Result { + if let Ok(true) = self.0.write_ready() { + self.0.write(s.as_bytes()).ok(); + } + Ok(()) + } +} + +impl embedded_io::ErrorType for BestEffortInterface +where + T: embedded_io::ErrorType, +{ + type Error = ::Error; +} + +impl embedded_io::Read for BestEffortInterface +where + T: embedded_io::Read, +{ + fn read(&mut self, buf: &mut [u8]) -> Result { + self.0.read(buf) + } +} + +impl embedded_io::ReadReady for BestEffortInterface +where + T: embedded_io::ReadReady, +{ + fn read_ready(&mut self) -> Result { + self.0.read_ready() + } +} diff --git a/serial-settings/src/lib.rs b/serial-settings/src/lib.rs new file mode 100644 index 000000000..cb188feaf --- /dev/null +++ b/serial-settings/src/lib.rs @@ -0,0 +1,357 @@ +//! Persistent Settings Management Serial Interface +//! +//! # Description +//! This crate provides a simple means to load, configure, and save device settings over a serial +//! (i.e. text-based) interface. It is ideal to be used with serial ports and terminal emulators, +//! and exposes a simple way to allow users to configure device operation. +//! +//! # Example +//! Let's assume that your settings structure looks as follows: +//! ```rust +//! #[derive(miniconf::Tree, ...)] +//! struct Settings { +//! broker: String, +//! id: String, +//! } +//! ``` +//! +//! A user would be displayed the following terminal interface: +//! ``` +//!> help +//! AVAILABLE ITEMS: +//! list +//! get +//! set +//! clear +//! platform +//! help [ ] +//! +//! > plaform dfu +//! Reset to DFU is not supported +//! +//! > plaform service +//! Service data not available +//! +//! > list +//! Available settings: +//! /broker: "test" [default: "mqtt"] +//! /id: "04-91-62-d2-a8-6f" [default: "04-91-62-d2-a8-6f"] +//! ``` +//! +//! # Design +//! Settings are specified in a [`Miniconf::Tree`] settings tree and are transferred over the +//! serial interface using JSON encoding. This means that things like strings must be encased in +//! qutoes. +//! +//! # Limitations +//! Currently, there is a hardcoded limit of 32-bytes on the settings path. This is arbitrary and +//! can be changed if needed. +#![no_std] + +use core::fmt::Write; +use embedded_io::{Read, ReadReady}; +use miniconf::{JsonCoreSlash, TreeKey}; + +mod interface; + +pub use interface::BestEffortInterface; + +/// Specifies the API required for objects that are used as settings with the serial terminal +/// interface. +pub trait Settings: for<'a> JsonCoreSlash<'a> + Clone { + /// Reset the settings to their default values. + fn reset(&mut self) {} +} + +pub trait Platform: Sized { + /// This type specifies the interface to the user, for example, a USB CDC-ACM serial port. + type Interface: embedded_io::Read + + embedded_io::ReadReady + + core::fmt::Write; + + /// Specifies the settings that are used on the device. + type Settings: Settings; + + /// `save()` Error type + type Error: core::fmt::Debug; + + /// Save the settings to storage + fn save(&mut self, buffer: &mut [u8]) -> Result<(), Self::Error>; + + /// Execute a platform specific command. + fn cmd(&mut self, cmd: &str); + + /// Return a mutable reference to the `Interface`. + fn interface_mut(&mut self) -> &mut Self::Interface; + + /// Return a reference to the `Settings` + fn settings(&self) -> &Self::Settings; + + /// Return a mutable reference to the `Settings`. + fn settings_mut(&mut self) -> &mut Self::Settings; +} + +struct Context<'a, P: Platform> { + platform: P, + buffer: &'a mut [u8], +} + +impl<'a, P: Platform> Context<'a, P> { + fn handle_platform( + _menu: &menu::Menu, + item: &menu::Item, + args: &[&str], + context: &mut Self, + ) { + let key = menu::argument_finder(item, args, "cmd").unwrap().unwrap(); + context.platform.cmd(key) + } + + fn handle_list( + _menu: &menu::Menu, + _item: &menu::Item, + _args: &[&str], + context: &mut Self, + ) { + let mut defaults = context.platform.settings().clone(); + defaults.reset(); + + for path in P::Settings::iter_paths::>("/") { + match path { + Err(e) => writeln!(context, "Failed to get path: {e}"), + Ok(path) => { + match context + .platform + .settings() + .get_json(&path, context.buffer) + { + Err(e) => { + writeln!(context, "Failed to read {path}: {e}") + .unwrap(); + continue; + } + Ok(len) => write!( + &mut context.platform.interface_mut(), + "{path}: {}", + core::str::from_utf8(&context.buffer[..len]) + .unwrap() + ) + .unwrap(), + } + + match defaults.get_json(&path, context.buffer) { + Err(e) => writeln!( + context, + "[default serialization error: {e}]" + ), + Ok(len) => writeln!( + &mut context.platform.interface_mut(), + " [default: {}]", + core::str::from_utf8(&context.buffer[..len]) + .unwrap() + ), + } + } + } + .unwrap() + } + } + + fn handle_clear( + _menu: &menu::Menu, + _item: &menu::Item, + _args: &[&str], + context: &mut Self, + ) { + context.platform.settings_mut().reset(); + match context.platform.save(context.buffer) { + Ok(_) => { + writeln!(context, "Settings cleared to defaults and saved.") + } + Err(e) => { + writeln!(context, "Failed to clear settings: {e:?}") + } + } + .unwrap(); + } + + fn handle_get( + _menu: &menu::Menu, + item: &menu::Item, + args: &[&str], + context: &mut Self, + ) { + let key = menu::argument_finder(item, args, "item").unwrap().unwrap(); + match context.platform.settings().get_json(key, context.buffer) { + Err(e) => { + writeln!(context, "Failed to read {key}: {e}") + } + Ok(len) => { + writeln!( + &mut context.platform.interface_mut(), + "{key}: {}", + core::str::from_utf8(&context.buffer[..len]).unwrap() + ) + } + } + .unwrap(); + } + + fn handle_set( + _menu: &menu::Menu, + item: &menu::Item, + args: &[&str], + context: &mut Self, + ) { + let key = menu::argument_finder(item, args, "item").unwrap().unwrap(); + let value = + menu::argument_finder(item, args, "value").unwrap().unwrap(); + + // Now, write the new value into memory. + // TODO: Validate it first? + match context + .platform + .settings_mut() + .set_json(key, value.as_bytes()) + { + Ok(_) => match context.platform.save(context.buffer) { + Ok(_) => { + writeln!( + context, + "Settings saved. Reboot device (`platform reboot`) to apply." + ) + } + Err(e) => { + writeln!(context, "Failed to save settings: {e:?}") + } + }, + Err(e) => { + writeln!(context, "Failed to update {key}: {e:?}") + } + }.unwrap(); + } + + fn menu() -> menu::Menu<'a, Self> { + menu::Menu { + label: "settings", + items: &[ + &menu::Item { + command: "list", + help: Some("List all available settings and their current values."), + item_type: menu::ItemType::Callback { + function: Self::handle_list, + parameters: &[], + }, + }, + &menu::Item { + command: "get", + help: Some("Read a setting_from the device."), + item_type: menu::ItemType::Callback { + function: Self::handle_get, + parameters: &[menu::Parameter::Mandatory { + parameter_name: "item", + help: Some("The name of the setting to read."), + }] + }, + }, + &menu::Item { + command: "set", + help: Some("Update a a setting in the device."), + item_type: menu::ItemType::Callback { + function: Self::handle_set, + parameters: &[ + menu::Parameter::Mandatory { + parameter_name: "item", + help: Some("The name of the setting to write."), + }, + menu::Parameter::Mandatory { + parameter_name: "value", + help: Some("Specifies the value to be written. Values must be JSON-encoded"), + }, + ] + }, + }, + &menu::Item { + command: "clear", + help: Some("Clear the device settings to default values."), + item_type: menu::ItemType::Callback { + function: Self::handle_clear, + parameters: &[] + }, + }, + &menu::Item { + command: "platform", + help: Some("Platform specific commands"), + item_type: menu::ItemType::Callback { + function: Self::handle_platform, + parameters: &[menu::Parameter::Mandatory { + parameter_name: "cmd", + help: Some("The name of the command (e.g. `reboot`, `service`, `dfu`)."), + }] + }, + }, + ], + entry: None, + exit: None, + } + } +} + +impl<'a, P: Platform> core::fmt::Write for Context<'a, P> { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + self.platform.interface_mut().write_str(s) + } +} + +/// The serial settings management object. +pub struct Runner<'a, P: Platform>(menu::Runner<'a, Context<'a, P>>); + +impl<'a, P: Platform> Runner<'a, P> { + /// Constructor + /// + /// # Args + /// * `platform` - The platform associated with the serial settings, providing the necessary + /// context and API to manage device settings. + /// * `line_buf` - A buffer used for maintaining the serial menu input line. It should be at + /// least as long as the longest user input. + /// * `serialize_buf` - A buffer used for serializing and deserializing settings. This buffer + /// needs to be at least as big as the entire serialized settings structure. + pub fn new( + platform: P, + line_buf: &'a mut [u8], + serialize_buf: &'a mut [u8], + ) -> Result { + Ok(Self(menu::Runner::new( + Context::menu(), + line_buf, + Context { + platform, + buffer: serialize_buf, + }, + ))) + } + + /// Get the current device settings. + pub fn settings(&self) -> &P::Settings { + self.0.context.platform.settings() + } + + /// Get the device communication interface + pub fn interface_mut(&mut self) -> &mut P::Interface { + self.0.context.platform.interface_mut() + } + + /// Must be called periodically to process user input. + pub fn process( + &mut self, + ) -> Result<(), ::Error> { + while self.interface_mut().read_ready()? { + let mut buffer = [0u8; 64]; + let count = self.interface_mut().read(&mut buffer)?; + for &value in &buffer[..count] { + self.0.input_byte(value); + } + } + Ok(()) + } +} diff --git a/src/bin/dual-iir.rs b/src/bin/dual-iir.rs index 5c27e50d4..8032e7c05 100644 --- a/src/bin/dual-iir.rs +++ b/src/bin/dual-iir.rs @@ -44,10 +44,10 @@ use stabilizer::{ afe::Gain, dac::{Dac0Output, Dac1Output, DacCode}, hal, - serial_terminal::SerialTerminal, signal_generator::{self, SignalGenerator}, timers::SamplingTimer, - DigitalInput0, DigitalInput1, SystemTimer, Systick, AFE0, AFE1, + DigitalInput0, DigitalInput1, SerialTerminal, SystemTimer, Systick, + UsbDevice, AFE0, AFE1, }, net::{ data_stream::{FrameGenerator, StreamFormat, StreamTarget}, @@ -94,10 +94,9 @@ pub struct Settings { /// * `` specifies which channel to configure. `` := [0, 1] /// * `` specifies which cascade to configure. `` := [0, 1], depending on [IIR_CASCADE_LENGTH] /// - /// # Value - /// See [iir::IIR#miniconf] + /// See [iir::Biquad] #[tree(depth(2))] - iir_ch: [[iir::IIR; IIR_CASCADE_LENGTH]; 2], + iir_ch: [[iir::Biquad; IIR_CASCADE_LENGTH]; 2], /// Specified true if DI1 should be used as a "hold" input. /// @@ -150,6 +149,9 @@ pub struct Settings { impl Default for Settings { fn default() -> Self { + let mut i = iir::Biquad::IDENTITY; + i.set_min(-SCALE); + i.set_max(SCALE); Self { // Analog frontend programmable gain amplifier gains (G1, G2, G5, G10) afe: [Gain::G1, Gain::G1], @@ -158,7 +160,7 @@ impl Default for Settings { // The array is `iir_state[channel-index][cascade-index][coeff-index]`. // The IIR coefficients can be mapped to other transfer function // representations, for example as described in https://arxiv.org/abs/1508.06319 - iir_ch: [[iir::IIR::new(1., -SCALE, SCALE); IIR_CASCADE_LENGTH]; 2], + iir_ch: [[i; IIR_CASCADE_LENGTH]; 2], // Permit the DI1 digital input to suppress filter output updates. allow_hold: false, @@ -183,7 +185,7 @@ mod app { #[shared] struct Shared { - usb_terminal: SerialTerminal, + usb: UsbDevice, network: NetworkUsers, settings: Settings, @@ -193,12 +195,13 @@ mod app { #[local] struct Local { + usb_terminal: SerialTerminal, sampling_timer: SamplingTimer, digital_inputs: (DigitalInput0, DigitalInput1), afes: (AFE0, AFE1), adcs: (Adc0Input, Adc1Input), dacs: (Dac0Output, Dac1Output), - iir_state: [[iir::Vec5; IIR_CASCADE_LENGTH]; 2], + iir_state: [[[f32; 4]; IIR_CASCADE_LENGTH]; 2], generator: FrameGenerator, cpu_temp_sensor: stabilizer::hardware::cpu_temp_sensor::CpuTempSensor, } @@ -216,13 +219,15 @@ mod app { SAMPLE_TICKS, ); + let settings = stabilizer.usb_serial.settings(); let mut network = NetworkUsers::new( stabilizer.net.stack, stabilizer.net.phy, clock, env!("CARGO_BIN_NAME"), - stabilizer.net.mac_address, - option_env!("BROKER").unwrap_or("mqtt"), + &settings.broker, + &settings.id, + stabilizer.metadata, ); let generator = network.configure_streaming(StreamFormat::AdcDacData); @@ -230,7 +235,7 @@ mod app { let settings = Settings::default(); let shared = Shared { - usb_terminal: stabilizer.usb_serial, + usb: stabilizer.usb, network, settings, telemetry: TelemetryBuffer::default(), @@ -249,12 +254,13 @@ mod app { }; let mut local = Local { + usb_terminal: stabilizer.usb_serial, sampling_timer: stabilizer.adc_dac_timer, digital_inputs: stabilizer.digital_inputs, afes: stabilizer.afes, adcs: stabilizer.adcs, dacs: stabilizer.dacs, - iir_state: [[[0.; 5]; IIR_CASCADE_LENGTH]; 2], + iir_state: [[[0.; 4]; IIR_CASCADE_LENGTH]; 2], generator, cpu_temp_sensor: stabilizer.temperature_sensor, }; @@ -341,7 +347,13 @@ mod app { .iter() .zip(iir_state[channel].iter_mut()) .fold(x, |yi, (ch, state)| { - ch.update(state, yi, hold) + let filter = if hold { + &iir::Biquad::HOLD + } else { + ch + }; + + filter.update(state, yi) }); // Note(unsafe): The filter limits must ensure that the value is in range. @@ -392,7 +404,7 @@ mod app { ); } - #[idle(shared=[network, usb_terminal])] + #[idle(shared=[network, usb])] fn idle(mut c: idle::Context) -> ! { loop { match c.shared.network.lock(|net| net.update()) { @@ -402,10 +414,10 @@ mod app { NetworkState::Updated => {} NetworkState::NoChange => { // We can't sleep if USB is not in suspend. - if c.shared - .usb_terminal - .lock(|terminal| terminal.usb_is_suspended()) - { + if c.shared.usb.lock(|usb| { + usb.state() + == usb_device::device::UsbDeviceState::Suspend + }) { cortex_m::asm::wfi(); } } @@ -464,10 +476,14 @@ mod app { .unwrap(); } - #[task(priority = 1, shared=[usb_terminal])] + #[task(priority = 1, shared=[usb], local=[usb_terminal])] fn usb(mut c: usb::Context) { // Handle the USB serial terminal. - c.shared.usb_terminal.lock(|usb| usb.process()); + c.shared.usb.lock(|usb| { + usb.poll(&mut [c.local.usb_terminal.interface_mut().inner_mut()]); + }); + + c.local.usb_terminal.process().unwrap(); // Schedule to run this task every 10 milliseconds. usb::spawn_after(10u64.millis()).unwrap(); diff --git a/src/bin/fnc.rs b/src/bin/fnc.rs new file mode 100644 index 000000000..46c135873 --- /dev/null +++ b/src/bin/fnc.rs @@ -0,0 +1,651 @@ +//! # Fibre Noise Cancellation +//! +//! Pounder samples the error signal input and mixes it down with a DDS at +//! 2*aom_f (channel::TWO). It passes it to Stabilizer for digital filtering +//! (normally a PI application) where it is read at a fixed rate. This is used +//! to feedback back into the phase offset of the DDS channel::ONE at aom_f +//! output through the Pounder. It currently only exposes CHANNEL 0 of the +//! Pounder for maximising feedback bandwidth, but can be easily extended to +//! expose both channels in the future if required and it proves sufficient +//! +//! Currently samples at 195.3 kHz, i.e. once in 5.12 us +//! +//! ## Features +//! * up to 200 kHz rate, timed sampling +//! * Run-time filter configuration +//! * Input/Output data streaming +//! * f32 IIR math +//! * Generic biquad (second order) IIR filter +//! * Anti-windup +//! * Derivative kick avoidance +//! +//! ## Settings +//! Refer to the [Settings] structure for documentation of run-time configurable settings for this +//! application. +//! +//! ## Telemetry +//! Refer to [Telemetry] for information about telemetry reported by this application. +//! +//! ## Livestreaming +//! This application streams raw ADC and DAC data over UDP. Refer to +//! [stabilizer::net::data_stream](../stabilizer/net/data_stream/index.html) for more information. +#![deny(warnings)] +#![no_std] +#![no_main] + +use core::mem::MaybeUninit; +use core::sync::atomic::{fence, Ordering}; + +use fugit::ExtU64; +use mutex_trait::prelude::*; + +use idsp::iir; + +use stabilizer::{ + hardware::{ + self, + adc::{Adc0Input, Adc1Input, AdcCode}, + afe::Gain, + hal, + pounder::{self, attenuators::AttenuatorInterface}, + timers::SamplingTimer, + DigitalInput0, DigitalInput1, SerialTerminal, SystemTimer, Systick, + UsbDevice, AFE0, AFE1, + }, + net::{ + data_stream::{FrameGenerator, StreamFormat, StreamTarget}, + miniconf::Tree, + telemetry::{Telemetry, TelemetryBuffer}, + NetworkState, NetworkUsers, + }, +}; + +const SCALE: f32 = i16::MAX as _; + +// The number of cascaded IIR biquads per channel. Select 1 or 2! +const IIR_CASCADE_LENGTH: usize = 1; + +// The number of samples in each batch process +const BATCH_SIZE: usize = 1; + +// The logarithm of the number of 100MHz timer ticks between each sample. With a value of 2^9 = +// 512, there is 5.12uS per sample, corresponding to a sampling frequency of 195.3125 KHz. +const SAMPLE_TICKS_LOG2: u8 = 9; +const SAMPLE_TICKS: u32 = 1 << SAMPLE_TICKS_LOG2; + +#[derive(Clone, Copy, Debug, Tree)] +pub struct Settings { + /// Configure the Analog Front End (AFE) gain. + /// + /// # Path + /// `afe/` + /// + /// * `` specifies which channel to configure. `` := [0, 1] + /// + /// # Value + /// Any of the variants of [Gain] enclosed in double quotes. + #[tree] + afe: [Gain; 2], + + /// Configure the IIR filter parameters. + /// + /// # Path + /// `iir_ch//` + /// + /// * `` specifies which channel to configure. `` := [0, 1] + /// * `` specifies which cascade to configure. `` := [0, 1], depending on [IIR_CASCADE_LENGTH] + /// + /// # Value + /// See [iir::IIR#Biquad] + #[tree(depth(2))] + iir_ch: [[iir::Biquad; IIR_CASCADE_LENGTH]; 2], + + /// Specified true if DI1 should be used as a "hold" input. + /// + /// # Path + /// `allow_hold` + /// + /// # Value + /// "true" or "false" + allow_hold: bool, + + /// Specified true if "hold" should be forced regardless of DI1 state and hold allowance. + /// + /// # Path + /// `force_hold` + /// + /// # Value + /// "true" or "false" + force_hold: bool, + + /// Specifies the telemetry output period in seconds. + /// + /// # Path + /// `telemetry_period` + /// + /// # Value + /// Any non-zero value less than 65536. + telemetry_period: u16, + + /// Specifies the target for data livestreaming. + /// + /// # Path + /// `stream_target` + /// + /// # Value + /// See [StreamTarget#miniconf] + stream_target: StreamTarget, + + /// Specifies the centre frequency of the fnc double-pass AOM in hertz + /// + /// # Path + /// `aom_centre_f` + /// + /// # Value + /// A positive 32-bit float in the range [1 MHz, 200 Mhz] + aom_centre_f: f32, + + /// Specifies the amplitude of the dds output driving the aom relative to max (10 dBm) + /// + /// # Path + /// `amplitude_out` + /// + /// # Value + /// A positive 32-bit float in the range [0.0, 1.0] + amplitude_out: f32, + + /// Specifies the amplitude of the dds output to mix down the error signal relative to max (10 dBm) + /// + /// # Path + /// `amplitude_mix` + /// + /// # Value + /// A positive 32-bit float in the range [0.0, 1.0] + amplitude_mix: f32, + + /// Specifies the attenuation applied to the output channel driving the aom (dB) + /// + /// # Path + /// `output_attenuation` + /// + /// # Value + /// A positive 32-bit float in the range [0.5, 31.5] in steps of 0.5 + output_attenuation: f32, + + /// Specifies the attenuation applied to the input channel from the photodiode (dB) + /// + /// # Path + /// `input_attenuation` + /// + /// # Value + /// A positive 32-bit float in the range [0.5, 31.5] in steps of 0.5 + input_attenuation: f32, +} + +impl Default for Settings { + fn default() -> Self { + let mut i = iir::Biquad::IDENTITY; + i.set_min(-SCALE); + i.set_max(SCALE); + Self { + // Analog frontend programmable gain amplifier gains (G1, G2, G5, G10) + afe: [Gain::G1, Gain::G1], + // IIR filter tap gains are an array `[b0, b1, b2, a1, a2]` such that the + // new output is computed as `y0 = a1*y1 + a2*y2 + b0*x0 + b1*x1 + b2*x2`. + // The array is `iir_state[channel-index][cascade-index][coeff-index]`. + // The IIR coefficients can be mapped to other transfer function + // representations, for example as described in https://arxiv.org/abs/1508.06319 + iir_ch: [[i; IIR_CASCADE_LENGTH]; 2], + + // Permit the DI1 digital input to suppress filter output updates. + allow_hold: false, + // Force suppress filter output updates. + force_hold: false, + // The default telemetry period in seconds. + telemetry_period: 10, + + stream_target: StreamTarget::default(), + + // Default AOM centre frequency for max efficiency + aom_centre_f: 80_000_000.0, + + // Default output and mixed amplitudes of 10dBm + amplitude_out: 1.0, + amplitude_mix: 1.0, + + // Output attenuation off by default to run near max + output_attenuation: 0.5, + + // Input attenuation off by default from photodiode + input_attenuation: 0.5, + } + } +} + +#[rtic::app(device = stabilizer::hardware::hal::stm32, peripherals = true, dispatchers=[DCMI, JPEG, LTDC, SDMMC])] +mod app { + use stabilizer::hardware::design_parameters::{self, DDS_SYSTEM_CLK}; + + use super::*; + + #[monotonic(binds = SysTick, default = true, priority = 2)] + type Monotonic = Systick; + + #[shared] + struct Shared { + usb: UsbDevice, + network: NetworkUsers, + + settings: Settings, + telemetry: TelemetryBuffer, + + dds: pounder::dds_output::DdsOutput, + } + + #[local] + struct Local { + usb_terminal: SerialTerminal, + sampling_timer: SamplingTimer, + digital_inputs: (DigitalInput0, DigitalInput1), + afes: (AFE0, AFE1), + adcs: (Adc0Input, Adc1Input), + iir_state: [[[f32; 4]; IIR_CASCADE_LENGTH]; 2], + generator: FrameGenerator, + cpu_temp_sensor: stabilizer::hardware::cpu_temp_sensor::CpuTempSensor, + phase_offset: u16, + pounder: pounder::PounderDevices, + } + + #[init] + fn init(c: init::Context) -> (Shared, Local, init::Monotonics) { + let clock = SystemTimer::new(|| monotonics::now().ticks() as u32); + + // Configure the microcontroller + let (stabilizer, pounder) = hardware::setup::setup( + c.core, + c.device, + clock, + BATCH_SIZE, + SAMPLE_TICKS, + ); + + let mut pounder = + pounder.expect("Fibre noise cancellation requires a Pounder"); + + let settings = stabilizer.usb_serial.settings(); + let mut network = NetworkUsers::new( + stabilizer.net.stack, + stabilizer.net.phy, + clock, + env!("CARGO_BIN_NAME"), + &settings.broker, + &settings.id, + stabilizer.metadata, + ); + + // todo: check streamformat for pounder data + let generator = network.configure_streaming(StreamFormat::AdcDacData); + + let settings = Settings::default(); + + // Turn off digital attenuators + pounder + .pounder + .set_attenuation( + pounder::Channel::Out0, + settings.output_attenuation, + ) + .unwrap(); + pounder + .pounder + .set_attenuation(pounder::Channel::In0, settings.input_attenuation) + .unwrap(); + + let mut shared = Shared { + usb: stabilizer.usb, + network, + settings, + telemetry: TelemetryBuffer::default(), + dds: pounder.dds_output, + }; + + let mut local = Local { + usb_terminal: stabilizer.usb_serial, + sampling_timer: stabilizer.adc_dac_timer, + digital_inputs: stabilizer.digital_inputs, + afes: stabilizer.afes, + adcs: stabilizer.adcs, + iir_state: [[[0.; 4]; IIR_CASCADE_LENGTH]; 2], + generator, + cpu_temp_sensor: stabilizer.temperature_sensor, + phase_offset: 0x00, + pounder: pounder.pounder, + }; + + let mut dds_profile = shared.dds.builder(); + + // set both channels to the same phase + dds_profile.update_channels( + ad9959::Channel::ONE | ad9959::Channel::TWO, + None, + Some(local.phase_offset), + None, + ); + + // amplitudes + let acr_out = + pounder::dds_output::amplitude_to_acr(settings.amplitude_out).ok(); + let acr_mix = + pounder::dds_output::amplitude_to_acr(settings.amplitude_mix).ok(); + + // aom frequency + let ftw = pounder::dds_output::frequency_to_ftw( + settings.aom_centre_f, + design_parameters::DDS_SYSTEM_CLK.to_Hz() as f32, + ) + .ok(); + dds_profile.update_channels(ad9959::Channel::ONE, ftw, None, acr_out); + + // Mix down 2 * aom centre frequency with input APD signal + let ftw = pounder::dds_output::frequency_to_ftw( + 2.0 * settings.aom_centre_f, + design_parameters::DDS_SYSTEM_CLK.to_Hz() as f32, + ) + .ok(); + + dds_profile.update_channels(ad9959::Channel::TWO, ftw, None, acr_mix); + + dds_profile.write(); + + // Enable ADC/DAC events + local.adcs.0.start(); + local.adcs.1.start(); + + // Spawn a settings update for default settings. + settings_update::spawn().unwrap(); + telemetry::spawn().unwrap(); + ethernet_link::spawn().unwrap(); + usb::spawn().unwrap(); + start::spawn_after(100.millis()).unwrap(); + + (shared, local, init::Monotonics(stabilizer.systick)) + } + + #[task(priority = 1, local=[sampling_timer])] + fn start(c: start::Context) { + // Start sampling ADCs and DACs. + c.local.sampling_timer.start(); + } + + /// Main DSP processing routine. + /// + /// # Note + /// Processing time for the DSP application code is bounded by the following constraints: + /// + /// DSP application code starts after the ADC has generated a batch of samples and must be + /// completed by the time the next batch of ADC samples has been acquired (plus the FIFO buffer + /// time). If this constraint is not met, firmware will panic due to an ADC input overrun. + /// + /// The DSP application code must also fill out the next DAC output buffer in time such that the + /// DAC can switch to it when it has completed the current buffer. If this constraint is not met + /// it's possible that old DAC codes will be generated on the output and the output samples will + /// be delayed by 1 batch. + /// + /// Because the ADC and DAC operate at the same rate, these two constraints actually implement + /// the same time bounds, meeting one also means the other is also met. + // todo: binds? + #[task(binds=DMA1_STR4, local=[digital_inputs, adcs, iir_state, generator, phase_offset], shared=[settings, telemetry, dds], priority=3)] + #[link_section = ".itcm.process"] + fn process(c: process::Context) { + let process::SharedResources { + settings, + telemetry, + dds, + } = c.shared; + + let process::LocalResources { + digital_inputs, + adcs: (adc0, adc1), + iir_state, + generator, + phase_offset, + } = c.local; + + (settings, telemetry, dds).lock(|settings, telemetry, dds| { + let digital_inputs = + [digital_inputs.0.is_high(), digital_inputs.1.is_high()]; + telemetry.digital_inputs = digital_inputs; + + let hold = settings.force_hold + || (digital_inputs[1] && settings.allow_hold); + + let mut dds_profile = dds.builder(); + + let power_in: f32 = 0.0; + (adc0, adc1).lock(|adc0, adc1| { + let adc_samples = [adc0, adc1]; + + // Preserve instruction and data ordering w.r.t. DMA flag access. + fence(Ordering::SeqCst); + + adc_samples[0] + .iter() + .map(|ai| { + let power_in = f32::from(*ai as i16); + + let iir_out = settings.iir_ch[0] + .iter() + .zip(iir_state[0].iter_mut()) + .fold(power_in, |iir_accumulator, (ch, state)| { + let filter = + if hold { &iir::Biquad::HOLD } else { ch }; + filter.update(state, iir_accumulator) + }); + + // Phase offset word might be off by 1 for negative iir_out, not sure. + *phase_offset = (((iir_out * (1 << 14) as f32) as i16 + & 0x3FFFi16) + as u16 + + *phase_offset) + & 0x3FFFu16; + + dds_profile.update_channels( + ad9959::Channel::ONE, + None, + Some(*phase_offset), + None, + ); + + dds_profile.write(); + }) + .last(); + + generator.add(|buf| { + let power_data = unsafe { + core::slice::from_raw_parts( + power_in.to_ne_bytes().as_ptr() + as *const MaybeUninit, + 4, + ) + }; + let phase_data = unsafe { + core::slice::from_raw_parts( + phase_offset.to_ne_bytes().as_ptr() + as *const MaybeUninit, + 2, + ) + }; + + buf[0..4].copy_from_slice(power_data); + buf[4..6].copy_from_slice(phase_data); + + 6 as usize + }); + + // Update telemetry measurements. + telemetry.adcs = + [AdcCode(adc_samples[0][0]), AdcCode(adc_samples[0][0])]; + + fence(Ordering::SeqCst); + }); + }); + } + + #[idle(shared=[network, usb])] + fn idle(mut c: idle::Context) -> ! { + loop { + match c.shared.network.lock(|net| net.update()) { + NetworkState::SettingsChanged(_path) => { + 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, local=[afes, pounder], shared=[network, settings, dds])] + fn settings_update(mut c: settings_update::Context) { + let settings = c.shared.network.lock(|net| *net.miniconf.settings()); + c.shared.settings.lock(|current| *current = settings); + + c.local.afes.0.set_gain(settings.afe[0]); + c.local.afes.1.set_gain(settings.afe[1]); + + let ftw_ch1 = pounder::dds_output::frequency_to_ftw( + settings.aom_centre_f, + DDS_SYSTEM_CLK.to_Hz() as f32, + ) + .ok(); + let ftw_ch2 = pounder::dds_output::frequency_to_ftw( + 2.0 * settings.aom_centre_f, + DDS_SYSTEM_CLK.to_Hz() as f32, + ) + .ok(); + + let acr_out = + pounder::dds_output::amplitude_to_acr(settings.amplitude_out).ok(); + let acr_mix = + pounder::dds_output::amplitude_to_acr(settings.amplitude_mix).ok(); + + if ftw_ch1.is_none() || ftw_ch2.is_none() { + log::warn!("Failed to set desired aom centre frequency"); + } else if acr_out.is_none() || acr_mix.is_none() { + log::warn!("Failed to set amplitude"); + } else { + c.shared.dds.lock(|dds| { + let mut dds_profile = dds.builder(); + dds_profile.update_channels( + ad9959::Channel::ONE, + ftw_ch1, + None, + None, + ); + dds_profile.update_channels( + ad9959::Channel::TWO, + ftw_ch2, + None, + None, + ); + dds_profile.write(); + }); + } + + if c.local + .pounder + .set_attenuation( + pounder::Channel::Out0, + settings.output_attenuation, + ) + .is_err() + { + log::warn!("Failed to set output attenuation"); + } + if c.local + .pounder + .set_attenuation(pounder::Channel::In0, settings.input_attenuation) + .is_err() + { + log::warn!("Failed to set input attenuation"); + } + + let target = settings.stream_target.into(); + c.shared.network.lock(|net| net.direct_stream(target)); + } + + // todo: fix pounder telemetry structuresl + #[task(priority = 1, shared=[network, settings, telemetry], local=[cpu_temp_sensor])] + fn telemetry(mut c: telemetry::Context) { + let telemetry: TelemetryBuffer = + c.shared.telemetry.lock(|telemetry| *telemetry); + + let (gains, telemetry_period) = c + .shared + .settings + .lock(|settings| (settings.afe, settings.telemetry_period)); + + c.shared.network.lock(|net| { + net.telemetry.publish(&telemetry.finalize( + gains[0], + gains[1], + c.local.cpu_temp_sensor.get_temperature().unwrap(), + )) + }); + + // Schedule the telemetry task in the future. + telemetry::Monotonic::spawn_after((telemetry_period as u64).secs()) + .unwrap(); + } + + #[task(priority = 1, shared=[usb], local=[usb_terminal])] + fn usb(mut c: usb::Context) { + // Handle the USB serial terminal. + c.shared.usb.lock(|usb| { + usb.poll(&mut [c.local.usb_terminal.interface_mut().inner_mut()]); + }); + + c.local.usb_terminal.process().unwrap(); + + // Schedule to run this task every 10 milliseconds. + usb::spawn_after(10u64.millis()).unwrap(); + } + + #[task(priority = 1, shared=[network])] + fn ethernet_link(mut c: ethernet_link::Context) { + c.shared.network.lock(|net| net.processor.handle_link()); + ethernet_link::Monotonic::spawn_after(1.secs()).unwrap(); + } + + #[task(binds = ETH, priority = 1)] + fn eth(_: eth::Context) { + unsafe { hal::ethernet::interrupt_handler() } + } + + #[task(binds = SPI2, priority = 4)] + fn spi2(_: spi2::Context) { + panic!("ADC0 SPI error"); + } + + #[task(binds = SPI3, priority = 4)] + fn spi3(_: spi3::Context) { + panic!("ADC1 SPI error"); + } + + #[task(binds = SPI4, priority = 4)] + fn spi4(_: spi4::Context) { + panic!("DAC0 SPI error"); + } + + #[task(binds = SPI5, priority = 4)] + fn spi5(_: spi5::Context) { + panic!("DAC1 SPI error"); + } +} diff --git a/src/bin/lockin.rs b/src/bin/lockin.rs index e89731171..c4237c8be 100644 --- a/src/bin/lockin.rs +++ b/src/bin/lockin.rs @@ -47,10 +47,10 @@ use stabilizer::{ dac::{Dac0Output, Dac1Output, DacCode}, hal, input_stamper::InputStamper, - serial_terminal::SerialTerminal, signal_generator, timers::SamplingTimer, - DigitalInput0, DigitalInput1, SystemTimer, Systick, AFE0, AFE1, + DigitalInput0, DigitalInput1, SerialTerminal, SystemTimer, Systick, + UsbDevice, AFE0, AFE1, }, net::{ data_stream::{FrameGenerator, StreamFormat, StreamTarget}, @@ -222,7 +222,7 @@ mod app { #[shared] struct Shared { - usb_terminal: SerialTerminal, + usb: UsbDevice, network: NetworkUsers, settings: Settings, telemetry: TelemetryBuffer, @@ -230,6 +230,7 @@ mod app { #[local] struct Local { + usb_terminal: SerialTerminal, sampling_timer: SamplingTimer, digital_inputs: (DigitalInput0, DigitalInput1), timestamper: InputStamper, @@ -256,20 +257,22 @@ mod app { SAMPLE_TICKS, ); + let settings = stabilizer.usb_serial.settings(); let mut network = NetworkUsers::new( stabilizer.net.stack, stabilizer.net.phy, clock, env!("CARGO_BIN_NAME"), - stabilizer.net.mac_address, - option_env!("BROKER").unwrap_or("mqtt"), + &settings.broker, + &settings.id, + stabilizer.metadata, ); let generator = network.configure_streaming(StreamFormat::AdcDacData); let shared = Shared { network, - usb_terminal: stabilizer.usb_serial, + usb: stabilizer.usb, telemetry: TelemetryBuffer::default(), settings: Settings::default(), }; @@ -284,6 +287,7 @@ mod app { }; let mut local = Local { + usb_terminal: stabilizer.usb_serial, sampling_timer: stabilizer.adc_dac_timer, digital_inputs: stabilizer.digital_inputs, afes: stabilizer.afes, @@ -454,7 +458,7 @@ mod app { }); } - #[idle(shared=[network, usb_terminal])] + #[idle(shared=[network, usb])] fn idle(mut c: idle::Context) -> ! { loop { match c.shared.network.lock(|net| net.update()) { @@ -464,10 +468,10 @@ mod app { NetworkState::Updated => {} NetworkState::NoChange => { // We can't sleep if USB is not in suspend. - if c.shared - .usb_terminal - .lock(|terminal| terminal.usb_is_suspended()) - { + if c.shared.usb.lock(|usb| { + usb.state() + == usb_device::device::UsbDeviceState::Suspend + }) { cortex_m::asm::wfi(); } } @@ -515,10 +519,14 @@ mod app { .unwrap(); } - #[task(priority = 1, shared=[usb_terminal])] + #[task(priority = 1, shared=[usb], local=[usb_terminal])] fn usb(mut c: usb::Context) { // Handle the USB serial terminal. - c.shared.usb_terminal.lock(|usb| usb.process()); + c.shared.usb.lock(|usb| { + usb.poll(&mut [c.local.usb_terminal.interface_mut().inner_mut()]); + }); + + c.local.usb_terminal.process().unwrap(); // Schedule to run this task every 10 milliseconds. usb::spawn_after(10u64.millis()).unwrap(); diff --git a/src/hardware/flash.rs b/src/hardware/flash.rs new file mode 100644 index 000000000..e40639b70 --- /dev/null +++ b/src/hardware/flash.rs @@ -0,0 +1,47 @@ +use stm32h7xx_hal::flash::LockedFlashBank; + +pub struct Flash(pub LockedFlashBank); + +impl Flash { + pub fn range(&self) -> core::ops::Range { + 0..(self.0.len() as u32) + } +} + +impl embedded_storage::nor_flash::ErrorType for Flash { + type Error = + ::Error; +} + +impl embedded_storage::nor_flash::ReadNorFlash for Flash { + const READ_SIZE: usize = LockedFlashBank::READ_SIZE; + + fn read( + &mut self, + offset: u32, + bytes: &mut [u8], + ) -> Result<(), Self::Error> { + self.0.read(offset, bytes) + } + + fn capacity(&self) -> usize { + self.0.capacity() + } +} + +impl embedded_storage::nor_flash::NorFlash for Flash { + const WRITE_SIZE: usize = + stm32h7xx_hal::flash::UnlockedFlashBank::WRITE_SIZE; + const ERASE_SIZE: usize = + stm32h7xx_hal::flash::UnlockedFlashBank::ERASE_SIZE; + + fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> { + let mut bank = self.0.unlocked(); + bank.erase(from, to) + } + + fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> { + let mut bank = self.0.unlocked(); + bank.write(offset, bytes) + } +} diff --git a/src/hardware/metadata.rs b/src/hardware/metadata.rs new file mode 100644 index 000000000..84cd632a9 --- /dev/null +++ b/src/hardware/metadata.rs @@ -0,0 +1,41 @@ +use crate::hardware::HardwareVersion; +use serde::Serialize; + +mod build_info { + include!(concat!(env!("OUT_DIR"), "/built.rs")); +} + +#[derive(Serialize)] +pub struct ApplicationMetadata { + pub firmware_version: &'static str, + pub rust_version: &'static str, + pub profile: &'static str, + pub git_dirty: bool, + pub features: &'static str, + pub panic_info: &'static str, + pub hardware_version: HardwareVersion, +} + +impl ApplicationMetadata { + /// Construct the global metadata. + /// + /// # Note + /// This may only be called once. + /// + /// # Args + /// * `hardware_version` - The hardware version detected. + /// + /// # Returns + /// A reference to the global metadata. + pub fn new(version: HardwareVersion) -> &'static ApplicationMetadata { + cortex_m::singleton!(: ApplicationMetadata = ApplicationMetadata { + firmware_version: build_info::GIT_VERSION.unwrap_or("Unspecified"), + rust_version: build_info::RUSTC_VERSION, + profile: build_info::PROFILE, + git_dirty: build_info::GIT_DIRTY.unwrap_or(false), + features: build_info::FEATURES_STR, + hardware_version: version, + panic_info: panic_persist::get_panic_message_utf8().unwrap_or("None"), + }).unwrap() + } +} diff --git a/src/hardware/mod.rs b/src/hardware/mod.rs index 3f58bb8e5..477192466 100644 --- a/src/hardware/mod.rs +++ b/src/hardware/mod.rs @@ -9,16 +9,17 @@ pub mod cpu_temp_sensor; pub mod dac; pub mod delay; pub mod design_parameters; +mod eeprom; +pub mod flash; pub mod input_stamper; +pub mod metadata; +pub mod platform; pub mod pounder; -pub mod serial_terminal; pub mod setup; pub mod shared_adc; pub mod signal_generator; pub mod timers; -mod eeprom; - // Type alias for the analog front-end (AFE) for ADC0. pub type AFE0 = afe::ProgrammableGainAmplifier< hal::gpio::gpiof::PF2>, @@ -33,6 +34,9 @@ pub type AFE1 = afe::ProgrammableGainAmplifier< pub type UsbBus = stm32h7xx_hal::usb_hs::UsbBus; +// Type alias for the USB device. +pub type UsbDevice = usb_device::device::UsbDevice<'static, UsbBus>; + // Type alias for digital input 0 (DI0). pub type DigitalInput0 = hal::gpio::gpiog::PG9; @@ -80,6 +84,56 @@ pub type I2c1 = hal::i2c::I2c; pub type I2c1Proxy = shared_bus::I2cProxy<'static, shared_bus::AtomicCheckMutex>; +pub type SerialTerminal = + serial_settings::Runner<'static, crate::settings::SerialSettingsPlatform>; + +pub enum HardwareVersion { + Rev1_0, + Rev1_1, + Rev1_2, + Rev1_3, + Unknown(u8), +} + +impl From for HardwareVersion { + fn from(bitfield: u8) -> Self { + match bitfield { + 0b000 => HardwareVersion::Rev1_0, + 0b001 => HardwareVersion::Rev1_1, + 0b010 => HardwareVersion::Rev1_2, + 0b011 => HardwareVersion::Rev1_3, + other => HardwareVersion::Unknown(other), + } + } +} + +impl core::fmt::Display for HardwareVersion { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + HardwareVersion::Rev1_0 => write!(f, "v1.0"), + HardwareVersion::Rev1_1 => write!(f, "v1.1"), + HardwareVersion::Rev1_2 => write!(f, "v1.2"), + HardwareVersion::Rev1_3 => write!(f, "v1.3"), + HardwareVersion::Unknown(other) => { + write!(f, "Unknown ({:#b})", other) + } + } + } +} + +impl serde::Serialize for HardwareVersion { + fn serialize( + &self, + serializer: S, + ) -> Result { + use core::fmt::Write; + + let mut version_string: heapless::String<32> = heapless::String::new(); + write!(&mut version_string, "{}", self).unwrap(); + serializer.serialize_str(&version_string) + } +} + #[inline(never)] #[panic_handler] fn panic(info: &core::panic::PanicInfo) -> ! { @@ -109,6 +163,8 @@ fn panic(info: &core::panic::PanicInfo) -> ! { writeln!(channel, "{info}").ok(); } + panic_persist::report_panic_info(info); + // Abort asm::udf(); // Halt diff --git a/src/hardware/platform.rs b/src/hardware/platform.rs new file mode 100644 index 000000000..2fee79df5 --- /dev/null +++ b/src/hardware/platform.rs @@ -0,0 +1,77 @@ +/// Flag used to indicate that a reboot to DFU is requested. +const DFU_REBOOT_FLAG: u32 = 0xDEAD_BEEF; + +/// Indicate a reboot to DFU is requested. +pub fn start_dfu_reboot() { + extern "C" { + static mut _bootflag: u8; + } + + unsafe { + let start_ptr = &mut _bootflag as *mut u8; + core::ptr::write_unaligned(start_ptr.cast::(), DFU_REBOOT_FLAG); + } + + cortex_m::peripheral::SCB::sys_reset(); +} + +/// Check if the DFU reboot flag is set, indicating a reboot to DFU is requested. +pub fn dfu_bootflag() -> bool { + // Obtain panic region start and end from linker symbol _panic_dump_start and _panic_dump_end + extern "C" { + static mut _bootflag: u8; + } + + unsafe { + let start_ptr = &mut _bootflag as *mut u8; + let set = DFU_REBOOT_FLAG + == core::ptr::read_unaligned(start_ptr.cast::()); + + // Clear the boot flag after checking it to ensure it doesn't stick between reboots. + core::ptr::write_unaligned(start_ptr.cast::(), 0); + set + } +} + +/// Execute the DFU bootloader stored in system memory. +/// +/// # Note +/// This function must be called before any system configuration is performed, as the DFU +/// bootloader expects the system in an uninitialized state. +pub fn execute_system_bootloader() { + // This process is largely adapted from + // https://community.st.com/t5/stm32-mcus/jump-to-bootloader-from-application-on-stm32h7-devices/ta-p/49510 + cortex_m::interrupt::disable(); + + // Disable the SysTick peripheral. + let systick = unsafe { &*cortex_m::peripheral::SYST::PTR }; + unsafe { + systick.csr.write(0); + systick.rvr.write(0); + systick.cvr.write(0); + } + + // Clear NVIC interrupt flags and enables. + let nvic = unsafe { &*cortex_m::peripheral::NVIC::PTR }; + for reg in nvic.icer.iter() { + unsafe { + reg.write(u32::MAX); + } + } + + for reg in nvic.icpr.iter() { + unsafe { + reg.write(u32::MAX); + } + } + + unsafe { cortex_m::interrupt::enable() }; + + // The chip does not provide a means to modify the BOOT pins during + // run-time. Jump to the bootloader in system memory instead. + unsafe { + let system_memory_address: *const u32 = 0x1FF0_9800 as *const u32; + log::info!("Jumping to DFU"); + cortex_m::asm::bootload(system_memory_address); + } +} diff --git a/src/hardware/pounder/dds_output.rs b/src/hardware/pounder/dds_output.rs index 3ae1ce901..215297a0e 100644 --- a/src/hardware/pounder/dds_output.rs +++ b/src/hardware/pounder/dds_output.rs @@ -55,7 +55,7 @@ use log::warn; use stm32h7xx_hal as hal; -use super::{hrtimer::HighResTimerE, QspiInterface}; +use super::{hrtimer::HighResTimerE, Error, QspiInterface}; use ad9959::{Channel, Mode, ProfileSerializer}; /// The DDS profile update stream. @@ -164,3 +164,59 @@ impl<'a> ProfileBuilder<'a> { self.dds_output.write(self.serializer.finalize()); } } + +/// Return the frequency tuning word to set the requested frequency. +/// +/// # Args +/// * `frequency_out` - the DDS output frequency to be set in hertz +/// * `dds_clock_frequency` - dds clock frequency in hertz +pub fn frequency_to_ftw( + frequency_out: f32, + dds_clock_frequency: f32, +) -> Result { + if frequency_out < 0.0 || frequency_out > dds_clock_frequency { + return Err(Error::Bounds); + } + + Ok( + (frequency_out / dds_clock_frequency * 1u64.wrapping_shl(32) as f32) + as u32, + ) +} + +/// Return the required amplitude control register value to set the requested relative amplitude +/// +/// # Args +/// * `amplitude` - the requested relative amplitude in range [0, 1] +pub fn amplitude_to_acr(amplitude: f32) -> Result { + if !(0.0..=1.0).contains(&litude) { + return Err(Error::Bounds); + } + + let mut amplitude_control: u16 = + (amplitude * (1 << 10) as f32) as u16 & 0x03FF; + + // Enable the amplitude multiplier for the channel if required. The amplitude control has + // full-scale at 0x3FF (amplitude of 1), so the multiplier should be disabled whenever + // full-scale is used by setting ACR[12] = 1; + if amplitude != 1.0 { + amplitude_control = amplitude_control | 0x1000; + } + + Ok(amplitude_control as u32) +} + +/// Return the phase offset word to set the requested phase offset +/// +/// # Args +/// * `phase_offset` - requested phase offset in turns. `0 <= phase_offset < 1` +pub fn phase_to_pow( + phase_offset: f32, + wrap_bounds: bool, +) -> Result { + if wrap_bounds || (0.0..1.0).contains(&phase_offset) { + Ok((phase_offset * (1 << 14) as f32) as u16 & 0x3FFF) + } else { + Err(Error::Bounds) + } +} diff --git a/src/hardware/pounder/mod.rs b/src/hardware/pounder/mod.rs index d6dd654fc..5bc7e9fff 100644 --- a/src/hardware/pounder/mod.rs +++ b/src/hardware/pounder/mod.rs @@ -22,10 +22,13 @@ pub enum GpioPin { Led7Red, Led8Green, Led9Red, + DetPwrdown0, + DetPwrdown1, AttLe0, AttLe1, AttLe2, AttLe3, + DdsReset, AttRstN, OscEnN, ExtClkSel, @@ -40,10 +43,13 @@ impl From for mcp230xx::Mcp23017 { GpioPin::Led7Red => Self::A3, GpioPin::Led8Green => Self::A4, GpioPin::Led9Red => Self::A5, + GpioPin::DetPwrdown0 => Self::A6, + GpioPin::DetPwrdown1 => Self::A7, GpioPin::AttLe0 => Self::B0, GpioPin::AttLe1 => Self::B1, GpioPin::AttLe2 => Self::B2, GpioPin::AttLe3 => Self::B3, + GpioPin::DdsReset => Self::B4, GpioPin::AttRstN => Self::B5, GpioPin::OscEnN => Self::B6, GpioPin::ExtClkSel => Self::B7, @@ -51,6 +57,29 @@ impl From for mcp230xx::Mcp23017 { } } +impl From for tca9539::Pin { + fn from(x: GpioPin) -> Self { + match x { + GpioPin::Led4Green => Self::P00, + GpioPin::Led5Red => Self::P01, + GpioPin::Led6Green => Self::P02, + GpioPin::Led7Red => Self::P03, + GpioPin::Led8Green => Self::P04, + GpioPin::Led9Red => Self::P05, + GpioPin::DetPwrdown0 => Self::P06, + GpioPin::DetPwrdown1 => Self::P07, + GpioPin::AttLe0 => Self::P10, + GpioPin::AttLe1 => Self::P11, + GpioPin::AttLe2 => Self::P12, + GpioPin::AttLe3 => Self::P13, + GpioPin::DdsReset => Self::P14, + GpioPin::AttRstN => Self::P15, + GpioPin::OscEnN => Self::P16, + GpioPin::ExtClkSel => Self::P17, + } + } +} + #[derive(Debug, Copy, Clone)] pub enum Error { Spi, @@ -306,101 +335,160 @@ impl ad9959::Interface for QspiInterface { } } +enum IoExpander { + Mcp(mcp230xx::Mcp230xx), + Pca(tca9539::Pca9539), +} + +impl IoExpander { + fn new(i2c: I2c1Proxy) -> Self { + // Population option on Pounder v1.2 and later. + let mut mcp23017 = + mcp230xx::Mcp230xx::new_default(i2c.clone()).unwrap(); + if mcp23017.read(0).is_ok() { + Self::Mcp(mcp23017) + } else { + let pca9359 = tca9539::Pca9539::new_default(i2c).unwrap(); + Self::Pca(pca9359) + } + } + + /// Set the state (its electrical level) of the given GPIO pin on Pounder. + fn set_gpio_dir( + &mut self, + pin: GpioPin, + dir: mcp230xx::Direction, + ) -> Result<(), Error> { + match self { + Self::Mcp(dev) => { + dev.set_direction(pin.into(), dir).map_err(|_| Error::I2c) + } + Self::Pca(dev) => { + let dir = match dir { + mcp230xx::Direction::Output => tca9539::Direction::Output, + _ => tca9539::Direction::Input, + }; + dev.set_direction(pin.into(), dir).map_err(|_| Error::I2c) + } + } + } + + /// Set the state (its electrical level) of the given GPIO pin on Pounder. + fn set_gpio_level( + &mut self, + pin: GpioPin, + level: mcp230xx::Level, + ) -> Result<(), Error> { + match self { + Self::Mcp(dev) => { + dev.set_gpio(pin.into(), level).map_err(|_| Error::I2c) + } + Self::Pca(dev) => { + let level = match level { + mcp230xx::Level::Low => tca9539::Level::Low, + _ => tca9539::Level::High, + }; + dev.set_level(pin.into(), level).map_err(|_| Error::I2c) + } + } + } +} + /// A structure containing implementation for Pounder hardware. pub struct PounderDevices { - mcp23017: mcp230xx::Mcp230xx, - pub lm75: lm75::Lm75, + io: IoExpander, + lm75: lm75::Lm75, attenuator_spi: hal::spi::Spi, - pwr0: AdcChannel< - 'static, - hal::stm32::ADC1, - hal::gpio::gpiof::PF11, - >, - pwr1: AdcChannel< - 'static, - hal::stm32::ADC2, - hal::gpio::gpiof::PF14, - >, - aux_adc0: AdcChannel< - 'static, - hal::stm32::ADC3, - hal::gpio::gpiof::PF3, - >, - aux_adc1: AdcChannel< - 'static, - hal::stm32::ADC3, - hal::gpio::gpiof::PF4, - >, -} - -impl PounderDevices { - /// Construct and initialize pounder-specific hardware. - /// - /// Args: - /// * `lm75` - The temperature sensor on Pounder. - /// * `mcp23017` - The GPIO expander on Pounder. - /// * `attenuator_spi` - A SPI interface to control digital attenuators. - /// * `pwr0` - The ADC channel to measure the IN0 input power. - /// * `pwr1` - The ADC channel to measure the IN1 input power. - /// * `aux_adc0` - The ADC channel to measure the ADC0 auxiliary input. - /// * `aux_adc1` - The ADC channel to measure the ADC1 auxiliary input. - pub fn new( - lm75: lm75::Lm75, - mcp23017: mcp230xx::Mcp230xx, - attenuator_spi: hal::spi::Spi, - pwr0: AdcChannel< + pwr: ( + AdcChannel< 'static, hal::stm32::ADC1, hal::gpio::gpiof::PF11, >, - pwr1: AdcChannel< + AdcChannel< 'static, hal::stm32::ADC2, hal::gpio::gpiof::PF14, >, - aux_adc0: AdcChannel< + ), + aux_adc: ( + AdcChannel< 'static, hal::stm32::ADC3, hal::gpio::gpiof::PF3, >, - aux_adc1: AdcChannel< + AdcChannel< 'static, hal::stm32::ADC3, hal::gpio::gpiof::PF4, >, + ), +} + +impl PounderDevices { + /// Construct and initialize pounder-specific hardware. + /// + /// Args: + /// * `i2c` - A Proxy to I2C1. + /// * `attenuator_spi` - A SPI interface to control digital attenuators. + /// * `pwr` - The ADC channels to measure the IN0/1 input power. + /// * `aux_adc` - The ADC channels to measure the ADC0/1 auxiliary input. + pub fn new( + i2c: I2c1Proxy, + attenuator_spi: hal::spi::Spi, + pwr: ( + AdcChannel< + 'static, + hal::stm32::ADC1, + hal::gpio::gpiof::PF11, + >, + AdcChannel< + 'static, + hal::stm32::ADC2, + hal::gpio::gpiof::PF14, + >, + ), + aux_adc: ( + AdcChannel< + 'static, + hal::stm32::ADC3, + hal::gpio::gpiof::PF3, + >, + AdcChannel< + 'static, + hal::stm32::ADC3, + hal::gpio::gpiof::PF4, + >, + ), ) -> Result { let mut devices = Self { - lm75, - mcp23017, + lm75: lm75::Lm75::new(i2c.clone(), lm75::Address::default()), + io: IoExpander::new(i2c.clone()), attenuator_spi, - pwr0, - pwr1, - aux_adc0, - aux_adc1, + pwr, + aux_adc, }; // Configure power-on-default state for pounder. All LEDs are off, on-board oscillator // selected and enabled, attenuators out of reset. Note that testing indicates the // output state needs to be set first to properly update the output registers. for pin in enum_iterator::all::() { - devices - .mcp23017 - .set_gpio(pin.into(), mcp230xx::Level::Low) - .map_err(|_| Error::I2c)?; - devices - .mcp23017 - .set_direction(pin.into(), mcp230xx::Direction::Output) - .map_err(|_| Error::I2c)?; + devices.io.set_gpio_level(pin, mcp230xx::Level::Low)?; + devices.io.set_gpio_dir(pin, mcp230xx::Direction::Output)?; } + devices.reset_attenuators().unwrap(); + + devices.reset_dds().unwrap(); + Ok(devices) } /// Sample one of the two auxiliary ADC channels associated with the respective RF input channel. pub fn sample_aux_adc(&mut self, channel: Channel) -> Result { let adc_scale = match channel { - Channel::In0 => self.aux_adc0.read_normalized().unwrap(), - Channel::In1 => self.aux_adc1.read_normalized().unwrap(), + Channel::In0 => self.aux_adc.0.read_normalized().unwrap(), + Channel::In1 => self.aux_adc.1.read_normalized().unwrap(), _ => return Err(Error::InvalidChannel), }; @@ -409,17 +497,6 @@ impl PounderDevices { Ok(adc_scale * 2.048) } - /// Set the state (its electrical level) of the given GPIO pin on Pounder. - pub fn set_gpio_pin( - &mut self, - pin: GpioPin, - level: mcp230xx::Level, - ) -> Result<(), Error> { - self.mcp23017 - .set_gpio(pin.into(), level) - .map_err(|_| Error::I2c) - } - /// Select external reference clock input. pub fn set_ext_clk(&mut self, enabled: bool) -> Result<(), Error> { let level = if enabled { @@ -428,8 +505,23 @@ impl PounderDevices { mcp230xx::Level::Low }; // Active low - self.set_gpio_pin(GpioPin::OscEnN, level)?; - self.set_gpio_pin(GpioPin::ExtClkSel, level) + self.io.set_gpio_level(GpioPin::OscEnN, level)?; + self.io.set_gpio_level(GpioPin::ExtClkSel, level) + } + + /// Reset the DDS via the GPIO extender (Pounder v1.2 and later) + pub fn reset_dds(&mut self) -> Result<(), Error> { + // DDS reset (Pounder v1.2 or later) + self.io + .set_gpio_level(GpioPin::DdsReset, mcp230xx::Level::High)?; + // I2C duration of this transaction is long enough (> 5 µs) to ensure valid reset. + self.io + .set_gpio_level(GpioPin::DdsReset, mcp230xx::Level::Low) + } + + /// Read the temperature reported by the LM75 temperature sensor on Pounder in deg C. + pub fn temperature(&mut self) -> Result { + self.lm75.read_temperature().map_err(|_| Error::I2c) } } @@ -437,8 +529,10 @@ impl attenuators::AttenuatorInterface for PounderDevices { /// Reset all of the attenuators to a power-on default state. fn reset_attenuators(&mut self) -> Result<(), Error> { // Active low - self.set_gpio_pin(GpioPin::AttRstN, mcp230xx::Level::Low)?; - self.set_gpio_pin(GpioPin::AttRstN, mcp230xx::Level::High) + self.io + .set_gpio_level(GpioPin::AttRstN, mcp230xx::Level::Low)?; + self.io + .set_gpio_level(GpioPin::AttRstN, mcp230xx::Level::High) } /// Latch a configuration into a digital attenuator. @@ -448,8 +542,10 @@ impl attenuators::AttenuatorInterface for PounderDevices { fn latch_attenuator(&mut self, channel: Channel) -> Result<(), Error> { // Rising edge sensitive // Be robust against initial state: drive low, then high (contrary to the datasheet figure). - self.set_gpio_pin(channel.into(), mcp230xx::Level::Low)?; - self.set_gpio_pin(channel.into(), mcp230xx::Level::High) + self.io + .set_gpio_level(channel.into(), mcp230xx::Level::Low)?; + self.io + .set_gpio_level(channel.into(), mcp230xx::Level::High) } /// Read the raw attenuation codes stored in the attenuator shift registers. @@ -479,8 +575,8 @@ impl rf_power::PowerMeasurementInterface for PounderDevices { /// The sampled voltage of the specified channel. fn sample_converter(&mut self, channel: Channel) -> Result { let adc_scale = match channel { - Channel::In0 => self.pwr0.read_normalized().unwrap(), - Channel::In1 => self.pwr1.read_normalized().unwrap(), + Channel::In0 => self.pwr.0.read_normalized().unwrap(), + Channel::In1 => self.pwr.1.read_normalized().unwrap(), _ => return Err(Error::InvalidChannel), }; diff --git a/src/hardware/serial_terminal.rs b/src/hardware/serial_terminal.rs deleted file mode 100644 index 649ad2836..000000000 --- a/src/hardware/serial_terminal.rs +++ /dev/null @@ -1,96 +0,0 @@ -use super::UsbBus; -use core::fmt::Write; - -static OUTPUT_BUFFER: bbqueue::BBBuffer<512> = bbqueue::BBBuffer::new(); - -pub struct OutputBuffer { - producer: bbqueue::Producer<'static, 512>, -} - -impl Write for OutputBuffer { - fn write_str(&mut self, s: &str) -> core::fmt::Result { - let data = s.as_bytes(); - - // Write as much data as possible to the output buffer. - let Ok(mut grant) = self.producer.grant_max_remaining(data.len()) - else { - // Output buffer is full, silently drop the data. - return Ok(()); - }; - - let len = grant.buf().len(); - grant.buf().copy_from_slice(&data[..len]); - grant.commit(len); - Ok(()) - } -} - -pub struct SerialTerminal { - usb_device: usb_device::device::UsbDevice<'static, UsbBus>, - usb_serial: usbd_serial::SerialPort<'static, UsbBus>, - output: bbqueue::Consumer<'static, 512>, - buffer: OutputBuffer, -} - -impl SerialTerminal { - pub fn new( - usb_device: usb_device::device::UsbDevice<'static, UsbBus>, - usb_serial: usbd_serial::SerialPort<'static, UsbBus>, - ) -> Self { - let (producer, consumer) = OUTPUT_BUFFER.try_split().unwrap(); - - Self { - buffer: OutputBuffer { producer }, - usb_device, - usb_serial, - output: consumer, - } - } - - fn flush(&mut self) { - let read = match self.output.read() { - Ok(grant) => grant, - Err(bbqueue::Error::InsufficientSize) => return, - err => err.unwrap(), - }; - - match self.usb_serial.write(read.buf()) { - Ok(count) => read.release(count), - Err(usbd_serial::UsbError::WouldBlock) => read.release(0), - Err(_) => { - let len = read.buf().len(); - read.release(len); - } - } - } - - pub fn usb_is_suspended(&self) -> bool { - self.usb_device.state() == usb_device::device::UsbDeviceState::Suspend - } - - pub fn process(&mut self) { - self.flush(); - - if !self.usb_device.poll(&mut [&mut self.usb_serial]) { - return; - } - - let mut buffer = [0u8; 64]; - match self.usb_serial.read(&mut buffer) { - Ok(count) => { - for &value in &buffer[..count] { - writeln!(self.buffer, "echo: {}", value as char).unwrap(); - } - } - - Err(usbd_serial::UsbError::WouldBlock) => {} - Err(_) => { - // Clear the output buffer if USB is not connected. - while let Ok(grant) = self.output.read() { - let len = grant.buf().len(); - grant.release(len); - } - } - } - } -} diff --git a/src/hardware/setup.rs b/src/hardware/setup.rs index 0873aca43..ac35ef9e1 100644 --- a/src/hardware/setup.rs +++ b/src/hardware/setup.rs @@ -1,6 +1,7 @@ //! Stabilizer hardware configuration //! //! This file contains all of the hardware-specific configuration of Stabilizer. +use bit_field::BitField; use core::sync::atomic::{self, AtomicBool, Ordering}; use core::{fmt::Write, ptr, slice}; use stm32h7xx_hal::{ @@ -14,11 +15,12 @@ use smoltcp_nal::smoltcp; use super::{ adc, afe, cpu_temp_sensor::CpuTempSensor, dac, delay, design_parameters, - eeprom, input_stamper::InputStamper, pounder, - pounder::dds_output::DdsOutput, serial_terminal::SerialTerminal, - shared_adc::SharedAdc, timers, DigitalInput0, DigitalInput1, - EemDigitalInput0, EemDigitalInput1, EemDigitalOutput0, EemDigitalOutput1, - EthernetPhy, NetworkStack, SystemTimer, Systick, UsbBus, AFE0, AFE1, + eeprom, input_stamper::InputStamper, metadata::ApplicationMetadata, + platform, pounder, pounder::dds_output::DdsOutput, shared_adc::SharedAdc, + timers, DigitalInput0, DigitalInput1, EemDigitalInput0, EemDigitalInput1, + EemDigitalOutput0, EemDigitalOutput1, EthernetPhy, HardwareVersion, + NetworkStack, SerialTerminal, SystemTimer, Systick, UsbBus, UsbDevice, + AFE0, AFE1, }; const NUM_TCP_SOCKETS: usize = 4; @@ -118,6 +120,8 @@ pub struct StabilizerDevices { pub digital_inputs: (DigitalInput0, DigitalInput1), pub eem_gpio: EemGpioDevices, pub usb_serial: SerialTerminal, + pub usb: UsbDevice, + pub metadata: &'static ApplicationMetadata, } /// The available Pounder-specific hardware interfaces. @@ -247,6 +251,11 @@ pub fn setup( log::info!("Starting"); } + // Check for a reboot to DFU before doing any system configuration. + if platform::dfu_bootflag() { + platform::execute_system_bootloader(); + } + let pwr = device.PWR.constrain(); let vos = pwr.freeze(); @@ -590,6 +599,25 @@ pub fn setup( ) }; + let metadata = { + // Read the hardware version pins. + let hardware_version = { + let hwrev0 = gpiog.pg0.into_pull_down_input(); + let hwrev1 = gpiog.pg1.into_pull_down_input(); + let hwrev2 = gpiog.pg2.into_pull_down_input(); + let hwrev3 = gpiog.pg3.into_pull_down_input(); + + HardwareVersion::from( + *0u8.set_bit(0, hwrev0.is_high()) + .set_bit(1, hwrev1.is_high()) + .set_bit(2, hwrev2.is_high()) + .set_bit(3, hwrev3.is_high()), + ) + }; + + ApplicationMetadata::new(hardware_version) + }; + let mac_addr = smoltcp::wire::EthernetAddress(eeprom::read_eui48( &mut eeprom_i2c, &mut delay, @@ -813,12 +841,6 @@ pub fn setup( shared_bus::new_atomic_check!(hal::i2c::I2c = i2c1).unwrap() }; - let io_expander = - mcp230xx::Mcp230xx::new_default(i2c1.acquire_i2c()).unwrap(); - - let temp_sensor = - lm75::Lm75::new(i2c1.acquire_i2c(), lm75::Address::default()); - let spi = { let mosi = gpiod.pd7.into_alternate(); let miso = gpioa.pa6.into_alternate(); @@ -846,13 +868,10 @@ pub fn setup( let aux_adc1 = adc3.create_channel(gpiof.pf4.into_analog()); let pounder_devices = pounder::PounderDevices::new( - temp_sensor, - io_expander, + i2c1.acquire_i2c(), spi, - pwr0, - pwr1, - aux_adc0, - aux_adc1, + (pwr0, pwr1), + (aux_adc0, aux_adc1), ) .unwrap(); @@ -1061,15 +1080,45 @@ pub fn setup( usb_bus.as_ref().unwrap(), usb_device::device::UsbVidPid(0x1209, 0x392F), ) - .manufacturer("ARTIQ/Sinara") - .product("Stabilizer") - .serial_number(serial_number.as_ref().unwrap()) + .strings(&[usb_device::device::StringDescriptors::default() + .manufacturer("ARTIQ/Sinara") + .product("Stabilizer") + .serial_number(serial_number.as_ref().unwrap())]) + .unwrap() .device_class(usbd_serial::USB_CLASS_CDC) .build(); (usb_device, serial) }; + let usb_serial = { + let (_, flash_bank2) = device.FLASH.split(); + + let input_buffer = + cortex_m::singleton!(: [u8; 256] = [0u8; 256]).unwrap(); + let serialize_buffer = + cortex_m::singleton!(: [u8; 512] = [0u8; 512]).unwrap(); + + let mut storage = super::flash::Flash(flash_bank2.unwrap()); + let mut settings = + crate::settings::Settings::new(network_devices.mac_address); + settings.reload(&mut storage); + + serial_settings::Runner::new( + crate::settings::SerialSettingsPlatform { + interface: serial_settings::BestEffortInterface::new( + usb_serial, + ), + storage, + settings, + metadata, + }, + input_buffer, + serialize_buffer, + ) + .unwrap() + }; + let stabilizer = StabilizerDevices { systick, afes, @@ -1084,7 +1133,9 @@ pub fn setup( timestamp_timer, digital_inputs, eem_gpio, - usb_serial: SerialTerminal::new(usb_device, usb_serial), + usb: usb_device, + usb_serial, + metadata, }; // info!("Version {} {}", build_info::PKG_VERSION, build_info::GIT_VERSION.unwrap()); diff --git a/src/lib.rs b/src/lib.rs index 85964a788..23a20da22 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,3 +3,4 @@ pub mod hardware; pub mod net; +pub mod settings; diff --git a/src/net/mod.rs b/src/net/mod.rs index 7e3b7b66b..4248049dd 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -13,7 +13,10 @@ pub mod data_stream; pub mod network_processor; pub mod telemetry; -use crate::hardware::{EthernetPhy, NetworkManager, NetworkStack, SystemTimer}; +use crate::hardware::{ + metadata::ApplicationMetadata, EthernetPhy, NetworkManager, NetworkStack, + SystemTimer, +}; use data_stream::{DataStream, FrameGenerator}; use network_processor::NetworkProcessor; use telemetry::TelemetryClient; @@ -84,8 +87,9 @@ where /// * `phy` - The ethernet PHY connecting the network. /// * `clock` - A `SystemTimer` implementing `Clock`. /// * `app` - The name of the application. - /// * `mac` - The MAC address of the network. /// * `broker` - The domain name of the MQTT broker to use. + /// * `id` - The MQTT client ID base to use. + /// * `metadata` - The application metadata /// /// # Returns /// A new struct of network users. @@ -94,8 +98,9 @@ where phy: EthernetPhy, clock: SystemTimer, app: &str, - mac: smoltcp_nal::smoltcp::wire::EthernetAddress, broker: &str, + id: &str, + metadata: &'static ApplicationMetadata, ) -> Self { let stack_manager = cortex_m::singleton!(: NetworkManager = NetworkManager::new(stack)) @@ -104,7 +109,7 @@ where let processor = NetworkProcessor::new(stack_manager.acquire_stack(), phy); - let prefix = get_device_prefix(app, mac); + let prefix = get_device_prefix(app, id); let store = cortex_m::singleton!(: MqttStorage = MqttStorage::default()) @@ -124,7 +129,7 @@ where named_broker, &mut store.settings, ) - .client_id(&get_client_id(app, "settings", mac)) + .client_id(&get_client_id(id, "settings")) .unwrap(), ) .unwrap(); @@ -141,11 +146,11 @@ where // The telemetry client doesn't receive any messages except MQTT control packets. // As such, we don't need much of the buffer for RX. .rx_buffer(minimq::config::BufferConfig::Maximum(100)) - .client_id(&get_client_id(app, "tlm", mac)) + .client_id(&get_client_id(id, "tlm")) .unwrap(), ); - let telemetry = TelemetryClient::new(mqtt, &prefix); + let telemetry = TelemetryClient::new(mqtt, &prefix, metadata); let (generator, stream) = data_stream::setup_streaming(stack_manager.acquire_stack()); @@ -218,19 +223,14 @@ where /// Get an MQTT client ID for a client. /// /// # Args -/// * `app` - The name of the application -/// * `client` - The unique tag of the client -/// * `mac` - The MAC address of the device. +/// * `id` - The base client ID +/// * `mode` - The operating mode of this client. (i.e. tlm, settings) /// /// # Returns /// A client ID that may be used for MQTT client identification. -fn get_client_id( - app: &str, - client: &str, - mac: smoltcp_nal::smoltcp::wire::EthernetAddress, -) -> String<64> { +fn get_client_id(id: &str, mode: &str) -> String<64> { let mut identifier = String::new(); - write!(&mut identifier, "{app}-{mac}-{client}").unwrap(); + write!(&mut identifier, "{id}-{mode}").unwrap(); identifier } @@ -238,18 +238,15 @@ fn get_client_id( /// /// # Args /// * `app` - The name of the application that is executing. -/// * `mac` - The ethernet MAC address of the device. +/// * `id` - The MQTT ID of the device. /// /// # Returns /// The MQTT prefix used for this device. -pub fn get_device_prefix( - app: &str, - mac: smoltcp_nal::smoltcp::wire::EthernetAddress, -) -> String<128> { +pub fn get_device_prefix(app: &str, id: &str) -> String<128> { // Note(unwrap): The mac address + binary name must be short enough to fit into this string. If // they are defined too long, this will panic and the device will fail to boot. let mut prefix: String<128> = String::new(); - write!(&mut prefix, "dt/sinara/{app}/{mac}").unwrap(); + write!(&mut prefix, "dt/sinara/{app}/{id}").unwrap(); prefix } diff --git a/src/net/telemetry.rs b/src/net/telemetry.rs index e5efd28ef..6cbbe768a 100644 --- a/src/net/telemetry.rs +++ b/src/net/telemetry.rs @@ -10,12 +10,17 @@ //! sampling frequency. Instead, the raw codes are stored and the telemetry is generated as //! required immediately before transmission. This ensures that any slower computation required //! for unit conversion can be off-loaded to lower priority tasks. +use crate::hardware::metadata::ApplicationMetadata; use heapless::{String, Vec}; +use minimq::{DeferredPublication, Publication}; use serde::Serialize; use super::NetworkReference; use crate::hardware::{adc::AdcCode, afe::Gain, dac::DacCode, SystemTimer}; +/// Default metadata message if formatting errors occur. +const DEFAULT_METADATA: &str = "{\"message\":\"Truncated: See USB terminal\"}"; + /// The telemetry client for reporting telemetry data over MQTT. pub struct TelemetryClient { mqtt: minimq::Minimq< @@ -24,8 +29,10 @@ pub struct TelemetryClient { SystemTimer, minimq::broker::NamedBroker, >, - telemetry_topic: String<128>, + prefix: String<128>, + meta_published: bool, _telemetry: core::marker::PhantomData, + metadata: &'static ApplicationMetadata, } /// The telemetry buffer is used for storing sample values during execution. @@ -114,14 +121,14 @@ impl TelemetryClient { minimq::broker::NamedBroker, >, prefix: &str, + metadata: &'static ApplicationMetadata, ) -> Self { - let mut telemetry_topic: String<128> = String::from(prefix); - telemetry_topic.push_str("/telemetry").unwrap(); - Self { mqtt, - telemetry_topic, + meta_published: false, + prefix: String::from(prefix), _telemetry: core::marker::PhantomData, + metadata, } } @@ -134,13 +141,17 @@ impl TelemetryClient { /// # Args /// * `telemetry` - The telemetry to report pub fn publish(&mut self, telemetry: &T) { + let mut topic = self.prefix.clone(); + topic.push_str("/telemetry").unwrap(); + let telemetry: Vec = serde_json_core::to_vec(telemetry).unwrap(); + self.mqtt .client() .publish( minimq::Publication::<&[u8]>::new(&telemetry) - .topic(&self.telemetry_topic) + .topic(&topic) .finish() .unwrap(), ) @@ -165,5 +176,50 @@ impl TelemetryClient { Err(error) => log::info!("Unexpected error: {:?}", error), _ => {} } + + if !self.mqtt.client().is_connected() { + self.meta_published = false; + return; + } + + // Publish application metadata + if !self.meta_published + && self.mqtt.client().can_publish(minimq::QoS::AtMostOnce) + { + let Self { + ref mut mqtt, + metadata, + .. + } = self; + + let mut topic = self.prefix.clone(); + topic.push_str("/meta").unwrap(); + + if mqtt + .client() + .publish( + DeferredPublication::new(|buf| { + serde_json_core::to_slice(&metadata, buf) + }) + .topic(&topic) + .finish() + .unwrap(), + ) + .is_err() + { + // Note(unwrap): We can guarantee that this message will be sent because we checked + // for ability to publish above. + mqtt.client() + .publish( + Publication::new(DEFAULT_METADATA.as_bytes()) + .topic(&topic) + .finish() + .unwrap(), + ) + .unwrap(); + } + + self.meta_published = true; + } } } diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 000000000..381001d7a --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,260 @@ +//! Stabilizer Settings Management +//! +//! # Design +//! Stabilizer supports two types of settings: +//! 1. Static Device Configuration +//! 2. Dynamic Run-time Settings +//! +//! Static device configuration settings are loaded and used only at device power-up. These include +//! things like the MQTT broker address and the MQTT identified. Conversely, the dynamic run-time +//! settings can be changed and take effect immediately during device operation. +//! +//! This settings management interface is currently targeted at the static device configuration +//! settings. Settings are persisted into the unused 1MB flash bank of Stabilizer for future +//! recall. They can be modified via the USB interface to facilitate device configuration. +//! +//! Settings are stored in flash using a key-value pair mapping, where the `key` is the name of the +//! entry in the settings structure. This has a number of benefits: +//! 1. The `Settings` structure can have new entries added to it in the future without losing old +//! settings values, as each entry of the `Settings` struct is stored separately as its own +//! key-value pair. +//! 2. The `Settings` can be used among multiple Stabilizer firmware versions that need the same +//! settings values +//! 3. Unknown/unneeded settings values in flash can be actively ignored, facilitating simple flash +//! storage sharing. +use crate::hardware::{flash::Flash, metadata::ApplicationMetadata, platform}; +use core::fmt::Write; +use miniconf::{TreeDeserialize, TreeKey, TreeSerialize}; +use postcard::ser_flavors::Flavor; +use stm32h7xx_hal::flash::LockedFlashBank; + +#[derive(Clone, miniconf::Tree)] +pub struct Settings { + pub broker: heapless::String<255>, + pub id: heapless::String<23>, + #[tree(skip)] + pub mac: smoltcp_nal::smoltcp::wire::EthernetAddress, +} + +impl serial_settings::Settings for Settings { + fn reset(&mut self) { + *self = Self::new(self.mac) + } +} + +impl Settings { + pub fn new(mac: smoltcp_nal::smoltcp::wire::EthernetAddress) -> Self { + let mut id = heapless::String::new(); + write!(&mut id, "{mac}").unwrap(); + + Self { + broker: "10.255.6.4".into(), + id, + mac, + } + } + + pub fn reload(&mut self, storage: &mut Flash) { + // Loop over flash and read settings + let mut buffer = [0u8; 512]; + for path in Settings::iter_paths::>("/") { + let path = path.unwrap(); + + // Try to fetch the setting from flash. + let Some(item) = + sequential_storage::map::fetch_item::( + storage, + storage.range(), + &mut buffer, + path.clone(), + ) + .unwrap() + else { + continue; + }; + + log::info!("Found `{path}` in flash settings"); + + let mut deserializer = postcard::Deserializer::from_flavor( + postcard::de_flavors::Slice::new(&item.data), + ); + if let Err(e) = self + .deserialize_by_key(path.split('/').skip(1), &mut deserializer) + { + log::warn!("Failed to load {path} from flash settings: {e:?}"); + } + } + } +} + +#[derive(Default, serde::Serialize, serde::Deserialize)] +pub struct SettingsItem { + // We only make these owned vec/string to get around lifetime limitations. + pub path: heapless::String<32>, + pub data: heapless::Vec, +} + +impl sequential_storage::map::StorageItem for SettingsItem { + type Key = heapless::String<32>; + type Error = postcard::Error; + + fn serialize_into(&self, buffer: &mut [u8]) -> Result { + Ok(postcard::to_slice(self, buffer)?.len()) + } + + fn deserialize_from(buffer: &[u8]) -> Result { + postcard::from_bytes(buffer) + } + + fn key(&self) -> Self::Key { + self.path.clone() + } +} + +#[derive(Debug)] +pub enum Error { + Postcard(postcard::Error), + Flash(F), +} + +impl From for Error { + fn from(e: postcard::Error) -> Self { + Self::Postcard(e) + } +} + +pub struct SerialSettingsPlatform { + /// The interface to read/write data to/from serially (via text) to the user. + pub interface: serial_settings::BestEffortInterface< + usbd_serial::SerialPort<'static, crate::hardware::UsbBus>, + >, + /// The Settings structure. + pub settings: Settings, + /// The storage mechanism used to persist settings to between boots. + pub storage: Flash, + + /// Metadata associated with the application + pub metadata: &'static ApplicationMetadata, +} + +impl serial_settings::Platform for SerialSettingsPlatform { + type Interface = serial_settings::BestEffortInterface< + usbd_serial::SerialPort<'static, crate::hardware::UsbBus>, + >; + type Settings = Settings; + type Error = Error< + ::Error, + >; + + fn save(&mut self, buf: &mut [u8]) -> Result<(), Self::Error> { + for path in Settings::iter_paths::>("/") { + let mut item = SettingsItem { + path: path.unwrap(), + ..Default::default() + }; + + item.data.resize(item.data.capacity(), 0).unwrap(); + + let mut serializer = postcard::Serializer { + output: postcard::ser_flavors::Slice::new(&mut item.data), + }; + + if let Err(e) = self + .settings + .serialize_by_key(item.path.split('/').skip(1), &mut serializer) + { + log::warn!("Failed to save {} to flash: {e:?}", item.path); + continue; + } + + let len = serializer.output.finalize()?.len(); + item.data.truncate(len); + + let range = self.storage.range(); + + // Check if the settings has changed from what's currently in flash (or if it doesn't + // yet exist). + if sequential_storage::map::fetch_item::( + &mut self.storage, + range.clone(), + buf, + item.path.clone(), + ) + .unwrap() + .map(|old| old.data != item.data) + .unwrap_or(true) + { + log::info!("Storing setting `{}` in flash", item.path); + sequential_storage::map::store_item( + &mut self.storage, + range, + buf, + item, + ) + .unwrap(); + } + } + + Ok(()) + } + + fn cmd(&mut self, cmd: &str) { + match cmd { + "reboot" => cortex_m::peripheral::SCB::sys_reset(), + "dfu" => platform::start_dfu_reboot(), + "service" => { + writeln!( + &mut self.interface, + "{:<20}: {} [{}]", + "Version", + self.metadata.firmware_version, + self.metadata.profile, + ) + .unwrap(); + writeln!( + &mut self.interface, + "{:<20}: {}", + "Hardware Revision", self.metadata.hardware_version + ) + .unwrap(); + writeln!( + &mut self.interface, + "{:<20}: {}", + "Rustc Version", self.metadata.rust_version + ) + .unwrap(); + writeln!( + &mut self.interface, + "{:<20}: {}", + "Features", self.metadata.features + ) + .unwrap(); + writeln!( + &mut self.interface, + "{:<20}: {}", + "Panic Info", self.metadata.panic_info + ) + .unwrap(); + } + _ => { + writeln!( + self.interface_mut(), + "Invalid platform command: `{cmd}` not in [`dfu`, `reboot`, `service`]" + ) + .ok(); + } + } + } + + fn settings(&self) -> &Self::Settings { + &self.settings + } + + fn settings_mut(&mut self) -> &mut Self::Settings { + &mut self.settings + } + + fn interface_mut(&mut self) -> &mut Self::Interface { + &mut self.interface + } +}