Що є в проєкті
Звичайний Rust-проєкт починається з Cargo.toml і src/main.rs. Тут до них додається ще кілька файлів. Не тому що embedded любить зайві ритуали, а тому що компілятору треба знати, куди покласти код та як його запустити без операційної системи.
Cargo.toml
Тут описуються залежності, features та профілі збірки. Назва пакета визначає назву ELF після компіляції: у basics це target/xtensa-esp32s3-none-elf/release/basics від кореня workspace.
Залежність на SDK не замінює всі інші. Якщо ваш код використовує embedded_graphics, він має бути у ваших dependencies. Якщо макрос задачі генерує звернення до embassy_executor, цей крейт теж потрібен напряму.
У репозиторії examples/* входять до workspace. Cargo.lock, target/ та release-профіль спільні. Це не означає, що всі прошивки треба збирати одночасно: features одного SDK об'єднаються, і blocking разом з async не зберуться.
rust-toolchain.toml
Обирає тулчейн esp, встановлений через espup. Стандартний stable потрібен для звичайних програм на комп'ютері, але Xtensa-прошивка Lilka збирається портом Rust під ESP32-S3.
Команда cargo +esp ... вказує цей вибір явно. Якщо esp не знайдено, повертайтесь до встановлення середовища, а не замінюйте target на схожу назву.
.cargo/config.toml
У перевірених прикладах він такий:
[target.xtensa-esp32s3-none-elf]
runner = "probe-rs run --chip=esp32s3 --preverify --always-print-stacktrace --no-location --catch-hardfault"
[env]
DEFMT_LOG="info"
[build]
rustflags = [
"-C", "link-arg=-nostartfiles",
"-Z", "stack-protector=all",
]
target = "xtensa-esp32s3-none-elf"
[unstable]
build-std = ["alloc", "core"]
target задає чіпову архітектуру. build-std збирає core та alloc для неї. Наявність alloc у цьому списку не створює heap автоматично — це лише збірка самої бібліотеки.
runner визначає, що зробить cargo run. У нашому випадку це probe-rs, тому команда вже записує код на пристрій. cargo build runner не запускає.
build.rs
fn main() {
println!("cargo:rustc-link-arg=-Tdefmt.x");
println!("cargo:rustc-link-arg=-Tlinkall.x");
}
Цей файл виконується на комп'ютері під час збірки. Він передає лінкеру скрипти розташування секцій та підтримки defmt. Це не код, який потім виконує Lilka.
src/main.rs
#![no_std] вимикає стандартну бібліотеку, а #![no_main] — звичайний запуск програми через системний main. Точку входу створює #[esp_hal::main] або #[esp_rtos::main], залежно від режиму.
esp_bootloader_esp_idf::esp_app_desc!() додає опис застосунку для ESP-завантажувача. Саме використання цього крейта не означає, що програма працює на всьому ESP-IDF: SDK залишається на esp-hal.
#[panic_handler] потрібен, бо звичайної реалізації зі std немає. У прикладах він друкує причину й зупиняє виконання. Для власної програми вже вирішуйте, чи потрібне перезавантаження, чи очікування підключення налагоджувача.
Як розділяти свій код
Не обов'язково тримати все у main.rs. Нехай точка входу створює периферію, а окремі модулі займаються станом, малюванням та даними. Звичайний модуль з mod ui; не потребує нового Cargo-пакета.
Логіку, яка не знає про GPIO та SPI, простіше перевіряти на комп'ютері. Але успішний host-тест не перевіряє дисплей або карту: апаратну частину все одно треба запускати на Lilka.