diff --git a/.cargo/config.toml b/.cargo/config.toml index 971c54803626c62bbb93ae6a47d043394d8c3dc9..2ac7976e771a56c2d3b21655589757b228f705a3 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -6,8 +6,5 @@ rustflags = [ "-C", "link-arg=-Tdefmt.x", ] -[build] -target = "thumbv7m-none-eabi" - [env] DEFMT_LOG = "info" diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5d5bc5c7f2664596f31b37bcd99dc574630fea85..42020fd8f8b6d879f7945558c69978c3756fd5c8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,15 +15,15 @@ stages: - .rustup - target/debug/deps - target/debug/build - - target/thumbv7m-none-eabi/release/deps - - target/thumbv7m-none-eabi/release/build + - target/thumbv7m-none-eabi/production/deps + - target/thumbv7m-none-eabi/production/build policy: pull-push variables: RUSTUP_HOME: ${CI_PROJECT_DIR}/.rustup CARGO_HOME: ${CI_PROJECT_DIR}/.cargo -rust-lint: +lint:rust: image: rust stage: check script: @@ -31,50 +31,79 @@ rust-lint: - rustup component add rustfmt clippy - rustup target install thumbv7m-none-eabi - cargo fmt --all --check - - cargo clippy --all + - cargo clippy --target thumbv7m-none-eabi -p bootloader -p bootloader-params -p controller -p i2c2-target -p pid -p support + - cargo clippy -p firmware-updater <<: *rust-cache -python-lint: +lint:python: image: alpine stage: check script: - apk add py3-flake8 + - flake8 bootloader/python - flake8 controller/python -build-rust: +build:rust:embedded: image: rust stage: build + needs: ["lint:rust"] script: - rustup default nightly - rustup target install thumbv7m-none-eabi - - cargo build --release + - (cd bootloader && cargo build --profile production) + - (cd controller && cargo build --profile production) <<: *rust-cache artifacts: paths: - - target/thumbv7m-none-eabi/release/dc-motor-driver-hat + - target/thumbv7m-none-eabi/production/bootloader + - target/thumbv7m-none-eabi/production/controller -test-rust: - image: rust - stage: test +build:rust:firmware-updater: + image: messense/rust-musl-cross:aarch64-musl + stage: build + needs: ["lint:rust"] script: - - rustup default nightly - - cd pid - - cargo test --target x86_64-unknown-linux-gnu - <<: *rust-cache + - rustup default stable + - rustup target add aarch64-unknown-linux-musl + - cd firmware-updater + - cargo build --target aarch64-unknown-linux-musl --profile production + artifacts: + paths: + - target/aarch64-unknown-linux-musl/production/firmware-updater + +test:rust: + image: rust + stage: test + script: + - rustup default stable + - cargo test -p pid -test-python: +test:python: image: python stage: test + needs: ["lint:python"] script: - cd controller/python - python -munittest controller_test.py +test:firmware: + image: rust + stage: test + needs: ["build:rust:embedded", "build:rust:firmware-updater"] + script: + - rustup default stable + - cargo build -p firmware-updater + - cargo run -q -p firmware-updater -- check-file target/thumbv7m-none-eabi/production/controller + deploy: image: alpine stage: deploy script: - mkdir firmware - - cp target/thumbv7m-none-eabi/release/dc-motor-driver-hat firmware/ + - cp target/thumbv7m-none-eabi/production/bootloader firmware/ + - cp target/thumbv7m-none-eabi/production/controller firmware/ + - cp target/aarch64-unknown-linux-musl/production/firmware-updater firmware/ + - cp bootloader/python/bootloader.py firmware/ - cp controller/python/controller.py firmware/ artifacts: paths: diff --git a/Cargo.lock b/Cargo.lock index 8ba760395868d168bf04ae1997cc12c492c8a34d..025a8d0cf7006e2c031ce977101da1c65c2a5d88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,91 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "anstream" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" + +[[package]] +name = "anstyle-parse" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +dependencies = [ + "anstyle", + "windows-sys", +] + [[package]] name = "autocfg" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "bare-metal" version = "0.2.5" @@ -41,6 +120,32 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "bootloader" +version = "0.1.0" +dependencies = [ + "bootloader-params", + "build-support", + "cortex-m", + "cortex-m-rt", + "critical-section", + "defmt", + "defmt-rtt", + "embassy-executor", + "embassy-stm32", + "embassy-sync", + "embassy-time", + "futures", + "heapless", + "i2c2-target", + "panic-probe", + "support", +] + +[[package]] +name = "bootloader-params" +version = "0.1.0" + [[package]] name = "build-support" version = "0.1.0" @@ -55,12 +160,126 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "cc" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f6b81fb3c84f5563d509c59b5a48d935f689e993afa90fe39047f05adef9142" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca6706fd5224857d9ac5eb9355f6683563cc0541c7cd9d014043b57cbec78ac" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.71", +] + +[[package]] +name = "clap_lex" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" + +[[package]] +name = "color-eyre" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + +[[package]] +name = "colorchoice" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" + +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys", +] + +[[package]] +name = "controller" +version = "0.3.1" +dependencies = [ + "bootloader-params", + "build-support", + "cortex-m", + "cortex-m-rt", + "critical-section", + "defmt", + "defmt-rtt", + "embassy-executor", + "embassy-stm32", + "embassy-sync", + "embassy-time", + "futures", + "heapless", + "i2c2-target", + "panic-probe", + "support", +] + [[package]] name = "cortex-m" version = "0.7.7" @@ -135,27 +354,6 @@ dependencies = [ "syn 2.0.71", ] -[[package]] -name = "dc-motor-driver-hat" -version = "0.3.0" -dependencies = [ - "build-support", - "cortex-m", - "cortex-m-rt", - "critical-section", - "defmt", - "defmt-rtt", - "embassy-executor", - "embassy-stm32", - "embassy-sync", - "embassy-time", - "futures", - "heapless", - "i2c2-target", - "panic-probe", - "support", -] - [[package]] name = "defmt" version = "0.3.8" @@ -207,10 +405,16 @@ dependencies = [ "litrs", ] +[[package]] +name = "elf" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4445909572dbd556c457c849c4ca58623d84b27c8fff1e74b0b4227d8b90d17b" + [[package]] name = "embassy-embedded-hal" version = "0.1.0" -source = "git+https://github.com/embassy-rs/embassy#e54c753537b4b12c3d2fd03ad8e8ba9eaaded06e" +source = "git+https://github.com/embassy-rs/embassy#2537fc6f4fcbdaa0fcea45a37382d61f59cc5767" dependencies = [ "defmt", "embassy-futures", @@ -227,7 +431,7 @@ dependencies = [ [[package]] name = "embassy-executor" version = "0.5.0" -source = "git+https://github.com/embassy-rs/embassy#e54c753537b4b12c3d2fd03ad8e8ba9eaaded06e" +source = "git+https://github.com/embassy-rs/embassy#2537fc6f4fcbdaa0fcea45a37382d61f59cc5767" dependencies = [ "cortex-m", "critical-section", @@ -241,7 +445,7 @@ dependencies = [ [[package]] name = "embassy-executor-macros" version = "0.4.1" -source = "git+https://github.com/embassy-rs/embassy#e54c753537b4b12c3d2fd03ad8e8ba9eaaded06e" +source = "git+https://github.com/embassy-rs/embassy#2537fc6f4fcbdaa0fcea45a37382d61f59cc5767" dependencies = [ "darling", "proc-macro2", @@ -252,12 +456,12 @@ dependencies = [ [[package]] name = "embassy-futures" version = "0.1.1" -source = "git+https://github.com/embassy-rs/embassy#e54c753537b4b12c3d2fd03ad8e8ba9eaaded06e" +source = "git+https://github.com/embassy-rs/embassy#2537fc6f4fcbdaa0fcea45a37382d61f59cc5767" [[package]] name = "embassy-hal-internal" version = "0.1.0" -source = "git+https://github.com/embassy-rs/embassy#e54c753537b4b12c3d2fd03ad8e8ba9eaaded06e" +source = "git+https://github.com/embassy-rs/embassy#2537fc6f4fcbdaa0fcea45a37382d61f59cc5767" dependencies = [ "cortex-m", "critical-section", @@ -268,7 +472,7 @@ dependencies = [ [[package]] name = "embassy-net-driver" version = "0.2.0" -source = "git+https://github.com/embassy-rs/embassy#e54c753537b4b12c3d2fd03ad8e8ba9eaaded06e" +source = "git+https://github.com/embassy-rs/embassy#2537fc6f4fcbdaa0fcea45a37382d61f59cc5767" dependencies = [ "defmt", ] @@ -276,7 +480,7 @@ dependencies = [ [[package]] name = "embassy-stm32" version = "0.1.0" -source = "git+https://github.com/embassy-rs/embassy#e54c753537b4b12c3d2fd03ad8e8ba9eaaded06e" +source = "git+https://github.com/embassy-rs/embassy#2537fc6f4fcbdaa0fcea45a37382d61f59cc5767" dependencies = [ "bit_field", "bitflags 2.6.0", @@ -320,7 +524,7 @@ dependencies = [ [[package]] name = "embassy-sync" version = "0.6.0" -source = "git+https://github.com/embassy-rs/embassy#e54c753537b4b12c3d2fd03ad8e8ba9eaaded06e" +source = "git+https://github.com/embassy-rs/embassy#2537fc6f4fcbdaa0fcea45a37382d61f59cc5767" dependencies = [ "cfg-if", "critical-section", @@ -333,7 +537,7 @@ dependencies = [ [[package]] name = "embassy-time" version = "0.3.1" -source = "git+https://github.com/embassy-rs/embassy#e54c753537b4b12c3d2fd03ad8e8ba9eaaded06e" +source = "git+https://github.com/embassy-rs/embassy#2537fc6f4fcbdaa0fcea45a37382d61f59cc5767" dependencies = [ "cfg-if", "critical-section", @@ -351,7 +555,7 @@ dependencies = [ [[package]] name = "embassy-time-driver" version = "0.1.0" -source = "git+https://github.com/embassy-rs/embassy#e54c753537b4b12c3d2fd03ad8e8ba9eaaded06e" +source = "git+https://github.com/embassy-rs/embassy#2537fc6f4fcbdaa0fcea45a37382d61f59cc5767" dependencies = [ "document-features", ] @@ -359,12 +563,12 @@ dependencies = [ [[package]] name = "embassy-time-queue-driver" version = "0.1.0" -source = "git+https://github.com/embassy-rs/embassy#e54c753537b4b12c3d2fd03ad8e8ba9eaaded06e" +source = "git+https://github.com/embassy-rs/embassy#2537fc6f4fcbdaa0fcea45a37382d61f59cc5767" [[package]] name = "embassy-usb-driver" version = "0.1.0" -source = "git+https://github.com/embassy-rs/embassy#e54c753537b4b12c3d2fd03ad8e8ba9eaaded06e" +source = "git+https://github.com/embassy-rs/embassy#2537fc6f4fcbdaa0fcea45a37382d61f59cc5767" dependencies = [ "defmt", ] @@ -372,7 +576,7 @@ dependencies = [ [[package]] name = "embassy-usb-synopsys-otg" version = "0.1.0" -source = "git+https://github.com/embassy-rs/embassy#e54c753537b4b12c3d2fd03ad8e8ba9eaaded06e" +source = "git+https://github.com/embassy-rs/embassy#2537fc6f4fcbdaa0fcea45a37382d61f59cc5767" dependencies = [ "critical-section", "embassy-sync", @@ -457,6 +661,35 @@ dependencies = [ "embedded-storage", ] +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "firmware-updater" +version = "0.1.0" +dependencies = [ + "bootloader-params", + "clap", + "color-eyre", + "elf", + "indicatif", + "rppal", + "thiserror", +] + [[package]] name = "fnv" version = "1.0.7" @@ -524,6 +757,12 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + [[package]] name = "hash32" version = "0.3.1" @@ -543,6 +782,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "i2c2-target" version = "0.1.0" @@ -559,12 +804,73 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "indicatif" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" +dependencies = [ + "console", + "instant", + "number_prefix", + "portable-atomic", + "unicode-width", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + [[package]] name = "litrs" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + [[package]] name = "nb" version = "0.1.3" @@ -589,6 +895,33 @@ dependencies = [ "autocfg", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + [[package]] name = "panic-probe" version = "0.3.2" @@ -599,6 +932,13 @@ dependencies = [ "defmt", ] +[[package]] +name = "pid" +version = "0.1.0" +dependencies = [ + "num-traits", +] + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -611,6 +951,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "portable-atomic" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -659,6 +1005,21 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +[[package]] +name = "rppal" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae44db779bd0898047804d22b662a9dc533b142c077b3f7e36003f658835c5b9" +dependencies = [ + "libc", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + [[package]] name = "rustc_version" version = "0.2.3" @@ -695,6 +1056,15 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -735,6 +1105,8 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" name = "support" version = "0.1.0" dependencies = [ + "bootloader-params", + "cortex-m", "defmt", "embassy-executor", "embassy-stm32", @@ -784,12 +1156,81 @@ dependencies = [ "syn 2.0.71", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcell" version = "0.1.3" @@ -816,3 +1257,76 @@ checksum = "de437e2a6208b014ab52972a27e59b33fa2920d3e00fe05026167a1c509d19cc" dependencies = [ "vcell", ] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml index c36c716e725e40e3e967bda91a123889c35741cc..7e4e9ef51b22073fd2ad09977cd22a8f312b2bb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,17 @@ [workspace] -members = ["controller", "i2c2-target", "support"] -exclude = ["build-support", "pid"] +members = ["bootloader", "bootloader-params", "build-support", "controller", "firmware-updater", "i2c2-target", "pid", "support"] resolver = "2" [profile.release] codegen-units = 1 debug = true lto = true + +[profile.production] +inherits = "release" +debug = false +strip = "symbols" + +[profile.release.package.bootloader] +opt-level = "z" +strip = "symbols" diff --git a/INSTALL.md b/INSTALL.md index c1a589a1a5dab4ce7a6c3571276d4197b0e7d464..bc8f2196ca556fa7cad4b851337195ceeaa91c1e 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,5 +1,14 @@ # Installing the motor controller +The Télécom Paris Gitlab CI will contain an artifact with the latest +version of those software: + +- controller.py: Python interface to the controller +- controller: the controller firmware +- firmware-updater: updater for the controller firmware +- bootloader: the I²C bootloader +- bootloader.py: limited Python interface to the I²C bootloader + ## STM32 You must install the appropriate Rust target and tools: @@ -9,20 +18,64 @@ $ rustup target install thumbv7m-none-eabi $ cargo install probe-rs-tools ``` -Then flash the executable using +### Flashing the bootloader + +You must flash the I²C bootloader using a JTAG probe: + +```shell +$ cargo run --profile production -p bootloader +``` + +The bootloader is in charge of launching the existing controller, or +allowing the flashing of a new one using the I²C bus without using a +JTAG probe. + +### Flashing the controller + +You canflash the controller directly using a JTAG probe: ```shell -$ cargo run --release -p dc-motor-driver-hat +$ cargo run --profile production -p controller ``` ## Raspberry Pi -On the Raspberry Pi, ensure that `boot/config.txt` contains the following line which defines a software I²C bus: +### System configuration + +On the Raspberry Pi, ensure that `boot/config.txt` contains the +following line which defines a software I²C bus: ```txt dtoverlay=i2c-gpio,i2c_gpio_sda=2,i2c_gpio_scl=3,bus=8 ``` -Ensure that the line `dtparam=i2c_arm=on` is not present: that would activate the hardware I²C bus, which cannot be used due to a hardware bug in the BCM2712 SOC with regard to clock stretching. +Ensure that the line `dtparam=i2c_arm=on` is not present: that would +activate the hardware I²C bus, which cannot be used due to a hardware +bug in the BCM2712 SOC with regard to clock stretching. The Python module is located in `controller/python/controller.py`. + +### Firmware updater + +The `firmware-updater` program can be either be built on the Raspberry Pi: + +```shell +$ cargo build --profile production -p firmware-updater +``` + +or can be cross-compiled from another machine, provided you have +already installed `aarch-unknown-linux-musl-gcc` which is needed for +linking: + +```shell +$ cargo build --profile production -p firmware-updater --target aarch-unknown-linux-musl +``` + +### Updating the firmware + +You can update the firmware on the microcontroller by running the +following command on the Raspberry Pi: + +```shell +$ firmware-updater flash /path/to/controller +``` diff --git a/README.org b/README.org index 23dda6f6d6b93d455d3f45e9f4928cd56f961179..694e6cff0d4fe561f477563c14e3f1f5ea3fa130 100644 --- a/README.org +++ b/README.org @@ -54,7 +54,14 @@ The led pattern is in Morse code. | LP | Low power (Vdd is below 2.9V) | .-.. .--. | | WW | Last reset was due to the window watchdog trigerring | .-- .-- | -* Command set +* Motor controller command set + +I²C address: 0x57 + +I²C uses clock stretching, so care must be taken when using a +Raspberry Pi whose SOC is bogus. Using 400kHz instead of the default +100kHz seems to mask the problem if not outputting too much debug +information. Using a software I²C solves the issue. Multibyte values are exchanged in little endian format. @@ -129,6 +136,10 @@ Multibyte values are exchanged in little endian format. - Reset the device. Used mainly for testing. +** [IMPLEMENTED] 0xE1 Reset to bootloader (W) + +- Reset the device in bootloader mode. Used when reprogramming. + ** [IMPLEMENTED] 0xF0-0xF7 Unique device ID (R) - Unique device ID as found in addresses 0x1FFFF7E8-0x1FFFF7EF @@ -170,3 +181,102 @@ the feature executes an alternate firmware. - 0x01: failure - 0x02: command executed too soon - 0x03: features locked because of previous failure + + +* Bootloader command set + +If a firmware is present, the bootloader will start it unless the +firmware has explicitly chosen to reboot in bootloader mode. + +I²C address: 0x58 + +The note about clock stretching also applies for the bootloader. + +** [IMPLEMENTED] 0x08 Firmware version (R) + +- Return three bytes containing the major, minor and patch version numbers + derived from =Cargo.toml=. + +** [IMPLEMENTED] 0x0F Who am I? (R) + +- The I²C address + +** [IMPLEMENTED] 0x10 Status (R) + +Returns two bytes: + +- One set of flags ORed together: + - 0x01: system is in programming mode + - 0x02: at least one error was detected since the last time the + system has been put in programming mode +- A XORed version of all data received so far since the programming + address was last set, either by entering programming mode, or by + using the SET PROGRAMMING ADDRESS command. + +** [IMPLEMENTED] 0x20 Change programming mode (W) + +- 0: leave programming mode without marking the application as + succesfully written +- 1: leave programming mode and mark the application as succesfully + written +- [0x17, 0x27, 0x65, 0x40]: enter programming mode + +** [IMPLEMENTED] 0x24 Erase pages (W) [PROGRAMMING MODE] + +- First byte: index of the first 1kB page in the application area +- Second byte: number of pages to erase (up to the end of the application area) +- Third byte: XOR of the first two bytes, as a safety + +The memory will be erased. The active programming address is then set to +the beginning of the erased area. + +Even if the last page of the application does not belong to the erased +page set, it will be erased with the first erase request of the +current session (or after having being set again) to ensure that the +application is no longer considered valid. + +Designating address outside the application area, or having a wrong checksum, +will be an error. + +** [IMPLEMENTED] 0x28 Set programming address (W) [PROGRAMMING MODE] + +- 4 bytes containing the next address to program, relative to the + application start +- One byte XORing the previous ones + +** [IMPLEMENTED] 0x2c Program data (W) [PROGRAMMING MODE] + +- 2, 4, 6, or 8 data bytes +- One byte XORing the previous ones + +In case of error, the bootloader leaves programming mode immediately. + +The programming address will advance with the right number of bytes. + +** [IMPLEMENTED] 0x30 Read memory (R) [PROGRAMMING MODE] + +- Read 8 consecutive bytes in memory from the programming address + (relative to the applicatin start), and advance it by 8. + +0 will be returned outside the application memory area. + +** [IMPLEMENTED] 0x34 Set checksum (W) [PROGRAMMING_MODE] + +- Set the checksum value to the given byte. + +** [IMPLEMENTED] 0xE0 Reset (W) + +- Reset the bootloader in automatic mode + +** [IMPLEMENTED] 0xE1 Reset to the bootloader (W) + +- Reset the bootloader in bootloader mode + +** [IMPLEMENTED] 0xE2 Reset to the application (W) + +- Reset the bootloader in application mode + +** [IMPLEMENTED] 0xF0-0xF7 Unique device ID (R) + +- Unique device ID as found in addresses 0x1FFFF7E8-0x1FFFF7EF + diff --git a/bootloader-params/Cargo.toml b/bootloader-params/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..43462db142195edc49cc182a933d6b9d27f13c16 --- /dev/null +++ b/bootloader-params/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "bootloader-params" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/bootloader-params/src/lib.rs b/bootloader-params/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..19338752161216513b7dcc5c81c9e339e9a623b6 --- /dev/null +++ b/bootloader-params/src/lib.rs @@ -0,0 +1,28 @@ +//! This crate will be used both by the embedded system and by the +//! build system. + +#![no_std] + +pub const FLASH_START: u32 = 0x0800_0000; +pub const FLASH_SIZE: u32 = 0x10000; +pub const FLASH_PAGE_SIZE: u32 = 1024; +pub const FLASH_PAGES: u32 = FLASH_SIZE / FLASH_PAGE_SIZE; +pub const BOOTLOADER_PAGES: u32 = 24; +pub const BOOTLOADER_SIZE: u32 = BOOTLOADER_PAGES * FLASH_PAGE_SIZE; +pub const APPLICATION_SIZE: u32 = FLASH_SIZE - BOOTLOADER_SIZE - 4; + +pub mod commands { + pub const CMD_FIRMWARE_VERSION: u8 = 0x08; + pub const CMD_WHO_AM_I: u8 = 0x0f; + pub const CMD_REBOOT_AUTO: u8 = 0xe0; + pub const CMD_REBOOT_BOOTLOADER: u8 = 0xe1; + pub const CMD_REBOOT_APPLICATION: u8 = 0xe2; + pub const CMD_DEVICE_ID: u8 = 0xf0; + pub const CMD_STATUS: u8 = 0x10; + pub const CMD_CHANGE_PROGRAMMING_MODE: u8 = 0x20; + pub const CMD_ERASE_PAGES: u8 = 0x24; + pub const CMD_SET_PROGRAMMING_ADDRESS: u8 = 0x28; + pub const CMD_PROGRAM_DATA: u8 = 0x2c; + pub const CMD_READ_MEMORY: u8 = 0x30; + pub const CMD_SET_CHECKSUM: u8 = 0x34; +} diff --git a/bootloader/.cargo/config.toml b/bootloader/.cargo/config.toml new file mode 100644 index 0000000000000000000000000000000000000000..b800c6d6c846a76d748bc2d605317e4725641c44 --- /dev/null +++ b/bootloader/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "thumbv7m-none-eabi" diff --git a/bootloader/Cargo.toml b/bootloader/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..10e700810234434dbf4b62fd842a8f8d9d49b0fa --- /dev/null +++ b/bootloader/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "bootloader" +description = "I²C bootloader for DC Motor Driver Hat DFR0592" +authors = ["Samuel Tardieu <sam@rfc1149.net>"] +version = "0.1.0" +edition = "2021" + +[dependencies] +bootloader-params = { path = "../bootloader-params" } +cortex-m = { version = "0.7.7", features = ["critical-section-single-core", "inline-asm"] } +cortex-m-rt = "0.7.3" +critical-section = "1.1.2" +defmt = "0.3.8" +defmt-rtt = "0.4.1" +embassy-executor = { git = "https://github.com/embassy-rs/embassy", features = ["arch-cortex-m", "executor-thread", "defmt", "integrated-timers"] } +embassy-stm32 = { git = "https://github.com/embassy-rs/embassy", features = ["defmt", "stm32f103c8", "unstable-pac", "time-driver-tim4"] } +embassy-sync = { git = "https://github.com/embassy-rs/embassy", features = ["defmt"] } +embassy-time = { git = "https://github.com/embassy-rs/embassy", features = ["defmt", "defmt-timestamp-uptime", "tick-hz-32_768"] } +futures = { version = "0.3.30", default-features = false } +heapless = "0.8.0" +i2c2-target = { path = "../i2c2-target" } +panic-probe = { version = "0.3.2", features = ["print-defmt"] } +support = { version = "0.1.0", path = "../support" } + +[lints.clippy] +pedantic = "deny" + +[build-dependencies] +bootloader-params = { path = "../bootloader-params" } +build-support = { path = "../build-support" } diff --git a/bootloader/build.rs b/bootloader/build.rs new file mode 100644 index 0000000000000000000000000000000000000000..c4e01d8ad3d8804c41e2c57bf10aad31169d749b --- /dev/null +++ b/bootloader/build.rs @@ -0,0 +1,12 @@ +use bootloader_params::{BOOTLOADER_SIZE, FLASH_START}; +use build_support::MemoryConfig; + +fn main() -> Result<(), build_support::Error> { + build_support::check_python_library_consistency("python/bootloader.py")?; + build_support::make_version()?; + build_support::make_memory_x(MemoryConfig { + flash_start: FLASH_START, + flash_size: BOOTLOADER_SIZE, + })?; + Ok(()) +} diff --git a/bootloader/python/bootloader.py b/bootloader/python/bootloader.py new file mode 100644 index 0000000000000000000000000000000000000000..dbb75448f9ef198602ecd446aaacca7dd347df95 --- /dev/null +++ b/bootloader/python/bootloader.py @@ -0,0 +1,90 @@ +import struct + +# Major and minor version of required firmware +_REQUIRED_FIRMWARE_VERSION = (0, 1) + + +class FirmwareVersionMismatch(Exception): + pass + + +class WhoAmIMismatch(Exception): + pass + + +class Bootloader: + + I2C_ADDR = 0x58 + + FIRMWARE_VERSION = 0x08 + WHO_AM_I = 0x0F + RESET = 0xE0 + DEVICE_ID = 0xF0 + + def __init__(self, i2c_bus=8): + import smbus + + self.i2c_bus = i2c_bus + self.i2c = smbus.SMBus(self.i2c_bus) + + self.check_who_am_i() + self.check_firmware_version() + + def _read(self, command, n, unpack_spec) -> tuple: + return struct.unpack( + unpack_spec, + bytes(self.i2c.read_i2c_block_data(self.I2C_ADDR, command, n)), + ) + + def _write(self, command: int, data: list[int]): + self.i2c.write_i2c_block_data(self.I2C_ADDR, command, data) + + def who_am_i(self) -> int: + """Check that the motors controller board is present. This + should return the same value as Bootloader.I2C_ADDR.""" + return self._read(self.WHO_AM_I, 1, "B")[0] + + def check_who_am_i(self): + """Check that the device answers to WHO_AM_I is correct.""" + w = self.who_am_i() + if w != self.I2C_ADDR: + error = ( + f"WHO_AM_I returns {w:#04x} " + f"instead of the expected {self.I2C_ADDR:#04x}" + ) + raise WhoAmIMismatch(error) + + def get_firmware_version(self) -> tuple[int, int, int]: + """Get the firmware version (major, minor, patch).""" + return self._read(self.FIRMWARE_VERSION, 3, "BBB") + + def check_firmware_version(self): + """Check that the firmware uses a version compatible with this + library.""" + version = self.get_firmware_version() + Bootloader._check_firmware_version_consistency( + _REQUIRED_FIRMWARE_VERSION, version + ) + + def _check_firmware_version_consistency( + required: tuple[int, int], version: tuple[int, int, int] + ): + (MAJOR, MINOR) = required + (major, minor, patch) = version + error = None + if major != MAJOR or minor < MINOR: + version = f"{major}.{minor}.{patch}" + VERSION = f"{MAJOR}.{MINOR}.*" + error = ( + f"Hardware runs firmware version {version} which " + f"is not compatible with this library version ({VERSION})" + ) + raise FirmwareVersionMismatch(error) + + def reset(self): + """Reset the device. Used mainly for testing.""" + self._write(self.RESET, []) + + def get_device_id(self): + """Return the 8 bytes composing the device id.""" + return list(self._read(self.DEVICE_ID, 8, "BBBBBBBB")) diff --git a/bootloader/src/bootloader.rs b/bootloader/src/bootloader.rs new file mode 100644 index 0000000000000000000000000000000000000000..9148aac60b8dcf5799c3b1c4377abf0cdeeb5bf7 --- /dev/null +++ b/bootloader/src/bootloader.rs @@ -0,0 +1,77 @@ +use crate::flash::ProgrammingState; +use bootloader_params::commands::{ + CMD_DEVICE_ID, CMD_FIRMWARE_VERSION, CMD_REBOOT_APPLICATION, CMD_REBOOT_AUTO, + CMD_REBOOT_BOOTLOADER, CMD_WHO_AM_I, +}; +use embassy_executor::Spawner; +use embassy_stm32::peripherals::FLASH; +use heapless::Vec; +use i2c2_target::{I2C2, MESSAGE_SIZE}; +use support::boot_control; + +pub const I2C_ADDRESS: u8 = 0x58; + +struct State { + _i2c: I2C2, + programming_state: ProgrammingState, +} + +impl State { + fn new(i2c2: I2C2, flash: FLASH) -> Self { + Self { + _i2c: i2c2, + programming_state: ProgrammingState::new(flash), + } + } +} + +#[allow(clippy::needless_pass_by_value, clippy::used_underscore_binding)] // Clippy bug +pub fn start_i2c_target(spawner: Spawner, i2c2: I2C2, flash: FLASH) { + let state = State::new(i2c2, flash); + spawner.spawn(i2c_engine(state)).unwrap(); +} + +#[embassy_executor::task] +async fn i2c_engine(state: State) { + I2C2::start(&i2c_callback, state).await; +} + +include!(concat!(env!("OUT_DIR"), "/version.rs")); + +fn i2c_callback(command: &[u8], response: &mut Vec<u8, MESSAGE_SIZE>, state: &mut State) { + defmt::trace!("Processing command {:?}", command); + match command { + _ if state + .programming_state + .programming_callback(command, response) => {} + [CMD_FIRMWARE_VERSION] => { + for b in PKG_VERSION { + response.push(b).unwrap(); + } + } + [CMD_WHO_AM_I] => { + response.push(I2C_ADDRESS).unwrap(); + } + [CMD_REBOOT_AUTO] => { + defmt::info!("restarting in auto mode"); + boot_control::next_boot_is_auto(); + boot_control::reboot(); + } + [CMD_REBOOT_BOOTLOADER] => { + defmt::info!("restarting in bootloader mode"); + boot_control::reboot_to_bootloader(); + } + [CMD_REBOOT_APPLICATION] => { + defmt::info!("restarting in application mode"); + boot_control::reboot_to_application(); + } + [CMD_DEVICE_ID] => { + for &b in support::device_id() { + response.push(b).unwrap(); + } + } + _ => { + defmt::warn!("unknown command or args {:#04x}", command); + } + } +} diff --git a/bootloader/src/flash.rs b/bootloader/src/flash.rs new file mode 100644 index 0000000000000000000000000000000000000000..41d3f3840891a11d27654ea0f775268bc8ba868e --- /dev/null +++ b/bootloader/src/flash.rs @@ -0,0 +1,265 @@ +use bootloader_params::{ + commands::{ + CMD_CHANGE_PROGRAMMING_MODE, CMD_ERASE_PAGES, CMD_PROGRAM_DATA, CMD_READ_MEMORY, + CMD_SET_CHECKSUM, CMD_SET_PROGRAMMING_ADDRESS, CMD_STATUS, + }, + APPLICATION_SIZE, BOOTLOADER_PAGES, BOOTLOADER_SIZE, FLASH_PAGES, FLASH_PAGE_SIZE, FLASH_START, +}; +use embassy_stm32::{ + flash::{self, Blocking, Flash}, + pac, + peripherals::FLASH, +}; +use heapless::Vec; +use i2c2_target::MESSAGE_SIZE; +use support::boot_control::APPLICATION_MAGIC; + +pub struct ProgrammingState { + flash: Flash<'static, Blocking>, + programming_mode: bool, + programming_error: bool, + application_marker_erased: bool, + programming_address: u32, + checksum: u8, +} + +impl ProgrammingState { + #[must_use] + pub fn new(flash: FLASH) -> Self { + Self { + flash: Flash::new_blocking(flash), + programming_mode: false, + programming_error: false, + application_marker_erased: false, + programming_address: 0, + checksum: 0, + } + } + + fn programming_error(&mut self) { + self.programming_mode = false; + self.programming_error = true; + self.checksum = 0; + } + + fn enter_programming_mode(&mut self) { + self.programming_mode = true; + self.programming_error = false; + self.programming_address = 0; + self.checksum = 0; + } + + fn leave_programming_mode(&mut self, register_app: bool) { + if register_app && !self.programming_error && self.application_marker_erased { + if apm32_blocking_write(BOOTLOADER_SIZE + APPLICATION_SIZE, &APPLICATION_MAGIC).is_err() + { + defmt::error!("error when programming the application marker"); + self.programming_error(); + return; + } + self.application_marker_erased = false; + } + self.programming_mode = false; + self.checksum = 0; + } + + /// Execute a command and enter programming mode error if it + /// returns `None`. + fn check_command(&mut self, f: impl FnOnce(&mut Self) -> Option<()>) { + if f(self).is_none() { + self.programming_error(); + } + } + + /// Handle a programming request. Return `true` if the command has + /// been handled, `false` otherwise. + /// + /// # Panics + /// This function may panic if the buffer size is exceeded. There + /// is nothing we can do here. + #[allow(clippy::too_many_lines)] + pub fn programming_callback( + &mut self, + command: &[u8], + response: &mut Vec<u8, MESSAGE_SIZE>, + ) -> bool { + match command { + [CMD_STATUS] => { + let flags = + u8::from(self.programming_mode) | (u8::from(self.programming_error) << 1); + response.push(flags).unwrap(); + response.push(self.checksum).unwrap(); + } + [CMD_CHANGE_PROGRAMMING_MODE, 0x17, 0x27, 0x65, 0x40] => { + if self.programming_mode { + self.programming_error(); + } else { + self.enter_programming_mode(); + } + } + _ if !self.programming_mode => { + // We do not recognize other commands outside programming mode + return false; + } + [CMD_CHANGE_PROGRAMMING_MODE, register_app @ (0x00 | 0x01)] => { + self.leave_programming_mode(*register_app == 0x01); + } + [CMD_ERASE_PAGES, data @ ..] if data.len() == 3 => { + self.check_command(|this| { + let data = unxored(data)?; + let (index, num) = (u32::from(data[0]), u32::from(data[1])); + if index + num > FLASH_PAGES - BOOTLOADER_PAGES { + return None; + } + if this + .flash + .blocking_erase( + (BOOTLOADER_PAGES + index) * FLASH_PAGE_SIZE, + (BOOTLOADER_PAGES + index + num) * FLASH_PAGE_SIZE, + ) + .is_err() + { + defmt::error!( + "cannot erase {} pages starting from application page {}", + num, + index + ); + return None; + } + if index + num < FLASH_PAGES - BOOTLOADER_PAGES + && !this.application_marker_erased + && this + .flash + .blocking_erase( + (FLASH_PAGES - 1) * FLASH_PAGE_SIZE, + FLASH_PAGES * FLASH_PAGE_SIZE, + ) + .is_err() + { + defmt::error!("cannot erase application marker page"); + return None; + } + this.application_marker_erased = true; + Some(()) + }); + } + [CMD_SET_PROGRAMMING_ADDRESS, data @ ..] => { + self.check_command(|this| { + let address = u32::from_le_bytes(unxored(data)?.try_into().ok()?); + if address < APPLICATION_SIZE { + this.programming_address = address; + Some(()) + } else { + None + } + }); + } + [CMD_PROGRAM_DATA, data @ ..] => { + self.check_command(|this| { + let data = unxored(data)?; + if data.len() > 8 || data.len() % 2 != 0 { + return None; + } + let data_len = u32::try_from(data.len()).unwrap(); // This cannot fail + if this.programming_address + data_len > APPLICATION_SIZE { + return None; + } + if let Err(e) = + apm32_blocking_write(BOOTLOADER_SIZE + this.programming_address, data) + { + defmt::error!( + "cannot program data at {:#x} ({:#x}): {}", + this.programming_address, + data, + e + ); + return None; + } + this.programming_address += data_len as u32; + for b in data { + this.checksum ^= *b; + } + Some(()) + }); + } + [CMD_READ_MEMORY] => { + for _ in 0..8 { + let b = match self.programming_address { + addr if addr < APPLICATION_SIZE => { + self.programming_address += 1; + unsafe { *((FLASH_START + BOOTLOADER_SIZE + addr) as *const u8) } + } + _ => 0, + }; + response.push(b).unwrap(); + } + } + [CMD_SET_CHECKSUM, checksum] => { + self.checksum = *checksum; + } + _ => { + // This is not a valid programming mode command + return false; + } + } + true + } +} + +/// Check XOR-ed data. data must not contain zero or one byte, +/// and the last byte must be a xor of previous bytes. All bytes +/// except the checksum are returned, or `None` if there is a +/// mismatch. +fn unxored(data: &[u8]) -> Option<&[u8]> { + match data { + [data @ .., checksum] if !data.is_empty() => { + (data.iter().fold(0, |a, b| a ^ *b) == *checksum).then_some(data) + } + _ => None, + } +} + +/// The APM32 requires that the BSY flag is checked before performing a new write, +/// while on STM32 two writes can be chained. +fn apm32_blocking_write(address: u32, data: &[u8]) -> Result<(), flash::Error> { + let words = data + .chunks_exact(2) + .map(|c| u16::from_le_bytes(c.try_into().unwrap())); + let addresses = (FLASH_START + address..).step_by(2).map(|a| a as *mut u16); + // Clear errors + pac::FLASH.sr().modify(|_| {}); + // Unlock + if pac::FLASH.cr().read().lock() { + pac::FLASH.keyr().write_value(0x4567_0123); + pac::FLASH.keyr().write_value(0xCDEF_89AB); + } + // Enable write + pac::FLASH.cr().modify(|w| w.set_pg(true)); + // Write every word and wait for BSY to clear + for (address, word) in addresses.zip(words) { + unsafe { + core::ptr::write_volatile(address, word); + } + loop { + let sr = pac::FLASH.sr().read(); + if sr.pgerr() { + apm32_unlock(); + return Err(flash::Error::Prog); + } + if sr.wrprterr() { + apm32_unlock(); + return Err(flash::Error::Protected); + } + if !sr.bsy() { + break; + } + } + } + // Disable write and lock + apm32_unlock(); + Ok(()) +} + +fn apm32_unlock() { + pac::FLASH.cr().write(|w| w.set_pg(false)); +} diff --git a/bootloader/src/main.rs b/bootloader/src/main.rs new file mode 100644 index 0000000000000000000000000000000000000000..c4d2cc96a9d9624669472a5915f486de90966097 --- /dev/null +++ b/bootloader/src/main.rs @@ -0,0 +1,118 @@ +#![no_std] +#![no_main] + +use defmt_rtt as _; +use embassy_executor::Spawner; +use embassy_stm32::{ + interrupt::Priority, + pac, + peripherals::RCC, + rcc::{APBPrescaler, Hse, HseMode, Pll, PllMul, PllPreDiv, PllSource, Sysclk}, + time::mhz, + Config, +}; +use panic_probe as _; +use support::{blinker, boot_control, power}; + +mod bootloader; +pub mod flash; + +#[derive(Clone, Copy, Debug)] +enum ResetCause { + ExternalReset, + WindowWatchdogReset, + IndependentWatchdogReset, + SoftwareReset, + LowPowerManagementReset, + PowerReset, + Unknown, +} + +fn reset_cause(_rcc: &mut RCC) -> ResetCause { + // Read reset cause, and reset it + let csr = pac::RCC.csr().read(); + pac::RCC.csr().modify(|w| w.set_rmvf(true)); + if csr.lpwrrstf() { + ResetCause::LowPowerManagementReset + } else if csr.wwdgrstf() { + ResetCause::WindowWatchdogReset + } else if csr.iwdgrstf() { + ResetCause::IndependentWatchdogReset + } else if csr.sftrstf() { + ResetCause::SoftwareReset + } else if csr.porrstf() { + ResetCause::PowerReset + } else if csr.pinrstf() { + ResetCause::ExternalReset + } else { + ResetCause::Unknown + } +} + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + if boot_control::is_application_mode() { + boot_control::next_boot_is_auto(); + unsafe { + boot_control::jump_to_application(); + } + } + + let mut config = Config::default(); + // Use 8MHz external oscillator + config.rcc.hse = Some(Hse { + freq: mhz(8), + mode: HseMode::Oscillator, + }); + // Use 72MHz (HSE×9) as system clock + config.rcc.sys = Sysclk::PLL1_P; + config.rcc.pll = Some(Pll { + src: PllSource::HSE, + prediv: PllPreDiv::DIV1, + mul: PllMul::MUL9, + }); + // APB1 speed (PCLK1) is max 36MHz (SysClk/2), use that + config.rcc.apb1_pre = APBPrescaler::DIV2; + let mut p = embassy_stm32::init(config); + let reset_cause = reset_cause(&mut p.RCC); + + defmt::info!( + "Bootloader firmware {}.{}.{} starting – reset cause: {}", + bootloader::PKG_VERSION[0], + bootloader::PKG_VERSION[1], + bootloader::PKG_VERSION[2], + defmt::Debug2Format(&reset_cause), + ); + + support::identify(); + + if boot_control::is_application_present() { + defmt::info!("An application has been detected"); + } else { + defmt::info!("No application has been detected"); + } + + let pattern = if boot_control::is_bootloader_mode() { + defmt::info!("Detected forced bootloader mode"); + boot_control::next_boot_is_auto(); + "-..." // B for bootloader + } else if boot_control::is_application_present() { + defmt::info!("Application present, rebooting in application mode"); + boot_control::reboot_to_application(); + } else { + defmt::info!("No application present, staying in bootloader mode"); + "-. .-" // NA for no application + }; + spawner.spawn(blinker::blink(p.PB15, pattern)).unwrap(); + + spawner.spawn(power::watch_power_level()).unwrap(); + + let i2c2 = i2c2_target::I2C2::new( + p.I2C2, + p.PB10, + p.PB11, + bootloader::I2C_ADDRESS.into(), + Priority::P3, + ); + bootloader::start_i2c_target(spawner, i2c2, p.FLASH); +} diff --git a/build-support/src/lib.rs b/build-support/src/lib.rs index dab75ba9dcc5ee6ce7b168582fa3b989676e23cf..5741830f91291a77d44f44f7e4259529819cd9dd 100644 --- a/build-support/src/lib.rs +++ b/build-support/src/lib.rs @@ -15,6 +15,8 @@ pub enum Error { MissingEnv(#[from] env::VarError), #[error(transparent)] BadSemVer(#[from] semver::Error), + #[error("semver component does not fit in a u8")] + LargeSemVer(#[from] std::num::TryFromIntError), } fn version() -> Result<Version, Error> { @@ -38,13 +40,33 @@ pub fn check_python_library_consistency(python_file: &str) -> Result<(), Error> pub fn make_version() -> Result<(), Error> { let version = version()?; - let mut out = File::create(Path::new(&env::var("OUT_DIR").unwrap()).join("version.rs"))?; + let mut out = File::create(Path::new(&env::var("OUT_DIR")?).join("version.rs"))?; writeln!( out, "pub const PKG_VERSION: [u8; 3] = [{}, {}, {}];", - u8::try_from(version.major).unwrap(), - u8::try_from(version.minor).unwrap(), - u8::try_from(version.patch).unwrap() + u8::try_from(version.major)?, + u8::try_from(version.minor)?, + u8::try_from(version.patch)?, )?; Ok(()) } + +pub struct MemoryConfig { + pub flash_start: u32, + pub flash_size: u32, +} + +pub fn make_memory_x(config: MemoryConfig) -> Result<(), Error> { + let out_dir = env::var("OUT_DIR")?; + let mut out = File::create(Path::new(&out_dir).join("memory.x"))?; + writeln!(out, "MEMORY {{")?; + writeln!( + out, + "FLASH : ORIGIN = {}, LENGTH = {}", + config.flash_start, config.flash_size + )?; + writeln!(out, "RAM : ORIGIN = 0x20000000, LENGTH = 20K")?; + writeln!(out, "}}")?; + println!("cargo::rustc-link-search={}", out_dir); + Ok(()) +} diff --git a/controller/.cargo/config.toml b/controller/.cargo/config.toml new file mode 100644 index 0000000000000000000000000000000000000000..b800c6d6c846a76d748bc2d605317e4725641c44 --- /dev/null +++ b/controller/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "thumbv7m-none-eabi" diff --git a/controller/Cargo.toml b/controller/Cargo.toml index 08c61521f7ad4ef0a14c49238404e54a1eaf37a4..c69a0b4d5719a1aa31ff2c1e393f3a6547a226d3 100644 --- a/controller/Cargo.toml +++ b/controller/Cargo.toml @@ -1,7 +1,8 @@ [package] -name = "dc-motor-driver-hat" -version = "0.3.0" # Update in python/controller.py as well +name = "controller" +description = "Firmware for DC Motor Driver Hat DFR0592" authors = ["Samuel Tardieu <sam@rfc1149.net>"] +version = "0.3.1" # Update in python/controller.py as well edition = "2021" [dependencies] @@ -11,7 +12,7 @@ critical-section = "1.1.2" defmt = "0.3.8" defmt-rtt = "0.4.1" embassy-executor = { git = "https://github.com/embassy-rs/embassy", features = ["arch-cortex-m", "executor-thread", "defmt", "integrated-timers"] } -embassy-stm32 = { git = "https://github.com/embassy-rs/embassy", features = ["defmt", "stm32f103c8", "unstable-pac", "time-driver-tim4", "memory-x"] } +embassy-stm32 = { git = "https://github.com/embassy-rs/embassy", features = ["defmt", "stm32f103c8", "unstable-pac", "time-driver-tim4"] } embassy-sync = { git = "https://github.com/embassy-rs/embassy", features = ["defmt"] } embassy-time = { git = "https://github.com/embassy-rs/embassy", features = ["defmt", "defmt-timestamp-uptime", "tick-hz-32_768"] } futures = { version = "0.3.30", default-features = false } @@ -24,4 +25,5 @@ support = { version = "0.1.0", path = "../support" } pedantic = "deny" [build-dependencies] +bootloader-params = { path = "../bootloader-params" } build-support = { path = "../build-support" } diff --git a/controller/build.rs b/controller/build.rs index e671c0eb135d410db17bf49745aaf88847c6174a..2c4bac303b584503d4dfe31b5d2289f72d3107cf 100644 --- a/controller/build.rs +++ b/controller/build.rs @@ -1,5 +1,12 @@ +use bootloader_params::{BOOTLOADER_SIZE, FLASH_SIZE, FLASH_START}; +use build_support::MemoryConfig; + fn main() -> Result<(), build_support::Error> { build_support::check_python_library_consistency("python/controller.py")?; build_support::make_version()?; + build_support::make_memory_x(MemoryConfig { + flash_start: FLASH_START + BOOTLOADER_SIZE, + flash_size: FLASH_SIZE - BOOTLOADER_SIZE - 4, + })?; Ok(()) } diff --git a/controller/python/controller.py b/controller/python/controller.py index 9c5e9fb7086953aec544c08525959fd8da43dd64..94f3785f99cdaf89f7a0fc8479f55ab09ad42824 100644 --- a/controller/python/controller.py +++ b/controller/python/controller.py @@ -27,6 +27,7 @@ class Controller: ENCODER_TICKS = 0x32 STATUS = 0x36 RESET = 0xE0 + RESET_TO_BOOTLOADER = 0xE1 DEVICE_ID = 0xF0 def __init__(self, i2c_bus=8): @@ -183,6 +184,10 @@ class Controller: """Reset the device. Used mainly for testing.""" self._write(self.RESET, []) + def reset_to_bootloader(self): + """Reset the device to bootloader mode. Used for reprogramming.""" + self._write(self.RESET_TO_BOOTLOADER, []) + def get_device_id(self): """Return the 8 bytes composing the device id.""" return list(self._read(self.DEVICE_ID, 8, "BBBBBBBB")) diff --git a/controller/src/logic/mod.rs b/controller/src/logic/mod.rs index a54caf0ad18881189183e53e6756f27e594efc48..5c6d2fb93ac3812cc6aa8c5d30ae7bcd21f0b2fc 100644 --- a/controller/src/logic/mod.rs +++ b/controller/src/logic/mod.rs @@ -4,10 +4,13 @@ use embassy_stm32::{peripherals::WWDG, time::hz}; use futures::FutureExt; use heapless::Vec; use i2c2_target::{I2C2, MESSAGE_SIZE}; +use support::boot_control; mod motor_control; mod watchdog; +pub const I2C_ADDRESS: u8 = 0x57; + struct State { _i2c: I2C2, encoders: Encoders<'static>, @@ -60,6 +63,7 @@ const CMD_MOTOR_SPEED: u8 = 0x30; const CMD_ENCODER_TICKS: u8 = 0x32; const CMD_STATUS: u8 = 0x36; const CMD_RESET: u8 = 0xe0; +const CMD_RESET_BOOTLOADER: u8 = 0xe1; const CMD_DEVICE_ID: u8 = 0xf0; include!(concat!(env!("OUT_DIR"), "/version.rs")); @@ -78,7 +82,7 @@ fn process_command( } } [CMD_WHO_AM_I] => { - response.push(0x57).unwrap(); + response.push(I2C_ADDRESS).unwrap(); } &[CMD_PWM_FREQUENCY, a, b, c] => { let f = u32::from_le_bytes([a, b, c, 0]); @@ -152,7 +156,11 @@ fn process_command( } [CMD_RESET] => { defmt::info!("resetting device after receiving the RESET command"); - cortex_m::peripheral::SCB::sys_reset(); + boot_control::reboot(); + } + [CMD_RESET_BOOTLOADER] => { + defmt::info!("resetting device into bootloader mode"); + boot_control::reboot_to_bootloader(); } [CMD_DEVICE_ID] => { for &b in support::device_id() { diff --git a/controller/src/main.rs b/controller/src/main.rs index 945546284420713b41d02b41b55e88a68f4c3313..40b8ecb1563eb0d95666a4c84ef267a97bc6c6dc 100644 --- a/controller/src/main.rs +++ b/controller/src/main.rs @@ -108,7 +108,13 @@ async fn main(spawner: Spawner) { let encoders = Encoders::new(p.PA0, p.PA1, p.PA6, p.PA7, p.TIM2, p.TIM3); - let i2c2 = i2c2_target::I2C2::new(p.I2C2, p.PB10, p.PB11, 0x57, Priority::P3); + let i2c2 = i2c2_target::I2C2::new( + p.I2C2, + p.PB10, + p.PB11, + logic::I2C_ADDRESS.into(), + Priority::P3, + ); logic::start_i2c_target(spawner, i2c2, motors, encoders, p.WWDG); } diff --git a/firmware-updater/.cargo/config.toml b/firmware-updater/.cargo/config.toml new file mode 100644 index 0000000000000000000000000000000000000000..c9b5c99dd1014720cd3cb343aa995dfe26adad94 --- /dev/null +++ b/firmware-updater/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.aarch64-unknown-linux-musl] +linker = "aarch64-unknown-linux-musl-gcc" diff --git a/firmware-updater/.envrc b/firmware-updater/.envrc new file mode 100644 index 0000000000000000000000000000000000000000..1d953f4bd73593aba0a2af3db2d14178e2b8b9fe --- /dev/null +++ b/firmware-updater/.envrc @@ -0,0 +1 @@ +use nix diff --git a/firmware-updater/Cargo.lock b/firmware-updater/Cargo.lock new file mode 100644 index 0000000000000000000000000000000000000000..c8a818ca667f31603c3ef16f67d14bd64aaa911f --- /dev/null +++ b/firmware-updater/Cargo.lock @@ -0,0 +1,358 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anstream" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" + +[[package]] +name = "anstyle-parse" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "bootloader-params" +version = "0.1.0" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" + +[[package]] +name = "colorchoice" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" + +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys", +] + +[[package]] +name = "elf" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4445909572dbd556c457c849c4ca58623d84b27c8fff1e74b0b4227d8b90d17b" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "firmware-updater" +version = "0.1.0" +dependencies = [ + "bootloader-params", + "clap", + "elf", + "indicatif", + "rppal", + "thiserror", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indicatif" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" +dependencies = [ + "console", + "instant", + "number_prefix", + "portable-atomic", + "unicode-width", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "portable-atomic" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rppal" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae44db779bd0898047804d22b662a9dc533b142c077b3f7e36003f658835c5b9" +dependencies = [ + "libc", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/firmware-updater/Cargo.toml b/firmware-updater/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..777b55eb978cbcf1621b76981f42f85c8a6bcad8 --- /dev/null +++ b/firmware-updater/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "firmware-updater" +description = "Firmware updater for DC Motor Driver Hat DFR0592" +authors = ["Samuel Tardieu <sam@rfc1149.net>"] +version = "0.1.0" +edition = "2021" + +[dependencies] +bootloader-params = { path = "../bootloader-params" } +clap = { version = "4.5.9", features = ["cargo", "derive"] } +color-eyre = "0.6.3" +elf = "0.7.4" +indicatif = "0.17.8" +rppal = "0.18.0" +thiserror = "1.0.63" diff --git a/firmware-updater/default.nix b/firmware-updater/default.nix new file mode 100644 index 0000000000000000000000000000000000000000..f1d9736d4cd426986b1486eaf062dc38f0141a11 --- /dev/null +++ b/firmware-updater/default.nix @@ -0,0 +1,9 @@ +with import <nixpkgs> { + crossSystem = { + #config = "aarch64-unknown-linux-gnu"; + config = "aarch64-unknown-linux-musl"; + }; +}; + +mkShell { +} diff --git a/firmware-updater/src/bootloader.rs b/firmware-updater/src/bootloader.rs new file mode 100644 index 0000000000000000000000000000000000000000..f592b9e8752ce788d150ce35e26d7d3200b48b5a --- /dev/null +++ b/firmware-updater/src/bootloader.rs @@ -0,0 +1,210 @@ +use bootloader_params::commands::{ + CMD_CHANGE_PROGRAMMING_MODE, CMD_ERASE_PAGES, CMD_FIRMWARE_VERSION, CMD_PROGRAM_DATA, + CMD_READ_MEMORY, CMD_REBOOT_AUTO, CMD_REBOOT_BOOTLOADER, CMD_SET_CHECKSUM, + CMD_SET_PROGRAMMING_ADDRESS, CMD_STATUS, CMD_WHO_AM_I, +}; +use indicatif::ProgressBar; +use rppal::i2c::{self, I2c}; +use std::{fmt::Display, thread, time::Duration}; + +use crate::elf_loader::Segment; + +pub struct Version { + major: u8, + minor: u8, + patch: u8, +} + +impl Display for Version { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + } +} + +impl From<&[u8; 3]> for Version { + fn from(value: &[u8; 3]) -> Self { + Self { + major: value[0], + minor: value[1], + patch: value[2], + } + } +} + +pub enum ActiveProgram { + Bootloader(Version), + Controller(Version), +} + +impl Display for ActiveProgram { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ActiveProgram::Bootloader(version) => write!(f, "bootloader {version}"), + ActiveProgram::Controller(version) => write!(f, "controller {version}"), + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("no active program found")] + NoActiveProgram, + #[error(transparent)] + I2C(#[from] i2c::Error), + #[error(transparent)] + Io(#[from] std::io::Error), + #[error("flash programming error")] + Programming, + #[error("checksum does not match expected content")] + ChecksumMismatch, + #[error("memory does not match expected content")] + MemoryContentMismatch, +} + +pub type Result<T, E = Error> = std::result::Result<T, E>; + +#[derive(Debug)] +pub struct Status { + pub programming_mode: bool, + pub error_detected: bool, + pub checksum: u8, +} + +const CONTROLLER_ID: u8 = 0x57; +const BOOTLOADER_ID: u8 = 0x58; + +pub fn active_program(i2c: &mut I2c) -> Result<ActiveProgram> { + for device in [CONTROLLER_ID, BOOTLOADER_ID] { + i2c.set_slave_address(u16::from(device)).unwrap(); + let mut buffer = [0; 3]; + if i2c.block_read(CMD_WHO_AM_I, &mut buffer[..1]).is_ok() + && buffer[0] == device + && i2c.block_read(CMD_FIRMWARE_VERSION, &mut buffer).is_ok() + { + let version = Version::from(&buffer); + if device == CONTROLLER_ID { + return Ok(ActiveProgram::Controller(version)); + } else { + return Ok(ActiveProgram::Bootloader(version)); + } + } + } + Err(Error::NoActiveProgram) +} + +pub fn get_status(i2c: &I2c) -> Result<Status> { + let mut data = [0, 0]; + i2c.block_read(CMD_STATUS, &mut data)?; + Ok(Status { + programming_mode: data[0] & 1 != 0, + error_detected: data[0] & 2 != 0, + checksum: data[1], + }) +} + +pub fn enter_programming_mode(i2c: &I2c) -> Result<()> { + Ok(i2c.block_write(CMD_CHANGE_PROGRAMMING_MODE, &[0x17, 0x27, 0x65, 0x40])?) +} + +pub fn leave_programming_mode(i2c: &I2c, commit: bool) -> Result<()> { + Ok(i2c.block_write(CMD_CHANGE_PROGRAMMING_MODE, &[u8::from(commit)])?) +} + +pub fn switch_to_bootloader(i2c: &I2c) -> Result<()> { + Ok(i2c.block_write(CMD_REBOOT_BOOTLOADER, &[])?) +} + +pub fn reboot(i2c: &I2c) -> Result<()> { + Ok(i2c.block_write(CMD_REBOOT_AUTO, &[])?) +} + +fn xor_data(data: &[u8]) -> Vec<u8> { + let mut v = Vec::from(data); + v.push(v.iter().fold(0, |a, b| a ^ *b)); + v +} + +pub fn erase_pages(i2c: &I2c, index: u8, count: u8) -> Result<()> { + let data = xor_data(&[index, count]); + i2c.block_write(CMD_ERASE_PAGES, &data)?; + thread::sleep(Duration::from_millis(u64::from(count + 1) * 30)); + Ok(()) +} + +pub fn set_programming_address(i2c: &I2c, address: u32) -> Result<()> { + let data = xor_data(&address.to_le_bytes()); + Ok(i2c.block_write(CMD_SET_PROGRAMMING_ADDRESS, &data)?) +} + +fn program_data(i2c: &I2c, data: &[u8], checksum: &mut u8) -> Result<()> { + for b in data { + *checksum ^= *b; + } + let data = xor_data(data); + i2c.block_write(CMD_PROGRAM_DATA, &data)?; + Ok(()) +} + +fn read_memory(i2c: &I2c) -> Result<Vec<u8>> { + let mut res = vec![0; 8]; + i2c.block_read(CMD_READ_MEMORY, &mut res)?; + Ok(res) +} + +pub fn reset_checksum(i2c: &I2c) -> Result<()> { + Ok(i2c.block_write(CMD_SET_CHECKSUM, &[0])?) +} + +pub fn check_programming_status(i2c: &I2c) -> Result<u8> { + let status = get_status(i2c)?; + if !status.programming_mode || status.error_detected { + return Err(Error::Programming); + } + Ok(status.checksum) +} + +/// Program flash memory that has previously been erased. +pub fn program_flash( + i2c: &I2c, + segment: &Segment, + checksum: &mut u8, + progress_bar: Option<&ProgressBar>, +) -> Result<()> { + set_programming_address(i2c, segment.application_offset)?; + check_programming_status(i2c)?; + for chunk in segment.contents.chunks(8) { + program_data(i2c, chunk, checksum)?; + if let Some(bar) = progress_bar { + bar.inc(chunk.len() as u64); + } + } + let device_checksum = check_programming_status(i2c)?; + if *checksum != device_checksum { + return Err(Error::ChecksumMismatch); + } + check_programming_status(i2c)?; + Ok(()) +} + +/// Check flash memory that has previously been programmed. The +/// `start_address` is relative to the beginning of the application +/// area. +pub fn verify_flash( + i2c: &I2c, + segment: &Segment, + progress_bar: Option<&ProgressBar>, +) -> Result<()> { + set_programming_address(i2c, segment.application_offset)?; + check_programming_status(i2c)?; + for chunk in segment.contents.chunks(8) { + let on_device_data = read_memory(i2c)?; + if chunk != &on_device_data[..chunk.len()] { + return Err(Error::MemoryContentMismatch); + } + if let Some(bar) = progress_bar { + bar.inc(chunk.len() as u64); + } + } + check_programming_status(i2c)?; + Ok(()) +} diff --git a/firmware-updater/src/cli.rs b/firmware-updater/src/cli.rs new file mode 100644 index 0000000000000000000000000000000000000000..f8cd635fa65766a1a9291f0fc786afcd29c877ed --- /dev/null +++ b/firmware-updater/src/cli.rs @@ -0,0 +1,56 @@ +use clap::{Args, Parser, Subcommand}; +use std::path::PathBuf; + +#[derive(Debug, Parser)] +#[command(author, version, about, long_about = None)] +pub struct Cli { + #[command(subcommand)] + pub command: Command, +} + +#[derive(Debug, Subcommand)] +pub enum Command { + /// Check that firmware is well built + CheckFile(CheckFileArgs), + /// Flash firmware + Flash(FlashArgs), + /// Read the status of the program currently running + Status(StatusArgs), +} + +#[derive(Args, Debug)] +pub struct CheckFileArgs { + /// ELF file containing the firmware + pub firmware: PathBuf, +} + +#[derive(Args, Debug)] +pub struct CheckFlashArgs { + /// ELF file containing the firmware + pub firmware: PathBuf, + /// I²C bus to use + #[clap(short, long, default_value = "8")] + pub i2c_bus: u8, +} + +#[derive(Args, Debug)] +pub struct FlashArgs { + /// Do not verify after flashing + #[clap(long)] + pub no_verify: bool, + /// Do not program (verify only) + #[clap(short, long, conflicts_with = "no_verify")] + pub no_program: bool, + /// I²C bus to use + #[clap(short, long, default_value = "8")] + pub i2c_bus: u8, + /// ELF file containing the firmware + pub firmware: PathBuf, +} + +#[derive(Args, Debug)] +pub struct StatusArgs { + /// I²C bus to use + #[clap(short, long, default_value = "8")] + pub i2c_bus: u8, +} diff --git a/firmware-updater/src/elf_loader.rs b/firmware-updater/src/elf_loader.rs new file mode 100644 index 0000000000000000000000000000000000000000..ea7d56e70689b08ca2e820735ec29f91a932ef74 --- /dev/null +++ b/firmware-updater/src/elf_loader.rs @@ -0,0 +1,96 @@ +use std::path::Path; + +use bootloader_params::{APPLICATION_SIZE, BOOTLOADER_SIZE, FLASH_PAGE_SIZE, FLASH_START}; +use elf::{endian::LittleEndian, ElfBytes, ParseError}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Parse(#[from] ParseError), + #[error("no segments found in firmware file")] + NoSegments, + #[error("application from {0:#x} to {1:#x} will not fit in memory")] + ApplicationWontFit(u64, u64), + #[error("unaligned writes are not supported")] + UnalignedWrite, + #[error("compressed segments are not unsupported")] + CompressedSegment, +} + +type Result<T, E = Error> = std::result::Result<T, E>; + +pub struct Firmware { + /// Loadable segments + pub segments: Vec<Segment>, +} + +pub struct Segment { + /// Offset since the application start + pub application_offset: u32, + pub contents: Vec<u8>, +} + +impl Firmware { + pub fn load(file: impl AsRef<Path>) -> Result<Self> { + let contents = std::fs::read(file)?; + let elf_segments = ElfBytes::<LittleEndian>::minimal_parse(&contents)?.segments(); + let mut segments = Vec::new(); + + if let Some(elf_segments) = elf_segments { + for segment in elf_segments { + if segment.p_filesz > 0 { + if segment.p_filesz != segment.p_memsz { + return Err(Error::CompressedSegment); + } + if segment.p_paddr < u64::from(FLASH_START + BOOTLOADER_SIZE) + || (segment.p_paddr + segment.p_memsz) + > u64::from(FLASH_START + BOOTLOADER_SIZE + APPLICATION_SIZE) + { + return Err(Error::ApplicationWontFit( + segment.p_paddr, + segment.p_paddr + segment.p_memsz, + )); + } + if segment.p_paddr % 2 != 0 || segment.p_memsz % 2 != 0 { + return Err(Error::UnalignedWrite); + } + let segment = Segment { + application_offset: segment.p_paddr as u32 - FLASH_START - BOOTLOADER_SIZE, + contents: contents[segment.p_offset as usize + ..(segment.p_offset + segment.p_memsz) as usize] + .to_vec(), + }; + segments.push(segment); + } + } + } + if segments.is_empty() { + return Err(Error::NoSegments); + } + Ok(Self { segments }) + } + + pub fn first_page(&self) -> u8 { + self.segments.iter().map(Segment::first_page).min().unwrap() + } + + pub fn last_page(&self) -> u8 { + self.segments.iter().map(Segment::last_page).max().unwrap() + } + + pub fn total_bytes(&self) -> usize { + self.segments.iter().map(|s| s.contents.len()).sum() + } +} + +impl Segment { + fn first_page(&self) -> u8 { + (self.application_offset / FLASH_PAGE_SIZE) as u8 + } + + fn last_page(&self) -> u8 { + ((self.application_offset + self.contents.len() as u32 - 1) / FLASH_PAGE_SIZE) as u8 + } +} diff --git a/firmware-updater/src/main.rs b/firmware-updater/src/main.rs new file mode 100644 index 0000000000000000000000000000000000000000..d83765042594499cb2cfdcb21bf3073d0c403444 --- /dev/null +++ b/firmware-updater/src/main.rs @@ -0,0 +1,247 @@ +use bootloader::ActiveProgram; +use bootloader_params::{APPLICATION_SIZE, BOOTLOADER_PAGES, FLASH_PAGES}; +use clap::Parser; +use cli::{CheckFileArgs, Cli, Command, FlashArgs, StatusArgs}; +use elf::ParseError; +use elf_loader::Firmware; +use indicatif::{HumanBytes, MultiProgress, ProgressBar, ProgressFinish, ProgressStyle}; +use rppal::i2c::{self, I2c}; +use std::{borrow::Cow, fmt::Debug, sync::OnceLock, thread, time::Duration}; + +mod bootloader; +mod cli; +mod elf_loader; + +#[derive(Debug, thiserror::Error)] +enum Error { + #[error(transparent)] + I2C(#[from] i2c::Error), + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Parse(#[from] ParseError), + #[error("cannot run bootloader")] + NoBootloader, + #[error(transparent)] + Bootloader(#[from] bootloader::Error), + #[error(transparent)] + Elf(#[from] elf_loader::Error), +} + +type Result<T, E = Error> = std::result::Result<T, E>; + +fn main() -> color_eyre::Result<()> { + color_eyre::install()?; + match Cli::parse().command { + Command::CheckFile(args) => cmd_check_file(args)?, + Command::Flash(args) => cmd_flash(args)?, + Command::Status(args) => cmd_status(args)?, + } + Ok(()) +} + +fn cmd_check_file(args: CheckFileArgs) -> Result<()> { + let firmware = Firmware::load(&args.firmware)?; + let total_bytes = firmware.total_bytes(); + let pages = firmware.last_page() - firmware.first_page() + 1; + println!("Firmware looks ok:"); + println!( + " - Size in flash memory: {} ({:.1}%)", + HumanBytes(total_bytes as u64), + total_bytes as f32 * 100.0 / APPLICATION_SIZE as f32 + ); + println!( + " - Memory pages: {} pages of 1 KiB ({:.1}%)", + pages, + pages as f32 * 100.0 / (FLASH_PAGES - BOOTLOADER_PAGES) as f32 + ); + Ok(()) +} + +fn success_msg_style() -> ProgressStyle { + static STYLE: OnceLock<ProgressStyle> = OnceLock::new(); + STYLE + .get_or_init(|| ProgressStyle::with_template("{spinner} [{prefix:1.green}] {msg}").unwrap()) + .clone() +} + +fn failure_msg_style() -> ProgressStyle { + static STYLE: OnceLock<ProgressStyle> = OnceLock::new(); + STYLE + .get_or_init(|| ProgressStyle::with_template("{spinner} [{prefix:1.red}] {msg}").unwrap()) + .clone() +} + +fn msg(msg: impl Into<Cow<'static, str>>) -> ProgressBar { + ProgressBar::new_spinner() + .with_message(msg) + .with_prefix(" ") + .with_style(success_msg_style()) + .with_finish(ProgressFinish::AndLeave) +} + +fn success(bar: &ProgressBar) { + bar.finish(); + bar.set_style(success_msg_style()); + bar.set_prefix("✓"); +} + +fn success_msg(bar: &ProgressBar, msg: impl Into<Cow<'static, str>>) { + bar.finish(); + success(bar); + bar.set_message(msg); +} + +fn failure(bar: &ProgressBar) { + bar.finish(); + bar.set_style(failure_msg_style()); + bar.set_prefix("✕"); +} + +fn failure_msg(bar: &ProgressBar, msg: impl Into<Cow<'static, str>>) { + failure(bar); + bar.set_message(msg); +} + +fn check_success<T, E, F>(bar: &ProgressBar, f: F) -> Result<T, E> +where + F: FnOnce() -> Result<T, E>, +{ + let result = f(); + if result.is_ok() { + success(bar); + } else { + failure(bar); + } + result +} + +fn cmd_flash(args: FlashArgs) -> Result<()> { + let bars = MultiProgress::new(); + + // Load firmware + let bar = bars.add(msg(format!("Loading input file {:?}", args.firmware))); + let firmware = check_success(&bar, || Firmware::load(&args.firmware))?; + + // Switch to bootloader mode + let bar = bars.add(msg("Checking running program")); + let (mut i2c, program) = check_success(&bar, || { + let mut i2c = I2c::with_bus(args.i2c_bus)?; + let program = bootloader::active_program(&mut i2c)?; + Ok::<_, Error>((i2c, program)) + })?; + bar.set_message(format!("Checking running program: {program}")); + if matches!(program, ActiveProgram::Controller(_)) { + let switch_bar = bars.add(msg("Attempting to switch to bootloader")); + if let Err(e) = bootloader::switch_to_bootloader(&i2c) { + failure(&switch_bar); + return Err(e.into()); + } + thread::sleep(Duration::from_millis(10)); + let bar = bars.add(msg("Checking running program")); + let program = check_success(&bar, || bootloader::active_program(&mut i2c))?; + bar.set_message(format!("Checking running program: {program}")); + if !matches!(program, ActiveProgram::Bootloader(_)) { + failure(&switch_bar); + return Err(Error::NoBootloader); + } + success(&switch_bar); + } + + let bar = bars.add(msg("Entering programming mode")); + check_success(&bar, || { + let status = bootloader::get_status(&i2c)?; + match ( + status.programming_mode, + status.error_detected, + status.checksum, + ) { + (false, _, _) => { + bootloader::enter_programming_mode(&i2c)?; + bootloader::check_programming_status(&i2c)?; + } + (true, true, _) => { + bootloader::leave_programming_mode(&i2c, false)?; + bootloader::enter_programming_mode(&i2c)?; + bootloader::check_programming_status(&i2c)?; + } + (true, false, 0) => {} + (true, false, _) => { + bootloader::reset_checksum(&i2c)?; + } + } + Ok::<_, Error>(()) + })?; + + let steady_duration = Duration::from_millis(50); + let memory_style = ProgressStyle::with_template( + "{spinner} [{prefix:1}] {msg:11} {wide_bar} {binary_bytes:>9}/{binary_total_bytes}", + ) + .unwrap(); + let total = firmware.total_bytes() as u64; + + if !args.no_program { + let first_page = firmware.first_page(); + let last_page = firmware.last_page(); + let page_count = last_page - first_page + 1; + let bar = bars.add(msg(format!("Erasing {page_count}×1KiB flash pages"))); + bar.enable_steady_tick(steady_duration); + check_success(&bar, || { + bootloader::erase_pages(&i2c, first_page, page_count)?; + bootloader::check_programming_status(&i2c)?; + Ok::<_, Error>(()) + })?; + success(&bar); + + let mut checksum = 0; + let bar = bars.add( + ProgressBar::new(total) + .with_message("Programming") + .with_style(memory_style.clone()), + ); + bar.enable_steady_tick(steady_duration); + for segment in &firmware.segments { + if let Err(e) = bootloader::program_flash(&i2c, segment, &mut checksum, Some(&bar)) { + failure_msg(&bar, "Programming failed"); + return Err(e.into()); + } + } + success_msg(&bar, "Programmation successful"); + } + + if !args.no_verify { + let bar = bars.add( + ProgressBar::new(total) + .with_message("Verifying") + .with_style(memory_style), + ); + for segment in &firmware.segments { + if let Err(e) = bootloader::verify_flash(&i2c, segment, Some(&bar)) { + failure_msg(&bar, "Verification failed"); + return Err(e.into()); + } + } + success_msg(&bar, "Verification successful"); + } + + if args.no_program { + let bar = bars.add(msg("Leaving programming mode")); + check_success(&bar, || bootloader::leave_programming_mode(&i2c, false))?; + } else { + let bar = bars.add(msg("Committing application")); + check_success(&bar, || bootloader::leave_programming_mode(&i2c, true))?; + let bar = bars.add(msg(String::from("Rebooting to firmware"))); + check_success(&bar, || bootloader::reboot(&i2c))?; + thread::sleep(Duration::from_millis(100)); + let bar = bars.add(msg("Checking running program")); + let program = check_success(&bar, || bootloader::active_program(&mut i2c))?; + bar.set_message(format!("Checking running program: {program}")); + } + Ok(()) +} + +fn cmd_status(args: StatusArgs) -> Result<()> { + let mut i2c = I2c::with_bus(args.i2c_bus)?; + println!("Running program: {}", bootloader::active_program(&mut i2c)?); + Ok(()) +} diff --git a/i2c2-target/src/lib.rs b/i2c2-target/src/lib.rs index 9a48baa68ca8f61fc03186118031f34e7ce666f2..ed7ebb96c4c3ec65f97ad88489d1269620eacad1 100644 --- a/i2c2-target/src/lib.rs +++ b/i2c2-target/src/lib.rs @@ -10,7 +10,7 @@ use embassy_stm32::{ use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal}; use heapless::Vec; -pub const MESSAGE_SIZE: usize = 8; +pub const MESSAGE_SIZE: usize = 10; pub type Callback<State> = &'static dyn Fn(&[u8], &mut Vec<u8, MESSAGE_SIZE>, &mut State); diff --git a/support/.cargo/config.toml b/support/.cargo/config.toml new file mode 100644 index 0000000000000000000000000000000000000000..b800c6d6c846a76d748bc2d605317e4725641c44 --- /dev/null +++ b/support/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "thumbv7m-none-eabi" diff --git a/support/Cargo.toml b/support/Cargo.toml index 520c41487a657cc9e13b44c1f12087db870f01a8..8c5260ad4372a8934ff3964c21c84befc49fb260 100644 --- a/support/Cargo.toml +++ b/support/Cargo.toml @@ -4,8 +4,10 @@ version = "0.1.0" edition = "2021" [dependencies] +bootloader-params = { path = "../bootloader-params" } +cortex-m = { version = "0.7.7", features = ["critical-section-single-core", "inline-asm"] } defmt = "0.3.8" embassy-executor = { git = "https://github.com/embassy-rs/embassy", features = ["arch-cortex-m", "executor-thread", "defmt", "integrated-timers"] } -embassy-stm32 = { git = "https://github.com/embassy-rs/embassy", features = ["unstable-pac"] } +embassy-stm32 = { git = "https://github.com/embassy-rs/embassy", features = ["stm32f103c8", "unstable-pac"] } embassy-sync = { git = "https://github.com/embassy-rs/embassy", features = ["defmt"] } embassy-time = { git = "https://github.com/embassy-rs/embassy", features = ["defmt", "defmt-timestamp-uptime", "tick-hz-32_768"] } diff --git a/support/src/blinker.rs b/support/src/blinker.rs index 4887e5266fb4cfb0531d60534c6925d1a902e009..0118243ba623cb1ea4f6fd6fdbfdd04471702515 100644 --- a/support/src/blinker.rs +++ b/support/src/blinker.rs @@ -22,7 +22,7 @@ pub async fn blink(pin: PB15, initial_pattern: &'static str) { for b in pattern.chars() { match b { ' ' => { - Timer::after_millis(200).await; + Timer::after_millis(250).await; } '.' => { led.toggle(); @@ -32,9 +32,9 @@ pub async fn blink(pin: PB15, initial_pattern: &'static str) { } '-' => { led.toggle(); - Timer::after_millis(300).await; + Timer::after_millis(350).await; led.toggle(); - Timer::after_millis(200).await; + Timer::after_millis(150).await; } _ => { defmt::error!("unknown pattern letter {}", b); diff --git a/support/src/boot_control.rs b/support/src/boot_control.rs new file mode 100644 index 0000000000000000000000000000000000000000..5ef7e8224388fbd8bba2413e4dd19b18ac6fa01c --- /dev/null +++ b/support/src/boot_control.rs @@ -0,0 +1,102 @@ +use bootloader_params::{APPLICATION_SIZE, BOOTLOADER_SIZE, FLASH_START}; +use core::arch::asm; +use embassy_stm32::pac::{self, bkp::Bkp}; + +/// Those values are written in the first backup registers to indicate +/// that the bootloader should stay in bootloader mode instead of +/// launching the main program even if one can be found in flash. +const BOOT_BOOTLOADER: [u16; 2] = [0x7531, 0x9783]; + +/// Those values are written in the first backup registers to indicate +/// that the bootloader should boot the application directly instead +/// of making any choice. The values will be reset prior to jumping to +/// the application. +const BOOT_APPLICATION: [u16; 2] = [0x2705, 0x1972]; + +/// Application magic written at the end of the memory to indicate the +/// presence of an application. +pub const APPLICATION_MAGIC: [u8; 4] = [0x59, 0x70, 0x24, 0x36]; + +fn bkp() -> Bkp { + pac::RCC.apb1enr().modify(|w| { + w.set_pwren(true); + w.set_bkpen(true); + }); + pac::PWR.cr().modify(|w| w.set_dbp(true)); + pac::BKP +} + +// Write data into the first backup registers +fn write_backup_data(data: &[u16]) { + let bkp = bkp(); + for (i, v) in data.iter().enumerate() { + // TODO: remove +1 when https://github.com/embassy-rs/stm32-data/pull/507 is merged + bkp.dr(i + 1).write(|w| w.set_d(*v)) + } +} + +// Check if the first backup registers contain data +fn backup_data_present(data: &[u16]) -> bool { + let bkp = bkp(); + // TODO: remove +1 when https://github.com/embassy-rs/stm32-data/pull/507 is merged + data.iter() + .enumerate() + .all(|(i, v)| bkp.dr(i + 1).read().d() == *v) +} + +pub fn reboot() -> ! { + cortex_m::peripheral::SCB::sys_reset(); +} + +/// Reboot to bootloader +pub fn reboot_to_bootloader() -> ! { + write_backup_data(&BOOT_BOOTLOADER); + reboot(); +} + +/// Next reboot will use automatic mode +pub fn next_boot_is_auto() { + write_backup_data(&[0, 0]); +} + +/// Reboot to the application +pub fn reboot_to_application() -> ! { + write_backup_data(&BOOT_APPLICATION); + reboot(); +} + +pub fn is_bootloader_mode() -> bool { + backup_data_present(&BOOT_BOOTLOADER) +} + +pub fn is_application_mode() -> bool { + backup_data_present(&BOOT_APPLICATION) +} + +pub fn is_application_present() -> bool { + let magic_location = (FLASH_START + BOOTLOADER_SIZE + APPLICATION_SIZE) as *const [u8; 4]; + unsafe { *magic_location == APPLICATION_MAGIC } +} + +/// Jump to the application stored at [`BOOTLOADER_SIZE`] bytes after +/// the beginning of the flash. `VTOR` will be set, then `SP` and `PC` +/// will be updated. +/// +/// # Note +/// This will not deinitialize any peripheral, nor block any interrupt. +/// +/// # Safety +/// If no valid application is installed, the system might crash. +pub unsafe fn jump_to_application() -> ! { + type VectorTablePtr = *const [usize; 2]; + const VTOR: *mut VectorTablePtr = 0xE000_ED08 as *mut VectorTablePtr; + const VECTORS: VectorTablePtr = (FLASH_START + BOOTLOADER_SIZE) as VectorTablePtr; + *VTOR = VECTORS; + asm!( + "mov sp, {sp}", + "bx lr", + sp = in(reg) (*VECTORS)[0], + in("lr") (*VECTORS)[1], + options(noreturn, nostack) + ); +} diff --git a/support/src/lib.rs b/support/src/lib.rs index 1ab4f8bd46254430f891875d26bb45b2494bdf50..effe8f8fc75d920040b32d3d73019c33d6af71ab 100644 --- a/support/src/lib.rs +++ b/support/src/lib.rs @@ -3,6 +3,7 @@ use embassy_stm32::pac; pub mod blinker; +pub mod boot_control; pub mod power; pub fn mcu_kind() -> (&'static str, &'static str) {