feat: native USB CDC-ACM support for Heltec V3

The default heltec_v3 feature now uses native USB OTG (same as V4),
appearing as /dev/ttyACM0 with the DongLoRa VID:PID (1209:5741).
This requires a hardware mod: solder R29/R3 and disconnect the CP2102
from the USB data lines.

Stock (unmodified) V3 boards should use the new heltec_v3_uart feature
which preserves the original UART-over-CP2102 behavior (/dev/ttyUSB0).

- heltec_v3: switched from UART to USB OTG
- heltec_v3_uart: new feature, UART fallback for stock boards
- Removed #[cfg] gates on UsbParts/UartParts (both always defined)
- Updated README with hardware mod instructions and board table
- Updated justfile with heltec_v3_uart board definition
This commit is contained in:
swaits committed 2026-04-07 10:44:25 -06:00
1 parent 07651dce44
commit 6db287c241
9 files changed
+142 -20

No files matched your search

+17 -6
View File
@@ -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<RadioParts, UartParts, DisplayParts> {
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts> {
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