Skip to content

[Feature Request] SYSTEMOFF/Transport mode for RAK4631 and equivalent #1430

@kevinmeister

Description

@kevinmeister

A feature to turn off or set the RAK Wisblocks into a transport mode would make transport, experimenting with different locations and or changing of antennas easier. If you have a RAK4631 powered repeater in a water-resistant enclosure it is a pain, always removing all the screws just to disconnect the battery. I know one could add a button for that purpose too but if that button does not fit the whole design, I think this would be a great solution.

I therefore created a UF2 file which can be added via DFU mode and put the device in a deep sleep mode were no tx and rx is happening. It is similar or probably the same mode the RAK Wismesh Mini Repeater comes from the factory. To turn it back on just simply connect it via USB, enter DFU mode and put the recent or last used Meshcore firmware back on it. All settings stay saved on the device.

This feature could be add either via CLI command, on the Web Flasher application as additional option. I assume it would be the best option to add it in future firmwares as a CLI command, which could make it leaving the deep sleep/transport mode and booting up the firmware again by itself without manual DFU transfer.

Measurements showed a current of ~4μA in transport mode.

Here is my short summary:

Purpose

This UF2 firmware is a “transport mode” image for the RAK4631 (nRF52840 + SX1262) used on WisBlock baseboards (e.g., RAK19003 / RAK19007). Its goal is to reduce current consumption to the lowest possible level and guarantee that the device cannot transmit, without adding a hardware power switch.

It achieves this by explicitly putting the SX1262 LoRa transceiver and the RAK1901 (SHTC3) sensor into their lowest-power states, then placing the nRF52840 into SYSTEMOFF (true hardware deep power-off).

What the firmware does (exact sequence)

On boot, the application performs the following steps and then never returns:

1. Put the RAK1901 (Sensirion SHTC3) into sleep over I2C

  • Initializes I2C (Wire.begin()), sets 400 kHz clock.

  • Sends the SHTC3 Wake-up command 0x3517 to ensure a deterministic state.

  • Sends the SHTC3 Sleep command 0xB098.

  • Calls Wire.end() to stop the I2C peripheral.

  • Forces the I2C pins to high-impedance with no pulls (nRF GPIO input, NOPULL) to prevent leakage via external pull-ups during SYSTEMOFF.

2. Put the SX1262 LoRa radio into sleep

  • Configures the SX1262 control pins and uses a minimal bit-banged SPI transaction (SPI mode 0).

  • Pulses SX1262 NRESET (reset low → high).

  • Waits for BUSY to deassert (with a timeout).

  • Issues SX1262 SetSleep command: opcode 0x84 followed by parameter 0x00.

  • This prevents the common ~1.5–1.7 mA “radio standby” current if the SX1262 was left in standby by previous firmware.

3. Enter nRF52840 SYSTEMOFF

  • Writes NRF_POWER->SYSTEMOFF = 1;

  • This is true hardware power-off: CPU halted, RAM off, radio off, timers off. Current becomes dominated only by baseboard leakage/quiescent current.

Expected behavior

  • After flashing, the board typically reboots once, then appears “dead”:

    • No USB serial
    • No LEDs (unless the baseboard has always-on LED circuitry)
    • No RF activity
  • The device can only “wake” by:

    • reconnecting USB / applying power in a way that triggers reset
    • pressing RESET (if wired appropriately by the baseboard/bootloader)

This is intentional: it is a transport/storage mode.

Why this exists (MeshCore context)

Some firmware stacks (including MeshCore and other LoRa stacks) do not provide a “true off” option, and even if the MCU enters a sleep mode, the SX1262 may remain in standby, consuming ~1.6 mA. This UF2 provides a deterministic way to:

guarantee no transmission

minimize storage drain (µA-level possible depending on baseboard and attached modules)

avoid adding a physical switch

Hardware assumptions / pin mapping

This implementation assumes the standard RAK4631 internal wiring:

  • SX1262:

    • NSS: P1.10
    • SCK: P1.11
    • MOSI: P1.12
    • MISO: P1.13
    • BUSY: P1.14
    • NRESET: P1.06
  • I2C (RAK4631 default):

    • SDA: P0.26
    • SCL: P0.27

(These are the standard RAK4631/WisBlock mappings used by the RAK Arduino core.)

Safety note (antenna handling)

In SYSTEMOFF, the nRF52840 cannot execute code and cannot start RF transmissions, and the SX1262 is commanded into sleep. Therefore, the device cannot transmit; removing the antenna is safe in this state.

Files provided

  • Arduino sketch/source: transport-mode firmware (SHTC3 sleep + I2C pins Hi-Z + SX1262 sleep + nRF SYSTEMOFF)
  • UF2: built for nRF52840 UF2 bootloaders; start address reported by uf2conv as 0x26000 (typical for S140-based layouts)

RAK 4631 transport mode.zip

#include <Arduino.h>
#include <Wire.h>
#include "nrf.h"
#include "nrf_gpio.h"

