Skip to content

STM32WBAX PM rework to support STOP/STDBY modes with radio activity #92847

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

asm5878
Copy link
Collaborator

@asm5878 asm5878 commented Jul 8, 2025

Power management has been split between rf driver and system.
SCM is no more used.

This PR is clearly a draft version to highlight where we are are on STDBY topic (working but intensive tests still to be done) and to start a public discussion on open topics we have.

Power management has been split between rf driver and system.
SCM is no more used.

Signed-off-by: Alessandro Manganaro <alessandro.manganaro@st.com>
@asm5878 asm5878 changed the title PM rework to support STOP/STDBY modes with radio activity STM32WBAX PM rework to support STOP/STDBY modes with radio activity Jul 8, 2025
Copy link

github-actions bot commented Jul 8, 2025

The following west manifest projects have changed revision in this Pull Request:

Name Old Revision New Revision Diff
hal_stm32 zephyrproject-rtos/hal_stm32@126cbbe (main) zephyrproject-rtos/hal_stm32#294 zephyrproject-rtos/hal_stm32#294/files

DNM label due to: 1 project with PR revision

Note: This message is automatically posted and updated by the Manifest GitHub Action.

@github-actions github-actions bot added manifest manifest-hal_stm32 DNM (manifest) This PR should not be merged (controlled by action-manifest) labels Jul 8, 2025
Copy link

sonarqubecloud bot commented Jul 8, 2025

Copy link
Contributor

@msmttchr msmttchr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @asm5878, thanks a lot for this draft PR, very interesting proposal. I added few preliminary comments, and I would like also to involve @HoZHel and @FrancescoCiminoST

Comment on lines +1 to +29
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright (c) 2023 STMicroelectronics
*/

/ {
/* Change min residency time to ease power consumption measurement */
cpus {
cpu0: cpu@0 {
cpu-power-states = <&stop0 &stop1 &standby>;
};

power-states {
standby: state2 {
compatible = "zephyr,power-state";
power-state-name = "suspend-to-ram";
substate-id = <1>;
min-residency-us = <10000>;
exit-latency-us = <1000>;
};
};
};

};

