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.