Zephyr: Last software timer expiration does not cause immediate transition into low power sleep

I've noticed weird behavior in the software timer scheduling. I my case, there is one timer handling LED blinking. It is used in single shot mode and rescheduled to time the lit up time and blank time. Once the blinking sequence is finished, the timer is not rescheduled and SoC keeps consuming way too much power (~500 uA).
There is something running with 60 s period since the app start, that resolves this increased power consumption.

If I time the LED blinking by sleeping the thread, the issue is not present - it is specific to software timer scheduling and power management tied to it.

Steps to reproduce

  1. Have a minimal application with power management enabled and SoC current measurement.
  2. make the main task sleep for some duration
  3. observer low power consumption (OK)
  4. Start software times with expiration before 60 s of uptime
  5. Observe power consumption after the timer expires -> elevated
  6. Wait for nearest future N*60 s uptime
  7. Observer power consumption drop to expected low power sleep levels

Environment

  • host OS: Archlinux (up to date)
  • toolchain: 16.2.0
  • NRF connect SDK: 3.4.0
  • Zephyr version: ncs-v3.4.0
  • hardware: custom NRF54L15 based board
  • measured with Joulescope JS-220

Appliction task body to get the idea about sequencing.

auto main() -> int {
	AbsTime::sleep(10_s);
	leds[0].blink(50_ms, 50_ms, 3);
	AbsTime::sleep(10_s);
	leds[0].blink(50_ms, 1_s, 3); // starts timer
	AbsTime::sleep(60_s);
	leds[0].blink(50_ms, 50_ms, 3); // starts timer
	return 0;
}

Timer callback

void GpioPulsed::timerExpiredHandler(const std::size_t id) {
	utils::RaiiExecutor flipper([&]{
		config.on = not config.on;
	});
	if (config.on) {
		gpio.setInactive();
		if (config.infinite) {
			Timer::start(config.offTime);
		} else if (config.remains > 0) { // bot the last blink
			Timer::start(config.offTime);
			--config.remains;
		} // else: end of sequence, no timer rescheduling
	} else {
		gpio.setActive();
		Timer::start(config.onTime);
	}
}
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_CENTRAL=y
CONFIG_BT_MAX_CONN=4
CONFIG_BT_CTLR_SDC_PERIPHERAL_COUNT=2
CONFIG_BT_BONDABLE=n
CONFIG_BT_PRIVACY=n
CONFIG_BT_RECV_WORKQ_SYS=y
CONFIG_BT_SCAN_AND_INITIATE_IN_PARALLEL=y

CONFIG_BT_GAP_AUTO_UPDATE_CONN_PARAMS=n
CONFIG_BT_AUTO_PHY_UPDATE=n


CONFIG_BT_L2CAP_TX_MTU=247
CONFIG_BT_BUF_ACL_TX_SIZE=251
CONFIG_BT_BUF_ACL_RX_SIZE=251
CONFIG_BT_BUF_ACL_TX_COUNT=6
CONFIG_BT_CTLR_DATA_LENGTH_MAX=251

CONFIG_BT_PERIPHERAL_PREF_MIN_INT=10


CONFIG_BT_GATT_CLIENT=y
CONFIG_BT_GATT_DYNAMIC_DB=y

CONFIG_BT_ATT_PREPARE_COUNT=6

CONFIG_BT_CHANNEL_SOUNDING=y
CONFIG_BT_CTLR_PHY_2M=y
CONFIG_BT_CTLR_SDC_CS_MAX_ANTENNA_PATHS=1
CONFIG_BT_CTLR_SDC_CS_NUM_ANTENNAS=1
CONFIG_BT_CTLR_SDC_CS_STEP_MODE3=n
CONFIG_BT_CTLR_CHANNEL_SOUNDING_TEST=n
CONFIG_BT_CTLR_SDC_CS_ROLE_REFLECTOR_ONLY=y

CONFIG_BT_CTLR_EXTENDED_FEAT_SET=y

CONFIG_BT_CTLR_CHANNEL_SOUNDING_TEST=n

CONFIG_BT_TRANSMIT_POWER_CONTROL=y

CONFIG_BT_SMP=y


CONFIG_BT_TRANSMIT_POWER_CONTROL=y

CONFIG_FPU=y
CONFIG_FPU_SHARING=y


CONFIG_MPSL_ASSERT_HANDLER=y
CONFIG_ASSERT=y
CONFIG_ASSERT_NO_MSG_INFO=y
CONFIG_ASSERT_NO_FILE_INFO=y

CONFIG_BT_BUF_ACL_RX_COUNT_EXTRA=2

CONFIG_CPP=y
CONFIG_STD_CPP2B=y
CONFIG_REQUIRES_FULL_LIBCPP=y

CONFIG_BOOTLOADER_MCUBOOT=n

CONFIG_DEBUG=y
CONFIG_DEBUG_THREAD_INFO=y

CONFIG_MULTITHREADING=y

CONFIG_SYS_HEAP_RUNTIME_STATS=y
CONFIG_SYS_HEAP_LISTENER=y
CONFIG_HEAP_MEM_POOL_SIZE=32768
CONFIG_NEWLIB_LIBC_MIN_REQUIRED_HEAP_SIZE=4096
CONFIG_NEWLIB_LIBC_HEAP_LISTENER=y
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=4096


CONFIG_PWM=y
CONFIG_FP16=n
CONFIG_CBPRINTF_NANO=y
CONFIG_ASSERT_VERBOSE=n
CONFIG_ASSERT_NO_MSG_INFO=y
CONFIG_BOOT_BANNER=n
CONFIG_NCS_BOOT_BANNER=n

CONFIG_HWINFO=y

CONFIG_LOG=n
CONFIG_CONSOLE=n
CONFIG_PRINTK=n

CONFIG_LOG_MODE_DEFERRED=n
CONFIG_RTT_CONSOLE=n
CONFIG_USE_SEGGER_RTT=n
CONFIG_LOG_BACKEND_RTT=n
CONFIG_LOG_BACKEND_RTT_MODE_OVERWRITE=n
CONFIG_FAULT_DUMP=0


CONFIG_BT_CTLR_CONN_RSSI=y
CONFIG_BT_CTLR_TX_PWR_DYNAMIC_CONTROL=y

CONFIG_PM=y
CONFIG_PM_DEVICE=y
CONFIG_PM_DEVICE_RUNTIME=y


CONFIG_MAIN_STACK_SIZE=1024

CONFIG_FLASH=y
CONFIG_FLASH_MAP=y
CONFIG_SPI=y
CONFIG_SPI_ASYNC=y
CONFIG_SPI_NOR=y
CONFIG_SPI_NOR_SFDP_RUNTIME=y
CONFIG_FLASH_JESD216_API=y
Initially opened issues at upstream zepyhr rtos GH repo, but closed due to using NCS (downstream) fork.
After disabling bluetooth and some other stuff, to make the test minimal, the 60 s "recovery tick" does not occur anymore and power consumption never drops back to expected levels. It still drops during 1s LED off times, when the timer runs.
Related