PSoC5LP RTOS
Lab 06 — Direct-to-Task Notification
s

FreeRTOS on PSoC 5LP · Lab Series. Platform: CY8CKIT-059 (PSoC 5LP, Cortex-M3) · PSoC Creator 4.4 · FreeRTOS Kernel V11. Event: Button on an interrupt pin (PIR as alternative) → LED. Pairs with Chapter 9 — Task Notifications (ISR rules from Chapter 6).


1. Introduction & Objectives

In Lab 03 a task polled a button — it kept checking even when nothing happened, wasting CPU. This lab pays that off: a button interrupt notifies a handler task that is blocked waiting, so the CPU does nothing until the event actually occurs. The notification does the job of a binary semaphore, but with no separate object — the lightest one-to-one signal FreeRTOS offers.

After this lab you can:

  • Signal a specific task directly with vTaskNotifyGiveFromISR / ulTaskNotifyTake.
  • Block a task on an event so it uses no CPU until the event — the opposite of polling.
  • Write a short, safe ISR: clear the source, notify, portYIELD_FROM_ISR.
  • Set an ISR priority in the safe 2–7 range so it may call the FreeRTOS API.
  • Explain when a notification fits (one waiter) and when it does not (many waiters / data).

2. Prerequisites

Hardware: CY8CKIT-059 PSoC 5LP Prototyping Kit; RobotSoC Kit 1× push button on an interrupt-capable pin, 1× LED + 220 Ω (HC-SR501 PIR as an alternative event source).

Software: PSoC Creator 4.4 with the FreeRTOS V11 BaseProject.

Chapters (sync progress): Required — Chapter 9 (Task Notifications), builds on Chapter 6 (first ISR + binary semaphore, where the *_FromISR and ISR-priority rules are introduced); Peripheral prerequisite — button GPIO / interrupt is covered in the separate (non-FreeRTOS) Lab 02.

3. Reading Materials / References

  • Chapter 9 slides — Task Notifications (give/take, no object, when it fits).
  • Chapter 6 slides — Binary Semaphores (the *_FromISR rules, ISR priority 2–7, portYIELD_FROM_ISR).
  • FreeRTOS API: vTaskNotifyGiveFromISR, ulTaskNotifyTake, xTaskNotify.

4. Background & Theory of Operation

Polling wastes the CPU. A task that loops checking a pin runs forever, even when nothing changes, and still only notices an event at the next check (latency up to the poll period). The event-driven alternative lets the hardware tell us when something happens.

A notification is a built-in signal. Every task already owns a notification value in its control block. An ISR (or another task) can "give" it, and the task "takes" it. Because the signal is built into the task, there is no object to create — less RAM and one fewer handle than a binary semaphore, and a faster path straight to the task.

Give and take. In the ISR: vTaskNotifyGiveFromISR(hTask, &woken) bumps the task's notification (acts like a binary give) and sets woken if a higher-priority task became Ready. In the task: ulTaskNotifyTake(pdTRUE, portMAX_DELAY) blocks until notified; pdTRUE clears the value to 0 on exit, so it behaves one-shot (like a binary semaphore). While blocked, the task uses no CPU.

The ISR must be tiny and safe. The deferred-interrupt pattern: do the minimum in the ISR and hand the real work to the task.

  1. Clear the source first, or the interrupt re-fires forever.
  2. Use only *_FromISR calls inside an ISR — never the plain ulTaskNotifyTake/semaphore calls.
  3. End with portYIELD_FROM_ISR(woken) so the woken task runs immediately, not at the next tick.
  4. ISR priority must be 2–7 (configMAX_SYSCALL_INTERRUPT_PRIORITY = level 2). A priority 0 or 1 ISR is above the syscall line and must not call any FreeRTOS API.

Notification vs binary semaphore. They do the same ISR→task signal here. Prefer a notification when exactly one task waits on the event — it is faster and lighter. Use a semaphore when several tasks wait on the same event, a counting semaphore when many events must be tallied, and a queue when you must carry data. A notification also carries one 32-bit value if you need it (xTaskNotify).

5. RTOS Task Architecture

EVENT-DRIVEN: ISR GIVES, TASK TAKES — NO POLLING Button on an interrupt pin (PIR as alternative) Button ISR (priority 2–7) 1. Clear the interrupt source 2. vTaskNotifyGiveFromISR(hHandler, &w) 3. portYIELD_FROM_ISR(w) tiny — the reaction is NOT here Handler Task for (;;) { ulTaskNotifyTake(pdTRUE, portMAX_DELAY); react: LED on 2 s, etc. } LED the visible reaction IRQ notify wakes the blocked task Why it beats polling: the task blocks on ulTaskNotifyTake and uses NO CPU until the event; the ISR wakes it. vs a binary semaphore: same job, but a notification needs no separate object — lighter & faster, for ONE waiting task.

The button interrupt fires the tiny Button ISR (priority 2–7), which clears the source, calls vTaskNotifyGiveFromISR, and yields. That notification wakes the Handler Task, blocked on ulTaskNotifyTake, which does the real reaction (light the LED). No polling, no separate semaphore object.

6. Polling vs. Interrupt + Notification — Over Time

Polling: the CPU keeps waking to check, even with no event:

Polling - the task checks the button on a timer a poll runs every ~20 ms whether or not anything happened; the CPU keeps waking to check 0 200 400 600 800 1000 1200 time (ms) PRESS CPU CPU never truly rests - checks forever Conceptual / idealized. Even with no press, the CPU wakes every poll period; response is up to one poll period late.

