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:
1 parent
73c5f572eb
commit
ad34fd3a0d
15 files changed
+514
-137
No files matched your search
@@ -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: |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Vendored
+1
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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()):
|
||||
|
||||
@@ -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()
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user