diff --git a/Cargo.toml b/Cargo.toml index f5b0c6e..51e3745 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,6 +84,12 @@ heltec_v3 = [ "dep:esp-println", "dep:esp-rtos", ] +heltec_v3_uart = [ + "dep:esp-backtrace", + "dep:esp-hal", + "dep:esp-println", + "dep:esp-rtos", +] heltec_v4 = [ "dep:esp-backtrace", "dep:esp-hal", diff --git a/README.md b/README.md index 419f276..f997b62 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,39 @@ All commands are run from the `firmware/` directory. ## Supported Boards -| Board | MCU | Radio | Target | -|-------|-----|-------|--------| -| Heltec V3 | ESP32-S3 | SX1262 | xtensa-esp32s3-none-elf | -| Heltec V4 | ESP32-S3 | SX1262 | xtensa-esp32s3-none-elf | -| RAK WisBlock 4631 | nRF52840 | SX1262 | thumbv7em-none-eabihf | +| Board | Feature | MCU | Radio | USB | Target | +|-------|---------|-----|-------|-----|--------| +| Heltec V3 | `heltec_v3` | ESP32-S3 | SX1262 | Native CDC-ACM | xtensa-esp32s3-none-elf | +| Heltec V3 (UART) | `heltec_v3_uart` | ESP32-S3 | SX1262 | CP2102 bridge | xtensa-esp32s3-none-elf | +| Heltec V4 | `heltec_v4` | ESP32-S3 | SX1262 | Native CDC-ACM | xtensa-esp32s3-none-elf | +| RAK WisBlock 4631 | `rak_wisblock_4631` | nRF52840 | SX1262 | Native CDC-ACM | thumbv7em-none-eabihf | +| Wio Tracker L1 | `wio_tracker_l1` | nRF52840 | SX1262 | Native CDC-ACM | thumbv7em-none-eabihf | + +### Heltec V3 USB Note + +The default `heltec_v3` build uses **native USB CDC-ACM**, which requires a +hardware modification to your board. Stock Heltec V3 boards route USB through a +CP2102 bridge chip to UART — the ESP32-S3's native USB pins (GPIO19/GPIO20) are +routed to header pins but not connected to the USB connector. + +**To enable native USB on a Heltec V3:** + +1. Solder 0-ohm (or 22-ohm) resistors at pads **R29** (D+) and **R3** (D-) +2. Disconnect the CP2102 from the USB connector's D+/D- lines (cut traces or + remove the CP2102 bridge resistors) + +After this mod, the board appears as `/dev/ttyACM0` with the DongLoRa VID:PID +(1209:5741), just like the V4 and nRF boards. + +**If you have a stock (unmodified) V3**, use `heltec_v3_uart` instead: + +```sh +just build heltec_v3_uart +just flash heltec_v3_uart +``` + +This builds firmware that communicates via the CP2102 bridge (appears as +`/dev/ttyUSB0`). ## Adding a Board diff --git a/justfile b/justfile index 5936206..cb60c4d 100644 --- a/justfile +++ b/justfile @@ -3,8 +3,9 @@ set shell := ["bash", "-c"] # Board definitions: feature target chip # Xtensa targets use nightly cargo + esp rustc + -Zbuild-std=core. # Tool versions are pinned in mise.toml; run `just setup` to install everything. -heltec_v3 := "heltec_v3 xtensa-esp32s3-none-elf esp32s3" -heltec_v4 := "heltec_v4 xtensa-esp32s3-none-elf esp32s3" +heltec_v3 := "heltec_v3 xtensa-esp32s3-none-elf esp32s3" +heltec_v3_uart := "heltec_v3_uart xtensa-esp32s3-none-elf esp32s3" +heltec_v4 := "heltec_v4 xtensa-esp32s3-none-elf esp32s3" rak_wisblock_4631 := "rak_wisblock_4631 thumbv7em-none-eabihf nRF52840_xxAA" wio_tracker_l1 := "wio_tracker_l1 thumbv7em-none-eabihf nRF52840_xxAA" @@ -12,7 +13,7 @@ builds_dir := "builds" version := `sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml` # All known boards -boards := "heltec_v3 heltec_v4 rak_wisblock_4631 wio_tracker_l1" +boards := "heltec_v3 heltec_v3_uart heltec_v4 rak_wisblock_4631 wio_tracker_l1" # Install all required tools and toolchains setup: @@ -203,6 +204,7 @@ _flash_uf2 board: [private] _info name: @if [ "{{name}}" == "heltec_v3" ]; then echo "{{heltec_v3}}"; \ + elif [ "{{name}}" == "heltec_v3_uart" ]; then echo "{{heltec_v3_uart}}"; \ elif [ "{{name}}" == "heltec_v4" ]; then echo "{{heltec_v4}}"; \ elif [ "{{name}}" == "rak_wisblock_4631" ]; then echo "{{rak_wisblock_4631}}"; \ elif [ "{{name}}" == "wio_tracker_l1" ]; then echo "{{wio_tracker_l1}}"; \ diff --git a/src/board/esp32s3.rs b/src/board/esp32s3.rs index 34c2670..f6b522e 100644 --- a/src/board/esp32s3.rs +++ b/src/board/esp32s3.rs @@ -32,12 +32,12 @@ pub struct RadioParts { pub delay: Delay, } -#[cfg(not(feature = "heltec_v3"))] +#[allow(dead_code)] // Not used by heltec_v3_uart pub struct UsbParts { pub driver: UsbDriver, } -#[cfg(feature = "heltec_v3")] +#[allow(dead_code)] // Only used by heltec_v3_uart pub struct UartParts { pub driver: UartDriver, } diff --git a/src/board/heltec_v3.rs b/src/board/heltec_v3.rs index 2e506dd..005bc8d 100644 --- a/src/board/heltec_v3.rs +++ b/src/board/heltec_v3.rs @@ -1,3 +1,11 @@ +//! Heltec WiFi LoRa 32 V3 — native USB (requires hardware mod). +//! +//! Stock V3 boards route USB through a CP2102 bridge to UART. To use native +//! USB CDC-ACM, solder R29 (D+) and R3 (D-) on the PCB and disconnect the +//! CP2102 from the USB data lines. See the project README for details. +//! +//! For stock (unmodified) V3 boards, use the `heltec_v3_uart` feature instead. + use esp_hal::gpio::{Level, Output, OutputConfig}; use super::esp32s3; @@ -6,8 +14,8 @@ use crate::hal::esp32s3 as mcu; #[allow(unused_imports)] // Re-exported for other modules (display, radio, host tasks) pub use super::esp32s3::{ - create_display, DisplayDriver, DisplayI2c, DisplayParts, RadioDriver, RadioParts, UartDriver, - UartParts, + create_display, DisplayDriver, DisplayI2c, DisplayParts, RadioDriver, RadioParts, UsbDriver, + UsbParts, }; pub struct Board { @@ -19,7 +27,7 @@ impl LoRaBoard for Board { const TX_POWER_RANGE: (i8, i8) = (-9, 22); type RadioParts = RadioParts; - type CommParts = UartParts; + type CommParts = UsbParts; type DisplayParts = DisplayParts; type DisplayDriver = DisplayDriver; @@ -32,7 +40,7 @@ impl LoRaBoard for Board { mcu::mac_address() } - fn into_parts(self) -> BoardParts { + fn into_parts(self) -> BoardParts { let p = self.p; mcu::start_timer(p.TIMG0); @@ -43,8 +51,11 @@ impl LoRaBoard for Board { let spi_bus = mcu::init_spi(p.SPI2, p.DMA_CH0, p.GPIO9, p.GPIO10, p.GPIO11); let radio = esp32s3::init_radio(spi_bus, p.GPIO8, p.GPIO12, p.GPIO14, p.GPIO13); - let host = UartParts { - driver: mcu::init_uart(p.UART0, p.GPIO43, p.GPIO44), + + // Note: switches internal USB PHY from Serial-JTAG to OTG. + // espflash --monitor will stop working after this point. + let host = UsbParts { + driver: mcu::init_usb(p.USB0, p.GPIO20, p.GPIO19), }; // SSD1306 display reset: pulse GPIO21 low->high before I2C init diff --git a/src/board/heltec_v3_uart.rs b/src/board/heltec_v3_uart.rs new file mode 100644 index 0000000..662bb7f --- /dev/null +++ b/src/board/heltec_v3_uart.rs @@ -0,0 +1,75 @@ +//! Heltec WiFi LoRa 32 V3 — stock UART (via CP2102 bridge). +//! +//! This is for unmodified V3 boards where USB goes through the CP2102 +//! bridge to UART0. The board appears as /dev/ttyUSB* (not ttyACM*). +//! +//! If you've done the hardware mod (solder R29/R3, disconnect CP2102), +//! use the `heltec_v3` feature instead for native USB CDC-ACM support. + +use esp_hal::gpio::{Level, Output, OutputConfig}; + +use super::esp32s3; +use super::traits::{BoardParts, LoRaBoard}; +use crate::hal::esp32s3 as mcu; + +#[allow(unused_imports)] // Re-exported for other modules (display, radio, host tasks) +pub use super::esp32s3::{ + create_display, DisplayDriver, DisplayI2c, DisplayParts, RadioDriver, RadioParts, UartDriver, + UartParts, +}; + +pub struct Board { + p: esp_hal::peripherals::Peripherals, +} + +impl LoRaBoard for Board { + const NAME: &'static str = "Heltec V3 (UART)"; + const TX_POWER_RANGE: (i8, i8) = (-9, 22); + + type RadioParts = RadioParts; + type CommParts = UartParts; + type DisplayParts = DisplayParts; + type DisplayDriver = DisplayDriver; + + fn init() -> Self { + let p = esp_hal::init(esp_hal::Config::default()); + Self { p } + } + + fn mac_address() -> [u8; 6] { + mcu::mac_address() + } + + fn into_parts(self) -> BoardParts { + let p = self.p; + + mcu::start_timer(p.TIMG0); + + // Vext power: GPIO36, active LOW to enable peripherals + let vext = Output::new(p.GPIO36, Level::Low, OutputConfig::default()); + core::mem::forget(vext); + + let spi_bus = mcu::init_spi(p.SPI2, p.DMA_CH0, p.GPIO9, p.GPIO10, p.GPIO11); + let radio = esp32s3::init_radio(spi_bus, p.GPIO8, p.GPIO12, p.GPIO14, p.GPIO13); + let host = UartParts { + driver: mcu::init_uart(p.UART0, p.GPIO43, p.GPIO44), + }; + + // SSD1306 display reset: pulse GPIO21 low->high before I2C init + let mut display_rst = Output::new(p.GPIO21, Level::Low, OutputConfig::default()); + 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); + + let i2c = mcu::init_i2c(p.I2C0, p.GPIO17, p.GPIO18); + let display = Some(DisplayParts { i2c }); + + BoardParts { + radio, + host, + display, + mac: Self::mac_address(), + } + } +} diff --git a/src/hal/mod.rs b/src/hal/mod.rs index d430ab3..372fea3 100644 --- a/src/hal/mod.rs +++ b/src/hal/mod.rs @@ -3,7 +3,7 @@ //! Each submodule provides low-level bus and peripheral init for one MCU family. //! Board files call these with specific pins and construct higher-level drivers. -#[cfg(any(feature = "heltec_v3", feature = "heltec_v4"))] +#[cfg(any(feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4"))] pub mod esp32s3; #[cfg(any(feature = "rak_wisblock_4631", feature = "wio_tracker_l1"))] diff --git a/src/host/mod.rs b/src/host/mod.rs index 49e0cd8..15467ba 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -6,7 +6,7 @@ pub mod framing; cfg_if::cfg_if! { - if #[cfg(feature = "heltec_v3")] { + if #[cfg(feature = "heltec_v3_uart")] { mod uart; pub use uart::host_task; } else { diff --git a/src/main.rs b/src/main.rs index 02c1aaa..c17b4b3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -44,7 +44,7 @@ cfg_if::cfg_if! { if #[cfg(any(feature = "rak_wisblock_4631", feature = "wio_tracker_l1"))] { use defmt_rtt as _; use panic_probe as _; - } else if #[cfg(any(feature = "heltec_v3", feature = "heltec_v4"))] { + } else if #[cfg(any(feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4"))] { use esp_backtrace as _; use esp_println as _; } @@ -66,7 +66,7 @@ cfg_if::cfg_if! { async fn main(spawner: Spawner) { run(spawner).await; } - } else if #[cfg(any(feature = "heltec_v3", feature = "heltec_v4"))] { + } else if #[cfg(any(feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4"))] { #[esp_rtos::main] async fn main(spawner: Spawner) { run(spawner).await;