Zephyr RTOS Timer Tick Calculator

Convert k_msleep()/k_timer_start() durations to Zephyr kernel ticks and back for any CONFIG_SYS_CLOCK_TICKS_PER_SEC. Shows the ceiling-rounded tick count, the exact actual delay, and the rounding error.

Input

ticks = ceil(duration_us × ticksPerSec / 1e6)
duration_us = ticks × 1e6 / ticksPerSec

Result

ticks (ceiling)

50

requested 1.5 ms

Actual duration1.525879 ms
Rounding error+25.879 µs (+1.725%)
ms / tick0.030518 ms
Rounding error > 0.5% of the requested duration.

50 ticks = exact duration

1.525879 ms

1525.878906 µs

Common durations at 32,768 Hz

RequestedTicksActual durationError
1 ms331.00708 ms+7.080 µs (+0.708%)
10 ms32810.009766 ms+9.766 µs (+0.098%)
100 ms3,277100.006104 ms+6.104 µs (+0.006%)
1000 ms32,7681 s0

Why Zephyr counts in ticks, not milliseconds

Every k_timeout_t the kernel schedules, k_msleep(), k_timer_start(), k_sem_take() with a timeout, semaphore and mutex waits, is stored and compared internally as a tick count, not as raw milliseconds. The tick rate is set by the Kconfig option CONFIG_SYS_CLOCK_TICKS_PER_SEC, an integer that’s board-dependent, not a project-wide constant. When you call k_msleep(10), the kernel converts 10 ms to ticks using the board’s configured rate before it ever touches the timer hardware.

The reason for ticks instead of a raw millisecond counter is that the underlying hardware timer, an RTC, a dedicated timer peripheral, whatever the SoC exposes, runs at whatever frequency it runs at, and that frequency rarely lines up with a clean 1 kHz. Ticks are the kernel’s internal unit of time precisely so the scheduler doesn’t have to care what that hardware frequency is. Every timeout conversion, in both directions, goes through the same tick arithmetic covered by this calculator (k_ms_to_ticks_ceil32, k_us_to_ticks_ceil32, and their inverses in zephyr/include/zephyr/sys_clock.h).

Use the calculator above to convert a duration to ticks, convert a tick count back to an exact duration, and see the reference table of common durations for whichever tick rate you’re targeting.

The ceiling-rounding formula

Forward conversion, duration to ticks:

ticks = ceil(duration_us * ticksPerSec / 1_000_000)

Reverse conversion, ticks to the exact duration those ticks represent:

duration_us = ticks * 1_000_000 / ticksPerSec

The forward direction always rounds up. This isn’t an implementation detail you can ignore: k_ms_to_ticks_ceil32() and k_us_to_ticks_ceil32() are the functions Zephyr actually calls when converting a k_timeout_t, and both ceil. The reasoning is straightforward. A timeout is a promise not to fire before the requested time elapses. If a partial tick got truncated or rounded to nearest, a duration could fall on the wrong side of a tick boundary and the timer could expire one tick before the caller actually asked for, waking a k_msleep() early or firing a k_timer a fraction early. Ceiling rounding guarantees the actual delay is always greater than or equal to the requested delay, never less.

That guarantee has a cost: except when the requested duration happens to be an exact multiple of the tick period, the actual delay is always slightly longer than requested, never shorter.

Worked example: 1500 us at 32768 Hz

32768 Hz is the exact hardware tick rate on nRF52 boards running the LFCLK-driven tickless kernel, since it matches the 32.768 kHz RTC crystal on those boards. Request a 1500 us delay:

ticks = ceil(1500 * 32768 / 1_000_000)
      = ceil(49.152)
      = 50

Convert those 50 ticks back to a duration:

duration_us = 50 * 1_000_000 / 32768
            = 1525.87890625 us

The requested 1500 us delay actually runs for 1525.879 us, a rounding error of +25.879 us, or +1.725% of the requested duration. That error is entirely deterministic. It isn’t jitter, isn’t measurement noise, and isn’t hardware-dependent beyond the tick rate itself. Anyone converting 1500 us at 32768 ticks/s gets exactly this number, every time.

Tickless kernel: the tick still matters

Most modern Zephyr boards build with CONFIG_TICKLESS_KERNEL=y. In a tickless kernel, the hardware timer isn’t reprogrammed on every tick boundary. Instead, the kernel looks at the next scheduled expiry across all active timeouts and programs the hardware to fire once, at that point, however many ticks away it is. There’s no periodic tick interrupt burning cycles when nothing is due.

What tickless does not change is the unit the kernel reasons in. k_timer_start(), k_msleep(), and every internal k_timeout_t comparison still go through the same ceiling conversion this calculator implements, tick counts still get compared and subtracted to figure out ordering and remaining time, and the hardware timer still gets programmed with a tick-derived count, converted to whatever units that specific timer peripheral wants. Tickless changes when the hardware interrupts you. It doesn’t change what unit the timeout math happens in.

When 32768 Hz is exact, and when it isn’t