// ---------------- SX1262 pins on RAK4631 ----------------
static constexpr uint32_t PIN_NSS     = NRF_GPIO_PIN_MAP(1, 10);
static constexpr uint32_t PIN_SCK     = NRF_GPIO_PIN_MAP(1, 11);
static constexpr uint32_t PIN_MOSI    = NRF_GPIO_PIN_MAP(1, 12);
static constexpr uint32_t PIN_MISO    = NRF_GPIO_PIN_MAP(1, 13);
static constexpr uint32_t PIN_BUSY    = NRF_GPIO_PIN_MAP(1, 14);
static constexpr uint32_t PIN_NRESET  = NRF_GPIO_PIN_MAP(1,  6);

// ---------------- RAK4631 I2C pins (Wire) ----------------
// RAK4631 standard I2C: SDA=P0.26, SCL=P0.27
static constexpr uint32_t PIN_I2C_SDA = NRF_GPIO_PIN_MAP(0, 26);
static constexpr uint32_t PIN_I2C_SCL = NRF_GPIO_PIN_MAP(0, 27);

// ---------------- SHTC3 (RAK1901) ----------------
static constexpr uint8_t  SHTC3_ADDR  = 0x70;   // Sensirion SHTC3 I2C address
static constexpr uint16_t CMD_WAKE    = 0x3517; // Wake-up command
static constexpr uint16_t CMD_SLEEP   = 0xB098; // Sleep command

static inline void pinHigh(uint32_t pin) { nrf_gpio_pin_set(pin); }
static inline void pinLow (uint32_t pin) { nrf_gpio_pin_clear(pin); }
static inline bool pinRead(uint32_t pin) { return nrf_gpio_pin_read(pin) != 0; }

static void busyWait(uint32_t timeout_ms = 200)
{
  uint32_t start = millis();
  while (pinRead(PIN_BUSY)) {
    if (millis() - start > timeout_ms) break;
  }
}

// SPI mode 0 bitbang
static uint8_t spiTransferByte(uint8_t out)
{
  uint8_t in = 0;
  for (int i = 7; i >= 0; --i) {
    (out & (1u << i)) ? pinHigh(PIN_MOSI) : pinLow(PIN_MOSI);

    pinHigh(PIN_SCK);
    __NOP(); __NOP(); __NOP();

    in <<= 1;
    if (pinRead(PIN_MISO)) in |= 1;

    pinLow(PIN_SCK);
    __NOP(); __NOP(); __NOP();
  }
  return in;
}

static void sx1262Reset()
{
  pinLow(PIN_NRESET);
  delay(5);
  pinHigh(PIN_NRESET);
  delay(10);
}

static void sx1262SetSleep()
{
  busyWait(200);
  pinLow(PIN_NSS);
  spiTransferByte(0x84); // SetSleep
  spiTransferByte(0x00); // param
  pinHigh(PIN_NSS);
  delay(2);
}

static void shtc3SendCmd16(uint16_t cmd)
{
  Wire.beginTransmission(SHTC3_ADDR);
  Wire.write((uint8_t)(cmd >> 8));
  Wire.write((uint8_t)(cmd & 0xFF));
  Wire.endTransmission(true);
}

static void shtc3SleepAndReleaseBus()
{
  // Start I2C (Wire uses the board's default I2C pins)
  Wire.begin();
  Wire.setClock(400000);

  // Wake -> Sleep to get into a deterministic low-power state
  shtc3SendCmd16(CMD_WAKE);
  delay(2);
  shtc3SendCmd16(CMD_SLEEP);
  delay(2);

  // Stop I2C peripheral
  Wire.end();

  // IMPORTANT: Set I2C lines to High-Z (no pull) before SYSTEMOFF
  nrf_gpio_cfg_input(PIN_I2C_SDA, NRF_GPIO_PIN_NOPULL);
  nrf_gpio_cfg_input(PIN_I2C_SCL, NRF_GPIO_PIN_NOPULL);
}

static void goSystemOff()
{
  __DSB();
  __ISB();
  NRF_POWER->SYSTEMOFF = 1;
  __DSB();
  while (1) { __WFE(); }
}

void setup()
{
  // 1) Put SHTC3 into sleep and release I2C lines
  shtc3SleepAndReleaseBus();

  // 2) Put SX1262 into sleep
  nrf_gpio_cfg_output(PIN_NSS);
  nrf_gpio_cfg_output(PIN_SCK);
  nrf_gpio_cfg_output(PIN_MOSI);
  nrf_gpio_cfg_input (PIN_MISO, NRF_GPIO_PIN_NOPULL);
  nrf_gpio_cfg_input (PIN_BUSY, NRF_GPIO_PIN_NOPULL);
  nrf_gpio_cfg_output(PIN_NRESET);

  pinHigh(PIN_NSS);
  pinLow(PIN_SCK);
  pinLow(PIN_MOSI);
  pinHigh(PIN_NRESET);

  sx1262Reset();
  sx1262SetSleep();

  // 3) Hard off
  goSystemOff();
}

void loop() {}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions