From 18b63bd6f5b082f47d8f605b3f39a6790d4912a3 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Sun, 14 Jun 2026 13:44:33 +0200 Subject: [PATCH] Add Lilygo T-beam 1.2 (SX1262) --- build.sh | 32 ++++- zephcore/CMakeLists.txt | 8 +- zephcore/adapters/board/ZephyrBoard.cpp | 36 +++++- zephcore/boards/esp32/ttgo_tbeam/board.conf | 40 ++++++ .../boards/esp32/ttgo_tbeam/board.overlay | 119 ++++++++++++++++++ zephcore/boards/supported_boards.md | 6 + 6 files changed, 237 insertions(+), 4 deletions(-) create mode 100644 zephcore/boards/esp32/ttgo_tbeam/board.conf create mode 100644 zephcore/boards/esp32/ttgo_tbeam/board.overlay diff --git a/build.sh b/build.sh index f10edd6..0f16c1e 100644 --- a/build.sh +++ b/build.sh @@ -40,6 +40,7 @@ ESP32_boards=( heltec_wifi_lora32_v3/esp32s3/procpu heltec_wifi_lora32_v4/esp32s3/procpu heltec_wifi_lora32_v43/esp32s3/procpu + ttgo_tbeam/esp32/procpu ) if [[ $1 == "nrf" ]]; then @@ -127,13 +128,40 @@ if [[ $1 == "esp32" ]]; then for board in "${ESP32_boards[@]}"; do board_clean_for_path=$(echo "$board" | sed -e 's/\//-/g') - if [[ $board =~ (esp32[^/]+) ]]; then + if [[ $board =~ (esp32[^/]*) ]]; then chip="${BASH_REMATCH[1]}" else echo "Unknown chip for: $board" exit 1; fi - + + # Classic ESP32 (e.g. T-Beam, PICO-D4) cannot fit the mesh stack + # alongside MCUboot in DRAM, so it uses Zephyr's simple-boot path: a + # self-contained zephyr.bin flashed at the 0x1000 ROM bootloader offset. + # The S3/C-series have the DRAM headroom and use sysbuild + MCUboot + # (handled in the blocks below). + if [[ $chip == "esp32" ]]; then + if [[ $2 == "companions" ]]; then + role="companion" + echo "Now building $board companion (simple boot)" + west build -b "$board" zephcore --pristine + elif [[ $2 == "repeaters" ]]; then + role="repeater" + echo "Now building $board repeater (simple boot)" + west build -b "$board" zephcore --pristine -- -DEXTRA_CONF_FILE="boards/common/repeater.conf" + else + continue + fi + # Simple-boot zephyr.bin is already the complete bootable image; + # wrap it in a full-flash merged image at the 0x1000 offset. + python -m esptool --chip "$chip" merge-bin \ + --output firmware/"$board_clean_for_path"-"$role"-"$COMMIT_HASH"-merged.bin \ + --flash-mode dio --flash-freq 40m --flash-size 4MB \ + 0x1000 build/zephyr/zephyr.bin + cp build/zephyr/zephyr.bin firmware/"$board_clean_for_path"-"$role"-"$COMMIT_HASH".bin + continue + fi + if [[ $2 == "companions" ]]; then # build ESP32 companions (production is the default) echo "Now building $board companion" diff --git a/zephcore/CMakeLists.txt b/zephcore/CMakeLists.txt index 9e426f5..5ca90dc 100644 --- a/zephcore/CMakeLists.txt +++ b/zephcore/CMakeLists.txt @@ -323,7 +323,13 @@ endif() # Enables WiFi AP + HTTP server + MCUboot image management. # Requires --sysbuild to build MCUboot alongside the app. # Non-ESP32 builds and companion builds are unaffected. -if(ZEPHCORE_PLATFORM_CONF MATCHES "esp32_common" AND EXTRA_CONF_FILE MATCHES "repeater") +# +# Excluded on the classic ESP32 (e.g. T-Beam, board qualifier ".../esp32/..."): +# its DRAM cannot fit the mesh stack alongside WiFi + MCUboot, so it uses the +# simple-boot path and a CLI-only repeater (no WiFi OTA). The ESP32-S3/C-series +# (esp32s3/esp32c*) keep WiFi OTA. +if(ZEPHCORE_PLATFORM_CONF MATCHES "esp32_common" AND EXTRA_CONF_FILE MATCHES "repeater" + AND NOT BOARD MATCHES "/esp32/") set(ZEPHCORE_WIFI_OTA_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/wifi_ota.conf") if(EXISTS ${ZEPHCORE_WIFI_OTA_CONF}) list(APPEND ZEPHCORE_CONF_FILES ${ZEPHCORE_WIFI_OTA_CONF}) diff --git a/zephcore/adapters/board/ZephyrBoard.cpp b/zephcore/adapters/board/ZephyrBoard.cpp index 5475ace..1436adc 100644 --- a/zephcore/adapters/board/ZephyrBoard.cpp +++ b/zephcore/adapters/board/ZephyrBoard.cpp @@ -66,6 +66,18 @@ static const struct device *vbat_enable_dev = NULL; #define VBAT_ADC_SAMPLES 8 #endif +/* Battery fuel gauge (AXP2101 PMU etc.) — preferred over the ADC divider when + * present. Reports battery voltage and state-of-charge over I2C, so boards with + * a PMU and no battery ADC (e.g. LilyGo T-Beam) still get battery telemetry. */ +#if DT_HAS_COMPAT_STATUS_OKAY(x_powers_axp2101_fuel_gauge) +#include +#define HAS_FUEL_GAUGE 1 +static const struct device *const fuel_gauge_dev = + DEVICE_DT_GET(DT_COMPAT_GET_ANY_STATUS_OKAY(x_powers_axp2101_fuel_gauge)); +#else +#define HAS_FUEL_GAUGE 0 +#endif + /* Initialize TX LED GPIO at boot */ #if HAS_TX_LED static int tx_led_init(void) @@ -82,7 +94,17 @@ namespace mesh { uint16_t ZephyrBoard::getBattMilliVolts() { -#if DT_NODE_EXISTS(DT_PATH(zephyr_user)) && \ +#if HAS_FUEL_GAUGE + if (device_is_ready(fuel_gauge_dev)) { + union fuel_gauge_prop_val val; + int ret = fuel_gauge_get_prop(fuel_gauge_dev, FUEL_GAUGE_VOLTAGE, &val); + if (ret == 0) { + return (uint16_t)(val.voltage / 1000); /* µV -> mV */ + } + LOG_WRN("Fuel gauge voltage read failed: %d", ret); + } + return 0; +#elif DT_NODE_EXISTS(DT_PATH(zephyr_user)) && \ DT_NODE_HAS_PROP(DT_PATH(zephyr_user), io_channels) if (!adc_is_ready_dt(&vbat_adc)) { LOG_ERR("ADC not ready"); @@ -149,6 +171,18 @@ uint16_t ZephyrBoard::getBattMilliVolts() uint8_t ZephyrBoard::getBattPercent() { +#if HAS_FUEL_GAUGE + if (device_is_ready(fuel_gauge_dev)) { + union fuel_gauge_prop_val val; + int ret = fuel_gauge_get_prop(fuel_gauge_dev, + FUEL_GAUGE_RELATIVE_STATE_OF_CHARGE, &val); + if (ret == 0) { + return val.relative_state_of_charge; + } + LOG_WRN("Fuel gauge SoC read failed: %d", ret); + } + /* Fall through to the voltage-curve estimate if the gauge read fails. */ +#endif return battery_curve_lookup(&battery_curve_default, getBattMilliVolts()); } diff --git a/zephcore/boards/esp32/ttgo_tbeam/board.conf b/zephcore/boards/esp32/ttgo_tbeam/board.conf new file mode 100644 index 0000000..2f640e6 --- /dev/null +++ b/zephcore/boards/esp32/ttgo_tbeam/board.conf @@ -0,0 +1,40 @@ +# LilyGo T-Beam v1.2 — ESP32 (PICO-D4) + SX1262 +# +# Hardware: +# ESP32-PICO-D4 (4MB flash) +# SX1262 on SPI3 (DIO2 RF switch, 1.8V TCXO via DIO3) +# SSD1306 128x64 OLED on I2C0 +# AXP2101 PMU — powers LoRa (ALDO2) and GPS (ALDO3) rails, battery fuel gauge +# NEO-6M / NEO-M8N GNSS on UART1 +# +# Upstream Zephyr board: lilygo/ttgo_tbeam — that DTS models the SX1276 radio +# variant; board.overlay overrides the radio to a native SX1262 on the same +# SPI3 wiring. The AXP2101 MFD + regulator + fuel-gauge drivers are stock +# Zephyr and auto-enable from the upstream DTS PMU nodes. +# +# Include order: prj.conf -> zephcore_common.conf -> esp32_common.conf -> board.conf + +# Board identification +CONFIG_ZEPHCORE_BOARD_NAME="LilyGo T-Beam" +CONFIG_BT_DIS_MODEL_NUMBER_STR="LilyGo T-Beam v1.2" + +# SX1262 — 22 dBm default TX power (matches Arduino LORA_TX_POWER) +CONFIG_ZEPHCORE_DEFAULT_TX_POWER_DBM=22 + +# Battery via the AXP2101 fuel-gauge subsystem (voltage + state-of-charge over +# I2C). CONFIG_REGULATOR (zephcore_common.conf) plus the AXP2101 MFD/regulator +# drivers default-enable from the devicetree PMU nodes and bring the LoRa/GPS +# rails up at boot from their regulator-boot-on/always-on properties. +CONFIG_FUEL_GAUGE=y + +# Flash mode: ESP32-PICO-D4 — use DIO, not QIO. PICO-D4 rev 1.0 hangs in the +# QIO-enable SPI sequence (RTCWDT bootloop), same as handled for ttgo_lora32. +CONFIG_ESPTOOLPY_FLASHMODE_QIO=n +CONFIG_ESPTOOLPY_FLASHMODE_DIO=y + +# Companion RAM budget — the classic ESP32 has a much smaller contiguous DRAM +# segment than the ESP32-S3 boards, and the default contact/queue arrays +# (350/256) overflow it. Use the Arduino MeshCore T-Beam companion sizes. +CONFIG_ZEPHCORE_MAX_CONTACTS=160 +CONFIG_ZEPHCORE_MAX_CHANNELS=8 +CONFIG_ZEPHCORE_OFFLINE_QUEUE_SIZE=128 diff --git a/zephcore/boards/esp32/ttgo_tbeam/board.overlay b/zephcore/boards/esp32/ttgo_tbeam/board.overlay new file mode 100644 index 0000000..bec7b47 --- /dev/null +++ b/zephcore/boards/esp32/ttgo_tbeam/board.overlay @@ -0,0 +1,119 @@ +/* + * SPDX-License-Identifier: MIT + * LilyGo T-Beam v1.2 (SX1262) — ZephCore overlay + * + * The upstream Zephyr DTS (lilygo/ttgo_tbeam) targets the SX1276 radio + * variant. The v1.2 board carries an SX1262 instead, on the same SPI3 + * wiring, so this overlay swaps the radio over to the native SX126x driver + * and adds the SX1262-specific control pins. + * + * Pin mapping (Arduino MeshCore variants/lilygo_tbeam_SX1262): + * LoRa SPI3: SCLK=5, MOSI=27, MISO=19, NSS=18 + * LoRa ctrl: RESET=23, BUSY=32 (gpio1 0), DIO1=33 (gpio1 1) + * DIO2 = antenna RF switch, DIO3 = 1.8V TCXO + * Display I2C0: SDA=21, SCL=22, SSD1306 @ 0x3C [base DTS] + * PMU I2C0: AXP2101 @ 0x34 [base DTS] + * GPS UART1: TX=12, RX=34, NEO-6M/M8N [base DTS] + * + * Power (AXP2101, brought up by the stock Zephyr regulator driver from the + * base DTS regulator-boot-on/always-on properties): + * DCDC1 = ESP32 + OLED (always-on) + * ALDO2 = LoRa -> raised to 3.3V below (upstream 2.9-3.0V) + * ALDO3 = GPS -> raised to 3.3V below (upstream 2.7-2.8V) + * 3.3V matches the Arduino/Meshtastic reference firmware for this board. + */ + +#include + +/* + * BT HCI controller. The upstream ttgo_tbeam DTS does not select the ESP32 BT + * HCI controller, so forcing CONFIG_BT=y would otherwise leave the Espressif + * controller/PHY Kconfig half-configured (BT_ESP32 stays off). Enabling the HCI + * node selects BT_ESP32, which in turn pulls in the shared PHY. + * + * WiFi is intentionally left disabled: the classic ESP32 cannot fit the mesh + * stack alongside both the WiFi and BLE controllers in DRAM, and the companion + * transport is BLE. (A WiFi-observer build would need its own RAM-trimmed conf.) + */ +/ { + chosen { + zephyr,bt-hci = &esp32_bt_hci; + }; + + aliases { + /* GPIO4 LED blinks on LoRa TX, matching Arduino P_LORA_TX_LED=4 + * and Meshtastic LED_POWER=4. */ + lora-tx-led = &red_led; + }; +}; + +&esp32_bt_hci { + status = "okay"; +}; + +/* + * GPIO4 LED is active-LOW on the T-Beam (Meshtastic LED_STATE_ON=0; Arduino + * drives the pin LOW to light it). The upstream DTS marks it active-high, so + * correct the polarity here — otherwise the TX LED logic is inverted. + */ +&red_led { + gpios = <&gpio0 4 GPIO_ACTIVE_LOW>; +}; + +/* Swap the upstream SX1276 node to a native SX1262. */ +&lora0 { + compatible = "semtech,sx1262"; + spi-max-frequency = <8000000>; + + /delete-property/ dio-gpios; + /delete-property/ power-amplifier-output; + + /* RESET=GPIO23 inherited from base DTS; NSS=GPIO18 via spi3 cs-gpios. */ + busy-gpios = <&gpio1 0 GPIO_ACTIVE_HIGH>; /* BUSY = GPIO32 */ + dio1-gpios = <&gpio1 1 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; /* DIO1 = GPIO33 */ + + /* DIO2 drives the antenna TX/RX switch. */ + dio2-tx-enable; + + /* 1.8V TCXO via DIO3 (matches Arduino SX126X_DIO3_TCXO_VOLTAGE=1.8). */ + dio3-tcxo-voltage = ; + tcxo-power-startup-delay-ms = <5>; + + rx-boosted; +}; + +/* LoRa rail (ALDO2): upstream 2.9-3.0V; reference firmware runs it at 3.3V. */ +&vdd_lora { + regulator-min-microvolt = <3300000>; + regulator-max-microvolt = <3300000>; +}; + +/* GPS rail (ALDO3): upstream 2.7-2.8V; reference firmware runs it at 3.3V. */ +&vdd_gnss { + regulator-min-microvolt = <3300000>; + regulator-max-microvolt = <3300000>; +}; + +/* I2C sensors — auto-detected at runtime (OLED + AXP2101 already in base DTS). */ +&i2c0 { + #include "../../common/sensors-i2c.dtsi" +}; + +/* + * Flash partitions — repurpose the 192KB storage_partition (0x3B0000) as + * LittleFS, same as ttgo_lora32. The 4MB AMP layout keeps its MCUboot slots. + */ +/delete-node/ &storage_partition; + +&flash0 { + partitions { + /* LittleFS 192KB for DataStore (identity, prefs, creds) */ + lfs_partition: partition@3b0000 { + label = "lfs"; + reg = <0x3B0000 0x30000>; + }; + }; +}; + +/* LittleFS auto-mount — standard /lfs mount point */ +#include "../../common/filesystem.dtsi" diff --git a/zephcore/boards/supported_boards.md b/zephcore/boards/supported_boards.md index 8ff4cac..9349427 100644 --- a/zephcore/boards/supported_boards.md +++ b/zephcore/boards/supported_boards.md @@ -37,11 +37,17 @@ station_g2/esp32s3/procpu heltec_wifi_lora32_v3/esp32s3/procpu heltec_wifi_lora32_v4/esp32s3/procpu heltec_wifi_lora32_v43/esp32s3/procpu +ttgo_tbeam/esp32/procpu ``` > ESP32 boards require `west blobs fetch hal_espressif` before first build. > > Heltec V3 console/shell use `uart0` (UART serial) in ZephCore. +> +> **LilyGo T-Beam** (`ttgo_tbeam/esp32/procpu`): classic ESP32 (PICO-D4) with +> SX1262, AXP2101 PMU, and GNSS. Use this for the **v1.2 SX1262** variant — the +> upstream Zephyr DTS models the SX1276 radio, which ZephCore overrides to +> SX1262 in `board.overlay`. Console/CLI are on `uart0` (onboard USB-UART). ## MG24 (Silicon Labs)