diff --git a/Cargo.lock b/Cargo.lock index 5e73b59..53418ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,6 +86,12 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.11.1" @@ -262,6 +268,57 @@ version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e" +[[package]] +name = "defmt" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" +dependencies = [ + "defmt 1.0.1", +] + +[[package]] +name = "defmt" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d4fc12a85bcf441cfe44344c4b72d58493178ce635338a3f3b78943aceb258e" +dependencies = [ + "defmt-parser", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "defmt-rtt" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6eca0aae8aa2cf8333200ecbd236274697bc0a394765c858b3d9372eb1abcfa" +dependencies = [ + "critical-section", + "defmt 0.3.100", +] + [[package]] name = "digest" version = "0.10.7" @@ -472,7 +529,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a25746d8b152b72fbf2a217f489a083dbbe243f281f09184a1f2cfbe9bbb245f" dependencies = [ - "bitflags", + "bitflags 2.11.1", "embassy-futures", "embassy-net-driver-channel", "embassy-sync", @@ -722,6 +779,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497ed9593e4baf6ef120b24aff80cb898f6832d32cdf01be9d90be0c06624e77" dependencies = [ + "defmt 0.3.100", "embassy-futures", "embassy-sync", "embassy-time", @@ -771,6 +829,8 @@ version = "0.2.0" dependencies = [ "cortex-m", "cortex-m-rt", + "defmt 0.3.100", + "defmt-rtt", "embassy-executor", "embassy-futures", "embassy-rp", @@ -779,6 +839,7 @@ dependencies = [ "embassy-usb", "hsmc", "libm", + "panic-probe", "panic-reset", "portable-atomic", "smart-leds", @@ -960,6 +1021,16 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "panic-probe" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4047d9235d1423d66cc97da7d07eddb54d4f154d6c13805c6d0793956f4f25b0" +dependencies = [ + "cortex-m", + "defmt 0.3.100", +] + [[package]] name = "panic-reset" version = "0.1.1" @@ -1150,7 +1221,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.11.1", ] [[package]] @@ -1420,6 +1491,26 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.9" diff --git a/Cargo.toml b/Cargo.toml index 99297ad..30735f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,11 +12,31 @@ categories = ["embedded", "no-std"] publish = false # firmware binary; not a crates.io library [features] -default = ["embassy"] +default = ["embassy", "panic-reset"] # `hsmc` emits `#[cfg(feature = "embassy")]` from inside `statechart!` and # the cfg is evaluated in this crate's context, so we mirror the flag here # and forward it to `hsmc/embassy`. embassy = ["hsmc/embassy"] +# Default panic handler — silently resets the chip. Mutually exclusive with +# `defmt` (which uses panic-probe). Build a defmt firmware with +# `cargo build --no-default-features --features embassy,defmt`. +panic-reset = ["dep:panic-reset"] +# Same trick as `embassy` above: `hsmc` emits `#[cfg(feature = "trace-defmt")]` +# from inside `__chart_observe!` (re-exported via `statechart!`), and that +# cfg is evaluated in *this* crate's feature set, not hsmc's. So we mirror +# the flag here and forward it to `hsmc/trace-defmt`. Without this mirror, +# `--features defmt` would compile cleanly but emit zero state-transition +# logs, because every defmt arm in __chart_observe! would be cfg'd out. +trace-defmt = ["hsmc/trace-defmt"] +# Debug build over RTT. Pulls in defmt + defmt-rtt, swaps the panic handler +# for panic-probe (so panics get printed instead of silently resetting), and +# turns on hsmc's chart/state/transition tracing. +defmt = [ + "dep:defmt", + "dep:defmt-rtt", + "dep:panic-probe", + "trace-defmt", +] [dependencies] embassy-executor = { version = "0.10.0", features = ["platform-cortex-m", "executor-thread"] } @@ -32,11 +52,16 @@ libm = "0.2" cortex-m = { version = "0.7.6", features = ["inline-asm"] } cortex-m-rt = "0.7.5" -panic-reset = "0.1" +panic-reset = { version = "0.1", optional = true } static_cell = "2.1" # Cortex-M0+ has no hardware CAS; static_cell + heapless need this shim. portable-atomic = { version = "1.13", features = ["critical-section"] } +# Defmt stack — only linked when the `defmt` feature is enabled. +defmt = { version = "0.3", optional = true } +defmt-rtt = { version = "0.4", optional = true } +panic-probe = { version = "0.3", features = ["print-defmt"], optional = true } + [lints.rust] unexpected_cfgs = { level = "allow", check-cfg = ['cfg(feature, values("tokio", "embassy"))'] } diff --git a/build.rs b/build.rs index 4c4bd66..f06a131 100644 --- a/build.rs +++ b/build.rs @@ -9,4 +9,11 @@ fn main() { println!("cargo:rustc-link-search={}", out.display()); println!("cargo:rerun-if-changed=memory.x"); println!("cargo:rerun-if-changed=build.rs"); + + // defmt ships its own linker script (`defmt.x`) that defines the + // `_defmt_*` interner sections. Only link it when the feature is on — + // otherwise the symbols don't exist and `ld` will refuse the build. + if env::var_os("CARGO_FEATURE_DEFMT").is_some() { + println!("cargo:rustc-link-arg=-Tdefmt.x"); + } } diff --git a/justfile b/justfile index 0606757..74b1240 100644 --- a/justfile +++ b/justfile @@ -7,6 +7,7 @@ firmware := "jiggly" target := "thumbv6m-none-eabi" base_addr := "0x10000000" family_id := "0xE48BFF56" +chip := "RP2040" out_release := "target" / target / "release" / firmware out_bin := "target" / target / "release" / firmware + ".bin" @@ -99,6 +100,31 @@ flash: uf2 echo "Timed out waiting for XIAO UF2 volume. Copy {{ out_uf2 }} manually." >&2 exit 1 +# Build the firmware with defmt-over-RTT enabled. Mutually exclusive with +# the default `panic-reset` feature, so we drop default features. +# DEFMT_LOG=trace overrides the `off` default in mise.toml so the macros +# actually emit. Built in release mode because thumbv6m debug builds plus +# defmt-rtt + panic-probe overflow flash quickly. +build-debug: + DEFMT_LOG=trace cargo build --release --no-default-features --features embassy,defmt + +# Flash the defmt firmware via the attached SWD probe and tail RTT logs. +# Plug the probe into the four SWD pads on the back of the Xiao +# (SWCLK + SWDIO + GND — leave 3V3 disconnected so USB-C powers the board). +# The Xiao's own USB-C can stay plugged into a separate host the whole time; +# the SWD path and the USB path are independent. +debug: build-debug + probe-rs run --chip {{ chip }} {{ out_release }} + +# Just tail RTT from a board that's already running the defmt firmware +# (no flash). Useful for rejoining a session after Ctrl-C without a reset. +attach: + probe-rs attach --chip {{ chip }} {{ out_release }} + +# List attached debug probes — sanity check that the DAPLink shows up. +probes: + probe-rs list + # Install the toolchain and cargo helpers declared in mise.toml. bootstrap: mise install diff --git a/mise.toml b/mise.toml index 825b4bf..73b8e30 100644 --- a/mise.toml +++ b/mise.toml @@ -7,6 +7,7 @@ just = "latest" "cargo:cargo-watch" = "latest" "cargo:cargo-bloat" = "latest" "cargo:cargo-expand" = "latest" +"cargo:probe-rs-tools" = "latest" [env] CARGO_TARGET_DIR = "target" diff --git a/src/main.rs b/src/main.rs index 19307c3..097b903 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,10 +24,13 @@ use embassy_usb::{ }; use hsmc::{Duration, statechart}; use libm::{cosf, powf, roundf, sinf}; +#[cfg(not(feature = "defmt"))] use panic_reset as _; use smart_leds::RGB8; use static_cell::StaticCell; use usbd_hid::descriptor::{KeyboardReport, MouseReport, SerializedDescriptor}; +#[cfg(feature = "defmt")] +use {defmt_rtt as _, panic_probe as _}; bind_interrupts!(struct Irqs { USBCTRL_IRQ => UsbInterruptHandler; @@ -291,11 +294,23 @@ impl JigglyActions for JigglyActionContext<'_> { } async fn keyboard_wake(&mut self) { - let _ = embassy_futures::select::select( + let result = embassy_futures::select::select( wake_with_keyboard(&mut self.kbd), Timer::after(KBD_WAKE_DEADLINE), ) .await; + #[cfg(feature = "defmt")] + match result { + embassy_futures::select::Either::First(_) => defmt::info!("kbd wake: completed"), + embassy_futures::select::Either::Second(_) => { + defmt::warn!( + "kbd wake: deadline hit ({} ms)", + KBD_WAKE_DEADLINE.as_millis() + ) + } + } + #[cfg(not(feature = "defmt"))] + let _ = result; // Belt-and-suspenders: always send an all-keys-released report, // even if the deadline preempted the loop *between* a key-down // and its key-up. Without this, a stuck modifier (Shift!) or key @@ -310,11 +325,21 @@ impl JigglyActions for JigglyActionContext<'_> { } async fn mouse_wake(&mut self) { - let _ = embassy_futures::select::select( + let result = embassy_futures::select::select( wake_with_mouse(&mut self.mouse), Timer::after(MOUSE_WAKE_DEADLINE), ) .await; + #[cfg(feature = "defmt")] + match result { + embassy_futures::select::Either::First(_) => defmt::info!("mouse wake: completed"), + embassy_futures::select::Either::Second(_) => defmt::warn!( + "mouse wake: deadline hit ({} ms)", + MOUSE_WAKE_DEADLINE.as_millis() + ), + } + #[cfg(not(feature = "defmt"))] + let _ = result; } async fn jiggle_pair(&mut self) { @@ -323,6 +348,8 @@ impl JigglyActions for JigglyActionContext<'_> { } else { (0, 1) }; + #[cfg(feature = "defmt")] + defmt::info!("jiggle: dx={} dy={}", dx, dy); send_mouse(&mut self.mouse, dx, dy).await; Timer::after(PIXEL_DWELL).await; send_mouse(&mut self.mouse, -dx, -dy).await; @@ -649,7 +676,14 @@ async fn watchdog_task(mut wd: Watchdog) -> ! { async fn main(spawner: Spawner) { let p = embassy_rp::init(Default::default()); + #[cfg(feature = "defmt")] + defmt::info!("jiggly v{} boot", env!("CARGO_PKG_VERSION")); + let mut watchdog = Watchdog::new(p.WATCHDOG); + // Pause the countdown while a debugger has the core halted, so + // breakpoints and single-stepping under `probe-rs` don't trip the + // 8 s reset. Cheap and always-correct, so leave it on for release too. + watchdog.pause_on_debug(true); watchdog.start(WATCHDOG_TIMEOUT); // NeoPixel: GPIO11 powers it, GPIO12 is the WS2812 data line driven from @@ -679,7 +713,10 @@ async fn main(spawner: Spawner) { let mut config = UsbConfig::new(0x1209, 0xb0b0); config.manufacturer = Some("swaits.com"); config.product = Some("jiggly"); - config.serial_number = Some(make_serial(p.FLASH)); + let serial = make_serial(p.FLASH); + #[cfg(feature = "defmt")] + defmt::info!("usb serial: {}", serial); + config.serial_number = Some(serial); config.device_release = 0x0200; // matches firmware version 0.2.0 config.max_power = 100; config.max_packet_size_0 = 64; @@ -731,6 +768,9 @@ async fn main(spawner: Spawner) { spawner.spawn(usb_task(usb).unwrap()); spawner.spawn(watchdog_task(watchdog).unwrap()); + #[cfg(feature = "defmt")] + defmt::info!("usb + watchdog tasks spawned, starting statechart"); + static EVENT_CHAN: Channel = Channel::new(); let ctx = Ctx {