chore(firmware): scaffold Spike 1 Blink crate

Generated from esp-rs/esp-idf-template for the ESP32-S3 std target.
src/main.rs toggles GPIO 2 every 500 ms and logs `blink N` over USB-
serial — the minimum bring-up surface called out in
docs/v0.1-mvp-technical.md (Spike 1: confirm toolchain, flash, and basic
GPIO). edition=2024 with rust-version=1.85.

No editor/render/git/usb/fs modules yet; those land per the spike
methodology when later spikes need them.
This commit is contained in:
Julien Calixte
2026-05-23 14:36:51 +02:00
parent 4bdcdd09ba
commit 55ba0db0f6
8 changed files with 209 additions and 0 deletions

View File

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

4
firmware/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/.vscode
/.embuild
/target
/Cargo.lock

32
firmware/Cargo.toml Normal file
View File

@@ -0,0 +1,32 @@
[package]
name = "firmware"
version = "0.1.0"
authors = ["Julien Calixte <julien.calixte@theodo.com>"]
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"

111
firmware/README.md Normal file
View File

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

3
firmware/build.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
embuild::espidf::sysenv::output();
}

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "esp"

View File

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

26
firmware/src/main.rs Normal file
View File

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