From 4bbcb5270a34b8b804afcba1f71abaec9712a9df Mon Sep 17 00:00:00 2001 From: MUSTARDTIGERFPV Date: Tue, 8 Sep 2026 14:25:37 -0700 Subject: [PATCH] Support boards that move the radio output alongside the PA gain Not every external-PA board parks its radio at a fixed drive level. Where the amplifier is driven somewhere other than its floor, the radio's own output steps with the requested power as well, and the board describes that with a second table paired to the first. ExpressLRS layouts call this "power_values2"; it is independent of dual-band operation, and appears on single-band modules such as the Radiomaster Bandit. An optional radio_dbm array supplies one radio output level per entry in the level table. When present the fixed drive level is unused, and each power change sets the radio before moving the amplifier so the PA is never asked to pass a level the radio has already exceeded. Values go to the SX1276 on the PA_BOOST path. A board wired to RFO_HF would need RadioLib's useRfo argument plumbed through, which this does not do. --- src/helpers/radiolib/DacPaSX1276Wrapper.h | 56 ++++++++++++++++++----- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/src/helpers/radiolib/DacPaSX1276Wrapper.h b/src/helpers/radiolib/DacPaSX1276Wrapper.h index bf8cea06..95832979 100644 --- a/src/helpers/radiolib/DacPaSX1276Wrapper.h +++ b/src/helpers/radiolib/DacPaSX1276Wrapper.h @@ -23,10 +23,28 @@ // static const DacPaLevel LEVELS[] = { {10, 30}, {17, 50}, {30, 130} }; // DacPaSX1276Wrapper radio_driver(radio, board, PIN_APC, LEVELS, 3); // -// Entries must be ordered by ascending dBm. The default drive level of +2 dBm -// is the SX1276's floor on PA_BOOST (RegPaConfig OutputPower = 0), which suits -// an amplifier expecting a small constant input; pass drive_dbm to override it -// for a PA that wants more. +// Entries must be ordered by ascending dBm. Nothing is assumed about the +// control codes themselves, so a board whose gain input runs backwards (a +// falling code for rising power) needs no special handling. +// +// The default drive level of +2 dBm is the SX1276's floor on PA_BOOST +// (RegPaConfig OutputPower = 0), which suits an amplifier expecting a small +// constant input; pass drive_dbm to override it for a PA that wants more. +// +// Not every board holds the radio still. Where the amplifier is driven +// somewhere other than its floor, the radio's own output moves with each step +// as well, and the board supplies a second table of radio output levels +// alongside the first. Pass it as radio_dbm and the fixed drive level is not +// used at all: +// +// static const DacPaLevel LEVELS[] = { {20, 165}, {24, 155}, {27, 142} }; +// static const int8_t RADIO_DBM[] = { 2, 6, 9 }; +// DacPaSX1276Wrapper radio_driver(radio, board, PIN_APC, LEVELS, 3, +// DAC_PA_TABLE_MAX, RADIO_DBM); +// +// radio_dbm must have one entry per level. Its values go to the SX1276 on the +// PA_BOOST path; a board wired to RFO_HF instead would need RadioLib's useRfo +// argument plumbed through, which this class does not do yet. // // The gain control is written through writeGainControl(), which uses the // ESP32's DAC by default. A board driving its PA from a PWM pin or an external @@ -69,10 +87,11 @@ public: uint8_t ctrl_pin, const DacPaLevel* levels, uint8_t num_levels, int8_t max_dbm = DAC_PA_TABLE_MAX, + const int8_t* radio_dbm = NULL, int8_t drive_dbm = DAC_PA_DEFAULT_DRIVE_DBM) : CustomSX1276Wrapper(radio, board), _ctrl_pin(ctrl_pin), _levels(levels), _num_levels(num_levels), - _drive_dbm(drive_dbm) { + _radio_dbm(radio_dbm), _drive_dbm(drive_dbm) { _min_dbm = levels[0].dbm; _max_dbm = levels[num_levels - 1].dbm; if (max_dbm < _max_dbm) _max_dbm = max_dbm; @@ -87,8 +106,13 @@ public: // Park the radio at its drive level and set the amplifier to dbm. Call once, // after the radio has started. A startup level outside the supported range // is brought into it rather than left unset. + // + // Boards carrying a radio_dbm table have no fixed drive level to park at; + // applyCachedTxPower() sets the radio for each step instead. void beginPowerControl(int8_t dbm) { - ((CustomSX1276 *)_radio)->setOutputPower(_drive_dbm); + if (_radio_dbm == NULL) { + ((CustomSX1276 *)_radio)->setOutputPower(_drive_dbm); + } if (dbm < _min_dbm) dbm = _min_dbm; if (dbm > _max_dbm) dbm = _max_dbm; applyCachedTxPower(dbm); @@ -111,23 +135,33 @@ protected: if (dbm < _min_dbm || dbm > _max_dbm) { return RADIOLIB_ERR_INVALID_OUTPUT_POWER; } - writeGainControl(codeForDbm(dbm)); + const uint8_t idx = indexForDbm(dbm); + if (_radio_dbm != NULL) { + // Move the radio first: on the way up this is the smaller of the two + // steps, so the amplifier is never asked to pass a level the radio has + // already exceeded. + const int16_t status = + ((CustomSX1276 *)_radio)->setOutputPower(_radio_dbm[idx]); + if (status != RADIOLIB_ERR_NONE) return status; + } + writeGainControl(_levels[idx].dac); return RADIOLIB_ERR_NONE; } private: // Highest level that does not exceed dbm. Callers have already range-checked. - uint8_t codeForDbm(int8_t dbm) const { - uint8_t code = _levels[0].dac; + uint8_t indexForDbm(int8_t dbm) const { + uint8_t idx = 0; for (uint8_t i = 0; i < _num_levels; i++) { - if (_levels[i].dbm <= dbm) code = _levels[i].dac; + if (_levels[i].dbm <= dbm) idx = i; } - return code; + return idx; } uint8_t _ctrl_pin; const DacPaLevel* _levels; uint8_t _num_levels; + const int8_t* _radio_dbm; int8_t _drive_dbm; int8_t _min_dbm; int8_t _max_dbm;