Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/helpers/CommonCLI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
_prefs->powersaving_enabled = 1;
savePrefs();
strcpy(reply, "on - Immediate effect");
#elif defined(ESP32) && !defined(WITH_BRIDGE)
#elif (defined(ESP32) || defined(STM32_PLATFORM)) && !defined(WITH_BRIDGE)
_prefs->powersaving_enabled = 1;
savePrefs();
strcpy(reply, "on - After 2 minutes");
Expand Down
146 changes: 146 additions & 0 deletions src/helpers/stm32/STM32Board.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
#if defined(STM32_PLATFORM)
#include "STM32Board.h"

#if defined(STM32WLxx)
#include <SubGhz.h>

// Low-power sleep for the STM32WL. The core enters a Stop mode while the on-chip
// radio keeps receiving, so either an incoming frame (through the radio's IRQ on
// EXTI line 44) or a periodic RTC timer wakes it again. Stop mode halts SysTick,
// so on wake millis() is advanced by the elapsed time read back from the RTC.

extern "C" __IO uint32_t uwTick; // the tick counter behind millis()
extern "C" void SystemClock_Config(void); // provided by the board variant

static RTC_HandleTypeDef s_hrtc;
static bool s_rtc_ready = false;

// Acknowledge the wake-up timer interrupt; the weak default handler would trap.
extern "C" void RTC_WKUP_IRQHandler(void) {
HAL_RTCEx_WakeUpTimerIRQHandler(&s_hrtc);
}

static bool stm32wl_rtc_begin() {
if (s_rtc_ready) return true;

HAL_PWR_EnableBkUpAccess();

__HAL_RCC_LSI_ENABLE();
while (__HAL_RCC_GET_FLAG(RCC_FLAG_LSIRDY) == RESET) { }

__HAL_RCC_RTC_CONFIG(RCC_RTCCLKSOURCE_LSI);
__HAL_RCC_RTCAPB_CLK_ENABLE();
__HAL_RCC_RTC_ENABLE();

s_hrtc.Instance = RTC;
s_hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
s_hrtc.Init.AsynchPrediv = 127; // the LSI runs at ~32 kHz, so /128 gives 250 Hz
s_hrtc.Init.SynchPrediv = 249; // and /250 yields a 1 Hz ck_spre
s_hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;
s_hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH;
s_hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN;
s_hrtc.Init.OutPutRemap = RTC_OUTPUT_REMAP_NONE;
if (HAL_RTC_Init(&s_hrtc) != HAL_OK) return false;

HAL_NVIC_SetPriority(RTC_WKUP_IRQn, 15, 0);
HAL_NVIC_EnableIRQ(RTC_WKUP_IRQn);

s_rtc_ready = true;
return true;
}

static void stm32wl_rtc_zero() {
RTC_TimeTypeDef t = {};
HAL_RTC_SetTime(&s_hrtc, &t, RTC_FORMAT_BIN);
}

static uint32_t stm32wl_rtc_elapsed_ms() {
RTC_TimeTypeDef t;
RTC_DateTypeDef d;
HAL_RTC_GetTime(&s_hrtc, &t, RTC_FORMAT_BIN);
// Reading the date is mandatory after the time: it releases the shadow registers
// that the time read locks.
HAL_RTC_GetDate(&s_hrtc, &d, RTC_FORMAT_BIN);

uint32_t ms = ((t.Hours * 60UL + t.Minutes) * 60UL + t.Seconds) * 1000UL;
if (t.SecondFraction) {
ms += ((t.SecondFraction - t.SubSeconds) * 1000UL) / (t.SecondFraction + 1);
}
return ms;
}

