Skip to main content

power-managed-sensor

The SDK's reference for the board.yaml cores.<id>.power: declarative block: a worked example of sleep-mode selection plus multi-source wakeup for a low-power sensor node.

This is the only example in the catalog that targets E1M-AEN301, and the only one that declares sleep_mode: deep.

Source: examples/power-timing/power-managed-sensor/.

:::warning Read this before you copy it src/main.c is a framing demo. It prints the three wake stages and their cadence; it does not open a sensor, program a counter, or enter a sleep state. It opens no I²C bus and calls no chip driver.

The example's teaching payload lives in board.yaml — the power: block and what the orchestrator does with it. Treat main.c as the shape of the duty cycle, not as a working sensor node. :::

The teaching payload: the power: block

The duty cycle for a real low-power sensor node is "wake briefly, sample, push, go back to sleep." Getting from milliamps to microamps is the difference between weeks and years of battery life — which means the sleep state, the wake sources, and the sample cadence all need to be first-class declarative facts, not hand-tuned CONFIG_PM_* knobs scattered across prj.conf.

cores:
m55_he:
os: zephyr
app: ./src
power:
sleep_mode: deep
wakeup_sources:
- uart
- gpio_int
- rtc
memory:
stack_kib: 4
heap_kib: 16

The orchestrator turns that into the slice's generated alp.conf:

CONFIG_PM=y
CONFIG_PM_DEVICE=y
# Sleep target: deep
CONFIG_PM_DEVICE_WAKE_UART=y # wakeup source
CONFIG_PM_DEVICE_WAKE_GPIO_INT=y # wakeup source
CONFIG_PM_DEVICE_WAKE_RTC=y # wakeup source

The customer never hand-edits a CONFIG_PM_* line. prj.conf stays empty by design.

Wake-source enables have to come from board.yaml rather than the app, because the silicon's PM controller needs the Kconfig set at build time — the relevant peripheral clocks and retention domains are configured during boot, before main() ever runs.

Why deep, when everything else uses standby

sleep_mode: takes disabled | idle | standby | deep. Across the whole example tree only two apps declare one:

Examplesleep_modeWhy
production-deploymentstandbyApplication core with a Mender poll thread — it needs wake-on-network, so the network stack's state has to stay resident.
power-managed-sensordeepThe lowest retention state the silicon supports. Nothing needs to stay live between wakes; the node is asleep until the RTC, a motion IRQ, or a console keystroke pulls it out.

deep is the right stance precisely because this node has no always-resident stack to preserve. An app-core node that dropped to deep would lose the network context it needs to answer a poll.

Why AEN301

AEN301 is the small AEN SKU — lower static power than the AEN801 used by the production-deployment flagship. Its E3 silicon has no A-core at all, so board.yaml carries no a32_cluster: entry; this SKU is M-class only. The M55-HE (high-efficiency) core is the right home for the always-on sensor task, and the M55-HP core stays parked with os: "off" unless burst compute is needed.

The wake-source matrix

Three orthogonal triggers, each a one-line declaration:

SourceLatencyUse case
rtc<100 µsPeriodic sensor sample (every 60 s) — the steady-state baseline, most wakes
gpio_int<50 µsIMU motion-detect IRQ / user button press — the event-driven path
uart<1 msDiagnostic console — the back-door for field debugging

Memory tuning

Also declarative. The sensor task is short-lived per wake, so 4 KiB of main-thread stack covers the sample plus the I²C transfer buffer with headroom, and 16 KiB of heap covers the Zephyr sensor subsystem's per-channel state. A low-power profile doesn't want the Zephyr default 8 KiB / 1 KiB layout — heap truncated, stack underused.

board.yaml

som:
sku: E1M-AEN301 # small AEN, lower-power profile

preset: e1m-evk

cores:
m55_hp:
os: "off" # HP core parked
m55_he:
os: zephyr
app: ./src
peripherals:
- gpio # IMU motion interrupt + user button
- i2c # sensor bus
- rtc # periodic wake source
- uart # diagnostic console
- sensor # Zephyr sensor subsystem
power:
sleep_mode: deep
wakeup_sources:
- uart
- gpio_int
- rtc
memory:
stack_kib: 4
heap_kib: 16

diagnostics:
log_level: info
modules:
alp_gpio: warn # less chatty for the always-on path
alp_i2c: warn

Note there is no chips: block — the demo declares the peripheral subsystems a real node would need, but instantiates no chip driver.

Expected output

[pm] power-managed-sensor (AEN301 / M55-HE)
[pm] wake sources: rtc(60s) | gpio_int(IMU/user) | uart(console)
[pm] sleep policy: deep -- see board.yaml cores.m55_he.power:
[pm] stage 1: wake-source=rtc
[pm] sample acquired, host channel push -> ok
[pm] re-entering deep sleep
[pm] stage 2: wake-source=gpio_int
[pm] sample acquired, host channel push -> ok
[pm] re-entering deep sleep
[pm] stage 3: wake-source=uart
[pm] sample acquired, host channel push -> ok
[pm] re-entering deep sleep
[pm] done

Every line above is a literal printf — including sample acquired and re-entering deep sleep. Same output on native_sim and on silicon.

Status

One twister row on native_sim/native/64; the harness latches on [pm] done. The power: block's Kconfig derivation is what's under test — the runtime sleep behaviour is not exercised anywhere in CI.

On HiL you'd confirm the policy with a current probe across the SoM's VDD rail: the average should be dominated by active-window energy × (active_ms / 60 000), not by static leakage.

Customising the cadence

The RTC tick period is an app concern — board.yaml says "RTC is a wake source," not "wake every 60 s." Change RTC_TICK_S in src/main.c (and, in a production node, program the alarm channel on the platform's counter device — see timer-periodic-interrupt for that). The power: block stays the same.

See also

Questions about this page? Discuss in Community Forum