diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index 73433434f..c3d275070 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -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: | diff --git a/docs/faq.md b/docs/faq.md index cfd5bcf27..3b80a1c43 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -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. diff --git a/docs/power_saving_diagnostics.md b/docs/power_saving_diagnostics.md index 369ffffe8..cad3ba1fa 100644 --- a/docs/power_saving_diagnostics.md +++ b/docs/power_saving_diagnostics.md @@ -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. diff --git a/src/helpers/radiolib/CustomLR2021.h b/src/helpers/radiolib/CustomLR2021.h index 65052727d..3f2426f8b 100644 --- a/src/helpers/radiolib/CustomLR2021.h +++ b/src/helpers/radiolib/CustomLR2021.h @@ -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; diff --git a/src/helpers/radiolib/CustomLR2021Wrapper.h b/src/helpers/radiolib/CustomLR2021Wrapper.h index a9308d79c..7e6c207e6 100644 --- a/src/helpers/radiolib/CustomLR2021Wrapper.h +++ b/src/helpers/radiolib/CustomLR2021Wrapper.h @@ -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(_radio)->beginMorseReceive(frequency_mhz); const bool finished = _board->finishRadioHardReset(); return status == RADIOLIB_ERR_NONE && !finished ? RADIOLIB_ERR_UNKNOWN : status; diff --git a/test/fixtures/morse_rx_native.cpp b/test/fixtures/morse_rx_native.cpp index faaa1705a..8b19151fa 100644 --- a/test/fixtures/morse_rx_native.cpp +++ b/test/fixtures/morse_rx_native.cpp @@ -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); } diff --git a/test/test_carrier_wave.py b/test/test_carrier_wave.py index 378e238ae..2d95018bc 100644 --- a/test/test_carrier_wave.py +++ b/test/test_carrier_wave.py @@ -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) { diff --git a/test/test_esp32_gpio_hold.py b/test/test_esp32_gpio_hold.py index 3ce05ac46..aaaaa1387 100644 --- a/test/test_esp32_gpio_hold.py +++ b/test/test_esp32_gpio_hold.py @@ -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<=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 -#include -#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 @@ -471,86 +441,208 @@ int main(){ using namespace mesh::esp32; #include #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 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 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< 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()): diff --git a/test/test_radio_interrupt_recovery.py b/test/test_radio_interrupt_recovery.py index 48a1c4fc8..a985eefa3 100644 --- a/test/test_radio_interrupt_recovery.py +++ b/test/test_radio_interrupt_recovery.py @@ -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() diff --git a/variants/meshnology_w12/MeshnologyW12Board.cpp b/variants/meshnology_w12/MeshnologyW12Board.cpp index d877d2ed1..206010678 100644 --- a/variants/meshnology_w12/MeshnologyW12Board.cpp +++ b/variants/meshnology_w12/MeshnologyW12Board.cpp @@ -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(); } diff --git a/variants/meshnology_w12/MeshnologyW12Board.h b/variants/meshnology_w12/MeshnologyW12Board.h index 4e18b9b4c..d7ed8422a 100644 --- a/variants/meshnology_w12/MeshnologyW12Board.h +++ b/variants/meshnology_w12/MeshnologyW12Board.h @@ -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; diff --git a/variants/meshnology_w12/platformio.ini b/variants/meshnology_w12/platformio.ini index 7f7108b7f..7117ca955 100644 --- a/variants/meshnology_w12/platformio.ini +++ b/variants/meshnology_w12/platformio.ini @@ -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 diff --git a/variants/meshnology_w12/target.cpp b/variants/meshnology_w12/target.cpp index 48c1f4dbc..4b594ba36 100644 --- a/variants/meshnology_w12/target.cpp +++ b/variants/meshnology_w12/target.cpp @@ -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;