Your MCU Has Two Ways to Keep Time — Picking the Wrong One Wrecks Your BLE Battery Budget

Your datasheet says System OFF draws 1.5 µA. Your board pulls 500 µA and your coin cell dies in a month. You’ve triple-checked the regulators, disabled every GPIO, and the number won’t budge.
The culprit usually isn’t a leaky pin or a stuck peripheral. It’s how your firmware keeps time.
An MCU tracks time one of two ways: the CPU counts, or a peripheral counts. One of those keeps the core awake, and on a battery design that’s the difference between 2 years and 2 months of life. Here’s how to tell them apart and pick the right one.
Why Timing Even Touches Power
A BLE device spends more than 99% of its life asleep. Between advertising events or connection intervals, there’s nothing to do, so the smart move is to shut down the CPU and the high-frequency clock and wait.
That “wait” is where the battery budget lives or dies. Active current during a radio event might be 5 to 15 mA, but it only lasts a few milliseconds. Sleep current runs 24/7. Do the arithmetic on a 225 mAh CR2032 and you’ll see that shaving 100 µA off sleep matters far more than shaving 1 mA off active.
Anything that keeps the CPU clocked, or leaves the HFCLK running when nothing needs it, quietly torches that budget. Your timing source decides whether the core gets to sleep at all: either the CPU drives timing (it stays powered and counts ticks), or a peripheral drives timing (the CPU sleeps and a tiny always-on block wakes it). That single choice sets your floor.
RTC vs SysTick: The Core Decision
SysTick is the ARM Cortex-M system timer. Every Cortex-M part has one, it’s dead simple to configure, and it’s the default RTOS tick source in a lot of ports. If you came from bare-metal or a mains-powered project, reaching for SysTick is muscle memory.
The problem is that SysTick runs off the CPU clock. To count, the core has to be clocked, so SysTick can’t tick while the CPU is in deep sleep. Any RTOS scheduler leaning on it for its tick will keep the core alive just to advance time.
On the nRF52 and nRF53, this is fatal. System ON idle expects the CPU to power down between events. System OFF shuts down almost everything. SysTick survives neither. If it’s your tick source, you never reach the sleep modes the datasheet is quoting.
Timing source Can CPU sleep? Typical sleep current
------------------- --------------- ---------------------
SysTick (core) No 100s of µA (core on)
RTC (LFCLK) Yes ~0.25–1.5 µA
TIMER (HFCLK) Yes* mA range if HFCLK on*CPU can sleep, but HFCLK draws heavily. Gate it.
The gap between the first and second row is your whole battery budget.
The RTC Peripheral: Timing That Lets the Core Sleep
The nRF RTC (real-time counter, not the calendar RTC you might be picturing) runs off the low-frequency clock, LFCLK, at 32.768 kHz. That crystal keeps ticking while the CPU is fully asleep, drawing somewhere in the 250 nA range depending on your LFCLK source.
Here’s the mechanism that saves you. You program an RTC compare register, the CPU goes to sleep, and when the counter hits your value the RTC fires an interrupt that wakes the core. The CPU does zero work in between. It’s off. The RTC is the only thing counting, and it costs almost nothing to run.
This is also what Zephyr uses on nRF by default, despite the SysTick habit many engineers bring with them. The nRF timer driver backs the kernel clock with an RTC, not SysTick. If you’re on Zephyr and you assumed SysTick was your tick source, check your config, because the wrong assumption sends you debugging the wrong thing.
# On nRF, you want the RTC-backed system timer, not Cortex-M SysTick:
CONFIG_CORTEX_M_SYSTICK=n
# The nrfx RTC timer driver provides the kernel tick instead.The tradeoff is resolution. At prescaler 0, the RTC ticks every 30.5 µs. That’s plenty for periodic wakeups and connection timing, but it won’t help you measure a nanosecond-scale pulse.
One gotcha: the RTC counter is 24 bits. At the full 32.768 kHz rate it overflows in about 512 seconds. Your timing layer has to handle that rollover, and most decent drivers already do, but if you’re rolling your own you’ll get mystery bugs around the 8.5 minute mark.
When you need real precision, that’s the TIMER peripheral, clocked off the HFCLK for nanosecond resolution. The catch is HFCLK draws heavily, so leaving it on for timing alone puts you back in the milliamp range.
The move that makes both cheap: core-independent peripherals. On nRF you wire the RTC, TIMER, and other blocks together through PPI or DPPI so an event on one peripheral triggers an action on another with no CPU involvement. An RTC compare can kick off a TIMER capture, or trigger an ADC sample, while the core stays asleep the entire time. The CPU only wakes if it actually needs to process a result.
The Decision Framework
Skip the clock-tree lecture. Answer three questions: does the CPU need to sleep, what resolution do you need, and can the work happen without the CPU at all?
Need timing?
|
Does the CPU need to sleep?
/ \
No Yes
| |
(bench/debug only) What resolution?
SysTick OK / \
ms-scale sub-µs
| |
RTC TIMER
(gate HFCLK!)
|
Event-driven, no CPU?
|
RTC/TIMER + PPI/DPPI
(core-independent path)The comparison table, if you want it flat:
Feature SysTick RTC TIMER
--------------- -------- --------- ----------
Clock source CPU clk LFCLK HFCLK
Resolution High ~30 µs ns-scale
Sleep-friendly No Yes With gating
CPU-independent No Yes Yes (PPI)
Best for Never* Periodic Precise
(battery) wakeups measurementRules of thumb I’d hand a teammate:
- Periodic wake at millisecond scale? RTC. This covers most advertising and sensor-poll designs. Pair it with sensible advertising interval tuning and you’re most of the way to a multi-year budget.
- Sub-microsecond timing or precise pulse generation? TIMER, but gate the HFCLK. Turn it on for the measurement, turn it off after. Never leave it running to hold a timer alive.
- Event-to-event with no processing in between? PPI or DPPI chaining the peripherals directly. The CPU never wakes.
- On the sleep path? Never SysTick. If any part of your timing touches sleep, SysTick is off the table.
There’s no single best timer, only the right one for the resolution-versus-power tradeoff you’re facing. RTC buys you sleep at the cost of resolution. TIMER buys you resolution at the cost of current. Match the tool to the budget.
Common Mistakes and Quick Wins
A few patterns show up over and over in battery designs that miss their target.
Busy-wait delays. A while loop burning cycles to wait 10 ms keeps the core fully awake for 10 ms every cycle. Replace it with an RTC-driven sleep so the CPU actually powers down.
Leaving HFCLK on. The high-frequency clock should be up only during radio events and precise TIMER work. If something requests it and never releases it, you’ll sit in the milliamp range forever. Audit your clock requests.
SysTick as the RTOS tick. The single most common own-goal. On Zephyr, confirm the nRF RTC timer is your tick source. On other RTOSes, check whether your port assumes SysTick and swap it.
Not measuring. You can’t optimize sleep current by reading source code. Put a Power Profiler Kit II inline and look at the actual trace. The number the profiler shows is the only one that’s real, and it’ll usually point straight at whatever’s holding the core awake.
What To Do Next
Reach for the RTC for periodic wakeups, gate the HFCLK around TIMER work, chain events through PPI/DPPI when the CPU has nothing to add, and keep SysTick off the sleep path entirely.
Then measure. Put a profiler on the rail and confirm you’re hitting the sleep current the datasheet promised. If you’re not, your timing source is the first place to look. If you’re building the firmware side of a Hubble device, the device SDK docs and the Zephyr reference application show the RTC-backed timing setup already wired up correctly, which saves you the debugging session that brought you here.
Hubble Network handles the low-power timing details so your BLE devices hit their sleep-current budget without a profiler-driven debugging marathon. See how it works →