mirror of
https://github.com/ALLFATHER-BV/wadamesh.git
synced 2026-09-25 22:05:35 +00:00
A two-axis magnetic heading assumes the device is level. At this latitude the
field dips ~60 degrees, so the vertical component is 1.6x the horizontal one
and tipping the device leaks it into the pair the heading is made from: about
1.5 degrees of heading per degree of tilt. That is what "it drifts" was once
the calibration was sound -- it was the hand holding it.
Driver: variants/thinknode_m9/M9Imu.{h,cpp}, QMI8658 at 0x6B on the peripheral
bus, accelerometer only (the gyro is most of the power budget and nothing here
needs it): +/-2 g at 62.5 Hz with the low-pass on, soft reset with a 160 ms
wait that covers both die variants, and the same idle-suspend as the compass so
it costs nothing when unused. CTRL1's ADDR_AI bit is set and read back -- with
it clear the burst read silently returns six copies of one byte, which looks
like a working sensor reporting nonsense. HAS_M9_IMU -> CAP_IMU ->
wada.sys.accel(), caps().accel.
Axes MEASURED, not guessed, by holding three attitudes and logging:
flat, screen up z = -1.02 -> +Z into the screen (down)
on bottom edge, top up x = +0.97 -> +X at the top edge (forward)
on left edge, right up y = +1.08 -> +Y at the right edge
So the IMU is already in the aerospace body frame, and it agrees with the
magnetometer's independently measured +Z-into-screen. (Meshtastic's M9 driver
passes both sensors through untransformed and mirrors the heading on the sign
of accel Z, so its compass flips when the device is turned over. Not copied.)
Heading now rotates the field back into the horizontal plane using gravity
(NXP AN4248 / ST AN3192) before taking the angle, and reports the tilt angle;
past 55 degrees it says "too steep to read" rather than lying. Calibration
returns to a 3D fit because tilt compensation needs the vertical offset too --
but the instruction is now "turn it every way ON ONE SPOT", and the
accelerometer VERIFIES it: coverage is measured by how far gravity swung, so a
flat spin is refused by name ("turn it nose over tail") instead of silently
fitting a degenerate sphere. Simulated in the harness against a modelled M9 in
a known attitude: offsets recovered exactly, and the heading holds within 3
degrees through 20 degrees of roll and pitch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
173 lines
7.7 KiB
C++
173 lines
7.7 KiB
C++
#include "target.h"
|
|
#include <Arduino.h>
|
|
|
|
ThinkNodeM9Board board;
|
|
|
|
// LR1110 has no std_init() helper in the core (unlike CustomSX1262/SX1268/
|
|
// LLCC68) — confirmed against MeshCore's own LR1110 boards (thinknode_m3,
|
|
// minewsemi_me25ls01). Module() takes the same (NSS, IRQ/DIO1, RESET, BUSY,
|
|
// spi) signature either way. M9: NSS=39, DIO1=42, RESET=45, BUSY=41,
|
|
// SCLK=40, MISO=38, MOSI=47 — all on the SAME physical SPI bus as the LCD
|
|
// (CS=16) and the microSD slot (CS=48), so radio and display share the
|
|
// literal global `SPI` object — matching how T-Deck/Heltec V4's radio (a
|
|
// plain default-constructed `static SPIClass spi;`, not an explicit-host
|
|
// instance) and the LR1110 reference boards (thinknode_m3, me25ls01, which
|
|
// also pass the global `SPI` to their Module()) both do it. The bus itself
|
|
// is begun once, in ThinkNodeM9Board::begin() (see M9Board.cpp for why that
|
|
// has to happen there rather than mirroring T-Deck/Heltec's "begin it inside
|
|
// radio_init()" exactly — their displays self-init on a separate dedicated
|
|
// bus, M9's display is on ST7789LCDDisplay's "default" branch and needs the
|
|
// global SPI already begun before display.begin() runs).
|
|
RADIO_CLASS radio =
|
|
new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
|
|
|
|
WRAPPER_CLASS radio_driver(radio, board);
|
|
|
|
ESP32RTCClock fallback_clock;
|
|
ClockFloorRTC rtc_clock(
|
|
fallback_clock); // wraps AutoDiscover: monotonic send-timestamp floor (#89)
|
|
// Wadamesh's own provider rather than the core's MicroNMEALocationProvider:
|
|
// identical behaviour, plus the RMC speed/course the core keeps private (see
|
|
// src/helpers/WadaNmeaLocationProvider.h for why it is a copy, not a subclass).
|
|
WadaNmeaLocationProvider gps(Serial1, &rtc_clock);
|
|
EnvironmentSensorManager sensors(gps);
|
|
|
|
#ifdef DISPLAY_CLASS
|
|
// periph_power gates the LCD/GPS/sensor rail (GPIO18, active-low P-MOS).
|
|
// Passing &board.periph_power lets ST7789LCDDisplay::begin()/turnOff()
|
|
// claim/release it alongside the panel's own lifecycle. The backlight
|
|
// (GPIO17, PNP) is handled separately by ThinkNodeM9Board — see M9Board.h
|
|
// for why it's NOT routed through PIN_TFT_LEDA_CTL.
|
|
DISPLAY_CLASS display(&board.periph_power);
|
|
// (No MomentaryButton here: the M9 has NO user/BOOT button — schematic-
|
|
// confirmed, only a power-cut slider and reset. PIN_USER_BTN is undefined for
|
|
// this env so UITask's button poll compiles out too.)
|
|
#endif
|
|
|
|
#ifndef LORA_CR
|
|
#define LORA_CR 5
|
|
#endif
|
|
|
|
bool radio_init() {
|
|
fallback_clock.begin();
|
|
rtc_clock.begin(Wire); // peripheral I2C bus (7/6) — RTC PCF8563 @ 0x51
|
|
|
|
#if defined(HAS_M9_KEYBOARD)
|
|
m9KeyboardBegin(); // own bus, Wire1 (20/21) — no contention with Wire
|
|
#endif
|
|
#if defined(HAS_M9_COMPASS)
|
|
// QMC6309 on the same peripheral bus as the RTC, behind the GPIO18 rail that
|
|
// board.begin() claimed well before this runs. Absent / still-booting chips
|
|
// are re-probed lazily from the read path, so a miss here is not fatal.
|
|
m9CompassBegin(Wire);
|
|
#endif
|
|
#if defined(HAS_M9_IMU)
|
|
m9ImuBegin(Wire); // QMI8658 at 0x6B, same bus, same lazy re-probe
|
|
#endif
|
|
|
|
#ifdef LR11X0_DIO3_TCXO_VOLTAGE
|
|
float tcxo = LR11X0_DIO3_TCXO_VOLTAGE;
|
|
#else
|
|
// Fallback = the hardware-confirmed value, NOT 0. An earlier theory (Y1 =
|
|
// self-powered active oscillator, so disable RadioLib's TCXO bias) was
|
|
// disproven on the real unit: with tcxo=0 the chip boots on its internal
|
|
// RC (SPI alive) but the first command needing the true 32 MHz clock is
|
|
// rejected — radio init fails -707. The schematic's "VTCXO" rail feeding
|
|
// Y1 is the LR1110's own TCXO-supply output, so DIO3 must drive it at
|
|
// 3.3 V — see the LR11X0_DIO3_TCXO_VOLTAGE comment in platformio.ini for
|
|
// the empirical confirmation.
|
|
float tcxo = 3.3f;
|
|
#endif
|
|
|
|
// SPI bus itself was already begun once in ThinkNodeM9Board::begin() (the
|
|
// display needs it before display.begin() runs) — this is the manual
|
|
// equivalent of what CustomSX1262/SX1268's std_init() would otherwise do,
|
|
// since CustomLR1110 has no std_init() helper.
|
|
int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR,
|
|
RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE,
|
|
LORA_TX_POWER, 16, tcxo);
|
|
if (status != RADIOLIB_ERR_NONE) {
|
|
// One retry after a settle: the first begin() enables the TCXO supply, and
|
|
// if Y1's startup outruns RadioLib's fixed internal wait the first
|
|
// calibration can fail while the second attempt finds a stable clock.
|
|
delay(150);
|
|
status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR,
|
|
RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, LORA_TX_POWER,
|
|
16, tcxo);
|
|
}
|
|
// Print the chip's own identification in BOTH outcomes: device type,
|
|
// transceiver FW revision and pending error flags. Preprod M9 units ship
|
|
// with old LR1110 firmware (pre-0x0308), which is exactly what this line
|
|
// catches — see scripts/build/patch_radiolib_lr11x0.py for the matching
|
|
// old-FW tolerance in RadioLib's config(). Safe to call even after a
|
|
// failed begin(): the SPI link is configured before the failing step.
|
|
{
|
|
LR11x0VersionInfo_t vinfo;
|
|
uint16_t chip_errors = 0;
|
|
if (radio.getVersionInfo(&vinfo) == RADIOLIB_ERR_NONE) {
|
|
radio.getErrors(&chip_errors);
|
|
Serial.printf("LR1110: hw=0x%02X device=0x%02X fw=%u.%u wifi=%u.%u "
|
|
"gnss=%u.%u errors=0x%04X\n",
|
|
vinfo.hardware, vinfo.device, vinfo.fwMajor, vinfo.fwMinor,
|
|
vinfo.fwMajorWiFi, vinfo.fwMinorWiFi, vinfo.fwGNSS,
|
|
vinfo.almanacGNSS, chip_errors);
|
|
} else {
|
|
Serial.println("LR1110: getVersionInfo failed (chip not answering)");
|
|
}
|
|
}
|
|
|
|
if (status != RADIOLIB_ERR_NONE) {
|
|
Serial.print("ERROR: radio init failed: ");
|
|
Serial.println(status);
|
|
return false;
|
|
}
|
|
|
|
radio.setCRC(2);
|
|
radio.explicitHeader();
|
|
|
|
// RF-switch DIO table: pin assignment is SCHEMATIC-CONFIRMED
|
|
// (Think_Node_M9_V1_0.pdf, the LR1110/U7 block) — DIO5 (pin 20) -> R20 -> net
|
|
// RFSW0_V1 -> switch IC (U8) V1; DIO6 (pin 19) -> R19 -> net RFSW1_V2 -> U8
|
|
// V2. DIO7/DIO8 are unconnected on this board (no net, dangling stubs) —
|
|
// unlike t1000-e/me25ls01's 4-pin DIO5-8 scheme, this is a plain 2-pin switch
|
|
// into a single antenna (U8 RFC -> C74 -> L12 -> ANT1), matching
|
|
// thinknode_m3's table shape exactly. What's NOT independently re-derived:
|
|
// the per-mode HIGH/LOW truth table below — U8's part number isn't printed on
|
|
// the schematic, so this reuses the conventional STBY/RX/TX/TX_HP polarity
|
|
// every other MeshCore LR1110 board's 2-pin table uses (thinknode_m3, same
|
|
// shape). If TX/RX work but seem swapped or dead, flip the RX/TX_HP rows here
|
|
// first.
|
|
#ifdef RF_SWITCH_TABLE
|
|
static const uint32_t rfswitch_dios[Module::RFSWITCH_MAX_PINS] = {
|
|
RADIOLIB_LR11X0_DIO5, // -> RFSW0_V1 (U8 V1)
|
|
RADIOLIB_LR11X0_DIO6, // -> RFSW1_V2 (U8 V2)
|
|
RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC};
|
|
static const Module::RfSwitchMode_t rfswitch_table[] = {
|
|
{LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}},
|
|
{LR11x0::MODE_TX, {HIGH, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}},
|
|
{LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}},
|
|
{LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE,
|
|
};
|
|
radio.setRfSwitchTable(rfswitch_dios, rfswitch_table);
|
|
#endif
|
|
#ifdef RX_BOOSTED_GAIN
|
|
radio.setRxBoostedGainMode(RX_BOOSTED_GAIN);
|
|
#endif
|
|
|
|
return true;
|
|
}
|
|
|
|
mesh::LocalIdentity radio_new_identity() {
|
|
RadioNoiseListener rng(radio);
|
|
return mesh::LocalIdentity(&rng);
|
|
}
|
|
|
|
SPIClass *m9SharedSPI() {
|
|
return &SPI; // global instance, shared by radio + display + SD; begun once in
|
|
// M9Board::begin()
|
|
}
|
|
|
|
bool wadaGpsMotion(float *speed_kmh, float *course_deg) {
|
|
return gps.motion(speed_kmh, course_deg);
|
|
}
|