Reduce Indicator radio expander traffic with guarded IQ reuse
This commit is contained in:
1 parent
5cf42d0ca1
commit
64a928d18f
14 files changed
+851
-40
No files matched your search
@@ -444,6 +444,7 @@ jobs:
|
||||
python3 -B test/test_mercer_mt_flash.py -v
|
||||
python3 -B test/test_mercer_nrf_app_flash.py -v
|
||||
python3 -B test/test_indicator_repeater_hil_recipe.py -v
|
||||
python3 -B test/test_indicator_radio_hal.py -v
|
||||
python3 -B test/test_mota_usb_seeder_hil.py -v
|
||||
python3 -B test/test_mota_espnow_chain_hil.py -v
|
||||
python3 -B test/test_meshtext_auto_radio2_hil.py -v
|
||||
|
||||
@@ -25,7 +25,12 @@
|
||||
#define MC_SX1262_FAST_PACKET_CACHE MC_SX1262_FAST_PROFILE_SWITCH
|
||||
#endif
|
||||
|
||||
#if (MC_SX1262_FAST_FS || MC_SX1262_FAST_PACKET_CACHE) && !MC_SX1262_FAST_PROFILE_SWITCH
|
||||
#ifndef MC_SX1262_FAST_IQ_CACHE
|
||||
// An additional optimization, enabled per board only after hardware testing.
|
||||
#define MC_SX1262_FAST_IQ_CACHE 0
|
||||
#endif
|
||||
|
||||
#if (MC_SX1262_FAST_FS || MC_SX1262_FAST_PACKET_CACHE || MC_SX1262_FAST_IQ_CACHE) && !MC_SX1262_FAST_PROFILE_SWITCH
|
||||
#error "SX1262 SetFs/packet reuse requires the owned fast profile-switch path"
|
||||
#endif
|
||||
|
||||
@@ -75,8 +80,9 @@ class CustomSX1262 : public SX1262 {
|
||||
// Same-image A/B overrides; these default off on every HIL reboot.
|
||||
bool hilUseFsOnRetune = false;
|
||||
bool hilSkipUnchangedPacket = false;
|
||||
bool hilSkipUnchangedIq = false;
|
||||
float hilFrequencyMHz() const { return freqMHz; }
|
||||
uint32_t hilFsEntries = 0, hilPacketSkips = 0;
|
||||
uint32_t hilFsEntries = 0, hilPacketSkips = 0, hilIqWorkaroundSkips = 0;
|
||||
#endif
|
||||
|
||||
int16_t restoreTcxoAfterSleep() {
|
||||
@@ -555,6 +561,38 @@ class CustomSX1262 : public SX1262 {
|
||||
return RADIOLIB_ERR_NONE;
|
||||
}
|
||||
|
||||
#if MC_SX1262_FAST_IQ_CACHE || defined(MC_SX1262_HIL)
|
||||
// Firmware-level direct register/packet writes are not an acknowledged
|
||||
// owned RX tuple, even when they fail or restore the same software IQ.
|
||||
int16_t writeRegister(uint16_t address, const uint8_t* data, uint8_t size) {
|
||||
_profileSwitch.invalidatePacket();
|
||||
return SX1262::writeRegister(address, data, size);
|
||||
}
|
||||
int16_t setPacketParams(uint16_t preamble, uint8_t crc, uint8_t payload,
|
||||
uint8_t header, uint8_t iq) {
|
||||
_profileSwitch.invalidatePacket();
|
||||
return SX1262::setPacketParams(preamble, crc, payload, header, iq);
|
||||
}
|
||||
private:
|
||||
int16_t writeProfilePacketParams(bool reuseIq) {
|
||||
if (!reuseIq) {
|
||||
return setPacketParams(preambleLengthLoRa, crcTypeLoRa, implicitLen,
|
||||
headerType, invertIQEnabled);
|
||||
}
|
||||
// Only the acknowledged IQ workaround is retained. A changed preamble
|
||||
// still needs the COMPLETE ordinary SetPacketParams command. Preserve
|
||||
// its on-wire field order and Module BUSY/status/error handling.
|
||||
const uint8_t data[] = {
|
||||
static_cast<uint8_t>(preambleLengthLoRa >> 8),
|
||||
static_cast<uint8_t>(preambleLengthLoRa), headerType,
|
||||
static_cast<uint8_t>(implicitLen),
|
||||
crcTypeLoRa, invertIQEnabled};
|
||||
return getMod()->SPIwriteStream(RADIOLIB_SX126X_CMD_SET_PACKET_PARAMS,
|
||||
data, sizeof(data));
|
||||
}
|
||||
public:
|
||||
#endif
|
||||
|
||||
int16_t startReceive() override {
|
||||
if (_profileSwitch.canResumeFast()) {
|
||||
// Existing continuous RX already established buffer bases and the
|
||||
@@ -567,14 +605,31 @@ class CustomSX1262 : public SX1262 {
|
||||
skipPacket = skipPacket && _profileSwitch.matchesPacket(
|
||||
preambleLengthLoRa, crcTypeLoRa, implicitLen,
|
||||
headerType, invertIQEnabled);
|
||||
#if MC_SX1262_FAST_IQ_CACHE || defined(MC_SX1262_HIL)
|
||||
bool reuseIq = MC_SX1262_FAST_IQ_CACHE != 0;
|
||||
#ifdef MC_SX1262_HIL
|
||||
reuseIq = hilSkipUnchangedIq;
|
||||
#endif
|
||||
// Capture this capability BEFORE revoking rxValid for the command
|
||||
// sequence. Packet/IQ setters, protocol changes, sleep, reset, TX/CAD,
|
||||
// duty cycling and command failures revoke the acknowledged tuple.
|
||||
reuseIq = reuseIq && _profileSwitch.matchesPacketIq(invertIQEnabled);
|
||||
#endif
|
||||
_profileSwitch.rxValid = false; // fail closed on any command error
|
||||
int16_t rc = clearIrqStatus();
|
||||
#ifdef MC_SX1262_HIL
|
||||
if (rc == RADIOLIB_ERR_NONE && skipPacket) ++hilPacketSkips;
|
||||
#endif
|
||||
if (rc == RADIOLIB_ERR_NONE && !skipPacket) {
|
||||
#if MC_SX1262_FAST_IQ_CACHE || defined(MC_SX1262_HIL)
|
||||
rc = writeProfilePacketParams(reuseIq);
|
||||
#ifdef MC_SX1262_HIL
|
||||
if (rc == RADIOLIB_ERR_NONE && reuseIq) ++hilIqWorkaroundSkips;
|
||||
#endif
|
||||
#else
|
||||
rc = setPacketParams(preambleLengthLoRa, crcTypeLoRa, implicitLen,
|
||||
headerType, invertIQEnabled);
|
||||
#endif
|
||||
_profileSwitch.packetResult(rc, preambleLengthLoRa, crcTypeLoRa,
|
||||
implicitLen, headerType, invertIQEnabled);
|
||||
}
|
||||
|
||||
@@ -64,6 +64,9 @@ struct SX1262ProfileSwitchState {
|
||||
&& packetCrc == crc && packetImplicitLen == implicitLen
|
||||
&& packetHeader == header && packetIq == iq;
|
||||
}
|
||||
bool matchesPacketIq(uint8_t iq) const {
|
||||
return canResumeFast() && packetValid && packetIq == iq;
|
||||
}
|
||||
void packetResult(int16_t result, size_t preamble, uint8_t crc,
|
||||
uint8_t implicitLen, uint8_t header, uint8_t iq) {
|
||||
if (result != 0) {
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
"""Compile the actual Indicator HAL against an error-injectable I2C device."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class IndicatorRadioHalTest(unittest.TestCase):
|
||||
def test_port_zero_transactions_fresh_busy_and_shared_irq_errors(self):
|
||||
arduino = r'''
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
constexpr uint32_t RADIOLIB_NC = 0xffffffff;
|
||||
constexpr int LOW=0, HIGH=1, INPUT_PULLUP=2;
|
||||
constexpr int PIN_BOARD_SDA=39, PIN_BOARD_SCL=40, P_LORA_BUSY=66;
|
||||
extern int nativeLevel, nativeWrites, nativeModes;
|
||||
inline void pinMode(uint32_t, uint32_t) { ++nativeModes; }
|
||||
inline int digitalRead(uint32_t) { return nativeLevel; }
|
||||
'''
|
||||
buffered = r'''
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
#include <SPI.h>
|
||||
struct ArduinoHal {
|
||||
enum { GpioModeOutput=1, GpioModeInput=0, GpioLevelLow=0, GpioLevelHigh=1,
|
||||
GpioInterruptRising=2, GpioInterruptFalling=3 };
|
||||
virtual ~ArduinoHal() = default;
|
||||
virtual void pinMode(uint32_t, uint32_t) { ++nativeModes; }
|
||||
virtual void digitalWrite(uint32_t, uint32_t) { ++nativeWrites; }
|
||||
virtual uint32_t digitalRead(uint32_t) { return nativeLevel; }
|
||||
virtual void attachInterrupt(uint32_t, void (*)(), uint32_t) {}
|
||||
virtual void detachInterrupt(uint32_t) {}
|
||||
virtual uint32_t pinToInterrupt(uint32_t pin) { return pin + 100; }
|
||||
};
|
||||
struct ESP32BufferedRadioHal : ArduinoHal {
|
||||
explicit ESP32BufferedRadioHal(SPIClass&) {}
|
||||
};
|
||||
constexpr uint32_t RISING = ArduinoHal::GpioInterruptRising;
|
||||
'''
|
||||
gfx = r'''
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
namespace lgfx { namespace i2c {
|
||||
struct Result { bool ok; bool has_value() const { return ok; } };
|
||||
Result readRegister(uint8_t, uint8_t, uint8_t, uint8_t*, size_t, uint32_t);
|
||||
Result transactionWrite(uint8_t, uint8_t, const uint8_t*, size_t, uint32_t);
|
||||
Result init(uint8_t, uint8_t, uint8_t);
|
||||
} }
|
||||
'''
|
||||
fixture = r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include "@ROOT@/variants/sensecap_indicator-espnow/IndicatorRadioHal.cpp"
|
||||
int nativeLevel=HIGH, nativeWrites=0, nativeModes=0;
|
||||
uint8_t registers[8] = {0,0,0xff,0xff,0,0,0xff,0xff};
|
||||
struct Transfer { bool write; uint8_t reg; size_t size; };
|
||||
std::vector<Transfer> transfers;
|
||||
bool failRead=false, failWrite=false, failInit=false;
|
||||
unsigned callbacks=0, inits=0;
|
||||
void irq() { ++callbacks; }
|
||||
namespace lgfx { namespace i2c {
|
||||
Result readRegister(uint8_t port,uint8_t address,uint8_t reg,uint8_t* data,
|
||||
size_t size,uint32_t frequency) {
|
||||
assert(port==0 && address==0x20 && frequency==400000);
|
||||
assert((size==1 || size==2) && reg+size<=8);
|
||||
transfers.push_back({false,reg,size});
|
||||
if(failRead) return {false};
|
||||
std::copy(registers+reg,registers+reg+size,data);return {true};
|
||||
}
|
||||
Result transactionWrite(uint8_t port,uint8_t address,const uint8_t* data,
|
||||
size_t size,uint32_t frequency) {
|
||||
assert(port==0 && address==0x20 && frequency==400000);
|
||||
assert((size==2 || size==3) && data[0]+size-1<=8);
|
||||
transfers.push_back({true,data[0],size});
|
||||
if(failWrite) return {false};
|
||||
std::copy(data+1,data+size,registers+data[0]);return {true};
|
||||
}
|
||||
Result init(uint8_t port,uint8_t sda,uint8_t scl) {
|
||||
assert(port==0 && sda==39 && scl==40);++inits;return {!failInit};
|
||||
}
|
||||
} }
|
||||
void last(bool write,uint8_t reg,size_t size) {
|
||||
assert(!transfers.empty());auto t=transfers.back();
|
||||
assert(t.write==write && t.reg==reg && t.size==size);
|
||||
}
|
||||
int main() {
|
||||
SPIClass spi;IndicatorRadioHal hal(spi);
|
||||
assert(hal.beginExpander() && transfers.size()==3);
|
||||
for(auto t:transfers) assert(!t.write && t.size==2);
|
||||
assert(hal.beginExpander() && transfers.size()==3);
|
||||
|
||||
// Low-bank pin updates send only that bank, leaving unrelated Port 1 alone.
|
||||
registers[7]=0x5a;
|
||||
hal.pinMode(64,ArduinoHal::GpioModeOutput);last(true,6,2);
|
||||
assert(registers[6]==0xfe && registers[7]==0x5a);
|
||||
registers[3]=0x5a;
|
||||
hal.digitalWrite(64,ArduinoHal::GpioLevelLow);last(true,2,2);
|
||||
assert(registers[2]==0xfe && registers[3]==0x5a);
|
||||
size_t count=transfers.size();hal.digitalWrite(64,0);
|
||||
assert(transfers.size()==count); // unchanged latch still needs no write
|
||||
|
||||
// Failed writes must not poison the software shadow or suppress the retry.
|
||||
failWrite=true;hal.digitalWrite(64,1);last(true,2,2);
|
||||
assert(registers[2]==0xfe);failWrite=false;
|
||||
hal.digitalWrite(64,1);assert(registers[2]==0xff);
|
||||
failWrite=true;hal.pinMode(65,ArduinoHal::GpioModeOutput);
|
||||
assert(registers[6]==0xfe);failWrite=false;
|
||||
hal.pinMode(65,ArduinoHal::GpioModeOutput);assert(registers[6]==0xfc);
|
||||
|
||||
// Preserve generic Port 1 support and its original whole-word transactions.
|
||||
hal.digitalWrite(72,0);last(true,2,3);assert(registers[3]==0xfe);
|
||||
hal.pinMode(72,ArduinoHal::GpioModeOutput);last(true,6,3);
|
||||
registers[1]=1;assert(hal.digitalRead(72)==1);last(false,0,2);
|
||||
|
||||
// BUSY must be read over I2C every time, even with expander INT inactive.
|
||||
nativeLevel=HIGH;registers[0]=4;
|
||||
assert(hal.digitalRead(66)==1);last(false,0,1);
|
||||
count=transfers.size();registers[0]=0;
|
||||
assert(hal.digitalRead(66)==0 && transfers.size()==count+1);last(false,0,1);
|
||||
#ifdef MC_SX1262_HIL
|
||||
// Only HIL can restore the old two-bank bus-width control in this image.
|
||||
hal.hilUsePort0Only=false;
|
||||
assert(hal.digitalRead(66)==0);last(false,0,2);
|
||||
hal.digitalWrite(64,0);last(true,2,3);
|
||||
hal.pinMode(65,ArduinoHal::GpioModeInput);last(true,6,3);
|
||||
hal.hilUsePort0Only=true;
|
||||
assert(hal.digitalRead(66)==0);last(false,0,1);
|
||||
hal.digitalWrite(64,1);last(true,2,2);
|
||||
hal.pinMode(65,ArduinoHal::GpioModeOutput);last(true,6,2);
|
||||
#endif
|
||||
failRead=true;assert(hal.digitalRead(66)==1 && hal.digitalRead(67)==0);
|
||||
failRead=false;
|
||||
|
||||
// IRQ service still reads BOTH banks, clearing all shared expander sources.
|
||||
hal.attachInterrupt(67,irq,ArduinoHal::GpioInterruptRising);last(false,0,2);
|
||||
registers[0]=8;nativeLevel=LOW;
|
||||
hal.serviceInterrupt();last(false,0,2);assert(callbacks==1);
|
||||
hal.serviceInterrupt();assert(callbacks==1);
|
||||
registers[0]=0;assert(hal.digitalRead(66)==0);assert(callbacks==1);
|
||||
failRead=true;registers[0]=8;hal.serviceInterrupt();assert(callbacks==1);
|
||||
failRead=false;hal.serviceInterrupt();assert(callbacks==2);
|
||||
hal.detachInterrupt(67);count=transfers.size();hal.serviceInterrupt();
|
||||
assert(count==transfers.size());
|
||||
|
||||
// Native and invalid pins do not accidentally address the expander.
|
||||
count=transfers.size();hal.digitalWrite(5,1);hal.pinMode(5,1);
|
||||
assert(hal.digitalRead(5)==LOW && nativeWrites==1);
|
||||
assert(hal.pinToInterrupt(5)==105 && hal.pinToInterrupt(67)==67);
|
||||
assert(hal.digitalRead(80)==0);hal.digitalWrite(80,1);hal.pinMode(80,1);
|
||||
assert(count==transfers.size());
|
||||
|
||||
// Initial bus recovery remains whole-word and fails closed when unavailable.
|
||||
IndicatorRadioHal failed(spi);failRead=true;
|
||||
assert(!failed.beginExpander() && inits==1);
|
||||
assert(failed.digitalRead(66)==1);failRead=false;
|
||||
assert(failed.beginExpander());
|
||||
}
|
||||
'''.replace("@ROOT@", str(ROOT))
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
base = Path(folder)
|
||||
(base / "helpers/radiolib").mkdir(parents=True)
|
||||
(base / "Arduino.h").write_text(arduino)
|
||||
(base / "SPI.h").write_text("#pragma once\nstruct SPIClass {};\n")
|
||||
(base / "LovyanGFX.hpp").write_text(gfx)
|
||||
(base / "helpers/radiolib/ESP32BufferedRadioHal.h").write_text(buffered)
|
||||
source, binary = base / "fixture.cpp", base / "fixture"
|
||||
source.write_text(fixture)
|
||||
for definitions in ([], ["-DMC_SX1262_HIL"]):
|
||||
with self.subTest(definitions=definitions):
|
||||
built = subprocess.run([os.environ.get("CXX", "g++"), "-std=c++17",
|
||||
"-Wall", "-Wextra", "-Werror", "-fsanitize=address,undefined",
|
||||
"-fno-omit-frame-pointer", "-no-pie", "-DSENSECAP_INDICATOR_LORA",
|
||||
*definitions, "-I", str(base), str(source), "-o", str(binary)],
|
||||
capture_output=True, text=True)
|
||||
self.assertEqual(built.returncode, 0, built.stderr)
|
||||
ran = subprocess.run([str(binary)], capture_output=True, text=True)
|
||||
self.assertEqual(ran.returncode, 0, ran.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,161 @@
|
||||
"""Execute HIL fast-policy parsing/admission without touching radio hardware."""
|
||||
from pathlib import Path
|
||||
import configparser
|
||||
import re
|
||||
import unittest
|
||||
|
||||
import test_sx1262_batched_modulation as compiler
|
||||
import test_radio_profile_clock as clock
|
||||
from test_radio_receive_contract import method
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class ProfileFastPolicyTest(unittest.TestCase):
|
||||
compile_run = compiler.BatchedModulationTests.compile_run
|
||||
|
||||
def test_hil_radiolib_matches_current_production_dependency(self):
|
||||
config = configparser.ConfigParser(interpolation=None, strict=False,
|
||||
inline_comment_prefixes=(";",))
|
||||
config.read([ROOT / "platformio.ini", ROOT / "tools/hil/profile_switch.ini"])
|
||||
|
||||
def resolve(section, option, depth=0):
|
||||
self.assertLess(depth, 30, "recursive HIL recipe interpolation")
|
||||
text = config.get(section, option)
|
||||
return re.sub(r"\$\{([^}]+)\}", lambda match:
|
||||
resolve(*match[1].split(".", 1), depth + 1), text)
|
||||
|
||||
pattern = r"https://github\.com/jgromes/RadioLib\.git#[0-9a-f]{40}"
|
||||
production = re.findall(pattern, resolve("arduino_base", "lib_deps"))
|
||||
self.assertEqual(len(production), 1)
|
||||
environments = [section for section in config.sections()
|
||||
if section.startswith("env:profile_switch_")]
|
||||
self.assertGreaterEqual(len(environments), 6)
|
||||
for section in environments:
|
||||
with self.subTest(environment=section):
|
||||
self.assertEqual(re.findall(pattern, resolve(section, "lib_deps")), production)
|
||||
|
||||
def test_policy_selection_is_exact_and_quiet_receiver_only(self):
|
||||
source = (ROOT / "tools/hil/profile_switch.cpp").read_text()
|
||||
policy = method(source, "bool selectProfileFastPolicy(")
|
||||
fixture = r'''
|
||||
#include <cassert>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
struct { bool active=false; } stationaryBaseline,preambleDiagnostic,channelSweep;
|
||||
bool packetListening=false;
|
||||
struct { bool hilUseFsOnRetune=false,hilSkipUnchangedPacket=false,hilSkipUnchangedIq=false; } chip;
|
||||
struct { bool hilUsePort0Only=true; } radioHal;
|
||||
struct SerialMock {
|
||||
std::string text;
|
||||
void println(const char* s) { text=s; }
|
||||
void printf(const char* format,...) {
|
||||
char bytes[256];va_list args;va_start(args,format);
|
||||
vsnprintf(bytes,sizeof(bytes),format,args);va_end(args);text=bytes;
|
||||
}
|
||||
} Serial;
|
||||
@POLICY@
|
||||
int main() {
|
||||
assert(selectProfileFastPolicy("fs 1") && chip.hilUseFsOnRetune);
|
||||
assert(Serial.text=="{\"fs\":true}\n");
|
||||
assert(selectProfileFastPolicy("packetcache 1") && chip.hilSkipUnchangedPacket);
|
||||
assert(Serial.text=="{\"packet_cache\":true}\n");
|
||||
assert(selectProfileFastPolicy("fs 0") && !chip.hilUseFsOnRetune);
|
||||
assert(chip.hilSkipUnchangedPacket);
|
||||
for(const char* invalid:{"fs 2","fs -1","fs 1 junk","fs 1 0","fs",
|
||||
"packetcache 2","packetcache 1 junk","packetcache", "info"}) {
|
||||
Serial.text.clear();assert(!selectProfileFastPolicy(invalid));
|
||||
assert(Serial.text.empty() && !chip.hilUseFsOnRetune && chip.hilSkipUnchangedPacket);
|
||||
}
|
||||
bool* guards[]={&packetListening,&stationaryBaseline.active,
|
||||
&preambleDiagnostic.active,&channelSweep.active};
|
||||
for(bool* guard:guards) {
|
||||
*guard=true;
|
||||
assert(selectProfileFastPolicy("fs 1") && !chip.hilUseFsOnRetune);
|
||||
assert(Serial.text.find("error")!=std::string::npos);
|
||||
assert(selectProfileFastPolicy("packetcache 0") && chip.hilSkipUnchangedPacket);
|
||||
assert(Serial.text.find("error")!=std::string::npos);
|
||||
*guard=false;
|
||||
}
|
||||
assert(selectProfileFastPolicy("packetcache 0") && !chip.hilSkipUnchangedPacket);
|
||||
assert(selectProfileFastPolicy("iqcache 1") && chip.hilSkipUnchangedIq);
|
||||
assert(Serial.text=="{\"iq_cache\":true}\n");
|
||||
for(bool* guard:guards) {
|
||||
*guard=true;assert(selectProfileFastPolicy("iqcache 0"));
|
||||
assert(chip.hilSkipUnchangedIq && Serial.text.find("error")!=std::string::npos);
|
||||
*guard=false;
|
||||
}
|
||||
assert(selectProfileFastPolicy("iqcache 0") && !chip.hilSkipUnchangedIq);
|
||||
for(const char* invalid:{"iqcache 2","iqcache -1","iqcache 1 junk","iqcache"})
|
||||
assert(!selectProfileFastPolicy(invalid) && !chip.hilSkipUnchangedIq);
|
||||
#ifdef HIL_INDICATOR
|
||||
assert(selectProfileFastPolicy("port0 0") && !radioHal.hilUsePort0Only);
|
||||
assert(Serial.text=="{\"port0\":false}\n");
|
||||
for(bool* guard:guards) {
|
||||
*guard=true;assert(selectProfileFastPolicy("port0 1"));
|
||||
assert(!radioHal.hilUsePort0Only && Serial.text.find("error")!=std::string::npos);
|
||||
*guard=false;
|
||||
}
|
||||
assert(selectProfileFastPolicy("port0 1") && radioHal.hilUsePort0Only);
|
||||
for(const char* invalid:{"port0 2","port0 -1","port0 0 junk","port0"})
|
||||
assert(!selectProfileFastPolicy(invalid) && radioHal.hilUsePort0Only);
|
||||
#else
|
||||
assert(!selectProfileFastPolicy("port0 0") && radioHal.hilUsePort0Only);
|
||||
#endif
|
||||
}
|
||||
'''.replace("@POLICY@", policy)
|
||||
self.compile_run(fixture)
|
||||
self.compile_run("#define HIL_INDICATOR\n" + fixture)
|
||||
|
||||
def test_receive_timestamp_retains_esp32_microsecond_units_and_wrap(self):
|
||||
source = (ROOT / "tools/hil/profile_switch.cpp").read_text()
|
||||
getter = method(source, "uint32_t receiveStartedUs()")
|
||||
self.assertNotIn("_profile_visit_us", getter)
|
||||
self.assertIn("#if !defined(ESP32_PLATFORM)", getter)
|
||||
self.assertIn('#error "profile_switch.cpp requires the ESP32 microsecond profile clock"', getter)
|
||||
fixture = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#define ESP32_PLATFORM
|
||||
uint32_t clockUs=0;
|
||||
uint32_t micros() { return clockUs; }
|
||||
@CLOCK@
|
||||
struct BenchWrapper { uint32_t _profile_visit_stamp=0; @GETTER@ };
|
||||
int main() {
|
||||
BenchWrapper driver;
|
||||
clockUs=0xfffffff0;
|
||||
driver._profile_visit_stamp=profileTimestamp(true);
|
||||
assert(driver.receiveStartedUs()==clockUs);
|
||||
clockUs=0x15;
|
||||
assert(uint32_t(micros()-driver.receiveStartedUs())==37);
|
||||
assert(profileElapsedUs(driver.receiveStartedUs(),profileTimestamp(true),true)==37);
|
||||
clockUs=1234567;driver._profile_visit_stamp=profileTimestamp(false);
|
||||
assert(driver.receiveStartedUs()==1234567);
|
||||
}
|
||||
'''.replace("@CLOCK@", clock.CLOCK).replace("@GETTER@", getter)
|
||||
self.compile_run(fixture)
|
||||
|
||||
def test_timing_and_rf_readiness_report_exercised_fast_operations(self):
|
||||
sources = (("profile_switch.cpp", "void run("),
|
||||
("profile_switch_packets.h", "void listenPacket("))
|
||||
for name, signature in sources:
|
||||
with self.subTest(source=name):
|
||||
source = (ROOT / "tools/hil" / name).read_text()
|
||||
body = method(source, signature)
|
||||
for field in ("fs", "packet_cache", "fs_entries", "packet_skips", "iq_cache", "iq_workaround_skips"):
|
||||
self.assertIn('\\"' + field + '\\"', body)
|
||||
self.assertIn("chip.hilFsEntries-fsStart", body)
|
||||
self.assertIn("chip.hilPacketSkips-packetStart", body)
|
||||
self.assertIn("chip.hilUseFsOnRetune", body)
|
||||
self.assertIn("chip.hilSkipUnchangedPacket", body)
|
||||
self.assertIn("chip.hilIqWorkaroundSkips-iqStart", body)
|
||||
chip = (ROOT / "src/helpers/radiolib/CustomSX1262.h").read_text()
|
||||
self.assertIn("bool hilUseFsOnRetune = false;", chip)
|
||||
self.assertIn("bool hilSkipUnchangedPacket = false;", chip)
|
||||
self.assertIn("bool hilSkipUnchangedIq = false;", chip)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,253 @@
|
||||
"""Execute owned IQ-workaround reuse, wire bytes, revocation and failures."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_radio_receive_contract import method
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class ProfileIqCacheTest(unittest.TestCase):
|
||||
def test_only_qualified_indicator_recipe_opts_in(self):
|
||||
opt_ins = []
|
||||
for recipe in (ROOT / "variants").glob("*/platformio.ini"):
|
||||
if re.search(r"^\s*-D\s+MC_SX1262_FAST_IQ_CACHE=1\s*$",
|
||||
recipe.read_text(), re.MULTILINE):
|
||||
opt_ins.append(recipe.relative_to(ROOT).as_posix())
|
||||
self.assertEqual(opt_ins, ["variants/sensecap_indicator-espnow/platformio.ini"])
|
||||
|
||||
def compile_run(self, source):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
cpp, binary = Path(folder) / "test.cpp", Path(folder) / "test"
|
||||
cpp.write_text(source, encoding="ascii")
|
||||
built = subprocess.run([os.environ.get("CXX", "g++"), "-std=c++17",
|
||||
"-Wall", "-Wextra", "-fsanitize=address,undefined",
|
||||
"-fno-omit-frame-pointer", "-no-pie", str(cpp), "-o", str(binary)],
|
||||
capture_output=True, text=True)
|
||||
self.assertEqual(built.returncode, 0, built.stderr)
|
||||
ran = subprocess.run([str(binary)], capture_output=True, text=True)
|
||||
self.assertEqual(ran.returncode, 0, ran.stderr)
|
||||
|
||||
def test_owned_iq_ack_wire_payload_setters_lifecycle_and_errors(self):
|
||||
source = (ROOT / "src/helpers/radiolib/CustomSX1262.h").read_text()
|
||||
state = (ROOT / "src/helpers/radiolib/SX1262ProfileSwitchState.h").read_text().replace("#pragma once", "")
|
||||
signatures = ("void setProfileSwitchOptimization(", "void beginProfileSwitch(",
|
||||
"void endProfileSwitch(", "int16_t reset(bool", "int16_t sleep(bool",
|
||||
"int16_t stageMode(", "int16_t setPreambleLength(", "int16_t setCRC(",
|
||||
"int16_t implicitHeader(", "int16_t explicitHeader()", "int16_t invertIQ(",
|
||||
"int16_t setProfileProtocol(", "int16_t writeRegister(",
|
||||
"int16_t setPacketParams(", "int16_t writeProfilePacketParams(",
|
||||
"int16_t startReceive()", "int16_t startReceiveDutyCycle(")
|
||||
methods = "\n".join(method(source, signature).replace(" override", "") for signature in signatures)
|
||||
fixture = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@DEFINES@
|
||||
#define MC_SX1262_FAST_PACKET_CACHE 0
|
||||
#define RADIOLIB_ERR_NONE 0
|
||||
#define RADIOLIB_SX126X_STANDBY_RC 0
|
||||
#define RADIOLIB_SX126X_RX_TIMEOUT_INF 0xffffff
|
||||
#define RADIOLIB_SX126X_PACKET_TYPE_LORA 1
|
||||
#define RADIOLIB_SX126X_CMD_SET_PACKET_PARAMS 0x8c
|
||||
#define RADIOLIB_RADIO_MODE_NONE 0
|
||||
#define RADIOLIB_IRQ_RX_DEFAULT_FLAGS 1
|
||||
#define RADIOLIB_IRQ_RX_DEFAULT_MASK 2
|
||||
#define RADIOLIB_IRQ_PREAMBLE_DETECTED 3
|
||||
#define RADIOLIB_SX126X_LORA_CRC_ON 1
|
||||
#define RADIOLIB_SX126X_LORA_HEADER_EXPLICIT 0
|
||||
#define RADIOLIB_SX126X_LORA_IQ_STANDARD 0
|
||||
using RadioModeType_t=int;
|
||||
using RadioLibIrqFlags_t=uint32_t;
|
||||
struct RadioModeConfig_t {};
|
||||
@STATE@
|
||||
struct Module {
|
||||
enum { MODE_RX=1 };
|
||||
std::string* trace;char* fail;std::vector<uint8_t> packet;
|
||||
int16_t SPIwriteStream(uint16_t command,const uint8_t* data,size_t size) {
|
||||
assert(command==0x8c && size==6);packet.assign(data,data+size);
|
||||
*trace+='P';return *fail=='P' ? -100:0;
|
||||
}
|
||||
void setRfSwitchState(int mode) { assert(mode==MODE_RX);*trace+='W'; }
|
||||
};
|
||||
struct SX1262 {
|
||||
std::string trace;char fail=0;int modem=1;bool standbyXOSC=true;
|
||||
uint16_t preambleLengthLoRa=32;
|
||||
uint8_t crcTypeLoRa=1,headerType=0,invertIQEnabled=0;
|
||||
size_t implicitLen=255;
|
||||
uint32_t rxTimeout=0;int stagedMode=3;float tcxoVoltage=1.8f;
|
||||
Module module{&trace,&fail,{}};Module* mod=&module;
|
||||
int16_t call(char c) { trace+=c;return c==fail ? -100:0; }
|
||||
Module* getMod() { return mod; }
|
||||
int16_t standby(uint8_t) { return call('S'); }
|
||||
int16_t sleep(bool) { return call('L'); }
|
||||
int16_t reset(bool) { return call('Z'); }
|
||||
virtual int16_t stageMode(RadioModeType_t,RadioModeConfig_t*) { return call('G'); }
|
||||
int16_t setPacketParams(uint16_t p,uint8_t crc,uint8_t len,uint8_t hdr,uint8_t iq) {
|
||||
int16_t rc=call('I');if(!rc) rc=call('i');
|
||||
const uint8_t data[]={uint8_t(p>>8),uint8_t(p),hdr,len,crc,iq};
|
||||
if(!rc) rc=module.SPIwriteStream(0x8c,data,sizeof(data));return rc;
|
||||
}
|
||||
virtual int16_t setPreambleLength(size_t p) {
|
||||
preambleLengthLoRa=p;return setPacketParams(p,crcTypeLoRa,implicitLen,headerType,invertIQEnabled);
|
||||
}
|
||||
int16_t setCRC(uint8_t crc,uint16_t,uint16_t,bool) {
|
||||
crcTypeLoRa=crc;return setPacketParams(preambleLengthLoRa,crc,implicitLen,headerType,invertIQEnabled);
|
||||
}
|
||||
int16_t implicitHeader(size_t n) {
|
||||
implicitLen=n;headerType=1;
|
||||
return setPacketParams(preambleLengthLoRa,crcTypeLoRa,implicitLen,headerType,invertIQEnabled);
|
||||
}
|
||||
int16_t explicitHeader() {
|
||||
implicitLen=255;headerType=0;
|
||||
return setPacketParams(preambleLengthLoRa,crcTypeLoRa,implicitLen,headerType,invertIQEnabled);
|
||||
}
|
||||
virtual int16_t invertIQ(bool enable) {
|
||||
invertIQEnabled=enable;
|
||||
return setPacketParams(preambleLengthLoRa,crcTypeLoRa,implicitLen,headerType,invertIQEnabled);
|
||||
}
|
||||
int16_t setSyncWord(uint8_t) { return call('Y'); }
|
||||
int16_t writeRegister(uint16_t,const uint8_t*,uint8_t) { return call('J'); }
|
||||
int16_t startReceive(uint32_t,uint32_t,uint32_t,size_t) {
|
||||
int16_t rc=stageMode(1,nullptr);if(!rc) rc=call('F');
|
||||
if(!rc) rc=setPacketParams(preambleLengthLoRa,crcTypeLoRa,implicitLen,headerType,invertIQEnabled);
|
||||
if(!rc) rc=call('R');return rc;
|
||||
}
|
||||
int16_t startReceiveDutyCycle(uint32_t,uint32_t,uint32_t,uint32_t) { return call('D'); }
|
||||
uint8_t getPacketType() { call('Q');return modem; }
|
||||
int16_t clearIrqStatus() { return call('C'); }
|
||||
int16_t setRx(uint32_t timeout) { assert(timeout==0xffffff);return call('R'); }
|
||||
};
|
||||
struct Radio : SX1262 {
|
||||
SX1262ProfileSwitchState _profileSwitch{true};
|
||||
bool _coldStandby=true,_tcxoWakePending=false,_rx_ps_rf_rx_disabled=false;
|
||||
uint32_t _profileFastResumes=0;
|
||||
#ifdef MC_SX1262_HIL
|
||||
bool hilSkipUnchangedPacket=false,hilSkipUnchangedIq=true;
|
||||
uint32_t hilPacketSkips=0,hilIqWorkaroundSkips=0;
|
||||
#endif
|
||||
@METHODS@
|
||||
};
|
||||
void prime(Radio& r) {
|
||||
assert(r.startReceive()==0);
|
||||
assert(r.trace.find("IiP")!=std::string::npos && r.trace.back()=='Q');
|
||||
assert(r._profileSwitch.rxValid && r._profileSwitch.packetValid);
|
||||
assert(!r._profileSwitch.matchesPacketIq(r.invertIQEnabled));
|
||||
r.trace.clear();
|
||||
}
|
||||
void arm(Radio& r) {
|
||||
r.beginProfileSwitch(true);
|
||||
assert(!r._profileSwitch.matchesPacketIq(r.invertIQEnabled));
|
||||
r._profileSwitch.standbyResult(0);
|
||||
}
|
||||
int main() {
|
||||
// Only IQ is reused: changing every other packet field still emits its full
|
||||
// six-byte command in the exact SX126x wire order, including both pre bytes.
|
||||
for(uint8_t iq:{0,1}) {
|
||||
Radio r;r.invertIQEnabled=iq;prime(r);arm(r);
|
||||
assert(r._profileSwitch.matchesPacketIq(iq));
|
||||
r.preambleLengthLoRa=0x1234;r.crcTypeLoRa=2;r.implicitLen=73;r.headerType=1;
|
||||
assert(r.startReceive()==0 && r.trace=="CPWR");
|
||||
assert(r.module.packet==std::vector<uint8_t>({0x12,0x34,1,73,2,iq}));
|
||||
assert(r._profileSwitch.packetValid && r._profileSwitch.packetPreamble==0x1234);
|
||||
r.endProfileSwitch(true);
|
||||
}
|
||||
// A changed IQ or an unacknowledged packet must run the original workaround.
|
||||
for(int gate=0;gate<3;++gate) {
|
||||
Radio r;prime(r);arm(r);
|
||||
if(gate==0) r.invertIQEnabled=1;
|
||||
if(gate==1) r._profileSwitch.invalidatePacket();
|
||||
#ifdef MC_SX1262_HIL
|
||||
if(gate==2) r.hilSkipUnchangedIq=false;
|
||||
#else
|
||||
if(gate==2) continue;
|
||||
#endif
|
||||
assert(r.startReceive()==0 && r.trace=="CIiPWR");
|
||||
}
|
||||
// Owned preamble staging is the intended fast case, unlike an ordinary
|
||||
// preamble command. Every ordinary packet/direct-register path revokes IQ.
|
||||
{ Radio r;prime(r);arm(r);assert(r.setPreambleLength(64)==0 && r.trace.empty());
|
||||
assert(r.startReceive()==0 && r.trace=="CPWR"); }
|
||||
for(int setter=0;setter<8;++setter) for(bool failure:{false,true}) {
|
||||
Radio r;prime(r);
|
||||
if(setter!=4) arm(r);
|
||||
r.fail=failure ? (setter==5?'Y':setter==6?'J':'P'):0;
|
||||
uint8_t value=4;
|
||||
switch(setter) {
|
||||
case 0:r.setCRC(2);break;
|
||||
case 1:r.implicitHeader(73);break;
|
||||
case 2:r.explicitHeader();break;
|
||||
case 3:r.invertIQ(false);break;
|
||||
case 4:r.setPreambleLength(64);break;
|
||||
case 5:r.setProfileProtocol(0x2b);break;
|
||||
case 6:r.writeRegister(0x736,&value,1);break;
|
||||
case 7:r.setPacketParams(64,1,255,0,0);break;
|
||||
}
|
||||
assert(!r._profileSwitch.packetValid);
|
||||
r.fail=0;r.trace.clear();
|
||||
if(failure && setter==5) {
|
||||
assert(!r._profileSwitch.rxValid);continue;
|
||||
}
|
||||
if(setter==4) arm(r);
|
||||
assert(!r._profileSwitch.matchesPacketIq(r.invertIQEnabled));
|
||||
assert(r.startReceive()==0 && r.trace=="CIiPWR");
|
||||
}
|
||||
// The acknowledged tuple cannot survive TX/CAD staging, either sleep,
|
||||
// reset, RX duty cycling, explicit disable, rollback, or a failed RX command.
|
||||
for(int event=0;event<8;++event) for(bool failure:{false,true}) {
|
||||
Radio r;prime(r);arm(r);r.fail=failure?'G':0;
|
||||
switch(event) {
|
||||
case 0:r.stageMode(2,nullptr);break;
|
||||
case 1:r.stageMode(3,nullptr);break;
|
||||
case 2:r.sleep(true);break;
|
||||
case 3:r.sleep(false);break;
|
||||
case 4:r.reset();break;
|
||||
case 5:r.startReceiveDutyCycle(1000,1000);break;
|
||||
case 6:r.setProfileSwitchOptimization(false);break;
|
||||
case 7:r.endProfileSwitch(false);break;
|
||||
}
|
||||
assert(!r._profileSwitch.packetValid && !r._profileSwitch.rxValid);
|
||||
assert(!r._profileSwitch.matchesPacketIq(r.invertIQEnabled));
|
||||
}
|
||||
for(char failure:std::string("CIiPR")) {
|
||||
Radio r;prime(r);arm(r);
|
||||
if(failure=='I' || failure=='i') r._profileSwitch.invalidatePacket();
|
||||
r.fail=failure;assert(r.startReceive()==-100 && r.trace.back()==failure);
|
||||
assert(!r._profileSwitch.rxValid && !r._profileSwitch.packetValid);
|
||||
r.endProfileSwitch(false);r.fail=0;r.trace.clear();prime(r);
|
||||
}
|
||||
for(int gate=0;gate<4;++gate) {
|
||||
Radio r;prime(r);
|
||||
switch(gate) {
|
||||
case 0:r.beginProfileSwitch(false);r._profileSwitch.standbyResult(0);break;
|
||||
case 1:r.standbyXOSC=false;arm(r);break;
|
||||
case 2:r.beginProfileSwitch(true);break;
|
||||
case 3:r._profileSwitch.packetResult(-100,32,1,255,0,0);break;
|
||||
}
|
||||
assert(!r._profileSwitch.matchesPacketIq(0));
|
||||
}
|
||||
}
|
||||
'''.replace("@STATE@", state).replace("@METHODS@", methods)
|
||||
for definitions in ("#define MC_SX1262_FAST_IQ_CACHE 1",
|
||||
"#define MC_SX1262_FAST_IQ_CACHE 0\n#define MC_SX1262_HIL"):
|
||||
with self.subTest(definitions=definitions):
|
||||
self.compile_run(fixture.replace("@DEFINES@", definitions))
|
||||
|
||||
def test_unqualified_boards_keep_the_original_live_modem_query_and_iq_path(self):
|
||||
source = (ROOT / "src/helpers/radiolib/CustomSX1262.h").read_text()
|
||||
self.assertIn("#define MC_SX1262_FAST_IQ_CACHE 0", source)
|
||||
modulation = method(source, "int16_t setLoRaModulationParams(")
|
||||
self.assertIn("if (getPacketType() != RADIOLIB_SX126X_PACKET_TYPE_LORA)", modulation)
|
||||
helper = method(source, "int16_t writeProfilePacketParams(")
|
||||
self.assertIn("return getMod()->SPIwriteStream(RADIOLIB_SX126X_CMD_SET_PACKET_PARAMS,", helper)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -59,7 +59,7 @@ struct Radio {
|
||||
int16_t result=0;
|
||||
uint8_t getPacketType() { ++queries; return packetType; }
|
||||
int16_t setModulationParams(uint8_t sf,uint8_t bw,uint8_t cr,uint8_t ldro) {
|
||||
// RadioLib 187ef247 reads these caches, not just the command arguments,
|
||||
// The pinned RadioLib reads these caches, not just the command arguments,
|
||||
// when deciding LDRO. Assert they are synchronized before that call.
|
||||
assert(spreadingFactor==sf && bandwidth==bw && codingRate==cr);
|
||||
if (ldroAuto) ldrOptimize=((1U<<spreadingFactor)/bandwidthKhz)>=16.0f;
|
||||
|
||||
@@ -75,7 +75,13 @@ class BenchWrapper : public CustomSX1262Wrapper {
|
||||
uint32_t detourFirstMod=0,detourFinalMod=0;
|
||||
uint32_t lastFirstPassUs=0,lastSecondPassUs=0,secondPassBlocked=0;
|
||||
using CustomSX1262Wrapper::CustomSX1262Wrapper;
|
||||
uint32_t receiveStartedUs() const { return _profile_visit_us; }
|
||||
uint32_t receiveStartedUs() const {
|
||||
#if !defined(ESP32_PLATFORM)
|
||||
#error "profile_switch.cpp requires the ESP32 microsecond profile clock"
|
||||
#endif
|
||||
// ESP32 profile stamps are micros(), not the nRF52 RTC2 tick clock.
|
||||
return _profile_visit_stamp;
|
||||
}
|
||||
mesh::RadioParamApplyResult hop(uint8_t target) {
|
||||
const bool detour=firstPassDetour && hopPasses==2;
|
||||
const auto desired=_profiles.params(target);
|
||||
@@ -231,6 +237,9 @@ void run(bool warm, int sf, unsigned count, bool batched, unsigned mask, unsigne
|
||||
uint32_t skipped = 0, failed = 0, modeErrors = 0, cacheErrors = 0, timeouts = 0, completed = 0;
|
||||
const uint32_t start = millis();
|
||||
const uint32_t fastStart = chip.getOptimizedProfileSwitches();
|
||||
const uint32_t fsStart = chip.hilFsEntries;
|
||||
const uint32_t packetStart = chip.hilPacketSkips;
|
||||
const uint32_t iqStart = chip.hilIqWorkaroundSkips;
|
||||
for (unsigned attempt = 0; completed < count + 8 && attempt < count * 4 + 32; ++attempt) {
|
||||
if (uint32_t(millis() - start) > 30000) break;
|
||||
drain();
|
||||
@@ -261,10 +270,19 @@ void run(bool warm, int sf, unsigned count, bool batched, unsigned mask, unsigne
|
||||
lastRunResult.text[0]=0;
|
||||
appendBenchResult(lastRunResult,"{\"result\":%u,\"warm\":%s,\"batched\":%s,\"mask\":%u,\"spi_mhz\":%u,\"sf500\":%d,\"requested\":%u,"
|
||||
"\"skipped_busy\":%u,\"failures\":%u,\"busy_timeouts\":%u,\"rx_mode_errors\":%u,"
|
||||
"\"cache_errors\":%u,\"tcxo_delay_us\":%u,\"optimized_rx_resumes\":%u,\"directions\":[",
|
||||
"\"cache_errors\":%u,\"tcxo_delay_us\":%u,\"optimized_rx_resumes\":%u,"
|
||||
"\"fs\":%s,\"packet_cache\":%s,\"fs_entries\":%u,\"packet_skips\":%u,"
|
||||
"\"iq_cache\":%s,\"iq_workaround_skips\":%u,",
|
||||
sequence,warm ? "true" : "false", batched ? "true" : "false", mask, spiMHz, sf, count,
|
||||
skipped, failed, timeouts, modeErrors, cacheErrors,
|
||||
unsigned(chip.tcxoDelay),unsigned(chip.getOptimizedProfileSwitches()-fastStart));
|
||||
unsigned(chip.tcxoDelay),unsigned(chip.getOptimizedProfileSwitches()-fastStart),
|
||||
chip.hilUseFsOnRetune ? "true" : "false", chip.hilSkipUnchangedPacket ? "true" : "false",
|
||||
unsigned(chip.hilFsEntries-fsStart),unsigned(chip.hilPacketSkips-packetStart),
|
||||
chip.hilSkipUnchangedIq ? "true" : "false",unsigned(chip.hilIqWorkaroundSkips-iqStart));
|
||||
#ifdef HIL_INDICATOR
|
||||
appendBenchResult(lastRunResult,"\"port0\":%s,",radioHal.hilUsePort0Only ? "true" : "false");
|
||||
#endif
|
||||
appendBenchResult(lastRunResult,"\"directions\":[");
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
if (i) appendBenchResult(lastRunResult,",");
|
||||
appendBenchResult(lastRunResult,"{\"to_profile\":%d,", i);
|
||||
@@ -293,6 +311,43 @@ void run(bool warm, int sf, unsigned count, bool batched, unsigned mask, unsigne
|
||||
#include "profile_preamble_diagnostic.h"
|
||||
#include "profile_stationary_baseline.h"
|
||||
|
||||
// HIL-only, same-image controls for the existing production state machine.
|
||||
// They do not relax ownership, BUSY polling, or command-status checks, and
|
||||
// CustomSX1262 resets all three fast policies to false on benchmark reboot.
|
||||
bool selectProfileFastPolicy(const char* line) {
|
||||
unsigned value;
|
||||
char trailing;
|
||||
unsigned policy;
|
||||
if (sscanf(line, "fs %u %c", &value, &trailing) == 1 && value <= 1) {
|
||||
policy = 0;
|
||||
} else if (sscanf(line, "packetcache %u %c", &value, &trailing) == 1
|
||||
&& value <= 1) {
|
||||
policy = 1;
|
||||
} else if (sscanf(line, "iqcache %u %c", &value, &trailing) == 1 && value <= 1) {
|
||||
policy = 3;
|
||||
#ifdef HIL_INDICATOR
|
||||
} else if (sscanf(line, "port0 %u %c", &value, &trailing) == 1 && value <= 1) {
|
||||
policy = 2;
|
||||
#endif
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if (packetListening || stationaryBaseline.active || preambleDiagnostic.active
|
||||
|| channelSweep.active) {
|
||||
Serial.println("{\"error\":\"stop receiver before changing fast policy\"}");
|
||||
} else {
|
||||
if (policy == 0) chip.hilUseFsOnRetune = value;
|
||||
else if (policy == 1) chip.hilSkipUnchangedPacket = value;
|
||||
else if (policy == 3) chip.hilSkipUnchangedIq = value;
|
||||
#ifdef HIL_INDICATOR
|
||||
else radioHal.hilUsePort0Only = value;
|
||||
#endif
|
||||
Serial.printf("{\"%s\":%s}\n", policy == 0 ? "fs" : policy == 1 ? "packet_cache" : policy == 3 ? "iq_cache" : "port0",
|
||||
value ? "true" : "false");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void setup() {
|
||||
#ifndef HIL_INDICATOR
|
||||
Serial.setTxBufferSize(4096);
|
||||
@@ -455,6 +510,8 @@ void loop() {
|
||||
stopChannelSweep();
|
||||
} else if (channelSweep.active && strcmp(line,"info") && strncmp(line,"result ",7) && strcmp(line,"reboot")) {
|
||||
Serial.println("{\"error\":\"stop channel sweep before other experiments\"}");
|
||||
} else if (selectProfileFastPolicy(line)) {
|
||||
// The helper also rejects an active single-packet probe.
|
||||
} else if (sscanf(line, "run %d %d %u %d %u %u %u", &warm, &sf, &count, &batched, &mask, &spiMHz, &seq) == 7 && seq
|
||||
&& (warm == 0 || warm == 1) && (batched == 0 || batched == 1)
|
||||
&& mask <= 1023 && (spiMHz == 2 || spiMHz == 4 || spiMHz == 8)
|
||||
|
||||
@@ -8,10 +8,12 @@ platform = platformio/espressif32@6.11.0
|
||||
board = seeed_xiao_esp32s3
|
||||
; Native USB ignores UART baud; avoid the legacy uploader's baud-change step.
|
||||
upload_speed = 115200
|
||||
; Match production's RadioLib pin (host recipe test prevents drift), without
|
||||
; pulling unrelated RTC/I2C-device dependencies into this radio-only harness.
|
||||
lib_deps =
|
||||
SPI
|
||||
Wire
|
||||
https://github.com/jgromes/RadioLib.git#187ef24791c3d844939b2be13a68bd890bd04e4c
|
||||
https://github.com/jgromes/RadioLib.git#2325a38a89dd0f0e402c19f67fe910111da1e246
|
||||
rweather/Crypto @ ^0.4.0
|
||||
extra_scripts =
|
||||
pre:scripts/esp32_wifi_scan_fix.py
|
||||
|
||||
@@ -84,9 +84,9 @@ into a matched A/B comparison; executable layout also affects small timings.
|
||||
|
||||
These frequencies do not exercise image recalibration for a large frequency
|
||||
jump. Other boards, especially ones with I/O expanders, require separate tests.
|
||||
The production 6 ms switch budget and 4 ms loop budget remain unchanged.
|
||||
The Indicator still exceeds 6 ms; do not shorten common preambles based on
|
||||
the XIAO result. Timing the wrapper excludes the full application's scheduling.
|
||||
Production uses the on-device switch self-test plus its 10 percent guard and
|
||||
the separate loop allowance. Do not shorten common preambles based on another
|
||||
board's result. Timing the wrapper excludes the full application's scheduling.
|
||||
|
||||
## Production implementation and recovery (V8)
|
||||
|
||||
@@ -111,11 +111,71 @@ fixture, not separately qualified for every board now using them.
|
||||
|
||||
Fast RX reuses IRQ mapping and buffer bases only after successful continuous
|
||||
LoRa RX, within an owned warm retune. It retains stale-IRQ clearing, RF-switch
|
||||
control, SetRx and BUSY waits. Packet-parameter/IQ programming remains on the
|
||||
Indicator and on any changed or untrusted packet tuple. TX, CAD, ordinary
|
||||
control, SetRx and BUSY waits. Full packet-parameter programming remains on the
|
||||
Indicator; only matching, acknowledged IQ workaround state may be reused there.
|
||||
Changed or untrusted IQ uses the original workaround. TX, CAD, ordinary
|
||||
standby, sleep, reset, duty cycling and failed commands invalidate reuse. A
|
||||
failed RX resume rolls back the old tuple/profile through full setup.
|
||||
|
||||
The HIL-only `fs 0|1` and `packetcache 0|1` commands now select these two
|
||||
existing shortcuts independently in the same executable, including on the
|
||||
Indicator. Both start disabled on every HIL reboot. Set them before `run`,
|
||||
`listen`, or a channel sweep; an active receiver rejects policy changes.
|
||||
Timing and packet-readiness replies include `fs`, `packet_cache`, `fs_entries`,
|
||||
and `packet_skips`. Require the corresponding counters to advance before
|
||||
claiming that a trial exercised a shortcut. For example, the existing
|
||||
`run 1 7 64 1 768 8 SEQUENCE` trial can be repeated with each policy pair.
|
||||
These benchmark controls do not enable the shortcuts in Indicator production
|
||||
artifacts; on-device status, lifecycle recovery, and packet tests must qualify
|
||||
them before changing that board's defaults.
|
||||
|
||||
The Indicator HAL clocks only Port 0 for its radio-pin reads and writes;
|
||||
all four radio control pins are in that bank. BUSY remains a fresh I2C read,
|
||||
not an interrupt-based cache. Initialization and shared-interrupt servicing
|
||||
still read both banks, and failed BUSY reads remain busy. This saves one
|
||||
unrelated byte per radio-pin transaction without increasing the shared bus
|
||||
above the TCA9535's 400 kHz limit. It does not remove the expander's latency.
|
||||
|
||||
### Stock Indicator qualification (2026-10-17)
|
||||
|
||||
Production enables `MC_SX1262_FAST_IQ_CACHE=1` only for the Indicator. Within an
|
||||
owned RX hop, an acknowledged packet tuple with the same IQ lets RX startup
|
||||
omit RadioLib's redundant IQ-register read/write. It still sends all six bytes
|
||||
of SetPacketParams, including changed preambles, and retains the live modem
|
||||
query, IRQ clearing, RF switching, BUSY waits and command-status checks. Packet
|
||||
setters, direct register/packet writes, protocol changes and lifecycle/error
|
||||
paths revoke reuse. Other boards keep the old path compiled without this
|
||||
additional optimization. Arbitrary base-qualified GODMODE/SPI writes are not
|
||||
part of the owned firmware API; such callers must revoke ownership first.
|
||||
|
||||
Same-image controls `port0 0|1` and `iqcache 0|1` isolated both savings on the
|
||||
stock board, using production RadioLib pin
|
||||
`2325a38a89dd0f0e402c19f67fe910111da1e246`. FS and whole-packet reuse stayed off.
|
||||
Two rounds reversed policy and SF order, comparing primary SF7/BW62.5 at
|
||||
909.5 MHz with secondary SF7/8/9/BW500 at 910.5 MHz, CR4/5 and 8 MHz SPI.
|
||||
Each of 18 trials excluded eight warmups and measured 128 hops, 64 per direction.
|
||||
|
||||
| Policy | Mean total | Maximum total | Measured hops |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Original two-port I2C accesses | 8553.835 us | 8621 us | 768 |
|
||||
| Port 0 only | 7892.758 us | 7951 us | 768 |
|
||||
| Port 0 plus acknowledged IQ reuse | 5999.056 us | 6040 us | 768 |
|
||||
|
||||
The combined reduction was 29.87 percent for these same-protocol pairs. All
|
||||
2,304 timed hops had zero switch, BUSY, RX-mode or cache errors; every IQ trial
|
||||
exercised 136 IQ-workaround skips including warmups. A separate tagged RF check
|
||||
received 36/36 packets bidirectionally against XIAO S3 WIO, across both profiles,
|
||||
SF7/8/9 and 16/64/255-byte payloads at -9 dBm, with no RF retries. The timing
|
||||
image SHA256 was
|
||||
`ae51df8415762c222b56fd61554dea815ced6b6b984c46cc2a1fcdac5d6dc030`.
|
||||
|
||||
Mixed MeshCore/Meshtext protocol changes deliberately lose IQ reuse; do not
|
||||
apply the 29.87 percent figure to them. These are short-range harness results,
|
||||
not packet-error-rate, sensitivity, long-soak, full-UI scheduling or current
|
||||
consumption qualification. Firmware still derives its own preamble allowance.
|
||||
The HIL `iqcache` switch starts off on reboot; timing/readiness replies report
|
||||
`iq_cache` and `iq_workaround_skips` so an unexercised shortcut is not a pass.
|
||||
|
||||
The recovery collector exercises 20 USB reopen cycles, deliberately lost
|
||||
timing/TX replies, stale TX acknowledgements, and 18 radio lifecycle cases: TX, CAD, retained
|
||||
sleep, reset/init, RC-policy sleep and explicitly reconfigured-TCXO sleep,
|
||||
@@ -797,12 +857,12 @@ The checked FS-mode fast profile switch is enabled by default in
|
||||
`CustomLR1110` for all MeshCore LR1110 targets: T1000-E, Wio WM1110,
|
||||
ThinkNode M3/M7/M9, and Minewsemi ME25LS01. Their radio SPI paths now use
|
||||
16 MHz buffered transfers. Each board keeps its existing TCXO startup delay;
|
||||
the 0.6 ms combined-hop goal is **not** a 600 µs TCXO delay. Only the T1000-E
|
||||
the 0.6 ms combined-hop goal is **not** a 600 us TCXO delay. Only the T1000-E
|
||||
has on-device timing validation so far. Other boards require measured retune
|
||||
timing, packet reception, and TX checks before claiming the 0.6 ms goal.
|
||||
|
||||
`profile_switch_t1000_lr1110` uses the production `CustomLR1110Wrapper` and
|
||||
the same 200 µs TCXO/fast-retune settings as the T1000-E variant. It never
|
||||
the same 200 us TCXO/fast-retune settings as the T1000-E variant. It never
|
||||
transmits, loads an identity, or writes saved settings. Its only frequencies
|
||||
are 909.5 and 909.75 MHz; the `mod`, `freq`, `both`, and `preamble` modes change
|
||||
SF7/SF8, frequency, both, or 32/48 preamble symbols respectively. A run first
|
||||
@@ -833,32 +893,32 @@ compares the corrected XOSC standby with the default FS-mode retune.
|
||||
`clear 0|1|2` compares always-clear, never-clear (unsafe diagnostic only), and the
|
||||
default checked-clear path. `inject 1` puts a one-shot RX-timeout IRQ into the
|
||||
next run to verify checked clearing. `tcxo`
|
||||
accepts 50, 100, 150–200 in 10 µs steps, 400, 800, and 1600 µs for RAM-only
|
||||
experiments; reboot restores the compiled 200 µs. Each JSON result reports
|
||||
accepts 50, 100, 150-200 in 10 us steps, 400, 800, and 1600 us for RAM-only
|
||||
experiments; reboot restores the compiled 200 us. Each JSON result reports
|
||||
command counts, busy deferrals, drained test packets, failures, RX/cache/RSSI
|
||||
errors, and both timing directions. The RX-only harness discards pending packets
|
||||
before retrying a deferred hop; otherwise an unprocessed interrupt can stall
|
||||
the test even though production firmware would consume it.
|
||||
|
||||
On the Pi-attached T1000-E, 16 MHz buffered SPI and default 200 µs TCXO,
|
||||
FS-mode retune plus checked IRQ clearing averaged about 0.504–0.508 ms for
|
||||
On the Pi-attached T1000-E, 16 MHz buffered SPI and default 200 us TCXO,
|
||||
FS-mode retune plus checked IRQ clearing averaged about 0.504-0.508 ms for
|
||||
simultaneous frequency/SF changes, 0.384 ms for frequency only, 0.409 ms for SF
|
||||
only, and 0.361 ms for a preamble change. Three boot/run cycles with the final
|
||||
default HIL image each completed 1,008 combined hops; earlier runs completed
|
||||
three more combined cycles plus 1,008 of each single-change mode. No completed
|
||||
run had a radio, cache, RSSI, or BUSY-timeout error. An injected stale timeout
|
||||
IRQ was detected and cleared before RX re-entry. A corrected XOSC standby path
|
||||
with checked IRQ clearing took about 0.568–0.575 ms; always clearing took about
|
||||
with checked IRQ clearing took about 0.568-0.575 ms; always clearing took about
|
||||
0.60 ms. The old RadioLib `STANDBY_XOSC` constant is erroneously zero (RC mode),
|
||||
whose previous combined result was about 0.87 ms at a requested 180 µs. The
|
||||
whose previous combined result was about 0.87 ms at a requested 180 us. The
|
||||
unbuffered 2 MHz RC-standby baseline was about 1.36 ms.
|
||||
|
||||
RadioLib converts the requested TCXO delay to 32.768 kHz ticks by truncating
|
||||
`delay / 30.52`, so requests of 160, 170, and 180 µs all programmed five ticks
|
||||
(about 153 µs), while 200 µs programs six (about 183 µs). The earlier 150 µs
|
||||
trial failed after reset; the 160/170 µs runs developed deferrals, but those
|
||||
`delay / 30.52`, so requests of 160, 170, and 180 us all programmed five ticks
|
||||
(about 153 us), while 200 us programs six (about 183 us). The earlier 150 us
|
||||
trial failed after reset; the 160/170 us runs developed deferrals, but those
|
||||
differences do not establish a precise hardware threshold. With true XOSC or
|
||||
FS-mode retuning, 180 and 200 µs produced indistinguishable warm-hop timing;
|
||||
200 µs remains the selected startup setting. These are room-temperature,
|
||||
FS-mode retuning, 180 and 200 us produced indistinguishable warm-hop timing;
|
||||
200 us remains the selected startup setting. These are room-temperature,
|
||||
single-unit, RX-only retune/command-health measurements, not over-the-air
|
||||
packet reception, TX operation, or voltage/temperature-corner qualification.
|
||||
@@ -28,6 +28,8 @@ void listenPacket(unsigned sf, unsigned target, unsigned seq, unsigned mask, uns
|
||||
chip.productionPath = chip.experiment == 0;
|
||||
chip.setProfileSwitchOptimization((mask & 512) != 0);
|
||||
const uint32_t fastStart=chip.getOptimizedProfileSwitches();
|
||||
const uint32_t fsStart=chip.hilFsEntries, packetStart=chip.hilPacketSkips;
|
||||
const uint32_t iqStart=chip.hilIqWorkaroundSkips;
|
||||
radioHal.bulkTransfer = (mask & 256) != 0;
|
||||
mesh::RadioProfileParams primary;
|
||||
primary.freq=909.5f;primary.bw=62.5f;primary.sf=7;primary.cr=5;
|
||||
@@ -47,8 +49,13 @@ void listenPacket(unsigned sf, unsigned target, unsigned seq, unsigned mask, uns
|
||||
packetSequence=seq;packetTarget=target;packetSf=sf;packetListening=true;
|
||||
packetErrorsAtStart=driver.getPacketsRecvErrors();
|
||||
packetDeadline=millis()+5000;
|
||||
recordBenchResult(lastListenResult,seq,"{\"listening\":%u,\"profile\":%u,\"sf500\":%u,\"preamble\":%u,\"optimized_rx_resumes\":%u}\n",
|
||||
seq,target,sf,driver.profilePreamble(target),unsigned(chip.getOptimizedProfileSwitches()-fastStart));
|
||||
recordBenchResult(lastListenResult,seq,"{\"listening\":%u,\"profile\":%u,\"sf500\":%u,\"preamble\":%u,\"optimized_rx_resumes\":%u,"
|
||||
"\"fs\":%s,\"packet_cache\":%s,\"fs_entries\":%u,\"packet_skips\":%u,"
|
||||
"\"iq_cache\":%s,\"iq_workaround_skips\":%u}\n",
|
||||
seq,target,sf,driver.profilePreamble(target),unsigned(chip.getOptimizedProfileSwitches()-fastStart),
|
||||
chip.hilUseFsOnRetune ? "true" : "false", chip.hilSkipUnchangedPacket ? "true" : "false",
|
||||
unsigned(chip.hilFsEntries-fsStart),unsigned(chip.hilPacketSkips-packetStart),
|
||||
chip.hilSkipUnchangedIq ? "true" : "false",unsigned(chip.hilIqWorkaroundSkips-iqStart));
|
||||
}
|
||||
|
||||
void sendPacket(unsigned sf, unsigned target, unsigned seq, unsigned len, unsigned preamble, int power, unsigned spiMHz, bool bulk) {
|
||||
|
||||
@@ -12,10 +12,11 @@ uint8_t IndicatorRadioHal::expanderIndex(uint32_t pin) {
|
||||
return static_cast<uint8_t>(pin & ~EXPANDER_FLAG);
|
||||
}
|
||||
|
||||
bool IndicatorRadioHal::readRegister(uint8_t reg, uint16_t& value) {
|
||||
uint8_t data[2];
|
||||
bool IndicatorRadioHal::readRegister(uint8_t reg, uint16_t& value,
|
||||
bool firstPortOnly) {
|
||||
uint8_t data[2] = {};
|
||||
if (!lgfx::i2c::readRegister(I2C_PORT, EXPANDER_ADDRESS, reg, data,
|
||||
sizeof(data), I2C_FREQUENCY)
|
||||
firstPortOnly ? 1 : sizeof(data), I2C_FREQUENCY)
|
||||
.has_value()) {
|
||||
return false;
|
||||
}
|
||||
@@ -24,14 +25,15 @@ bool IndicatorRadioHal::readRegister(uint8_t reg, uint16_t& value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IndicatorRadioHal::writeRegister(uint8_t reg, uint16_t value) {
|
||||
bool IndicatorRadioHal::writeRegister(uint8_t reg, uint16_t value,
|
||||
bool firstPortOnly) {
|
||||
uint8_t data[] = {
|
||||
reg,
|
||||
static_cast<uint8_t>(value),
|
||||
static_cast<uint8_t>(value >> 8),
|
||||
};
|
||||
return lgfx::i2c::transactionWrite(I2C_PORT, EXPANDER_ADDRESS, data,
|
||||
sizeof(data), I2C_FREQUENCY)
|
||||
firstPortOnly ? 2 : sizeof(data), I2C_FREQUENCY)
|
||||
.has_value();
|
||||
}
|
||||
|
||||
@@ -58,8 +60,9 @@ bool IndicatorRadioHal::beginExpander() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IndicatorRadioHal::readInputs(uint16_t& value) {
|
||||
if (!beginExpander() || !readRegister(INPUT_REGISTER, value)) return false;
|
||||
bool IndicatorRadioHal::readInputs(uint16_t& value, bool firstPortOnly) {
|
||||
if (!beginExpander()
|
||||
|| !readRegister(INPUT_REGISTER, value, firstPortOnly)) return false;
|
||||
processInterruptLevel(value);
|
||||
return true;
|
||||
}
|
||||
@@ -100,7 +103,9 @@ void IndicatorRadioHal::pinMode(uint32_t pin, uint32_t mode) {
|
||||
} else {
|
||||
next |= 1U << index;
|
||||
}
|
||||
if (next != _config && writeRegister(CONFIG_REGISTER, next)) _config = next;
|
||||
if (next != _config && writeRegister(CONFIG_REGISTER, next, useFirstPortOnly(index))) {
|
||||
_config = next;
|
||||
}
|
||||
}
|
||||
|
||||
void IndicatorRadioHal::digitalWrite(uint32_t pin, uint32_t value) {
|
||||
@@ -113,7 +118,9 @@ void IndicatorRadioHal::digitalWrite(uint32_t pin, uint32_t value) {
|
||||
if (index >= 16 || !beginExpander()) return;
|
||||
uint16_t next = value == GpioLevelHigh ? _output | (1U << index)
|
||||
: _output & ~(1U << index);
|
||||
if (next != _output && writeRegister(OUTPUT_REGISTER, next)) _output = next;
|
||||
if (next != _output && writeRegister(OUTPUT_REGISTER, next, useFirstPortOnly(index))) {
|
||||
_output = next;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t IndicatorRadioHal::digitalRead(uint32_t pin) {
|
||||
@@ -121,7 +128,10 @@ uint32_t IndicatorRadioHal::digitalRead(uint32_t pin) {
|
||||
|
||||
uint8_t index = expanderIndex(pin);
|
||||
uint16_t inputs;
|
||||
if (index >= 16 || !readInputs(inputs)) {
|
||||
// All four radio signals occupy Port 0. Do not clock the unrelated Port 1
|
||||
// byte for each BUSY check. This is still a fresh I2C sample every time;
|
||||
// the expander's INT level is not a safe replacement for reading BUSY.
|
||||
if (index >= 16 || !readInputs(inputs, useFirstPortOnly(index))) {
|
||||
// A failed BUSY read must prevent an unsafe SPI command. Other failed
|
||||
// reads are treated as inactive.
|
||||
return pin == P_LORA_BUSY ? GpioLevelHigh : GpioLevelLow;
|
||||
|
||||
@@ -16,6 +16,10 @@ public:
|
||||
|
||||
bool beginExpander();
|
||||
void serviceInterrupt();
|
||||
#ifdef MC_SX1262_HIL
|
||||
// Same-image bus-width A/B only; production always uses the narrow port.
|
||||
bool hilUsePort0Only = true;
|
||||
#endif
|
||||
|
||||
void pinMode(uint32_t pin, uint32_t mode) override;
|
||||
void digitalWrite(uint32_t pin, uint32_t value) override;
|
||||
@@ -45,8 +49,15 @@ private:
|
||||
|
||||
static bool isExpanderPin(uint32_t pin);
|
||||
static uint8_t expanderIndex(uint32_t pin);
|
||||
bool readRegister(uint8_t reg, uint16_t& value);
|
||||
bool writeRegister(uint8_t reg, uint16_t value);
|
||||
bool readInputs(uint16_t& value);
|
||||
bool useFirstPortOnly(uint8_t index) const {
|
||||
return index < 8
|
||||
#ifdef MC_SX1262_HIL
|
||||
&& hilUsePort0Only
|
||||
#endif
|
||||
;
|
||||
}
|
||||
bool readRegister(uint8_t reg, uint16_t& value, bool firstPortOnly = false);
|
||||
bool writeRegister(uint8_t reg, uint16_t value, bool firstPortOnly = false);
|
||||
bool readInputs(uint16_t& value, bool firstPortOnly = false);
|
||||
void processInterruptLevel(uint16_t inputs);
|
||||
};
|
||||
@@ -95,6 +95,9 @@ build_flags =
|
||||
; enabled owned-RX path, but not the direct-I/O SetFs/packet shortcuts.
|
||||
-D MC_SX1262_FAST_FS=0
|
||||
-D MC_SX1262_FAST_PACKET_CACHE=0
|
||||
; Reuse only acknowledged IQ workaround state, not complete packet params.
|
||||
; Qualified on the stock I2C-expander board; protocol changes revoke reuse.
|
||||
-D MC_SX1262_FAST_IQ_CACHE=1
|
||||
-D MC_SX1262_SPI_HZ=8000000
|
||||
-D RADIO_CLASS=CustomSX1262
|
||||
-D WRAPPER_CLASS=IndicatorSX1262Wrapper
|
||||
|
||||
Reference in new issue
Block a user