PSoC5LP RTOS
Lab 08 — Binary & Counting Semaphore
s

FreeRTOS on PSoC 5LP · Lab Series. Platform: CY8CKIT-059 (PSoC 5LP, Cortex-M3) · PSoC Creator 4.4 · FreeRTOS Kernel V11. Events: Rotary Encoder (bursts) + Button (single). Pairs with Chapter 6 — Binary Semaphores & Chapter 7 — Counting Semaphores.


1. Introduction & Objectives

A binary semaphore remembers that at least one event happened. A counting semaphore remembers how many. This lab makes the difference physical: a rotary encoder turned fast fires a burst of pulses. A binary semaphore collapses that burst to one — losing counts; a counting semaphore keeps the whole tally. A button, firing one event at a time, shows where a binary semaphore is exactly enough.

After this lab you can:

  • Create and use a binary semaphore (xSemaphoreCreateBinary) for a single event.
  • Create and use a counting semaphore (xSemaphoreCreateCounting) to count events without loss.
  • Give a semaphore from an ISR (xSemaphoreGiveFromISR) and take it in a task.
  • Drain a counting semaphore (take with timeout 0) so a burst is fully counted.
  • Explain when a burst is lost by a binary semaphore, and use a counting semaphore as a resource pool.

2. Prerequisites

Hardware: CY8CKIT-059 PSoC 5LP Prototyping Kit; RobotSoC Kit Rotary Encoder module, 1× push button, 1× LED + 220 Ω.

Software: PSoC Creator 4.4 with the FreeRTOS V11 BaseProject.

Chapters (sync progress): Required — Chapter 6 (Binary Semaphores: the *_FromISR rules, ISR priority 2–7) and Chapter 7 (Counting Semaphores); Peripheral prerequisite — the rotary encoder is covered in the separate (non-FreeRTOS) Lab 16 (Rotary Encoder).

3. Reading Materials / References

  • Chapter 6 slides — Binary Semaphores (ISR gives, task takes; portYIELD_FROM_ISR; ISR priority 2–7).
  • Chapter 7 slides — Counting Semaphores (event tally vs resource pool; drain a burst).
  • FreeRTOS API: xSemaphoreCreateBinary, xSemaphoreCreateCounting, xSemaphoreTake, xSemaphoreGiveFromISR, uxSemaphoreGetCount.

4. Background & Theory of Operation

A binary semaphore is a one-bit flag. It is empty or full. An ISR "gives" it (fills it) and a task "takes" it (empties it, blocking until it is full). It is perfect for a single "it happened" signal — a button press, a data-ready interrupt. But it saturates at one: if a second event arrives before the task takes the first, the extra give is simply dropped. A burst collapses to one.

A counting semaphore keeps a tally. Created with xSemaphoreCreateCounting(max, initial), it holds a count from 0 to max. Each give increments the count; each take decrements it. So a fast burst of N events (if N ≤ max) leaves the count at N, and the task can process all N. The two classic uses: count events — create with initial 0; the ISR gives once per event; the task takes once per event. Resource pool — create with initial N (all free); take to claim a permit, give to return it; models N identical resources.

Draining a burst. The task blocks on the first event with xSemaphoreTake(sem, portMAX_DELAY), then drains the rest with xSemaphoreTake(sem, 0) in a loop until it returns pdFALSE. Take once per loop and the count outruns you; drain and nothing is lost. uxSemaphoreGetCount reads how many are pending.

Why the encoder shows it best. A rotary encoder turned quickly fires pulses faster than a task can wake per pulse. With a binary semaphore the task sees "moved" but under-counts the steps. With a counting semaphore, every pulse increments the count and the drain loop recovers all of them — the step count is exact.

The ISR rules still apply (Chapter 6). Use xSemaphoreGiveFromISR (never the plain give) inside an ISR, clear the source first, end with portYIELD_FROM_ISR, and keep the ISR at priority 2–7.

Binary semaphore vs task notification. Lab 06 used a notification for the same ISR→task signal. A binary semaphore is the heavier equivalent — but it is a real object, so several tasks can wait on it, which a notification cannot do. Choose a notification for one waiter, a semaphore when more than one task waits or you need a counting tally.

5. RTOS Task Architecture

COUNTING vs BINARY — ISR GIVES, TASK TAKES Rotary Encoder pulses in bursts Encoder ISR (2–7) clear source xSemaphoreGiveFromISR(enc) Counting Sem count up to max one give = count++ Encoder Task block for first, then DRAIN: while (Take(enc, 0)) steps++; every pulse counted — no loss Button one event at a time Button ISR (2–7) clear source xSemaphoreGiveFromISR(btn) Binary Sem empty / full (0 or 1) saturates at 1 Button Task Take(btn, portMAX_DELAY); react once per event single event — binary is enough The lesson: turn the encoder fast and a BINARY semaphore saturates at 1 — extra gives before a take are lost, so a burst collapses to a single step. A COUNTING semaphore increments per give, so the task drains every pulse with no loss. Counting also = a resource pool: create with init N permits — Take to claim one, Give to return it (Exp03).

Two event sources. The Rotary Encoder fires bursts into a counting semaphore; the Encoder Task blocks for the first pulse then drains the rest, so every pulse is counted. The Button fires single events into a binary semaphore; the Button Task reacts once per press. Turn the encoder fast and a binary semaphore would saturate at 1 and lose pulses — the counting semaphore does not.

