
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 advertising currents in the 10-20uA range between peaks, far too high for multi-year coin cell operation. This article details the specific kernel, driver, and BLE stack configurations needed to push advertising interval currents below 1uA while maintaining reliable connectivity.
The fundamental approach uses Zephyr’s power management system to:
+-------------------+ Advertise (10ms) +------------------+| DEEP SLEEP | <------------------- | BLE ADVERTISING || (STOP2, ~0.6uA) | | (~2-5mA peak) |+-------------------+ +------------------+^ ^| || 1-10s interval |+----------------------------------------+
First, enable the power management subsystem:
CONFIG_PM=y - Core power managementCONFIG_PM_DEVICE=y - Device runtime PMCONFIG_PM_DEVICE_RUNTIME=y - Runtime PM APICONFIG_PM_DEVICE_RUNTIME_AUTO=y - Auto-enable PM in driversCONFIG_PM_DEVICE_RUNTIME_USE_DEDICATED_WQ=y - Prevent workqueue stallsFor STM32WB series, additionally enable:
CONFIG_SOC_SERIES_STM32WB=yCONFIG_PM_STATE_SUSPEND_TO_IDLE=yCONFIG_PM_STATE_SUSPEND_TO_IDLE_SUBSTATE_STOP2=yUse a low-power timer to schedule advertising events rather than sleeping in the main thread:
#define ADV_INTERVAL_MS 1000#define ADV_DURATION_MS 10static struct k_work_delayable adv_work;static struct bt_le_adv_param *adv_param = BT_LE_ADV_PARAM(BT_LE_ADV_OPT_CONNECTABLE | BT_LE_ADV_OPT_USE_IDENTITY,BT_GAP_ADV_FAST_INT_MIN_2, BT_GAP_ADV_FAST_INT_MAX_2, NULL);static void start_advertising(struct k_work *work){bt_le_adv_start(adv_param, ad, ARRAY_SIZE(ad), NULL, 0);k_work_schedule(&adv_work, K_MSEC(ADV_INTERVAL_MS - ADV_DURATION_MS));}static void stop_advertising(struct k_work *work){bt_le_adv_stop();k_work_schedule(&adv_work, K_MSEC(ADV_DURATION_MS));}void main(void){k_work_init_delayable(&adv_work, start_advertising);k_work_schedule(&adv_work, K_NO_WAIT);// Main thread can now sleep or perform other low-power tasks}
The primary reason Zephyr BLE applications fail to reach nanoamp currents 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_1StepLock(HSEM, CFG_HSEM_CLK48);} else if (state_val == LL_PWR_STATE_RUN) {// Re-acquire CLK48 HSEM when waking upLL_HSEM_1StepLock(HSEM, CFG_HSEM_CLK48);}// ... rest of function ...}
With this fix, CPU2 can now enter its low-power states between advertising events, dropping total system current to ~600nA in STOP2 mode.
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);// Optional: auto-enable based on devicetree flagif (dev->pm_usage_cnt > 0) {pm_device_runtime_get(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 downpm_device_runtime_put(dev);return ret;}
Rather than using fixed TX power, adjust based on connection quality to minimize power while maintaining link:
#define RSSI_TARGET -70 // Target RSSI for connection#define RSSI_HYSTERESIS 5 // Hysteresis to prevent oscillationstatic void connection_updated(uint8_t conn_index,struct bt_conn_le_param *param){int8_t rssi;if (bt_le_conn_param_get(conn_index, NULL, NULL, NULL, &rssi) == 0) {if (rssi < (RSSI_TARGET - RSSI_HYSTERESIS)) {// Weak signal - increase TX powerbt_hci_vs_le_set_tx_power(conn_index, BT_HCI_VS_LE_TX_POWER_HIGH);} else if (rssi > (RSSI_TARGET + RSSI_HYSTERESIS)) {// Strong signal - decrease TX powerbt_hci_vs_le_set_tx_power(conn_index, BT_HCI_VS_LE_TX_POWER_LOW);}// else maintain current power}}
Validate your power optimization with these steps:
Quick Links
Legal Stuff