&lptim1 {
status = "okay";
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, all these changes should be moved in the dtsi file of the device since they are not board specific.
In particular, the standby power state should be added where stop 0 and stop 1 are defined.
LPTIM should be selected when CONFIG_PM is set.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed and for LPTIM we could use PM config options to enable/disable LPTIM (board or soc dts )

Comment on lines +1 to +28
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright (c) 2023 STMicroelectronics
*/

/ {
/* Change min residency time to ease power consumption measurement */
cpus {
cpu0: cpu@0 {
cpu-power-states = <&stop0 &stop1 &standby>;
};

power-states {
standby: state2 {
compatible = "zephyr,power-state";
power-state-name = "suspend-to-ram";
substate-id = <1>;
min-residency-us = <10000>;
exit-latency-us = <1000>;
};
};
};
};

&lptim1 {
status = "okay";
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar considerations done for nucleo_wba55cg.overlay

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as before

Comment on lines +11 to +16

CONFIG_PM=y
CONFIG_PM_DEVICE=y
CONFIG_PM_DEVICE_RUNTIME=y
CONFIG_PM_DEVICE_SYSTEM_MANAGED=y
CONFIG_PM_S2RAM=y
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these should be moved to a specific file related to the relevant NUCLEO, if we want to enable power management by default.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could just leave CONFIG_PM to be selected by user in each sample and use it to conditionally enable all the rest of configs in nucleo_wba65ri_defconfig

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CONFIG_PM_S2RAM could be implyed by CONFIG_PM at SoC level.

Or even better: imply only if the corresponding state is status = "okay" in DTS. (but not sure that status = "disabled" on a power state really disables it)

Comment on lines +26 to +27
#include "linklayer_plat.h"
#include "ll_sys.h"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is not needed, and it should be removed, including the enclosing #ifdef CONFIG_BT_STM32WBA

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, for sure an error during the rebase.

Comment on lines +22 to +24
#ifdef CONFIG_BT_STM32WBA
#include "ll_sys.h"
#endif
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I consider this code experimental and eventually to be removed as it is device specific.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, this file must stay untouched compared to upstream

Comment on lines +172 to +176
#ifdef CONFIG_BT_STM32WBA
{
uint32_t radio_remaining_time, cmd_status;
int32_t radio_ticks;
cmd_status =
Copy link
Contributor

@msmttchr msmttchr Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand this logic, but all these changes in this file should ideally be reverted and an approach using

void pm_policy_event_register(struct pm_policy_event *evt, int64_t uptime_ticks)
, and
void pm_policy_event_unregister(struct pm_policy_event *evt)

should be used

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as before, this file will stay untouched and in hci_stm32wba.c we need to understand when the next radio event has been set and inform zephyr when this event will occur (or not)

revision: 126cbbee19208b7aaca5ad8b287cf104e8bc837a
revision: pull/294/head
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be reverted before marking this PR ready for review

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed when PR 294 will be approved we will have a new commit to be used

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

zephyrproject-rtos/hal_stm32#294 won't be approved before review of current PR has made good progress.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed it's reasonable

@@ -462,11 +514,22 @@ static DEVICE_API(bt_hci, drv) = {
.send = bt_hci_stm32wba_send,
};


#if defined (CONFIG_PM_DEVICE)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#if defined (CONFIG_PM_DEVICE)
#if defined(CONFIG_PM_DEVICE)

Comment on lines +11 to +16

CONFIG_PM=y
CONFIG_PM_DEVICE=y
CONFIG_PM_DEVICE_RUNTIME=y
CONFIG_PM_DEVICE_SYSTEM_MANAGED=y
CONFIG_PM_S2RAM=y
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CONFIG_PM_S2RAM could be implyed by CONFIG_PM at SoC level.

Or even better: imply only if the corresponding state is status = "okay" in DTS. (but not sure that status = "disabled" on a power state really disables it)

Comment on lines +86 to +90
/* MCU enters in low-power modes */
basepri = __get_BASEPRI();
__set_BASEPRI(0);
__ISB();
__DSB();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be mostly identical to arch_irq_lock() (except we set BASEPRI to 0):

static ALWAYS_INLINE unsigned int arch_irq_lock(void)
{
unsigned int key;
#if defined(CONFIG_ARMV6_M_ARMV8_M_BASELINE)
#if CONFIG_MP_MAX_NUM_CPUS == 1 || defined(CONFIG_ARMV8_M_BASELINE)
key = __get_PRIMASK();
__disable_irq();
#else
#error "Cortex-M0 and Cortex-M0+ require SoC specific support for cross core synchronisation."
#endif
#elif defined(CONFIG_ARMV7_M_ARMV8_M_MAINLINE)
key = __get_BASEPRI();
__set_BASEPRI_MAX(_EXC_IRQ_DEFAULT_PRIO);
__ISB();
#elif defined(CONFIG_ARMV7_R) || defined(CONFIG_AARCH32_ARMV8_R) \
|| defined(CONFIG_ARMV7_A)
__asm__ volatile(
"mrs %0, cpsr;"
"and %0, #" STRINGIFY(I_BIT) ";"
"cpsid i;"
: "=r" (key)
:
: "memory", "cc");
#else
#error Unknown ARM architecture
#endif /* CONFIG_ARMV6_M_ARMV8_M_BASELINE */
return key;
}

Q: do we really need to duplicate this code here? Can't we just call irq_lock()/irq_unlock()?

(but also see remark lower in file, only __DSB() + __WFI() should be needed)

Copy link
Contributor

@msmttchr msmttchr Jul 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the reasoning here is the following:

  1. When executing these power management procedures, we do not want any interrupt, regardless of his priority to preempt us
  2. We would like any possible pending interrupt to transform the Wfi in a nop and skip the power mode entry

For 1), we globally disable interrupts with __disable_irq
For 2), we unmask all interrupt priorities

The original code of Zephyr using irq_lock, allows effectively to preempt this critical function by high priority irq (priority lower than _EXC_IRQ_DEFAULT_PRIO).

In turn, since interrupt handlers are calling the power management routines both in direct or regular case, in the middle of this critical routine, not only the high priority interrupt is executed, but also the pm_system_resume is executed effectively re-enabling all the interrupts.
See

#ifdef CONFIG_PM
/*
* All non-ZLI interrupts are disabled when handling idle wakeup. For
* tickless idle, this ensures that the calculation and programming of
* the device for the next timer deadline is not interrupted. For
* non-tickless idle, this ensures that the clearing of the kernel idle
* state is not interrupted. In each case, pm_system_resume is called
* with non-ZLI interrupts disabled.
*/
/*
* Disable non-ZLI interrupts to prevent nesting while exiting idle
* state. This is only necessary for the Cortex-M because it is the only
* ARM architecture variant that automatically enables interrupts when
* entering an ISR.
*/
unsigned int key = arch_irq_lock();
/* is this a wakeup from idle ? */
/* requested idle duration, in ticks */
if (_kernel.idle != 0) {
/* clear kernel idle state */
_kernel.idle = 0;
pm_system_resume();
}
/* re-enable interrupts */
arch_irq_unlock(key);
#endif /* CONFIG_PM */

As such, we took inspiration from

void pm_state_set(enum pm_state state, uint8_t substate_id)
{
if (state == PM_STATE_SUSPEND_TO_IDLE) {
__disable_irq();
sys_trace_idle();
s2idle_enter(substate_id);
/* Resume here. */
s2idle_exit(substate_id);
sys_trace_idle_exit();
__enable_irq();
}
#if defined(CONFIG_PM_S2RAM)
else if (state == PM_STATE_SUSPEND_TO_RAM) {
__disable_irq();
sys_trace_idle();
s2ram_enter();
/* On resuming or error we return exactly *HERE* */
s2ram_exit();
sys_trace_idle_exit();
__enable_irq();
}
#endif
else {
k_cpu_idle();
}
}
for our implementation.

This implementation will remove the feature that allows high priority irq to preempt cpu in these critical routines, but I believe this feature would not work during s2ram procedure execution where being interrupted is a system crash guaranteed.

Comment on lines +199 to +202
basepri = __get_BASEPRI();
__set_BASEPRI(0);
__ISB();
__DSB();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same remark: can't we use irq_lock() + __WFI() + irq_unlock()?

(but also see remark lower in file, only __DSB() + __WFI() should be needed)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

__WFI();

/* MCU exits from low power mode and set back the basepri */
__set_BASEPRI(basepri);

return 0;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. If we fall here we should return an error.
  2. Could use this routine for entry in Stop mode too (in this case, ignore return value)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you are right in both point

LOG_DBG("Suspend to RAM needs CONFIG_PM_S2RAM to be enabled");
#endif
case PM_STATE_SUSPEND_TO_RAM:
__disable_irq();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pm_system_suspend which calls pm_state_set can only be called by the idle thread, which acquires IRQ lock before calling it.

I think this should not be needed? (nor any IRQ lock call in lower layers)

zephyr/kernel/idle.c

Lines 46 to 74 in 732a3a5

/* Note weird API: k_cpu_idle() is called with local
* CPU interrupts masked, and returns with them
* unmasked. It does not take a spinlock or other
* higher level construct.
*/
(void) arch_irq_lock();
#ifdef CONFIG_PM
_kernel.idle = z_get_next_timeout_expiry();
/*
* Call the suspend hook function of the soc interface
* to allow entry into a low power state. The function
* returns false if low power state was not entered, in
* which case, kernel does normal idle processing.
*
* This function is entered with interrupts disabled.
* If a low power state was entered, then the hook
* function should enable interrupts before exiting.
* This is because the kernel does not do its own idle
* processing in those cases i.e. skips k_cpu_idle().
* The kernel's idle processing re-enables interrupts
* which is essential for the kernel's scheduling
* logic.
*/
if (k_is_pre_kernel() || !pm_system_suspend(_kernel.idle)) {
k_cpu_idle();
}
#else

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -97,88 +114,150 @@ static void set_mode_suspend_to_ram(void)
/* Select standby mode */
LL_PWR_SetPowerMode(LL_PWR_MODE_STANDBY);

/* Save FPU state and configuration */
z_arm_save_fp_context(&fpu_state);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not 100% OK with this because z_arm_save_fp_context may be a no-op if FPU_SHARING is disabled (I think the option is enabled by default, but still):

zephyr/arch/Kconfig

Lines 1025 to 1040 in 732a3a5

config FPU_SHARING
bool "FPU register sharing"
depends on FPU && MULTITHREADING
help
This option enables preservation of the hardware floating point registers
across context switches to allow multiple threads to perform concurrent
floating point operations.
Note that some compiler configurations may activate a floating point
context by generating FP instructions for any thread, and that
context must be preserved when switching such threads in and out.
The developers can still disable the FP sharing mode in their
application projects, and switch to Unshared FP registers mode,
if it is guaranteed that the image code does not generate FP
instructions outside the single thread context that is allowed
to do so.

IMO we should:

  1. remove the #if CONFIG_FPU_SHARING logic from z_arm_save_fp_context
  2. replace all calls to z_arm_save_fp_context with #ifdef'ed wrapper (see below)
  3. call z_arm_save_fp_context from arch_pm_s2ram_suspend, which is now guaranteed to always save FPU context

[2] would be something like

#ifdef CONFIG_FPU_SHARING
#define z_arm_save_shared_fpu_context z_arm_save_fp_context
#else
#define z_arm_save_shared_fpu_context /* nothing */
#endif

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion, I think you’re right.

Comment on lines +42 to +74
static void scb_suspend(SCB_Type *backup)
{
backup->ICSR = SCB->ICSR;
backup->VTOR = SCB->VTOR;
backup->AIRCR = SCB->AIRCR;
backup->SCR = SCB->SCR;
backup->CCR = SCB->CCR;
memcpy((uint32_t *) backup->SHPR, (uint32_t *)SCB->SHPR, sizeof(SCB->SHPR));
backup->SHCSR = SCB->SHCSR;
backup->CFSR = SCB->CFSR;
backup->HFSR = SCB->HFSR;
backup->DFSR = SCB->DFSR;
backup->MMFAR = SCB->MMFAR;
backup->BFAR = SCB->BFAR;
backup->AFSR = SCB->AFSR;
backup->CPACR = SCB->CPACR;
}
static void scb_resume(SCB_Type *backup)
{
SCB->ICSR = backup->ICSR;
SCB->VTOR = backup->VTOR;
SCB->AIRCR = backup->AIRCR;
SCB->SCR = backup->SCR;
SCB->CCR = backup->CCR;
memcpy((uint32_t *)SCB->SHPR, (uint32_t *)backup->SHPR, sizeof(SCB->SHPR));
SCB->SHCSR = backup->SHCSR;
SCB->CFSR = backup->CFSR;
SCB->HFSR = backup->HFSR;
SCB->DFSR = backup->DFSR;
SCB->MMFAR = backup->MMFAR;
SCB->BFAR = backup->BFAR;
SCB->AFSR = backup->AFSR;
SCB->CPACR = backup->CPACR;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could use generalization and move to arch level. But SoC-specific is OK for now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent comment, in my opinion. Valid also for fpu?
SCB is implementation specific, at least in some fields, like SHPR.
We used the CMSIS typedef SCB_Type.
I am not sure whether this can be moved at arch level, but I would be 100%, in favour, if it’s possible

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
DNM (manifest) This PR should not be merged (controlled by action-manifest) manifest manifest-hal_stm32
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants