Initial commit
This commit is contained in:
9
.cargo/config.toml
Normal file
9
.cargo/config.toml
Normal 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
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
target/
|
||||
result
|
||||
.direnv/
|
||||
.DS_Store
|
||||
*.log
|
||||
Embed.local.*
|
||||
.embed.local.*
|
||||
906
Cargo.lock
generated
Normal file
906
Cargo.lock
generated
Normal 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
16
Cargo.toml
Normal 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
95
README.md
Normal 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
7
build.rs
Normal 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
42
docs/blue-pill.md
Normal 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
47
docs/troubleshooting.md
Normal 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
1748
examples.md
Normal file
File diff suppressed because it is too large
Load Diff
96
flake.lock
generated
Normal file
96
flake.lock
generated
Normal 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
85
flake.nix
Normal 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
7
memory.x
Normal 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
2318
presentation.html
Normal file
File diff suppressed because it is too large
Load Diff
5
rust-toolchain.toml
Normal file
5
rust-toolchain.toml
Normal 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
9
src/bin/00_check.rs
Normal 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
50
src/bin/01_rust_basics.rs
Normal 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
85
src/bin/02_ownership.rs
Normal 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
152
src/bin/03_types.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
25
src/bin/04_first_firmware.rs
Normal file
25
src/bin/04_first_firmware.rs
Normal 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
73
src/bin/05_rgb_output.rs
Normal 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
111
src/bin/06_button_events.rs
Normal 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
91
src/bin/07_events.rs
Normal 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
240
src/bin/08_state_machine.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
208
src/bin/09_sensor_pipeline.rs
Normal file
208
src/bin/09_sensor_pipeline.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
179
src/bin/10_motor_commands.rs
Normal file
179
src/bin/10_motor_commands.rs
Normal 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
192
src/bin/11_kinematics.rs
Normal 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
65
src/bin/12_embassy.rs
Normal 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
31
verify-all.sh
Normal 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
1426
workshop.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user