PSoC5LP RTOS
Lab 04 — Preemptive Scheduling, Starvation & Time Slicing
FreeRTOS on PSoC 5LP · Lab Series. Platform: CY8CKIT-059 (PSoC 5LP, Cortex-M3) · PSoC Creator 4.4 · FreeRTOS Kernel V11. Pairs with Chapter 2 — Timing & Priority.
1. Introduction & Objectives
The scheduler decides who runs. Two mechanisms control that decision: priority + pre-emption (across different priorities) and time slicing (among equal priorities). This lab makes both visible. Part A: three tasks at different priorities show pre-emption and starvation. Part B: three tasks at the same priority show what configUSE_TIME_SLICING does — and, just as important, what it does not do.
Course default: this course builds with configUSE_TIME_SLICING = 1 (ON). Part B temporarily sets it to 0 to compare, then you set it back to 1.
After this lab you can:
- State the pre-emption rule and show it on LEDs / a logic analyzer.
- Explain starvation across priorities: a high-priority task that never blocks stops everything below it.
- Explain time slicing among equal priorities: ON = tick round-robin; OFF = run-until-block.
- Compare configUSE_TIME_SLICING = 0 vs = 1 — behaviour, pros, cons — and choose by scenario.
- Change a task's priority at run time (vTaskPrioritySet / uxTaskPriorityGet) and yield with taskYIELD().
2. Prerequisites
Hardware: CY8CKIT-059 PSoC 5LP Prototyping Kit; RobotSoC Kit 3× LED + 3× 220 Ω; optional logic analyzer on 3 timing pins.
Software: PSoC Creator 4.4 with the FreeRTOS V11 BaseProject.
Chapters (sync progress): Required — Chapter 2 (Timing & Priority: pre-emption, priority by deadline, time slicing, run-time priority); Peripheral prerequisite — basic GPIO is covered in the separate (non-FreeRTOS) Lab 02.
3. Reading Materials / References
- Chapter 2 slides — Timing & Priority (pre-emption, choosing priorities, time slicing, run-time priority & taskYIELD).
- FreeRTOS API: vTaskPrioritySet, uxTaskPriorityGet, taskYIELD, vTaskDelay.
4. Background & Theory of Operation
4.1 Pre-emption and cross-priority starvation (Part A). With configUSE_PREEMPTION = 1, the scheduler always runs the highest-priority Ready task. The instant a higher-priority task becomes Ready it pre-empts whatever is running. If every task blocks (e.g. vTaskDelay), they take turns by priority and all make progress. Starvation is the failure mode: a high-priority task that never blocks stays Ready forever, so the scheduler never runs anything below it. The lesson: a task that never blocks breaks the system, no matter how it is prioritised.
4.2 Time slicing and equal-priority sharing (Part B). Time slicing decides what happens among tasks of equal priority:
- configUSE_TIME_SLICING = 1 (ON — course default): among equal-priority Ready tasks, the scheduler round-robins on every tick (1 ms here). N equal tasks each get ~1/N of the CPU; even a long computation is chopped each tick so peers keep running.
- configUSE_TIME_SLICING = 0 (OFF): no automatic rotation among equals. The running task keeps the CPU until it blocks or calls taskYIELD(). Equal-priority tasks that all block still take turns, but only at block boundaries (coarse); and a single equal task that never blocks starves all its equal peers.
4.3 The one rule that ties it together. Time slicing only affects EQUAL priority. It changes nothing across priorities: a higher-priority Ready task always pre-empts, and a higher-priority task that never blocks starves lower ones whether slicing is on or off. So cross-priority sharing is decided by priority + pre-emption, and equal-priority sharing by time slicing.
4.4 Run-time priority & yielding. uxTaskPriorityGet(NULL) reads the caller's priority; vTaskPrioritySet(h, p) changes a task's priority while it runs (may pre-empt at once). taskYIELD() voluntarily hands the CPU to an equal-priority peer — the only way to share between equals when slicing is off. Priority should be set by deadline, not importance (rate-monotonic).
5. RTOS Task Architecture
Part A uses three different priorities over one CPU: High (p3) pre-empts Medium (p2), which pre-empts Low (p1); Idle (p0) runs only when all are blocked. Part B instead gives three tasks the same priority, so pre-emption is out of the picture and time slicing alone decides how they share the CPU.
6. Part A — Cross-Priority: Preemption vs. Starvation
Every task blocks — all three make progress:
High runs first, then blocks; Medium runs, then Low; Idle fills the gaps.
The High task busy-loops — Medium and Low are starved:
After the High task stops yielding, nothing below it ever runs again. Slicing cannot fix this — it is a priority problem.
7. Part B — Equal Priority: Time Slicing ON vs. OFF
Now all three tasks share one priority. Priority no longer separates them, so the only thing deciding the CPU split is configUSE_TIME_SLICING.
= 1 (ON, course default): tick round-robin — fair, fine-grained sharing:
Each tick the scheduler rotates A → B → C; all three advance together.
= 0 (OFF): run-until-block — coarse, one task at a time:
Each task runs its whole burst, then blocks and the next runs. If a task never blocks, it hogs the CPU and the equal peers are starved.
8. Time Slicing: Behaviour, Trade-offs & How to Choose
| = 1 (ON) — course default | = 0 (OFF) | |
|---|---|---|
| Equal-priority behaviour | Round-robin every tick; each equal task gets ~1/N CPU | No rotation; run until block / taskYIELD; equal peers take turns only at block points |
| Pros | Fair among equals; forgiving (a non-blocking equal task won't fully lock peers out); least surprising (FreeRTOS default) | Deterministic worst-case timing; fewer context switches (lower overhead, better bursts, lower power); encourages distinct-priority design |
| Cons | More context switches each tick; less deterministic; chops coherent bursts; more wake-ups (worse for tickless idle) | No auto-fairness — a non-blocking equal task starves peers; needs disciplined design (always block/yield); reverse-intuitive for beginners |
Choose by scenario:
| Scenario | Pick |
|---|---|
| General-purpose app; several comparable background tasks; want fairness & easy coding | ON (= 1) |
| Hard real-time; rate-monotonic; need deterministic worst-case, low jitter | OFF (= 0) |
| Throughput / long compute bursts you don't want chopped every tick | OFF (= 0) (let a burst finish; give distinct priorities or yield) |
| Strictly event-driven (tasks mostly block on queues/semaphores/notifications) | Either; OFF avoids needless switches |
| Low power with tickless idle | OFF (= 0) (fewer forced wake-ups) |
Remember: none of this helps across priorities — slicing only governs equal-priority tasks.
9. Schematic Circuit & Pin Configuration
- LED_H / LED_M / LED_L (Part A) or LED_A / LED_B / LED_C (Part B) → three GPIOs + 220 Ω → GND.
- (Optional) TP_x → three timing pins to a logic analyzer; pulse one high while its task runs.
- Assign all pins in <project>.cydwr → Pins.
10. PSoC Creator Configuration
- Three Digital Output Pin components for the LEDs; optional three more for timing pins.
- FreeRTOSConfig.h: configUSE_PREEMPTION = 1, configUSE_TIME_SLICING = 1 (course default), configMAX_PRIORITIES ≥ 4, heap_4.
- Part B toggles configUSE_TIME_SLICING to 0 to compare, then back to 1. Rebuild after each change.
11. Provided Template
Two templates are provided:
- Lab04_Template_A_crosspriority_main.c — Part A. Three tasks at priorities 3 / 2 / 1; a HIGH_BUSY_LOOP switch demonstrates starvation.
- Lab04_Template_B_equalpriority_main.c — Part B. Three tasks at the same priority driving LED_A/B/C (+ timing pins). A TASKS_BLOCK switch chooses "burst then block" vs "never block"; you flip configUSE_TIME_SLICING in FreeRTOSConfig.h to compare ON vs OFF.
Both have a complete main() and task placeholders so the project builds and runs; note #include "FreeRTOS_PSoC.h". Do not change the boot sequence in main().
/* Part B - three tasks, SAME priority */
#define TASKS_BLOCK 1 /* 1 = burst then vTaskDelay; 0 = never block */
/* ...create A,B,C all at tskIDLE_PRIORITY + 1u... */
static void vEqualTask(void *pvName) /* one body, three instances */
{
for (;;) {
/* TODO: TP_x high; busy-work ~ burst; TP_x low; */
#if TASKS_BLOCK
vTaskDelay(pdMS_TO_TICKS(5)); /* yields -> peers run */
#endif
/* with TASKS_BLOCK=0 and slicing OFF, this task starves its peers */
}
}
12. Lab Experiments
Exp01 — Basic: cross-priority preemption & starvation (Part A). Run Template A. First with blocking (HIGH_BUSY_LOOP = 0): all three LEDs blink, High leads but Med/Low still run. Then HIGH_BUSY_LOOP = 1: LED_M and LED_L freeze (starvation). Confirm slicing does not help — try toggling it and show Med/Low are still starved. Expected: you can explain cross-priority starvation and why slicing is irrelevant to it.
Exp02 — Advanced: equal-priority time slicing ON vs OFF (Part B). Run Template B with all three tasks at the same priority and TASKS_BLOCK = 1. (1) With configUSE_TIME_SLICING = 1 (default): the three timing pins interleave every ~1 ms; the LEDs look evenly active. (2) Rebuild with configUSE_TIME_SLICING = 0: the pins now run one full burst at a time. (3) Set TASKS_BLOCK = 0 (never block) with slicing = 0: only Task A runs — equal-priority starvation; switch slicing back to = 1: all three share. Then set configUSE_TIME_SLICING back to 1. Expected: captures showing fine interleave (ON) vs one-at-a-time (OFF), and the starvation case.
Exp03 — Challenge: run-time priority, yielding & the choice. From one equal task, vTaskPrioritySet(NULL, 2) to lift it above its peers and show it now pre-empts them (restore with the value from uxTaskPriorityGet). With slicing off, add taskYIELD() to two equal tasks and show they now share. Finally, write one paragraph: for a given requirement (e.g. hard-real-time control vs. general background logging), which configUSE_TIME_SLICING setting you would choose and why. Expected: a working boost + yield, and a justified slicing choice.
13. Testing & Verification
| # | Pass condition |
|---|---|
| Exp01 | All three blink with blocking; busy-loop starves Med/Low; slicing does not change that. |
| Exp02a | Slicing ON: equal-priority pins interleave each tick; all three advance. |
| Exp02b | Slicing OFF: equal-priority tasks run one burst at a time; non-blocking task starves peers. |
| Exp03 | Run-time boost pre-empts peers; taskYIELD shares under slicing off; slicing choice justified. |
| Cleanup | configUSE_TIME_SLICING set back to 1 after the lab. |
Submit: Part A and Part B captures (or LED descriptions), your task bodies, and your Exp03 slicing-choice paragraph.
14. Q&A & Troubleshooting
- Only the High LED is active (Part A) → the High task never blocks; that is starvation, expected in Exp01's busy-loop case.
- Equal tasks don't interleave even with slicing ON → they may not all be Ready (one is blocking long); or priorities aren't actually equal.
- Equal tasks run one-at-a-time and never alternate (slicing OFF, no blocking) → expected: that is equal-priority starvation; add taskYIELD() or blocking.
- Slicing didn't fix cross-priority starvation → correct — it never can; slicing is equal-priority only.
- vTaskPrioritySet seems ignored → target above configMAX_PRIORITIES-1 is clamped.
- Forgot to revert → set configUSE_TIME_SLICING = 1 before moving on.
15. Analysis Questions & Lab Report
- State the pre-emption rule and give the single change that turns Exp01 from "all run" into "starvation".
- With all tasks at the same priority, contrast configUSE_TIME_SLICING = 1 and = 0: what does the CPU timeline look like in each?
- Why can time slicing not fix cross-priority starvation?
- For a hard-real-time design and for a general-purpose background design, which slicing setting fits each, and why?
Your report should include your captures/observations, your task bodies, and answers to the four questions.