Integrate W12 RF switching and LR2021 receive PRs

Adapt upstream MeshCore #3579 at 7457cff43c22697119e06ad7d192a37de0990403:
retain GC1109 LF standby bias, with checked cold controls before band supply
changes and shutdown. Preserve exclusive LF/HF rails and TX error cleanup.
Reset before boot rail changes and before SPI-dependent hard recovery.

Retain our stronger implementation of #3512 at
d208bfef94ab83280de30ba4a0675ff73b3ecef3. Add regressions for suspect FIFO
contents, failed RX_DONE acknowledgement, and fresh preamble timing.
W12 TX power defaults from #3319 were already included via 65aa1138a.

Validation: 79 targeted native tests; canonical W12 Full repeater and Full
Companion builds, capability checks, app-size and RAM checks passed.
Physical W12 RF envelope, current and LoRa management confirmation remain.

Upstream PRs:
https://github.com/meshcore-dev/MeshCore/pull/3579
https://github.com/meshcore-dev/MeshCore/pull/3512
https://github.com/meshcore-dev/MeshCore/pull/3319
This commit is contained in:
mikecarper committed 2026-10-25 15:46:18 -07:00
1 parent 73c5f572eb
commit ad34fd3a0d
15 files changed
+514 -137

No files matched your search

+2 -2
View File
@@ -129,10 +129,10 @@ jobs:
bash build.sh build-firmware Station_G3_ESP32_r2_repeater_observer_mqtt
--firmware-version v1.18.0.1-halo-keymind-cascade-dev
--radio-preset usa-cascade-fixed --profile cascade --require-ota --full-exact
- name: Compile W12 low-power FEM TX preparation in repeater and Full Companion
- name: Compile W12 band-aware RF switching in repeater and Full Companion
run: |
pio run -e meshnology_w12_repeater
bash build.sh build-firmware meshnology_w12_companion_radio_full --firmware-version v1.17.1.10-halo-keymind-cascade-dev --radio-preset usa-cascade-fixed --profile cascade --require-ota
bash build.sh build-firmware meshnology_w12_companion_radio_full --firmware-version v1.18.0.1-halo-keymind-cascade-dev --radio-preset usa-cascade-fixed --profile cascade --require-ota
- name: Show qualification build logs on failure
if: failure()
run: |
+9
View File
@@ -360,6 +360,15 @@ radio settings preserve the saved TX power and safely clamp only the active
band; returning to the saved settings restores its applicable power. A
permanent frequency change clamps the saved TX power to the new band's limit.
The W12 RF-switch update from [upstream #3579](https://github.com/meshcore-dev/MeshCore/pull/3579)
keeps the sub-GHz amplifier biased in standby. Before changing FEM supplies,
firmware lowers its control pins; high-band standby and shutdown keep the
unpowered sub-GHz amplifier's controls low. The existing LF/HF rail isolation
is retained. The [LR2021 receive fix in #3512](https://github.com/meshcore-dev/MeshCore/pull/3512)
is covered by checked FIFO reads, interrupt acknowledgement and automatic RX
rearming. Physical W12 packet-start power and current measurements are still
required to qualify the RF result.
The external SX1262, SX1268, LLCC68, LR1110, and LR2021 drivers also bound the
post-SetTx BUSY wait. A failed PA ramp therefore returns an error and can enter
the physical-reset recovery path instead of hanging the node indefinitely.
+29 -14
View File
@@ -370,19 +370,34 @@ inductor/capacitor network. The MCU and LoRa regulator choices are independent.
## Meshnology W12 transmit preparation
The W12 adopts the GC1109 TX preparation from
[upstream PR #3579](https://github.com/meshcore-dev/MeshCore/pull/3579), but keeps
the PA shut down in ordinary standby rather than permanently selecting TX mode.
Before sub-GHz packet or Morse CW transmission, it temporarily enables the LF
PA in standby, waits 1 ms, starts TX, then restores the normal standby mask.
The TX mask stays unchanged during that restoration. Failure paths restore all
affected DIOs and request radio recovery if the PA state cannot be confirmed.
The 2.4 GHz path and other LR2021 boards do not use this LF prearm policy.
The W12 uses the RF-switch table from
[upstream PR #3579](https://github.com/meshcore-dev/MeshCore/pull/3579), head
`7457cff43c22697119e06ad7d192a37de0990403`. Low-band standby keeps GC1109 CTX,
CPS and CSD high; LF receive keeps CTX low and CPS/CSD high. This replaces the earlier
1 ms temporary prearm as the fix for the weak opening of a transmitted packet.
The PR author reported a roughly 36 ms power step before the table change and
no idle-current increase in their power-saving setup. Those measurements are
upstream evidence, not measurements of this fork.
LF/HF FEM rail changes now settle for 1 ms instead of 100 ms. Packet-wake deep
sleep retains only the selected FEM rail; terminal shutdown retains both rails
off. This is separate from the W12's existing circuit-verified DC/DC setting.
The 1 ms pre-TX settle guard and TX failure recovery remain. With the warm
W12 table they do not rewrite the standby masks or remove its retained bias.
Other LR2021 tables can still opt into temporary prearming with full cleanup.
The upstream table needs one adaptation for this fork's exclusive FEM rails.
The [GC1109 datasheet](https://www.geochipinc.com/uploads/23940615_1756826539.pdf)
requires controls not to exceed VBAT. Before changing supplies, firmware
checks a cold standby with all control outputs low; after the selected supply
settles for 1 ms it restores warm standby only for LF. High-band standby and
2.4 GHz RX/TX keep the GC1109 controls low. A failed routing change is retried
on the next request, including one for the same band.
Native fault-injection tests cover the mask transitions and power retention;
RF envelope and battery-current measurements on a physical W12 are still needed
to validate the TX improvement and quantify power use.
Packet-wake deep sleep retains only the selected FEM rail. Terminal shutdown
checks cold controls after stopping the radio, then retains both rails off.
If that final SPI check fails, the transceiver is held in reset before its FEM
supplies are removed. Boot asserts reset before dropping retained FEM rails,
including after an MCU-only reset, and releases it after selecting the initial
supply. Hard recovery pulses reset before checked routing so a prior SPI fault
cannot prevent recovery. The W12's existing circuit-verified DC/DC setting is independent of this routing.
Native fault-injection tests cover the actual W12 table, temporary-mask
cleanup on a cold-standby table, and power retention. RF envelope and battery
current measurements on this fork's physical W12 remain hardware checks.
+20 -4
View File
@@ -16,7 +16,7 @@
#define LR2021_USE_DCDC 0
#endif
// Board-specific FEM warm-up. Do not leave a PA enabled in ordinary standby.
// Optional board-specific FEM settling; the RF table defines standby bias.
#ifndef LR2021_TX_PREARM_MS
#define LR2021_TX_PREARM_MS 0
#endif
@@ -72,8 +72,9 @@ class CustomLR2021 : public LR2021 {
}
int16_t prepareTxRfSwitch() {
// stageMode(TX) does not enter standby. Stop RX before temporarily enabling
// the LF PA there; HF routing and boards without this opt-in are unchanged.
// stageMode(TX) does not enter standby. Stop RX before settling the LF PA.
// A warm standby table (W12 PR #3579) retains its original DIO masks;
// other tables temporarily add the TX bias only for this preparation.
int16_t status = standby();
if (status == RADIOLIB_ERR_NONE && !mesh::lr2021::isHighBand(freqMHz)) {
status = setTxStandbyMask(true);
@@ -124,7 +125,9 @@ class CustomLR2021 : public LR2021 {
#if LR2021_TX_PREARM_MS > 0
_tx_standby_armed = false;
#endif
return RADIOLIB_ERR_NONE;
// Some DIO implementations latch a new mask only on a mode transition.
// Reset restoration must physically select the restored standby outputs.
return LR2021::standby();
}
uint8_t rfSwitchRxMode() const {
@@ -264,6 +267,17 @@ class CustomLR2021 : public LR2021 {
return true; // success
}
int16_t replaceRfSwitchTable(const uint32_t (&pins)[Module::RFSWITCH_MAX_PINS],
const Module::RfSwitchMode_t table[]) {
// Used by W12 supply transitions. Keep the old rail powered until a
// checked cold standby has confirmed every control output is low.
int16_t status = LR2021::standby();
if (status != RADIOLIB_ERR_NONE) return status;
for (size_t i = 0; i < Module::RFSWITCH_MAX_PINS; ++i) _rf_switch_pins[i] = pins[i];
_rf_switch_table = table;
return restoreRfSwitchTable();
}
void setRfSwitchTable(const uint32_t (&pins)[Module::RFSWITCH_MAX_PINS],
const Module::RfSwitchMode_t table[]) {
for (size_t i = 0; i < Module::RFSWITCH_MAX_PINS; i++) {
@@ -421,6 +435,8 @@ class CustomLR2021 : public LR2021 {
}
int16_t readData(uint8_t* data, size_t len) override {
// PR #3512 (d208bfef): reject continuous-RX FIFO races. The wrapper
// stops RX before this snapshot; reject errors/mismatches before copying.
uint16_t fifo_level = 0;
const int16_t status = getRxFifoLevel(&fifo_level);
if (status != RADIOLIB_ERR_NONE) return status;
+3 -1
View File
@@ -59,9 +59,11 @@ protected:
float morseReceiverBandwidthKHz() const override { return 4.8f; }
int16_t prepareMorseReceiver(float frequency_mhz) override {
if (!supportsRadioDeepInit()) return RADIOLIB_ERR_UNSUPPORTED;
if (!_board->prepareRadioFrequency(frequency_mhz)) return RADIOLIB_ERR_INVALID_FREQUENCY;
_morse_rx_fsk_mode = true;
// W12's rail preparation checks DIO routing over SPI. Recover a failed
// transceiver before asking it to quiesce controls for a band change.
if (!prepareRadioHardReset()) return RADIOLIB_ERR_CHIP_NOT_FOUND;
if (!_board->prepareRadioFrequency(frequency_mhz)) return RADIOLIB_ERR_INVALID_FREQUENCY;
const int16_t status = static_cast<CustomLR2021*>(_radio)->beginMorseReceive(frequency_mhz);
const bool finished = _board->finishRadioHardReset();
return status == RADIOLIB_ERR_NONE && !finished ? RADIOLIB_ERR_UNKNOWN : status;
+1
View File
@@ -102,6 +102,7 @@ struct Native {
int16_t startReceive() { const auto result=call(7); receiving=result==0; return result; }
int16_t setRSSIConfig(uint8_t samples) { rssi_samples=samples; return call(6); }
int16_t receiveDirect() { const auto result=call(7); receiving=result==0; return result; }
int16_t standby() { return 0; }
int16_t setDioFunction(uint8_t dio,uint8_t function,uint8_t pull) {
assert(dio>=5 && function==6 && (pull==0 || pull==1)); return call(4);
}
+1 -1
View File
@@ -141,7 +141,7 @@ struct RadioLibWrapper : mesh::Radio {
uint32_t rssi_latency=0;
unsigned rssi_reads=0;
bool _params_valid=true, _rx_ps_armed=true, _rx_hold_continuous=false;
bool _rx_boosted_gain_valid=false, _cur_rx_boosted_gain=false;
bool _rx_boosted_gain_valid=false, _cur_rx_boosted_gain=false, _rx_boosted_gain_applied=false;
bool rx_gain_apply_ok=true, physical_rx_gain=false;
unsigned rx_gain_applies=0,rx_gain_order=0;
virtual bool applyRxBoostedGainMode(bool enabled) {
+179 -87
View File
@@ -25,6 +25,7 @@ inline unsigned held=0,released=0,deinitialized=0,digital_held=0;
inline int api_result=0;
struct Pad { int latch=0; bool output=false, hold=false; int retained=-1; };
inline Pad pads[64];
inline void (*pad_changed)(int)=nullptr;
inline bool deep_hold=false;
inline bool require_configured_release=false;
inline unsigned deep_hold_en_calls=0, deep_hold_dis_calls=0;
@@ -37,12 +38,12 @@ inline bool valid_output(int pin) {
}
#define GPIO_IS_VALID_OUTPUT_GPIO(pin) valid_output(pin)
inline int physical(int pin) { return pads[pin].hold ? pads[pin].retained : (pads[pin].output ? pads[pin].latch : -1); }
inline int gpio_set_level(int pin,unsigned level) { assert(valid_output(pin)); pads[pin].latch=level; return api_result; }
inline int gpio_set_level(int pin,unsigned level) { assert(valid_output(pin)); pads[pin].latch=level; if(pad_changed) pad_changed(pin); return api_result; }
inline int gpio_config(const gpio_config_t* config) {
assert(config->mode==GPIO_MODE_OUTPUT && config->pull_up_en==0 && config->pull_down_en==0 && config->intr_type==0);
if (api_result) return api_result;
for(int pin=0;pin<64;++pin) if(config->pin_bit_mask&(1ULL<<pin)) {
assert(valid_output(pin)); pads[pin].output=true;
assert(valid_output(pin)); pads[pin].output=true; if(pad_changed) pad_changed(pin);
}
return ESP_OK;
}
@@ -56,7 +57,7 @@ inline bool rtc_gpio_is_valid_gpio(int pin) {
#endif
}
inline int rtc_gpio_hold_en(int pin) { assert(pin>=0 && rtc_gpio_is_valid_gpio(pin)); ++held; if(!api_result) { pads[pin].retained=physical(pin); pads[pin].hold=true; } return api_result; }
inline int rtc_gpio_hold_dis(int pin) { assert(pin>=0 && rtc_gpio_is_valid_gpio(pin)); assert(!require_configured_release || !pads[pin].hold || pads[pin].output); ++released; if(!api_result) pads[pin].hold=false; return api_result; }
inline int rtc_gpio_hold_dis(int pin) { assert(pin>=0 && rtc_gpio_is_valid_gpio(pin)); assert(!require_configured_release || !pads[pin].hold || pads[pin].output); ++released; if(!api_result) pads[pin].hold=false; if(pad_changed) pad_changed(pin); return api_result; }
inline int rtc_gpio_deinit(int pin) { assert(pin>=0 && rtc_gpio_is_valid_gpio(pin)); ++deinitialized; return api_result; }
#endif
inline int gpio_hold_en(int pin) {
@@ -75,6 +76,7 @@ inline int gpio_hold_dis(int pin) {
if(rtc_gpio_is_valid_gpio(pin)) return rtc_gpio_hold_dis(pin);
#endif
if(!api_result) pads[pin].hold=false;
if(pad_changed) pad_changed(pin);
return api_result;
}
#if !SOC_GPIO_SUPPORT_HOLD_SINGLE_IO_IN_DSLP
@@ -423,47 +425,15 @@ int main(){ EnvironmentSensorManager sensors;
):
self.compile_run(code,defines=definitions)
def test_w12_fem_rails_restore_known_output_before_releasing_terminal_hold(self):
def w12_board_harness(self, case):
source=(ROOT/'variants/meshnology_w12/MeshnologyW12Board.cpp').read_text()
begin=body(source,'void MeshnologyW12Board::begin()')
prefix=begin[:begin.index(' prepareRadioFrequency(')]+'}\n'
code=r'''
#include <helpers/ESP32GpioHold.h>
#include <initializer_list>
#define HIGH 1
#define LOW 0
#define OUTPUT 1
#define P_LORA_LF_PA_POWER 4
#define P_LORA_HF_PA_POWER 3
#define PIN_ADC_CTRL 2
void pinMode(int pin,int mode){assert(mode==OUTPUT);pads[pin].output=true;}
void digitalWrite(int pin,int level){gpio_set_level(pin,level);}
class ESP32Board {public:void begin(){mesh::esp32::resetTerminalDeepSleepHolds();}};
class MeshnologyW12Board:public ESP32Board {public:void begin();};
@BEGIN@
int main(){ using namespace mesh::esp32;
for(int active : {-1,3,4}) {
// Wake safely from terminal shutdown or either band's packet-wake sleep.
holdOutputForDeepSleep(4,active==4?HIGH:LOW);
holdOutputForDeepSleep(3,active==3?HIGH:LOW);
pads[4].output=false;pads[3].output=false;require_configured_release=true;
MeshnologyW12Board board;board.begin();
assert(!pads[3].hold && !pads[4].hold && physical(3)==LOW && physical(4)==LOW);
assert(!deep_hold && !terminalDigitalHoldPending());
}
}
'''
self.compile_run(code.replace('@BEGIN@',prefix),s3=1)
# The prior release-before-pinMode sequence must be caught.
broken=prefix.replace('releaseOutputAfterDeepSleep((gpio_num_t)P_LORA_LF_PA_POWER, LOW)',
'releaseRtcGpioIfSupported((gpio_num_t)P_LORA_LF_PA_POWER)')
self.compile_run(code.replace('@BEGIN@',broken),s3=1,helper_mutation=lambda text:text)
def test_w12_packet_wake_retains_only_selected_band_and_terminal_shutdown_disables_both(self):
source=(ROOT/'variants/meshnology_w12/MeshnologyW12Board.cpp').read_text()
methods='\n'.join(body(source,signature) for signature in (
begin=begin[:begin.index(' periph_power.begin();')]+'}\n'
methods=begin+'\n'+'\n'.join(body(source,signature) for signature in (
'bool MeshnologyW12Board::prepareRadioFrequency(float frequency)',
'bool MeshnologyW12Board::resetRadio()',
'void MeshnologyW12Board::enterDeepSleep(uint32_t secs, int pin_wake_btn)',
'void MeshnologyW12Board::shutdownPeripherals()',
'void MeshnologyW12Board::powerOff()'))
code=r'''
#include <helpers/ESP32GpioHold.h>
@@ -471,86 +441,208 @@ int main(){ using namespace mesh::esp32;
#include <vector>
#define HIGH 1
#define LOW 0
#define OUTPUT 1
#define PIN_ADC_CTRL 2
#define P_LORA_LF_PA_POWER 4
#define P_LORA_HF_PA_POWER 3
#define P_LORA_DIO_1 14
#define P_LORA_NSS 8
#define P_LORA_RESET 12
#define P_LORA_BUSY 13
#define LORA_FREQ 909.5f
constexpr int ESP_PD_DOMAIN_RTC_PERIPH=1,ESP_PD_OPTION_ON=1;
constexpr int RTC_GPIO_MODE_INPUT_ONLY=0,ESP_EXT1_WAKEUP_ANY_HIGH=1;
static bool slept=false,shutdown=false;
static unsigned settles=0;
static bool slept=false,shutdown=false,controls_high=false,spi_working=true;
static bool busy_stuck=false,base_restore_lf=false;
static int fail_callback_at=0,callback_calls=0,sleep_callbacks=0,reset_pulses=0;
static uint32_t clock_ms=0,reset_low_at=0;
static uint64_t wake_mask=0,timer=0;
static std::vector<int> writes;
void digitalWrite(int pin,int level){
assert(!pads[pin].hold);pads[pin].output=true;gpio_set_level(pin,level);
writes.push_back(pin*2+level);
// Never power both FEMs, even transiently, while changing bands.
assert(physical(3)!=HIGH || physical(4)!=HIGH);
static std::vector<unsigned> delays;
void observe_pad(int pin){
if(pin==12 && physical(12)==LOW) reset_low_at=clock_ms;
if(pin==3 || pin==4){
// Supplies can be removed only after checked cold controls or settled NRST.
assert(physical(4)!=LOW || !controls_high);
assert(physical(3)!=HIGH || physical(4)!=HIGH);
writes.push_back(pin*2+(physical(pin)==HIGH));
}
}
void delay(unsigned ms){assert(ms==1);++settles;}
void pinMode(int pin,int mode){assert(mode==OUTPUT);pads[pin].output=true;}
void digitalWrite(int pin,int level){
assert(!pads[pin].hold);gpio_set_level(pin,level);
if(pin==12 && level==HIGH && clock_ms-reset_low_at>=1){++reset_pulses;spi_working=true;}
}
void delay(unsigned ms){
clock_ms+=ms;delays.push_back(ms);
if(physical(12)==LOW && clock_ms-reset_low_at>=1) controls_high=false;
}
uint32_t millis(){return clock_ms;}
void yield(){++clock_ms;}
int digitalRead(int pin){assert(pin==13);return busy_stuck;}
void esp_sleep_pd_config(int domain,int option){assert(domain==1 && option==1);}
void rtc_gpio_set_direction(int pin,int mode){assert(pin==14 && mode==0);}
void rtc_gpio_pulldown_en(int pin){assert(pin==14);}
void esp_sleep_enable_ext1_wakeup(uint64_t mask,int mode){assert(mode==1);wake_mask=mask;}
void esp_sleep_enable_timer_wakeup(uint64_t value){timer=value;}
void sleep_pads(){
// Unretained outputs may float once the CPU domain powers down.
for(int pin : {3,4}) if(!pads[pin].hold) pads[pin].output=false;
}
void sleep_pads(){for(int pin:{3,4}) if(!pads[pin].hold) pads[pin].output=false;}
void esp_deep_sleep_start(){sleep_pads();slept=true;}
class ESP32Board {public:void powerOff(){
assert(pads[3].hold && pads[4].hold && physical(3)==LOW && physical(4)==LOW);
sleep_pads();shutdown=true;
}};
class ESP32Board {public:
virtual bool prepareRadioFrequency(float){return false;}
void begin(){mesh::esp32::resetTerminalDeepSleepHolds();}
virtual void shutdownPeripherals(){
assert(physical(3)==HIGH || physical(4)==HIGH);
// Stopping CW can restore the LF packet profile and its warm standby table.
if(base_restore_lf){assert(prepareRadioFrequency(909.5f));controls_high=true;}
mesh::esp32::holdOutputForDeepSleep(P_LORA_NSS,HIGH);
}
void powerOff(){shutdownPeripherals();
assert(pads[3].hold && pads[4].hold && physical(3)==LOW && physical(4)==LOW);
assert(pads[8].hold && physical(8)==HIGH);shutdown=true;sleep_pads();
}
};
class MeshnologyW12Board:public ESP32Board {
public:
bool radio_band_initialized=false,radio_high_band=false;
bool prepareRadioFrequency(float frequency);
bool (*radio_rf_switch_handler)(bool,bool)=nullptr;
void begin();
bool prepareRadioFrequency(float frequency) override;
bool resetRadio();
void enterDeepSleep(uint32_t secs,int pin_wake_btn);
void shutdownPeripherals() override;
void powerOff();
};
@METHODS@
bool rf_switch(bool lf_awake,bool sleep_after){
++callback_calls;
// Checked SPI cannot execute with a retained HIGH chip select.
assert(!pads[8].hold && physical(8)==HIGH);
if(!spi_working || callback_calls==fail_callback_at) return false;
if(lf_awake){assert(physical(4)==HIGH && physical(3)==LOW);assert(!delays.empty() && delays.back()==1);}
controls_high=lf_awake;
if(sleep_after){assert(!lf_awake);++sleep_callbacks;}
return true;
}
void fresh(){
pad_changed=nullptr;
for(auto& pad:pads) pad=Pad{};
clock_ms=reset_low_at=0;controls_high=false;spi_working=true;busy_stuck=false;
callback_calls=sleep_callbacks=reset_pulses=fail_callback_at=0;
base_restore_lf=slept=shutdown=false;writes.clear();delays.clear();
mesh::esp32::releaseOutputAfterDeepSleep(12,HIGH);
mesh::esp32::releaseOutputAfterDeepSleep(8,HIGH);
mesh::esp32::releaseOutputAfterDeepSleep(3,LOW);
mesh::esp32::releaseOutputAfterDeepSleep(4,LOW);
pad_changed=observe_pad;
}
int main(){using namespace mesh::esp32;
for(bool high_band : {false,true}) {
resetTerminalDeepSleepHolds();
releaseOutputAfterDeepSleep(3,LOW);releaseOutputAfterDeepSleep(4,LOW);
MeshnologyW12Board board;
@CASE@
}
'''
return code.replace('@METHODS@',methods).replace('@CASE@',case)
def test_w12_boot_quiesces_retained_warm_radio_before_releasing_fem_rails(self):
case=r'''
for(int active:{-1,3,4}){
fresh();pad_changed=nullptr;
holdOutputForDeepSleep(4,active==4?HIGH:LOW);
holdOutputForDeepSleep(3,active==3?HIGH:LOW);
if(active<0) holdOutputForDeepSleep(12,LOW);
// MCU-only restart preserves radio DIOs, including the LF warm standby.
controls_high=active==4;
pads[4].output=false;pads[3].output=false;pads[12].output=false;
require_configured_release=true;pad_changed=observe_pad;
MeshnologyW12Board board;board.begin();
assert(!pads[3].hold && !pads[4].hold && !pads[12].hold);
assert(physical(3)==LOW && physical(4)==HIGH && physical(12)==HIGH);
assert(!controls_high && reset_pulses==1 && board.radio_band_initialized);
assert(!deep_hold && !terminalDigitalHoldPending());
}
'''
code=self.w12_board_harness(case)
self.compile_run(code,s3=1)
# Releasing retained rails before quiescing a warm radio must fail.
broken=code.replace('releaseOutputAfterDeepSleep((gpio_num_t)P_LORA_RESET, LOW)',
'releaseOutputAfterDeepSleep((gpio_num_t)P_LORA_RESET, HIGH)',1)
self.compile_run(broken,s3=1,helper_mutation=lambda text:text)
def test_w12_band_switch_failures_retry_and_physical_reset_recover_spi(self):
case=r'''
fresh();MeshnologyW12Board board;board.begin();board.radio_rf_switch_handler=rf_switch;
board.radio_band_initialized=false;assert(board.prepareRadioFrequency(909.5f));
assert(controls_high);const auto original_writes=writes.size();
// A failed cold callback must not drop the powered LF rail.
fail_callback_at=callback_calls+1;
assert(!board.prepareRadioFrequency(2440.0f));
assert(!board.radio_band_initialized && writes.size()==original_writes && physical(4)==HIGH);
fail_callback_at=0;
// Even returning to the cached old band repeats a previously failed transition.
int callbacks_before=callback_calls;
assert(board.prepareRadioFrequency(909.5f) && callback_calls==callbacks_before+2);
assert(board.prepareRadioFrequency(2440.0f) && !controls_high && physical(3)==HIGH);
// Failure after powering LF also invalidates the band cache for a same-band retry.
fail_callback_at=callback_calls+2;
assert(!board.prepareRadioFrequency(909.5f));
assert(!board.radio_band_initialized && physical(4)==HIGH);
fail_callback_at=0;callbacks_before=callback_calls;
assert(board.prepareRadioFrequency(909.5f) && callback_calls==callbacks_before+2 && controls_high);
// A physical reset must work without SPI; only then may frequency prep use SPI.
spi_working=false;const auto before_reset=writes.size();
assert(board.resetRadio() && spi_working && !board.radio_band_initialized);
assert(writes.size()==before_reset && physical(4)==HIGH && !controls_high);
assert(board.prepareRadioFrequency(909.5f) && controls_high);
busy_stuck=true;const uint32_t started=clock_ms;
assert(!board.resetRadio() && clock_ms-started>=3310 && clock_ms-started<3400);
assert(!board.radio_band_initialized && writes.size()>before_reset);
'''
self.compile_run(self.w12_board_harness(case),s3=1)
def test_w12_packet_wake_retains_selected_rail_and_shutdown_quiesces_after_cw(self):
case=r'''
for(bool high_band:{false,true}){
fresh();MeshnologyW12Board board;board.begin();board.radio_rf_switch_handler=rf_switch;
const float frequency=high_band?2440.0f:909.5f;
const unsigned before=settles;
assert(board.prepareRadioFrequency(frequency) && settles==before+1);
assert(writes[writes.size()-3]==4*2 && writes[writes.size()-2]==3*2);
assert(physical(3)==high_band && physical(4)==!high_band);
assert(board.prepareRadioFrequency(frequency) && settles==before+1);
// A real LF/HF crossing disables the previous rail before enabling the next.
assert(board.prepareRadioFrequency(high_band?909.5f:2440.0f));
assert(physical(3)==!high_band && physical(4)==high_band);
assert(board.prepareRadioFrequency(frequency) && settles==before+3);
for(int button : {-1,0}) {
board.radio_band_initialized=false;assert(board.prepareRadioFrequency(frequency));
for(int button:{-1,0}){
slept=false;timer=0;
board.enterDeepSleep(button<0?5:0,button);
assert(slept && pads[3].hold && pads[4].hold);
assert(physical(3)==high_band && physical(4)==!high_band);
assert(wake_mask==((1ULL<<14)|(button<0?0:(1ULL<<button))));
assert(timer==(button<0?5000000:0));
assert(!deep_hold && !terminalDigitalHoldPending());
// Deep sleep never returns: model begin() releasing the retained rails,
// then its normal band selection, before the next runtime operation.
releaseOutputAfterDeepSleep(3,LOW);releaseOutputAfterDeepSleep(4,LOW);
board.radio_band_initialized=false;
assert(board.prepareRadioFrequency(frequency));
// Model a new MCU instance: radio retains state, the callback is not ready.
board.radio_rf_switch_handler=nullptr;board.radio_band_initialized=false;
board.begin();releaseOutputAfterDeepSleep(8,HIGH);
board.radio_rf_switch_handler=rf_switch;
board.radio_band_initialized=false;assert(board.prepareRadioFrequency(frequency));
}
// Explicit runtime shutdown must disable whichever band was powered.
board.powerOff();assert(shutdown && physical(3)==LOW && physical(4)==LOW);
// Base shutdown may finish a HF CW and rewarm LF before holding NSS HIGH.
base_restore_lf=true;board.powerOff();
assert(shutdown && sleep_callbacks==1 && !controls_high);
assert(physical(3)==LOW && physical(4)==LOW && !board.radio_band_initialized);
assert(pads[8].hold && physical(8)==HIGH && !pads[12].hold);
}
}
'''
self.compile_run(code.replace('@METHODS@',methods),s3=1)
# Detect either active-rail loss or a floating inactive rail on sleep.
for rail in ('P_LORA_LF_PA_POWER','P_LORA_HF_PA_POWER'):
broken=re.sub(r'(?m)^\s*mesh::esp32::holdOutputForDeepSleep\(\(gpio_num_t\)'+rail+
r', radio_high_band[^;]*;', '', methods)
self.assertNotEqual(broken,methods)
self.compile_run(code.replace('@METHODS@',broken),s3=1,helper_mutation=lambda text:text)
code=self.w12_board_harness(case)
self.compile_run(code,s3=1)
broken=code.replace('mesh::esp32::releaseOutputAfterDeepSleep((gpio_num_t)P_LORA_NSS, HIGH);','')
self.compile_run(broken,s3=1,helper_mutation=lambda text:text)
def test_w12_failed_shutdown_quiescence_holds_reset_before_removing_rails(self):
case=r'''
fresh();MeshnologyW12Board board;board.begin();board.radio_rf_switch_handler=rf_switch;
board.radio_band_initialized=false;assert(board.prepareRadioFrequency(909.5f));
assert(controls_high);
// A failed SPI response leaves controls unknown; reset is the independent fallback.
spi_working=false;board.powerOff();
assert(shutdown && physical(3)==LOW && physical(4)==LOW && !controls_high);
assert(pads[12].hold && physical(12)==LOW && !board.radio_band_initialized);
assert(pads[8].hold && physical(8)==HIGH && sleep_callbacks==0);
'''
code=self.w12_board_harness(case)
self.compile_run(code,s3=1)
broken=code.replace('mesh::esp32::holdOutputForDeepSleep((gpio_num_t)P_LORA_RESET, LOW);','')
self.compile_run(broken,s3=1,helper_mutation=lambda text:text)
def test_all_production_hold_release_deinit_sites_use_capability_wrapper(self):
# No board or FEM may keep using unsupported RTC calls behind the fix.
+1
View File
@@ -110,6 +110,7 @@ class LR2021 {
}
int16_t setCRC(uint8_t value) { assert(value==0 || value==2);return step(3); }
int16_t explicitHeader() { return step(4); }
int16_t standby() { return 0; }
int16_t setRxBoostedGainMode(uint8_t value) { gain=value;return step(5); }
void setRfSwitchTable(const uint32_t (&)[Module::RFSWITCH_MAX_PINS],const Module::RfSwitchMode_t*) {}
int16_t setDioFunction(uint8_t dio,uint8_t function,uint8_t pull) {
+105 -14
View File
@@ -3,6 +3,8 @@
The native radio mock models the distinction between host RF-switch GPIOs and
LR2021 DIO masks. No PlatformIO or RF hardware is needed. Failed SPI responses
may still apply a mask, so cleanup must not assume an error left a pin unchanged.
The actual warm W12 and band-switch cold tables are exercised separately from
optional transient prearming on the cold table.
"""
from pathlib import Path
import os
@@ -39,6 +41,9 @@ HARNESS = r'''
@POLICY@
#define LR2021_TX_BUSY_TIMEOUT_MS 4UL
#define LR2021_USE_DCDC 0
#ifndef TEST_WARM_STANDBY
#define TEST_WARM_STANDBY 0
#endif
#define RADIOLIB_ERR_NONE 0
#define RADIOLIB_ERR_UNKNOWN -16
#define RADIOLIB_ERR_UNSUPPORTED -99
@@ -75,7 +80,10 @@ HARNESS = r'''
using RadioLibTime_t = uint32_t;
struct SPIClass {};
struct Logger { void print(const char*) {} void println(int) {} };
namespace mesh { Logger& usbLoggingPort() { static Logger logger;return logger; } }
namespace mesh {
Logger& usbLoggingPort() { static Logger logger;return logger; }
struct MorseRx { static constexpr float ReceiverBitRateKbps=4.8f; };
}
struct Hal {
uint32_t GpioLevelHigh=HIGH;
uint32_t now=0;
@@ -109,7 +117,8 @@ public:
MODE_TX_HF,
};
Module* mod;
float freqMHz=909.5f;
float freqMHz=909.5f,tcxoVoltage=0;
uint8_t gainModeLf=1,gainModeHf=3,preambleDetLength=4;
uint8_t stagedMode=RADIOLIB_RADIO_MODE_NONE,chip_mode=MODE_STBY;
uint32_t rxTimeout=0xffffff;
uint8_t masks[7]={};
@@ -159,9 +168,6 @@ public:
int16_t setTxTestMode(uint8_t mode) { assert(mode==0);return setTx(0); }
int16_t setRx(uint32_t timeout) {
assert(timeout==rxTimeout);++rx_calls;
#if LR2021_TX_PREARM_MS > 0
assert(offInStandby());
#endif
applyMode(mesh::lr2021::isHighBand(freqMHz) ? MODE_RX_HF : MODE_RX);
return 0;
}
@@ -197,6 +203,13 @@ public:
int16_t begin(float,float,uint8_t,uint8_t,uint8_t,int8_t,uint16_t,float) {
std::fill(masks,masks+7,0);applyMode(MODE_STBY);return 0;
}
int16_t beginGFSK(float frequency,float rate,float deviation,float bandwidth,
int8_t power,uint16_t preamble,float tcxo) {
assert(rate==4.8f && deviation==0.6f && bandwidth==4.8f && power==2 && preamble==16 && tcxo==0);
freqMHz=frequency;std::fill(masks,masks+7,0);applyMode(MODE_STBY);return 0;
}
int16_t setRxBoostedGainMode(uint8_t) { return 0; }
int16_t setSyncWord(uint8_t*,size_t len) { assert(len==0);return 0; }
int16_t setRegulatorDCDC() { assert(false); return -1; }
int16_t setCRC(uint8_t) { return 0; }
int16_t explicitHeader() { return 0; }
@@ -221,7 +234,9 @@ public:
#endif
@RESTORE@
@TABLE@
@REPLACE_TABLE@
@INIT@
@MORSE_RECEIVE@
@RX_MODE@
@TX_MODE@
@LAUNCH@
@@ -230,15 +245,20 @@ public:
};
@BOARD_PINS@
@BOARD_TABLE@
@BOARD_COLD_TABLE@
static CustomLR2021* target_radio=nullptr;
@BOARD_CALLBACK@
static void configure(CustomLR2021& radio) {
radio.setRfSwitchTable(rfswitch_dios,rfswitch_table);
assert(radio.offInStandby());
radio.setRfSwitchTable(rfswitch_dios,
TEST_WARM_STANDBY ? rfswitch_table : rfswitch_table_cold);
assert(radio.offInStandby()==!TEST_WARM_STANDBY);
radio.writes.clear();radio.clear_attempts.clear();
}
static void noBias(CustomLR2021& radio) {
assert(radio.offInStandby());
const bool warm=radio._rf_switch_table==rfswitch_table;
assert(radio.offInStandby()==!warm);
if(radio.chip_mode==LR2021::MODE_STBY)
assert(!radio.level(9) && !radio.level(10) && !radio.level(11));
assert(radio.level(9)==warm && radio.level(10)==warm && radio.level(11)==warm);
#if LR2021_TX_PREARM_MS > 0
assert(!radio._tx_standby_armed);
#endif
@@ -251,11 +271,70 @@ static int packet(CustomLR2021& radio) {
}
int main(int argc,char** argv) {
assert(argc==2);std::string test=argv[1];
Module module;CustomLR2021 radio(&module);configure(radio);
Module module;CustomLR2021 radio(&module);configure(radio);target_radio=&radio;
if(test.rfind("latched_",0)==0) {
radio.latch_masks_on_mode=true;test=test.substr(8);
}
if(test=="packet" || test=="cw") {
if(test=="warm_routes" || test=="warm_reset" || test=="warm_failures" || test=="warm_replace_failures" || test=="warm_sleep") {
assert(TEST_WARM_STANDBY);
// This is exactly PR #3579's LF standby/RX/TX mask, not a timed substitute.
assert(radio.masks[9-5]==5 && radio.masks[10-5]==7 && radio.masks[11-5]==7);
noBias(radio);
if(test=="warm_routes") {
for(float frequency:{909.5f,2440.0f,909.5f,2440.0f,909.5f}) {
// The board first selects checked cold standby, then changes supplies,
// then restores warm standby only after the LF rail is powered.
assert(set_w12_rf_switch_standby(false,false));
assert(radio.offInStandby() && !radio.level(5) && !radio.level(6));noBias(radio);
radio.freqMHz=frequency;
assert(set_w12_rf_switch_standby(frequency<1500,false));
for(bool cw:{false,true}) {
radio.writes.clear();
assert((cw ? radio.transmitMorseCarrier() : packet(radio))==0);
assert(radio.chip_mode==(frequency<1500 ? LR2021::MODE_TX : LR2021::MODE_TX_HF));
assert(radio.writes.empty()); // Warm standby never removes retained LF bias.
noBias(radio);
assert(radio.standby()==0);noBias(radio);
assert(radio.startReceive()==0);
assert(radio.chip_mode==(frequency<1500 ? LR2021::MODE_RX : LR2021::MODE_RX_HF));
assert(!radio.level(9) && radio.level(10)==(frequency<1500) && radio.level(11)==(frequency<1500));
}
}
} else if(test=="warm_reset") {
for(bool warm:{true,false,true}) {
assert(radio.replaceRfSwitchTable(rfswitch_dios,warm?rfswitch_table:rfswitch_table_cold)==0);
assert(radio.beginMorseReceive(warm?909.5f:2440.0f)==0);noBias(radio);
assert(radio.std_init());noBias(radio);
assert(radio.sleep()==0);noBias(radio);
}
} else if(test=="warm_failures") {
for(int fault:{0,1,2}) {
radio.fail_tx=fault<2;radio.busy_after_tx=fault==2;
const int status=fault==1 ? radio.transmitMorseCarrier() : packet(radio);
assert(status==(fault==2 ? RADIOLIB_ERR_SPI_CMD_TIMEOUT : -72));
assert(radio.chip_mode==LR2021::MODE_STBY);noBias(radio);
assert(radio.writes.empty());module.hal->busy=false;
}
} else if(test=="warm_sleep") {
assert(set_w12_rf_switch_standby(false,true));
assert(radio.sleep_calls==1 && radio.offInStandby());noBias(radio);
assert(set_w12_rf_switch_standby(true,false));noBias(radio);
radio.fail_standby=true;
assert(!set_w12_rf_switch_standby(false,true));
assert(radio.sleep_calls==1); // Failed quiescence must not claim sleep.
} else {
// A replacement is checked at the initial standby, each mask write,
// and the final physical standby latch; errors may follow applied writes.
for(int fault:{0,1,2}) {
radio.fail_standby_at=fault==0 ? radio.standby_calls+1 : fault==2 ? radio.standby_calls+2 : 0;
radio.fail_dio=10;radio.fail_disarm=fault==1;radio.fail_count=1;
assert(radio.replaceRfSwitchTable(rfswitch_dios,rfswitch_table_cold)==(fault==1 ? -73 : -71));
radio.fail_standby_at=0;radio.fail_disarm=false;
assert(radio.replaceRfSwitchTable(rfswitch_dios,rfswitch_table_cold)==0);noBias(radio);
assert(radio.replaceRfSwitchTable(rfswitch_dios,rfswitch_table)==0);noBias(radio);
}
}
} else if(test=="packet" || test=="cw") {
radio.chip_mode=LR2021::MODE_RX;
assert((test=="packet" ? packet(radio) : radio.transmitMorseCarrier())==0);
assert(radio.tx_calls==1 && radio.chip_mode==LR2021::MODE_TX);
@@ -382,7 +461,9 @@ class LR2021TxPrearmTest(unittest.TestCase):
"@SLEEP@": "int16_t sleep()",
"@RESTORE@": "int16_t restoreRfSwitchTable(",
"@TABLE@": "void setRfSwitchTable(",
"@REPLACE_TABLE@": "int16_t replaceRfSwitchTable(",
"@INIT@": "bool std_init(",
"@MORSE_RECEIVE@": "int16_t beginMorseReceive(",
"@RX_MODE@": "uint8_t rfSwitchRxMode(",
"@TX_MODE@": "uint8_t rfSwitchTxMode(",
"@LAUNCH@": "int16_t launchMode(",
@@ -390,14 +471,18 @@ class LR2021TxPrearmTest(unittest.TestCase):
"@RECEIVE@": "int16_t startReceive()",
}.items():
source = source.replace(marker, method(driver, signature))
source = source.replace("@BOARD_CALLBACK@", method(board,
"static bool set_w12_rf_switch_standby(").replace("radio.", "target_radio->"))
source = source.replace("@POLICY@", re.search(
r"#ifndef LR2021_TX_PREARM_MS\b.*?#endif", driver, re.S).group(0))
for marker, pattern in {
"@BOARD_PINS@": r"const uint32_t rfswitch_dios\[\].*?\n};",
"@BOARD_TABLE@": r"static const Module::RfSwitchMode_t rfswitch_table\[\].*?\n};",
"@BOARD_COLD_TABLE@": r"static const Module::RfSwitchMode_t rfswitch_table_cold\[\].*?\n};",
}.items():
source = source.replace(marker, re.search(pattern, board, re.S).group(0))
for name, flags in {"w12": ["-DLR2021_TX_PREARM_MS=1"], "default": [],
for name, flags in {"w12": ["-DLR2021_TX_PREARM_MS=1", "-DTEST_WARM_STANDBY=1"],
"cold_prearm": ["-DLR2021_TX_PREARM_MS=1"], "default": [],
"disabled": ["-DLR2021_TX_PREARM_MS=0"]}.items():
cpp = Path(cls.temp.name) / (name + ".cpp")
binary = Path(cls.temp.name) / (name + (".exe" if os.name == "nt" else ""))
@@ -411,11 +496,17 @@ class LR2021TxPrearmTest(unittest.TestCase):
raise AssertionError(built.stdout + built.stderr)
cls.binaries[name] = binary
def run_case(self, case, build="w12"):
def run_case(self, case, build="cold_prearm"):
ran = subprocess.run([str(self.binaries[build]), case], capture_output=True,
text=True, timeout=10)
self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
def test_actual_w12_warm_table_and_checked_cold_band_transitions(self):
for case in ["warm_routes", "warm_reset", "warm_failures", "warm_replace_failures", "warm_sleep"]:
for prefix in ["", "latched_"]:
with self.subTest(case=case, latched=bool(prefix)):
self.run_case(prefix + case, "w12")
def test_packet_settles_pa_without_retaining_standby_bias(self):
self.run_case("packet")
@@ -466,7 +557,7 @@ class LR2021TxPrearmTest(unittest.TestCase):
with self.subTest(case=case):
self.run_case(case)
def test_only_w12_enables_prearm_and_normal_standby_remains_off(self):
def test_only_w12_enables_the_tx_settle_and_cleanup_guard(self):
opted_in = []
for recipe in sorted((ROOT / "variants").glob("*/platformio.ini")):
if re.search(r"(?m)^\s*-D\s+LR2021_TX_PREARM_MS=[1-9]\d*\b", recipe.read_text()):
+80 -5
View File
@@ -82,6 +82,7 @@ uint32_t millis(){return now_ms;}
struct LR2021 {
bool stopped=false, inject_read_irq=false;
int rx_status=1, tx_status=1, fifo_status=0, clear_status=0, base_status=0, standby_status=0;
int irq_clear_status=0;
uint32_t irq=0;
uint16_t fifo=4;
int packet_length=4;
@@ -95,11 +96,23 @@ struct LR2021 {
void setPacketReceivedAction(void (*fn)()){callback=fn;}
int checkIrq(int type){++irq_probes;return type==RADIOLIB_IRQ_RX_DONE?rx_status:tx_status;}
int standby(){++standbys;if(standby_status)return standby_status;stopped=true;return 0;}
int clearIrqFlags(uint32_t){assert(stopped);++irq_clears;rx_status=0;return 0;}
int getPacketLength(){return packet_length;}
float getSNR(){return 7;}
float getRSSI(){return -90;}
int getRxFifoLevel(uint16_t* output){*output=fifo;return fifo_status;}
int clearIrqFlags(uint32_t flags){
if(flags&RADIOLIB_LR2021_IRQ_RX_DONE)assert(stopped);
++irq_clears;
if(irq_clear_status)return irq_clear_status;
irq &= ~flags;
if(flags&RADIOLIB_LR2021_IRQ_RX_DONE)rx_status=0;
return 0;
}
void assertStoppedSnapshot(){
#if defined(USE_LR2021)
assert(stopped && "LR2021 RX must stop before metadata/FIFO snapshots");
#endif
}
int getPacketLength(){assertStoppedSnapshot();return packet_length;}
float getSNR(){assertStoppedSnapshot();return 7;}
float getRSSI(){assertStoppedSnapshot();return -90;}
int getRxFifoLevel(uint16_t* output){assertStoppedSnapshot();*output=fifo;return fifo_status;}
int clearRxFifo(){++clears;if(clear_status)return clear_status;fifo=0;return 0;}
virtual int16_t readData(uint8_t* output,size_t len){
++reads;
@@ -726,6 +739,68 @@ int main(){
}
''', ["-DUSE_LR2021=1"])
def test_lr2021_upstream_3512_fifo_failures_never_copy_suspect_bytes(self):
# #3512 at d208bfef94ab83280de30ba4a0675ff73b3ecef3 rejects FIFO
# length mismatches. Keep the checked, stop-before-copy integration.
compile_run(self.source() + r'''
int main(){
Chip chip;chip.stopped=true;uint8_t bytes[8];memset(bytes,0x3c,sizeof(bytes));
for(int fifo:{0,3,5}){
chip.fifo=fifo;
assert(chip.readData(bytes,4)==RADIOLIB_ERR_PACKET_TOO_SHORT);
assert(chip.reads==0&&chip.fifo==0);
for(uint8_t byte:bytes)assert(byte==0x3c);
}
chip.fifo=4;chip.fifo_status=-99;
const unsigned clears=chip.clears;
assert(chip.readData(bytes,4)==-99&&chip.clears==clears&&chip.reads==0);
chip.fifo_status=0;chip.fifo=3;chip.clear_status=-44;
assert(chip.readData(bytes,4)==-44&&chip.fifo==3&&chip.reads==0);
for(uint8_t byte:bytes)assert(byte==0x3c);
chip.clear_status=0;chip.fifo=4;
// RadioLib permits len=0 to select the actual packet length.
assert(chip.readData(bytes,0)==0&&chip.reads==1);
chip.packet_length=0;chip.fifo=0;
assert(chip.readData(bytes,0)==RADIOLIB_ERR_PACKET_TOO_SHORT&&chip.reads==1);
}
''', ["-DUSE_LR2021=1"])
def test_lr2021_failed_rx_done_ack_blocks_rearm_until_recovery(self):
compile_run(self.source() + r'''
int main(){
CustomLR2021Wrapper w;assert(w.registerInterruptAction());uint8_t bytes[16]={};
w.chip.irq_clear_status=-44;w.chip.callback();
assert(w.recvRaw(bytes,sizeof(bytes))==0);
assert(w.chip.stopped&&w.state==STATE_IDLE&&w.chip.arms==0);
assert(w.chip.irq_clears==1&&w.chip.rx_status==1&&w.n_recv_errors==1);
w.chip.irq_clear_status=0;
assert(w.recvRaw(bytes,sizeof(bytes))==0&&w.chip.arms==1&&w.state==STATE_RX);
w.chip.fifo=4;w.chip.rx_status=1;w.chip.callback();
assert(w.recvRaw(bytes,sizeof(bytes))==4&&w.chip.irq_clears==2);
assert(!w.chip.stopped&&w.state==STATE_RX&&w.chip.arms==2);
}
''', ["-DUSE_LR2021=1"])
def test_lr2021_upstream_3512_fresh_preamble_gets_a_new_activity_timeout(self):
compile_run(self.source() + r'''
int main(){
Chip chip;
chip.irq=RADIOLIB_LR2021_IRQ_LORA_HEADER_VALID|RADIOLIB_LR2021_IRQ_LORA_HDR_CRC_ERROR;
assert(chip.isReceiving()&&chip._headerSeen);
// A stale error flag and the preceding header timer must not cancel the
// next frame's preamble after continuous RX cleared HEADER_VALID.
now_ms+=chip._maxPayloadMillis;
chip.irq=RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED|RADIOLIB_LR2021_IRQ_LORA_HDR_CRC_ERROR;
assert(chip.isReceiving()&&!chip._headerSeen&&chip._activityAt==now_ms);
assert(chip.irq_clears==0);
now_ms+=chip._preambleMillis;
assert(chip.isReceiving());
++now_ms;
assert(!chip.isReceiving()&&chip._activityAt==0&&chip.irq_clears==1);
assert(!(chip.irq&RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED));
}
''', ["-DUSE_LR2021=1"])
if __name__ == "__main__":
unittest.main()
+53 -6
View File
@@ -4,6 +4,10 @@
void MeshnologyW12Board::begin() {
ESP32Board::begin();
// An MCU-only reset can leave the radio's warm LF controls HIGH. Assert
// radio reset before releasing or dropping either retained FEM supply.
mesh::esp32::releaseOutputAfterDeepSleep((gpio_num_t)P_LORA_RESET, LOW);
delay(1);
// Keep both FEM rails off while releasing a retained terminal shutdown.
mesh::esp32::releaseOutputAfterDeepSleep((gpio_num_t)P_LORA_LF_PA_POWER, LOW);
mesh::esp32::releaseOutputAfterDeepSleep((gpio_num_t)P_LORA_HF_PA_POWER, LOW);
@@ -15,6 +19,7 @@ void MeshnologyW12Board::begin() {
digitalWrite(P_LORA_LF_PA_POWER, LOW);
digitalWrite(P_LORA_HF_PA_POWER, LOW);
prepareRadioFrequency(LORA_FREQ);
digitalWrite(P_LORA_RESET, HIGH);
periph_power.begin();
esp_reset_reason_t reason = esp_reset_reason();
@@ -45,21 +50,44 @@ void MeshnologyW12Board::begin() {
const bool high_band = mesh::lr2021::isHighBand(frequency);
if (radio_band_initialized && high_band == radio_high_band) return true;
// DIO controls must be LOW before either FEM loses its supply. A failed
// transition remains uninitialized so even a same-band retry repeats it.
radio_band_initialized = false;
if (radio_rf_switch_handler && !radio_rf_switch_handler(false, false)) return false;
// The W12 schematic connects PA_EN_M (GPIO4) to the GC1109 sub-GHz rail
// and PA_EN_G (GPIO3) to the RFX2402E 2.4 GHz rail. Keep only the selected
// FEM powered, and disable the old path before enabling the new one.
digitalWrite(P_LORA_LF_PA_POWER, LOW);
digitalWrite(P_LORA_HF_PA_POWER, LOW);
digitalWrite(high_band ? P_LORA_HF_PA_POWER : P_LORA_LF_PA_POWER, HIGH);
// The FEM rail needs only 1 ms to settle. TX control-line prearming is
// handled separately immediately before transmit, not by idling the PA on.
// PR #3579 uses a 1 ms FEM rail settling delay. Only after that delay
// may the selected LF FEM receive its retained standby control bias.
delay(1);
if (radio_rf_switch_handler && !radio_rf_switch_handler(!high_band, false)) return false;
radio_high_band = high_band;
radio_band_initialized = true;
return true;
}
bool MeshnologyW12Board::resetRadio() {
// Recovery must pulse NRST before rail preparation, whose checked DIO
// routing would otherwise depend on the SPI fault being recovered.
radio_band_initialized = false;
mesh::esp32::releaseOutputAfterDeepSleep((gpio_num_t)P_LORA_RESET, LOW);
delay(10);
digitalWrite(P_LORA_RESET, HIGH);
// Match RadioLib's LRxxxx reset startup interval and bounded BUSY wait.
delay(300);
const uint32_t started = millis();
while (digitalRead(P_LORA_BUSY)) {
yield();
if (millis() - started >= 3000) return false;
}
return true;
}
void MeshnologyW12Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) {
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
@@ -89,12 +117,31 @@ void MeshnologyW12Board::begin() {
esp_deep_sleep_start(); // CPU halts here and never returns!
}
void MeshnologyW12Board::powerOff() {
// A shutdown must not retain the packet-wake deep-sleep behavior above.
// Disable both external FEM rails, then use the base shutdown path so the
// display, GPS, radio, serial buffers, and stale wake sources are handled.
void MeshnologyW12Board::shutdownPeripherals() {
// Quiesce controls while the selected FEM is still powered, then let the
// radio enter sleep before removing its FEM supplies.
if (radio_rf_switch_handler) radio_rf_switch_handler(false, false);
ESP32Board::shutdownPeripherals();
// Stopping an active Morse carrier can restore a different packet band
// during base shutdown. Base shutdown also holds NSS HIGH; release only
// that hold for the final checked cold routing/sleep, then retain NSS again.
mesh::esp32::releaseOutputAfterDeepSleep((gpio_num_t)P_LORA_NSS, HIGH);
const bool controls_low = !radio_rf_switch_handler || radio_rf_switch_handler(false, true);
mesh::esp32::holdOutputForDeepSleep((gpio_num_t)P_LORA_NSS, HIGH);
if (!controls_low) {
// A failed SPI response cannot prove that the controls went LOW. Hold
// the transceiver in reset before removing the external FEM supplies.
mesh::esp32::holdOutputForDeepSleep((gpio_num_t)P_LORA_RESET, LOW);
delay(1);
}
radio_band_initialized = false;
mesh::esp32::holdOutputForDeepSleep((gpio_num_t)P_LORA_LF_PA_POWER, LOW);
mesh::esp32::holdOutputForDeepSleep((gpio_num_t)P_LORA_HF_PA_POWER, LOW);
}
void MeshnologyW12Board::powerOff() {
// Base powerOff dispatches shutdownPeripherals() above, clears stale wake
// sources, and enters terminal deep sleep with both FEM rails retained LOW.
ESP32Board::powerOff();
}
@@ -16,6 +16,7 @@ protected:
float adc_mult = ADC_MULTIPLIER;
bool radio_band_initialized = false;
bool radio_high_band = false;
bool (*radio_rf_switch_handler)(bool lf_awake, bool sleep_after) = nullptr;
public:
RefCountedDigitalPin periph_power;
@@ -27,7 +28,13 @@ public:
void onAfterTransmit(void) override;
void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1);
void powerOff() override;
void shutdownPeripherals() override;
bool prepareRadioFrequency(float frequency) override;
bool supportsRadioHardReset() const override { return true; }
bool resetRadio() override;
void setRadioRfSwitchHandler(bool (*handler)(bool lf_awake, bool sleep_after)) {
radio_rf_switch_handler = handler;
}
bool supportsBatteryCalibration() const override { return true; }
uint16_t getBattMilliVolts() override;
+1 -1
View File
@@ -11,7 +11,7 @@ build_flags =
-D WRAPPER_CLASS=CustomLR2021Wrapper
-D USE_LR2021
-D LR2021_USE_DCDC=1 ; verified W12 schematic: L5 2.2uH across LXA/LXB
-D LR2021_TX_PREARM_MS=1 ; settle GC1109 before LF TX, without an always-on standby PA
-D LR2021_TX_PREARM_MS=1 ; LF TX settle guard; warm standby follows upstream PR #3579
-D NEOPIXEL_LED=46
-D NEOPIXEL_BRIGHTNESS=64
-D P_LORA_DIO_1=14
+23 -2
View File
@@ -41,7 +41,9 @@ const uint32_t rfswitch_dios[] = {
static const Module::RfSwitchMode_t rfswitch_table[] = {
// DIO5 DIO6 DIO9 DIO10 DIO11
{ LR2021::MODE_STBY, { LOW, LOW, LOW, LOW, LOW } }, // everything off
// PR #3579 (7457cff): retain LF PA bias between packets to avoid a weak
// packet opening. This table is selected only while the LF rail is powered.
{ LR2021::MODE_STBY, { LOW, LOW, HIGH, HIGH, HIGH } }, // GC1109 stays awake
{ LR2021::MODE_RX, { LOW, LOW, LOW, HIGH, HIGH } }, // rx_lf: GC1109 LNA (CPS is don't-care in RX)
{ LR2021::MODE_TX, { LOW, LOW, HIGH, HIGH, HIGH } }, // tx_lf: GC1109 full PA
{ LR2021::MODE_RX_HF, { LOW, HIGH, LOW, LOW, LOW } }, // rx_hf: 2G4 LNA, GC1109 off
@@ -49,6 +51,22 @@ static const Module::RfSwitchMode_t rfswitch_table[] = {
END_OF_MODE_TABLE,
};
// Keep both FEMs' controls low in standby while switching supplies or using
// the HF rail. GC1109 control voltage must not exceed its switched VBAT.
static const Module::RfSwitchMode_t rfswitch_table_cold[] = {
{ LR2021::MODE_STBY, { LOW, LOW, LOW, LOW, LOW } },
rfswitch_table[1],
rfswitch_table[2],
rfswitch_table[3],
rfswitch_table[4],
END_OF_MODE_TABLE,
};
static bool set_w12_rf_switch_standby(bool lf_awake, bool sleep_after) {
if (radio.replaceRfSwitchTable(rfswitch_dios,
lf_awake ? rfswitch_table : rfswitch_table_cold) != RADIOLIB_ERR_NONE) return false;
return !sleep_after || radio.sleep() == RADIOLIB_ERR_NONE;
}
bool radio_init() {
fallback_clock.begin();
@@ -63,7 +81,10 @@ bool radio_init() {
#endif
#ifdef RF_SWITCH_TABLE
radio.setRfSwitchTable(rfswitch_dios, rfswitch_table);
// The callback is installed only after initialization: board.begin() may
// select its first supply before the radio is ready to accept SPI commands.
board.setRadioRfSwitchHandler(set_w12_rf_switch_standby);
if (!set_w12_rf_switch_standby(!mesh::lr2021::isHighBand(radio.getFreqMHz()), false)) return false;
#endif
return true;