M3 step 1: RadioLib and lora probe

The probe finds the SX1262 (TCXO 1.8 V works) and measures the antenna
path: the Cap's PI4IOE5V6408 at 0x43 must drive P0 high, or the receiver
is deaf (-111.9 dBm flat vs -87 to -94 dBm with P0 high). DIO2 makes no
difference to reception. Receive only; the radio is left asleep.

RadioLib 7.8.1 + probe: +23.6 KB flash, +656 B static RAM (release).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
2026-10-05 18:52:38 +02:00
co-authored by Claude Opus 5.5
parent 6485f277b5
commit fed065a6f9
6 changed files with 154 additions and 1 deletions
+3
View File
@@ -20,6 +20,7 @@
#include "key_mapper.h"
#include "platform/console.h"
#include "platform/crash_report.h"
#include "platform/lora_probe.h"
#include "platform/nvs_store.h"
#include "platform/system_info.h"
#include "service_manager.h"
@@ -347,6 +348,7 @@ static const char* const kHelp =
"reboot restart\n"
"boot other restart into the other app slot (manual Rollback)\n"
"log level <0-5> ESP-IDF log level (0 none ... 5 verbose)\n"
"lora probe find the LoRa radio: chip, oscillator, antenna switch, noise floor (receive only)\n"
"gnss status | gnss restart | gnss track start|stop | gnss nmea on|off | gnss send <sentence without $ and checksum>\n"
"crash the last crash: firmware, reason, task, backtrace\n"
"coredump erase forget the core dump in flash\n"
@@ -410,6 +412,7 @@ static void runCommand(String line) {
});
}
if (line.startsWith("install ")) update->installFromSd(line.substring(8).c_str()); // Update from SD
if (line == "lora probe") loraProbe(console);
if (line == "gnss status" && gnssService) gnssService->printStatus(console, millis());
if ((line == "gnss nmea on" || line == "gnss nmea off") && gnssService) gnssService->setEcho(line.endsWith("on"));
if (line == "gnss restart" && gnssService) gnssService->restart(millis());
+124
View File
@@ -0,0 +1,124 @@
#include "lora_probe.h"
#include <M5Cardputer.h>
#include <RadioLib.h>
#include "pins.h"
#include "shared_spi.h"
namespace roro {
namespace {
// The antenna switch enable M5Stack's page describes: P0 of a PI4IOE5V6408 on the internal I2C bus.
constexpr uint8_t kExpanderAddresses[] = {0x43, 0x44};
constexpr uint32_t kI2cFreq = 400000;
enum ExpanderReg : uint8_t { kId = 0x01, kDirection = 0x03, kOutput = 0x05, kHighZ = 0x07, kInput = 0x0F };
// Meshtastic EU868 LongFast (Q95).
constexpr float kFreq = 869.525f, kBw = 250.0f;
constexpr uint8_t kSf = 11, kCr = 5, kSyncWord = 0x2B;
constexpr uint16_t kPreamble = 16;
Module& module() {
// SPI transactions take the bus lock the SD card uses (Q93); the HAL doesn't re-begin the bus.
static ArduinoHal hal(sharedSpi(), SPISettings(8000000, MSBFIRST, SPI_MODE0));
static Module m(&hal, pins::kLoraCs, pins::kLoraIrq, pins::kLoraReset, pins::kLoraBusy);
return m;
}
SX1262& radio() {
static SX1262 sx(&module());
return sx;
}
// Mean and spread of the instantaneous RSSI while receiving: the noise floor where nothing transmits.
void noiseFloor(SX1262& sx, float& mean, float& low, float& high) {
sx.startReceive();
delay(20);
float sum = 0;
low = 0, high = -200;
constexpr int kSamples = 64;
for (int i = 0; i < kSamples; ++i) {
float r = sx.getRSSI(false);
sum += r;
low = std::min(low, r), high = std::max(high, r);
delay(3);
}
mean = sum / kSamples;
sx.standby();
}
int findExpander() {
for (uint8_t a : kExpanderAddresses)
if (M5.In_I2C.scanID(a, kI2cFreq)) return a;
return -1;
}
void setP0High(uint8_t addr) {
auto& i2c = M5.In_I2C;
i2c.writeRegister8(addr, kOutput, i2c.readRegister8(addr, kOutput, kI2cFreq) | 1, kI2cFreq);
i2c.writeRegister8(addr, kHighZ, i2c.readRegister8(addr, kHighZ, kI2cFreq) & ~1, kI2cFreq);
i2c.writeRegister8(addr, kDirection, i2c.readRegister8(addr, kDirection, kI2cFreq) | 1, kI2cFreq);
}
} // namespace
void loraProbe(Print& out) {
// The internal I2C bus: what answers, and the expander's registers if there is one.
bool found[120] = {};
M5.In_I2C.scanID(found, kI2cFreq);
out.print("lora probe: i2c (internal bus, 8/9):");
for (int a = 8; a < 120; ++a)
if (found[a]) out.printf(" 0x%02X", a);
out.println();
int expander = findExpander();
if (expander < 0) {
out.println("lora probe: expander none at 0x43/0x44");
} else {
auto r = [&](uint8_t reg) { return M5.In_I2C.readRegister8(expander, reg, kI2cFreq); };
out.printf("lora probe: expander 0x%02X id %02X dir %02X out %02X highz %02X in %02X\n", expander, r(kId),
r(kDirection), r(kOutput), r(kHighZ), r(kInput));
}
// The chip, with the TCXO first and the crystal as the fallback (Meshtastic's TCXO_OPTIONAL).
SX1262& sx = radio();
float tcxo = 1.8f;
uint32_t t0 = millis();
int16_t state = sx.begin(kFreq, kBw, kSf, kCr, kSyncWord, 0, kPreamble, tcxo, false);
if (state != RADIOLIB_ERR_NONE) {
out.printf("lora probe: begin with TCXO 1.8 V: error %d, trying the crystal\n", state);
tcxo = 0;
state = sx.begin(kFreq, kBw, kSf, kCr, kSyncWord, 0, kPreamble, tcxo, false);
}
if (state != RADIOLIB_ERR_NONE) return (void)out.printf("lora probe: error %d, no SX1262 found\n", state);
char version[17] = {};
module().SPIreadRegisterBurst(RADIOLIB_SX126X_REG_VERSION_STRING, 16, reinterpret_cast<uint8_t*>(version));
out.printf("lora probe: %s, %s, ready in %u ms\n", version, tcxo > 0 ? "TCXO 1.8 V" : "crystal (no TCXO)",
(unsigned)(millis() - t0));
out.printf("lora probe: %.3f MHz, BW %.0f kHz, SF %u, CR 4/%u, sync 0x%02X (LongFast)\n", kFreq, kBw, kSf, kCr,
kSyncWord);
// The antenna switch, by measuring: a receive path that isn't connected to the antenna is
// quieter, so the arrangement with the higher noise floor is the one that hears the antenna.
auto measure = [&](const char* what) {
float mean, low, high;
noiseFloor(sx, mean, low, high);
out.printf("lora probe: noise %-24s %7.1f dBm (%.1f to %.1f)\n", what, mean, low, high);
};
sx.setDio2AsRfSwitch(false);
measure("DIO2 off");
sx.setDio2AsRfSwitch(true);
measure("DIO2 on");
if (expander >= 0) {
setP0High(expander);
measure("DIO2 on, expander P0 high");
sx.setDio2AsRfSwitch(false);
measure("DIO2 off, expander P0 high");
sx.setDio2AsRfSwitch(true);
}
sx.sleep();
out.println("lora probe: done, radio asleep");
}
} // namespace roro
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <Print.h>
namespace roro {
// `lora probe` (M3 step 1, Q91): finds the SX1262 on the Cap and how its oscillator and antenna
// switch are wired, by measuring. Receive only; leaves the radio asleep.
void loraProbe(Print& out);
} // namespace roro
+5
View File
@@ -14,4 +14,9 @@ constexpr int kSpiMosi = 14;
constexpr int kSdCs = 12;
constexpr int kLoraCs = 5;
// The SX1262 on the Cap (as in Meshtastic's board file for the Cardputer ADV).
constexpr int kLoraReset = 3;
constexpr int kLoraIrq = 4; // DIO1
constexpr int kLoraBusy = 6;
} // namespace roro::pins