From fcf95f44e9095ef1716bdb8fb81b11457e98cb95 Mon Sep 17 00:00:00 2001 From: wieerwill Date: Fri, 3 Jul 2026 12:59:18 +0200 Subject: [PATCH] Initial commit --- .cargo/config.toml | 9 + .gitignore | 7 + Cargo.lock | 906 +++++++ Cargo.toml | 16 + README.md | 95 + build.rs | 7 + docs/blue-pill.md | 42 + ...on-stm32_jeutter_1154375_feedback-code.png | Bin 0 -> 26934 bytes docs/troubleshooting.md | 47 + examples.md | 1748 +++++++++++++ flake.lock | 96 + flake.nix | 85 + memory.x | 7 + presentation.html | 2318 +++++++++++++++++ rust-toolchain.toml | 5 + src/bin/00_check.rs | 9 + src/bin/01_rust_basics.rs | 50 + src/bin/02_ownership.rs | 85 + src/bin/03_types.rs | 152 ++ src/bin/04_first_firmware.rs | 25 + src/bin/05_rgb_output.rs | 73 + src/bin/06_button_events.rs | 111 + src/bin/07_events.rs | 91 + src/bin/08_state_machine.rs | 240 ++ src/bin/09_sensor_pipeline.rs | 208 ++ src/bin/10_motor_commands.rs | 179 ++ src/bin/11_kinematics.rs | 192 ++ src/bin/12_embassy.rs | 65 + verify-all.sh | 31 + workshop.md | 1426 ++++++++++ 30 files changed, 8325 insertions(+) create mode 100644 .cargo/config.toml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 build.rs create mode 100644 docs/blue-pill.md create mode 100644 docs/rust-on-robots-hands-on-embedded-rust-on-stm32_jeutter_1154375_feedback-code.png create mode 100644 docs/troubleshooting.md create mode 100644 examples.md create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 memory.x create mode 100644 presentation.html create mode 100644 rust-toolchain.toml create mode 100644 src/bin/00_check.rs create mode 100644 src/bin/01_rust_basics.rs create mode 100644 src/bin/02_ownership.rs create mode 100644 src/bin/03_types.rs create mode 100644 src/bin/04_first_firmware.rs create mode 100644 src/bin/05_rgb_output.rs create mode 100644 src/bin/06_button_events.rs create mode 100644 src/bin/07_events.rs create mode 100644 src/bin/08_state_machine.rs create mode 100644 src/bin/09_sensor_pipeline.rs create mode 100644 src/bin/10_motor_commands.rs create mode 100644 src/bin/11_kinematics.rs create mode 100644 src/bin/12_embassy.rs create mode 100644 verify-all.sh create mode 100644 workshop.md diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..a7bc834 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,9 @@ +[target.thumbv7m-none-eabi] +linker = "flip-link" +rustflags = [ + "-C", + "link-arg=-Tlink.x", +] + +[env] +DEFMT_LOG = "info" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d41448 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +target/ +result +.direnv/ +.DS_Store +*.log +Embed.local.* +.embed.local.* diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..d8d8bfa --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,906 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bare-metal" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5deb64efa5bd81e31fcd1938615a6d98c82eafcbcd787162b6f63b91d6bac5b3" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitfield" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "block-device-driver" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c051592f59fe68053524b4c4935249b806f72c1f544cfb7abe4f57c3be258e" +dependencies = [ + "aligned", +] + +[[package]] +name = "bxcan" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3110c36f496cf7110ab17c48ad714225330862101ec30197a9898006cd3e2862" +dependencies = [ + "bitflags 1.3.2", + "embedded-can", + "nb 1.1.0", + "vcell", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cortex-m" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ec610d8f49840a5b376c69663b6369e71f4b34484b9b2eb29fb918d92516cb9" +dependencies = [ + "bare-metal", + "bitfield", + "critical-section", + "embedded-hal 0.2.7", + "volatile-register", +] + +[[package]] +name = "cortex-m-rt" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801d4dec46b34c299ccf6b036717ae0fce602faa4f4fe816d9013b9a7c9f5ba6" +dependencies = [ + "cortex-m-rt-macros", +] + +[[package]] +name = "cortex-m-rt-macros" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e37549a379a9e0e6e576fd208ee60394ccb8be963889eebba3ffe0980364f472" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "cortex-m-semihosting" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c23234600452033cc77e4b761e740e02d2c4168e11dbf36ab14a0f58973592b0" +dependencies = [ + "cortex-m", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "embassy-embedded-hal" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0641612053b2f34fc250bb63f6630ae75de46e02ade7f457268447081d709ce" +dependencies = [ + "embassy-futures", + "embassy-hal-internal 0.4.0", + "embassy-sync", + "embassy-time", + "embedded-hal 0.2.7", + "embedded-hal 1.0.0", + "embedded-hal-async", + "embedded-storage", + "embedded-storage-async", + "nb 1.1.0", +] + +[[package]] +name = "embassy-executor" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06070468370195e0e86f241c8e5004356d696590a678d47d6676795b2e439c6b" +dependencies = [ + "cortex-m", + "critical-section", + "document-features", + "embassy-executor-macros", + "embassy-executor-timer-queue", +] + +[[package]] +name = "embassy-executor-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfdddc3a04226828316bf31393b6903ee162238576b1584ee2669af215d55472" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "embassy-executor-timer-queue" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc328bf943af66b80b98755db9106bf7e7471b0cf47dc8559cd9a6be504cc9c" + +[[package]] +name = "embassy-futures" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc2d050bdc5c21e0862a89256ed8029ae6c290a93aecefc73084b3002cdebb01" + +[[package]] +name = "embassy-hal-internal" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f10ce10a4dfdf6402d8e9bd63128986b96a736b1a0a6680547ed2ac55d55dba" +dependencies = [ + "num-traits", +] + +[[package]] +name = "embassy-hal-internal" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "568659fc53866d3d85c60fa33723fb751aa69e71507634fc2c19e7649432fb75" +dependencies = [ + "cortex-m", + "critical-section", + "num-traits", +] + +[[package]] +name = "embassy-net-driver" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524eb3c489760508f71360112bca70f6e53173e6fe48fc5f0efd0f5ab217751d" + +[[package]] +name = "embassy-stm32" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486c0622deb5a519fc4d2cb8e3ef324f7568fcdfff201ff8fcab46557d663ceb" +dependencies = [ + "aligned", + "bit_field", + "bitflags 2.13.0", + "block-device-driver", + "cfg-if", + "cortex-m", + "cortex-m-rt", + "critical-section", + "document-features", + "embassy-embedded-hal", + "embassy-futures", + "embassy-hal-internal 0.5.0", + "embassy-net-driver", + "embassy-sync", + "embassy-time", + "embassy-time-driver", + "embassy-time-queue-utils", + "embassy-usb-driver", + "embassy-usb-synopsys-otg", + "embedded-can", + "embedded-hal 0.2.7", + "embedded-hal 1.0.0", + "embedded-hal-async", + "embedded-hal-nb", + "embedded-io 0.7.1", + "embedded-io-async 0.7.0", + "embedded-storage", + "embedded-storage-async", + "futures-util", + "heapless 0.9.3", + "nb 1.1.0", + "proc-macro2", + "quote", + "rand_core 0.6.4", + "rand_core 0.9.5", + "regex", + "sdio-host", + "static_assertions", + "stm32-fmc", + "stm32-metapac", + "trait-set", + "vcell", + "volatile-register", +] + +[[package]] +name = "embassy-sync" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bbd85cf5a5ae56bdf26f618364af642d1d0a4e245cdd75cd9aabda382f65a81" +dependencies = [ + "cfg-if", + "critical-section", + "embedded-io-async 0.7.0", + "futures-core", + "futures-sink", + "heapless 0.9.3", +] + +[[package]] +name = "embassy-time" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "592b0c143ec626e821d4d90da51a2bd91d559d6c442b7c74a47d368c9e23d97a" +dependencies = [ + "cfg-if", + "critical-section", + "document-features", + "embassy-time-driver", + "embedded-hal 0.2.7", + "embedded-hal 1.0.0", + "embedded-hal-async", + "futures-core", +] + +[[package]] +name = "embassy-time-driver" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ee71af1b3a0deaa53eaf2d39252f83504c853646e472400b763060389b9fcc9" +dependencies = [ + "document-features", +] + +[[package]] +name = "embassy-time-queue-utils" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168297bf80aaf114b3c9ad589bf38b01b3009b9af7f97cd18086c5bbf96f5693" +dependencies = [ + "embassy-executor-timer-queue", + "heapless 0.9.3", +] + +[[package]] +name = "embassy-usb-driver" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa675c5f4349b6aa0fcffc4bf9b241f18cd11b97c1f8323273fb9a5449937fbd" +dependencies = [ + "embedded-io-async 0.6.1", + "embedded-io-async 0.7.0", +] + +[[package]] +name = "embassy-usb-synopsys-otg" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb205e26d59e483e8c48f91f637ec217ec7e2cfb0b61d50482301b991b4ff431" +dependencies = [ + "critical-section", + "embassy-sync", + "embassy-time", + "embassy-usb-driver", + "portable-atomic", +] + +[[package]] +name = "embedded-can" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d2e857f87ac832df68fa498d18ddc679175cf3d2e4aa893988e5601baf9438" +dependencies = [ + "nb 1.1.0", +] + +[[package]] +name = "embedded-dma" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "994f7e5b5cb23521c22304927195f236813053eb9c065dd2226a32ba64695446" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "embedded-hal" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff" +dependencies = [ + "nb 0.1.3", + "void", +] + +[[package]] +name = "embedded-hal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" + +[[package]] +name = "embedded-hal-async" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884" +dependencies = [ + "embedded-hal 1.0.0", +] + +[[package]] +name = "embedded-hal-nb" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba4268c14288c828995299e59b12babdbe170f6c6d73731af1b4648142e8605" +dependencies = [ + "embedded-hal 1.0.0", + "nb 1.1.0", +] + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "embedded-io" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eb1aa714776b75c7e67e1da744b81a129b3ff919c8712b5e1b32252c1f07cc7" + +[[package]] +name = "embedded-io-async" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff09972d4073aa8c299395be75161d582e7629cd663171d62af73c8d50dba3f" +dependencies = [ + "embedded-io 0.6.1", +] + +[[package]] +name = "embedded-io-async" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2564b9f813c544241430e147d8bc454815ef9ac998878d30cc3055449f7fd4c0" +dependencies = [ + "embedded-io 0.7.1", +] + +[[package]] +name = "embedded-storage" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21dea9854beb860f3062d10228ce9b976da520a73474aed3171ec276bc0c032" + +[[package]] +name = "embedded-storage-async" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1763775e2323b7d5f0aa6090657f5e21cfa02ede71f5dc40eead06d64dcd15cc" +dependencies = [ + "embedded-storage", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "fugit" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e639847d312d9a82d2e75b0edcc1e934efcc64e6cb7aa94f0b1fbec0bc231d6" +dependencies = [ + "gcd", +] + +[[package]] +name = "fugit-timer" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9607bfc4c388f9d629704f56ede4a007546cad417b3bcd6fc7c87dc7edce04a" +dependencies = [ + "fugit", + "nb 1.1.0", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", +] + +[[package]] +name = "gcd" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d758ba1b47b00caf47f24925c0074ecb20d6dfcffe7f6d53395c0465674841a" + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heapless" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "nb" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f" +dependencies = [ + "nb 1.1.0", +] + +[[package]] +name = "nb" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "panic-probe" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd402d00b0fb94c5aee000029204a46884b1262e0c443f166d86d2c0747e1a1a" +dependencies = [ + "cortex-m", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rust-stm32" +version = "0.1.0" +dependencies = [ + "cortex-m", + "cortex-m-rt", + "cortex-m-semihosting", + "embassy-executor", + "embassy-stm32", + "embassy-time", + "panic-probe", + "stm32f1xx-hal", +] + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + +[[package]] +name = "sdio-host" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b328e2cb950eeccd55b7f55c3a963691455dcd044cfb5354f0c5e68d2c2d6ee2" + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stm32-fmc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72692594faa67f052e5e06dd34460951c21e83bc55de4feb8d2666e2f15480a2" +dependencies = [ + "embedded-hal 1.0.0", +] + +[[package]] +name = "stm32-metapac" +version = "21.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e74b78632cea498cfb28386a29f8bfae7476d6570a78733eb5fecbee66c2f4ce" +dependencies = [ + "cortex-m", + "cortex-m-rt", +] + +[[package]] +name = "stm32-usbd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e633d559b5fe03caed24e1df85c9fa3916fe6191fc7099ac6fcb0281f380e9f0" +dependencies = [ + "cortex-m", + "usb-device", + "vcell", +] + +[[package]] +name = "stm32f1" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f2b32d5afdbd179dbfa05ad835e4623fb3790c187c2a8d6301eff1507b2c6c9" +dependencies = [ + "cortex-m", + "cortex-m-rt", + "critical-section", + "portable-atomic", + "vcell", +] + +[[package]] +name = "stm32f1xx-hal" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f15ad470c8df998f9ef3a28dd625c7696179f091122235a971266999f8b0a66" +dependencies = [ + "bitflags 1.3.2", + "bxcan", + "cortex-m", + "cortex-m-rt", + "embedded-dma", + "embedded-hal 0.2.7", + "embedded-hal 1.0.0", + "embedded-hal-nb", + "embedded-io 0.6.1", + "fugit", + "fugit-timer", + "nb 1.1.0", + "stm32-usbd", + "stm32f1", + "vcell", + "void", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "trait-set" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79e2e9c9ab44c6d7c20d5976961b47e8f49ac199154daa514b77cd1ab536625" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "usb-device" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98816b1accafbb09085168b90f27e93d790b4bfa19d883466b5e53315b5f06a6" +dependencies = [ + "heapless 0.8.0", + "portable-atomic", +] + +[[package]] +name = "vcell" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "volatile-register" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de437e2a6208b014ab52972a27e59b33fa2920d3e00fe05026167a1c509d19cc" +dependencies = [ + "vcell", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..067b3a6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "rust-stm32" +version = "0.1.0" +edition = "2024" +publish = false +build = "build.rs" + +[target.'cfg(target_arch = "arm")'.dependencies] +cortex-m = { version = "=0.7.7", features = ["critical-section-single-core"] } +cortex-m-rt = "=0.7.5" +cortex-m-semihosting = "=0.5.0" +embassy-executor = { version = "=0.9.1", features = ["arch-cortex-m", "executor-thread"] } +embassy-stm32 = { version = "=0.6.0", features = ["memory-x", "stm32f103c8", "time-driver-any"] } +embassy-time = "=0.5.1" +panic-probe = "=1.0.0" +stm32f1xx-hal = { version = "=0.11.0", features = ["medium", "stm32f103"] } diff --git a/README.md b/README.md new file mode 100644 index 0000000..27606bf --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +# Rust on STM32 Blue Pill + +This repository is a hands-on Rust and robotics workshop for the STM32F103C8T6 Blue Pill. +The workshop builds the software shape of a small soccer robot step by step: + +## Hardware Baseline + +- Board: STM32F103C8T6 Blue Pill +- Core: Cortex-M3 +- Default target: `thumbv7m-none-eabi` +- Default `probe-rs` chip name: `STM32F103C8` +- Onboard LED: usually `PC13`, active-low on most Blue Pill boards + +See [docs/blue-pill.md](docs/blue-pill.md) for wiring notes. + +## Workshop Chapters + +- `00_check` - toolchain and sanity check +- `01_rust_basics` - values, functions, mutation +- `02_ownership` - ownership and borrowing +- `03_types` - domain types, enums and `Result` +- `04_first_firmware` - first STM32 firmware loop +- `05_rgb_output` - RGB LED status output +- `06_button_events` - 5-way button as fake sensor rig +- `07_events` - event priority and safety-first interpretation +- `08_state_machine` - robot state machine and status LED +- `09_sensor_pipeline` - fake camera and ultrasonic pipeline +- `10_motor_commands` - motion intent to wheel speeds +- `11_kinematics` - geometry and inverse kinematics +- `12_embassy` - async Embassy loop and timers + + +## Start With Nix + +```bash +nix develop +``` + +The shell provides Rust, `probe-rs`, `flip-link`, and aliases for the workshop flow. + +Typical first commands: + +```bash +fmt +check +verify +run-00 +run-01 +run-02 +run-03 +``` + +## Start With Rustup + +If you do not want Nix, use the pinned toolchain directly: + +```bash +rustup toolchain install 1.95.0 +rustup target add thumbv7m-none-eabi +``` + +You also need `flip-link` and `probe-rs` on your `PATH` for embedded builds and flashing. + +Then run the host chapter bins with Cargo: + +```bash +cargo run --bin 00_check +cargo run --bin 01_rust_basics +cargo run --bin 02_ownership +cargo run --bin 03_types +``` + +For the Blue Pill chapters: + +```bash +cargo build --target thumbv7m-none-eabi --bin 04_first_firmware +probe-rs run --chip STM32F103C8 target/thumbv7m-none-eabi/debug/04_first_firmware +cargo build --target thumbv7m-none-eabi --bin 12_embassy +probe-rs run --chip STM32F103C8 target/thumbv7m-none-eabi/debug/12_embassy +``` + +## Probe Wiring + +Typical SWD wiring: + +- `SWDIO` -> `SWDIO` +- `SWCLK` -> `SWCLK` +- `GND` -> `GND` +- `3V3` -> `3V3` reference + +Keep `BOOT0` low for normal flash-and-run. + +## Troubleshooting + +See [docs/troubleshooting.md](docs/troubleshooting.md). diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..60674d7 --- /dev/null +++ b/build.rs @@ -0,0 +1,7 @@ +fn main() { + println!("cargo:rerun-if-changed=memory.x"); + println!( + "cargo:rustc-link-search={}", + std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).display() + ); +} diff --git a/docs/blue-pill.md b/docs/blue-pill.md new file mode 100644 index 0000000..a89fed8 --- /dev/null +++ b/docs/blue-pill.md @@ -0,0 +1,42 @@ +# Blue Pill Notes + +## Baseline + +- Board: STM32F103C8T6 Blue Pill +- Core: Cortex-M3 +- Default target: `thumbv7m-none-eabi` + +## LED + +- Most Blue Pill boards wire the onboard LED to `PC13` +- It is usually active-low +- `set_low()` usually turns it on +- `set_high()` usually turns it off + +## RGB LED + +- `PA0` = GND, drive low +- `PA1` = red +- `PA2` = green +- `PA3` = blue +- The RGB examples use these pins as a visible status output + +## 5-Way Button Board + +- `PB12` = GND, drive low +- `PA10` = VCC, drive high +- `PB13` = right +- `PB14` = down +- `PB15` = left +- `PA8` = center +- `PA9` = up +- Assumption: the button inputs read high when pressed +- The workshop uses `PA8` as the default center button in the simplest examples +- TODO: flip the input polarity if your module is active-low instead + +## Probe Wiring + +- `SWDIO` -> `SWDIO` +- `SWCLK` -> `SWCLK` +- `GND` -> `GND` +- `3V3` -> `3V3` reference diff --git a/docs/rust-on-robots-hands-on-embedded-rust-on-stm32_jeutter_1154375_feedback-code.png b/docs/rust-on-robots-hands-on-embedded-rust-on-stm32_jeutter_1154375_feedback-code.png new file mode 100644 index 0000000000000000000000000000000000000000..b89e67e40d2d7bff8bca4877262b58bb6e6496a2 GIT binary patch literal 26934 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 build -> run -> observe result +``` + +For embedded work this later becomes: + +```text +edit code -> build -> flash -> observe hardware +``` + +## Code behaviour + +The host-side program should print a short confirmation message: + +```text +Rust on Robots toolchain check +Host program runs correctly +Next step: build the embedded firmware +``` + +If useful, also print the selected workshop target/chip as a constant string. + +## Participant tasks + +Participants should: + +1. Run the example. +2. Confirm that the output appears. +3. Change one printed line. +4. Run it again. + +## Suggested command + +```text +cargo run --bin 00_check +``` + +## Expected result + +The program compiles and prints the configured sanity-check message. + +## Implementation notes for the code bot + +- Keep this example host-side and `std`-based. +- No STM32 code here. +- No complicated dependencies. +- Use simple `println!` output. +- This file should be nearly impossible to fail except for toolchain setup problems. + +## Acceptance criteria + +- `cargo run --bin 00_check` works. +- The output is obvious and friendly. +- Participants can change one string and rerun it. + +--- + +# 01_rust_basics.rs — Values, functions and visible mutation + +## Purpose + +This example introduces only the Rust basics needed for the rest of the workshop: + +- values +- immutable-by-default variables +- `mut` +- functions +- simple structs or enums only if helpful + +It should not become a full Rust introduction. + +## Conceptual role in the robot journey + +Robot software should not mutate everything from everywhere. + +This example introduces the idea that values are stable by default and mutation must be intentional. + +```text +sensor reading -> event -> state transition -> command +``` + +That model only works if state changes are controlled. + +## Code behaviour + +The program should model a tiny status calculation, not generic arithmetic. + +Example behaviour: + +- Start with a fixed `current_speed`. +- Compute a `target_speed` with a function. +- Print both values. +- Show one intentionally mutable variable, such as `status_message` or `target_speed`. + +Possible output: + +```text +current speed: 0 +selected target speed: 40 +status: searching +``` + +## Suggested code shape + +The code should include: + +```rust +fn select_target_speed(searching: bool) -> i32 { + if searching { + 40 + } else { + 0 + } +} +``` + +Then in `main`: + +```rust +let current_speed = 0; +let mut target_speed = select_target_speed(true); + +target_speed = target_speed + 5; +``` + +The participant should see where mutation happens. + +## Participant tasks + +Participants should: + +1. Run the example. +2. Change the search speed. +3. Add a second function, for example `select_status_name`. +4. Change one immutable variable and observe the compiler error. +5. Decide whether the variable should actually be mutable. + +## Suggested command + +```text +cargo run --bin 01_rust_basics +``` + +## Expected result + +The participant sees how Rust separates stable values from mutable values. + +## Implementation notes for the code bot + +- Keep the code host-side. +- Use `println!` for feedback. +- Include one commented-out line that would fail because it tries to mutate an immutable value. +- Avoid ownership examples here; that belongs to the next chapter. + +## Acceptance criteria + +- The example compiles in the baseline state. +- Participants can modify a constant or function return value. +- There is one clear optional compiler-error experiment about mutation. + +--- + +# 02_ownership.rs — Ownership and borrowing as resource responsibility + +## Purpose + +This example introduces ownership and borrowing through robot commands. + +It should teach: + +- moving a value +- borrowing a value with `&T` +- mutably borrowing a value with `&mut T` +- why this matters for hardware resources and robot state + +## Conceptual role in the robot journey + +A robot controller should not have five different parts silently changing the same command or peripheral. + +Ownership answers: + +```text +Who is responsible for this value or resource? +``` + +Borrowing answers: + +```text +Who may temporarily inspect or modify it? +``` + +## Code behaviour + +The program should define a simple robot command type: + +```rust +#[derive(Debug)] +struct MotionCommand { + forward: f32, + strafe: f32, + rotate: f32, +} +``` + +The program should then: + +1. Create a command. +2. Print it by borrowing it. +3. Modify it through a mutable borrow. +4. Print it again. + +## Suggested code shape + +Include functions like: + +```rust +fn print_command(command: &MotionCommand) { + println!("command = {command:?}"); +} + +fn stop(command: &mut MotionCommand) { + command.forward = 0.0; + command.strafe = 0.0; + command.rotate = 0.0; +} +``` + +Also include a commented-out move example: + +```rust +// let next_command = command; +// print_command(&command); // This would no longer compile after the move. +``` + +## Participant tasks + +Participants should: + +1. Run the baseline. +2. Uncomment the move example and read the compiler error. +3. Revert it. +4. Add a function `rotate_left(command: &mut MotionCommand)`. +5. Call it before `stop`. +6. Print the command after each step. + +## Suggested command + +```text +cargo run --bin 02_ownership +``` + +## Expected result + +Participants should understand ownership as responsibility, not as abstract syntax. + +## Implementation notes for the code bot + +- Keep this host-side. +- Use `#[derive(Debug, Clone, Copy)]` only if needed, but avoid hiding ownership too early. +- For the first version, do not make `MotionCommand` `Copy`; the move error is useful. +- If the example gets too frustrating, a later comment may explain when `Copy` would be acceptable. + +## Acceptance criteria + +- Baseline compiles and prints command changes. +- There is a safe mutable-borrow update function. +- There is one optional ownership error experiment. + +--- + +# 03_types.rs — Domain types, enums and Result + +## Purpose + +This example shows how Rust types can model robot concepts better than raw numbers and booleans. + +It should teach: + +- newtype structs for units +- enums for meaningful alternatives +- `Result` for sensor failure +- `match` for explicit handling + +## Conceptual role in the robot journey + +Robots often fail because raw values are treated as meaning. + +Bad: + +```text +42 +``` + +Better: + +```text +DistanceCm(42) +BallObservation::Left +SensorError::Timeout +``` + +## Code behaviour + +The program should simulate reading a fake ultrasonic sensor and a fake camera result. + +It should: + +1. Convert a raw echo value into `DistanceCm`. +2. Classify distance as clear, warning or too close. +3. Represent ball position with `BallObservation`. +4. Print the resulting interpretation. + +## Suggested types + +```rust +#[derive(Debug, Clone, Copy, PartialEq)] +struct RawEchoMicros(u32); + +#[derive(Debug, Clone, Copy, PartialEq)] +struct DistanceCm(u16); + +#[derive(Debug, Clone, Copy, PartialEq)] +enum SensorError { + Timeout, + OutOfRange, + NotCalibrated, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum ObstacleObservation { + Clear, + Warning, + TooClose, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum BallObservation { + NotSeen, + Left, + Center, + Right, + TooClose, +} +``` + +## Suggested functions + +```rust +fn echo_to_distance(raw: RawEchoMicros) -> Result { + if raw.0 == 0 { + return Err(SensorError::Timeout); + } + + let cm = raw.0 / 58; + + if cm > 400 { + Err(SensorError::OutOfRange) + } else { + Ok(DistanceCm(cm as u16)) + } +} + +fn classify_obstacle(distance: DistanceCm) -> ObstacleObservation { + match distance.0 { + 0..=15 => ObstacleObservation::TooClose, + 16..=35 => ObstacleObservation::Warning, + _ => ObstacleObservation::Clear, + } +} +``` + +## Participant tasks + +Participants should: + +1. Run the example. +2. Change the fake echo value. +3. Observe how the classification changes. +4. Add a new `BallObservation` variant, for example `FarAway`. +5. Add or update one `match` arm. +6. Trigger a fake sensor error and handle it. + +## Suggested command + +```text +cargo run --bin 03_types +``` + +## Expected result + +Participants should see that type design is robot design. + +## Implementation notes for the code bot + +- Keep this host-side. +- Add at least two sample raw echo values. +- Print both successful and failing cases. +- Use `match`, not `unwrap`, so error handling is visible. + +## Acceptance criteria + +- Baseline compiles and prints at least one valid distance classification. +- Baseline demonstrates one `Err` path. +- Participants can change thresholds and variants easily. + +--- + +# 04_first_firmware.rs — First STM32 firmware + +## Purpose + +This is the first embedded example. It introduces the structure of a minimal STM32 firmware program. + +It should teach: + +- embedded firmware is not a desktop application +- `no_std` means no normal standard library +- the program initializes hardware and then stays in a loop +- flashing is part of the development loop + +## Conceptual role in the robot journey + +Before a robot can read sensors or drive motors, the firmware must boot reliably. + +This chapter establishes the embedded loop: + +```text +init hardware -> loop forever -> observe behaviour +``` + +## Code behaviour + +The program should: + +1. Start on the STM32. +2. Initialize the minimal required runtime/HAL setup. +3. Enter an endless loop and keep the setup minimal. + +There does not need to be visible LED output yet. That comes in the next chapter. + +## Participant tasks + +Participants should: + +1. Build the firmware. +2. Flash it to the STM32. +3. Confirm that flashing succeeds. +4. Confirm that the firmware reaches the loop. + +## Suggested command + +```text +build-04 +``` + +## Suggested run + +```text +run-04 +``` + +## Rustup fallback + +```text +cargo build --target thumbv7m-none-eabi --bin 04_first_firmware +probe-rs run --chip STM32F103C8 target/thumbv7m-none-eabi/debug/04_first_firmware +``` + +## Expected result + +The binary builds for the embedded target and can be flashed to the board. + +## Implementation notes for the code bot + +- This file is embedded-target code. +- Use the HAL/runtime style already selected for the repo. +- Keep initialization minimal. +- Do not introduce LED or button logic here. +- Keep the loop empty or insert a `cortex_m::asm::nop()`. + +## Acceptance criteria + +- The firmware builds for STM32. +- The firmware flashes successfully with the selected command. +- The code clearly shows the initialization and endless loop structure. + +--- + +# 05_rgb_output.rs — RGB LED as actuator and status output + +## Purpose + +This example introduces output control. + +The RGB LED is treated as a real actuator and status display, not as a toy. + +It should teach: + +- configure GPIO as output +- set pins high/low +- use timing/delay +- represent robot state visibly + +## Conceptual role in the robot journey + +A robot needs visible status. + +Without a screen or logs, a status LED can show: + +```text +Idle +Searching +Driving +Obstacle +Error +``` + +This is useful during real floor debugging. + +## Code behaviour + +The firmware should blink or switch the RGB LED according to a status pattern. + +Recommended mapping: + +```text +Off -> Idle +Blue blink -> SearchBall +Green -> DriveToBall +Yellow -> AlignToBall +Red blink -> AvoidObstacle or Error +``` + +The baseline should at least implement two states: + +```text +Idle +SearchBall +``` + +## Suggested types + +```rust +#[derive(Debug, Clone, Copy, PartialEq)] +enum RobotMode { + Idle, + SearchBall, +} +``` + +A helper function should map mode to LED output: + +```rust +fn show_status(mode: RobotMode /* plus LED pins */) { + // Set RGB pins according to mode. +} +``` + +The exact signature depends on the HAL pin types. + +## Participant tasks + +Participants should: + +1. Flash the baseline. +2. Confirm the LED behaviour. +3. Change the blink timing. +4. Add a second or third status pattern. +5. Change the mode in code and observe the LED. + +## Suggested command + +```text +run-05 +``` + +## Rustup fallback + +```text +cargo build --target thumbv7m-none-eabi --bin 05_rgb_output +probe-rs run --chip STM32F103C8 target/thumbv7m-none-eabi/debug/05_rgb_output +``` + +## Expected result + +Participants can see that firmware controls physical output. + +## Implementation notes for the code bot + +- This file is embedded-target code. +- The actual RGB LED pin mapping must match the workshop hardware. +- Define constants or comments for pin mapping. +- Keep active-high vs active-low behaviour explicit. +- Avoid hiding all pin operations behind too much abstraction; participants should still see what is happening. + +## Acceptance criteria + +- The firmware flashes. +- The LED visibly changes state. +- Participants can change timing or status mapping in one obvious place. + +--- + +# 06_button_events.rs — Button input as fake sensor rig + +## Purpose + +This example introduces input. + +The 5-way button is used as a proxy for real robot perception: + +```text +Center -> start / stop +Left -> ball left +Right -> ball right +Up -> ball centered +Down -> obstacle detected +``` + +It should teach: + +- configure GPIO as input +- read button state +- map raw hardware state to a domain event +- separate hardware input from robot meaning + +## Conceptual role in the robot journey + +In the real robot, `BallLeft` may come from a camera pipeline. + +In the workshop, it comes from a button. + +The controller should not care. + +```text +button direction -> RobotEvent +camera result -> RobotEvent +simulator input -> RobotEvent +``` + +Same interface, different source. + +## Code behaviour + +The firmware should: + +1. Read the 5-way button. +2. Convert the pressed direction to `RobotEvent`. +3. Show feedback through LED or logs. +4. Keep a neutral `NoEvent` state when nothing is pressed. + +## Suggested types + +```rust +#[derive(Debug, Clone, Copy, PartialEq)] +enum ButtonDirection { + None, + Center, + Up, + Down, + Left, + Right, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum RobotEvent { + NoEvent, + StartStop, + BallLeft, + BallRight, + BallCentered, + ObstacleDetected, +} +``` + +## Suggested function + +```rust +fn direction_to_event(direction: ButtonDirection) -> RobotEvent { + match direction { + ButtonDirection::None => RobotEvent::NoEvent, + ButtonDirection::Center => RobotEvent::StartStop, + ButtonDirection::Left => RobotEvent::BallLeft, + ButtonDirection::Right => RobotEvent::BallRight, + ButtonDirection::Up => RobotEvent::BallCentered, + ButtonDirection::Down => RobotEvent::ObstacleDetected, + } +} +``` + +## Participant tasks + +Participants should: + +1. Flash the baseline. +2. Press each button direction. +3. Observe LED/log feedback. +4. Change the mapping, for example swap left and right. +5. Add a new event such as `BallLost` if the hardware interaction allows it. + +## Suggested command + +```text +run-06 +``` + +## Rustup fallback + +```text +cargo build --target thumbv7m-none-eabi --bin 06_button_events +probe-rs run --chip STM32F103C8 target/thumbv7m-none-eabi/debug/06_button_events +``` + +## Expected result + +Participants understand that raw input must be interpreted before robot logic uses it. + +## Implementation notes for the code bot + +- This file is embedded-target code. +- The button pin mapping must match the workshop hardware. +- If the button hardware is analog or multiplexed, keep the reading logic isolated in one function. +- If debounce is implemented, keep it simple and visible. +- If debounce is not implemented, mention in a comment that mechanical inputs may bounce. + +## Acceptance criteria + +- Each button direction maps to a visible or logged event. +- `NoEvent` is handled explicitly. +- The mapping function is easy to modify. + +--- + +# 07_events.rs — Event priority and safety-first interpretation + +## Purpose + +This example moves from single input events to event selection and priority. + +It should teach: + +- not all events are equally important +- safety-relevant events should win +- event processing can be tested without hardware + +## Conceptual role in the robot journey + +A soccer robot may see the ball and detect an obstacle at the same time. + +The robot should not blindly drive forward because the ball is centered. + +```text +BallCentered + ObstacleDetected -> Stop / AvoidObstacle +``` + +## Code behaviour + +The host-side program or test should combine possible observations and choose one dominant `RobotEvent`. + +The baseline should include: + +- ball event +- obstacle event +- no event +- priority logic + +## Suggested types + +Use the existing `RobotEvent` and add if needed: + +```rust +#[derive(Debug, Clone, Copy, PartialEq)] +enum RobotEvent { + NoEvent, + StartStop, + BallLeft, + BallRight, + BallCentered, + BallLost, + ObstacleDetected, + Timeout, +} +``` + +## Suggested function + +```rust +fn prioritize_event(events: &[RobotEvent]) -> RobotEvent { + if events.contains(&RobotEvent::ObstacleDetected) { + return RobotEvent::ObstacleDetected; + } + + if events.contains(&RobotEvent::StartStop) { + return RobotEvent::StartStop; + } + + for event in events { + if *event != RobotEvent::NoEvent { + return *event; + } + } + + RobotEvent::NoEvent +} +``` + +## Participant tasks + +Participants should: + +1. Run the tests. +2. Add a test where `BallCentered` and `ObstacleDetected` happen together. +3. Ensure `ObstacleDetected` wins. +4. Add `Timeout` as another high-priority event. +5. Decide whether `StartStop` should beat `ObstacleDetected` or not. + +## Suggested command + +```text +cargo test --bin 07_events +``` + +## Expected result + +Participants understand that event handling is a design decision, not just syntax. + +## Implementation notes for the code bot + +- Keep this host-side. +- Add several unit tests. +- Do not require STM32 hardware. +- Keep priorities simple and explicit. +- Avoid overengineering with priority queues or traits. + +## Acceptance criteria + +- `ObstacleDetected` wins over ball events. +- `NoEvent` does not trigger behaviour. +- The priority rules are covered by tests. + +--- + +# 08_state_machine.rs — Robot mode and behaviour + +## Purpose + +This is the main robotics software example. + +It combines: + +- robot events +- robot modes +- motion commands +- transition logic +- LED status output on STM32 + +It should teach that robot behaviour should be explicit and inspectable. + +## Conceptual role in the robot journey + +A soccer robot needs memory. + +It must know whether it is: + +- idle +- searching for the ball +- aligning to the ball +- driving to the ball +- avoiding an obstacle +- in an error state + +This is a state machine. + +## Code behaviour + +The firmware should: + +1. Read button input. +2. Convert it to `RobotEvent`. +3. Update `RobotMode` through a `decide` or `transition` function. +4. Produce a `MotionCommand`. +5. Show the current mode with the RGB LED. + +The baseline does not need motors. The LED is the visible actuator. + +## Suggested types + +```rust +#[derive(Debug, Clone, Copy, PartialEq)] +enum RobotMode { + Idle, + SearchBall, + AlignToBall, + DriveToBall, + AvoidObstacle, + Error, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum MotionCommand { + Stop, + Forward, + RotateLeft, + RotateRight, + StrafeLeft, + StrafeRight, + Avoid, +} +``` + +## Suggested transition function + +```rust +fn decide( + mode: RobotMode, + event: RobotEvent, +) -> (RobotMode, MotionCommand) { + match (mode, event) { + (_, RobotEvent::ObstacleDetected) => { + (RobotMode::AvoidObstacle, MotionCommand::Stop) + } + + (RobotMode::Idle, RobotEvent::StartStop) => { + (RobotMode::SearchBall, MotionCommand::RotateLeft) + } + + (RobotMode::SearchBall, RobotEvent::BallLeft) => { + (RobotMode::AlignToBall, MotionCommand::RotateLeft) + } + + (RobotMode::SearchBall, RobotEvent::BallRight) => { + (RobotMode::AlignToBall, MotionCommand::RotateRight) + } + + (RobotMode::SearchBall, RobotEvent::BallCentered) => { + (RobotMode::DriveToBall, MotionCommand::Forward) + } + + (RobotMode::DriveToBall, RobotEvent::BallLost) => { + (RobotMode::SearchBall, MotionCommand::RotateLeft) + } + + (_, RobotEvent::StartStop) => { + (RobotMode::Idle, MotionCommand::Stop) + } + + (mode, _) => { + (mode, MotionCommand::Stop) + } + } +} +``` + +## LED status mapping + +Recommended mapping: + +```text +Idle -> Off or slow blue +SearchBall -> blue blink +AlignToBall -> yellow +DriveToBall -> green +AvoidObstacle -> red blink +Error -> fast red blink +``` + +## Participant tasks + +Participants should: + +1. Flash the baseline. +2. Press `Center` to leave `Idle`. +3. Use `Left`, `Right`, and `Up` to simulate ball observations. +4. Use `Down` to simulate an obstacle. +5. Add or fix one transition. +6. Add one unit test for transition logic if the code is split into testable pure functions. + +## Suggested command + +```text +run-08 +``` + +## Rustup fallback + +```text +cargo build --target thumbv7m-none-eabi --bin 08_state_machine +probe-rs run --chip STM32F103C8 target/thumbv7m-none-eabi/debug/08_state_machine +``` + +Optional host-side tests: + +```text +cargo test --bin 08_state_machine +``` + +## Expected result + +Participants see that button input can drive a simplified robot behaviour loop. + +## Implementation notes for the code bot + +- This is embedded-target code, but the transition logic should be pure and testable. +- Keep hardware reading and state transition separate. +- If unit tests are awkward in the embedded binary, move shared logic into a library module later. For now, make the pure functions copyable or testable in a host-side companion if needed. +- Do not introduce motors yet. + +## Acceptance criteria + +- Button input changes robot mode. +- Robot mode changes LED status. +- Obstacle handling has priority. +- The transition function is visible and easy to edit. + +--- + +# 09_sensor_pipeline.rs — Camera and ultrasonic sensor model + +## Purpose + +This example models real robot perception without requiring actual camera or ultrasonic hardware in the workshop. + +It should teach: + +- camera frames are not decisions +- ultrasonic echo values are not decisions +- raw data must become observations +- observations become robot events +- multiple observations can be fused into one behaviour-relevant event + +## Conceptual role in the robot journey + +The real RoboCup-style robot might have: + +```text +camera frame -> ball detection -> BallObservation +ultrasonic echo -> distance -> ObstacleObservation +``` + +The controller should only consume domain observations or events, not raw pixels or echo timings. + +## Code behaviour + +The host-side program should simulate: + +1. A fake camera input. +2. A fake ultrasonic input. +3. Conversion into `BallObservation` and `ObstacleObservation`. +4. Conversion into one or more `RobotEvent` values. +5. Event priority. +6. A final call into `decide` or a simplified decision function. + +## Suggested types + +```rust +#[derive(Debug, Clone, Copy, PartialEq)] +enum BallObservation { + NotSeen, + Left, + Center, + Right, + TooClose, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +struct RawEchoMicros(u32); + +#[derive(Debug, Clone, Copy, PartialEq)] +struct DistanceCm(u16); + +#[derive(Debug, Clone, Copy, PartialEq)] +enum ObstacleObservation { + Clear, + Warning, + TooClose, +} +``` + +## Suggested fake camera model + +Use a simple fake input type: + +```rust +#[derive(Debug, Clone, Copy, PartialEq)] +struct FakeBallPixelX(Option); +``` + +Then classify it: + +```rust +fn classify_ball(pixel_x: FakeBallPixelX) -> BallObservation { + match pixel_x.0 { + None => BallObservation::NotSeen, + Some(0..=200) => BallObservation::Left, + Some(201..=440) => BallObservation::Center, + Some(_) => BallObservation::Right, + } +} +``` + +The numbers do not need to be realistic; they only show the principle. + +## Suggested ultrasonic model + +Reuse `echo_to_distance` and `classify_obstacle` from chapter 03 or reimplement them locally for clarity. + +## Suggested fusion function + +```rust +fn observations_to_event( + ball: BallObservation, + obstacle: ObstacleObservation, +) -> RobotEvent { + match obstacle { + ObstacleObservation::TooClose => RobotEvent::ObstacleDetected, + _ => match ball { + BallObservation::NotSeen => RobotEvent::BallLost, + BallObservation::Left => RobotEvent::BallLeft, + BallObservation::Center => RobotEvent::BallCentered, + BallObservation::Right => RobotEvent::BallRight, + BallObservation::TooClose => RobotEvent::BallCentered, + }, + } +} +``` + +## Participant tasks + +Participants should: + +1. Run the tests. +2. Change fake ball positions. +3. Change distance thresholds. +4. Add a warning behaviour for `ObstacleObservation::Warning`. +5. Add a test where the ball is centered but the obstacle is too close. +6. Ensure the resulting event is `ObstacleDetected`. + +## Suggested command + +```text +cargo test --bin 09_sensor_pipeline +``` + +Optional run output: + +```text +cargo run --bin 09_sensor_pipeline +``` + +## Expected result + +Participants understand how a real camera or ultrasonic sensor can later be swapped in without rewriting the robot behaviour layer. + +## Implementation notes for the code bot + +- Keep this host-side. +- Include unit tests. +- Use deterministic fake inputs. +- Avoid image processing libraries. +- Avoid hardware dependencies. +- This chapter is about the interface between perception and behaviour. + +## Acceptance criteria + +- Fake camera values become `BallObservation`. +- Fake ultrasonic values become `ObstacleObservation`. +- Observation fusion produces `RobotEvent`. +- Obstacle priority is tested. + +--- + +# 10_motor_commands.rs — Motion intent to wheel speeds + +## Purpose + +This example models the transition from robot decision to motor-level command. + +It should teach: + +- robot behaviour should not directly produce PWM +- symbolic commands can become motion vectors +- motion vectors can be mixed into wheel speeds +- real motor control needs normalization and calibration + +## Conceptual role in the robot journey + +The state machine may decide: + +```text +RotateLeft +Forward +Stop +``` + +But a four-wheel omni-style robot needs four wheel speeds: + +```text +front_left +front_right +rear_left +rear_right +``` + +This example builds that translation. + +## Code behaviour + +The host-side program should: + +1. Convert `MotionCommand` to `MotionVector`. +2. Convert `MotionVector` to `WheelSpeeds`. +3. Normalize wheel speeds to `-1.0..=1.0`. +4. Test basic command mappings. + +## Suggested types + +```rust +#[derive(Debug, Clone, Copy, PartialEq)] +enum MotionCommand { + Stop, + Forward, + RotateLeft, + RotateRight, + StrafeLeft, + StrafeRight, + Avoid, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +struct MotionVector { + forward: f32, + strafe: f32, + rotate: f32, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +struct WheelSpeeds { + front_left: f32, + front_right: f32, + rear_left: f32, + rear_right: f32, +} +``` + +## Suggested command mapping + +```rust +fn command_to_vector(command: MotionCommand) -> MotionVector { + match command { + MotionCommand::Stop => MotionVector { forward: 0.0, strafe: 0.0, rotate: 0.0 }, + MotionCommand::Forward => MotionVector { forward: 0.6, strafe: 0.0, rotate: 0.0 }, + MotionCommand::RotateLeft => MotionVector { forward: 0.0, strafe: 0.0, rotate: -0.4 }, + MotionCommand::RotateRight => MotionVector { forward: 0.0, strafe: 0.0, rotate: 0.4 }, + MotionCommand::StrafeLeft => MotionVector { forward: 0.0, strafe: -0.5, rotate: 0.0 }, + MotionCommand::StrafeRight => MotionVector { forward: 0.0, strafe: 0.5, rotate: 0.0 }, + MotionCommand::Avoid => MotionVector { forward: -0.3, strafe: 0.0, rotate: 0.3 }, + } +} +``` + +## Suggested four-wheel mixer + +Use a simple omni/mecanum-style mixer: + +```rust +fn mix(cmd: MotionVector) -> WheelSpeeds { + WheelSpeeds { + front_left: cmd.forward + cmd.strafe + cmd.rotate, + front_right: cmd.forward - cmd.strafe - cmd.rotate, + rear_left: cmd.forward - cmd.strafe + cmd.rotate, + rear_right: cmd.forward + cmd.strafe - cmd.rotate, + } +} +``` + +Important: document in comments that signs and wheel layout depend on the actual robot. + +## Suggested normalization + +```rust +fn normalize(speeds: WheelSpeeds) -> WheelSpeeds { + let max = speeds.front_left + .abs() + .max(speeds.front_right.abs()) + .max(speeds.rear_left.abs()) + .max(speeds.rear_right.abs()); + + if max <= 1.0 { + speeds + } else { + WheelSpeeds { + front_left: speeds.front_left / max, + front_right: speeds.front_right / max, + rear_left: speeds.rear_left / max, + rear_right: speeds.rear_right / max, + } + } +} +``` + +## Participant tasks + +Participants should: + +1. Run the tests. +2. Add tests for `Stop`, `Forward`, and `RotateLeft`. +3. Change the speed for `Forward`. +4. Add a new command such as `KickApproach` or `SlowAlign`. +5. Confirm that normalization keeps all wheel speeds within range. + +## Suggested command + +```text +cargo test --bin 10_motor_commands +``` + +Optional run output: + +```text +cargo run --bin 10_motor_commands +``` + +## Expected result + +Participants understand the separation between robot intent and actuator-level command. + +## Implementation notes for the code bot + +- Keep this host-side. +- Do not require motor hardware. +- Include comments about calibration and real wheel orientation. +- Keep `f32` acceptable for the workshop model. +- Tests involving floats should use small tolerance helpers instead of direct equality where needed. + +## Acceptance criteria + +- `MotionCommand` maps to `MotionVector`. +- `MotionVector` maps to `WheelSpeeds`. +- Speeds can be normalized. +- At least three unit tests exist. + +--- + +# 11_kinematics.rs — Geometry, forward kinematics and inverse kinematics + +## Purpose + +This final example gives a conceptual and code-level outlook into kinematics. + +It should not become a full robotics math lecture. + +It should teach: + +- geometry enters when movement depends on robot shape +- forward kinematics asks where the robot/tool ends up +- inverse kinematics asks which commands are needed to reach a target +- invalid movement should fail before hardware moves + +## Conceptual role in the robot journey + +For the soccer base: + +```text +motion intent -> wheel speeds +``` + +For a robot arm: + +```text +target position -> joint angles -> motor commands +``` + +Both are translations from desired behaviour into actuator commands. + +## Code behaviour + +The host-side program should model a very small 2D or simplified 3-axis arm calculation. + +Keep it simple enough for workshop participants to read. + +Recommended approach: + +- Define target position. +- Define joint angles. +- Define possible kinematics errors. +- Implement a simplified reachability check. +- Optionally return placeholder angles for reachable targets. + +The purpose is not physically perfect kinematics. The purpose is modelling safe movement. + +## Suggested types + +```rust +#[derive(Debug, Clone, Copy, PartialEq)] +struct Millimeters(f32); + +#[derive(Debug, Clone, Copy, PartialEq)] +struct Degrees(f32); + +#[derive(Debug, Clone, Copy, PartialEq)] +struct ArmTarget { + x: Millimeters, + y: Millimeters, + z: Millimeters, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +struct JointAngles { + base: Degrees, + shoulder: Degrees, + elbow: Degrees, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum KinematicsError { + TargetOutOfReach, + JointLimitExceeded, + CollisionRisk, +} +``` + +## Suggested inverse kinematics shape + +```rust +fn inverse_kinematics(target: ArmTarget) -> Result { + let horizontal_distance = (target.x.0 * target.x.0 + target.y.0 * target.y.0).sqrt(); + let distance = (horizontal_distance * horizontal_distance + target.z.0 * target.z.0).sqrt(); + + const MAX_REACH_MM: f32 = 250.0; + + if distance > MAX_REACH_MM { + return Err(KinematicsError::TargetOutOfReach); + } + + Ok(JointAngles { + base: Degrees(0.0), + shoulder: Degrees(45.0), + elbow: Degrees(45.0), + }) +} +``` + +This is intentionally simplified. It teaches the API shape and safety boundary. + +## Participant tasks + +Participants should: + +1. Run the tests. +2. Change the target position. +3. Trigger `TargetOutOfReach`. +4. Add a `JointLimitExceeded` rule. +5. Add one test for reachable and one test for unreachable targets. + +## Suggested command + +```text +cargo test --bin 11_kinematics +``` + +Optional run output: + +```text +cargo run --bin 11_kinematics +``` + +## Expected result + +Participants understand that robot geometry should be encoded as explicit logic with explicit failure modes. + +## Implementation notes for the code bot + +- Keep this host-side. +- Avoid heavy math and external crates. +- Use simplified geometry and be honest in comments. +- The key concept is `Result`. +- Do not imply that placeholder angles are a complete inverse kinematics solution. + +## Acceptance criteria + +- Reachable targets return `Ok(JointAngles)`. +- Unreachable targets return `Err(KinematicsError::TargetOutOfReach)`. +- At least two tests exist. +- The example closes the workshop story by connecting safe typed APIs to physical robot movement. + +--- + +# Cross-chapter consistency + +The implementation should keep names and mental models consistent across examples. + +## Shared vocabulary + +Use these names consistently: + +```text +RobotEvent +RobotMode +MotionCommand +MotionVector +WheelSpeeds +BallObservation +ObstacleObservation +DistanceCm +SensorError +``` + +## Shared architecture + +The whole workshop should repeatedly reinforce this structure: + +```text +raw hardware input + ↓ +domain observation + ↓ +robot event + ↓ +state machine + ↓ +motion command + ↓ +hardware output +``` + +## Host-side vs embedded examples + +Host-side examples: + +```text +00_check.rs +01_rust_basics.rs +02_ownership.rs +03_types.rs +07_events.rs +09_sensor_pipeline.rs +10_motor_commands.rs +11_kinematics.rs +``` + +Embedded examples: + +```text +04_first_firmware.rs +05_rgb_output.rs +06_button_events.rs +08_state_machine.rs +12_embassy.rs +``` + +This split is intentional. If hardware setup fails for a participant, they can still learn and run most of the robot logic locally. + +## Suggested workshop flow + +Participants should experience the examples in this order: + +1. Run host sanity check. +2. Learn explicit values and mutation. +3. Learn ownership and borrowing through commands. +4. Learn domain types and sensor errors. +5. Flash first firmware. +6. Control LED as robot status output. +7. Read button as fake robot sensor. +8. Add event priority. +9. Build the robot state machine. +10. Model camera and ultrasonic processing. +11. Convert decisions to wheel speeds. +12. Close with kinematics and safe movement APIs. +13. Show Embassy async timers, tasks, and printing. + +## Final learning outcome + +At the end of the examples, participants should be able to explain this sentence: + +```text +Rust helps us build robot firmware as explicit transformations from physical signals into typed observations, events, states, commands and actuator outputs. +``` + +--- + +# 12_embassy.rs — Async Embassy loop, timers, and printing + +## Purpose + +This example shows why a small async framework is useful on embedded Rust. + +It should teach: + +- one manual loop can become multiple async tasks +- timers replace ad-hoc delay code +- printing can happen while other work keeps running +- setup stays explicit, but the scheduling gets simpler + +## Conceptual role in the robot journey + +The workshop has mostly shown hand-written loops. + +This chapter shows the next step: + +```text +init hardware -> spawn tasks -> await timers -> print progress +``` + +## Code behaviour + +The firmware should: + +1. Initialize Embassy for the Blue Pill. +2. Spawn one periodic async task. +3. Print a heartbeat from that task. +4. Print a second message from the main task. +5. Use `Timer` or `Ticker` instead of a manual busy loop delay. + +## Suggested types and tools + +```rust +use embassy_executor::Spawner; +use embassy_time::{Duration, Ticker, Timer}; +use cortex_m_semihosting::hprintln; +``` + +## Participant tasks + +Participants should: + +1. Run the baseline. +2. Change the print text. +3. Change the timer period. +4. Add a second async task. +5. Compare this flow with the hand-written loop from chapter 04. + +## Suggested command + +```text +run-12 +``` + +## Rustup fallback + +```text +cargo build --target thumbv7m-none-eabi --bin 12_embassy +probe-rs run --chip STM32F103C8 target/thumbv7m-none-eabi/debug/12_embassy +``` + +## Expected result + +Participants see that Embassy can handle waiting and concurrent work without manual loop plumbing. + +## Implementation notes for the code bot + +- Keep the example short and readable. +- Use `Timer` or `Ticker` for waiting. +- Use a small print message for visible progress. +- Avoid adding hardware drivers unless they help the lesson. +- Keep the example focused on the async model, not on full application logic. + +## Acceptance criteria + +- The firmware builds for the Blue Pill. +- At least one async task runs. +- Timer-driven waiting is visible in the code. +- The output shows that the firmware stays alive while work continues. diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..88af6b9 --- /dev/null +++ b/flake.lock @@ -0,0 +1,96 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1767313136, + "narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1744536153, + "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1779679233, + "narHash": "sha256-qSIAAfK66X6waos6alIYxVze1ZU3C/WPp7NlN4ooP54=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "47ab6c7b3c6a68beac60067490240efa32ae344c", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..7ff3506 --- /dev/null +++ b/flake.nix @@ -0,0 +1,85 @@ +{ + description = "Rust on STM32 Blue Pill"; + + inputs = { + flake-utils.url = "github:numtide/flake-utils"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; + rust-overlay.url = "github:oxalica/rust-overlay"; + }; + + outputs = { + self, + flake-utils, + nixpkgs, + rust-overlay, + }: + flake-utils.lib.eachDefaultSystem (system: let + pkgs = import nixpkgs { + inherit system; + overlays = [(import rust-overlay)]; + }; + + chip = "STM32F103C8"; + target = "thumbv7m-none-eabi"; + + rustToolchain = + pkgs.rust-bin.stable."1.95.0".default.override { + extensions = [ + "clippy" + "llvm-tools-preview" + "rust-src" + "rustfmt" + ]; + targets = [target]; + }; + in { + devShells.default = pkgs.mkShell { + packages = [ + rustToolchain + pkgs.cargo-binutils + pkgs.flip-link + pkgs.llvmPackages.bintools + pkgs.probe-rs + ]; + + RUST_SRC_PATH = "${rustToolchain}/lib/rustlib/src/rust/library"; + + shellHook = '' + alias fmt='cargo fmt --all' + alias check='cargo check --bin 00_check --bin 01_rust_basics --bin 02_ownership --bin 03_types --bin 07_events --bin 09_sensor_pipeline --bin 10_motor_commands --bin 11_kinematics && cargo check --target ${target} --bin 04_first_firmware --bin 05_rgb_output --bin 06_button_events --bin 08_state_machine --bin 12_embassy' + alias verify='bash ./verify-all.sh' + + alias run-00='cargo run --bin 00_check' + alias run-01='cargo run --bin 01_rust_basics' + alias run-02='cargo run --bin 02_ownership' + alias run-03='cargo run --bin 03_types' + alias run-04='probe-rs run --chip ${chip} target/${target}/debug/04_first_firmware' + alias run-05='probe-rs run --chip ${chip} target/${target}/debug/05_rgb_output' + alias run-06='probe-rs run --chip ${chip} target/${target}/debug/06_button_events' + alias run-08='probe-rs run --chip ${chip} target/${target}/debug/08_state_machine' + alias run-12='probe-rs run --chip ${chip} target/${target}/debug/12_embassy' + + alias test-07='cargo test --bin 07_events' + alias test-09='cargo test --bin 09_sensor_pipeline' + alias test-10='cargo test --bin 10_motor_commands' + alias test-11='cargo test --bin 11_kinematics' + + alias build-04='cargo build --target ${target} --bin 04_first_firmware' + alias build-05='cargo build --target ${target} --bin 05_rgb_output' + alias build-06='cargo build --target ${target} --bin 06_button_events' + alias build-08='cargo build --target ${target} --bin 08_state_machine' + alias build-12='cargo build --target ${target} --bin 12_embassy' + + alias flash-04='probe-rs download --chip ${chip} target/${target}/debug/04_first_firmware' + alias flash-05='probe-rs download --chip ${chip} target/${target}/debug/05_rgb_output' + alias flash-06='probe-rs download --chip ${chip} target/${target}/debug/06_button_events' + alias flash-08='probe-rs download --chip ${chip} target/${target}/debug/08_state_machine' + alias flash-12='probe-rs download --chip ${chip} target/${target}/debug/12_embassy' + + echo "Rust on STM32 Blue Pill dev shell" + echo "Target: ${target}" + echo "Chip: ${chip}" + ''; + }; + }); +} diff --git a/memory.x b/memory.x new file mode 100644 index 0000000..75ffdd9 --- /dev/null +++ b/memory.x @@ -0,0 +1,7 @@ +/* Conservative Blue Pill memory map. + Some boards expose 128K flash, but this repo stays within 64K. */ +MEMORY +{ + FLASH : ORIGIN = 0x08000000, LENGTH = 64K + RAM : ORIGIN = 0x20000000, LENGTH = 20K +} diff --git a/presentation.html b/presentation.html new file mode 100644 index 0000000..c461b59 --- /dev/null +++ b/presentation.html @@ -0,0 +1,2318 @@ + + + + + + +Rust on Robots: Hands-on Embedded Rust on STM32 + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+
+
+ + + + + + + diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..a0ae951 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,5 @@ +[toolchain] +channel = "1.95.0" +components = ["clippy", "rustfmt", "rust-src", "llvm-tools-preview"] +targets = ["thumbv7m-none-eabi"] +profile = "minimal" diff --git a/src/bin/00_check.rs b/src/bin/00_check.rs new file mode 100644 index 0000000..c8dc733 --- /dev/null +++ b/src/bin/00_check.rs @@ -0,0 +1,9 @@ +// Host sanity check. +// Confirm the Rust and Blue Pill setup. +// Start here before firmware. + +fn main() { + println!("Rust on Robots toolchain check"); + println!("Host program runs correctly"); + println!("Next step: build the embedded firmware"); +} diff --git a/src/bin/01_rust_basics.rs b/src/bin/01_rust_basics.rs new file mode 100644 index 0000000..1810ac4 --- /dev/null +++ b/src/bin/01_rust_basics.rs @@ -0,0 +1,50 @@ +// Rust basics. +// Show values, functions, and mutation. +// Keep the robot idea visible. + +/// Pick a target speed for the current robot mode. +fn select_target_speed(searching: bool) -> i32 { + if searching { 40 } else { 0 } +} + +/// Pick a short status label for the current robot mode. +fn select_status_name(searching: bool) -> &'static str { + if searching { "searching" } else { "idle" } +} + +fn main() { + let current_speed = 0; + let mut target_speed = select_target_speed(true); + let status = select_status_name(true); + + // current_speed = 10; // This would fail because the value is immutable. + target_speed += 5; + + println!("current speed: {current_speed}"); + println!("selected target speed: {target_speed}"); + println!("status: {status}"); +} + +#[cfg(test)] +mod tests { + use super::{select_status_name, select_target_speed}; + + /// Check that searching picks a non-zero speed. + #[test] + fn searching_speed_is_high() { + assert_eq!(select_target_speed(true), 40); + } + + /// Check that the idle state stays stopped. + #[test] + fn idle_speed_is_zero() { + assert_eq!(select_target_speed(false), 0); + } + + /// Check that the status label matches the search flag. + #[test] + fn status_label_changes_with_mode() { + assert_eq!(select_status_name(true), "searching"); + assert_eq!(select_status_name(false), "idle"); + } +} diff --git a/src/bin/02_ownership.rs b/src/bin/02_ownership.rs new file mode 100644 index 0000000..ab4e0d7 --- /dev/null +++ b/src/bin/02_ownership.rs @@ -0,0 +1,85 @@ +// Ownership and borrowing. +// Share robot resources safely. +// Show who may change what. + +#[derive(Debug)] +struct MotionCommand { + forward: f32, + strafe: f32, + rotate: f32, +} + +/// Print a motion command without taking ownership of it. +fn print_command(command: &MotionCommand) { + println!("command = {command:?}"); +} + +/// Stop the command by clearing every motion axis. +fn stop(command: &mut MotionCommand) { + command.forward = 0.0; + command.strafe = 0.0; + command.rotate = 0.0; +} + +/// Turn the command a little to the left. +fn rotate_left(command: &mut MotionCommand) { + command.forward = 0.0; + command.strafe = 0.0; + command.rotate = -0.4; +} + +fn main() { + let mut command = MotionCommand { + forward: 0.4, + strafe: 0.0, + rotate: 0.0, + }; + + print_command(&command); + + // let next_command = command; // This would move the value and make `command` unusable. + rotate_left(&mut command); + print_command(&command); + + stop(&mut command); + print_command(&command); + + // Q: What happens when we start that on a robot in real world scenario? +} + +#[cfg(test)] +mod tests { + use super::{MotionCommand, rotate_left, stop}; + + /// Check that rotating left changes the turn axis only. + #[test] + fn rotate_left_sets_turn() { + let mut command = MotionCommand { + forward: 1.0, + strafe: 1.0, + rotate: 0.0, + }; + + rotate_left(&mut command); + + assert_eq!(command.forward, 0.0); + assert_eq!(command.strafe, 0.0); + assert_eq!(command.rotate, -0.4); + } + + /// Check that stop clears every field. + #[test] + fn stop_clears_everything() { + let mut command = MotionCommand { + forward: 1.0, + strafe: -0.5, + rotate: 0.25, + }; + + stop(&mut command); + + assert_eq!(command.forward, 0.0); + assert_eq!(command.strafe, 0.0); + assert_eq!(command.rotate, 0.0); + } +} diff --git a/src/bin/03_types.rs b/src/bin/03_types.rs new file mode 100644 index 0000000..86ab55a --- /dev/null +++ b/src/bin/03_types.rs @@ -0,0 +1,152 @@ +// Domain types. +// Use units and enums for meaning. +// Turn raw data into robot results. + +#[derive(Debug, Clone, Copy, PartialEq)] +struct RawEchoMicros(u32); + +#[derive(Debug, Clone, Copy, PartialEq)] +struct DistanceCm(u16); + +#[derive(Debug, Clone, Copy, PartialEq)] +enum SensorError { + Timeout, + OutOfRange, + NotCalibrated, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum ObstacleObservation { + Clear, + Warning, + TooClose, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum BallObservation { + NotSeen, + Left, + Center, + Right, + TooClose, +} + +/// Check that the fake sensor is calibrated. +fn sensor_ready(calibrated: bool) -> Result<(), SensorError> { + if calibrated { + Ok(()) + } else { + Err(SensorError::NotCalibrated) + } +} + +/// Convert an ultrasonic echo time into a distance. +fn echo_to_distance(raw: RawEchoMicros) -> Result { + if raw.0 == 0 { + return Err(SensorError::Timeout); + } + + let cm = raw.0 / 58; + + if cm > 400 { + Err(SensorError::OutOfRange) + } else { + Ok(DistanceCm(cm as u16)) + } +} + +/// Classify a distance into a simple obstacle state. +fn classify_obstacle(distance: DistanceCm) -> ObstacleObservation { + match distance.0 { + 0..=15 => ObstacleObservation::TooClose, + 16..=35 => ObstacleObservation::Warning, + _ => ObstacleObservation::Clear, + } +} + +/// Classify a fake camera x position into a ball observation. +fn classify_ball(pixel_x: Option) -> BallObservation { + match pixel_x { + None => BallObservation::NotSeen, + Some(0..=200) => BallObservation::Left, + Some(201..=440) => BallObservation::Center, + Some(441..=620) => BallObservation::Right, + Some(_) => BallObservation::TooClose, + } +} + +fn main() { + let raw_values = [RawEchoMicros(0), RawEchoMicros(580), RawEchoMicros(25_000)]; + + for raw in raw_values { + match echo_to_distance(raw) { + Ok(distance) => { + let obstacle = classify_obstacle(distance); + println!("echo {raw:?} -> {distance:?} -> {obstacle:?}"); + } + Err(err) => { + println!("echo {raw:?} -> error {err:?}"); + } + } + } + + match sensor_ready(false) { + Ok(()) => println!("sensor calibrated"), + Err(err) => println!("calibration check failed: {err:?}"), + } + + let ball = classify_ball(Some(120)); + let far_ball = classify_ball(Some(700)); + println!("ball near left: {ball:?}"); + println!("ball too close: {far_ball:?}"); +} + +#[cfg(test)] +mod tests { + use super::{ + BallObservation, DistanceCm, ObstacleObservation, RawEchoMicros, SensorError, + classify_ball, classify_obstacle, echo_to_distance, + }; + + /// Check that a zero echo becomes a timeout. + #[test] + fn timeout_is_reported() { + assert_eq!( + echo_to_distance(RawEchoMicros(0)), + Err(SensorError::Timeout) + ); + } + + /// Check that a regular echo turns into a distance. + #[test] + fn echo_converts_to_distance() { + assert_eq!(echo_to_distance(RawEchoMicros(580)), Ok(DistanceCm(10))); + } + + /// Check that the distance thresholds still classify the obstacle. + #[test] + fn distance_classifies_obstacle() { + assert_eq!( + classify_obstacle(DistanceCm(12)), + ObstacleObservation::TooClose + ); + assert_eq!( + classify_obstacle(DistanceCm(24)), + ObstacleObservation::Warning + ); + assert_eq!( + classify_obstacle(DistanceCm(120)), + ObstacleObservation::Clear + ); + } + + /// Check that the fake camera x position becomes a ball observation. + #[test] + fn ball_positions_become_observations() { + assert_eq!(classify_ball(None), BallObservation::NotSeen); + assert_eq!(classify_ball(Some(80)), BallObservation::Left); + assert_eq!(classify_ball(Some(320)), BallObservation::Center); + assert_eq!(classify_ball(Some(520)), BallObservation::Right); + assert_eq!(classify_ball(Some(700)), BallObservation::TooClose); + } +} diff --git a/src/bin/04_first_firmware.rs b/src/bin/04_first_firmware.rs new file mode 100644 index 0000000..900cb1b --- /dev/null +++ b/src/bin/04_first_firmware.rs @@ -0,0 +1,25 @@ +// First firmware. +// Boot the Blue Pill with the smallest loop. +// Prove the board runs code. + +#![deny(unsafe_code)] +#![no_std] +#![no_main] + +use cortex_m_rt::entry; +use cortex_m_semihosting::hprintln; +use panic_probe as _; +use stm32f1xx_hal::{pac, prelude::*}; + +#[entry] +fn main() -> ! { + let dp = pac::Peripherals::take().unwrap(); + let cp = cortex_m::Peripherals::take().unwrap(); + let rcc = dp.RCC.constrain(); + let mut delay = cp.SYST.delay(&rcc.clocks); + + loop { + hprintln!("04_first_firmware: loop alive"); + delay.delay_ms(1000u32); + } +} diff --git a/src/bin/05_rgb_output.rs b/src/bin/05_rgb_output.rs new file mode 100644 index 0000000..fa629d1 --- /dev/null +++ b/src/bin/05_rgb_output.rs @@ -0,0 +1,73 @@ +// RGB status output. +// Map robot mode to visible LED state. +// Make the board show what the robot is doing. + +#![deny(unsafe_code)] +#![no_std] +#![no_main] + +use cortex_m_rt::entry; +use panic_probe as _; +use stm32f1xx_hal::{pac, prelude::*}; + +#[derive(Debug, Clone, Copy, PartialEq)] +enum RobotMode { + Idle, + SearchBall, +} + +/// Map a robot mode to RGB LED state. +fn mode_to_rgb(mode: RobotMode) -> (bool, bool, bool) { + match mode { + RobotMode::Idle => (false, false, false), + RobotMode::SearchBall => (false, false, true), + } +} + +#[entry] +fn main() -> ! { + let dp = pac::Peripherals::take().unwrap(); + let cp = cortex_m::Peripherals::take().unwrap(); + + let mut rcc = dp.RCC.constrain(); + let mut gpioa = dp.GPIOA.split(&mut rcc); + let mut delay = cp.SYST.delay(&rcc.clocks); + + let mut _gnd = gpioa.pa0.into_push_pull_output(&mut gpioa.crl); + let mut red = gpioa.pa1.into_push_pull_output(&mut gpioa.crl); + let mut green = gpioa.pa2.into_push_pull_output(&mut gpioa.crl); + let mut blue = gpioa.pa3.into_push_pull_output(&mut gpioa.crl); + + _gnd.set_low(); + let mut mode = RobotMode::Idle; + + loop { + let (red_on, green_on, blue_on) = mode_to_rgb(mode); + + if red_on { + red.set_high(); + } else { + red.set_low(); + } + + if green_on { + green.set_high(); + } else { + green.set_low(); + } + + if blue_on { + blue.set_high(); + } else { + blue.set_low(); + } + + delay.delay_ms(1000u32); + + mode = match mode { + RobotMode::Idle => RobotMode::SearchBall, + RobotMode::SearchBall => RobotMode::Idle, + // TODO: add "FoundBall" as new Mode with fitting LED outputo + }; + } +} diff --git a/src/bin/06_button_events.rs b/src/bin/06_button_events.rs new file mode 100644 index 0000000..f3fce83 --- /dev/null +++ b/src/bin/06_button_events.rs @@ -0,0 +1,111 @@ +// Button events. +// Turn the 5-way button into robot meaning. +// Keep raw input separate from behaviour. + +#![deny(unsafe_code)] +#![no_std] +#![no_main] + +use cortex_m_rt::entry; +use panic_probe as _; +use stm32f1xx_hal::{pac, prelude::*}; + +#[derive(Debug, Clone, Copy, PartialEq)] +enum ButtonDirection { + None, + Center, + Up, + Down, + Left, + Right, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum RobotEvent { + NoEvent, + StartStop, + BallLeft, + BallRight, + BallCentered, + ObstacleDetected, +} + +/// Turn raw button states into one direction. +fn read_direction(center: bool, up: bool, down: bool, left: bool, right: bool) -> ButtonDirection { + if center { + ButtonDirection::Center + } else if up { + ButtonDirection::Up + } else if down { + ButtonDirection::Down + } else if left { + ButtonDirection::Left + } else if right { + ButtonDirection::Right + } else { + ButtonDirection::None + } + // Q: Why not match? +} + +/// Turn one button direction into one robot event. +fn direction_to_event(direction: ButtonDirection) -> RobotEvent { + match direction { + ButtonDirection::None => RobotEvent::NoEvent, + ButtonDirection::Center => RobotEvent::StartStop, + ButtonDirection::Left => RobotEvent::BallLeft, + ButtonDirection::Right => RobotEvent::BallRight, + ButtonDirection::Up => RobotEvent::BallCentered, + ButtonDirection::Down => RobotEvent::ObstacleDetected, + } +} + +#[entry] +fn main() -> ! { + let dp = pac::Peripherals::take().unwrap(); + let cp = cortex_m::Peripherals::take().unwrap(); + + let mut rcc = dp.RCC.constrain(); + let mut gpioa = dp.GPIOA.split(&mut rcc); + let mut gpiob = dp.GPIOB.split(&mut rcc); + let mut gpioc = dp.GPIOC.split(&mut rcc); + let mut delay = cp.SYST.delay(&rcc.clocks); + + let center = gpioa.pa8.into_pull_down_input(&mut gpioa.crh); + let up = gpioa.pa9.into_pull_down_input(&mut gpioa.crh); + let mut vcc = gpioa.pa10.into_push_pull_output(&mut gpioa.crh); + let right = gpiob.pb13.into_pull_down_input(&mut gpiob.crh); + let down = gpiob.pb14.into_pull_down_input(&mut gpiob.crh); + let left = gpiob.pb15.into_pull_down_input(&mut gpiob.crh); + let mut gnd = gpiob.pb12.into_push_pull_output(&mut gpiob.crh); + let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh); + + vcc.set_high(); + gnd.set_low(); + led.set_high(); + + let mut last_event = RobotEvent::NoEvent; + + loop { + let direction = read_direction( + center.is_high(), + up.is_high(), + down.is_high(), + left.is_high(), + right.is_high(), + ); + let event = direction_to_event(direction); + + if event != last_event { + last_event = event; + + if event == RobotEvent::NoEvent { + led.set_high(); + } else { + led.set_low(); + } + } + + delay.delay_ms(25u32); + } +} diff --git a/src/bin/07_events.rs b/src/bin/07_events.rs new file mode 100644 index 0000000..7051611 --- /dev/null +++ b/src/bin/07_events.rs @@ -0,0 +1,91 @@ +// Event priority. +// Pick the most important robot event. +// Safety events should win first. + +#[allow(dead_code)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RobotEvent { + NoEvent, + StartStop, + BallLeft, + BallRight, + BallCentered, + BallLost, + ObstacleDetected, + Timeout, +} + +/// Pick the strongest event from a batch of observations. +fn prioritize_event(events: &[RobotEvent]) -> RobotEvent { + if events.contains(&RobotEvent::Timeout) { + return RobotEvent::Timeout; + } + + if events.contains(&RobotEvent::ObstacleDetected) { + return RobotEvent::ObstacleDetected; + } + + if events.contains(&RobotEvent::StartStop) { + return RobotEvent::StartStop; + } + + for event in events { + if *event != RobotEvent::NoEvent { + return *event; + } + } + + RobotEvent::NoEvent +} + +fn main() { + let events = [ + RobotEvent::NoEvent, + RobotEvent::BallLeft, + RobotEvent::ObstacleDetected, + RobotEvent::StartStop, + ]; + + let chosen = prioritize_event(&events); + + println!("events = {events:?}"); + println!("chosen = {chosen:?}"); +} + +#[cfg(test)] +mod tests { + use super::{RobotEvent, prioritize_event}; + + /// Check that empty input stays empty. + #[test] + fn no_event_stays_no_event() { + let events = [RobotEvent::NoEvent, RobotEvent::NoEvent]; + assert_eq!(prioritize_event(&events), RobotEvent::NoEvent); + } + + /// Check that the obstacle wins over ball events. + #[test] + fn obstacle_wins_over_ball() { + let events = [RobotEvent::BallCentered, RobotEvent::ObstacleDetected]; + assert_eq!(prioritize_event(&events), RobotEvent::ObstacleDetected); + } + + /// Check that timeout beats every other event. + #[test] + fn timeout_wins_over_everything() { + let events = [ + RobotEvent::BallLeft, + RobotEvent::StartStop, + RobotEvent::Timeout, + RobotEvent::ObstacleDetected, + ]; + assert_eq!(prioritize_event(&events), RobotEvent::Timeout); + } + + /// Check that start and stop still beat ball observations. + #[test] + fn start_stop_beats_ball() { + let events = [RobotEvent::BallRight, RobotEvent::StartStop]; + assert_eq!(prioritize_event(&events), RobotEvent::StartStop); + } +} diff --git a/src/bin/08_state_machine.rs b/src/bin/08_state_machine.rs new file mode 100644 index 0000000..2d56a69 --- /dev/null +++ b/src/bin/08_state_machine.rs @@ -0,0 +1,240 @@ +// State machine. +// Turn events into robot mode and motion. +// Keep behaviour explicit and testable. + +#![deny(unsafe_code)] +#![no_std] +#![no_main] + +use cortex_m_rt::entry; +use panic_probe as _; +use stm32f1xx_hal::{pac, prelude::*}; + +#[derive(Debug, Clone, Copy, PartialEq)] +enum ButtonDirection { + None, + Center, + Up, + Down, + Left, + Right, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum RobotEvent { + NoEvent, + StartStop, + BallLeft, + BallRight, + BallCentered, + BallLost, + ObstacleDetected, +} + +#[allow(dead_code)] +#[derive(Debug, Clone, Copy, PartialEq)] +enum RobotMode { + Idle, + SearchBall, + AlignToBall, + DriveToBall, + AvoidObstacle, + Error, +} + +#[allow(dead_code)] +#[derive(Debug, Clone, Copy, PartialEq)] +enum MotionCommand { + Stop, + Forward, + RotateLeft, + RotateRight, + StrafeLeft, + StrafeRight, + Avoid, +} + +/// Turn raw button states into one direction. +fn read_direction(center: bool, up: bool, down: bool, left: bool, right: bool) -> ButtonDirection { + if center { + ButtonDirection::Center + } else if up { + ButtonDirection::Up + } else if down { + ButtonDirection::Down + } else if left { + ButtonDirection::Left + } else if right { + ButtonDirection::Right + } else { + ButtonDirection::None + } +} + +/// Turn one button direction into one robot event. +fn direction_to_event(direction: ButtonDirection) -> RobotEvent { + match direction { + ButtonDirection::None => RobotEvent::NoEvent, + ButtonDirection::Center => RobotEvent::StartStop, + ButtonDirection::Left => RobotEvent::BallLeft, + ButtonDirection::Right => RobotEvent::BallRight, + ButtonDirection::Up => RobotEvent::BallCentered, + ButtonDirection::Down => RobotEvent::ObstacleDetected, + } +} + +/// Decide the next mode and command from one event. +fn decide(mode: RobotMode, event: RobotEvent) -> (RobotMode, MotionCommand) { + match (mode, event) { + (_, RobotEvent::ObstacleDetected) => (RobotMode::AvoidObstacle, MotionCommand::Stop), + + (RobotMode::Idle, RobotEvent::StartStop) => { + (RobotMode::SearchBall, MotionCommand::RotateLeft) + } + + (RobotMode::SearchBall, RobotEvent::BallLeft) => { + (RobotMode::AlignToBall, MotionCommand::RotateLeft) + } + + (RobotMode::SearchBall, RobotEvent::BallRight) => { + (RobotMode::AlignToBall, MotionCommand::RotateRight) + } + + (RobotMode::SearchBall, RobotEvent::BallCentered) => { + (RobotMode::DriveToBall, MotionCommand::Forward) + } + + (RobotMode::AlignToBall, RobotEvent::BallLeft) => { + (RobotMode::AlignToBall, MotionCommand::RotateLeft) + } + + (RobotMode::AlignToBall, RobotEvent::BallRight) => { + (RobotMode::AlignToBall, MotionCommand::RotateRight) + } + + (RobotMode::AlignToBall, RobotEvent::BallCentered) => { + (RobotMode::DriveToBall, MotionCommand::Forward) + } + + (RobotMode::DriveToBall, RobotEvent::BallLost) => { + (RobotMode::SearchBall, MotionCommand::RotateLeft) + } + + (RobotMode::SearchBall, RobotEvent::BallLost) => { + (RobotMode::SearchBall, MotionCommand::RotateLeft) + } + + (RobotMode::AlignToBall, RobotEvent::BallLost) => { + (RobotMode::SearchBall, MotionCommand::RotateLeft) + } + + (_, RobotEvent::StartStop) => (RobotMode::Idle, MotionCommand::Stop), + + (mode, _) => (mode, MotionCommand::Stop), + } +} + +/// Map one mode to RGB LED state. +fn mode_to_rgb(mode: RobotMode) -> (bool, bool, bool) { + match mode { + RobotMode::Idle => (false, false, false), + RobotMode::SearchBall => (false, false, true), + RobotMode::AlignToBall => (true, true, false), + RobotMode::DriveToBall => (false, true, false), + RobotMode::AvoidObstacle => (true, false, false), + RobotMode::Error => (true, false, false), + } +} + +#[entry] +fn main() -> ! { + let dp = pac::Peripherals::take().unwrap(); + let cp = cortex_m::Peripherals::take().unwrap(); + + let mut rcc = dp.RCC.constrain(); + let mut gpioa = dp.GPIOA.split(&mut rcc); + let mut gpiob = dp.GPIOB.split(&mut rcc); + let mut gpioc = dp.GPIOC.split(&mut rcc); + let mut delay = cp.SYST.delay(&rcc.clocks); + + let mut _gnd = gpioa.pa0.into_push_pull_output(&mut gpioa.crl); + let mut red = gpioa.pa1.into_push_pull_output(&mut gpioa.crl); + let mut green = gpioa.pa2.into_push_pull_output(&mut gpioa.crl); + let mut blue = gpioa.pa3.into_push_pull_output(&mut gpioa.crl); + + _gnd.set_low(); + let center = gpioa.pa8.into_pull_down_input(&mut gpioa.crh); + let up = gpioa.pa9.into_pull_down_input(&mut gpioa.crh); + let mut vcc = gpioa.pa10.into_push_pull_output(&mut gpioa.crh); + let right = gpiob.pb13.into_pull_down_input(&mut gpiob.crh); + let down = gpiob.pb14.into_pull_down_input(&mut gpiob.crh); + let left = gpiob.pb15.into_pull_down_input(&mut gpiob.crh); + let mut gnd = gpiob.pb12.into_push_pull_output(&mut gpiob.crh); + let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh); + + vcc.set_high(); + gnd.set_low(); + led.set_high(); + + let mut mode = RobotMode::Idle; + let mut quiet = 0u8; + + loop { + let direction = read_direction( + center.is_high(), + up.is_high(), + down.is_high(), + left.is_high(), + right.is_high(), + ); + let mut event = direction_to_event(direction); + + if event == RobotEvent::NoEvent { + quiet = quiet.saturating_add(1); + + if quiet >= 40 + && matches!( + mode, + RobotMode::SearchBall | RobotMode::AlignToBall | RobotMode::DriveToBall + ) + { + event = RobotEvent::BallLost; + } + } else { + quiet = 0; + } + + if event != RobotEvent::NoEvent { + let (next_mode, _command) = decide(mode, event); + mode = next_mode; + } + + let (red_on, green_on, blue_on) = mode_to_rgb(mode); + + if red_on { + red.set_high(); + } else { + red.set_low(); + } + + if green_on { + green.set_high(); + } else { + green.set_low(); + } + + if blue_on { + blue.set_high(); + } else { + blue.set_low(); + } + + if mode == RobotMode::AvoidObstacle { + led.set_low(); + } else { + led.set_high(); + } + + delay.delay_ms(25u32); + } +} diff --git a/src/bin/09_sensor_pipeline.rs b/src/bin/09_sensor_pipeline.rs new file mode 100644 index 0000000..b5b9a61 --- /dev/null +++ b/src/bin/09_sensor_pipeline.rs @@ -0,0 +1,208 @@ +// Sensor pipeline. +// Turn fake camera and distance data into events. +// Show how observations feed behaviour. + +#[derive(Debug, Clone, Copy, PartialEq)] +enum BallObservation { + NotSeen, + Left, + Center, + Right, + TooClose, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +struct RawEchoMicros(u32); + +#[derive(Debug, Clone, Copy, PartialEq)] +struct DistanceCm(u16); + +#[allow(dead_code)] +#[derive(Debug, Clone, Copy, PartialEq)] +enum SensorError { + Timeout, + OutOfRange, + NotCalibrated, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum ObstacleObservation { + Clear, + Warning, + TooClose, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +struct FakeBallPixelX(Option); + +#[allow(dead_code)] +#[derive(Debug, Clone, Copy, PartialEq)] +enum RobotEvent { + NoEvent, + BallLeft, + BallCentered, + BallRight, + BallLost, + ObstacleDetected, +} + +/// Classify a fake camera x position into a ball observation. +fn classify_ball(pixel_x: FakeBallPixelX) -> BallObservation { + match pixel_x.0 { + None => BallObservation::NotSeen, + Some(0..=200) => BallObservation::Left, + Some(201..=440) => BallObservation::Center, + Some(441..=620) => BallObservation::Right, + Some(_) => BallObservation::TooClose, + } +} + +/// Convert an ultrasonic echo time into a distance. +fn echo_to_distance(raw: RawEchoMicros) -> Result { + if raw.0 == 0 { + return Err(SensorError::Timeout); + } + + let cm = raw.0 / 58; + + if cm > 400 { + Err(SensorError::OutOfRange) + } else { + Ok(DistanceCm(cm as u16)) + } +} + +/// Classify a distance into a simple obstacle state. +fn classify_obstacle(distance: DistanceCm) -> ObstacleObservation { + match distance.0 { + 0..=15 => ObstacleObservation::TooClose, + 16..=35 => ObstacleObservation::Warning, + _ => ObstacleObservation::Clear, + } +} + +/// Explain what the obstacle state means for the controller. +fn obstacle_hint(obstacle: ObstacleObservation) -> &'static str { + match obstacle { + ObstacleObservation::Clear => "clear", + ObstacleObservation::Warning => "caution", + ObstacleObservation::TooClose => "stop", + } +} + +/// Turn observations into one robot event. +fn observations_to_event(ball: BallObservation, obstacle: ObstacleObservation) -> RobotEvent { + match obstacle { + ObstacleObservation::TooClose => RobotEvent::ObstacleDetected, + ObstacleObservation::Warning | ObstacleObservation::Clear => match ball { + BallObservation::NotSeen => RobotEvent::BallLost, + BallObservation::Left => RobotEvent::BallLeft, + BallObservation::Center => RobotEvent::BallCentered, + BallObservation::Right => RobotEvent::BallRight, + BallObservation::TooClose => RobotEvent::BallCentered, + }, + } +} + +fn main() { + let ball_samples = [ + FakeBallPixelX(None), + FakeBallPixelX(Some(120)), + FakeBallPixelX(Some(700)), + ]; + let echo_samples = [RawEchoMicros(0), RawEchoMicros(580), RawEchoMicros(2_900)]; + + for ball in ball_samples { + println!("ball sample {ball:?} -> {:?}", classify_ball(ball)); + } + + for echo in echo_samples { + match echo_to_distance(echo) { + Ok(distance) => { + let obstacle = classify_obstacle(distance); + let event = observations_to_event(BallObservation::Center, obstacle); + println!( + "echo {echo:?} -> {distance:?} -> {obstacle:?} -> {} -> {event:?}", + obstacle_hint(obstacle) + ); + } + Err(err) => { + println!("echo {echo:?} -> error {err:?}"); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::{ + BallObservation, DistanceCm, FakeBallPixelX, ObstacleObservation, RawEchoMicros, + RobotEvent, SensorError, classify_ball, classify_obstacle, echo_to_distance, + observations_to_event, + }; + + /// Check that a zero echo becomes a timeout. + #[test] + fn timeout_is_reported() { + assert_eq!( + echo_to_distance(RawEchoMicros(0)), + Err(SensorError::Timeout) + ); + } + + /// Check that the camera x position becomes a ball observation. + #[test] + fn ball_positions_are_classified() { + assert_eq!( + classify_ball(FakeBallPixelX(None)), + BallObservation::NotSeen + ); + assert_eq!( + classify_ball(FakeBallPixelX(Some(80))), + BallObservation::Left + ); + assert_eq!( + classify_ball(FakeBallPixelX(Some(320))), + BallObservation::Center + ); + assert_eq!( + classify_ball(FakeBallPixelX(Some(520))), + BallObservation::Right + ); + assert_eq!( + classify_ball(FakeBallPixelX(Some(700))), + BallObservation::TooClose + ); + } + + /// Check that the obstacle warning still lets the ball event through. + #[test] + fn warning_keeps_ball_event() { + let event = observations_to_event(BallObservation::Center, ObstacleObservation::Warning); + assert_eq!(event, RobotEvent::BallCentered); + } + + /// Check that a close obstacle wins over the ball. + #[test] + fn obstacle_wins_over_ball() { + let event = observations_to_event(BallObservation::Center, ObstacleObservation::TooClose); + assert_eq!(event, RobotEvent::ObstacleDetected); + } + + /// Check that the obstacle thresholds still classify the range. + #[test] + fn distance_classifies_obstacle() { + assert_eq!( + classify_obstacle(DistanceCm(12)), + ObstacleObservation::TooClose + ); + assert_eq!( + classify_obstacle(DistanceCm(24)), + ObstacleObservation::Warning + ); + assert_eq!( + classify_obstacle(DistanceCm(120)), + ObstacleObservation::Clear + ); + } +} diff --git a/src/bin/10_motor_commands.rs b/src/bin/10_motor_commands.rs new file mode 100644 index 0000000..12a2cf0 --- /dev/null +++ b/src/bin/10_motor_commands.rs @@ -0,0 +1,179 @@ +// Motor commands. +// Convert motion intent into wheel speeds. +// Keep the mixing logic readable. + +#[allow(dead_code)] +#[derive(Debug, Clone, Copy, PartialEq)] +enum MotionCommand { + Stop, + Forward, + RotateLeft, + RotateRight, + StrafeLeft, + StrafeRight, + Avoid, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +struct MotionVector { + forward: f32, + strafe: f32, + rotate: f32, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +struct WheelSpeeds { + front_left: f32, + front_right: f32, + rear_left: f32, + rear_right: f32, +} + +/// Turn a symbolic command into a motion vector. +fn command_to_vector(command: MotionCommand) -> MotionVector { + match command { + MotionCommand::Stop => MotionVector { + forward: 0.0, + strafe: 0.0, + rotate: 0.0, + }, + MotionCommand::Forward => MotionVector { + forward: 0.6, + strafe: 0.0, + rotate: 0.0, + }, + MotionCommand::RotateLeft => MotionVector { + forward: 0.0, + strafe: 0.0, + rotate: -0.4, + }, + MotionCommand::RotateRight => MotionVector { + forward: 0.0, + strafe: 0.0, + rotate: 0.4, + }, + MotionCommand::StrafeLeft => MotionVector { + forward: 0.0, + strafe: -0.5, + rotate: 0.0, + }, + MotionCommand::StrafeRight => MotionVector { + forward: 0.0, + strafe: 0.5, + rotate: 0.0, + }, + MotionCommand::Avoid => MotionVector { + forward: -0.3, + strafe: 0.0, + rotate: 0.3, + }, + } +} + +/// Mix a motion vector into four wheel speeds. +fn mix(cmd: MotionVector) -> WheelSpeeds { + WheelSpeeds { + front_left: cmd.forward + cmd.strafe + cmd.rotate, + front_right: cmd.forward - cmd.strafe - cmd.rotate, + rear_left: cmd.forward - cmd.strafe + cmd.rotate, + rear_right: cmd.forward + cmd.strafe - cmd.rotate, + } +} + +/// Scale wheel speeds so the largest magnitude is at most one. +fn normalize(speeds: WheelSpeeds) -> WheelSpeeds { + let max = speeds + .front_left + .abs() + .max(speeds.front_right.abs()) + .max(speeds.rear_left.abs()) + .max(speeds.rear_right.abs()); + + if max <= 1.0 { + speeds + } else { + WheelSpeeds { + front_left: speeds.front_left / max, + front_right: speeds.front_right / max, + rear_left: speeds.rear_left / max, + rear_right: speeds.rear_right / max, + } + } +} + +/// Check whether two floats are close enough for workshop math. +#[cfg(test)] +fn close(a: f32, b: f32) -> bool { + (a - b).abs() < 0.001 +} + +fn main() { + let commands = [ + MotionCommand::Stop, + MotionCommand::Forward, + MotionCommand::RotateLeft, + MotionCommand::Avoid, + ]; + + for command in commands { + let vector = command_to_vector(command); + let speeds = normalize(mix(vector)); + + println!("command = {command:?}"); + println!("vector = {vector:?}"); + println!("wheel speeds = {speeds:?}"); + } +} + +#[cfg(test)] +mod tests { + use super::{MotionCommand, WheelSpeeds, close, command_to_vector, mix, normalize}; + + /// Check that stop maps to zero. + #[test] + fn stop_maps_to_zero_vector() { + let vector = command_to_vector(MotionCommand::Stop); + assert!(close(vector.forward, 0.0)); + assert!(close(vector.strafe, 0.0)); + assert!(close(vector.rotate, 0.0)); + } + + /// Check that forward motion spreads across every wheel equally. + #[test] + fn forward_mixes_symmetrically() { + let speeds = mix(command_to_vector(MotionCommand::Forward)); + + assert!(close(speeds.front_left, 0.6)); + assert!(close(speeds.front_right, 0.6)); + assert!(close(speeds.rear_left, 0.6)); + assert!(close(speeds.rear_right, 0.6)); + } + + /// Check that normalization clamps a large vector to range. + #[test] + fn normalize_scales_large_values() { + let speeds = WheelSpeeds { + front_left: 2.0, + front_right: 1.0, + rear_left: -1.0, + rear_right: -2.0, + }; + + let normalized = normalize(speeds); + + assert!(close(normalized.front_left, 1.0)); + assert!(close(normalized.front_right, 0.5)); + assert!(close(normalized.rear_left, -0.5)); + assert!(close(normalized.rear_right, -1.0)); + } + + /// Check that the avoid command still points backward and sideways. + #[test] + fn avoid_turns_back_and_right() { + let vector = command_to_vector(MotionCommand::Avoid); + + assert!(close(vector.forward, -0.3)); + assert!(close(vector.strafe, 0.0)); + assert!(close(vector.rotate, 0.3)); + } +} diff --git a/src/bin/11_kinematics.rs b/src/bin/11_kinematics.rs new file mode 100644 index 0000000..e7a5fc4 --- /dev/null +++ b/src/bin/11_kinematics.rs @@ -0,0 +1,192 @@ +// Kinematics. +// Solve simple arm geometry with limits. +// Reject motion that cannot work safely. + +#[derive(Debug, Clone, Copy, PartialEq)] +struct Millimeters(f32); + +#[derive(Debug, Clone, Copy, PartialEq)] +struct Degrees(f32); + +#[derive(Debug, Clone, Copy, PartialEq)] +struct ArmTarget { + x: Millimeters, + y: Millimeters, + z: Millimeters, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +struct JointAngles { + base: Degrees, + shoulder: Degrees, + elbow: Degrees, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum KinematicsError { + TargetOutOfReach, + JointLimitExceeded, + CollisionRisk, +} + +const LINK_A_MM: f32 = 120.0; +const LINK_B_MM: f32 = 110.0; +const BASE_LIMIT_DEG: f32 = 150.0; +const CLEARANCE_MM: f32 = 20.0; + +/// Turn joint angles into a simple arm target. +fn forward_kinematics(angles: JointAngles) -> ArmTarget { + let base = angles.base.0.to_radians(); + let shoulder = angles.shoulder.0.to_radians(); + let elbow_bend = (180.0 - angles.elbow.0).to_radians(); + + let arm = LINK_A_MM * shoulder.cos() + LINK_B_MM * (shoulder + elbow_bend).cos(); + let z = LINK_A_MM * shoulder.sin() + LINK_B_MM * (shoulder + elbow_bend).sin(); + + ArmTarget { + x: Millimeters(arm * base.cos()), + y: Millimeters(arm * base.sin()), + z: Millimeters(z), + } +} + +/// Solve a simplified 3D target with a two-link arm model. +fn inverse_kinematics(target: ArmTarget) -> Result { + let base = target.y.0.atan2(target.x.0).to_degrees(); + + if base.abs() > BASE_LIMIT_DEG { + return Err(KinematicsError::JointLimitExceeded); + } + + let planar = (target.x.0 * target.x.0 + target.y.0 * target.y.0).sqrt(); + let reach = (planar * planar + target.z.0 * target.z.0).sqrt(); + let max_reach = LINK_A_MM + LINK_B_MM; + let min_reach = (LINK_A_MM - LINK_B_MM).abs(); + + if reach > max_reach || reach < min_reach { + return Err(KinematicsError::TargetOutOfReach); + } + + if planar < CLEARANCE_MM && target.z.0 < CLEARANCE_MM { + return Err(KinematicsError::CollisionRisk); + } + + let lift = target.z.0.atan2(planar).to_degrees(); + let elbow_bend = ((reach * reach - LINK_A_MM * LINK_A_MM - LINK_B_MM * LINK_B_MM) + / (2.0 * LINK_A_MM * LINK_B_MM)) + .clamp(-1.0, 1.0) + .acos() + .to_degrees(); + let shoulder_pull = ((LINK_B_MM * elbow_bend.to_radians().sin()) + / (LINK_A_MM + LINK_B_MM * elbow_bend.to_radians().cos())) + .atan() + .to_degrees(); + let shoulder = lift - shoulder_pull; + let elbow = 180.0 - elbow_bend; + + if !(-60.0..=170.0).contains(&shoulder) || !(5.0..=175.0).contains(&elbow) { + return Err(KinematicsError::JointLimitExceeded); + } + + Ok(JointAngles { + base: Degrees(base), + shoulder: Degrees(shoulder), + elbow: Degrees(elbow), + }) +} + +/// Check whether two floats are close enough for workshop math. +#[cfg(test)] +fn close(a: f32, b: f32) -> bool { + (a - b).abs() < 0.5 +} + +fn main() { + let target = ArmTarget { + x: Millimeters(140.0), + y: Millimeters(40.0), + z: Millimeters(60.0), + }; + + match inverse_kinematics(target) { + Ok(angles) => { + let check = forward_kinematics(angles); + println!("target = {target:?}"); + println!("angles = {angles:?}"); + println!("forward check = {check:?}"); + } + Err(err) => { + println!("target = {target:?}"); + println!("error = {err:?}"); + } + } +} + +#[cfg(test)] +mod tests { + use super::{ + ArmTarget, KinematicsError, Millimeters, close, forward_kinematics, inverse_kinematics, + }; + + /// Check that a reachable target returns angles. + #[test] + fn reachable_target_solves() { + let target = ArmTarget { + x: Millimeters(120.0), + y: Millimeters(40.0), + z: Millimeters(50.0), + }; + + let angles = inverse_kinematics(target).expect("target should be reachable"); + let check = forward_kinematics(angles); + + assert!(close(check.x.0, target.x.0)); + assert!(close(check.y.0, target.y.0)); + assert!(close(check.z.0, target.z.0)); + } + + /// Check that a far target is rejected. + #[test] + fn far_target_is_out_of_reach() { + let target = ArmTarget { + x: Millimeters(400.0), + y: Millimeters(0.0), + z: Millimeters(0.0), + }; + + assert_eq!( + inverse_kinematics(target), + Err(KinematicsError::TargetOutOfReach) + ); + } + + /// Check that a target past the base limit is rejected. + #[test] + fn joint_limit_is_enforced() { + let target = ArmTarget { + x: Millimeters(-120.0), + y: Millimeters(0.0), + z: Millimeters(60.0), + }; + + assert_eq!( + inverse_kinematics(target), + Err(KinematicsError::JointLimitExceeded) + ); + } + + /// Check that a low target close to the base is blocked. + #[test] + fn collision_risk_is_rejected() { + let target = ArmTarget { + x: Millimeters(10.0), + y: Millimeters(10.0), + z: Millimeters(0.0), + }; + + assert_eq!( + inverse_kinematics(target), + Err(KinematicsError::CollisionRisk) + ); + } +} diff --git a/src/bin/12_embassy.rs b/src/bin/12_embassy.rs new file mode 100644 index 0000000..dcc04f7 --- /dev/null +++ b/src/bin/12_embassy.rs @@ -0,0 +1,65 @@ +// Embassy async loop. +// Show timers, tasks, and printing together. +// This is why a framework can beat a hand-written loop. + +#![allow(unsafe_code)] +#![no_std] +#![no_main] + +use cortex_m_semihosting::hprintln; +use embassy_executor::Spawner; +use embassy_time::{Duration, Ticker, Timer}; +use panic_probe as _; + +// Stub out interrupts that the PAC expects but this chapter does not use. +#[allow(non_snake_case)] +#[unsafe(no_mangle)] +pub extern "C" fn USB_HP_CAN1_TX() {} + +// Stub out interrupts that the PAC expects but this chapter does not use. +#[allow(non_snake_case)] +#[unsafe(no_mangle)] +pub extern "C" fn USB_LP_CAN1_RX0() {} + +// Stub out interrupts that the PAC expects but this chapter does not use. +#[allow(non_snake_case)] +#[unsafe(no_mangle)] +pub extern "C" fn CAN1_RX1() {} + +// Stub out interrupts that the PAC expects but this chapter does not use. +#[allow(non_snake_case)] +#[unsafe(no_mangle)] +pub extern "C" fn CAN1_SCE() {} + +// Stub out interrupts that the PAC expects but this chapter does not use. +#[allow(non_snake_case)] +#[unsafe(no_mangle)] +pub extern "C" fn RTC_ALARM() {} + +// Stub out interrupts that the PAC expects but this chapter does not use. +#[allow(non_snake_case)] +#[unsafe(no_mangle)] +pub extern "C" fn USBWAKEUP() {} + +#[embassy_executor::task] +async fn heartbeat() { + let mut ticker = Ticker::every(Duration::from_secs(1)); + + loop { + ticker.next().await; + hprintln!("embassy heartbeat"); + } +} + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + let _p = embassy_stm32::init(Default::default()); + + hprintln!("embassy boot"); + spawner.spawn(heartbeat()).unwrap(); + + loop { + Timer::after_secs(2).await; + hprintln!("embassy main loop"); + } +} diff --git a/verify-all.sh b/verify-all.sh new file mode 100644 index 0000000..162d9c7 --- /dev/null +++ b/verify-all.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$repo_root" + +host_bins=( + --bin 00_check + --bin 01_rust_basics + --bin 02_ownership + --bin 03_types + --bin 07_events + --bin 09_sensor_pipeline + --bin 10_motor_commands + --bin 11_kinematics +) + +embedded_bins=( + --target thumbv7m-none-eabi + --bin 04_first_firmware + --bin 05_rgb_output + --bin 06_button_events + --bin 08_state_machine + --bin 12_embassy +) + +cargo fmt --all --check +cargo clippy "${host_bins[@]}" -- -D warnings +cargo clippy "${embedded_bins[@]}" -- -D warnings +cargo test "${host_bins[@]}" +cargo build "${embedded_bins[@]}" diff --git a/workshop.md b/workshop.md new file mode 100644 index 0000000..cccc4f3 --- /dev/null +++ b/workshop.md @@ -0,0 +1,1426 @@ +# Rust on Robots +## Hands-on Embedded Rust on STM32 + +**Duration:** 120 minutes +**Participants:** ~20 + +--- +# Today is about robot firmware architecture + +We use Rust to understand how robot software is structured: + +```text +hardware signal + | +software type + | +robot event + | +robot state + | +decision + | +actuator command +``` + +--- +# We will not build a full robot in two hours + +We do not have: + +- full chassis +- motors +- motor drivers +- camera module +- ultrasonic hardware +- playing field + +--- +# We will build a robot framework +We do have: + +- STM32 Blue Pill +- ST-Link programmer +- RGB LED +- 5-way button +- Rust +- a robot control model + +=> The STM32 board is our small, reduced robot. + +--- +# The workshop target + +By the end, we want this model: + +```text +button / sensor input + | +RobotEvent + | +RobotMode + | +MotionCommand + | +LED status or wheel-speed model +``` + +This is the same shape as a real robot controller. + +Different hardware. Same architecture. + +--- +# Room calibration + +1. Who has written Rust before? +2. Who has programmed a microcontroller before? +3. Who has built, programmed or broken a robot before? + +--- +# Get the Repo + +Code along and try out yourself + +`git.wieerwill.dev/wieerwill/rust-stm32` + +1. Clone it +2. install RustUp or Nix shell +3. try it out (more in Chapter 0) + +--- +# My RoboCup Junior Soccer journey +A soccer robot starts simple: + +```text +turn on + | +show status + | +wait for start + | +find the ball + | +drive toward the ball + | +avoid problems + | +recover and continue +``` + +--- +# A (soccer) robot is a loop with consequences +```text +observe + | +interpret + | +decide + | +move + | +observe again +``` + +The hard part is keeping this loop understandable when reality becomes messy. + +--- +# Robot software touches the real world + +A robot has messy inputs: + +- camera frame +- ball position +- line / field detection +- ultrasonic distance +- start button +- battery state +- motor feedback + +And physical outputs: + +- wheel speeds +- LED status +- logs +- stop behaviour +- recovery behaviour + +--- +# The board is our reduced robot + +```text +RGB LED -> actuator / status output +5-way button -> input device / fake sensor rig +STM32 loop -> robot control loop +Rust types -> domain model +``` + +Small hardware. Real architecture. + +--- +# Chapter map + +```text +00_check toolchain check +01_rust_basics values, functions, mutation +02_ownership ownership and borrowing +03_types domain types, enums, Result +04_first_firmware minimal STM32 firmware +05_rgb_output LED as actuator +06_button_events button as sensor +07_events raw input to robot event +08_state_machine robot behaviour +09_sensor_pipeline camera and ultrasonic model +10_motor_commands motion intent to wheel speeds +11_kinematics geometry and inverse kinematics +12_embassy async Embassy loop and timers +``` + +--- +# Chapter 0: Toolchain +--- +# Chapter 0: Toolchain check + +Open: `src/bin/00_check.rs` + +Run: `run-00` + +Expected result: +```text +The host program prints the workshop toolchain check. +``` + +--- +# Why Rust for this? + +- Who owns this peripheral? +- Who may mutate robot state? +- What happens when a sensor fails? +- Which commands are valid? +- Which states should be impossible? +- Where does hardware end and behaviour begin? + +Rust makes many of those boundaries visible. + +--- +# Rust is strict in useful places +- memory safety without a garbage collector +- explicit ownership +- controlled mutation +- strong enums and pattern matching +- `Option` and `Result` +- small `no_std` programs for microcontrollers + +--- +# But Rust does not fix everything +- wrong wiring +- noisy sensors +- weak batteries +- bad control logic +- missing calibration + +--- +# Chapter 01: Values +--- +# Chapter 01: Values are explicit + +```rust +let speed = 42; +let mut target_speed = 0; + +target_speed = speed; +``` + +Default: values are immutable. +Mutation must be requested with `mut`. + +For robot code, that is a good default. + +--- +# Robot translation: Mutation should have a home +Bad: +```text +everything can change everything +``` + +Better: +```text +sensor reading + | +event + | +state transition + | +command +``` + +The robot state changes in one obvious place. + +--- +# Chapter 01 exercise: Change behaviour without changing structure +Open: +```text +src/bin/01_rust_basics.rs +``` + +Tasks: +1. Change a constant. +2. Change a function return value. +3. Add a new status value. +4. Run the program. + +Checkpoint: +```text +run-01 +``` + +--- +# Chapter 02: Ownership +--- +# Chapter 02: Ownership means responsibility +```rust +let command = String::from("forward"); +let next_command = command; +// command is no longer usable here +``` + +One value has one owner. +When ownership moves, responsibility moves. + +--- +# Robot translation: Peripherals are resources +A GPIO pin is not just an integer. + +It represents access to real hardware. + +```text +LED pin owner + | +configure output + | +set high / low +``` + +Ownership makes accidental sharing harder. + +--- +# Borrowing means temporary access +```rust +fn print_command(command: &str) { + println!("command = {command}"); +} + +let command = String::from("forward"); +print_command(&command); + +// command is still usable here +``` + +A borrow can inspect without taking ownership. + +--- +# Mutable borrowing: One writer at a time +```rust +fn stop(command: &mut MotionCommand) { + command.forward = 0.0; + command.strafe = 0.0; + command.rotate = 0.0; +} +``` + +Rust does not like hidden shared mutation. + +For robot code, that is useful. + +--- +# Robot translation: Who may change the robot state? +A robot state should not be mutated from everywhere. + +Better: +```text +sensor module -> creates observations +event module -> creates RobotEvent +state machine -> changes RobotMode +command module -> creates MotionCommand +hardware module -> applies output +``` + +Each layer has a job. + +--- +# Chapter 02 exercise: Fix the ownership error +Open: +```text +src/bin/02_ownership.rs +``` + +Tasks: +1. Run it. +2. Read the compiler error. +3. Fix it with a borrow. +4. Add a mutable command update. + +Checkpoint: +```text +run-02 +``` + +--- +# Chapter 03: Types +--- +# Chapter 03: Numbers are not enough +```rust +let value = 42; +``` + +What is it? +- centimetres? +- percent? +- PWM duty cycle? +- motor speed? +- battery voltage? +- camera pixel coordinate? + +Robot code needs meaning, not just numbers. + +--- +# Types are safety rails +```rust +struct DistanceCm(u16); +struct MotorPower(f32); +struct BallXPixel(u16); +struct BatteryMillivolts(u16); +``` + +A number alone does not tell us what it means. + +Types carry meaning. + +--- +# Enums model robot reality better than booleans +```rust +enum BallObservation { + NotSeen, + Left, + Center, + Right, + TooClose, +} +``` + +This is clearer than: + +```rust +ball_seen: bool +ball_left: bool +ball_right: bool +``` + +Because impossible combinations disappear. + +--- +# Failure is part of the API + +```rust +enum SensorError { + Timeout, + OutOfRange, + NotCalibrated, +} + +fn read_distance() -> Result { + todo!() +} +``` + +A sensor may fail. + +The type should admit that. + +--- +# Robot translation: Sensor data is evidence +A sensor reading is not behaviour. + +```text +raw echo time + | +distance + | +classification + | +RobotEvent + | +state transition +``` + +The robot should not directly react to raw numbers. + +--- +# Chapter 03 exercise: Replace loose values with domain types +Open: +```text +src/bin/03_types.rs +``` + +Tasks: +1. Replace a raw distance number with `DistanceCm`. +2. Replace booleans with `BallObservation`. +3. Return `Result` from a fake sensor function. +4. Match on the result. + +Checkpoint: +```text +run-03 +``` + +--- +# Chapter 04: Embedded Rust +--- +# Chapter 04: First embedded Rust program +On the STM32 we usually do not have: +- an operating system +- heap by default +- files +- a normal terminal +- `std` + +We write `no_std` firmware. + +--- +# Bare metal means we own the loop +```text +configure hardware + | +enter loop + | +read input + | +update state + | +write output + | +repeat +``` + +That loop is the beginning of a robot controller. + +--- +# Firmware is not an app with a main window +On a microcontroller: +- startup code prepares the chip +- firmware configures peripherals +- the loop runs forever +- timing is explicit +- debugging is limited +- output may be one LED + +--- +# Tooling checkpoint +Open: +```text +src/bin/04_first_firmware.rs +``` + +Build: +```text +build-04 +``` + +Flash: +```text +run-04 +``` + +Expected result: The board boots and reaches the firmware loop. + +--- +# Hardware silence is a valid embedded state +If nothing happens, debug in layers: +1. Is the board powered? +2. Is the ST-Link visible? +3. Is the chip target correct? +4. Did the firmware flash? +5. Is the pin correct? +6. Is the LED orientation correct? + +Do not randomly change five things at once! + +--- +# Embedded debugging is layered +1. host build works? +2. target build works? +3. probe visible? +4. flash succeeds? +5. firmware boots? +6. pin toggles? +7. external wiring works? + +Debug from the outside in. + +--- +# Chapter 05: Output +--- +# Chapter 05: Output - LED as actuator +An LED is a tiny actuator. + +It gives us visible robot state: + +```text +Off -> idle +Blue blink -> searching +Green -> driving +Red blink -> obstacle / error +``` + +The LED becomes the robot dashboard. + +--- +# Robot translation: Status output matters +A robot needs visible state because: +- it may not have a screen +- logs may be unavailable +- behaviour may look ambiguous +- debugging happens on the floor +- humans need to know if it is safe + +A status LED is not decoration. + +--- +# Chapter 05 exercise: Status output +Open: +```text +src/bin/05_rgb_output.rs +``` + +Tasks: +1. Make the LED blink. +2. Change the colour. +3. Add a status pattern. +4. Use one pattern for `Idle`. +5. Use one pattern for `Searching`. + +Checkpoint: +```text +run-05 +``` + +--- +# Chapter 06: Input +--- +# Chapter 06: Input - button as sensor +The 5-way button becomes our fake sensor rig. + +```text +Center -> start / stop +Left -> ball left +Right -> ball right +Up -> ball centered +Down -> obstacle detected +``` + +This lets us simulate camera or ultrasonic input. + +--- +# Raw input is not robot meaning +Button press: +```text +raw electrical state +``` + +Robot event: +```text +BallLeft +ObstacleDetected +StartStop +``` + +We separate hardware reading from robot meaning. + +--- +# Robot translation: Perception is a boundary +The controller should not care whether `BallLeft` came from: +- a camera +- a test file +- a simulator +- a button +- a future ML model + +It only needs the event. + +--- +# Chapter 06 exercise: Button to event +Open: +```text +src/bin/06_button_events.rs +``` + +Tasks: +1. Read the 5-way button. +2. Convert direction to `RobotEvent`. +3. Show the event with LED status or logging. +4. Add a default `NoEvent` case. + +Checkpoint: +```text +run-06 +``` + +--- +# Chapter 07: Events +--- +# Chapter 07: Events +```rust +enum RobotEvent { + NoEvent, + StartStop, + BallLeft, + BallRight, + BallCentered, + BallLost, + ObstacleDetected, + Timeout, +} +``` + +Events are interpreted facts. + +They are not raw hardware. + +--- +# Event priority matters +What should win? +```text +BallCentered + ObstacleDetected +``` + +Probably not: +```text +drive forward +``` + +Some events are safety-critical. + +--- +# Chapter 07 exercise: Add event priority +Open: +```text +src/bin/07_events.rs +``` + +Tasks: +1. Convert button direction to `RobotEvent`. +2. Add a function `prioritize_event`. +3. Ensure `ObstacleDetected` wins. +4. Keep `NoEvent` harmless. + +Checkpoint: +```text +test-07 +``` + +--- +# Chapter 08: State Machine +--- +# Chapter 08: Robot state machine +The robot needs memory. + +```rust +enum RobotMode { + Idle, + SearchBall, + AlignToBall, + DriveToBall, + AvoidObstacle, + Error, +} +``` + +It must know what it is currently trying to do. + +--- +# State is not the same as event +Event: +```text +BallLeft +``` + +State: +```text +AlignToBall +``` + +Command: +```text +RotateLeft +``` + +Keep them separate. + +--- +# State produces command +```rust +enum MotionCommand { + Stop, + Forward, + RotateLeft, + RotateRight, + StrafeLeft, + StrafeRight, + Avoid, +} +``` + +The robot should think in intent first. + +--- +# Decision function +```rust +fn decide( + mode: RobotMode, + event: RobotEvent, +) -> (RobotMode, MotionCommand) { + match (mode, event) { + (_, RobotEvent::ObstacleDetected) => + (RobotMode::AvoidObstacle, MotionCommand::Stop), + + (RobotMode::Idle, RobotEvent::StartStop) => + (RobotMode::SearchBall, MotionCommand::RotateLeft), + + (RobotMode::SearchBall, RobotEvent::BallLeft) => + (RobotMode::AlignToBall, MotionCommand::RotateLeft), + + (RobotMode::SearchBall, RobotEvent::BallRight) => + (RobotMode::AlignToBall, MotionCommand::RotateRight), + + (RobotMode::SearchBall, RobotEvent::BallCentered) => + (RobotMode::DriveToBall, MotionCommand::Forward), + + (mode, _) => + (mode, MotionCommand::Stop), + } +} +``` + +One place. One transition table. Less mystery. + +--- +# Chapter 08 exercise: Make the robot behave +Open: +```text +src/bin/08_state_machine.rs +``` + +Tasks: +1. Add `BallLost -> SearchBall`. +2. Add `StartStop -> Idle`. +3. Ensure `ObstacleDetected` always wins. +4. Map `RobotMode` to LED status. +5. Add one new mode or command. + +Checkpoint: +```text +run-08 +``` + +--- +# We can test robot behaviour without a robot +```rust +#[test] +fn obstacle_wins_over_ball() { + let (mode, command) = decide( + RobotMode::DriveToBall, + RobotEvent::ObstacleDetected, + ); + + assert_eq!(mode, RobotMode::AvoidObstacle); + assert_eq!(command, MotionCommand::Stop); +} +``` + +Robot logic can be tested on the laptop. + +--- +# Chapter 09: Sensors +--- +# Chapter 09: Camera model +Real camera pipeline: +```text +camera frame + | +color / blob / ML detection + | +BallObservation + | +RobotEvent +``` + +--- +# Camera model -> Workshop model +```rust +enum BallObservation { + NotSeen, + Left, + Center, + Right, + TooClose, +} +``` + +The controller should not care *how* the ball was detected. + +--- +# Camera data is not clean +A camera may give: +- no ball +- multiple candidates +- wrong colour +- motion blur +- partial visibility +- bad lighting +- stale frame + +So convert pixels into a small domain model (later). + +--- +# Chapter 09: Ultrasonic model +```rust +struct RawEchoMicros(u32); +struct DistanceCm(u16); +enum ObstacleObservation { + Clear, + Warning, + TooClose, +} +``` + +Pipeline: +```text +raw echo -> distance -> classification -> event +``` + +A distance value is not behaviour. + +--- +# Sensor fusion starts small +For a soccer robot with vision and Ultrasonic: +```text +BallCentered + Clear -> DriveToBall + +BallCentered + TooClose -> Stop or AvoidObstacle + +BallLost + Clear -> SearchBall + +BallLeft + Warning -> Align carefully +``` + +Multiple inputs must become one decision. + +--- +# Chapter 09 exercise: Sensor pipeline +Open: +```text +src/bin/09_sensor_pipeline.rs +``` + +Tasks: +1. Convert fake camera input to `BallObservation`. +2. Convert fake ultrasonic distance to `ObstacleObservation`. +3. Convert observations to `RobotEvent`. +4. Feed the event into `decide()`. +5. Add one test for obstacle priority. + +Checkpoint: +```text +test-09 +``` + +--- +# Chapter 10: Motion +--- +# Chapter 10: Motion commands +The robot does not think in PWM first. + +It thinks in motion intent: +```rust +struct MotionVector { + forward: f32, + strafe: f32, + rotate: f32, +} +``` + +Then a lower layer translates intent into wheel speeds. + +--- +# From symbolic command to vector +```rust +fn command_to_vector(command: MotionCommand) -> MotionVector { + match command { + MotionCommand::Stop => MotionVector { + forward: 0.0, + strafe: 0.0, + rotate: 0.0, + }, + MotionCommand::Forward => MotionVector { + forward: 0.6, + strafe: 0.0, + rotate: 0.0, + }, + MotionCommand::RotateLeft => MotionVector { + forward: 0.0, + strafe: 0.0, + rotate: -0.4, + }, + MotionCommand::RotateRight => MotionVector { + forward: 0.0, + strafe: 0.0, + rotate: 0.4, + }, + _ => MotionVector { + forward: 0.0, + strafe: 0.0, + rotate: 0.0, + }, + } +} +``` + +Intent becomes a numeric movement request. + +--- +# Four-wheel omni drive: Command to wheels +```rust +struct WheelSpeeds { + front_left: f32, + front_right: f32, + rear_left: f32, + rear_right: f32, +} + +fn mix(cmd: MotionVector) -> WheelSpeeds { + WheelSpeeds { + front_left: cmd.forward + cmd.strafe + cmd.rotate, + front_right: cmd.forward - cmd.strafe - cmd.rotate, + rear_left: cmd.forward - cmd.strafe + cmd.rotate, + rear_right: cmd.forward + cmd.strafe - cmd.rotate, + } +} +``` + +The formula is simple. The real robot is not. + +--- +# Real motors add boring but important details +A real motor layer needs: +- speed normalization +- motor orientation calibration +- PWM generation +- motor driver limits +- battery compensation +- emergency stop behaviour +- testing on the floor + +Rust helps keep the layers honest. + +--- +# Chapter 10 exercise: From robot command to wheel speeds +Open: +```text +src/bin/10_motor_commands.rs +``` + +Tasks: +1. Convert `MotionCommand` to `MotionVector`. +2. Convert `MotionVector` to `WheelSpeeds`. +3. Normalize speeds to `-1.0..=1.0`. +4. Add a test for `Stop`. +5. Add a test for `RotateLeft`. + +Checkpoint: +```text +test-10 +``` + +--- +# Chapter 11: Kinematics +--- +# Chapter 11: Kinematics enters when geometry matters +For a mobile base: +```text +robot motion intent -> wheel speeds +``` + +For a robot arm: +```text +target position -> joint angles -> motor commands +``` + +That second step is inverse kinematics. + +--- +# Forward kinematics asks: Where did we end up? +```text +joint angles + | +robot geometry + | +tool position +``` + +Example: + +```text +base angle + shoulder angle + elbow angle +=> x / y / z position +``` + +Good for checking and simulation. + +--- +# Inverse kinematics asks: How do we get there? +```text +target position + | +robot geometry + | +joint angles +``` + +Example: + +```rust +struct ArmTarget { + x_mm: f32, + y_mm: f32, + z_mm: f32, +} + +struct JointAngles { + base_deg: f32, + shoulder_deg: f32, + elbow_deg: f32, +} +``` + +--- +# Invalid movement should fail before hardware moves +```rust +enum KinematicsError { + TargetOutOfReach, + JointLimitExceeded, + CollisionRisk, +} + +fn inverse_kinematics( + target: ArmTarget, +) -> Result { + todo!() +} +``` + +The safest motor command is the one you never send. + +--- +# Chapter 11 exercise: Reject invalid motion +Open: +```text +src/bin/11_kinematics.rs +``` + +Tasks: +1. Run the tests. +2. Change one reachable target. +3. Add one rejected target. +4. Keep unsafe motion out of the command layer. + +Checkpoint: +```text +test-11 +``` + +--- +# Chapter 12: Embassy +--- +# Chapter 12: Why Embassy? +So far, we wrote the loop ourselves: +```text +configure hardware + ↓ +loop forever + ↓ +check time + ↓ +read input + ↓ +update state + ↓ +write output +``` + +But it becomes noisy when several things must happen at different rates. + +--- +# Chapter 12: Robots rarely do only one thing +A robot may need to: +* blink status LED every 500 ms +* read buttons or sensors often +* print diagnostics occasionally +* update behaviour state +* send motor commands regularly +* wait without blocking everything else + +A single hand-written loop can do this. + +It just gets ugly. + +--- +# Chapter 12: Reuse established embedded crates + +We do not need to build every firmware primitive ourselves. + +Embassy gives us reusable embedded building blocks: + +* async executor +* task spawning +* timers +* tickers +* async waiting +* embedded-focused scheduling model + +The setup stays explicit. + +The waiting and task structure get cleaner. + +--- +# Chapter 12: What Embassy is +Embassy is an embedded Rust framework built around async Rust. + +It provides an async executor designed for embedded systems. + +Important embedded properties: + +* no heap required +* tasks are statically allocated +* async/await works on microcontrollers +* timers can express waiting without busy-loop plumbing + +This is not a desktop runtime. + +--- +# Chapter 12: From one loop to tasks +Manual loop model: +```text +main loop + ↓ +do everything + ↓ +manually track time + ↓ +repeat +``` + +Embassy model: +```text +init hardware + ↓ +spawn tasks + ↓ +await timers + ↓ +executor resumes work +``` + +The firmware still owns the hardware. + +The structure is cleaner. + +--- +# Chapter 12: Timer-driven waiting +Instead of manual delay plumbing: +```rust +loop { + do_work(); + busy_wait(); +} +``` +Embassy lets us express waiting directly: +```rs +loop { + do_work(); + Timer::after(Duration::from_millis(500)).await; +} +``` +The code says what it means: +`do work, then wait` + +--- +# Chapter 12: Periodic work with Ticker +For periodic work: +```rs +let mut ticker = Ticker::every(Duration::from_millis(500)); +loop { + ticker.next().await; + hprintln!("heartbeat"); +} +``` + +A ticker is useful for: + +* heartbeat output +* periodic sensor sampling +* regular status updates +* control-loop prototypes + +--- +# Chapter 12: Async task shape +```rs +#[embassy_executor::task] +async fn heartbeat_task() { + let mut ticker = Ticker::every(Duration::from_millis(500)); + loop { + ticker.next().await; + hprintln!("heartbeat"); + } +} +``` +The task looks sequential. + +The executor handles waiting and resuming. + +--- +# Chapter 12: Main task stays explicit +```rs +#[embassy_executor::main] +async fn main(spawner: Spawner) { + spawner.spawn(heartbeat_task()).unwrap(); + loop { + hprintln!("main task alive"); + Timer::after(Duration::from_secs(2)).await; + } +} +``` +The structure is still visible: +```text +start executor + ↓ +spawn heartbeat + ↓ +main task keeps running +``` + +--- +# Chapter 12: Embassy example +Open: `src/bin/12_embassy.rs` + +Run: `run-12` + +Tasks: +1. Run the baseline. +2. Change the heartbeat text. +3. Change the timer period. +4. Add a second async task. +5. Compare this with chapter 04. +6. Decide which version is easier to extend. + +--- +# Chapter 12: What Embassy does not solve +Embassy does not remove: +* hardware setup +* ownership rules +* bad wiring +* wrong pin mapping +* sensor noise +* control logic bugs +* the need for testing on real hardware + +It helps structure waiting and concurrent tasks. + +It does not make the robot correct by itself. + +--- +# The full architecture we built toward +```text +camera / ultrasonic / button (Sensors) + | +raw reading + | +domain observation + | +RobotEvent + | +RobotMode + | +MotionCommand + | +MotionVector + | +WheelSpeeds + | +motor driver (Output) +``` + +--- +# Hardware and software work together in layers +```text +Hardware layer + GPIO, timers, PWM, ADC, buses + +Driver layer + LED, button, motor driver, sensor + +Domain layer + BallObservation, ObstacleObservation + +Behaviour layer + RobotMode, state machine + +Control layer + MotionCommand, wheel speeds +``` + +Do not mix this into one loop! + +--- +# What Rust helped with +- explicit ownership of resources +- visible mutation +- clear domain types +- enums for valid states +- `Result` for sensor failure +- testable robot logic +- separation between hardware and behaviour +- small embedded programs without `std` + +Rust did not remove the need for calibration, physics and debugging. + +--- +# What robotics taught us +- Raw input is not meaning +- Meaning is not behaviour +- Behaviour is not motor output +- Motor output is not guaranteed motion +- Real systems need feedback +- Safety should win over ambition + +The robot is a loop, not a script + +--- +# Where to continue +Next useful steps: + +0. Get yourself a microcontroller +1. Add real ultrasonic hardware +2. Add motor driver output +3. Add a camera module or external vision process +4. Add logs and host-side tests +5. Move repeated tasks to Embassy async +6. Put the board onto a small chassis +7. Test on the floor + +Build one layer at a time + +--- +# Useful references +- The Rust Book: ownership and borrowing +- The Embedded Rust Book: `no_std` and bare-metal concepts +- probe-rs: flashing and embedded debugging tooling +- RoboCupJunior Soccer rules and description