How Nano's Priority Inheritance Stops Priority Inversion in Small Embedded Systems
Priority inversion silently breaks real-time deadlines on small RTOSes. Nano's mutexes ship with priority inheritance — here's how it works, how to verify it on hardware, and where it falls short.
03 Oct 2025, 10:04 UTC

A motor-control task misses its deadline not because it ran out of CPU time, but because a logging task was holding a mutex it needed — and a medium-priority sensor task kept preempting the logger. That is priority inversion, and on a small RTOS it is one of the most common causes of "the firmware works on the bench but glitches in the field." Nano addresses it directly: its preemptive scheduler ships with priority inheritance built into the mutex API, so the fix is mostly a matter of using the right primitive correctly.
The problem in concrete terms
Priority inversion happens when three tasks collide:
- A low-priority task acquires a mutex protecting a shared resource, such as a UART or a sensor bus.
- A high-priority task wakes up and blocks on that same mutex.
- A medium-priority task — which wants nothing to do with the mutex — preempts the low-priority task, so the high-priority task waits behind work it should outrank.
The high-priority task's effective priority has been dragged down to below the medium task's. On a preemptive kernel like Nano's, which performs a context switch the moment a higher-priority task becomes ready, this is exactly the scenario the scheduler is designed to avoid — but preemption alone cannot fix it, because the blocker is a resource, not a runnable task.
What priority inheritance actually does
Nano's mutex implementation includes priority inheritance. When a high-priority task blocks on a mutex held by a lower-priority task, the holder's priority is temporarily raised to match the waiter's. The low-priority task now runs at the elevated priority, finishes its critical section without being preempted by medium-priority work, releases the mutex, and drops back to its original priority. The high-priority task unblocks and runs.
The inversion is bounded to the length of one critical section instead of being unbounded by whatever medium-priority tasks happen to be ready. That bound is what makes the system analyzable for real-time deadlines.
Under the hood, Nano's scheduler uses a priority bitmap for O(1) lookup of the highest ready task. Finding the next task to run is a constant-time operation regardless of how many tasks exist, which keeps the inheritance boost cheap even on microcontrollers with tight cycle budgets. The trade-off is that the bitmap assumes a fixed maximum number of priority levels — exceeding that count requires source changes, so plan your priority map at design time.
A worked example
Consider three tasks on a Cortex-M class target sharing a UART:
nano_mutex_t uart_mutex;
void app_init(void)
{
/* Must be initialized before any task locks it */
nano_mutex_init(&uart_mutex);
nano_task_create(&log_task, "log", LOG_PRIO_LOW, log_entry, ...);
nano_task_create(&imu_task, "imu", IMU_PRIO_MED, imu_entry, ...);
nano_task_create(&ctrl_task, "ctrl", CTRL_PRIO_HIGH, ctrl_entry, ...);
}
void log_entry(void *arg)
{
for (;;) {
nano_mutex_lock(&uart_mutex); /* inherits CTRL_PRIO_HIGH if ctrl blocks */
uart_write(dma_buf, len);
nano_mutex_unlock(&uart_mutex); /* priority restored here */
nano_sleep(LOG_PERIOD_TICKS);
}
}Function names above are illustrative — check your Nano tree's headers (kernel/mutex.h or equivalent) for the exact API, since names vary between versions and forks. The behavioral contract is the point: lock triggers the inheritance boost when a higher-priority waiter appears, and unlock restores the original priority.
How to verify it on real hardware
Do not take inheritance on faith. A practical check:
- Build a sample with the three tasks above and enable Nano's trace or debug logging facility.
- Arrange for the low-priority task to hold the mutex in a long-ish critical section (a few milliseconds of UART output works), then make the high-priority task block on the same mutex while the medium task spins in a busy loop.
- Watch the trace output: you should see the low-priority task's priority jump to the high level during the critical section and drop back at unlock. Without inheritance, you would instead see the medium task running while the high-priority task stays blocked.
- Use the SysTick or a GPIO toggle plus a logic analyzer to confirm the high-priority task preempts promptly once the mutex is released.
You can also read the source: the priority bitmap lives in the scheduler code (kernel/sched.c in typical trees), and the inheritance logic sits in the mutex lock/unlock paths. Confirming the mechanism exists in the exact version you ship is worth ten minutes.
Limitations worth designing around
Priority inheritance is not a universal fix, and Nano's implementation has sharp edges:
- Only mutexes participate. If a task blocks on a semaphore, queue, or other non-mutex primitive, no inheritance occurs. Protect shared resources with
nano_mutex-type objects, not binary semaphores, when inversion matters. - Stack usage grows. A boosted task runs its critical section at a priority where it may now preempt differently than you assumed; size stacks with the boosted scenario in mind.
- Initialization is mandatory. A forgotten
mutex_initor misconfigured attribute can produce unprotected critical sections or deadlock, and these failures are intermittent by nature. - Priorities are static in practice. Changing task priorities dynamically requires reinitializing task control blocks, so assign priorities at startup using a deliberate scheme (rate-monotonic assignment is a reasonable default).
- Inheritance does not handle chains perfectly. If a task holds multiple mutexes, worst-case blocking time grows; keep critical sections short and avoid nested locks where possible.
Actionable takeaway
Audit your firmware for every shared resource and ask two questions: is it protected by a Nano mutex (not a semaphore), and is that mutex initialized before any task can lock it? Then run the three-task trace experiment above on your target board. If you see the priority boost in the log, your high-priority deadlines are bounded; if you do not, you have found the bug before your customers do.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.