Sleep unused onboard SX1262 in RAK13302 builds

Adapt the optional onboard-radio probe for RAK3401 and RAK4631 cores using the external RAK13302 image. Preserve SPI1 radio ownership, external FEM startup, and ordinary RAK4631 primary-radio operation.

Probe before boot voltage protection, bound missing-chip waits, release temporary pads, and confirm cold sleep without waking the chip. Add sanitizer-backed host regression tests and CI coverage.
This commit is contained in:
mikecarper committed 2026-10-17 17:02:54 -07:00
1 parent ff548d2481
commit 3d04bb3048
7 files changed
+666

No files matched your search

+1
View File
@@ -610,6 +610,7 @@ jobs:
python3 -B test/test_gps_sync_preferences.py -v
python3 -B test/test_rak_gps_power.py -v
python3 -B test/test_rak_shutdown_power.py -v
python3 -B test/test_rak3401_onboard_radio.py -v
python3 -B test/test_xiao_battery_safety.py -v
python3 -B test/test_peripheral_mode_prefs.py -v
python3 -B test/test_t1000e_sensor_power.py -v
+29
View File
@@ -106,6 +106,35 @@ The [nRF52 idle-sleep investigation](nrf52_sleep_event_validation.md) describes
an eligible-but-not-sleeping regression, its fix, controlled hardware evidence
and the limits of CPU-state sampling without a current meter.
## RAK13302 builds with an unused onboard radio
The RAK3401/external RAK13302 firmware now probes the private onboard-radio
bus once during startup. This supports both a RAK3401 core (no onboard SX1262)
and a RAK4631 core using the external RAK13302. It is adapted from
[agessaman's optional-radio sleep change](https://github.com/agessaman/MeshCore/tree/feat/rak13302-auto-onboard-sleep).
[RAK's internal connection table](https://docs.rakwireless.com/product-categories/wisblock/rak4631/datasheet/#internal-connections-between-nrf52840-mcu-and-sx1262-lora-transceiver)
identifies the private SPI, reset, BUSY and RF-switch pins used by the probe.
A matching six-byte chip signature triggers cold sleep with the RTC disabled.
The RF switch stays off, NSS/reset stay high, and the temporary SPI/data/BUSY
pads are released. Sleep confirmation reads BUSY only: another SPI transaction
would wake the chip. Each BUSY wait is bounded to 20 ms, including when no chip
is fitted. A timeout or mismatched signature does not block startup of the
external radio. Debug-enabled startup logging reports `asleep`, `absent`,
`busy-timeout` or `sleep-failed` when the USB debug output is available; a
missing chip normally produces `busy-timeout`, not a verified sleep result.
This happens before the boot-voltage check, so a low-voltage shutdown cannot
skip the unused-radio sleep attempt. It does not cycle the shared GPS/storage
rail, change external-radio recovery, or enable its FEM before initialization
succeeds. There is no runtime probing or automatic primary-radio selection.
Ordinary **RAK4631 firmware is unchanged**: its onboard SX1262 is the active
primary radio and must not be disabled. Use the external RAK13302 image for
that hardware arrangement, not a normal RAK4631 image. This startup change has
host regression tests; sleep-current improvement still needs matched current
measurements on the physical core/module combination.
## Board regulator initialization
RAK4631 Companion uses the same nRF52840 REG1 DC/DC initialization as the other
+481
View File
@@ -0,0 +1,481 @@
#!/usr/bin/env python3
"""Exercise the optional RAK4631 radio probe on the host, without PIO or RF."""
from pathlib import Path
import os
import re
import shutil
import subprocess
import sys
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
VARIANT = ROOT / "variants/rak3401"
def numeric_define(source, name):
match = re.search(rf"^\s*#define\s+{name}\s+\(?\s*(\d+)\s*\)?", source, re.MULTILINE)
if match is None:
raise AssertionError(f"missing numeric definition for {name}")
return int(match.group(1))
def compile_source(testcase, source, *, expect_success=True):
compiler = shutil.which("c++") or shutil.which("g++")
testcase.assertIsNotNone(compiler, "this host-only suite requires a C++ compiler")
with tempfile.TemporaryDirectory(prefix="rak3401-onboard-radio-") as folder:
work = Path(folder)
generated = work / "case.cpp"
generated.write_text(source, encoding="ascii")
binary = work / ("case.exe" if os.name == "nt" else "case")
command = [compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-I", str(ROOT), str(generated), "-o", str(binary)]
if sys.platform.startswith("linux"):
command[1:1] = ["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-omit-frame-pointer", "-fno-pie", "-no-pie"]
built = subprocess.run(command, capture_output=True, text=True, timeout=60)
testcase.assertEqual(built.returncode, 0, built.stdout + built.stderr)
ran = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
if expect_success:
testcase.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
else:
testcase.assertNotEqual(ran.returncode, 0, "negative control did not fail")
def onboard_defines():
variant = (VARIANT / "variant.h").read_text()
return "\n".join(f"#define ONBOARD_LORA_{name} {numeric_define(variant, 'ONBOARD_LORA_' + name)}"
for name in ("NSS", "SCLK", "MOSI", "MISO", "BUSY", "RESET", "ANT_SW"))
ADAPTER_MOCKS = r'''
#include <array>
#include <cassert>
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
#include "variants/rak3401/OnboardRadioSleep.h"
#define HIGH 1
#define LOW 0
#define INPUT 0
#define OUTPUT 1
#define INPUT_PULLUP 2
#define INPUT_PULLDOWN 3
#define MSBFIRST 1
#define SPI_MODE0 0
#define RADIOLIB_SX126X_CMD_READ_REGISTER 0x1d
#define RADIOLIB_SX126X_REG_VERSION_STRING 0x0320
#define RADIOLIB_SX126X_CMD_SET_SLEEP 0x84
#define RADIOLIB_SX126X_SLEEP_START_COLD 0
#define RADIOLIB_SX126X_SLEEP_RTC_OFF 0
#define RADIOLIB_SX1262_CHIP_TYPE "SX1261"
template <typename... Args> void debug(const char*, Args...) {}
#define MESH_DEBUG_PRINTLN(...) debug(__VA_ARGS__)
enum class Scenario { Present, MissingBusy, WrongVersion, ZeroVersion,
FloatingVersion, BusyAfterRead, SleepRejected };
struct Event { std::string name; unsigned pin; int value; };
struct Hardware {
Scenario scenario = Scenario::Present;
uint32_t clock = 0;
uint32_t elapsed = 0;
std::array<int, 48> modes;
std::array<int, 48> levels;
std::array<unsigned, 48> defaults{};
std::vector<Event> events;
std::vector<std::vector<uint8_t>> frames;
std::vector<uint8_t> frame;
bool version_read = false;
bool sleep_issued = false;
bool reset_low_seen = false;
bool transaction = false;
Hardware() { modes.fill(-1); levels.fill(-1); }
} hw;
std::array<uint32_t, 48> pin_map() {
std::array<uint32_t, 48> pins{};
for (unsigned i = 0; i < pins.size(); ++i) pins[i] = i;
return pins;
}
const std::array<uint32_t, 48> g_ADigitalPinMap = pin_map();
void event(const char* name, unsigned pin = 0, int value = 0) {
hw.events.push_back({name, pin, value});
}
void pinMode(unsigned pin, int mode) {
hw.modes.at(pin) = mode;
event("mode", pin, mode);
}
void digitalWrite(unsigned pin, int value) {
if (pin == ONBOARD_LORA_RESET && value == LOW) {
assert(hw.modes.at(pin) == OUTPUT && hw.levels.at(pin) == HIGH);
hw.reset_low_seen = true;
}
if (pin == ONBOARD_LORA_NSS && value == LOW) {
assert(hw.transaction && !hw.sleep_issued);
}
if (pin == ONBOARD_LORA_NSS && value == HIGH && hw.transaction &&
!hw.frame.empty() && hw.frame.front() == RADIOLIB_SX126X_CMD_SET_SLEEP) {
assert((hw.frame == std::vector<uint8_t>{RADIOLIB_SX126X_CMD_SET_SLEEP, 0}));
assert(hw.modes.at(ONBOARD_LORA_BUSY) == INPUT);
assert(hw.modes.at(ONBOARD_LORA_MISO) == INPUT);
hw.sleep_issued = true;
}
hw.levels.at(pin) = value;
event("write", pin, value);
}
int digitalRead(unsigned pin) {
assert(pin == ONBOARD_LORA_BUSY);
assert(hw.modes.at(pin) == INPUT || hw.modes.at(pin) == INPUT_PULLUP);
event("read", pin);
if (hw.scenario == Scenario::MissingBusy) return HIGH;
if (hw.sleep_issued) return hw.scenario == Scenario::SleepRejected ? LOW : HIGH;
if (hw.version_read && hw.scenario == Scenario::BusyAfterRead) return HIGH;
return LOW;
}
uint32_t millis() { return hw.clock; }
void delay(uint32_t interval) {
hw.clock += interval;
hw.elapsed += interval;
assert(hw.elapsed <= 100); // Infinite or wrap-unsafe waits fail promptly.
event("delay", 0, static_cast<int>(interval));
}
void nrf_gpio_cfg_default(uint32_t pin) {
assert(pin == ONBOARD_LORA_SCLK || pin == ONBOARD_LORA_MOSI ||
pin == ONBOARD_LORA_MISO || pin == ONBOARD_LORA_BUSY);
hw.modes.at(pin) = 99;
++hw.defaults.at(pin);
event("default", pin);
}
struct SPISettings {
SPISettings(unsigned frequency, unsigned bit_order, unsigned mode) {
assert(frequency == 2000000 && bit_order == MSBFIRST && mode == SPI_MODE0);
}
};
struct SPIClass {
std::string name;
bool begun = false;
bool pins_set = false;
explicit SPIClass(const char* value) : name(value) {}
void check_owner() const { assert(name == "SPI"); }
void setPins(unsigned miso, unsigned sclk, unsigned mosi) {
check_owner();
assert(!begun && miso == ONBOARD_LORA_MISO &&
sclk == ONBOARD_LORA_SCLK && mosi == ONBOARD_LORA_MOSI);
pins_set = true;
event("spi-pins");
}
void begin() {
check_owner();
assert(pins_set && !begun);
assert(hw.levels.at(ONBOARD_LORA_NSS) == HIGH);
assert(hw.modes.at(ONBOARD_LORA_NSS) == OUTPUT);
assert(hw.levels.at(ONBOARD_LORA_RESET) == HIGH);
assert(hw.modes.at(ONBOARD_LORA_RESET) == OUTPUT);
assert(hw.levels.at(ONBOARD_LORA_ANT_SW) == LOW);
assert(hw.modes.at(ONBOARD_LORA_ANT_SW) == OUTPUT);
assert(hw.modes.at(ONBOARD_LORA_BUSY) == INPUT_PULLUP);
begun = true;
event("spi-begin");
}
void beginTransaction(SPISettings) {
check_owner();
assert(begun && !hw.transaction && !hw.sleep_issued);
assert(hw.levels.at(ONBOARD_LORA_NSS) == HIGH);
hw.transaction = true;
hw.frame.clear();
event("transaction");
}
uint8_t transfer(uint8_t value) {
check_owner();
assert(begun && hw.transaction && !hw.sleep_issued);
assert(hw.levels.at(ONBOARD_LORA_NSS) == LOW && hw.reset_low_seen);
hw.frame.push_back(value);
event("transfer", 0, value);
if (hw.frame.front() != RADIOLIB_SX126X_CMD_READ_REGISTER || hw.frame.size() < 5)
return 0;
assert(hw.frame.size() <= 10);
assert(hw.modes.at(ONBOARD_LORA_MISO) == INPUT_PULLDOWN);
const unsigned index = static_cast<unsigned>(hw.frame.size() - 5);
if (hw.scenario == Scenario::ZeroVersion) return 0;
if (hw.scenario == Scenario::FloatingVersion) return 0xff;
if (hw.scenario == Scenario::WrongVersion) return static_cast<uint8_t>("SX126X"[index]);
return static_cast<uint8_t>("SX1261"[index]);
}
void endTransaction() {
check_owner();
assert(begun && hw.transaction && hw.levels.at(ONBOARD_LORA_NSS) == HIGH);
if (hw.frame.front() == RADIOLIB_SX126X_CMD_READ_REGISTER) {
assert((hw.frame == std::vector<uint8_t>{0x1d, 3, 0x20, 0, 0, 0, 0, 0, 0, 0}));
hw.version_read = true;
}
hw.frames.push_back(hw.frame);
hw.transaction = false;
event("transaction-end");
}
void end() {
check_owner();
assert(begun && !hw.transaction);
begun = false;
event("spi-end");
}
} SPI("SPI"), SPI1("SPI1");
'''
ADAPTER_MAIN = r'''
size_t first(const char* name) {
for (size_t i = 0; i < hw.events.size(); ++i)
if (hw.events[i].name == name) return i;
return hw.events.size();
}
int main() {
for (uint32_t start : {0u, UINT32_MAX - 10u}) {
for (Scenario scenario : {Scenario::Present, Scenario::MissingBusy, Scenario::WrongVersion,
Scenario::ZeroVersion, Scenario::FloatingVersion, Scenario::BusyAfterRead,
Scenario::SleepRejected}) {
hw = Hardware(); hw.clock = start; hw.scenario = scenario;
SPI.begun = SPI.pins_set = SPI1.begun = SPI1.pins_set = false;
const auto result = rak3401::sleepOnboardRadio();
using State = rak3401::OnboardRadioState;
const auto expected = scenario == Scenario::Present ? State::Asleep :
scenario == Scenario::MissingBusy || scenario == Scenario::BusyAfterRead ? State::BusyTimeout :
scenario == Scenario::SleepRejected ? State::SleepFailed : State::Absent;
assert(result == expected);
assert(!SPI.begun && !SPI1.begun && !SPI1.pins_set && !hw.transaction);
assert(first("spi-pins") < first("spi-begin"));
assert(first("spi-begin") < first("spi-end"));
for (unsigned pin : {ONBOARD_LORA_SCLK, ONBOARD_LORA_MOSI,
ONBOARD_LORA_MISO, ONBOARD_LORA_BUSY}) {
assert(hw.modes.at(pin) == 99 && hw.defaults.at(pin) == 1);
for (size_t i = 0; i < hw.events.size(); ++i)
if (hw.events[i].name == "default" && hw.events[i].pin == pin)
assert(first("spi-end") < i);
}
for (unsigned pin : {ONBOARD_LORA_NSS, ONBOARD_LORA_RESET}) {
assert(hw.modes.at(pin) == OUTPUT && hw.levels.at(pin) == HIGH);
assert(hw.defaults.at(pin) == 0);
}
assert(hw.modes.at(ONBOARD_LORA_ANT_SW) == OUTPUT);
assert(hw.levels.at(ONBOARD_LORA_ANT_SW) == LOW);
// The optional radio cannot touch shared slot power, the primary radio,
// GPS, storage CS, or the internal radio's unused DIO1 interrupt pin.
for (unsigned pin : {3u, 4u, 9u, 10u, 15u, 16u, 17u, 21u, 26u, 29u, 30u, 31u, 34u, 47u})
assert(hw.modes.at(pin) == -1 && hw.levels.at(pin) == -1 && hw.defaults.at(pin) == 0);
const bool should_sleep = scenario == Scenario::Present || scenario == Scenario::SleepRejected;
assert(hw.sleep_issued == should_sleep);
assert(hw.frames.size() == (scenario == Scenario::MissingBusy ? 0u : should_sleep ? 2u : 1u));
if (scenario == Scenario::MissingBusy || scenario == Scenario::BusyAfterRead ||
scenario == Scenario::SleepRejected) assert(hw.elapsed >= 20);
assert(hw.elapsed <= 70);
assert(static_cast<uint32_t>(hw.clock - start) == hw.elapsed);
}
}
}
'''
def adapter_source(mutation=None):
target = (VARIANT / "target.cpp").read_text()
adapter = extract_braced(target, "class OnboardRadioIO") + ";"
if mutation == "probe-pulls-retained":
adapter = adapter.replace("pinMode(ONBOARD_LORA_BUSY, INPUT);", "// negative control")
elif mutation == "bus-left-active":
adapter = adapter.replace("SPI.end();", "// negative control")
elif mutation == "nss-wakes-radio":
before, after = adapter.rsplit("digitalWrite(ONBOARD_LORA_NSS, HIGH);", 1)
adapter = before + "digitalWrite(ONBOARD_LORA_NSS, LOW);" + after
function = extract_braced(target, "rak3401::OnboardRadioState rak3401::sleepOnboardRadio()")
return onboard_defines() + ADAPTER_MOCKS + adapter + "\n" + function + ADAPTER_MAIN
STARTUP_MOCKS = r'''
#include <array>
#include <cassert>
#include <cstdint>
#include <string>
#include <vector>
#include "variants/rak3401/OnboardRadioSleep.h"
#define HIGH 1
#define LOW 0
#define INPUT 0
#define OUTPUT 1
#define INPUT_PULLUP 2
#define NRF52_POWER_MANAGEMENT 1
#define PIN_VBAT_READ 5
#define PIN_USER_BTN 33
#define PIN_USER_BTN_ANA 31
#define PIN_BOARD_SDA 13
#define PIN_BOARD_SCL 14
#define PIN_3V3_EN 34
#define SX126X_POWER_EN 21
#define P_LORA_NSS 26
#define P_LORA_BUSY 9
#define P_LORA_RESET 4
#define SHUTDOWN_REASON_LOW_VOLTAGE 0x4c
#define SHUTDOWN_REASON_BOOT_PROTECT 0x42
struct Event { std::string name; unsigned pin; int value; };
std::vector<Event> events;
std::array<int, 48> levels;
std::array<int, 48> modes;
bool permit_boot = true;
bool primary_busy = false;
uint32_t clock_ms = 0;
rak3401::OnboardRadioState radio_state;
void event(const char* name, unsigned pin = 0, int value = 0) {
events.push_back({name, pin, value});
}
void pinMode(unsigned pin, int mode) { modes.at(pin) = mode; event("mode", pin, mode); }
void digitalWrite(unsigned pin, int value) { levels.at(pin) = value; event("write", pin, value); }
int digitalRead(unsigned pin) { assert(pin == P_LORA_BUSY); return primary_busy ? HIGH : LOW; }
uint32_t millis() { return clock_ms; }
void delay(uint32_t value) { clock_ms += value; assert(clock_ms <= 200); }
struct PowerMgtConfig { uint8_t lpcomp_ain_channel = 3, lpcomp_refsel = 4; } power_config;
struct SystemOff { uint8_t reason; };
class NRF52BoardDCDC {
public:
void begin() { event("dcdc"); }
virtual void initiateShutdown(uint8_t) = 0;
void checkBootVoltage(const PowerMgtConfig*) {
event("voltage");
if (!permit_boot) initiateShutdown(SHUTDOWN_REASON_BOOT_PROTECT);
}
void configureVoltageWake(uint8_t ain, uint8_t ref) {
assert(ain == 3 && ref == 4); event("voltage-wake");
}
void enterSystemOff(uint8_t reason) { event("systemoff"); throw SystemOff{reason}; }
void serviceWatchdog() { event("watchdog"); }
};
class RAK3401Board : public NRF52BoardDCDC {
public:
void begin();
bool recoverRadio();
void enableRadioFrontend();
void initiateShutdown(uint8_t reason) override;
};
struct WireMock {
void setPins(unsigned sda, unsigned scl) { assert(sda == 13 && scl == 14); event("wire-pins"); }
void begin() { event("wire-begin"); }
} Wire;
rak3401::OnboardRadioState rak3401::sleepOnboardRadio() {
event("onboard");
return radio_state;
}
'''
STARTUP_MAIN = r'''
size_t first(const char* name) {
for (size_t i = 0; i < events.size(); ++i)
if (events[i].name == name) return i;
return events.size();
}
void prepare() {
events.clear(); modes.fill(-1); levels.fill(-1); clock_ms = 0; primary_busy = false;
// initVariant() already holds the shared rail up and external FEM down.
levels.at(PIN_3V3_EN) = HIGH; levels.at(SX126X_POWER_EN) = LOW;
}
int main() {
using State = rak3401::OnboardRadioState;
for (State state : {State::Asleep, State::Absent, State::BusyTimeout, State::SleepFailed}) {
for (bool boot : {true, false}) {
prepare(); radio_state = state; permit_boot = boot; RAK3401Board board;
bool stopped = false;
try { board.begin(); } catch (const SystemOff& off) {
assert(off.reason == SHUTDOWN_REASON_BOOT_PROTECT); stopped = true;
}
assert(stopped == !boot);
assert(first("dcdc") == 0 && first("onboard") == 1);
assert(first("onboard") < first("voltage"));
assert(levels.at(SX126X_POWER_EN) == LOW);
if (boot) {
assert(levels.at(PIN_3V3_EN) == HIGH);
assert(first("systemoff") == events.size());
assert(board.recoverRadio());
assert(levels.at(SX126X_POWER_EN) == LOW);
assert(levels.at(PIN_3V3_EN) == HIGH);
board.enableRadioFrontend();
assert(levels.at(SX126X_POWER_EN) == HIGH);
// A later failed external-radio reset must keep shared peripherals up
// and the external frontend quiescent, without reproving the optional radio.
primary_busy = true;
assert(!board.recoverRadio());
assert(levels.at(PIN_3V3_EN) == HIGH && levels.at(SX126X_POWER_EN) == LOW);
unsigned probes = 0;
for (const auto& e : events) if (e.name == "onboard") ++probes;
assert(probes == 1);
} else {
assert(first("voltage") < first("voltage-wake"));
assert(first("voltage-wake") < first("systemoff"));
assert(levels.at(PIN_3V3_EN) == LOW);
}
}
}
}
'''
def startup_source(mutation=None):
board = (VARIANT / "RAK3401Board.cpp").read_text()
methods = "\n".join(extract_braced(board, signature) for signature in (
"void RAK3401Board::begin()", "void RAK3401Board::initiateShutdown(uint8_t reason)",
"bool RAK3401Board::recoverRadio()", "void RAK3401Board::enableRadioFrontend()"))
if mutation == "probe-after-voltage":
methods = methods.replace("rak3401::sleepOnboardRadio();", "// negative control")
methods = methods.replace("checkBootVoltage(&power_config);",
"checkBootVoltage(&power_config); rak3401::sleepOnboardRadio();")
return STARTUP_MOCKS + methods + STARTUP_MAIN
class Rak3401OnboardRadioTests(unittest.TestCase):
def test_actual_adapter_and_template_all_outcomes_and_timer_wrap(self):
for debug_enabled in (0, 1):
with self.subTest(debug=debug_enabled):
compile_source(self, f"#define MESH_DEBUG {debug_enabled}\n" + adapter_source())
def test_gpio_cleanup_and_sleep_readback_negative_controls(self):
for mutation in ("probe-pulls-retained", "bus-left-active", "nss-wakes-radio"):
with self.subTest(mutation=mutation):
compile_source(self, adapter_source(mutation), expect_success=False)
def test_real_startup_protects_low_voltage_and_preserves_external_recovery(self):
compile_source(self, startup_source())
def test_boot_voltage_order_negative_control(self):
compile_source(self, startup_source("probe-after-voltage"), expect_success=False)
def test_optional_pin_map_matches_the_rak4631_internal_radio(self):
external_variant = (VARIANT / "variant.h").read_text()
internal_variant = (ROOT / "variants/rak4631/variant.h").read_text()
for optional, active in (("NSS", "P_LORA_NSS"), ("SCLK", "P_LORA_SCLK"),
("MOSI", "P_LORA_MOSI"), ("MISO", "P_LORA_MISO"),
("BUSY", "P_LORA_BUSY"), ("RESET", "P_LORA_RESET"),
("DIO_1", "P_LORA_DIO_1"),
("ANT_SW", "SX126X_POWER_EN")):
with self.subTest(pin=optional):
self.assertEqual(numeric_define(external_variant, "ONBOARD_LORA_" + optional),
numeric_define(internal_variant, active))
def test_standard_rak4631_keeps_its_active_radio(self):
board = (ROOT / "variants/rak4631/RAK4631Board.cpp").read_text()
target = (ROOT / "variants/rak4631/target.cpp").read_text()
self.assertNotIn("sleepOnboardRadio", board + target)
self.assertNotIn("ONBOARD_LORA_", board + target)
begin = extract_braced(board, "void RAK4631Board::begin()")
self.assertIn("digitalWrite(SX126X_POWER_EN, HIGH);", begin)
init = extract_braced(target, "bool radio_init()")
self.assertIn("radio.std_init(&SPI)", init)
self.assertIn("P_LORA_RESET, P_LORA_BUSY, SPI)", target)
# External-radio construction and retry must retain their SPI1 owner.
external_target = (VARIANT / "target.cpp").read_text()
self.assertIn("P_LORA_RESET, P_LORA_BUSY, SPI1)", external_target)
external_init = extract_braced(external_target, "bool radio_init()")
self.assertIn("radio.std_init(&SPI1)", external_init)
self.assertNotIn("sleepOnboardRadio", external_init)
if __name__ == "__main__":
unittest.main()
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include <string.h>
// Adapted from agessaman/MeshCore feat/rak13302-auto-onboard-sleep
// (d58042d8d25fbc9177f1b393ba0e393e3e5b276f).
namespace rak3401 {
enum class OnboardRadioState { Absent, Asleep, BusyTimeout, SleepFailed };
// Startup only, before radio, storage or other SPI users have begun. The
// RAK3401 image also supports a RAK4631 core using the external RAK13302.
OnboardRadioState sleepOnboardRadio();
// IO owns the temporary SPI setup and must release it on every outcome.
template <typename IO>
OnboardRadioState sleepOnboardRadio(IO& io, const char* expectedVersion) {
io.begin();
io.reset();
const auto state = [&]() {
// A missing chip also times out: the probe pull keeps floating BUSY high.
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();
// Never read SPI to confirm sleep: an NSS falling edge wakes the chip.
return io.waitBusy(true, 20) ? OnboardRadioState::Asleep : OnboardRadioState::SleepFailed;
}();
io.end();
return state;
}
} // namespace rak3401
+4
View File
@@ -4,6 +4,7 @@
#include "RAK3401Board.h"
#include "target.h"
#include "OnboardRadioSleep.h"
#include "../../src/helpers/nrf52/TerminalPeripheralShutdown.h"
#ifdef NRF52_POWER_MANAGEMENT
@@ -82,6 +83,9 @@ void RAK3401Board::shutdownPeripherals() {
void RAK3401Board::begin() {
NRF52BoardDCDC::begin();
// Also handles a RAK4631 core with an external RAK13302. Do this before
// voltage protection can enter SYSTEMOFF and leave the unused chip awake.
rak3401::sleepOnboardRadio();
pinMode(PIN_VBAT_READ, INPUT);
#ifdef PIN_USER_BTN
pinMode(PIN_USER_BTN, INPUT_PULLUP);
+101
View File
@@ -1,10 +1,111 @@
#include <Arduino.h>
#include "target.h"
#include "OnboardRadioSleep.h"
#include <helpers/ArduinoHelpers.h>
#include <nrf_gpio.h>
#ifdef USE_CC310_HW_CRYPTO
#include <helpers/NRF52Crypto.h>
#endif
namespace {
// This is the optional private radio bus, NOT the external RAK13302's SPI1.
// It is only probed during board startup, before any other bus owner starts.
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);
pinMode(ONBOARD_LORA_BUSY, INPUT_PULLUP);
SPI.setPins(ONBOARD_LORA_MISO, ONBOARD_LORA_SCLK, ONBOARD_LORA_MOSI);
SPI.begin();
// Reject a floating MISO rather than mistaking it for a version string.
pinMode(ONBOARD_LORA_MISO, INPUT_PULLDOWN);
}
void reset() {
digitalWrite(ONBOARD_LORA_RESET, LOW);
delay(1);
digitalWrite(ONBOARD_LORA_RESET, HIGH);
delay(5);
}
bool waitBusy(bool high, uint32_t timeout) {
const uint32_t started = millis();
while ((digitalRead(ONBOARD_LORA_BUSY) != LOW) != high) {
if (millis() - started >= timeout) return false;
delay(1);
}
return true;
}
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();
}
void releaseProbePulls() {
pinMode(ONBOARD_LORA_BUSY, INPUT);
pinMode(ONBOARD_LORA_MISO, INPUT);
}
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);
}
void end() {
SPI.end();
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]);
// Retain NSS/reset HIGH and RF switch LOW. Releasing NSS can wake the
// sleeping SX1262; only its clock/data/BUSY pads should be disconnected.
digitalWrite(ONBOARD_LORA_NSS, HIGH);
}
};
static_assert(ONBOARD_LORA_MISO == PIN_SPI_MISO &&
ONBOARD_LORA_MOSI == PIN_SPI_MOSI &&
ONBOARD_LORA_SCLK == PIN_SPI_SCK,
"The optional radio probe must leave SPI on its default pins");
} // namespace
rak3401::OnboardRadioState rak3401::sleepOnboardRadio() {
OnboardRadioIO onboard;
#if MESH_DEBUG
const uint32_t started = millis();
#endif
// RadioLib identifies the SX1262 by the same six-byte SX1261 prefix.
const auto state = rak3401::sleepOnboardRadio(onboard, RADIOLIB_SX1262_CHIP_TYPE);
#if MESH_DEBUG
const char* outcome = state == OnboardRadioState::Absent ? "absent" :
state == OnboardRadioState::Asleep ? "asleep" :
state == OnboardRadioState::BusyTimeout ? "busy-timeout" : "sleep-failed";
MESH_DEBUG_PRINTLN("onboard SX1262: %s elapsed=%lu ms", outcome,
static_cast<unsigned long>(millis() - started));
#endif
return state;
}
RAK3401Board board;
#ifndef PIN_USER_BTN
+12
View File
@@ -122,6 +122,18 @@ static const uint8_t AREF = PIN_AREF;
#define PIN_SPI1_MOSI (30)
#define PIN_SPI1_SCK (3)
// Optional private SX1262 on a RAK4631 core running this external-radio image.
// These pads are unconnected on a RAK3401 core. The external RAK13302 remains
// on SPI1; ordinary RAK4631 firmware instead uses its onboard chip as primary.
#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
static const uint8_t SS = 42;
static const uint8_t MOSI = PIN_SPI_MOSI;
static const uint8_t MISO = PIN_SPI_MISO;