From a413a361ddcf134d76588d9dd8ca220a103e44f1 Mon Sep 17 00:00:00 2001 From: MUSTARDTIGERFPV Date: Tue, 8 Sep 2026 14:06:04 -0700 Subject: [PATCH] Add GEPRC LINKFLOW 900M TX variant An ExpressLRS 900 MHz transmitter module: ESP32-D0WDQ6, 4 MB flash, SX1276 driving an external PA through a DAC gain input on GPIO26. Pin values come from the ExpressLRS hardware layout for this target (Unified_ESP32_900_TX). The board brings out only RX_EN, so RadioLib's single-pin RF switch handling covers it. Note GPIO12 is both RX_EN and the MTDI flash-voltage strapping pin, so it must stay low through reset. Verified on hardware: boots, radio initialises, TX power changes take effect and are rejected outside the configured range, and the node repeats flood traffic. Uses dual_ota_1536k.csv, and 115200 for upload because the CH340 on this board corrupts transfers above 230400. --- variants/geprc_linkflow_900/platformio.ini | 136 +++++++++++++++++++++ variants/geprc_linkflow_900/target.cpp | 51 ++++++++ variants/geprc_linkflow_900/target.h | 17 +++ 3 files changed, 204 insertions(+) create mode 100644 variants/geprc_linkflow_900/platformio.ini create mode 100644 variants/geprc_linkflow_900/target.cpp create mode 100644 variants/geprc_linkflow_900/target.h diff --git a/variants/geprc_linkflow_900/platformio.ini b/variants/geprc_linkflow_900/platformio.ini new file mode 100644 index 000000000..400befa4e --- /dev/null +++ b/variants/geprc_linkflow_900/platformio.ini @@ -0,0 +1,136 @@ +; GEPRC LINKFLOW 900M TX - an ExpressLRS 900 MHz transmitter module. +; +; ESP32-D0WDQ6, 4 MB flash, SX1276 driving an external PA whose gain is set +; from a DAC pin. The board itself is a plain ExpressLRS TX module, so the +; two classes it uses are shared: +; +; helpers/esp32/ELRSTxBoard.h fan + backpack + no battery +; helpers/radiolib/DacPaSX1276Wrapper.h DAC-controlled amplifier gain +; +; Every pin below is the value from the ExpressLRS hardware layout for this +; module - ExpressLRS target "GEPRC LINKFLOW 900M TX", firmware +; Unified_ESP32_900_TX - read back out of this module's own flash and +; transcribed here. Two entries in that layout are deliberately unused: +; serial_rx/serial_tx (both GPIO13, the inverted half-duplex CRSF link to the +; handset) and the backpack UART on GPIO16/17. + +[GEPRC_Linkflow_900] +extends = esp32_base +board = esp32dev +board_build.partitions = variants/dual_ota_1536k.csv +build_flags = + ${esp32_base.build_flags} + -I variants/geprc_linkflow_900 + -D GEPRC_LINKFLOW_900 + -D MANUFACTURER_NAME='"GEPRC LINKFLOW 900M TX"' + + ; ---- radio ---- + -D RADIO_CLASS=CustomSX1276 + -D WRAPPER_CLASS=DacPaSX1276Wrapper + -D P_LORA_SCLK=18 + -D P_LORA_MISO=19 + -D P_LORA_MOSI=23 + -D P_LORA_NSS=5 + -D P_LORA_RESET=14 + -D P_LORA_DIO_0=4 + -D P_LORA_DIO_1=21 + -D SX127X_CURRENT_LIMIT=120 + + ; ---- RF switch ---- + ; The board only brings out RX_EN (hardware.json "power_rxen"); there is no + ; TX_EN pin. Leaving SX127X_TXEN undefined makes RadioLib drive this pin + ; HIGH for RX and LOW for TX, which is what a single-pin switch wants. + ; NOTE: GPIO12 is also the MTDI flash-voltage strapping pin, so it has to + ; stay low through reset. Do not add a pull-up here. + -D SX127X_RXEN=12 + + ; ---- frequency ---- + ; This module's own ExpressLRS options blob had "domain": 1, which is + ; FCC915 in FHSS.cpp (903.5 - 926.9 MHz). So it is a US 915 MHz unit, and + ; the 869.618 inherited from arduino_base is wrong for it. build_unflags + ; below drops the inherited one so this is the only definition. + -D LORA_FREQ=910.525 + + ; ---- TX power ---- + ; ExpressLRS layout "power_apc2". The dBm-to-DAC table lives in target.cpp. + -D PIN_PA_APC2=26 + + ; These numbers are dBm at the antenna, i.e. after the amplifier. The + ; SX1276 itself sits at a fixed +2 dBm; see DacPaSX1276Wrapper.h. + ; 17 dBm (50 mW) is the stock ExpressLRS "power_default" for this board. + ; + ; LORA_TX_POWER must stay within 2..17. CustomSX1276::std_init() passes it + ; straight to RadioLib's begin(), and PA_BOOST rejects anything outside that + ; range, which would fail radio_init(). It only sets the startup level; + ; MIN/MAX below are what bound the range at runtime, and those go through + ; the DAC, so they are free to be higher. + ; + ; MAX_LORA_TX_POWER is defined once and used twice: the CLI validates + ; against it, and target.cpp hands the same value to the wrapper, which + ; enforces it in applyCachedTxPower(). The wrapper also refuses anything + ; outside its own table regardless, so a board that omits these flags is + ; still bounded by what the amplifier table actually describes. + ; + ; The amplifier will do 33 dBm (2 W), but ExpressLRS only ever asks for that + ; in short packet bursts. MeshCore transmits for far longer, so the ceiling + ; here is one table entry lower. Raise it to 33 only with cooling you + ; trust, and check your local duty-cycle and EIRP limits first. + -D LORA_TX_POWER=17 + -D MIN_LORA_TX_POWER=10 + -D MAX_LORA_TX_POWER=30 + + ; ---- board ---- + -D PIN_FAN_EN=32 ; hardware.json "misc_fan_en" + -D PIN_BACKPACK_EN=25 ; hardware.json "backpack_en" + -D PIN_BACKPACK_BOOT=15 ; hardware.json "backpack_boot" + + ; WS2812 on GPIO27. hardware.json says GRB ordering, which is what + ; neopixelWrite() emits. + -D P_LORA_TX_NEOPIXEL_LED=27 + +; The CH340 on this board is not reliable above 230400 for flash access; +; 460800 and 921600 both corrupt mid-transfer. +upload_speed = 115200 +build_unflags = + -D LORA_FREQ=869.618 + ; esp32_base turns LoRa OTA (mOTA) on for every ESP32 target. Its context + ; is ~15.6 KiB of .bss - two 4 KiB Merkle proof scratch buffers and two + ; 2 KiB block buffers - resident whether or not the node ever serves an + ; image. Classic ESP32 cannot afford it. Dropping ENABLE_OTA compiles the + ; whole engine out (MyMesh.h only includes OtaContext.h when it is set). + -D ENABLE_OTA=1 + -D OTA_FLASH_STORE=1 + -D OTA_FOLDER_SERIAL +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/geprc_linkflow_900> + - +lib_deps = + ${esp32_base.lib_deps} + +[env:GEPRC_Linkflow_900_repeater] +extends = GEPRC_Linkflow_900 +build_flags = + ${GEPRC_Linkflow_900.build_flags} + -D ADVERT_NAME='"Linkflow Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + ; --- classic ESP32 DRAM strip --------------------------------------- + ; These are all fixed .bss tables, sized for a comfortable target and + ; allocated whether or not a single rule is ever configured. Stock cost on + ; this build was ~18.5 KiB of flood tables plus 9.6 KiB of client ACL. + ; The node still repeats; it just has almost no room for forwarding rules, + ; scope rewrites, group moderation, or logged-in clients. + -D MAX_CLIENTS=2 ; was 32, at ~300 B/entry + -D MESH_ENABLE_FLOOD_RULE_ENGINE=0 ; drops the forward-rule engine + -D FLOOD_PACKET_FILTER_SLOTS=1 ; was 63, at 200 B/entry + -D FLOOD_CHANNEL_SCOPE_SLOTS=1 ; ESP32 default is 255; also drives + ; DIRECT_SCOPE and SCOPE_REQUIRE + -D MESH_ENABLE_FLOOD_GROUP_MODERATION=0 ; drops text moderation + -D FLOOD_GROUP_MODERATION_SLOTS=1 ; was 16, at 124 B/entry +build_src_filter = ${GEPRC_Linkflow_900.build_src_filter} + +<../examples/simple_repeater> +lib_deps = + ${GEPRC_Linkflow_900.lib_deps} + ${esp32_ota.lib_deps} diff --git a/variants/geprc_linkflow_900/target.cpp b/variants/geprc_linkflow_900/target.cpp new file mode 100644 index 000000000..3522b167f --- /dev/null +++ b/variants/geprc_linkflow_900/target.cpp @@ -0,0 +1,51 @@ +#include +#include "target.h" + +ELRSTxBoard board; + +static SPIClass spi; +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_0, P_LORA_RESET, P_LORA_DIO_1, spi); + +// The "power_values" array from the ExpressLRS hardware layout for this +// module (ExpressLRS target "GEPRC LINKFLOW 900M TX", firmware +// Unified_ESP32_900_TX), paired with the PowerLevels_e step each index maps +// to. power_min is 0 on this board, so index 0 is PWR_10mW. +static const DacPaLevel POWER_LEVELS[] = { + { 10, 30 }, // 10 mW + { 14, 40 }, // 25 mW + { 17, 50 }, // 50 mW <- ExpressLRS "power_default" + { 20, 60 }, // 100 mW + { 24, 80 }, // 250 mW + { 27, 90 }, // 500 mW + { 30, 130 }, // 1000 mW + { 33, 225 }, // 2000 mW <- above MAX_LORA_TX_POWER, see platformio.ini +}; + +// MAX_LORA_TX_POWER is the single definition of this board's ceiling: the CLI +// validates against it, and the wrapper enforces it independently so a request +// that gets past the CLI cannot saturate the amplifier at its top table entry. +WRAPPER_CLASS radio_driver(radio, board, PIN_PA_APC2, + POWER_LEVELS, + sizeof(POWER_LEVELS) / sizeof(POWER_LEVELS[0]), + MAX_LORA_TX_POWER); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +SensorManager sensors; + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + + if (!radio.std_init(&spi)) return false; + + // std_init() left the SX1276 at LORA_TX_POWER, which on this board is a + // post-amplifier number. Hand power control over to the amplifier instead. + radio_driver.beginPowerControl(LORA_TX_POWER); + return true; +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} diff --git a/variants/geprc_linkflow_900/target.h b/variants/geprc_linkflow_900/target.h new file mode 100644 index 000000000..6a6022163 --- /dev/null +++ b/variants/geprc_linkflow_900/target.h @@ -0,0 +1,17 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include + +extern ELRSTxBoard board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern SensorManager sensors; + +bool radio_init(); +mesh::LocalIdentity radio_new_identity();