A poll runs every period whether or not anything happened; response is up to one poll period late.

Interrupt + notification: the task sleeps until the event:

Interrupt + notification - the task sleeps until the event the handler task blocks on ulTaskNotifyTake (no CPU); a press fires the ISR, which notifies and wakes it 0 200 400 600 800 1000 1200 time (ms) PRESS CPU CPU idle (task blocked) ISR task reacts ISR task reacts CPU rests between events; wakes within ~1 tick Conceptual / idealized. The ISR is tiny (clear + notify + yield); the real work runs in the woken task. No CPU used while idle.

The handler task blocks (no CPU); a press fires the tiny ISR, which notifies and wakes it within ~1 tick.

The contrast is the whole point: polling spends CPU to find nothing; the notification spends CPU only when there is something to do.

7. Schematic Circuit & Pin Configuration

  • BTN → push button to GND on an interrupt-capable pin, internal pull-up (active-low). Add an Interrupt component on its falling edge.
  • LED → GPIO + 220 Ω → GND.
  • (Alternative) PIR (HC-SR501) output → an interrupt pin instead of the button.
  • Assign the pins in <project>.cydwr → Pins.

8. PSoC Creator Configuration

  • A Digital Input Pin + Interrupt component (isr_Button) on the button edge; a Digital Output Pin for the LED.
  • Set the interrupt component's priority to 3 (within 2–7), or call isr_Button_SetPriority(3) at startup (the template does this).
  • FreeRTOSConfig.h (already set): configUSE_TASK_NOTIFICATIONS = 1, configMAX_SYSCALL_INTERRUPT_PRIORITY = (2<<5), heap_4.

9. Provided Template

You are given Lab06_Template_main.c:

  • Given: all includes (note #include "FreeRTOS_PSoC.h"), the handler handle, and a complete main() that creates the handler task, sets the ISR priority into the safe 2–7 range, and starts the interrupt. The handler has a placeholder so the project builds and runs.
  • You write: Button_ISR() (clear → notify → yield) and vHandlerTask() (block on ulTaskNotifyTake, then react), per the TODO(Exp01) comments.
  • Do not change the boot sequence in main().
CY_ISR(Button_ISR)
{
    BaseType_t woken = pdFALSE;
    Button_ClearInterrupt();                       /* 1: clear the source */
    vTaskNotifyGiveFromISR(hHandler, &woken);      /* 2: notify the task  */
    portYIELD_FROM_ISR(woken);                     /* 3: switch now       */
}

static void vHandlerTask(void *arg)
{
    for (;;) {
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);   /* block - no CPU used */
        LED_Write(1); vTaskDelay(pdMS_TO_TICKS(2000)); LED_Write(0);
    }
}

10. Lab Experiments

Exp01 — Basic: motion/press by interrupt. Write the ISR and handler. A button press fires the ISR, which notifies the handler; the handler lights the LED for 2 s. Expected: each press triggers exactly one reaction; the ISR is short and the reaction lives in the task.

Exp02 — Advanced: prove the CPU rests (notification vs polling). Build a polling version (a task that checks the button every ~20 ms) and the notification version. Show the difference: e.g. enable configUSE_IDLE_HOOK to pulse an "idle" LED, or print run-time stats. Expected: with the notification the idle time is high and the handler is Blocked between presses; with polling the CPU keeps waking. Confirm a debounced press gives one reaction, with no lost or duplicated events.

Exp03 — Challenge: carry a value, and know the limit. Use xTaskNotify to send a 32-bit value (e.g. which of two buttons fired) and have the handler read it with xTaskNotifyWait. Then argue the limit: a notification targets one task with one value — show (or explain) why a second waiting task or many independent senders would need a semaphore or a queue instead. Expected: a working value-carrying notification and a one-paragraph statement of when a notification is the wrong tool.

11. Testing & Verification

#Pass condition
Exp01 A press fires the ISR → notify → handler reacts (LED 2 s); ISR is tiny; priority in 2–7.
Exp02 Notification version: handler Blocked between events, CPU mostly idle; one press = one reaction.
Exp03 xTaskNotify carries a value the handler reads; you can state when a notification is the wrong choice.

Submit: your ISR and handler, evidence the CPU rests (idle LED or run-time stats), and the Exp03 value-notification + explanation.

12. Q&A & Troubleshooting

  • The ISR fires continuously / system hangs → you did not clear the interrupt source first.
  • HardFault or configASSERT in the ISR → the ISR priority is 0 or 1 (above the syscall line); set it to 2–7.
  • Used ulTaskNotifyTake inside the ISR → wrong: the ISR must use vTaskNotifyGiveFromISR.
  • The reaction runs late (next tick) → you forgot portYIELD_FROM_ISR(woken).
  • One press gives several reactions → contact bounce; the 2 s reaction usually masks it, else add a short guard.
  • Nothing happens → the Interrupt component is not started (isr_Button_StartEx), or the edge/pin is wrong.

13. Analysis Questions & Lab Report

  1. Why does a blocked task waiting on a notification use no CPU, while a polling task does?
  2. List the four things a correct RTOS-aware ISR does, in order, and why each matters.
  3. Why must the ISR be priority 2–7, and what breaks at priority 0 or 1?
  4. When is a task notification the right tool, and when must you reach for a semaphore or a queue instead?

Your report should include your ISR and handler, your CPU-rest evidence, and answers to the four questions.

© 2026 Air Supply Information Center (Air Supply BBS)