From de5d6697d1bd502942140cf4f72b3ecb197009fb Mon Sep 17 00:00:00 2001 From: agessaman Date: Fri, 16 Oct 2026 19:02:23 -0700 Subject: [PATCH] feat(rak3401): auto-sleep optional onboard SX1262 Use the existing RAK3401 firmware roles for RAK4631 cores paired with a RAK13302. Probe the optional onboard radio and put it into cold sleep before handing SPI to the primary radio, removing seven duplicate build environments. Preserve BUSY startup readiness, release probe pulls before sleep, and disconnect unused GPIO input buffers during cleanup. Add focused tests for sequencing, missing signatures, timeouts, and failed sleep readback. Validated with four native tests, seven firmware-role builds, and repeater boot checks on RAK4631 + RAK13302. Supply current was not measured. --- platformio.ini | 6 + .../test_onboard_radio.cpp | 66 +++++++++ variants/rak3401/OnboardRadioSleep.h | 34 +++++ variants/rak3401/platformio.ini | 125 ------------------ variants/rak3401/target.cpp | 110 ++++++++++----- variants/rak3401/variant.h | 7 +- 6 files changed, 184 insertions(+), 164 deletions(-) create mode 100644 test/test_rak3401_onboard_radio/test_onboard_radio.cpp create mode 100644 variants/rak3401/OnboardRadioSleep.h diff --git a/platformio.ini b/platformio.ini index e78124a4..e305d177 100644 --- a/platformio.ini +++ b/platformio.ini @@ -173,6 +173,12 @@ build_src_filter = lib_deps = google/googletest @ 1.17.0 +[env:native_rak3401_onboard_radio] +; Header-only tests without unrelated production sources; also included in env:native CI. +extends = env:native +test_build_src = no +test_filter = test_rak3401_onboard_radio + [env:native_kiss_modem] platform = native test_framework = googletest diff --git a/test/test_rak3401_onboard_radio/test_onboard_radio.cpp b/test/test_rak3401_onboard_radio/test_onboard_radio.cpp new file mode 100644 index 00000000..cc616612 --- /dev/null +++ b/test/test_rak3401_onboard_radio/test_onboard_radio.cpp @@ -0,0 +1,66 @@ +#include +#include +#include +#include "../../variants/rak3401/OnboardRadioSleep.h" + +namespace { +struct FakeRadioIO { + std::string version = "SX1261"; + std::array ready = {true, true, true}; + std::string calls; + unsigned waits = 0; + bool probePulls = true; + + void begin() { calls += "begin "; } + void reset() { calls += "reset "; } + bool waitBusy(bool high, unsigned timeout) { + calls += high ? "busy-high " : "busy-low "; + EXPECT_EQ(timeout, 20u); + if (high) EXPECT_FALSE(probePulls); + const bool result = ready.at(waits++); + return result; + } + void readVersion(char* out) { calls += "version "; memcpy(out, version.data(), 6); } + void releaseProbePulls() { calls += "release-pulls "; probePulls = false; } + void sleep() { EXPECT_FALSE(probePulls); calls += "sleep "; } + void end() { calls += "end "; } +}; +} + +TEST(RakOnboardRadio, PresentRadioSleepsBeforeBusRelease) { + FakeRadioIO io; + EXPECT_EQ(rak3401::sleepOnboardRadio(io, "SX1261"), rak3401::OnboardRadioState::Asleep); + EXPECT_EQ(io.calls, "begin reset busy-low version release-pulls busy-low sleep busy-high end "); +} + +TEST(RakOnboardRadio, AbsentAndFloatingResponsesNeverReceiveSleep) { + for (const std::string version : {std::string(6, '\0'), std::string(6, '\xff'), std::string("SX126X")}) { + FakeRadioIO io; + io.version = version; + EXPECT_EQ(rak3401::sleepOnboardRadio(io, "SX1261"), rak3401::OnboardRadioState::Absent); + EXPECT_EQ(io.calls, "begin reset busy-low version end "); + } +} + +TEST(RakOnboardRadio, BusyTimeoutsReleaseBusWithoutSleep) { + for (unsigned failure = 0; failure < 2; failure++) { + FakeRadioIO io; + io.ready[failure] = false; + EXPECT_EQ(rak3401::sleepOnboardRadio(io, "SX1261"), rak3401::OnboardRadioState::BusyTimeout); + EXPECT_EQ(io.calls.find("sleep "), std::string::npos); + EXPECT_EQ(io.calls, failure == 0 ? "begin reset busy-low end " : + "begin reset busy-low version release-pulls busy-low end "); + } +} + +TEST(RakOnboardRadio, MissingSleepReadbackIsReportedAndBusReleased) { + FakeRadioIO io; + io.ready[2] = false; + EXPECT_EQ(rak3401::sleepOnboardRadio(io, "SX1261"), rak3401::OnboardRadioState::SleepFailed); + EXPECT_EQ(io.calls.substr(io.calls.size() - 4), "end "); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/variants/rak3401/OnboardRadioSleep.h b/variants/rak3401/OnboardRadioSleep.h new file mode 100644 index 00000000..7f6d1c55 --- /dev/null +++ b/variants/rak3401/OnboardRadioSleep.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +namespace rak3401 { + +enum class OnboardRadioState { Absent, Asleep, BusyTimeout, SleepFailed }; + +// IO owns the temporary SPI pin assignment; every outcome must release it. +template +OnboardRadioState sleepOnboardRadio(IO& io, const char* expectedVersion) { + io.begin(); + io.reset(); + const auto state = [&]() { + // A missing chip also times out: BUSY is pulled high until a radio drives it low. + if (!io.waitBusy(false, 20)) return OnboardRadioState::BusyTimeout; + + char version[6]; + io.readVersion(version); + if (memcmp(version, expectedVersion, sizeof(version)) != 0) { + return OnboardRadioState::Absent; + } + io.releaseProbePulls(); + if (!io.waitBusy(false, 20)) return OnboardRadioState::BusyTimeout; + + io.sleep(); + // Reading over SPI here would wake the radio again. BUSY is a passive check. + return io.waitBusy(true, 20) ? OnboardRadioState::Asleep : OnboardRadioState::SleepFailed; + }(); + io.end(); + return state; +} + +} diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index 56883e75..48e71922 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -134,128 +134,3 @@ build_src_filter = ${rak3401.build_src_filter} extends = rak3401 build_src_filter = ${rak3401.build_src_filter} +<../examples/kiss_modem/> - -; ------------------------- RAK4631 core + RAK13302 ------------------------- -; Same RF hardware as the RAK3401 1W kit, but the RAK4631 core carries a second, -; unused SX1262 that would otherwise idle in standby forever. These envs match -; the RAK_3401_* ones and additionally cold-sleep that radio at boot. -; Do not flash them to a genuine RAK3401 -- there is no onboard radio to sleep. - -[rak4631_13302] -extends = rak3401 -board = rak4631 -build_flags = ${rak3401.build_flags} - -D RAK4631_ONBOARD_SX1262_SLEEP -build_src_filter = ${rak3401.build_src_filter} -lib_deps = ${rak3401.lib_deps} - -[env:RAK_4631_13302_repeater] -extends = rak4631_13302 -build_flags = - ${rak4631_13302.build_flags} - -D DISPLAY_CLASS=SSD1306Display - -D ADVERT_NAME='"RAK4631 1W Repeater"' - -D ADVERT_LAT=0.0 - -D ADVERT_LON=0.0 - -D ADMIN_PASSWORD='"password"' - -D MAX_NEIGHBOURS=50 - ;-D MESH_PACKET_LOGGING=1 - ;-D MESH_DEBUG=1 -build_src_filter = ${rak4631_13302.build_src_filter} - + - +<../examples/simple_repeater> - -[env:RAK_4631_13302_room_server] -extends = rak4631_13302 -build_flags = - ${rak4631_13302.build_flags} - -D DISPLAY_CLASS=SSD1306Display - -D ADVERT_NAME='"Test Room"' - -D ADVERT_LAT=0.0 - -D ADVERT_LON=0.0 - -D ADMIN_PASSWORD='"password"' - -D ROOM_PASSWORD='"hello"' - ;-D MESH_PACKET_LOGGING=1 - ;-D MESH_DEBUG=1 -build_src_filter = ${rak4631_13302.build_src_filter} - + - +<../examples/simple_room_server> - -[env:RAK_4631_13302_companion_radio_usb] -extends = rak4631_13302 -board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld -board_upload.maximum_size = 712704 -build_flags = - ${rak4631_13302.build_flags} - -I examples/companion_radio/ui-new - -D PIN_USER_BTN_ANA=31 - -D DISPLAY_CLASS=SSD1306Display - -D MAX_CONTACTS=350 - -D MAX_GROUP_CHANNELS=40 - -D ENABLE_USB_INTERFACE -; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 -; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 -build_src_filter = ${rak4631_13302.build_src_filter} - +<../examples/companion_radio/*.cpp> - +<../examples/companion_radio/ui-new/*.cpp> -lib_deps = - ${rak4631_13302.lib_deps} - densaugeo/base64 @ ~1.4.0 - -[env:RAK_4631_13302_companion_radio_ble] -extends = rak4631_13302 -board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld -board_upload.maximum_size = 712704 -build_flags = - ${rak4631_13302.build_flags} - -I examples/companion_radio/ui-new - -D PIN_USER_BTN_ANA=31 - -D DISPLAY_CLASS=SSD1306Display - -D MAX_CONTACTS=350 - -D MAX_GROUP_CHANNELS=40 - -D BLE_PIN_CODE=123456 - -D BLE_DEBUG_LOGGING=1 - -D OFFLINE_QUEUE_SIZE=256 - ;-D MESH_PACKET_LOGGING=1 - ;-D MESH_DEBUG=1 -build_src_filter = ${rak4631_13302.build_src_filter} - + - +<../examples/companion_radio/*.cpp> - +<../examples/companion_radio/ui-new/*.cpp> -lib_deps = - ${rak4631_13302.lib_deps} - densaugeo/base64 @ ~1.4.0 - -[env:RAK_4631_13302_terminal_chat] -extends = rak4631_13302 -build_flags = - ${rak4631_13302.build_flags} - -D MAX_CONTACTS=100 - -D MAX_GROUP_CHANNELS=1 - ;-D MESH_PACKET_LOGGING=1 - ;-D MESH_DEBUG=1 -build_src_filter = ${rak4631_13302.build_src_filter} - +<../examples/simple_secure_chat/main.cpp> -lib_deps = - ${rak4631_13302.lib_deps} - densaugeo/base64 @ ~1.4.0 - -[env:RAK_4631_13302_sensor] -extends = rak4631_13302 -build_flags = - ${rak4631_13302.build_flags} - -D DISPLAY_CLASS=SSD1306Display - -D ADVERT_NAME='"RAK4631 Sensor"' - -D ADVERT_LAT=0.0 - -D ADVERT_LON=0.0 - -D ADMIN_PASSWORD='"password"' - ;-D MESH_PACKET_LOGGING=1 - ;-D MESH_DEBUG=1 -build_src_filter = ${rak4631_13302.build_src_filter} - + - +<../examples/simple_sensor> - -[env:RAK_4631_13302_kiss_modem] -extends = rak4631_13302 -build_src_filter = ${rak4631_13302.build_src_filter} - +<../examples/kiss_modem/> diff --git a/variants/rak3401/target.cpp b/variants/rak3401/target.cpp index dac12ede..b9d142b3 100644 --- a/variants/rak3401/target.cpp +++ b/variants/rak3401/target.cpp @@ -1,6 +1,8 @@ #include #include "target.h" #include +#include "OnboardRadioSleep.h" +#include RAK3401Board board; @@ -32,49 +34,91 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); EnvironmentSensorManager sensors; #endif -#ifdef RAK4631_ONBOARD_SX1262_SLEEP -// With a RAK13302 fitted the RAK4631's own SX1262 never gets initialized, so it -// sits in STDBY_RC drawing standby current for the life of the board. Firmware -// cannot cut its supply -- P1.05 is the RF switch rail, not the radio's -- so -// bring it up on its own pins just long enough to put it into cold sleep. -static void sleep_onboard_radio() { - pinMode(ONBOARD_LORA_ANT_SW, OUTPUT); - digitalWrite(ONBOARD_LORA_ANT_SW, LOW); // nothing downstream needs the RF switch +namespace { +class OnboardRadioIO { +public: + void begin() { + digitalWrite(ONBOARD_LORA_NSS, HIGH); + pinMode(ONBOARD_LORA_NSS, OUTPUT); + digitalWrite(ONBOARD_LORA_RESET, HIGH); + pinMode(ONBOARD_LORA_RESET, OUTPUT); + digitalWrite(ONBOARD_LORA_ANT_SW, LOW); + pinMode(ONBOARD_LORA_ANT_SW, OUTPUT); + // BUSY is weakly pulled high during startup; never override it with a pull-down. + pinMode(ONBOARD_LORA_BUSY, INPUT_PULLUP); + SPI.setPins(ONBOARD_LORA_MISO, ONBOARD_LORA_SCLK, ONBOARD_LORA_MOSI); + SPI.begin(); + pinMode(ONBOARD_LORA_MISO, INPUT_PULLDOWN); + } - SPI.setPins(ONBOARD_LORA_MISO, ONBOARD_LORA_SCLK, ONBOARD_LORA_MOSI); - SPI.begin(); + void reset() { + digitalWrite(ONBOARD_LORA_RESET, LOW); + delay(1); + digitalWrite(ONBOARD_LORA_RESET, HIGH); + delay(5); + } - Module mod(ONBOARD_LORA_NSS, ONBOARD_LORA_DIO_1, ONBOARD_LORA_RESET, ONBOARD_LORA_BUSY, SPI); - SX1262 onboard(&mod); + bool waitBusy(bool high, uint32_t timeout) { + uint32_t start = millis(); + while ((digitalRead(ONBOARD_LORA_BUSY) != LOW) != high) { + if (millis() - start >= timeout) return false; + delay(1); + } + return true; + } - // BUSY is driven low in STDBY_RC and high while asleep, so it doubles as a - // non-invasive readback of what state we found the chip in and left it in. - pinMode(ONBOARD_LORA_BUSY, INPUT); - int busy_before = digitalRead(ONBOARD_LORA_BUSY); + void readVersion(char* version) { + SPI.beginTransaction(SPISettings(2000000, MSBFIRST, SPI_MODE0)); + digitalWrite(ONBOARD_LORA_NSS, LOW); + SPI.transfer(RADIOLIB_SX126X_CMD_READ_REGISTER); + SPI.transfer(RADIOLIB_SX126X_REG_VERSION_STRING >> 8); + SPI.transfer(RADIOLIB_SX126X_REG_VERSION_STRING & 0xFF); + SPI.transfer(0); + for (unsigned i = 0; i < 6; i++) version[i] = SPI.transfer(0); + digitalWrite(ONBOARD_LORA_NSS, HIGH); + SPI.endTransaction(); + } - // begin() resets the chip and leaves it in standby. A TCXO calibration failure - // still leaves SPI usable, so issue the sleep regardless of what it returns. - int status = onboard.begin(434.0f, 125.0f, 9, 7, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, - 0, 8, ONBOARD_LORA_TCXO_VOLTAGE, false); - int slept = onboard.sleep(false); // cold sleep: config discarded, RTC off - delay(2); - int busy_after = digitalRead(ONBOARD_LORA_BUSY); - MESH_DEBUG_PRINTLN("onboard SX1262: begin=%d sleep=%d busy %d->%d", - status, slept, busy_before, busy_after); + void releaseProbePulls() { + pinMode(ONBOARD_LORA_BUSY, INPUT); + pinMode(ONBOARD_LORA_MISO, INPUT); + } - SPI.end(); // release SPIM3 so std_init() can repin it onto the RAK13302 + void sleep() { + SPI.beginTransaction(SPISettings(2000000, MSBFIRST, SPI_MODE0)); + digitalWrite(ONBOARD_LORA_NSS, LOW); + SPI.transfer(RADIOLIB_SX126X_CMD_SET_SLEEP); + SPI.transfer(RADIOLIB_SX126X_SLEEP_START_COLD | RADIOLIB_SX126X_SLEEP_RTC_OFF); + digitalWrite(ONBOARD_LORA_NSS, HIGH); + SPI.endTransaction(); + delay(1); + } - // The SX1262 wakes on an NSS falling edge, so keep NSS driven high from here on. - digitalWrite(ONBOARD_LORA_NSS, HIGH); - pinMode(ONBOARD_LORA_NSS, OUTPUT); + void end() { + SPI.end(); + // INPUT leaves buffers enabled on floating pins; restore Nordic's disconnected default. + nrf_gpio_cfg_default(g_ADigitalPinMap[ONBOARD_LORA_SCLK]); + nrf_gpio_cfg_default(g_ADigitalPinMap[ONBOARD_LORA_MOSI]); + nrf_gpio_cfg_default(g_ADigitalPinMap[ONBOARD_LORA_MISO]); + nrf_gpio_cfg_default(g_ADigitalPinMap[ONBOARD_LORA_BUSY]); + // An NSS falling edge wakes the radio, so leave it driven high. + digitalWrite(ONBOARD_LORA_NSS, HIGH); + } +}; } -#endif bool radio_init() { rtc_clock.begin(Wire); -#ifdef RAK4631_ONBOARD_SX1262_SLEEP - sleep_onboard_radio(); -#endif + // Called once from setup, before SPI is started for the RAK13302. + OnboardRadioIO onboard; + const uint32_t started = millis(); + // The SX1262 version register shares the "SX1261" prefix used by RadioLib. + const auto state = rak3401::sleepOnboardRadio(onboard, RADIOLIB_SX1262_CHIP_TYPE); + const char* outcome = state == rak3401::OnboardRadioState::Absent ? "absent" : + state == rak3401::OnboardRadioState::Asleep ? "asleep" : + state == rak3401::OnboardRadioState::BusyTimeout ? "busy-timeout" : "sleep-failed"; + MESH_DEBUG_PRINTLN("onboard SX1262: %s elapsed=%lu ms", outcome, + (unsigned long)(millis() - started)); return radio.std_init(&SPI); } diff --git a/variants/rak3401/variant.h b/variants/rak3401/variant.h index 0576dadb..779cb3cd 100644 --- a/variants/rak3401/variant.h +++ b/variants/rak3401/variant.h @@ -175,10 +175,7 @@ static const uint8_t AREF = PIN_AREF; #define P_LORA_BUSY SX126X_BUSY #define P_LORA_RESET SX126X_RESET -// The RAK4631 core carries its own SX1262, which is unused when a RAK13302 -// supplies the RF path. RAK4631_ONBOARD_SX1262_SLEEP builds put it into cold -// sleep at boot; on a genuine RAK3401 these pins are unconnected. -#ifdef RAK4631_ONBOARD_SX1262_SLEEP +// Optional RAK4631 onboard radio; these internal pins are unused on RAK3401. #define ONBOARD_LORA_NSS (42) // P1.10 #define ONBOARD_LORA_SCLK (43) // P1.11 #define ONBOARD_LORA_MOSI (44) // P1.12 @@ -187,8 +184,6 @@ static const uint8_t AREF = PIN_AREF; #define ONBOARD_LORA_DIO_1 (47) // P1.15 #define ONBOARD_LORA_RESET (38) // P1.06 #define ONBOARD_LORA_ANT_SW (37) // P1.05, powers the onboard RF switch -#define ONBOARD_LORA_TCXO_VOLTAGE 1.8 -#endif // enables 3.3V periphery like GPS or IO Module // Do not toggle this for GPS power savings