Add acknowledged SetFs and packet-cache shortcuts for SX1262, extend the LR1110 FS-mode fast path with buffered SPI, and keep the expander-backed Indicator on its previous path. Include HIL fixtures, validation notes, and regression tests.
297 lines
14 KiB
C++
297 lines
14 KiB
C++
// Application-only Heltec T096 SX1262 experiment. No autonomous TX, identity,
|
|
// saved settings, display, or filesystem. RX frequencies: 909.5/909.75 MHz.
|
|
#include <Arduino.h>
|
|
#include <Adafruit_TinyUSB.h>
|
|
#include <SPI.h>
|
|
#include <helpers/radiolib/CustomSX1262Wrapper.h>
|
|
#include <helpers/radiolib/Nrf52BufferedRadioHal.h>
|
|
#include "ProfileSwitchUsb.h"
|
|
|
|
class BenchBoard : public mesh::MainBoard {
|
|
public:
|
|
uint16_t getBattMilliVolts() override { return 0; }
|
|
const char* getManufacturerName() const override { return "Heltec T096 SX1262 bench"; }
|
|
void reboot() override { NVIC_SystemReset(); }
|
|
uint8_t getStartupReason() const override { return 0; }
|
|
} board;
|
|
|
|
class CountingHal : public Nrf52BufferedRadioHal {
|
|
public:
|
|
explicit CountingHal(SPIClass& bus) : Nrf52BufferedRadioHal(bus, 8000000) {}
|
|
uint32_t spiHz = 8000000;
|
|
uint32_t rfWrites = 0, modWrites = 0, packetWrites = 0;
|
|
uint32_t standbyWrites = 0, fsWrites = 0, clearWrites = 0, rxWrites = 0;
|
|
void setSpiHz(uint32_t hz) {
|
|
spiHz = hz;
|
|
spiSettings = SPISettings(hz, MSBFIRST, SPI_MODE0);
|
|
}
|
|
void spiTransfer(uint8_t* out, size_t len, uint8_t* in) override {
|
|
if (len) {
|
|
switch (out[0]) {
|
|
case 0x86: ++rfWrites; break;
|
|
case 0x8B: ++modWrites; break;
|
|
case 0x8C: ++packetWrites; break;
|
|
case 0x80: ++standbyWrites; break;
|
|
case 0xC1: ++fsWrites; break;
|
|
case 0x02: ++clearWrites; break;
|
|
case 0x82: ++rxWrites; break;
|
|
}
|
|
}
|
|
Nrf52BufferedRadioHal::spiTransfer(out, len, in);
|
|
}
|
|
} radioHal(SPI);
|
|
|
|
CustomSX1262 chip = new Module(&radioHal, P_LORA_NSS, P_LORA_DIO_1,
|
|
P_LORA_RESET, P_LORA_BUSY);
|
|
class BenchWrapper : public CustomSX1262Wrapper {
|
|
public:
|
|
using CustomSX1262Wrapper::CustomSX1262Wrapper;
|
|
mesh::RadioParamApplyResult hop(uint8_t profile) { return tuneProfile(profile); }
|
|
void startRx() { startRecv(); }
|
|
void warm() { setProfileStandbyWarm(true); }
|
|
} driver(chip, board);
|
|
|
|
struct Timing {
|
|
uint32_t n = 0, lo = UINT32_MAX, hi = 0;
|
|
uint64_t sum = 0;
|
|
void add(uint32_t cycles) { ++n; if (cycles < lo) lo = cycles; if (cycles > hi) hi = cycles; sum += cycles; }
|
|
double toUs(uint64_t cycles) const { return double(cycles) * 1000000.0 / SystemCoreClock; }
|
|
double mean() const { return n ? toUs(sum) / n : 0; }
|
|
double min() const { return n ? toUs(lo) : 0; }
|
|
double max() const { return n ? toUs(hi) : 0; }
|
|
};
|
|
|
|
bool ready = false, pilotReady = false;
|
|
int initError = 0;
|
|
uint32_t lastSequence = 0;
|
|
|
|
bool waitBusy(uint32_t limitUs = 20000) {
|
|
const uint32_t start = DWT->CYCCNT;
|
|
while (chip.isChipBusy())
|
|
if (uint32_t(DWT->CYCCNT - start) >= uint64_t(limitUs) * SystemCoreClock / 1000000) return false;
|
|
return true;
|
|
}
|
|
|
|
void drain() {
|
|
uint8_t packet[256] = {};
|
|
driver.recvRaw(packet, sizeof(packet));
|
|
}
|
|
|
|
bool configureProfiles(uint8_t sf, bool varySf) {
|
|
drain();
|
|
mesh::RadioProfileParams primary;
|
|
primary.freq = 909.5f; primary.bw = 125; primary.sf = sf; primary.cr = 5;
|
|
primary.preamble = 32;
|
|
auto applied = driver.trySetPrimaryParams(primary, true);
|
|
for (unsigned retry = 0; applied == mesh::RadioParamApplyResult::BUSY && retry < 20; ++retry) {
|
|
drain(); delay(20);
|
|
applied = driver.trySetPrimaryParams(primary, true);
|
|
}
|
|
if (applied != mesh::RadioParamApplyResult::APPLIED) return false;
|
|
mesh::RadioProfileConfig secondary;
|
|
secondary.params = primary;
|
|
secondary.params.freq = 909.75f;
|
|
if (varySf) secondary.params.sf = sf + 1;
|
|
secondary.mode = mesh::RadioProfileMode::Rx;
|
|
driver.profiles()->setSecondary(secondary, true);
|
|
driver.startRx();
|
|
driver.warm();
|
|
return driver.isInRecvMode() && waitBusy();
|
|
}
|
|
|
|
void run(const char* mode, unsigned count, uint32_t sequence) {
|
|
const bool varySf = !strcmp(mode, "both");
|
|
if ((!varySf && strcmp(mode, "freq")) || !ready || sequence <= lastSequence
|
|
|| count < 16 || count > 1000) {
|
|
Serial.println("{\"error\":\"invalid run\"}"); return;
|
|
}
|
|
lastSequence = sequence;
|
|
pilotReady = false;
|
|
if (!configureProfiles(7, varySf)) { Serial.println("{\"error\":\"setup failed\"}"); return; }
|
|
const uint32_t rf0 = radioHal.rfWrites, mod0 = radioHal.modWrites;
|
|
const uint32_t packet0 = radioHal.packetWrites, standby0 = radioHal.standbyWrites;
|
|
const uint32_t fs0 = radioHal.fsWrites, clear0 = radioHal.clearWrites;
|
|
const uint32_t rx0 = radioHal.rxWrites, fast0 = chip.getOptimizedProfileSwitches();
|
|
const uint32_t skip0 = chip.hilPacketSkips, fsEntry0 = chip.hilFsEntries;
|
|
Timing timing[2];
|
|
uint32_t completed = 0, deferrals = 0, failures = 0, modeErrors = 0;
|
|
uint32_t cacheErrors = 0, rssiErrors = 0, busyTimeouts = 0;
|
|
for (unsigned attempt = 0; completed < count + 8 && attempt < (count + 8) * 4; ++attempt) {
|
|
drain();
|
|
if (!waitBusy()) { ++busyTimeouts; break; }
|
|
delay(10); // host traffic and settling stay outside the timed region
|
|
const uint8_t target = driver.receiveProfile() ^ 1;
|
|
const uint32_t start = DWT->CYCCNT;
|
|
const auto result = driver.hop(target);
|
|
if (result == mesh::RadioParamApplyResult::BUSY) { ++deferrals; drain(); continue; }
|
|
if (result != mesh::RadioParamApplyResult::APPLIED) { ++failures; break; }
|
|
if (!waitBusy()) { ++busyTimeouts; break; }
|
|
const uint32_t elapsed = DWT->CYCCNT - start;
|
|
if (sx126xReceiveMode(&chip) != 1) ++modeErrors;
|
|
if (chip.hilFrequencyMHz() != (target ? 909.75f : 909.5f)
|
|
|| driver.getSpreadingFactor() != (target && varySf ? 8 : 7)) ++cacheErrors;
|
|
delay(7);
|
|
const float rssi = driver.getCurrentRSSI();
|
|
if (!isfinite(rssi) || rssi < -180 || rssi > 20) ++rssiErrors;
|
|
if (completed++ >= 8) timing[target].add(elapsed);
|
|
}
|
|
driver.profiles()->setSecondary({}, true);
|
|
drain();
|
|
auto restored = driver.hop(0);
|
|
for (unsigned retry = 0; restored == mesh::RadioParamApplyResult::BUSY && retry < 20; ++retry) {
|
|
drain(); delay(20); restored = driver.hop(0);
|
|
}
|
|
const bool safe = restored == mesh::RadioParamApplyResult::APPLIED
|
|
&& driver.isInRecvMode() && chip.hilFrequencyMHz() == 909.5f;
|
|
recordBenchResult(lastRunResult, sequence,
|
|
"{\"result\":%lu,\"board\":\"Heltec T096 SX1262\",\"mode\":\"%s\","
|
|
"\"fs\":%s,\"packet_cache\":%s,\"spi_hz\":%lu,\"requested\":%u,"
|
|
"\"completed\":%lu,\"deferrals\":%lu,\"failures\":%lu,"
|
|
"\"mode_errors\":%lu,\"cache_errors\":%lu,\"rssi_errors\":%lu,"
|
|
"\"busy_timeouts\":%lu,\"fast_resumes\":%lu,\"fs_entries\":%lu,"
|
|
"\"packet_skips\":%lu,\"rf_commands\":%lu,\"mod_commands\":%lu,"
|
|
"\"packet_commands\":%lu,\"standby_commands\":%lu,\"fs_commands\":%lu,"
|
|
"\"clear_commands\":%lu,\"rx_commands\":%lu,\"restored\":%s,"
|
|
"\"to_primary\":{\"n\":%lu,\"min_us\":%.3f,\"mean_us\":%.3f,\"max_us\":%.3f},"
|
|
"\"to_secondary\":{\"n\":%lu,\"min_us\":%.3f,\"mean_us\":%.3f,\"max_us\":%.3f}}\n",
|
|
(unsigned long)sequence, mode, chip.hilUseFsOnRetune ? "true" : "false",
|
|
chip.hilSkipUnchangedPacket ? "true" : "false", (unsigned long)radioHal.spiHz,
|
|
count, (unsigned long)completed, (unsigned long)deferrals, (unsigned long)failures,
|
|
(unsigned long)modeErrors, (unsigned long)cacheErrors, (unsigned long)rssiErrors,
|
|
(unsigned long)busyTimeouts, (unsigned long)(chip.getOptimizedProfileSwitches() - fast0),
|
|
(unsigned long)(chip.hilFsEntries - fsEntry0), (unsigned long)(chip.hilPacketSkips - skip0),
|
|
(unsigned long)(radioHal.rfWrites - rf0), (unsigned long)(radioHal.modWrites - mod0),
|
|
(unsigned long)(radioHal.packetWrites - packet0), (unsigned long)(radioHal.standbyWrites - standby0),
|
|
(unsigned long)(radioHal.fsWrites - fs0), (unsigned long)(radioHal.clearWrites - clear0),
|
|
(unsigned long)(radioHal.rxWrites - rx0), safe ? "true" : "false",
|
|
(unsigned long)timing[0].n, timing[0].min(), timing[0].mean(), timing[0].max(),
|
|
(unsigned long)timing[1].n, timing[1].min(), timing[1].mean(), timing[1].max());
|
|
}
|
|
|
|
void pilot() {
|
|
if (!ready || !configureProfiles(10, false)) {
|
|
pilotReady = false;
|
|
Serial.println("{\"pilot\":false}");
|
|
return;
|
|
}
|
|
pilotReady = driver.receiveProfile() == 0 && driver.isInRecvMode()
|
|
&& chip.hilFrequencyMHz() == 909.5f && driver.getSpreadingFactor() == 10;
|
|
Serial.printf("{\"pilot\":%s,\"freq_khz\":909500,\"sf\":10,\"bw_khz\":125}\n",
|
|
pilotReady ? "true" : "false");
|
|
}
|
|
|
|
void pilotHop(unsigned target) {
|
|
if (!pilotReady || target > 1 || driver.receiveProfile() == target) {
|
|
Serial.println("{\"error\":\"invalid pilot hop\"}"); return;
|
|
}
|
|
const uint32_t fast0 = chip.getOptimizedProfileSwitches();
|
|
const uint32_t fs0 = chip.hilFsEntries, skip0 = chip.hilPacketSkips;
|
|
const uint32_t started = DWT->CYCCNT;
|
|
const auto result = driver.hop(target);
|
|
const uint32_t cycles = DWT->CYCCNT - started;
|
|
const bool okay = result == mesh::RadioParamApplyResult::APPLIED
|
|
&& driver.receiveProfile() == target && driver.isInRecvMode()
|
|
&& chip.hilFrequencyMHz() == (target ? 909.75f : 909.5f)
|
|
&& driver.getSpreadingFactor() == 10 && waitBusy();
|
|
if (!okay) pilotReady = false;
|
|
Serial.printf("{\"pilot_hop\":%u,\"ok\":%s,\"us\":%.3f,\"fast\":%lu,"
|
|
"\"fs\":%lu,\"packet_skips\":%lu}\n", target,
|
|
okay ? "true" : "false", double(cycles) * 1000000.0 / SystemCoreClock,
|
|
(unsigned long)(chip.getOptimizedProfileSwitches() - fast0),
|
|
(unsigned long)(chip.hilFsEntries - fs0),
|
|
(unsigned long)(chip.hilPacketSkips - skip0));
|
|
}
|
|
|
|
void poll(uint32_t expected) {
|
|
if (!pilotReady || !expected) { Serial.println("{\"error\":\"pilot not ready\"}"); return; }
|
|
uint8_t bytes[256] = {};
|
|
const int len = driver.recvRaw(bytes, sizeof(bytes));
|
|
uint32_t seen = 0;
|
|
if (len == 16 && !memcmp(bytes, "CHS1", 4)) memcpy(&seen, bytes + 4, 4);
|
|
bool valid = len == 16 && seen == expected && bytes[8] == 0;
|
|
for (unsigned i = 9; valid && i < 16; ++i) valid = bytes[i] == uint8_t(i ^ expected);
|
|
Serial.printf("{\"expected\":%lu,\"received\":%lu,\"len\":%d,\"valid\":%s,\"rx_mode\":%s}\n",
|
|
(unsigned long)expected, (unsigned long)seen, len,
|
|
valid ? "true" : "false", driver.isInRecvMode() ? "true" : "false");
|
|
}
|
|
|
|
void setup() {
|
|
Serial.begin(115200); delay(1500);
|
|
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
|
|
DWT->CYCCNT = 0;
|
|
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
|
|
NRF_POWER->DCDCEN = 1;
|
|
pinMode(30, OUTPUT); digitalWrite(30, HIGH); // T096 RF front-end supply
|
|
pinMode(12, OUTPUT); digitalWrite(12, HIGH); // KCT8103L enabled
|
|
pinMode(41, OUTPUT); digitalWrite(41, LOW); // LNA/RX path
|
|
SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI);
|
|
SPI.begin();
|
|
initError = chip.begin(909.5f, 125, 7, 5,
|
|
RADIOLIB_SX126X_SYNC_WORD_PRIVATE, 0, 32, 1.8f);
|
|
if (!initError) initError = chip.setCRC(true);
|
|
if (!initError) initError = chip.explicitHeader();
|
|
if (!initError) initError = chip.setDio2AsRfSwitch(true);
|
|
if (!initError) initError = chip.setCurrentLimit(140);
|
|
if (!initError) initError = chip.setRxBoostedGainMode(true);
|
|
const uint32_t cycles = DWT->CYCCNT;
|
|
delayMicroseconds(2);
|
|
ready = initError == RADIOLIB_ERR_NONE && DWT->CYCCNT != cycles;
|
|
if (ready) {
|
|
chip.setProfileSwitchOptimization(true);
|
|
driver.begin(); driver.startRx(); driver.warm();
|
|
ready = driver.isInRecvMode() && waitBusy();
|
|
}
|
|
Serial.printf("{\"ready\":%s,\"bench\":\"t096-sx1262-fs-packet-v1\",\"init_rc\":%d}\n",
|
|
ready ? "true" : "false", initError);
|
|
}
|
|
|
|
void loop() {
|
|
if (NRF_WDT->RUNSTATUS)
|
|
for (unsigned i = 0; i < 8; ++i) NRF_WDT->RR[i] = 0x6E524635;
|
|
static char line[96];
|
|
static size_t used = 0;
|
|
while (Serial.available()) {
|
|
const char c = Serial.read();
|
|
if (c == '\r' || c == '\n') {
|
|
if (!used) continue;
|
|
line[used] = 0; used = 0;
|
|
unsigned value; unsigned long seq; char trailing, mode[16], kind[12];
|
|
if (!strcmp(line, "info")) {
|
|
Serial.printf("{\"ready\":%s,\"bench\":\"t096-sx1262-fs-packet-v1\","
|
|
"\"board\":\"Heltec T096 SX1262\",\"autonomous_tx\":false,"
|
|
"\"sd_fwid\":%u,\"app_base\":%lu,\"spi_hz\":%lu,"
|
|
"\"fs\":%s,\"packet_cache\":%s}\n",
|
|
ready ? "true" : "false",
|
|
unsigned(*reinterpret_cast<volatile uint16_t*>(0x300c)),
|
|
(unsigned long)*reinterpret_cast<volatile uint32_t*>(0x3008),
|
|
(unsigned long)radioHal.spiHz,
|
|
chip.hilUseFsOnRetune ? "true" : "false",
|
|
chip.hilSkipUnchangedPacket ? "true" : "false");
|
|
} else if (sscanf(line, "fs %u %c", &value, &trailing) == 1 && value <= 1) {
|
|
chip.hilUseFsOnRetune = value;
|
|
Serial.printf("{\"fs\":%s}\n", value ? "true" : "false");
|
|
} else if (sscanf(line, "packetcache %u %c", &value, &trailing) == 1 && value <= 1) {
|
|
chip.hilSkipUnchangedPacket = value;
|
|
Serial.printf("{\"packet_cache\":%s}\n", value ? "true" : "false");
|
|
} else if (sscanf(line, "spihz %u %c", &value, &trailing) == 1
|
|
&& (value == 8000000 || value == 16000000)) {
|
|
radioHal.setSpiHz(value);
|
|
Serial.printf("{\"spi_hz\":%lu}\n", (unsigned long)radioHal.spiHz);
|
|
} else if (sscanf(line, "run %15s %u %lu %c", mode, &value, &seq, &trailing) == 3) {
|
|
run(mode, value, seq);
|
|
} else if (!strcmp(line, "pilot")) {
|
|
pilot();
|
|
} else if (sscanf(line, "pilothop %u %c", &value, &trailing) == 1) {
|
|
pilotHop(value);
|
|
} else if (sscanf(line, "poll %lu %c", &seq, &trailing) == 1) {
|
|
poll(seq);
|
|
} else if (sscanf(line, "result result %lu %c", &seq, &trailing) == 1) {
|
|
replayBenchResult("result", seq);
|
|
} else if (!strcmp(line, "reboot")) {
|
|
Serial.flush(); delay(20); NVIC_SystemReset();
|
|
} else Serial.println("{\"error\":\"unsupported command\"}");
|
|
} else if (used + 1 < sizeof(line)) line[used++] = c;
|
|
}
|
|
delay(1);
|
|
}
|