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:

ToolWhat it blocksUse 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

CRITICAL SECTIONS — THE SHORTEST WAY TO MAKE AN ACCESS ATOMIC Task taskENTER_CRITICAL(); shared.count++; shared.sum += v; // both, // or neither (atomic) taskEXIT_CRITICAL(); Shared data struct { count; sum; } must stay consistent across its fields ISR (NVIC 2–7) UBaseType_t s = taskENTER_CRITICAL_FROM_ISR(); shared.count++; shared.sum += w; taskEXIT_CRITICAL_FROM_ISR(s); the ISR uses the _FROM_ISR forms — never the plain ones The mutual-exclusion toolbox — pick by length Tool What it blocks Use for critical section IRQ 2–7 + scheduler a few instructions ..._FROM_ISR IRQ 2–7 (inside an ISR) short ISR region vTaskSuspendAll scheduler only (IRQs run) longer, task-only mutex other takers (can block/yield) resource ownership Shorter & cheaper going up; longer & more flexible going down. A critical section masks 2–7, never 0–1 (Lab 12’s safety line). Critical-section rules · Keep it tiny — it raises interrupt latency for every 2–7 IRQ while held. · Never call a blocking API inside (no delay, no take with timeout) — the kernel is frozen. · It nests: N enters need N exits. · In an ISR use only the _FROM_ISR forms, and restore the exact saved mask value. Rule of thumb: a few instructions on shared data → critical section; anything that must wait or block → a mutex, never a critical section.

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:

Why a critical section — a half-done update is a corrupt one The task updates two linked fields; an ISR that lands between them sees (and leaves) inconsistent data Unprotected — the ISR tears the update Task ISR Shared data count++ sum += v ISR preempts mid-update reads count & sum consistent count new, sum OLD (INCONSISTENT) consistent again ✗ The ISR acted on count+sum that never both held at once — a torn read. With a critical section — the pair updates atomically Task ISR Shared data ENTER_CRITICAL · count++ · sum+=v · EXIT IRQ 2–7 held off reads consistent pair EXIT → ISR runs now never observed half-updated consistent Conceptual / idealized

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:

The cost — a critical section delays every 2–7 interrupt while it is held Keep it to a few instructions: the hold time is added straight onto interrupt latency IRQ asserts ISR runs crit. section short IRQ ISR small latency long (bad) IRQ ISR latency = the whole hold time The IRQ is pending but cannot be serviced until EXIT — so a long critical section = a long blind spot. Guarding only a few instructions keeps the added latency negligible; guarding a loop or an I/O wait does not. Conceptual / idealized

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

  1. What two things does taskENTER_CRITICAL do, and which NVIC priorities does it mask (and not mask)?
  2. Why does a multi-field update need a critical section, and what is a "torn read"?
  3. How does vTaskSuspendAll differ from a critical section, and when is each the right choice?
  4. 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.

© 2026 Air Supply Information Center (Air Supply BBS)