Move T-Beam-BPF work from #9703 at vidplace7/t-beam-bpf

Initial work to add T-Beam BPF (144-148 Mhz LoRa)
This commit is contained in:
Austin
2026-05-26 19:02:30 -04:00
committed by GitHub
11 changed files with 266 additions and 2 deletions

39
boards/t-beam-bpf.json Normal file
View File

@@ -0,0 +1,39 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"memory_type": "qio_opi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DLILYGO_TBEAM_BPF",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "opi",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "t-beam-bpf"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino"],
"name": "LilyGo TBeam-BPF",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"require_upload_port": true,
"speed": 921600
},
"url": "http://www.lilygo.cn/",
"vendor": "LilyGo"
}

View File

@@ -1362,6 +1362,22 @@ bool Power::axpChipInit()
PMU->disablePowerOutput(XPOWERS_DLDO1); // Invalid power channel, it does not exist
PMU->disablePowerOutput(XPOWERS_DLDO2); // Invalid power channel, it does not exist
PMU->disablePowerOutput(XPOWERS_VBACKUP);
} else if (HW_VENDOR == meshtastic_HardwareModel_TBEAM_BPF) {
// T-Beam BPF rail map (per schematic LilyGo_TBeam_BPF r2025-05-08):
// DCDC1 -> ESP32 + OLED 3V3 (always on, protected)
// ALDO2 -> MicroSD 3V3 (OFF at reset, must enable)
// ALDO4 -> L76K GNSS 3V3 (OFF at reset, must enable)
// ALDO1/3, BLDO1/2, DLDO1 -> user headers / unused at boot, leave at reset defaults.
// LoRa power is outside the PMU (external P-MOSFET switched by RF95_POWER_EN / IO16).
PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300);
PMU->enablePowerOutput(XPOWERS_ALDO4);
PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300);
PMU->enablePowerOutput(XPOWERS_ALDO2);
// Make sure nothing's driving into an unused rail
PMU->disablePowerOutput(XPOWERS_DCDC5);
PMU->disablePowerOutput(XPOWERS_DLDO1);
}
// disable all axp chip interrupt

View File

