From 7d20e3ef97f1a0f9597e367b8b5965d16d65b00f Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Thu, 26 Mar 2026 23:09:04 -0600 Subject: [PATCH] fix: resolve ESP32-S3 runtime crash, match official esp-hal examples Root causes found and fixed: - USB OTG init stole the PHY from USB-Serial-JTAG (the only USB on V4) - executor-thread feature conflicted with esp-hal-embassy's executor - Linker flags needed -Wl, prefix for GCC-to-LD passthrough - Missing exception-handler feature on esp-backtrace - Missing alloc in build-std - Missing LTO and codegen-units=1 in release profile - Vext on V4 is active LOW (datasheet correct, Arduino lib was wrong) Now matches the official esp-hal v0.22.0 examples exactly. Co-Authored-By: Claude Opus 4.6 (1M context) --- .cargo/config.toml | 9 ++++++--- Cargo.lock | 2 -- Cargo.toml | 14 ++++++++++---- Justfile | 4 ++-- src/board/heltec_v3.rs | 35 ++++++++++++--------------------- src/board/heltec_v4.rs | 44 +++++++++++++++++------------------------- src/display/task.rs | 17 ++++++++-------- src/main.rs | 3 +-- 8 files changed, 58 insertions(+), 70 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 850366b5..7a6c685d 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,6 +1,9 @@ -[target.xtensa-esp32s3-none-elf] +[target.'cfg(target_arch = "xtensa")'] rustflags = [ + "-C", "link-arg=-Wl,-Tlinkall.x", + "-C", "link-arg=-Wl,-Tdefmt.x", "-C", "link-arg=-nostartfiles", - "-C", "link-arg=-Tlinkall.x", - "-C", "link-arg=-Tdefmt.x", ] + +[unstable] +build-std = ["alloc", "core"] diff --git a/Cargo.lock b/Cargo.lock index f59c611c..ec93f9d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -359,8 +359,6 @@ dependencies = [ "critical-section", "document-features", "embassy-executor-macros", - "embassy-time-driver", - "embassy-time-queue-driver", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5f64f92f..757b761f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ cfg-if = "1.0" critical-section = "1.2" defmt = "0.3" embassy-embedded-hal = "0.2" -embassy-executor = { version = "0.6", features = ["task-arena-size-12288", "executor-thread"] } +embassy-executor = { version = "0.6", features = ["task-arena-size-32768"] } embassy-sync = "0.6" embassy-time = "0.3" embassy-usb = { version = "0.3", features = ["defmt"] } @@ -27,7 +27,7 @@ ssd1306 = { version = "0.10", features = ["async"] } embassy-futures = "0.1" # ESP32-S3 (Heltec V3) — requires esp toolchain (espup) -esp-backtrace = { version = "0.14", optional = true, features = ["esp32s3", "panic-handler", "defmt"] } +esp-backtrace = { version = "0.14", optional = true, features = ["esp32s3", "panic-handler", "exception-handler", "defmt"] } esp-hal = { version = "0.22", optional = true, features = ["esp32s3"] } esp-hal-embassy = { version = "0.5", optional = true, features = ["esp32s3"] } esp-println = { version = "0.12", optional = true, features = ["esp32s3", "defmt-espflash"] } @@ -47,18 +47,24 @@ heltec_v3 = [ "dep:esp-hal", "dep:esp-hal-embassy", "dep:esp-println", - "embassy-executor/integrated-timers", + "embassy-time/generic-queue-8", ] heltec_v4 = [ "dep:esp-backtrace", "dep:esp-hal", "dep:esp-hal-embassy", "dep:esp-println", - "embassy-executor/integrated-timers", + "embassy-time/generic-queue-8", ] rak_wisblock_4631 = [ "dep:cortex-m-rt", "dep:embassy-nrf", "dep:panic-probe", "embassy-executor/arch-cortex-m", + "embassy-executor/executor-thread", ] + +[profile.release] +codegen-units = 1 +lto = "fat" +opt-level = 3 diff --git a/Justfile b/Justfile index 00276e88..43d26791 100644 --- a/Justfile +++ b/Justfile @@ -47,7 +47,7 @@ clippy board: case "$target" in xtensa-*) \ just _require_esp_toolchain; \ [ -f "$HOME/export-esp.sh" ] && . "$HOME/export-esp.sh"; \ - env="RUSTC=$(rustup which rustc --toolchain esp)"; extra="+nightly -Zbuild-std=core";; \ + env="RUSTC=$(rustup which rustc --toolchain esp)"; extra="+nightly";; \ esac; \ eval $env cargo $extra clippy --target $target --features $feat -- -D warnings @@ -81,7 +81,7 @@ _cargo board cmd: case "$target" in xtensa-*) \ just _require_esp_toolchain; \ [ -f "$HOME/export-esp.sh" ] && . "$HOME/export-esp.sh"; \ - env="RUSTC=$(rustup which rustc --toolchain esp)"; extra="+nightly -Zbuild-std=core";; \ + env="RUSTC=$(rustup which rustc --toolchain esp)"; extra="+nightly";; \ esac; \ eval $env cargo $extra {{cmd}} --target $target --features $feat diff --git a/src/board/heltec_v3.rs b/src/board/heltec_v3.rs index 67095396..7c51ec0c 100644 --- a/src/board/heltec_v3.rs +++ b/src/board/heltec_v3.rs @@ -21,7 +21,8 @@ type Iv = GenericSx126xInterfaceVariant, Input<'static>>; type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, SpiBus, Nss>; pub type RadioDriver = Sx126x; -pub type UsbDriver = esp_hal::otg_fs::asynch::Driver<'static>; +// TODO: V3 has CP2102 on UART0 — USB OTG on GPIO19/20 goes nowhere +pub type UsbDriver = (); pub type DisplayI2c = I2c<'static, esp_hal::Async>; @@ -48,22 +49,20 @@ pub struct Board { impl Board { pub fn init() -> Self { - let mut p = esp_hal::init(esp_hal::Config::default()); - - let timg0 = TimerGroup::new(core::mem::replace( - &mut p.TIMG0, - unsafe { esp_hal::peripherals::TIMG0::steal() }, - )); - esp_hal_embassy::init(timg0.timer0); - + let p = esp_hal::init(esp_hal::Config::default()); Self { p } } pub fn into_parts(self) -> (RadioParts, UsbParts, Option) { let p = self.p; + // ── Embassy time driver ──────────────────────────────────── + let timg0 = TimerGroup::new(p.TIMG0); + esp_hal_embassy::init(timg0.timer0); + // ── Vext power: GPIO21, active LOW to enable OLED ────────── - let _vext = Output::new(p.GPIO21, Level::Low); + let vext = Output::new(p.GPIO21, Level::Low); + core::mem::forget(vext); // ── DMA for SPI ──────────────────────────────────────────── let dma = Dma::new(p.DMA); @@ -112,20 +111,10 @@ impl Board { delay: Delay, }; - // ── USB (native ESP32-S3 OTG) ───────────────────────────── - let usb_inst = esp_hal::otg_fs::Usb::new(p.USB0, p.GPIO20, p.GPIO19); - static EP_OUT_BUF: StaticCell<[u8; 1024]> = StaticCell::new(); - let ep_out_buf = EP_OUT_BUF.init([0u8; 1024]); - let usb = UsbParts { - driver: esp_hal::otg_fs::asynch::Driver::new( - usb_inst, - ep_out_buf, - esp_hal::otg_fs::asynch::Config::default(), - ), - }; + // ── USB (placeholder — V3 uses CP2102 on UART0) ─────────── + let usb = UsbParts { driver: () }; - // ── Display (optional SSD1306 OLED on I2C, 0x3C) ────────── - // Always provide I2C; display_task detects presence via SSD1306 init. + // ── Display (SSD1306 OLED on I2C, 0x3C) ─────────────────── let i2c = I2c::new(p.I2C0, I2cConfig::default()) .with_sda(p.GPIO17) .with_scl(p.GPIO18) diff --git a/src/board/heltec_v4.rs b/src/board/heltec_v4.rs index 54b331ae..adf3a659 100644 --- a/src/board/heltec_v4.rs +++ b/src/board/heltec_v4.rs @@ -21,7 +21,8 @@ type Iv = GenericSx126xInterfaceVariant, Input<'static>>; type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, SpiBus, Nss>; pub type RadioDriver = Sx126x; -pub type UsbDriver = esp_hal::otg_fs::asynch::Driver<'static>; +// TODO: USB CDC-ACM (UsbSerialJtag for dev, or full OTG stack for production) +pub type UsbDriver = (); pub type DisplayI2c = I2c<'static, esp_hal::Async>; @@ -48,23 +49,21 @@ pub struct Board { impl Board { pub fn init() -> Self { - let mut p = esp_hal::init(esp_hal::Config::default()); - - // Init embassy time driver before consuming peripherals - let timg0 = TimerGroup::new(core::mem::replace( - &mut p.TIMG0, - unsafe { esp_hal::peripherals::TIMG0::steal() }, - )); - esp_hal_embassy::init(timg0.timer0); - + let p = esp_hal::init(esp_hal::Config::default()); Self { p } } pub fn into_parts(self) -> (RadioParts, UsbParts, Option) { let p = self.p; - // ── Vext power: GPIO36, active HIGH to enable OLED ───────── - let _vext = Output::new(p.GPIO36, Level::High); + // ── Embassy time driver ──────────────────────────────────── + let timg0 = TimerGroup::new(p.TIMG0); + esp_hal_embassy::init(timg0.timer0); + + // ── Vext power: GPIO36 ───────────────────────────────────── + // V4 datasheet says "pull low to enable Vext" + let vext = Output::new(p.GPIO36, Level::Low); + core::mem::forget(vext); // ── DMA for SPI ──────────────────────────────────────────── let dma = Dma::new(p.DMA); @@ -113,24 +112,17 @@ impl Board { delay: Delay, }; - // ── USB (native ESP32-S3 OTG) ───────────────────────────── - let usb_inst = esp_hal::otg_fs::Usb::new(p.USB0, p.GPIO20, p.GPIO19); - static EP_OUT_BUF: StaticCell<[u8; 1024]> = StaticCell::new(); - let ep_out_buf = EP_OUT_BUF.init([0u8; 1024]); - let usb = UsbParts { - driver: esp_hal::otg_fs::asynch::Driver::new( - usb_inst, - ep_out_buf, - esp_hal::otg_fs::asynch::Config::default(), - ), - }; + // ── USB (placeholder) ────────────────────────────────────── + let usb = UsbParts { driver: () }; - // ── Display (optional SSD1315 OLED on I2C, 0x3C) ────────── - // Reset display controller via GPIO21 + // ── Display (SSD1315 OLED on I2C, 0x3C) ─────────────────── + // Reset display controller let mut display_rst = Output::new(p.GPIO21, Level::Low); + esp_hal::delay::Delay::new().delay_millis(10); display_rst.set_high(); + esp_hal::delay::Delay::new().delay_millis(10); + core::mem::forget(display_rst); - // Always provide I2C; display_task detects presence via SSD1306 init. let i2c = I2c::new(p.I2C0, I2cConfig::default()) .with_sda(p.GPIO17) .with_scl(p.GPIO18) diff --git a/src/display/task.rs b/src/display/task.rs index d1af2827..77c3b457 100644 --- a/src/display/task.rs +++ b/src/display/task.rs @@ -1,10 +1,10 @@ use embassy_executor::task; use embassy_futures::select::{select, Either}; use embassy_time::Timer; -use ssd1306::mode::DisplayConfig; +use ssd1306::mode::DisplayConfigAsync; use ssd1306::prelude::DisplayRotation; use ssd1306::size::DisplaySize128x64; -use ssd1306::{I2CDisplayInterface, Ssd1306}; +use ssd1306::{I2CDisplayInterface, Ssd1306Async}; use crate::board::DisplayParts; use crate::channel::{DisplayCommand, DisplayCommandChannel, StatusWatch}; @@ -52,17 +52,18 @@ pub async fn display_task( display_commands: &'static DisplayCommandChannel, ) { let interface = I2CDisplayInterface::new(parts.i2c); - let mut display = Ssd1306::new(interface, DisplaySize128x64, DisplayRotation::Rotate0) + let mut display = Ssd1306Async::new(interface, DisplaySize128x64, DisplayRotation::Rotate0) .into_buffered_graphics_mode(); - if display.init().is_err() { + if display.init().await.is_err() { defmt::error!("SSD1306 init failed"); return; } + let _ = display.set_brightness(ssd1306::prelude::Brightness::BRIGHTEST).await; // Splash screen render::splash(&mut display, BOARD_NAME, env!("CARGO_PKG_VERSION")); - let _ = display.flush(); + let _ = display.flush().await; Timer::after_millis(1500).await; // Dashboard @@ -76,7 +77,7 @@ pub async fn display_task( &state.rssi_history, state.rssi_count, ); - let _ = display.flush(); + let _ = display.flush().await; loop { match select(receiver.changed(), display_commands.receive()).await { @@ -99,13 +100,13 @@ pub async fn display_task( &state.rssi_history, state.rssi_count, ); - let _ = display.flush(); + let _ = display.flush().await; } Either::Second(cmd) => match cmd { DisplayCommand::Off => { state.display_on = false; render::blank(&mut display); - let _ = display.flush(); + let _ = display.flush().await; } DisplayCommand::On => { state.display_on = true; diff --git a/src/main.rs b/src/main.rs index 629dbf65..eed77bd5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,7 +17,7 @@ cfg_if::cfg_if! { use panic_probe as _; } else if #[cfg(any(feature = "heltec_v3", feature = "heltec_v4"))] { use esp_backtrace as _; - use esp_println as _; // provides defmt global_logger + use esp_println as _; } } @@ -26,7 +26,6 @@ static RESPONSES: ResponseChannel = ResponseChannel::new(); static STATUS: StatusWatch = StatusWatch::new(); static DISPLAY_COMMANDS: DisplayCommandChannel = DisplayCommandChannel::new(); -// Entry point: each platform needs its own executor macro. cfg_if::cfg_if! { if #[cfg(feature = "rak_wisblock_4631")] { #[embassy_executor::main]