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:
| Example | sleep_mode | Why |
|---|---|---|
production-deployment | standby | Application core with a Mender poll thread — it needs wake-on-network, so the network stack's state has to stay resident. |
power-managed-sensor | deep | The 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:
| Source | Latency | Use case |
|---|---|---|
rtc | <100 µs | Periodic sensor sample (every 60 s) — the steady-state baseline, most wakes |
gpio_int | <50 µs | IMU motion-detect IRQ / user button press — the event-driven path |
uart | <1 ms | Diagnostic 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
board.yamlreference — thepower:block schema<alp/power.h>referenceproduction-deployment— the application-core counterpart (sleep_mode: standbywith wake-on-network)timer-periodic-interrupt— how you'd actually program the periodic wake- Board portability — what stays portable across SoM families
- Examples overview