@@ -176,6 +176,15 @@ void menuHandler::OnboardMessage()
void menuHandler::LoraRegionPicker(uint32_t duration)
{
#ifdef HAS_HAM_2M_ONLY
// Hardware is restricted to the amateur 2m band — offer only the two 2m regions
// so the user cannot pick a sub-GHz region the RF path cannot emit or receive.
static const LoraRegionOption regionOptions[] = {
{"Back", OptionsAction::Back},
{"ITU1_2M (144-146)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M},
{"ITU23_2M (144-148)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M},
};
#else
static const LoraRegionOption regionOptions[] = {
{"Back", OptionsAction::Back},
{"US", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_US},
@@ -208,6 +217,7 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
{"BR_902", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_BR_902},
};
#endif
constexpr size_t regionCount = sizeof(regionOptions) / sizeof(regionOptions[0]);
static std::array<const char *, regionCount> regionLabels{};

View File

@@ -46,7 +46,7 @@ extern const RegionProfile PROFILE_EU868;
extern const RegionProfile PROFILE_UNDEF;
extern const RegionProfile PROFILE_LITE;
extern const RegionProfile PROFILE_NARROW;
// extern const RegionProfile PROFILE_HAM;
extern const RegionProfile PROFILE_HAM;
// Map from old region names to new region enums
struct RegionInfo {

View File

@@ -1520,6 +1520,13 @@ void NodeDB::installDefaultDeviceState()
memcpy(owner.macaddr, ourMacAddr, sizeof(owner.macaddr));
owner.has_is_unmessagable = true;
owner.is_unmessagable = false;
#ifdef HAS_HAM_2M_ONLY
// Ham-band-only hardware defaults to licensed operation. The user can still flip this off later
// (e.g. a commercial operator on an adjacent allocation who wants to keep encryption on) — we
// only set the default here, not on every boot.
owner.is_licensed = true;
#endif
}
// We reserve a few nodenums for future use

View File

@@ -113,6 +113,11 @@ void RF95Interface::setTransmitEnable(bool txon)
/// \return true if initialisation succeeded.
bool RF95Interface::init()
{
#ifdef RF95_POWER_EN
pinMode(RF95_POWER_EN, OUTPUT);
digitalWrite(RF95_POWER_EN, HIGH);
#endif
RadioLibInterface::init();
#if defined(RADIOMASTER_900_BANDIT_NANO) || defined(RADIOMASTER_900_BANDIT)
@@ -335,6 +340,10 @@ bool RF95Interface::sleep()
setStandby(); // First cancel any active receiving/sending
lora->sleep();
#ifdef RF95_POWER_EN
digitalWrite(RF95_POWER_EN, LOW);
#endif
#ifdef RF95_FAN_EN
digitalWrite(RF95_FAN_EN, 0);
#endif

View File

@@ -56,6 +56,7 @@ const RegionProfile PROFILE_EU868 = {PRESETS_EU_868, 0, 0, false, false, 0, 1, 1
const RegionProfile PROFILE_UNDEF = {PRESETS_UNDEF, 0, 0, true, false, 0, 1, 1, 0};
const RegionProfile PROFILE_LITE = {PRESETS_LITE, 0.4, 0.0375f, false, false, 0, 10, 10, 0};
const RegionProfile PROFILE_NARROW = {PRESETS_NARROW, 0, 0.0104f, true, false, 0, 1, 1, 1};
const RegionProfile PROFILE_HAM = {PRESETS_NARROW, 0, 0, false, true, 0, 1, 1, 17};
#define RDEF(name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, wide_lora, profile_ptr, default_preset) \
{ \
@@ -225,6 +226,19 @@ const RegionInfo regions[] = {
*/
RDEF(BR_902, 902.0f, 907.5f, 100, 30, false, false, PROFILE_STD, PRESET(LONG_FAST)),
/*
ITU Region 1 (Europe, Africa, Middle East, former USSR) amateur 2m allocation: 144.000 - 146.000 MHz.
Power limit is the regulatory ceiling (1 W / 30 dBm) — individual hardware will cap below this
via its own PA curve; the field here is just the legal upper bound.
*/
RDEF(ITU1_2M, 144.0f, 146.0f, 100, 30, false, false, PROFILE_HAM, PRESET(NARROW_FAST)),
/*
ITU Region 2 (Americas) and Region 3 (Asia/Pacific) amateur 2m allocation: 144.000 - 148.000 MHz.
Typical admin rules (e.g. US FCC Part 97) allow well above 30 dBm for licensed operators.
*/
RDEF(ITU23_2M, 144.0f, 148.0f, 100, 30, false, false, PROFILE_HAM, PRESET(NARROW_FAST)),
/*
2.4 GHZ WLAN Band equivalent. Only for SX128x chips.
*/
@@ -535,6 +549,23 @@ std::unique_ptr<RadioInterface> initLoRa()
rebootAtMsec = millis() + 5000;
}
}
// Hardware/region crosscheck for the amateur 2m band: ham-only boards must run a 2m region,
// and boards without 2m support must not run one. In either mismatch, drop to UNSET so the
// first-start picker runs and the user re-selects a legal region for the hardware.
const bool is2mRegion = config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M ||
config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M;
#ifdef HAS_HAM_2M_ONLY
const bool mismatch = !is2mRegion && config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET;
#else
const bool mismatch = is2mRegion;
#endif
if (mismatch) {
LOG_WARN("Saved region incompatible with this hardware's RF path. Revert to unset");
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
nodeDB->saveToDisk(SEGMENT_CONFIG);
}
return rIf;
}
@@ -834,7 +865,14 @@ bool RadioInterface::validateConfigRegion(const meshtastic_Config_LoRaConfig &lo
const RegionInfo *newRegion = getRegion(loraConfig.region);
// If you are not licensed, you can't use ham regions.
if (newRegion->profile->licensedOnly && !devicestate.owner.is_licensed) {
// Exception: on hardware that can *only* operate on a ham band (e.g. T-Beam BPF), the user has
// no other region to choose, so allow unlicensed selection — a commercial operator on adjacent
// frequencies can still use the band plan and keep encryption enabled.
bool allowUnlicensedHam = false;
#ifdef HAS_HAM_2M_ONLY
allowUnlicensedHam = true;
#endif
if (newRegion->profile->licensedOnly && !devicestate.owner.is_licensed && !allowUnlicensedHam) {
char err_string[160];
snprintf(err_string, sizeof(err_string), "Region %s requires licensed mode", newRegion->name);
LOG_ERROR("%s", err_string);
@@ -843,6 +881,34 @@ bool RadioInterface::validateConfigRegion(const meshtastic_Config_LoRaConfig &lo
return false;
}
const bool is2mRegion = loraConfig.region == meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M ||
loraConfig.region == meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M;
#ifdef HAS_HAM_2M_ONLY
// This hardware's front-end / band-pass filter only passes 144-148 MHz. Any other region
// selection would key the radio on a frequency the RF path cannot emit or receive.
if (!is2mRegion && loraConfig.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
char err_string[160];
snprintf(err_string, sizeof(err_string), "Region %s not supported: this hardware is 2m-only", newRegion->name);
LOG_ERROR("%s", err_string);
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
sendErrorNotification(err_string);
return false;
}
#else
// Conversely, the 2m ham regions are illegal RF output for hardware not designed for that band
// (e.g. selecting ITU23_2M on a 915 MHz node would transmit at ~3x the expected frequency with
// an untuned antenna and filter). Refuse the selection entirely.
if (is2mRegion) {
char err_string[160];
snprintf(err_string, sizeof(err_string), "Region %s requires 2m-band hardware", newRegion->name);
LOG_ERROR("%s", err_string);
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
sendErrorNotification(err_string);
return false;
}
#endif
return true;
}

View File

@@ -198,6 +198,8 @@
#define HW_VENDOR meshtastic_HardwareModel_T_DECK_PRO
#elif defined(T_BEAM_1W)
#define HW_VENDOR meshtastic_HardwareModel_TBEAM_1_WATT
#elif defined(T_BEAM_BPF)
#define HW_VENDOR meshtastic_HardwareModel_TBEAM_BPF
#elif defined(T_LORA_PAGER)
#define HW_VENDOR meshtastic_HardwareModel_T_LORA_PAGER
#elif defined(HELTEC_V4)

View File

@@ -0,0 +1,26 @@
#ifndef Pins_Arduino_h
#define Pins_Arduino_h
#include <stdint.h>
#define USB_VID 0x303a
#define USB_PID 0x1001
// UART1 (qwiic)
static const uint8_t TX = 43;
static const uint8_t RX = 44;
// I2C for OLED and sensors
static const uint8_t SDA = 8;
static const uint8_t SCL = 9;
// Default SPI mapped to Radio/SD
static const uint8_t SS = 1; // LoRa CS
static const uint8_t MOSI = 11;
static const uint8_t MISO = 13;
static const uint8_t SCK = 12;
// SD Card CS
#define SDCARD_CS 10
#endif /* Pins_Arduino_h */

View File

@@ -0,0 +1,23 @@
; LilyGo T-Beam-BPF (144-148Mhz)
[env:t-beam-bpf]
custom_meshtastic_hw_model = 124
custom_meshtastic_hw_model_slug = TBEAM_BPF
custom_meshtastic_architecture = esp32s3
custom_meshtastic_actively_supported = true
custom_meshtastic_support_level = 3
custom_meshtastic_display_name = LILYGO T-Beam BPF
custom_meshtastic_images = tbeam-1w.svg
custom_meshtastic_tags = LilyGo
extends = esp32s3_base
board = t-beam-bpf
board_build.partitions = default_16MB.csv
board_check = true
lib_deps =
${esp32s3_base.lib_deps}
build_flags =
${esp32s3_base.build_flags}
-I variants/esp32s3/t-beam-bpf
-D T_BEAM_BPF

View File

@@ -0,0 +1,66 @@
// LilyGo T-Beam-BPF variant.h
// Configuration based on LilyGO utilities.h and RF documentation
// Hardware is restricted to the amateur 2m band (144-148 MHz).
#define HAS_HAM_2M_ONLY 1
// I2C for OLED display (SH1106 at 0x3C)
#define I2C_SDA 8
#define I2C_SCL 9
// GPS - Quectel L76K. Per schematic sheet 7:
#define GPS_RX_PIN 5
#define GPS_TX_PIN 6
#define GPS_1PPS_PIN 7
#define HAS_GPS 1
#define GPS_BAUDRATE 9600
// Buttons
#define BUTTON_PIN 0 // BUTTON 1
#define ALT_BUTTON_PIN 3 // BUTTON 2
// SPI (shared by LoRa and SD)
#define SPI_MOSI 11
#define SPI_SCK 12
#define SPI_MISO 13
#define SPI_CS 10
// SD Card
#define HAS_SDCARD
#define SDCARD_USE_SPI1
// #define SDCARD_CS SPI_CS (already defined in pins_arduino.h)
// LoRa Radio - SX1278 144-148Mhz
#define USE_RF95
#define LORA_SCK SPI_SCK
#define LORA_MISO SPI_MISO
#define LORA_MOSI SPI_MOSI
#define LORA_CS 1
#define LORA_RESET 18
#define LORA_IRQ 14
#define LORA_DIO0 LORA_IRQ
#define LORA_DIO1 21
#define RF95_RXEN 39 // LNA enable - HIGH during RX
// CRITICAL: Radio power enable - MUST be HIGH before lora.begin()!
// GPIO 16 powers the SX1278 via LDO
#define RF95_POWER_EN 16
// "+27dBm"? PA! Investigate further (poorly documented)
// LilyGo Docs specify SX1278 power must be capped at 10
#define RF95_MAX_POWER 10
// TODO map PA output curve
// #define TX_GAIN_LORA 17
// Display - SH1106 OLED (128x64)
#define USE_SH1106
#define OLED_WIDTH 128
#define OLED_HEIGHT 64
// 32768 Hz crystal present
#define HAS_32768HZ 1
// PMU
#define HAS_AXP2101
// #define PMU_IRQ 4 // Leave disabled for now