void STM32Board::sleep(uint32_t secs) {
// A frame that arrived since the last loop() should be handled, not slept through.
if (SubGhz.isInterruptPending()) return;

bool timed = (secs > 0) && stm32wl_rtc_begin();

// The radio's interrupt is wired to EXTI line 44; unmasking it lets an incoming
// frame wake the core from Stop.
LL_EXTI_EnableIT_32_63(LL_EXTI_LINE_44);

if (timed) {
stm32wl_rtc_zero();
HAL_RTCEx_SetWakeUpTimer_IT(&s_hrtc, secs - 1, RTC_WAKEUPCLOCK_CK_SPRE_16BITS, 0);
}

// Keep interrupts masked across the transition so that the waking ISR runs only
// after SystemClock_Config() has restored the clock, rather than at the slower
// MSI range that the core wakes into.
__disable_irq();

// Re-check now that interrupts are masked. Had a frame arrived during the setup
// above, its ISR would already have run and disabled the radio interrupt in the
// NVIC, and a disabled but pending interrupt cannot wake WFI. The request is still
// asserted on EXTI line 44, so bail out rather than sleep through it.
if (SubGhz.isInterruptPending()) {
if (timed) HAL_RTCEx_DeactivateWakeUpTimer(&s_hrtc);
__enable_irq();
return;
}

HAL_SuspendTick();

// For a node that stays in continuous RX, Stop0 is the lower-power choice even
// though it is the shallower sleep. Stop1, Stop2 and Standby force the SMPS into
// bypass, which drops the radio back onto the LDO, whereas Stop0 keeps the SMPS
// running and the radio on DC-DC. The radio it saves outweighs the slightly
// higher core current, so idle-RX firmware should build with STM32WL_SLEEP_STOP0.
#if defined(STM32WL_SLEEP_STOP0)
HAL_PWREx_EnterSTOP0Mode(PWR_STOPENTRY_WFI);
#else
HAL_PWREx_EnterSTOP2Mode(PWR_STOPENTRY_WFI);
#endif

// A Stop mode always exits on the MSI clock, so restore the configured clock tree.
SystemClock_Config();
HAL_ResumeTick();

if (timed) {
HAL_RTCEx_DeactivateWakeUpTimer(&s_hrtc);
uwTick += stm32wl_rtc_elapsed_ms();
}

__enable_irq();
}

// Enable the SMPS step-down so that the core and on-chip radio draw from the DC-DC
// regulator rather than the LDO, which roughly halves the radio's RX current. The
// board must have the SMPS coil on VLXSMPS populated. Stop1, Stop2 and Standby force
// the converter back to bypass, so the saving only applies in Run, LP-Run and Stop0.
void STM32Board::lowPowerInit() {
#if defined(STM32WL_ENABLE_SMPS)
SET_BIT(PWR->CR5, PWR_CR5_SMPSEN);
#endif
}

#else // other STM32 parts have no on-chip radio to wake on, so there is nothing to do

void STM32Board::sleep(uint32_t secs) {
(void)secs;
}

void STM32Board::lowPowerInit() { }

#endif // STM32WLxx
#endif // STM32_PLATFORM
8 changes: 8 additions & 0 deletions src/helpers/stm32/STM32Board.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ class STM32Board : public mesh::MainBoard {
protected:
uint8_t startup_reason;

// Performs one-time low-power configuration at boot (the SMPS on STM32WL); it is
// a no-op on other STM32 parts.
void lowPowerInit();

public:
virtual void begin() {
startup_reason = BD_STARTUP_NORMAL;
lowPowerInit();
}

uint8_t getStartupReason() const override { return startup_reason; }
Expand All @@ -32,6 +37,9 @@ class STM32Board : public mesh::MainBoard {
HAL_PWREx_EnterSHUTDOWNMode();
}

// Low-power sleep until a LoRa frame arrives or 'secs' elapse (STM32WL only).
void sleep(uint32_t secs) override;

#if defined(P_LORA_TX_LED)
void onBeforeTransmit() override {
digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED on
Expand Down
4 changes: 3 additions & 1 deletion variants/rak3x72/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ build_flags = ${stm32_base.build_flags}
-D WRAPPER_CLASS=CustomSTM32WLxWrapper
-D SPI_INTERFACES_COUNT=0
-D RX_BOOSTED_GAIN=true
; -D STM32WL_TCXO_VOLTAGE=1.6 ; defaults to 0 if undef
-D STM32WL_ENABLE_SMPS ; run the radio from the SMPS/DC-DC regulator (needs the VLXSMPS coil)
-D STM32WL_SLEEP_STOP0 ; sleep in Stop0 so the SMPS stays active during continuous RX
; -D STM32WL_TCXO_VOLTAGE=1.8 ; override the auto-detected default from target.cpp
; -D LORA_TX_POWER=14 ; Defaults to 22 for HP, 14 is for LP version
-I variants/rak3x72
build_src_filter = ${stm32_base.build_src_filter}
Expand Down
6 changes: 4 additions & 2 deletions variants/rak3x72/target.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ SensorManager sensors;
#define LORA_CR 5
#endif

// Auto-detect the oscillator. begin() is given a TCXO voltage; RadioLib falls
// back to the crystal if it fails to start, so one build runs on both the
// crystal RAK3172 and the TCXO -T. A hard-coded 0 leaves the -T failing (-707).
#ifndef STM32WL_TCXO_VOLTAGE
// TCXO set to 0 for RAK3172
#define STM32WL_TCXO_VOLTAGE 0
#define STM32WL_TCXO_VOLTAGE 1.8
#endif

#ifndef LORA_TX_POWER
Expand Down