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