Public Access
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:
+10
-1
@@ -1,6 +1,6 @@
|
|||||||
# M3 — Radio bring-up: the LoRa Scanner
|
# M3 — Radio bring-up: the LoRa Scanner
|
||||||
|
|
||||||
**Status:** planned (branch `m3`).
|
**Status:** in progress (branch `m3`): step 1 done.
|
||||||
|
|
||||||
**Goal:** the LoRa radio on the Cap works, receive only: a Radio Service owns it and shares the SPI bus with the SD card safely, and a LoRa Scanner App shows what's on the air, either packets (Sniffer) or energy across the band (Sweep). Nothing in M3 can transmit. The mesh comes on top of this in M4 (receive) and M5 (transmit).
|
**Goal:** the LoRa radio on the Cap works, receive only: a Radio Service owns it and shares the SPI bus with the SD card safely, and a LoRa Scanner App shows what's on the air, either packets (Sniffer) or energy across the band (Sweep). Nothing in M3 can transmit. The mesh comes on top of this in M4 (receive) and M5 (transmit).
|
||||||
|
|
||||||
@@ -8,6 +8,15 @@
|
|||||||
|
|
||||||
**No other LoRa device yet.** meshmap.net (2026-10-05) lists two Meshtastic nodes within 10 km of the desk and six within 30 km, with positions blurred by a few km; none is known to be in range. M3 needs none: it only receives.
|
**No other LoRa device yet.** meshmap.net (2026-10-05) lists two Meshtastic nodes within 10 km of the desk and six within 30 km, with positions blurred by a few km; none is known to be in range. M3 needs none: it only receives.
|
||||||
|
|
||||||
|
## Measured (step 1, `lora probe`)
|
||||||
|
|
||||||
|
- **Internal I2C bus (8/9):** 0x18 (ES8311 codec), 0x34 (TCA8418 keyboard), **0x43 (PI4IOE5V6408, ID register 0xA2)**, 0x69 (BMI270 IMU).
|
||||||
|
- **The SX1262 answers** on NSS 5, RST 3, DIO1 4, BUSY 6. Its version string reads `SX1261 V2D 2D02`, which SX1262 chips report too. **The 1.8 V TCXO works** on the first try; the radio is ready 38 ms after `begin`.
|
||||||
|
- **The expander's P0 connects the antenna; it's required.** At power-on P0 is an input (direction 0x00, high-impedance 0xFF), and the receiver reads a flat **-111.9 dBm** at 869.525 MHz, BW 250 kHz: the chip's own floor, deaf. With P0 driven high, the noise floor is **-87 to -94 dBm**: the antenna hearing the room. So the Radio Service drives P0 high at boot (Q91).
|
||||||
|
- **DIO2 doesn't change reception** (within ±2 dB over three runs, P0 high). It likely selects TX versus RX in the FM8625H; it stays the RF switch, as in Meshtastic.
|
||||||
|
- **The noise floor at the desk is high** (-87 to -94 dBm, varying run to run), about 25 dB above thermal noise for 250 kHz. Something nearby is loud, possibly the Cardputer itself or the PC; Sweep (step 5) should show where it sits.
|
||||||
|
- **Cost:** RadioLib 7.8.1 and the probe add 23.6 KB of flash and 656 bytes of static RAM to the release firmware (1,679,843 bytes of 3,342,336).
|
||||||
|
|
||||||
## Decisions (design round 2026-10-05)
|
## Decisions (design round 2026-10-05)
|
||||||
|
|
||||||
| # | Decision |
|
| # | Decision |
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ build_flags =
|
|||||||
-DARDUINO_USB_MODE=1
|
-DARDUINO_USB_MODE=1
|
||||||
lib_deps =
|
lib_deps =
|
||||||
m5stack/M5Cardputer @ 1.1.1
|
m5stack/M5Cardputer @ 1.1.1
|
||||||
|
jgromes/RadioLib @ 7.8.1
|
||||||
test_ignore = *
|
test_ignore = *
|
||||||
; Smaller TLS buffers (M2): the framework is rebuilt with these settings (pioarduino "hybrid
|
; Smaller TLS buffers (M2): the framework is rebuilt with these settings (pioarduino "hybrid
|
||||||
; compile"). Receive stays 16 KB (servers send full TLS records); send drops to 4 KB (IRC lines are
|
; compile"). Receive stays 16 KB (servers send full TLS records); send drops to 4 KB (IRC lines are
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
#include "key_mapper.h"
|
#include "key_mapper.h"
|
||||||
#include "platform/console.h"
|
#include "platform/console.h"
|
||||||
#include "platform/crash_report.h"
|
#include "platform/crash_report.h"
|
||||||
|
#include "platform/lora_probe.h"
|
||||||
#include "platform/nvs_store.h"
|
#include "platform/nvs_store.h"
|
||||||
#include "platform/system_info.h"
|
#include "platform/system_info.h"
|
||||||
#include "service_manager.h"
|
#include "service_manager.h"
|
||||||
@@ -347,6 +348,7 @@ static const char* const kHelp =
|
|||||||
"reboot restart\n"
|
"reboot restart\n"
|
||||||
"boot other restart into the other app slot (manual Rollback)\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"
|
"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"
|
"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"
|
"crash the last crash: firmware, reason, task, backtrace\n"
|
||||||
"coredump erase forget the core dump in flash\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.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 status" && gnssService) gnssService->printStatus(console, millis());
|
||||||
if ((line == "gnss nmea on" || line == "gnss nmea off") && gnssService) gnssService->setEcho(line.endsWith("on"));
|
if ((line == "gnss nmea on" || line == "gnss nmea off") && gnssService) gnssService->setEcho(line.endsWith("on"));
|
||||||
if (line == "gnss restart" && gnssService) gnssService->restart(millis());
|
if (line == "gnss restart" && gnssService) gnssService->restart(millis());
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -14,4 +14,9 @@ constexpr int kSpiMosi = 14;
|
|||||||
constexpr int kSdCs = 12;
|
constexpr int kSdCs = 12;
|
||||||
constexpr int kLoraCs = 5;
|
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
|
} // namespace roro::pins
|
||||||
|
|||||||
Reference in New Issue
Block a user