
Rate Monotonic Scheduling (RMS) assigns static priorities based on period — shorter period, higher priority. It’s optimal among fixed-priority schemes, but it has a structural weakness: priority inversion. A high-priority task blocks on a mutex held by a low-priority task, and a medium-priority task preempts the low-priority holder, indefinitely delaying the high-priority task.
The Priority Ceiling Protocol (PCP) eliminates this by assigning each mutex a ceiling priority equal to the highest priority of any task that may lock it. When a task locks the mutex, its effective priority is immediately elevated to the ceiling. No medium-priority task can preempt it. The high-priority task waits at most one critical section — the blocking is bounded and non-transitive.
This article shows how PCP works, how to implement it in FreeRTOS, and how to verify the blocking bound in response-time analysis.
Consider three tasks under RMS:
+--------+--------+-----------+--------------------------+| Task | Period | Priority | Critical Section (mutex) |+--------+--------+-----------+--------------------------+| T_High | 10 ms | 3 (High) | Locks M for 2 ms || T_Med | 20 ms | 2 (Med) | No mutex || T_Low | 50 ms | 1 (Low) | Locks M for 5 ms |+--------+--------+-----------+--------------------------+
Timeline without PCP:
Time →T_Low: [===== Lock M (5ms) =====]T_High: [BLOCKED on M]T_Med: [======== PREEMPTS T_Low ========]T_High: [STILL BLOCKED]T_Low: [UNLOCK M]T_High: [RUNS]
T_High suffers unbounded blocking — it waits for T_Low and T_Med. The medium-priority task indirectly blocks the high-priority task.
Each mutex M gets a ceiling priority C(M):
C(M) = max{ priority(T) | T may lock M }
In the example above: C(M) = priority(T_High) = 3.
When T_Low locks M, its effective priority becomes 3. T_Med (priority 2) cannot preempt. T_Low runs to completion of its critical section at priority 3, then unlocks and drops back to priority 1.
Time →T_Low: [===== Lock M (5ms) =====] ← runs at priority 3 (ceiling)T_High: [BLOCKED on M]T_Med: [CANNOT PREEMPT — T_Low at ceiling 3]T_High: [RUNS immediately after unlock]
Key properties:
FreeRTOS does not implement PCP natively. It provides Priority Inheritance (PIP) via configUSE_MUTEXES + xSemaphoreCreateMutex(). For PCP, you must implement the ceiling logic manually or use a wrapper.
#include "FreeRTOS.h"#include "semphr.h"#include "task.h"typedef struct {SemaphoreHandle_t mutex;UBaseType_t ceiling_priority; // PCP ceilingUBaseType_t holder_priority; // Original priority of holderTaskHandle_t holder; // Current holder} CeilingMutex_t;/* Create a ceiling mutex with explicit ceiling priority */CeilingMutex_t *CeilingMutex_Create(UBaseType_t ceiling_prio) {CeilingMutex_t *cm = pvPortMalloc(sizeof(CeilingMutex_t));if (!cm) return NULL;cm->mutex = xSemaphoreCreateMutex();cm->ceiling_priority = ceiling_prio;cm->holder = NULL;cm->holder_priority = 0;return cm;}/* Lock with priority ceiling elevation */BaseType_t CeilingMutex_Lock(CeilingMutex_t *cm, TickType_t timeout) {if (xSemaphoreTake(cm->mutex, timeout) != pdTRUE) {return pdFALSE;}/* Elevate priority to ceiling */cm->holder = xTaskGetCurrentTaskHandle();cm->holder_priority = uxTaskPriorityGet(cm->holder);vTaskPrioritySet(cm->holder, cm->ceiling_priority);return pdTRUE;}/* Unlock and restore original priority */void CeilingMutex_Unlock(CeilingMutex_t *cm) {if (cm->holder == xTaskGetCurrentTaskHandle()) {vTaskPrioritySet(cm->holder, cm->holder_priority);cm->holder = NULL;cm->holder_priority = 0;}xSemaphoreGive(cm->mutex);}/* Delete and free */void CeilingMutex_Delete(CeilingMutex_t *cm) {vSemaphoreDelete(cm->mutex);vPortFree(cm);}
/* Mutex ceiling = highest priority of any task using it (Priority 3) */static CeilingMutex_t *spi_mutex = NULL;void SystemInit(void) {spi_mutex = CeilingMutex_Create(3); /* Ceiling = Priority 3 (T_High) */}void Task_High(void *pv) {for (;;) {if (CeilingMutex_Lock(spi_mutex, pdMS_TO_TICKS(100))) {/* Critical section: SPI transaction */SPI_Transaction();CeilingMutex_Unlock(spi_mutex);}vTaskDelay(pdMS_TO_TICKS(10));}}void Task_Low(void *pv) {for (;;) {if (CeilingMutex_Lock(spi_mutex, pdMS_TO_TICKS(100))) {/* Long critical section: 5ms SPI DMA setup */SPI_DMA_Setup();CeilingMutex_Unlock(spi_mutex);}vTaskDelay(pdMS_TO_TICKS(50));}}
The classic response-time equation for task τ_i under RMS:
R_i = C_i + B_i + Σ_{j ∈ hp(i)} ⌈R_i / T_j⌉ × C_j
Where:
C_i = WCET of task τ_iB_i = Blocking time from lower-priority taskshp(i) = set of higher-priority tasksB_i with PCPUnder PCP, task τ_i can be blocked at most once by a lower-priority task holding a resource whose ceiling is ≥ priority of τ_i:
B_i = max{ C_k^crit | priority(k) < priority(i) AND ceiling(M) ≥ priority(i) }
Where C_k^crit is the WCET of the critical section of task τ_k on mutex M.
Example calculation:
| Task | Period | Priority | WCET | Critical Section (M) |
|---|---|---|---|---|
| T_H | 10 ms | 3 | 2 ms | 1 ms |
| T_M | 20 ms | 2 | 3 ms | — |
| T_L | 50 ms | 1 | 5 ms | 4 ms |
Ceiling(M) = 3 (from T_H)
Wait — this is a common misconception. Let’s correct:
Under PCP, T_H can be blocked by T_L if T_L holds M when T_H is released. But the blocking is bounded by T_L’s critical section (4 ms). The key is: T_H is blocked only by T_L’s critical section, not by T_M.
Correct blocking terms:
B_H = max critical section of lower-priority tasks with ceiling ≥ 3 = 4 ms (T_L)B_M = max critical section of lower-priority tasks with ceiling ≥ 2 = 4 ms (T_L)B_L = 0
Response times:
All tasks schedulable.
+------------------------------------------------------------------+| PRIORITY CEILING PROTOCOL || STATE MACHINE |+------------------------------------------------------------------+TASK STATES (per task) MUTEX STATE+-----------------+ +-----------------+| | | || READY | | UNLOCKED || | | | ceiling = 3 || v | | || RUNNING | +-----------------+| | | || | lock(M) | | lock(M) by T_Low| v | v| BLOCKED <----+---------------------------------->+-----------------+| | | | LOCKED || | unlock(M) | | holder = T_Low|| v | | eff_prio = 3 || READY | | (ceiling) || | +-----------------++-----------------+ |^ | unlock(M)| v| +-----------------++-------------------------------------->| UNLOCKED |+-----------------+PREEMPTION BEHAVIOR WITH PCPTimeline (Time ->)+------------------------------------------------------------------+| T_Low (prio 1): [====== LOCK M ========] [UNLOCK] [==========] || | | | || | eff_prio=3 | eff_prio=3 | || v v v || T_High (prio 3): ------ BLOCKED ------> RUNNING ------------> || ^ ^ ^ || T_Med (prio 2): --- CANNOT ----- CANNOT ---- CANNOT --------- || PREEMPT PREEMPT PREEMPT |+------------------------------------------------------------------+Legend: [====] = Running at effective priority [===] = Critical section
| Criterion | Priority Ceiling (PCP) | Priority Inheritance (PIP) |
|---|---|---|
| Blocking bound | Single critical section max | Single critical section max |
| Chained blocking | Impossible | Possible (transitive) |
| Deadlock freedom | Guaranteed | Not guaranteed |
| Implementation complexity | Higher (static ceilings) | Lower (dynamic) |
| Runtime overhead | One priority change per lock | Multiple priority changes |
| RTOS support | Manual / AUTOSAR / ARINC 653 | FreeRTOS (native), Zephyr, QNX |
| Best for | Safety-critical, certifiable | General-purpose, dynamic workloads |
Ceiling assignment: Compute ceilings offline during system configuration. The ceiling is the maximum priority of any task that may lock the mutex — not just tasks that currently do.
Interrupt safety: ISRs must not call CeilingMutex_Lock with blocking timeout. Use timeout = 0 or a separate lock-free ring buffer for ISR-to-task communication.
Nested critical sections: PCP handles nested locks correctly if ceilings are computed transitively. The effective priority becomes the maximum of all held mutex ceilings.
Stack usage: Priority elevation means the task runs at a higher priority — ensure its stack can accommodate worst-case preemption depth at that priority.
Verification: Use response-time analysis with the B_i blocking term. Tools like Rapita RapiTime, LDRA, or Mast can automate this.
Priority inversion is not a theoretical curiosity — it caused the Mars Pathfinder reset in 1997. Rate Monotonic Scheduling guarantees optimal fixed-priority assignment, but without a resource protocol, the scheduling guarantees collapse under resource sharing.
The Priority Ceiling Protocol restores the guarantees:
B_i fits directly into response-time analysisFor safety-critical systems (automotive, avionics, medical), PCP or its dynamic variants (DPCP, Stack Resource Policy) are mandatory. For general-purpose FreeRTOS applications, the native Priority Inheritance mutex is often sufficient — but understand its limits. If your system cannot tolerate chained blocking, implement PCP explicitly as shown.
Quick Links
Legal Stuff