It’s easy to conflate two different claims about 32768 Hz. The first is true: it’s the exact hardware rate on nRF52 LFCLK-tickless boards, so a tick count corresponds 1:1 to real RTC cycles. There’s no additional hardware-side rounding beyond the kernel’s own tick conversion.

The second claim, that converting a round millisecond duration into 32768 Hz ticks lands on a whole number of ticks, is false, and it’s the more consequential one in practice. 1000 does not divide evenly into 32768. One tick at 32768 Hz is 1,000,000 / 32768 = 30.517578125 us, not a round number of microseconds. Request 10 ms (10,000 us): 10,000 / 30.517578125 = 327.68 ticks, which ceils to 328, giving an actual duration of 328 * 30.517578125 = 10,009.765625 us. That’s 9.766 us over the 10 ms request, about 0.098%. Small, but not zero, and not something you’d predict from “32768 Hz is the exact RTC rate” alone. Exact hardware rate and exact ms-to-tick conversion are two different properties, and 32768 Hz has the first without the second.

When the ms ≈ ticks rule of thumb breaks down

At high enough tick rates the rounding error stays small relative to typical timeout durations, and it’s tempting to eyeball ticks as roughly equal to milliseconds. That approximation collapses at low tick rates.

CONFIG_SYS_CLOCK_TICKS_PER_SEC=100 gives a 10 ms tick period, a value that does show up as a coarse or legacy override on some boards. Request a 5 ms delay:

ticks = ceil(5000 * 100 / 1_000_000) = ceil(0.5) = 1
duration_us = 1 * 1_000_000 / 100 = 10,000 us = 10 ms

A 5 ms request becomes a 10 ms actual delay, a 100% rounding error. At 100 Hz, any duration shorter than one full tick period rounds up to that entire tick, and the shorter the request relative to the tick period, the worse the relative error gets. This is exactly why CONFIG_SYS_CLOCK_TICKS_PER_SEC matters for anything doing precise short-interval timing, not just an internal Kconfig knob to leave at whatever the board default happens to be. If your application schedules sub-millisecond or low-single-digit-millisecond timeouts, check the board’s tick rate before assuming it’s fine.

Two tick rates avoid this entirely for whole-millisecond inputs: 100 Hz and 1000 Hz both divide 1000 evenly (10 ms and 1 ms per tick respectively), so any duration that’s a whole multiple of the tick period converts with zero rounding error. 32768 Hz and 1,000,000 Hz don’t divide 1000 evenly, so they always carry some rounding error on ordinary millisecond-granularity requests, small as it usually is. Run your actual tick rate and requested durations through the calculator above rather than assuming either extreme.

Frequently asked questions

Why does the calculator round ticks UP instead of to the nearest tick? +

Zephyr's real conversion functions (k_ms_to_ticks_ceil32, k_us_to_ticks_ceil32) always round up. A kernel timeout is a promise not to fire before the requested time. Rounding to nearest, or truncating, could produce a tick count that expires before the requested duration has actually elapsed, which would mean k_msleep() or a k_timer wakes the caller early. Ceiling rounding guarantees the actual delay is always >= the requested delay, never less.

Is CONFIG_SYS_CLOCK_TICKS_PER_SEC always 32768 on Zephyr boards? +

No. 32768 Hz is the exact hardware tick rate on nRF52 boards using the LFCLK-driven tickless kernel (it matches the 32.768 kHz RTC crystal), which is why it shows up so often in nRF52 board configs. It is not a project-wide default. CONFIG_SYS_CLOCK_TICKS_PER_SEC is set per board (or overridable in a prj.conf), and other boards commonly use 100 Hz or 1000 Hz. Check your board's defconfig before assuming a value.

If Zephyr runs tickless, why does the tick count still matter? +

CONFIG_TICKLESS_KERNEL=y (the default on most modern boards) means the kernel doesn't take a periodic tick interrupt. It programs the hardware timer to fire once, at the next actual expiry, instead of counting ticks on a fixed interrupt cadence. The tick is still the unit all timeout arithmetic is done in internally: k_timer_start(), k_msleep(), and every k_timeout_t conversion still go through the same ms-to-ticks-ceil path before the kernel programs the timer. Tickless changes when the hardware interrupts you, not the unit the kernel reasons in.

Why doesn't a 10 ms delay convert to a whole number of ticks at 32768 Hz? +

1000 does not divide evenly by 32768 (or vice versa): 32768 ticks/s means 1 tick = 1,000,000 / 32768 = 30.517578125 us, not a round number of microseconds or milliseconds. 10 ms = 10,000 us needs 10,000 / 30.517578125 = 327.68 ticks, which ceils to 328. That's why 32768 Hz, despite being an exact hardware rate, still produces small rounding error on ordinary millisecond-granularity requests. Only tick rates that evenly divide 1000 (100 Hz, 1000 Hz) give zero rounding error at whole-millisecond inputs.

Newsletter

The embedded engineer's weekly cheat sheet

Register tricks, timing gotchas, and tool updates. One email per week. No fluff.

No spam. Unsubscribe anytime.