feat: add Heltec V4, wire up V3/V4 peripherals, restructure entry point
- Add heltec_v4 board: ESP32-S3R2, SX1262, SSD1315 display, native USB, Vext on GPIO36 (active HIGH) - Wire up heltec_v3 with real esp-hal peripherals (was a stub) - V3 and V4 share radio/I2C pins, differ in Vext pin/polarity and display reset - Restructure main.rs: cfg_if selects embassy_executor::main (Cortex-M) vs esp_hal_embassy::main (Xtensa) per board - Display detection via SSD1306 init in display_task — boards always provide I2C bus, task exits gracefully if no display responds Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
6 files changed
+276
-22
No files matched your search
@@ -48,6 +48,12 @@ heltec_v3 = [
|
|||||||
"dep:esp-hal-embassy",
|
"dep:esp-hal-embassy",
|
||||||
"dep:esp-println",
|
"dep:esp-println",
|
||||||
]
|
]
|
||||||
|
heltec_v4 = [
|
||||||
|
"dep:esp-backtrace",
|
||||||
|
"dep:esp-hal",
|
||||||
|
"dep:esp-hal-embassy",
|
||||||
|
"dep:esp-println",
|
||||||
|
]
|
||||||
rak_4631 = [
|
rak_4631 = [
|
||||||
"dep:cortex-m-rt",
|
"dep:cortex-m-rt",
|
||||||
"dep:embassy-nrf",
|
"dep:embassy-nrf",
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ set shell := ["bash", "-c"]
|
|||||||
# Xtensa targets use nightly cargo + esp rustc + -Zbuild-std=core.
|
# Xtensa targets use nightly cargo + esp rustc + -Zbuild-std=core.
|
||||||
# check-all gracefully skips boards whose toolchain isn't installed.
|
# check-all gracefully skips boards whose toolchain isn't installed.
|
||||||
heltec_v3 := "heltec_v3 xtensa-esp32s3-none-elf esp32s3"
|
heltec_v3 := "heltec_v3 xtensa-esp32s3-none-elf esp32s3"
|
||||||
|
heltec_v4 := "heltec_v4 xtensa-esp32s3-none-elf esp32s3"
|
||||||
rak_4631 := "rak_4631 thumbv7em-none-eabihf nRF52840_xxAA"
|
rak_4631 := "rak_4631 thumbv7em-none-eabihf nRF52840_xxAA"
|
||||||
|
|
||||||
firmware_dir := "firmware"
|
firmware_dir := "firmware"
|
||||||
|
|
||||||
# All known boards
|
# All known boards
|
||||||
boards := "heltec_v3 rak_4631"
|
boards := "heltec_v3 heltec_v4 rak_4631"
|
||||||
|
|
||||||
# Check all boards that can build with the available toolchain
|
# Check all boards that can build with the available toolchain
|
||||||
check-all:
|
check-all:
|
||||||
@@ -79,5 +80,6 @@ _copy_firmware board profile:
|
|||||||
[private]
|
[private]
|
||||||
_info name:
|
_info name:
|
||||||
@if [ "{{name}}" == "heltec_v3" ]; then echo "{{heltec_v3}}"; \
|
@if [ "{{name}}" == "heltec_v3" ]; then echo "{{heltec_v3}}"; \
|
||||||
|
elif [ "{{name}}" == "heltec_v4" ]; then echo "{{heltec_v4}}"; \
|
||||||
elif [ "{{name}}" == "rak_4631" ]; then echo "{{rak_4631}}"; \
|
elif [ "{{name}}" == "rak_4631" ]; then echo "{{rak_4631}}"; \
|
||||||
else echo "Unknown board: {{name}}" >&2; exit 1; fi
|
else echo "Unknown board: {{name}}" >&2; exit 1; fi
|
||||||
+106
-17
@@ -1,21 +1,30 @@
|
|||||||
|
use embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice;
|
||||||
|
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
|
||||||
use embassy_time::Delay;
|
use embassy_time::Delay;
|
||||||
|
use esp_hal::dma::{Dma, DmaPriority};
|
||||||
|
use esp_hal::gpio::{Input, Level, Output, Pull};
|
||||||
|
use esp_hal::i2c::master::{Config as I2cConfig, I2c};
|
||||||
|
use esp_hal::prelude::*;
|
||||||
|
use esp_hal::spi::master::{Config as SpiConfig, Spi, SpiDmaBus};
|
||||||
|
use esp_hal::spi::SpiMode;
|
||||||
|
use esp_hal::timer::timg::TimerGroup;
|
||||||
|
use static_cell::StaticCell;
|
||||||
|
|
||||||
use lora_phy::iv::GenericSx126xInterfaceVariant;
|
use lora_phy::iv::GenericSx126xInterfaceVariant;
|
||||||
use lora_phy::sx126x::{Sx1262, Sx126x};
|
use lora_phy::sx126x::{self, Sx1262, Sx126x};
|
||||||
|
|
||||||
// ── Concrete peripheral types ────────────────────────────────────────
|
// ── Concrete peripheral types ────────────────────────────────────────
|
||||||
|
|
||||||
// TODO: replace these placeholders with real esp-hal peripheral types
|
type SpiBus = SpiDmaBus<'static, esp_hal::Async>;
|
||||||
// SPI2 for SX1262 (SCK=9, MOSI=10, MISO=11, NSS=8)
|
type Nss = Output<'static>;
|
||||||
// GPIO for radio control (RST=12, DIO1=14, BUSY=13)
|
type Iv = GenericSx126xInterfaceVariant<Output<'static>, Input<'static>>;
|
||||||
// Vext pin (GPIO21) LOW to power OLED
|
type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, SpiBus, Nss>;
|
||||||
// I2C0 for SSD1306 (SDA=17, SCL=18)
|
|
||||||
// USB OTG for CDC-ACM
|
|
||||||
|
|
||||||
type Iv = GenericSx126xInterfaceVariant<(), ()>;
|
|
||||||
type RadioSpiDevice = ();
|
|
||||||
pub type RadioDriver = Sx126x<RadioSpiDevice, Iv, Sx1262>;
|
pub type RadioDriver = Sx126x<RadioSpiDevice, Iv, Sx1262>;
|
||||||
|
|
||||||
|
pub type UsbDriver = esp_hal::otg_fs::asynch::Driver<'static>;
|
||||||
|
|
||||||
|
pub type DisplayI2c = I2c<'static, esp_hal::Async>;
|
||||||
|
|
||||||
// ── Peripheral bundles ───────────────────────────────────────────────
|
// ── Peripheral bundles ───────────────────────────────────────────────
|
||||||
|
|
||||||
pub struct RadioParts {
|
pub struct RadioParts {
|
||||||
@@ -24,25 +33,105 @@ pub struct RadioParts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct UsbParts {
|
pub struct UsbParts {
|
||||||
pub driver: (),
|
pub driver: UsbDriver,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DisplayParts {
|
pub struct DisplayParts {
|
||||||
pub i2c: (),
|
pub i2c: DisplayI2c,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Board init ───────────────────────────────────────────────────────
|
// ── Board init ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub struct Board;
|
pub struct Board {
|
||||||
|
p: esp_hal::peripherals::Peripherals,
|
||||||
|
}
|
||||||
|
|
||||||
impl Board {
|
impl Board {
|
||||||
pub fn init() -> Self {
|
pub fn init() -> Self {
|
||||||
// TODO: call esp_hal::init(esp_hal::Config::default())
|
let mut p = esp_hal::init(esp_hal::Config::default());
|
||||||
Self
|
|
||||||
|
let timg0 = TimerGroup::new(core::mem::replace(
|
||||||
|
&mut p.TIMG0,
|
||||||
|
unsafe { esp_hal::peripherals::TIMG0::steal() },
|
||||||
|
));
|
||||||
|
esp_hal_embassy::init(timg0.timer0);
|
||||||
|
|
||||||
|
Self { p }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_parts(self) -> (RadioParts, UsbParts, Option<DisplayParts>) {
|
pub fn into_parts(self) -> (RadioParts, UsbParts, Option<DisplayParts>) {
|
||||||
// TODO: wire up real peripherals once building with nightly + -Zbuild-std
|
let p = self.p;
|
||||||
todo!("Heltec V3 peripheral init")
|
|
||||||
|
// ── Vext power: GPIO21, active LOW to enable OLED ──────────
|
||||||
|
let _vext = Output::new(p.GPIO21, Level::Low);
|
||||||
|
|
||||||
|
// ── DMA for SPI ────────────────────────────────────────────
|
||||||
|
let dma = Dma::new(p.DMA);
|
||||||
|
let dma_ch = dma.channel0.configure(false, DmaPriority::Priority0);
|
||||||
|
|
||||||
|
// ── SPI + DMA for SX1262 ───────────────────────────────────
|
||||||
|
let spi = Spi::new_with_config(p.SPI2, SpiConfig {
|
||||||
|
frequency: 1u32.MHz(),
|
||||||
|
mode: SpiMode::Mode0,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.with_sck(p.GPIO9)
|
||||||
|
.with_mosi(p.GPIO10)
|
||||||
|
.with_miso(p.GPIO11)
|
||||||
|
.with_dma(dma_ch);
|
||||||
|
|
||||||
|
let (rx_buf, rx_desc, tx_buf, tx_desc) = esp_hal::dma_buffers!(256);
|
||||||
|
let dma_rx = esp_hal::dma::DmaRxBuf::new(rx_desc, rx_buf).unwrap();
|
||||||
|
let dma_tx = esp_hal::dma::DmaTxBuf::new(tx_desc, tx_buf).unwrap();
|
||||||
|
|
||||||
|
let spi = spi.with_buffers(dma_rx, dma_tx).into_async();
|
||||||
|
|
||||||
|
static SPI_BUS: StaticCell<embassy_sync::mutex::Mutex<NoopRawMutex, SpiBus>> =
|
||||||
|
StaticCell::new();
|
||||||
|
let spi_bus = SPI_BUS.init(embassy_sync::mutex::Mutex::new(spi));
|
||||||
|
|
||||||
|
let nss = Output::new(p.GPIO8, Level::High);
|
||||||
|
let spi_device = SpiDevice::new(spi_bus, nss);
|
||||||
|
|
||||||
|
// ── SX1262 control pins ────────────────────────────────────
|
||||||
|
let reset = Output::new(p.GPIO12, Level::High);
|
||||||
|
let dio1 = Input::new(p.GPIO14, Pull::Down);
|
||||||
|
let busy = Input::new(p.GPIO13, Pull::Down);
|
||||||
|
|
||||||
|
let iv = GenericSx126xInterfaceVariant::new(reset, dio1, busy, None, None).unwrap();
|
||||||
|
|
||||||
|
let sx_config = sx126x::Config {
|
||||||
|
chip: Sx1262,
|
||||||
|
tcxo_ctrl: Some(sx126x::TcxoCtrlVoltage::Ctrl1V7),
|
||||||
|
use_dcdc: true,
|
||||||
|
rx_boost: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let radio = RadioParts {
|
||||||
|
driver: Sx126x::new(spi_device, iv, sx_config),
|
||||||
|
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(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Display (optional SSD1306 OLED on I2C, 0x3C) ──────────
|
||||||
|
// 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)
|
||||||
|
.into_async();
|
||||||
|
let display = Some(DisplayParts { i2c });
|
||||||
|
|
||||||
|
(radio, usb, display)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
use embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice;
|
||||||
|
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
|
||||||
|
use embassy_time::Delay;
|
||||||
|
use esp_hal::dma::{Dma, DmaPriority};
|
||||||
|
use esp_hal::gpio::{Input, Level, Output, Pull};
|
||||||
|
use esp_hal::i2c::master::{Config as I2cConfig, I2c};
|
||||||
|
use esp_hal::prelude::*;
|
||||||
|
use esp_hal::spi::master::{Config as SpiConfig, Spi, SpiDmaBus};
|
||||||
|
use esp_hal::spi::SpiMode;
|
||||||
|
use esp_hal::timer::timg::TimerGroup;
|
||||||
|
use static_cell::StaticCell;
|
||||||
|
|
||||||
|
use lora_phy::iv::GenericSx126xInterfaceVariant;
|
||||||
|
use lora_phy::sx126x::{self, Sx1262, Sx126x};
|
||||||
|
|
||||||
|
// ── Concrete peripheral types ────────────────────────────────────────
|
||||||
|
|
||||||
|
type SpiBus = SpiDmaBus<'static, esp_hal::Async>;
|
||||||
|
type Nss = Output<'static>;
|
||||||
|
type Iv = GenericSx126xInterfaceVariant<Output<'static>, Input<'static>>;
|
||||||
|
type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, SpiBus, Nss>;
|
||||||
|
pub type RadioDriver = Sx126x<RadioSpiDevice, Iv, Sx1262>;
|
||||||
|
|
||||||
|
pub type UsbDriver = esp_hal::otg_fs::asynch::Driver<'static>;
|
||||||
|
|
||||||
|
pub type DisplayI2c = I2c<'static, esp_hal::Async>;
|
||||||
|
|
||||||
|
// ── Peripheral bundles ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub struct RadioParts {
|
||||||
|
pub driver: RadioDriver,
|
||||||
|
pub delay: Delay,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UsbParts {
|
||||||
|
pub driver: UsbDriver,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DisplayParts {
|
||||||
|
pub i2c: DisplayI2c,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Board init ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub struct Board {
|
||||||
|
p: esp_hal::peripherals::Peripherals,
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
Self { p }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_parts(self) -> (RadioParts, UsbParts, Option<DisplayParts>) {
|
||||||
|
let p = self.p;
|
||||||
|
|
||||||
|
// ── Vext power: GPIO36, active HIGH to enable OLED ─────────
|
||||||
|
let _vext = Output::new(p.GPIO36, Level::High);
|
||||||
|
|
||||||
|
// ── DMA for SPI ────────────────────────────────────────────
|
||||||
|
let dma = Dma::new(p.DMA);
|
||||||
|
let dma_ch = dma.channel0.configure(false, DmaPriority::Priority0);
|
||||||
|
|
||||||
|
// ── SPI + DMA for SX1262 ───────────────────────────────────
|
||||||
|
let spi = Spi::new_with_config(p.SPI2, SpiConfig {
|
||||||
|
frequency: 1u32.MHz(),
|
||||||
|
mode: SpiMode::Mode0,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.with_sck(p.GPIO9)
|
||||||
|
.with_mosi(p.GPIO10)
|
||||||
|
.with_miso(p.GPIO11)
|
||||||
|
.with_dma(dma_ch);
|
||||||
|
|
||||||
|
let (rx_buf, rx_desc, tx_buf, tx_desc) = esp_hal::dma_buffers!(256);
|
||||||
|
let dma_rx = esp_hal::dma::DmaRxBuf::new(rx_desc, rx_buf).unwrap();
|
||||||
|
let dma_tx = esp_hal::dma::DmaTxBuf::new(tx_desc, tx_buf).unwrap();
|
||||||
|
|
||||||
|
let spi = spi.with_buffers(dma_rx, dma_tx).into_async();
|
||||||
|
|
||||||
|
static SPI_BUS: StaticCell<embassy_sync::mutex::Mutex<NoopRawMutex, SpiBus>> =
|
||||||
|
StaticCell::new();
|
||||||
|
let spi_bus = SPI_BUS.init(embassy_sync::mutex::Mutex::new(spi));
|
||||||
|
|
||||||
|
let nss = Output::new(p.GPIO8, Level::High);
|
||||||
|
let spi_device = SpiDevice::new(spi_bus, nss);
|
||||||
|
|
||||||
|
// ── SX1262 control pins ────────────────────────────────────
|
||||||
|
let reset = Output::new(p.GPIO12, Level::High);
|
||||||
|
let dio1 = Input::new(p.GPIO14, Pull::Down);
|
||||||
|
let busy = Input::new(p.GPIO13, Pull::Down);
|
||||||
|
|
||||||
|
let iv = GenericSx126xInterfaceVariant::new(reset, dio1, busy, None, None).unwrap();
|
||||||
|
|
||||||
|
let sx_config = sx126x::Config {
|
||||||
|
chip: Sx1262,
|
||||||
|
tcxo_ctrl: Some(sx126x::TcxoCtrlVoltage::Ctrl1V7),
|
||||||
|
use_dcdc: true,
|
||||||
|
rx_boost: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let radio = RadioParts {
|
||||||
|
driver: Sx126x::new(spi_device, iv, sx_config),
|
||||||
|
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(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Display (optional SSD1315 OLED on I2C, 0x3C) ──────────
|
||||||
|
// Reset display controller via GPIO21
|
||||||
|
let mut display_rst = Output::new(p.GPIO21, Level::Low);
|
||||||
|
display_rst.set_high();
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
.into_async();
|
||||||
|
let display = Some(DisplayParts { i2c });
|
||||||
|
|
||||||
|
(radio, usb, display)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -103,7 +103,8 @@ impl Board {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ── Display (optional RAK1921 SSD1306 OLED on I2C) ──────
|
// ── Display (optional RAK1921 SSD1306 OLED on I2C) ──────
|
||||||
// TODO: probe I2C address 0x3C to detect presence
|
// Always provide the I2C bus; display_task detects presence
|
||||||
|
// via SSD1306 init (fails gracefully if no display attached).
|
||||||
let i2c_cfg = twim::Config::default();
|
let i2c_cfg = twim::Config::default();
|
||||||
let i2c = Twim::new(p.TWISPI0, Irqs, p.P0_13, p.P0_14, i2c_cfg);
|
let i2c = Twim::new(p.TWISPI0, Irqs, p.P0_13, p.P0_14, i2c_cfg);
|
||||||
let display = Some(DisplayParts { i2c });
|
let display = Some(DisplayParts { i2c });
|
||||||
|
|||||||
+17
-3
@@ -15,7 +15,7 @@ use crate::channel::{CommandChannel, DisplayCommandChannel, ResponseChannel, Sta
|
|||||||
cfg_if::cfg_if! {
|
cfg_if::cfg_if! {
|
||||||
if #[cfg(feature = "rak_4631")] {
|
if #[cfg(feature = "rak_4631")] {
|
||||||
use panic_probe as _;
|
use panic_probe as _;
|
||||||
} else if #[cfg(feature = "heltec_v3")] {
|
} else if #[cfg(any(feature = "heltec_v3", feature = "heltec_v4"))] {
|
||||||
use esp_backtrace as _;
|
use esp_backtrace as _;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,8 +25,22 @@ static RESPONSES: ResponseChannel = ResponseChannel::new();
|
|||||||
static STATUS: StatusWatch = StatusWatch::new();
|
static STATUS: StatusWatch = StatusWatch::new();
|
||||||
static DISPLAY_COMMANDS: DisplayCommandChannel = DisplayCommandChannel::new();
|
static DISPLAY_COMMANDS: DisplayCommandChannel = DisplayCommandChannel::new();
|
||||||
|
|
||||||
#[embassy_executor::main]
|
// Entry point: each platform needs its own executor macro.
|
||||||
async fn main(spawner: Spawner) {
|
cfg_if::cfg_if! {
|
||||||
|
if #[cfg(feature = "rak_4631")] {
|
||||||
|
#[embassy_executor::main]
|
||||||
|
async fn main(spawner: Spawner) {
|
||||||
|
run(spawner).await;
|
||||||
|
}
|
||||||
|
} else if #[cfg(any(feature = "heltec_v3", feature = "heltec_v4"))] {
|
||||||
|
#[esp_hal_embassy::main]
|
||||||
|
async fn main(spawner: Spawner) {
|
||||||
|
run(spawner).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(spawner: Spawner) {
|
||||||
let board = board::Board::init();
|
let board = board::Board::init();
|
||||||
let (radio, usb, display) = board.into_parts();
|
let (radio, usb, display) = board.into_parts();
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user