6. Binary vs. Counting — Over a Burst

Binary: the burst collapses to one (events lost):

Binary semaphore - a burst collapses to ONE 5 encoder pulses arrive fast; the binary sem saturates at 1, so 4 gives are LOST; the task counts 1 0 100 200 300 400 500 600 time (ms) PULSE binary (0/1) give give give give give task runs & takes value = 1 (capped) × × × × these gives are dropped (already full) Task counted: 1 (4 pulses LOST) Conceptual / idealized. A binary semaphore remembers only that at least one event happened. Measure on hardware.

Five pulses arrive fast; the binary sem caps at 1, so four gives are dropped. The task counts 1.

Counting: every pulse is kept:

Counting semaphore - every pulse counted each give increments the count (up to max); the task drains all 5 with Take-timeout-0, losing none 0 100 200 300 400 500 600 time (ms) PULSE counting give give give give give task runs & takes value climbs to 5 Task counted: 5 (no loss) Conceptual / idealized. Drain with Take(sem, 0) in a loop until it returns pdFALSE. Measure on hardware.

Each give increments the count to 5; the task drains all five with take-timeout-0 and counts 5.

The only difference is the semaphore type. The same ISR, the same burst — one loses four events, the other loses none.

7. Schematic Circuit & Pin Configuration

  • Encoder A/B → interrupt-capable pin(s); an Interrupt component on the pulse edge (isr_Enc).
  • BTN → push button to GND on an interrupt pin (isr_Btn), internal pull-up (active-low).
  • LED → GPIO + 220 Ω → GND.
  • Assign the pins in <project>.cydwr → Pins.

8. PSoC Creator Configuration

  • Encoder input + Interrupt component; button input + Interrupt component; an LED output.
  • Set both interrupt priorities to 3 (within 2–7), or call isr_*_SetPriority(3) at startup (the template does this).
  • FreeRTOSConfig.h (already set): configUSE_COUNTING_SEMAPHORES = 1, configMAX_SYSCALL_INTERRUPT_PRIORITY = (2<<5), heap_4.

9. Provided Template

You are given Lab08_Template_main.c:

  • Given: all includes (note #include "FreeRTOS_PSoC.h"), both semaphore handles, both task handles, and a complete main() that creates the counting and binary semaphores, sets both ISR priorities into 2–7, starts the ISRs, and creates both tasks. Placeholders keep the project building and running.
  • You write: Enc_ISR() / Btn_ISR() (clear → give → yield) and the two task bodies — the encoder task must drain with take-timeout-0.
  • Do not change the boot sequence in main().
static void vEncoderTask(void *arg)
{
    for (;;) {
        xSemaphoreTake(xEncSem, portMAX_DELAY);          /* wait for first   */
        do { g_steps++; }                                /* count it         */
        while (xSemaphoreTake(xEncSem, 0) == pdTRUE);    /* drain the rest    */
        /* show g_steps */
    }
}

10. Lab Experiments

Exp01 — Basic: count every pulse, react to every press. Write both ISRs and tasks. Turn the encoder: the counting semaphore + drain loop count every step. Press the button: the binary semaphore triggers one reaction. Expected: the step count matches the physical detents; each press gives exactly one reaction.

Exp02 — Advanced: make the binary semaphore lose the burst. Temporarily route the encoder pulses into a binary semaphore. Turn the encoder fast. Expected: the binary version under-counts — a fast turn registers far fewer steps than detents, because the burst saturated at 1. Compare side by side with the counting version and quantify the loss.

Exp03 — Challenge: counting semaphore as a resource pool. Create a counting semaphore with initial N (e.g. 3 permits). Have several tasks Take a permit before using a shared "resource" and Give it back after. Expected: at most N tasks hold the resource at once; a task blocks when all permits are out, and proceeds when one is returned. Read uxSemaphoreGetCount to show how many are free.

11. Testing & Verification

#Pass condition
Exp01 Encoder step count is exact (drained); each button press = one reaction.
Exp02 Binary version under-counts a fast burst; you can quantify the loss vs counting.
Exp03 At most N tasks hold the pooled resource; blocking/release works; free count shown.

Submit: your ISRs and tasks, the Exp02 counting-vs-binary comparison (numbers), and the Exp03 pool behaviour.

12. Q&A & Troubleshooting

  • Encoder under-counts even with the counting sem → you took once per loop instead of draining; add the take-timeout-0 loop.
  • Count saturates / stops rising → the counting sem hit max; raise ENC_MAX_COUNT or drain faster.
  • ISR re-fires forever → the interrupt source was not cleared first.
  • HardFault / assert in an ISR → ISR priority is 0 or 1; set it to 2–7, and use only *_FromISR calls.
  • Button gives several reactions → contact bounce; add a short debounce guard.
  • Pool never blocks → you gave back more permits than you took; balance Take/Give.

13. Analysis Questions & Lab Report

  1. Why does a binary semaphore lose events in a burst, while a counting semaphore does not?
  2. What does "draining" mean, and why is take-with-timeout-0 the right tool for it?
  3. Give the two classic uses of a counting semaphore and how the initial count differs between them.
  4. When would you choose a binary semaphore over a task notification (Lab 06)?

Your report should include your ISRs and tasks, the Exp02 numbers, and answers to the four questions.

© 2026 Air Supply Information Center (Air Supply BBS)