RTOS code often contains calls like:
vTaskDelay(1);
xQueueReceive(q, &msg, 10);
xTimerStart(timer, 0);
They all look like “wait for some time”. But RTOS time is usually not continuous, precise, and immediately executed. Many failures come from reading tick, delay, timeout, and software timers as precise real-time behavior.
A safer model is:
hardware clock generates tick
-> RTOS updates tick count
-> expired tasks or timers become ready/pending
-> scheduler decides when code actually runs
-> low power and interrupt masking change this path
So delay 10 ms is closer to “do not run until at least a tick boundary and then wait for scheduling” than “this code will run exactly 10 ms later”.
tick Is the RTOS Time Granularity
Many RTOSes use a periodic tick interrupt to advance system time: 1 ms per tick, 10 ms per tick, or another interval.
tick affects:
- task delays
- timeouts
- software timers
- time slicing
- low-power wake decisions
If the system tick is 10 ms, a 1 ms delay request cannot reliably sleep for real 1 ms. It is quantized to tick boundaries and then affected by scheduling latency.
tick: 0ms 10ms 20ms 30ms
task calls delay(1 tick) at 9ms
it may become ready around 10ms
actual execution still depends on the scheduler
Many RTOS delay parameters are in ticks, not milliseconds. Confusing milliseconds with ticks creates subtle bugs.
delay(1) Is Not Precise Timing
delay(1) is often misused as “wait 1 ms” or “let hardware settle”.
It may actually mean:
- delay for one tick
- yield until the next tick
- wait until the scheduler runs this task again
None of these is precise busy-wait timing.
If the tick is 10 ms, delay(1) is a 10 ms-scale wait. If the call happens near a tick boundary, the actual wait also varies.
For microsecond or sub-tick waits, task delay is usually the wrong tool. Consider:
- hardware timers
- short bounded polling of peripheral status
- capture/compare
- DMA or peripheral completion interrupts
- dedicated high-resolution counters
Task delay is for scheduler-level waiting, not waveform timing, protocol bit timing, or precise hardware setup time.
timeout Is a Maximum Wait Boundary
A wait with timeout is not just sleep.
For example:
xQueueReceive(q, &msg, timeout);
It means:
if a message arrives first, return early
if no message arrives, return timeout after the timeout boundary
timeout is the maximum wait boundary, not a guarantee to sleep the whole time.
Also, timeout expiry only means the task can leave the wait queue. Actual execution still depends on priorities, interrupt-disabled time, critical sections, CPU load, and higher-priority tasks.
So “10 tick timeout” means “around 10 ticks, the timeout condition can be satisfied”, not “the next line executes exactly after 10 ticks”.
Software Timers Are Not Thread Pools
In many RTOSes, software timer callbacks do not run in the task that created the timer. They run in a timer service task, daemon task, softirq-like context, or similar mechanism.
This creates important boundaries:
- callbacks must not run for long
- callbacks should not perform blocking I/O
- callbacks should not wait on objects that depend on the same timer service
- heavy logging in callbacks can delay other timers
- one stuck callback can affect all timers served by that context
Software timers fit lightweight work:
- set a state bit
- post a message
- notify a task
- advance a state machine
Heavy work belongs in normal tasks.
Periodic Timers Accumulate Jitter
Periodic tasks are often written in two styles:
delay(period) after each execution
wait until the next absolute period boundary
The first style includes execution time in the period:
run 3ms
delay 10ms
next interval is about 13ms plus scheduling latency
The second style is better for fixed frequency because it uses target time points.
If the RTOS provides a “delay until” style API, periodic tasks should usually prefer it over simply delaying a relative amount every loop.
Otherwise execution time, scheduling latency, logs, and higher-priority tasks cause drift.
tickless idle Changes Tick Intuition
To save power, many RTOSes support tickless idle. When the system is idle, it does not wake the CPU on every tick. It computes the next required wake time and enters deeper sleep.
This changes several assumptions:
- tick interrupts are no longer continuous and equally spaced
- sleep may be timed by a low-power timer or RTC
- after wake, the RTOS must compensate for elapsed ticks
- compensation accuracy depends on the low-power clock
- entering and leaving deep sleep has overhead
If the low-power clock is inaccurate, or compensation is wrong, task delays, timeouts, and software timers drift.
Debugging “bad timeouts” in low-power systems requires checking which clock runs during sleep, not only the RTOS tick configuration.
Interrupt Masking and Critical Sections Delay tick
tick usually comes from a timer interrupt. If the system masks interrupts for too long, tick handling is delayed.
Results include:
- delay appears longer
- timeout returns late
- software timers expire in a burst
- scheduling latency grows
- watchdog heartbeat tasks jitter
Long critical sections, high-priority ISRs, scheduler locks, and interrupt masking in drivers can all prevent “time has arrived” from being processed promptly.
For RTOS timing issues, check:
- whether tick interrupts arrive on time
- maximum interrupt-disabled time
- whether high-priority tasks monopolize CPU
- whether the timer service task has enough priority
- whether software timer callbacks are too heavy
timeout Should Not Replace Real Readiness
Many field issues come from using timeout to hide unclear state design:
wait 100ms and assume device is ready
wait 5 ticks and assume module init is done
wait 1 second and assume network recovered
This turns real dependencies into guessed time. Load, low power, tickless idle, slow peripherals, and retry paths break the assumption.
Prefer:
- wait for explicit events
- wait for status bits
- wait for queue messages
- wait for hardware interrupts
- wait for condition combinations
- use timeout only as the failure boundary
timeout should answer “what if it never happens”, not replace “what are we waiting for”.
Debugging Order
For RTOS problems such as inaccurate timing, long delays, jittery software timers, or low-power timeout failures, inspect:
tick period
-> whether API parameters are ticks or ms
-> relative delay or absolute periodic wait
-> timeout waiting for event or guessed time
-> software timer callback context
-> whether timer service task is blocked
-> long interrupt or scheduler masking
-> whether tickless idle is enabled
-> which low-power clock runs during sleep
-> whether tick compensation after wake is correct
If the issue appears only under low power, verbose logging, high load, or high interrupt rate, focus on scheduling latency, interrupt-disabled time, and timer callback context.
RTOS Time Is a System Path
RTOS time is not just a number. It goes through hardware clocks, ticks, kernel counters, wait queues, software timers, the scheduler, and power management.
vTaskDelay(1), software timers, and timeouts all depend on that path. They express scheduler-level waiting and failure boundaries, but they do not automatically provide precise real time.
Separate tick granularity, scheduling latency, low-power compensation, and callback context, and many “bad timing” failures become measurable system behavior.