Initial commit

This commit is contained in:
2026-07-03 12:59:18 +02:00
commit fcf95f44e9
30 changed files with 8325 additions and 0 deletions

9
.cargo/config.toml Normal file
View File

@@ -0,0 +1,9 @@
[target.thumbv7m-none-eabi]
linker = "flip-link"
rustflags = [
"-C",
"link-arg=-Tlink.x",
]
[env]
DEFMT_LOG = "info"

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
target/
result
.direnv/
.DS_Store
*.log
Embed.local.*
.embed.local.*

906
Cargo.lock generated Normal file
View File

@@ -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",
]

16
Cargo.toml Normal file
View File

@@ -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"] }

95
README.md Normal file
View File

@@ -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).

7
build.rs Normal file
View File

@@ -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()
);
}

42
docs/blue-pill.md Normal file
View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

47
docs/troubleshooting.md Normal file
View File

@@ -0,0 +1,47 @@
# Troubleshooting
## Probe Not Found
- Run `probe-rs list`.
- Check USB cable and power.
- Check `SWDIO`, `SWCLK`, `GND`, and `3V3`.
## Linux Udev Permissions
- You may need udev rules for ST-Link or your probe.
- Install the vendor or distro rules, then replug the probe.
## Wrong Chip Selected
- Run `probe-rs chip list | rg STM32F103`.
- Override the default with `PROBE_RS_CHIP=STM32F103C8`.
## No Firmware Output
- Use `run-04` or `run-08` from `nix develop`.
- Confirm the firmware did not crash early.
- Confirm the probe stays attached.
- Confirm `BOOT0` is low.
## Host Chapter Does Not Run
- Use `run-00`, `run-01`, `run-02`, or `run-03`.
- If you skipped Nix, make sure `rustup toolchain install 1.95.0` and `rustup target add thumbv7m-none-eabi` are done.
- If you skipped Nix, make sure `flip-link` is on your `PATH` for embedded builds.
## Blue Pill LED Looks Inverted
- That is expected on most boards.
- `PC13` is usually active-low, so driving it low turns the LED on.
## Safety
- Check board voltage before connecting anything.
- Check `SWDIO`, `SWCLK`, `GND`, and `3V3` wiring before flashing.
- Do not assume a random button or LED pinout without checking the board.
## Contribution
- Keep `nix develop`, `fmt`, `check`, and the chapter build or run aliases working.
- Document hardware assumptions when they matter.
- Do not claim hardware-tested behavior unless it was actually tested on hardware.

1748
examples.md Normal file

File diff suppressed because it is too large Load Diff

96
flake.lock generated Normal file
View File

@@ -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
}

85
flake.nix Normal file
View File

@@ -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}"
'';
};
});
}

7
memory.x Normal file
View File

@@ -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
}

2318
presentation.html Normal file

File diff suppressed because it is too large Load Diff

5
rust-toolchain.toml Normal file
View File

@@ -0,0 +1,5 @@
[toolchain]
channel = "1.95.0"
components = ["clippy", "rustfmt", "rust-src", "llvm-tools-preview"]
targets = ["thumbv7m-none-eabi"]
profile = "minimal"

9
src/bin/00_check.rs Normal file
View File

@@ -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");
}

50
src/bin/01_rust_basics.rs Normal file
View File

@@ -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");
}
}

85
src/bin/02_ownership.rs Normal file
View File

@@ -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);
}
}

152
src/bin/03_types.rs Normal file
View File

@@ -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<DistanceCm, SensorError> {
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<u16>) -> 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);
}
}

View File

@@ -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);
}
}

73
src/bin/05_rgb_output.rs Normal file
View File

@@ -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
};
}
}

111
src/bin/06_button_events.rs Normal file
View File

@@ -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);
}
}

91
src/bin/07_events.rs Normal file
View File

@@ -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);
}
}

240
src/bin/08_state_machine.rs Normal file
View File

@@ -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);
}
}

View File

@@ -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<u16>);
#[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<DistanceCm, SensorError> {
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
);
}
}

View File

@@ -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));
}
}

192
src/bin/11_kinematics.rs Normal file
View File

@@ -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<JointAngles, KinematicsError> {
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)
);
}
}

65
src/bin/12_embassy.rs Normal file
View File

@@ -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");
}
}

31
verify-all.sh Normal file
View File

@@ -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[@]}"

1426
workshop.md Normal file

File diff suppressed because it is too large Load Diff