diff --git a/firmware/.cargo/config.toml b/firmware/.cargo/config.toml new file mode 100644 index 0000000..4b8eb47 --- /dev/null +++ b/firmware/.cargo/config.toml @@ -0,0 +1,19 @@ +[build] +target = "xtensa-esp32s3-espidf" + +[target.'cfg(target_os = "espidf")'] +linker = "ldproxy" +runner = "espflash flash --monitor" +rustflags = [ "--cfg", "espidf_time64"] + +[unstable] +build-std = ["std", "panic_abort"] + +[env] +MCU = "esp32s3" +ESP_IDF_VERSION = "v5.5.3" +ESP_IDF_TOOLS_INSTALL_DIR = "workspace" + +# Uncomment this if you have moved the target dir by setting CARGO_TARGET_DIR or similar. +# Required for now due to embuild limitations. +#CARGO_WORKSPACE_DIR = { value = "", relative = true } diff --git a/firmware/.gitignore b/firmware/.gitignore new file mode 100644 index 0000000..73a638b --- /dev/null +++ b/firmware/.gitignore @@ -0,0 +1,4 @@ +/.vscode +/.embuild +/target +/Cargo.lock diff --git a/firmware/Cargo.toml b/firmware/Cargo.toml new file mode 100644 index 0000000..d1af9b4 --- /dev/null +++ b/firmware/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "firmware" +version = "0.1.0" +authors = ["Julien Calixte "] +edition = "2024" +resolver = "2" +rust-version = "1.85" + +[[bin]] +name = "firmware" +harness = false # do not use the built-in cargo test harness -> resolve rust-analyzer errors + +[profile.release] +opt-level = "s" + +[profile.dev] +debug = true # Symbols are nice, and they don't increase the size on Flash +opt-level = "z" +[patch.crates-io] +esp-idf-sys = { git = "https://github.com/esp-rs/esp-idf-sys.git" } +esp-idf-hal = { git = "https://github.com/esp-rs/esp-idf-hal.git" } +esp-idf-svc = { git = "https://github.com/esp-rs/esp-idf-svc.git" } + +[dependencies] +anyhow = "1" +log = "0.4" +esp-idf-svc = { version = "0.52.1", features = ["critical-section", "embassy-time-driver", "embassy-sync"] } +# Remove `generic-queue-8` if you plan to use `embassy-time` WITH `embassy-executor` +embassy-time = { version = "0.5", features = ["generic-queue-8"] } + +[build-dependencies] +embuild = "0.33" diff --git a/firmware/README.md b/firmware/README.md new file mode 100644 index 0000000..1f0b81f --- /dev/null +++ b/firmware/README.md @@ -0,0 +1,111 @@ +# Typoena firmware + +Rust crate targeting `xtensa-esp32s3-espidf`. See the project root +[`README.md`](../README.md) and +[`docs/v0.1-mvp-technical.md`](../docs/v0.1-mvp-technical.md) for the wider +context. + +## Current state + +**Spike 1 — Blink.** Toggles GPIO 2 every 500 ms and logs `blink N` to the +USB-serial console. This proves three things only: + +1. The Espressif Rust toolchain (Xtensa) is installed and on PATH. +2. The crate links against `esp-idf-svc` and compiles for + `xtensa-esp32s3-espidf`. +3. Basic GPIO output works on real silicon (verified post-flash, once the + board is on the bench). + +Everything past that — EPD, SD, USB host, partial refresh, Wi-Fi/TLS, +gitoxide push — is its own follow-up spike per +[`docs/v0.1-mvp-technical.md`](../docs/v0.1-mvp-technical.md#hardware-bring-up-order). + +## Build + +Once per shell session, source the espup env (sets `LIBCLANG_PATH` and adds +the Xtensa GCC to `PATH`): + +```sh +. ~/export-esp.sh +``` + +Then from this directory: + +```sh +cargo build --release +``` + +The first build is slow (the esp-idf C sources are checked out and built +under `.embuild/`). Subsequent builds are incremental. + +## Flash (when hardware is on the bench) + +`cargo run --release` triggers `espflash flash --monitor` via the runner +configured in `.cargo/config.toml`. With the ESP32-S3-DevKitC-1 connected +over USB you should see: + +``` +[…] blink 0 +[…] blink 1 +[…] blink 2 +… +``` + +at 1 Hz on the serial monitor, and — if an LED is wired from GPIO 2 → 330 Ω +→ GND — the LED blinks in lockstep. + +## Pin choice + +GPIO 2 is a safe general-purpose pin on the ESP32-S3-DevKitC-1: it's not +tied to a strapping function at boot and not muxed to the USB or PSRAM +peripherals. If you want to drive the on-board addressable LED instead, +that's WS2812 on GPIO 48 and needs a different driver — out of scope for +Spike 1. + +## Editor / rust-analyzer + +The repo-level `.zed/settings.json` configures `rust-analyzer` for this +crate: + +- `cargo.target` is pinned to `xtensa-esp32s3-espidf` with + `allTargets = false`, so RA doesn't try to also check the crate for the + host target (which can't build `esp-idf-sys`). +- `binary.path` is pinned to the **rustup-managed** rust-analyzer + (`stable` toolchain), not Zed's bundled one. Reason: recent Zed builds + ship a rust-analyzer that calls `cargo metadata --lockfile-path`, which + is still gated behind `-Z unstable-options` in cargo 1.95 and fails on + both the `stable` and `esp` toolchains. The rustup-managed RA is + version-locked to the cargo it ships with and avoids the flag. + +If a contributor on a different machine has issues, regenerate the path: + +```sh +rustup component add rust-analyzer --toolchain stable +rustup which rust-analyzer --toolchain stable +# put the printed path into .zed/settings.json under lsp.rust-analyzer.binary.path +``` + +Two things rust-analyzer still needs from the **environment Zed was +launched in**: + +- `LIBCLANG_PATH` — required by `bindgen` inside `esp-idf-sys`. +- The Xtensa GCC on `PATH` — required by `embuild` during `cargo check`. + +Both are set by `~/export-esp.sh`. The pragmatic workflow: + +```sh +. ~/export-esp.sh +zed /Users/julien/jclab/typewriter # or: open from this shell +``` + +If Zed is launched from Finder/Dock instead, rust-analyzer will report +`bindgen` errors on the first `esp-idf-sys` check. Close Zed, source the +env in a terminal, and relaunch from there. + +## Toolchain pins + +`rust-toolchain.toml` pins the channel to `esp` (installed by `espup +install`). Cargo.toml currently includes git `[patch.crates-io]` overrides +for `esp-idf-sys` / `esp-idf-hal` / `esp-idf-svc` (template default). These +follow master and may need pinning to released versions if a master commit +breaks the build. diff --git a/firmware/build.rs b/firmware/build.rs new file mode 100644 index 0000000..112ec3f --- /dev/null +++ b/firmware/build.rs @@ -0,0 +1,3 @@ +fn main() { + embuild::espidf::sysenv::output(); +} diff --git a/firmware/rust-toolchain.toml b/firmware/rust-toolchain.toml new file mode 100644 index 0000000..a2f5ab5 --- /dev/null +++ b/firmware/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "esp" diff --git a/firmware/sdkconfig.defaults b/firmware/sdkconfig.defaults new file mode 100644 index 0000000..ca7a8a8 --- /dev/null +++ b/firmware/sdkconfig.defaults @@ -0,0 +1,12 @@ +# Rust often needs a bit of an extra main task stack size compared to C (the default is 3K) +# You might have to increase this further if you allocate large stack variables in the main task +CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 + +# Increase a bit these stack sizes as they are also a bit too small by default +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 +CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=4096 + +# You might have to increase this further if you spawn your own Rust threads +# that allocate large stack variables; or better yet - use +# `std::thread::Builder::new().stack_size(XXX)` for spawning +CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT=4096 diff --git a/firmware/src/main.rs b/firmware/src/main.rs new file mode 100644 index 0000000..cc230f5 --- /dev/null +++ b/firmware/src/main.rs @@ -0,0 +1,26 @@ +use esp_idf_svc::hal::delay::FreeRtos; +use esp_idf_svc::hal::gpio::PinDriver; +use esp_idf_svc::hal::peripherals::Peripherals; + +fn main() -> anyhow::Result<()> { + // Required once before any esp-idf-svc call; some runtime patches + // only link if this symbol is referenced. See esp-idf-template#71. + esp_idf_svc::sys::link_patches(); + esp_idf_svc::log::EspLogger::initialize_default(); + + let peripherals = Peripherals::take()?; + let mut led = PinDriver::output(peripherals.pins.gpio2)?; + + log::info!("Typoena Spike 1 — Blink on GPIO 2"); + + let mut n: u32 = 0; + loop { + led.set_high()?; + log::info!("blink {n}"); + n = n.wrapping_add(1); + FreeRtos::delay_ms(500); + + led.set_low()?; + FreeRtos::delay_ms(500); + } +}