May 3, 2026

Three milliamps, hiding in a default

Our beacon firmware targets the Ezurio BL653, a module built around Nordic's nRF52833. It is a close cousin of the nRF52833 development kit, close enough that most code moves across untouched, and close enough that one difference between them can cost you your entire power budget without a single warning at build time.

The development kit has a 32.768 kHz watch crystal on the board. The BL653 module does not. If you say nothing, Zephyr's BLE controller needs a low-frequency clock from somewhere, and falls back to synthesising one from the high-frequency oscillator. That works. It also means the high-frequency clock can never switch off, which pins the module at roughly 3 mA of continuous idle draw — on a device whose whole reason for existing is to sit still and say nothing for forty-eight seconds at a time. Everything looks healthy. The tag advertises correctly. It simply drains far faster than it should, and nothing in the build output mentions it.

The fix is three lines of Kconfig: run the low-frequency clock from the internal RC oscillator, and turn on its calibration. RC drifts more than a crystal would, but we are doing non-connectable advertising — the tag never has to hold a timing window open with a peer — so 500 ppm is comfortably good enough. The high-frequency oscillator is then free to go back to sleep between bursts, which is the entire point.

While we were in there, a few other defaults turned out to be worth money.

What actually moved the needle

  • Low-frequency clock on RC, not synth. The big one. Roughly 3 mA of continuous idle draw, removed by three lines of configuration.
  • Console, UART, printk and RTT all off. Worth about 1–2 mA continuous against the defaults. There is deliberately no serial output on a shipping tag; if you need to debug one, you turn them back on.
  • TX power down to −4 dBm from 0 dBm. A small reduction in burst energy, and still comfortably in range of a phone in the same room.
  • DCDC regulator on. The board's own defconfig already handles this for the BL653, so the Kconfig symbol we originally reached for was both unnecessary and, as of the current SDK, no longer valid.
  • A 4% duty cycle. Two seconds of advertising, forty-eight seconds asleep. Legacy advertising caps the on-air interval at about 40.96 seconds, so the fifty-second cadence has to be driven from the application rather than handed to the controller.

One change we made and then reverted is worth recording, because it looked productive and was not. We had enabled Zephyr's power management subsystem, on the reasonable assumption that a power-sensitive device should have power management switched on. It does nothing here: CONFIG_PM depends on a capability that nRF52 parts do not advertise, so the symbol is quietly inert. The chip was already sleeping without it — the idle thread's wait-for-interrupt puts the CPU into System ON sleep and gates the peripherals automatically. The savings were never going to come from a Kconfig switch. They came from not leaving a UART running.

What we cannot tell you

We wrote a small tool to check our work, which reads peripheral state over J-Link while the chip runs, without halting it. It confirms the radio is disabled in nearly every sample, the DCDC regulator is enabled, the low-frequency clock is running on RC, and no interrupts are stuck active. One thing it taught us is that you cannot use the debugger to check whether the high-frequency clock is off: reading that register makes the probe pulse the oscillator, so it reports itself as always on. The measurement creates the result. That is a probe artifact, not a bug in the firmware.

Which brings us to the limit of the whole exercise, in the tool's own words:

“ What it CANNOT prove: actual idle current draw. For real current numbers, use a PPK2 or in-line ammeter. ”

tools/check_sleep.sh

So here is the thing we are not going to do, which every other tracker's website does. We are not going to quote you a battery life. We know the radio is asleep 96% of the time. We know which defaults were costing milliamps and we have removed them. What we do not have is a measured idle current from a proper power analyser across a real duty cycle at temperature, and until we do, any number with the word "years" in it would be arithmetic performed on a guess. When we have measured it, we will publish it, along with how we measured it.

If you want the other half of the story — how a tag with no modem and no GPS ends up on a map at all — that is over here.