PSoC5LP RTOS
Lab 24 — Critical Sections & Mutual Exclusion
FreeRTOS on PSoC 5LP · Lab Series. Platform: CY8CKIT-059 (PSoC 5LP, Cortex-M3) · PSoC Creator 4.4 · FreeRTOS Kernel V11. Topics: taskENTER/EXIT_CRITICAL, the _FROM_ISR forms, vTaskSuspendAll/xTaskResumeAll, and when to use each. Pairs with Chapter 5 — Mutexes & Chapter 15 — Interrupts on PSoC 5LP.
1. Introduction & Objectives
A mutex (Lab 09) protects a resource for the duration of a transaction, and it can block. But sometimes all you need to protect is a few instructions on a shared variable — incrementing a 64-bit counter, updating two fields that must stay consistent, reading a value an ISR also writes. For that, blocking is overkill; you need the shortest possible atomic window. That is a critical section. This lab teaches the taskENTER_CRITICAL family, its _FROM_ISR variants, and the sibling tool vTaskSuspendAll, and places them in the mutual-exclusion toolbox alongside the mutex so you can pick the right one by the length of what you are protecting.
After this lab you can:
- Make a short multi-field access atomic with taskENTER_CRITICAL/taskEXIT_CRITICAL.
- Protect a short region inside an ISR with taskENTER_CRITICAL_FROM_ISR/taskEXIT_CRITICAL_FROM_ISR.
- Use vTaskSuspendAll/xTaskResumeAll for a longer, task-only section that must not block interrupts.
- Choose between a critical section, suspend-all, and a mutex by length and context.
- Explain the cost — a critical section adds its hold time to interrupt latency — and keep it tiny.
2. Prerequisites
Hardware: CY8CKIT-059 PSoC 5LP Prototyping Kit; RobotSoC Kit a hardware Timer (for the ISR that races the task); an LED to flag a detected tear.
Software: PSoC Creator 4.4 with the FreeRTOS V11 BaseProject.
Chapters (sync progress): Required — Chapter 5 (Mutexes — the mutual-exclusion family) and Chapter 15 (Interrupts on PSoC 5LP — the NVIC masking / syscall line); Related — Lab 09 (mutex, priority inversion), Lab 12 (the NVIC 2–7 safety line and masking), Lab 03 (task states); Peripheral prerequisite — the Timer/interrupt is covered in the separate (non-FreeRTOS) Lab 02.
3. Reading Materials / References
- Chapter 15 slides — Interrupts on PSoC 5LP (configMAX_SYSCALL_INTERRUPT_PRIORITY, masking 2–7 not 0–1).
- Chapter 5 slides — Mutexes (the mutual-exclusion family; critical section vs mutex).
- FreeRTOS API: taskENTER_CRITICAL, taskEXIT_CRITICAL, taskENTER_CRITICAL_FROM_ISR, taskEXIT_CRITICAL_FROM_ISR, vTaskSuspendAll, xTaskResumeAll.
4. Background & Theory of Operation
What a critical section does. taskENTER_CRITICAL() does two things at once: it masks interrupts up to configMAX_SYSCALL_INTERRUPT_PRIORITY (so NVIC priorities 2–7 are held off — but 0–1 still fire, exactly the safety line from Lab 12) and it prevents context switches. Between taskENTER_CRITICAL() and taskEXIT_CRITICAL() nothing else touches the CPU: no other task, no kernel-level interrupt. That makes the enclosed instructions atomic with respect to every task and every maskable ISR. It is the cheapest and shortest mutual-exclusion tool there is — a few instructions, no blocking, no object to create.
Why you need it: torn access. A value made of more than one field (or wider than one machine word) cannot be updated in a single instruction. If a task writes count and is preempted by an ISR before it writes the matching sum, the ISR sees count new but sum old — a torn read of an invariant that should always hold. Wrapping both writes in a critical section means the ISR can only ever observe the pair before or after, never half-done.
The _FROM_ISR forms. Inside an ISR you must not call the plain taskENTER_CRITICAL — you call taskENTER_CRITICAL_FROM_ISR(), which returns the current interrupt mask, and later taskEXIT_CRITICAL_FROM_ISR(saved), which restores it. This protects a short region of an ISR from a higher-priority (but still ≤ syscall) ISR that could nest. Same idea, interrupt-safe form.
It nests. Critical sections keep a nesting count: two ENTERs need two EXITs before interrupts are re-enabled. So you can call a function that takes a critical section from within one you already hold — but every enter must be balanced.
The sibling tool: vTaskSuspendAll. Sometimes a task-only section is too long for a critical section (which would starve interrupts) but must still be protected from other tasks. vTaskSuspendAll() suspends the scheduler — no context switches — while leaving interrupts fully enabled, so ISRs keep running. xTaskResumeAll() ends it. Use it when you are touching data shared only between tasks (not an ISR), for longer than a handful of instructions. You cannot call a blocking API inside it either.
The toolbox — pick by length. The mutual-exclusion tools form a spectrum:
| Tool | What it blocks | Use for |
|---|---|---|
| taskENTER/EXIT_CRITICAL | IRQ 2–7 + the scheduler | a few instructions on data shared with an ISR |
| ..._FROM_ISR | IRQ 2–7 (inside an ISR) | a short region inside an ISR |
| vTaskSuspendAll/xTaskResumeAll | the scheduler only (IRQs still run) | a longer, task-only section |
| mutex (Lab 09/17) | other takers (can block/yield, priority inheritance) | resource ownership, longer or blocking sections |
Shorter and cheaper going up; longer and more flexible going down.
The cost — keep it tiny. The hold time of a critical section is added straight onto interrupt latency: any 2–7 interrupt that asserts while it is held cannot be serviced until EXIT. For a few instructions that is negligible; around a loop or an I/O wait it is a real-time disaster. Never call a blocking API (a delay, a take with timeout, a long CyDelay) inside a critical section — the kernel is frozen and cannot switch away. If you need to wait, you need a mutex, not a critical section.
5. RTOS Task Architecture
A task and an ISR both update the same multi-field shared data. The task guards its paired write with taskENTER_CRITICAL/taskEXIT_CRITICAL; the ISR guards its write with the _FROM_ISR forms. The panels give the mutual-exclusion toolbox (critical section vs suspend-all vs mutex) and the critical-section rules — keep it tiny, never block inside, balance the nesting, use the _FROM_ISR forms in an ISR.
6. The Mechanism Over Time
Why a critical section — a half-done update is a corrupt one:
Unprotected, an ISR landing between the two field writes observes an inconsistent pair; a critical section holds the ISR off until both writes complete, so the pair is only ever seen whole.
The cost — a critical section delays every 2–7 interrupt while it is held:
A short section adds negligible latency; a long one adds its entire hold time to the pending interrupt — so guard only a few instructions, never a wait.
Together: a critical section buys atomicity with latency. Spend as little of it as possible — a few instructions — and reach for vTaskSuspendAll or a mutex when the section is longer or must wait.
7. Schematic Circuit & Pin Configuration
- Timer → a Timer/Counter with a terminal-count Interrupt (isr_Timer), at NVIC priority 2–7.
- LED → GPIO + 220 Ω → GND; lit by the checker when it detects a torn pair.
- Assign the pins in <project>.cydwr → Pins.
8. PSoC Creator Configuration
- A Timer/Counter + Interrupt component; a Digital Output Pin for the fault LED.
- Set the timer interrupt NVIC priority to 3 (within 2–7), or isr_Timer_SetPriority(3) at startup (the template does this).
- FreeRTOSConfig.h (already set): configMAX_SYSCALL_INTERRUPT_PRIORITY = (2<<5), configUSE_TIME_SLICING = 1 (course default), heap_4, configASSERT defined.
9. Provided Template
You are given Lab24_Template_main.c:
- Given: all includes (note #include "FreeRTOS_PSoC.h"), the shared count/sum pair with the invariant sum == count × STEP, a consistent() checker, a checker task, a vTaskSuspendAll reference section, and a complete main() that starts the timer ISR (at a safe priority) and the tasks.
- You write: the taskENTER_CRITICAL/taskEXIT_CRITICAL guard in the task, and the _FROM_ISR guard in the ISR; then the Exp02 comparisons.
- Do not change the boot sequence in main().
/* task: make the paired write atomic */
taskENTER_CRITICAL();
g_count += 1;
g_sum += STEP; /* both, or neither */
taskEXIT_CRITICAL();
/* ISR: the interrupt-safe form */
UBaseType_t saved = taskENTER_CRITICAL_FROM_ISR();
g_count += 1; g_sum += STEP;
taskEXIT_CRITICAL_FROM_ISR(saved);
10. Lab Experiments
Exp01 — Basic: make the pair atomic. Run first without the guards and let the checker watch the invariant sum == count × STEP. Then add taskENTER_CRITICAL/taskEXIT_CRITICAL to the task and the _FROM_ISR forms to the ISR. Expected: the checker lights the fault LED when unprotected (the ISR tears the pair) and never lights it once both sides use critical sections.
Exp02 — Advanced: the right tool for the length. For the same data, try the alternatives and reason about them: a mutex (works, but heavier and it can block — overkill for two writes), and vTaskSuspendAll (protects against other tasks but not the ISR — so it is wrong here, and you should be able to say why). Expected: a clear statement of when each tool is correct; the critical section is right for a short task+ISR shared access, the mutex for resource ownership, suspend-all for a longer task-only section.
Exp03 — Challenge: measure the latency cost. Pulse a pin inside the critical section and time how long a pending timer interrupt is delayed when the section is short vs when you deliberately make it long (add a dummy loop). Show on an analyzer that the added interrupt latency equals the hold time. Restore it to a few instructions. Expected: measured evidence that a long critical section directly inflates interrupt latency — the concrete reason to keep it tiny and never block inside.
11. Testing & Verification
| # | Pass condition |
|---|---|
| Exp01 | Unprotected tears the invariant (LED on); critical sections on both sides keep it consistent. |
| Exp02 | Correct tool identified for the case; why a mutex is overkill and suspend-all is wrong here. |
| Exp03 | Measured interrupt latency grows with the critical-section hold time; kept tiny in the final code. |
Submit: your guarded task and ISR, your Exp02 tool comparison, and your Exp03 latency measurement.
12. Q&A & Troubleshooting
- The invariant still tears with guards on → one side is unprotected, or the ISR used the plain taskENTER_CRITICAL instead of the _FROM_ISR form.
- System hangs inside a critical section → you called a blocking API (delay / take with timeout) while interrupts and the scheduler are masked; move the wait outside, or use a mutex.
- Interrupts feel sluggish / jittery → a critical section is too long; shrink it to the minimal shared access.
- configASSERT fires on EXIT → unbalanced nesting (more exits than enters), or the _FROM_ISR saved value was not restored.
- vTaskSuspendAll did not protect the data → the data is shared with an ISR; suspend-all stops only the scheduler, not interrupts — use a critical section.
- A single int read still looked wrong → even word-wide access can race if it is read-modify-write; guard the whole RMW, not just the write.
13. Analysis Questions & Lab Report
- What two things does taskENTER_CRITICAL do, and which NVIC priorities does it mask (and not mask)?
- Why does a multi-field update need a critical section, and what is a "torn read"?
- How does vTaskSuspendAll differ from a critical section, and when is each the right choice?
- What is the cost of a critical section, and why must you never call a blocking API inside one?
Your report should include your guarded task and ISR, your tool comparison, your latency measurement, and answers to the four questions.