From 61ae0920df567f97ad16565ff9ee446d390e3872 Mon Sep 17 00:00:00 2001 From: agessaman Date: Mon, 17 Aug 2026 21:04:06 -0700 Subject: [PATCH 1/2] feat(rak3401): cold-sleep RAK4631 onboard SX1262 on RAK13302 builds A RAK4631 core paired with a RAK13302 leaves the core's own SX1262 powered but never initialized, idling in STDBY_RC 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 issue a cold sleep, before std_init() repins SPI onto the RAK13302. Gated behind RAK4631_ONBOARD_SX1262_SLEEP and exposed through seven RAK_4631_13302_* envs, so RAK_3401_* artifacts are unchanged. The sleep is issued regardless of what begin() returns: Module::init() runs first, so SPI stays usable even if TCXO calibration fails. NSS is then held high, since the SX1262 wakes on an NSS falling edge. Verified on RAK19007 + RAK4631 + RAK13302: begin=0 sleep=0, RAK13302 init clean (device_errors=0x0000), TX and RX both confirmed. The current saving itself is derived from the datasheet (600 uA STDBY_RC -> 160 nA cold sleep) and is not measured. --- variants/rak3401/platformio.ini | 125 ++++++++++++++++++++++++++++++++ variants/rak3401/target.cpp | 33 +++++++++ variants/rak3401/variant.h | 15 ++++ 3 files changed, 173 insertions(+) diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index 48e7192266..56883e752d 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -134,3 +134,128 @@ 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 5309e6b22b..640b4d0271 100644 --- a/variants/rak3401/target.cpp +++ b/variants/rak3401/target.cpp @@ -32,8 +32,41 @@ 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 + + SPI.setPins(ONBOARD_LORA_MISO, ONBOARD_LORA_SCLK, ONBOARD_LORA_MOSI); + SPI.begin(); + + Module mod(ONBOARD_LORA_NSS, ONBOARD_LORA_DIO_1, ONBOARD_LORA_RESET, ONBOARD_LORA_BUSY, SPI); + SX1262 onboard(&mod); + + // 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 + MESH_DEBUG_PRINTLN("onboard SX1262: begin=%d sleep=%d", status, slept); + + SPI.end(); // release SPIM3 so std_init() can repin it onto the RAK13302 + + // 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); +} +#endif + bool radio_init() { rtc_clock.begin(Wire); +#ifdef RAK4631_ONBOARD_SX1262_SLEEP + sleep_onboard_radio(); +#endif return radio.std_init(&SPI); } diff --git a/variants/rak3401/variant.h b/variants/rak3401/variant.h index 9882788608..0576dadb8c 100644 --- a/variants/rak3401/variant.h +++ b/variants/rak3401/variant.h @@ -175,6 +175,21 @@ 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 +#define ONBOARD_LORA_NSS (42) // P1.10 +#define ONBOARD_LORA_SCLK (43) // P1.11 +#define ONBOARD_LORA_MOSI (44) // P1.12 +#define ONBOARD_LORA_MISO (45) // P1.13 +#define ONBOARD_LORA_BUSY (46) // P1.14 +#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 #define PIN_3V3_EN (34) From 51cbab53110fea91a63f5fb77d97d20b07240d6b Mon Sep 17 00:00:00 2001 From: agessaman Date: Mon, 17 Aug 2026 22:19:49 -0700 Subject: [PATCH 2/2] feat(rak3401): report onboard SX1262 sleep state via BUSY BUSY is driven low in STDBY_RC and high while asleep, so reading it either side of the sleep gives a non-invasive readback of the state the chip was found in and the state it was left in. Confirmed on hardware. This is what makes the sleep observable at all. The only other symptom of a failed sleep is ~600 uA, which is below what an inline USB meter can resolve, so without this a regression would be silent. It immediately earned itself: busy_before reads 0 on every boot, showing the cold sleep does not survive an MCU reset. GPIOs revert to input during reset, NSS stops being driven high, and the falling edge wakes the chip. The helper re-sleeps it, so this is self-healing, but it means the chip is never found already asleep. --- variants/rak3401/target.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/variants/rak3401/target.cpp b/variants/rak3401/target.cpp index 640b4d0271..dac12ede70 100644 --- a/variants/rak3401/target.cpp +++ b/variants/rak3401/target.cpp @@ -47,12 +47,20 @@ static void sleep_onboard_radio() { Module mod(ONBOARD_LORA_NSS, ONBOARD_LORA_DIO_1, ONBOARD_LORA_RESET, ONBOARD_LORA_BUSY, SPI); SX1262 onboard(&mod); + // 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); + // 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 - MESH_DEBUG_PRINTLN("onboard SX1262: begin=%d sleep=%d", status, slept); + 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); SPI.end(); // release SPIM3 so std_init() can repin it onto the RAK13302