#include "target.h" #include 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); }