
Zephyr RTOS provides powerful power management capabilities that enable Bluetooth Low Energy applications to run for years on coin cell batteries — but only when configured correctly. Default Zephyr BLE samples often show sleep currents in the 10-20µA range between advertising peaks, far too high for multi-year coin cell operation. This article details the specific kernel, driver, and BLE stack configurations needed to approach the datasheet-typical STOP2 floor of ~1.8µA on the STM32WB55 while maintaining reliable connectivity.
The fundamental approach uses Zephyr’s power management system to:
+-------------------+ wake up & advertise +-------------------+| DEEP SLEEP | --------------------------> | BLE ADVERTISING || (STOP2, ~1.8uA) | | (~2-5mA peak) |+-------------------+ +-------------------+^ || 1-10s interval |+-------------------------------------------------+return to sleep
First, enable the power management subsystem in your prj.conf:
CONFIG_PM=y - Core system power managementCONFIG_PM_DEVICE=y - Device power managementCONFIG_PM_DEVICE_RUNTIME=y - Device runtime PM APIIn modern Zephyr (v3.0+), system power states are defined in the Devicetree rather than Kconfig. For the STM32WB series, ensure your board’s Devicetree configures the CPU to use the STOP2 state (which provides full RAM retention at ~1.8µA typical per the STM32WB55 datasheet):
&cpu0 {cpu-power-states = <&stop0 &stop1 &stop2>;};
To auto-enable runtime PM for specific peripherals (like I2C or SPI), add this property to their devicetree nodes:
&i2c1 {zephyr,pm-device-runtime-auto;};
A common anti-pattern in power optimization is attempting to manually start and stop advertising using a host workqueue. This actually increases power consumption by waking up the main CPU (CPU1) for every advertising event. Instead, configure the native advertising parameters and allow the BLE controller to manage the events autonomously while the host remains in deep sleep.
#include <zephyr/bluetooth/bluetooth.h>/** BT_GAP_ADV_FAST_INT_MIN_2 = 0x00a0 (160 * 0.625ms = 100ms)* BT_GAP_ADV_FAST_INT_MAX_2 = 0x00f0 (240 * 0.625ms = 150ms)* For longer intervals to save power, use custom values in* units of 0.625ms, e.g., 0x0640 = 1000ms.*/#define ADV_INTERVAL_MIN BT_GAP_ADV_FAST_INT_MIN_2#define ADV_INTERVAL_MAX BT_GAP_ADV_FAST_INT_MAX_2static struct bt_le_adv_param adv_param =BT_LE_ADV_PARAM_INIT(BT_LE_ADV_OPT_CONNECTABLE |BT_LE_ADV_OPT_USE_IDENTITY,ADV_INTERVAL_MIN, ADV_INTERVAL_MAX, NULL);static const struct bt_data ad[] = {BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),BT_DATA_BYTES(BT_DATA_UUID16_ALL, 0x0a, 0x18),};int main(void){int err;err = bt_enable(NULL);if (err) {return 0;}/* Start advertising once. The controller handles the interval and sleeping. */err = bt_le_adv_start(&adv_param, ad, ARRAY_SIZE(ad), NULL, 0);if (err) {return err;}/* Main thread can now sleep or perform other low-power tasks */return 0;}
The primary reason Zephyr BLE applications on STM32WB fail to reach the datasheet STOP2 current floor is the CLK48 HSEM issue. Zephyr’s STM32WB clock driver permanently locks the CLK48 hardware semaphore during initialization to prevent CPU2 from disabling the clock needed by CPU1’s RNG/USB peripherals. However, it never releases this lock during CPU1 sleep, preventing CPU2 from entering its lowest power states.
Solution: Modify the clock control driver to release the HSEM when entering sleep and re-acquire upon wake:
// In clock_stm32_ll_common.cstatic void clock_control_stm32_set_state(const struct device *dev,uint32_t subsystem,uint8_t state_val){// ... existing code ...if (state_val == LL_PWR_STATE_STOP2) {// Release CLK48 HSEM to allow CPU2 to stop the clockLL_HSEM_ReleaseLock(HSEM, CFG_HW_CLK48_CONFIG_SEMID, 0);} else if (state_val == LL_PWR_STATE_RUN) {// Re-acquire CLK48 HSEM when waking upLL_HSEM_1StepLock(HSEM, CFG_HW_CLK48_CONFIG_SEMID);}// ... rest of function ...}
With this fix, CPU2 can now enter its low-power states between advertising events. The combined system current in STOP2 should approach the datasheet-typical value of ~1.8µA for the STM32WB55.
Enable automatic power management for peripherals in their drivers:
// In I2C driver examplestatic int i2c_xy_init(const struct device *dev){// ... hardware init ...// Enable runtime PM - driver will manage suspend/resumepm_device_runtime_enable(dev);return 0;}// In driver operations, use get/put around accessesstatic int i2c_xy_transfer(const struct device *dev,struct i2c_msg *msgs, uint8_t num_msgs,uint16_t addr){int ret;// Ensure peripheral is poweredret = pm_device_runtime_get(dev);if (ret < 0) {return ret;}// Perform I2C transactionret = i2c_xy_core_transfer(dev, msgs, num_msgs, addr);// Allow peripheral to be powered down asynchronouslypm_device_runtime_put_async(dev, K_NO_WAIT);return ret;}
Rather than maintaining a static high TX power, utilize Bluetooth 5.2 LE Power Control (LEPC) to allow the controller to autonomously request power level changes based on link quality, optimizing power without waking the host.
Enable it in your configuration:
CONFIG_BT_TRANSMIT_POWER_CONTROL=y
If LEPC is not supported and manual Vendor Specific (VS) commands are required, first enable dynamic control in your prj.conf:
CONFIG_BT_CTLR_TX_PWR_DYNAMIC_CONTROL=y
Then use the standard Zephyr HCI API:
#include <zephyr/bluetooth/hci_vs.h>#include <zephyr/bluetooth/hci.h>#include <zephyr/sys/byteorder.h>static int set_tx_power(uint16_t handle, int8_t tx_power_level){struct bt_hci_cp_vs_write_tx_power_level *cp;struct net_buf *buf;int err;buf = bt_hci_cmd_create(BT_HCI_OP_VS_WRITE_TX_POWER_LEVEL, sizeof(*cp));if (!buf) {return -ENOBUFS;}cp = net_buf_add(buf, sizeof(*cp));cp->handle_type = BT_HCI_VS_LL_HANDLE_TYPE_CONN; /* Or BT_HCI_VS_LL_HANDLE_TYPE_ADV */cp->handle = sys_cpu_to_le16(handle);cp->tx_power_level = tx_power_level;err = bt_hci_cmd_send_sync(BT_HCI_OP_VS_WRITE_TX_POWER_LEVEL, buf, NULL);if (err) {return err;}return 0;}
Validate your power optimization with these steps:
Quick Links
Legal Stuff




