PSoC5LP RTOS
Lab 17 — I2C / SPI Resource Management
FreeRTOS on PSoC 5LP · Lab Series Platform: CY8CKIT-059 (PSoC 5LP, Cortex-M3) · PSoC Creator 4.4 · FreeRTOS Kernel V11 Theme: several tasks share one bus; a mutex makes each transaction atomic Pairs with: Chapter 5 — Mutexes & Priority Inversion & Chapter 17 — I2C & SPI
1. Introduction & Objectives
A real board has one I2C bus and one SPI bus, but many tasks that want to use them. A bus transaction is a multi-step sequence — for I2C, START, address, register, restart, read, STOP; for SPI, CS low, shift bytes, CS high. If the scheduler switches tasks in the middle of one, a second task's transaction is spliced into the first and both corrupt: wrong register, lost bytes, a frame addressed to the wrong chip. The fix is the mutex you met in Lab 09, applied to a shared peripheral: take the mutex, do the whole transaction, give it back.
After this lab you can:
- Explain why a bus transaction must be atomic, and what interleaving corrupts.
- Guard a shared I2C/SPI bus with a mutex: xSemaphoreTake before the first bus action, xSemaphoreGive after the last.
- Rely on priority inheritance so a high-priority task is not held off for long by a low-priority one.
- Keep the held window tight — the transaction only, not the surrounding compute.
- Compare the mutex approach with a bus-server task.
- (Extension) Use a recursive mutex when a locked helper may be called from code that already holds the lock.
2. Prerequisites
Hardware - CY8CKIT-059 PSoC 5LP Prototyping Kit - RobotSoC Kit: an I2C device (temperature sensor and/or EEPROM) and an SPI device (display or similar); pull-ups on SDA/SCL
Software - PSoC Creator 4.4 with the FreeRTOS V11 BaseProject
Chapters (sync progress) - Required: Chapter 5 (Mutexes & Priority Inversion) and Chapter 17 (I2C & SPI). - Related: Lab 09 (the mutex and priority inheritance, RTOS side). - Peripheral prerequisite: I2C is covered in the separate (non-FreeRTOS) Lab 09 (I2C); SPI in non-FreeRTOS Lab 14/18 (SPI).
3. Reading Materials / References
- Chapter 5 slides — Mutexes & Priority Inversion (take/give, priority inheritance, mutex vs binary semaphore).
- Chapter 17 slides — I2C & SPI (transaction structure, addressing, CS framing).
- Lab 09 / Lab 14/18 (non-RTOS) — configuring the I2C and SPI components and their basic transactions.
- FreeRTOS API: xSemaphoreCreateMutex, xSemaphoreTake, xSemaphoreGive.
- (Extension) xSemaphoreCreateRecursiveMutex, xSemaphoreTakeRecursive, xSemaphoreGiveRecursive.
4. Background & Theory of Operation
A transaction is an indivisible unit. On a shared bus, correctness depends on one task completing its entire exchange before another starts. I2C spans START → addr+W → register → RESTART → addr+R → data → STOP; SPI spans CS-low → bytes → CS-high. These are not independent steps — they address a specific device and move a specific payload. Interleave two of them and the bus sees a nonsense mixture: the second device answers the first's clock, bytes land in the wrong register, a CS frames the wrong chip.
The mutex makes it atomic. Create one mutex for the bus. Every task that uses the bus does the same thing: xSemaphoreTake(busMutex, portMAX_DELAY) before its first bus action, the full transaction, then xSemaphoreGive(busMutex) after the last. While one task holds the mutex, any other that tries to take it blocks until the first gives it back — so transactions run strictly one at a time, never interleaved.
Why a mutex, not a binary semaphore? A mutex has an owner and gives priority inheritance: if a low-priority task holds the bus and a high-priority task blocks waiting for it, the holder is temporarily raised to the high priority so it finishes and releases the bus quickly. That bounds the time the high-priority task waits — the priority-inversion fix from Lab 09, now protecting a real peripheral. A binary semaphore has no owner and no inheritance, so it is the wrong tool here.
Keep the held window tight. Hold the mutex for the transaction only. Do the computation that prepares the data, and the handling of the result, outside the held window. A mutex held across think-time needlessly blocks every other task from the bus. Take late, give early.
Two ways to serialize a bus. The mutex is the simple, direct way — every task cooperates by taking the same lock. The alternative is a bus-server task: a single task owns the bus and others send it requests through a queue. That gives full serialization with no shared lock, at the cost of more latency and plumbing. For most designs the mutex is enough; reach for the server when many tasks contend or you want all bus access in one place.
Extension — the recursive mutex for re-entrant code. A plain mutex deadlocks itself if the same task tries to take it twice: imagine a high-level helper read_sensor() takes the bus mutex, then calls a lower-level read_reg() that also takes it — the second take blocks forever waiting for a lock the task already holds. A recursive mutex (xSemaphoreCreateRecursiveMutex, xSemaphoreTakeRecursive, xSemaphoreGiveRecursive) solves this: it lets the owning task take it again, counting the nesting, and only truly releases when the matching number of gives have run. Use it when a locked public function may be called from within another function that already holds the lock; prefer a plain mutex otherwise, because recursion can hide a lock you forgot to release. It keeps priority inheritance, like a normal mutex.
5. RTOS Task Architecture
The Temp, Log, and Display tasks all reach the bus only by taking the Bus Mutex first; the peripheral is driven by whichever task currently owns it. The mutex gives priority inheritance, so the high-priority Temp task is never held off for long by the slow Log write. The panels show the atomic-transaction rule and contrast the mutex with a bus-server task.
6. Protection Over Time
Why the mutex — two tasks, one bus:
Unprotected, a preemption splices the Log transaction into the middle of the Temp read and both corrupt. With the mutex, each transaction owns the bus end-to-end.
The mutex-held window spans the whole transaction:
Take before START, give after STOP; the compute and the result-handling stay outside the held window so the bus is free the moment the transaction ends.
The rule both diagrams teach: the unit of mutual exclusion is the transaction, not the byte. Lock around the whole exchange, and only the exchange.
7. Schematic Circuit & Pin Configuration
- I2C → SDA/SCL with pull-ups; the sensor and EEPROM share the two lines.
- SPI → SCLK/MOSI/MISO shared; a separate CS per SPI device.
- LED → optional, to flag a detected corruption in Exp02.
- Assign the pins in <project>.cydwr → Pins.
8. PSoC Creator Configuration
- An I2C Master component and (optionally) an SPI Master with a CS output pin.
- FreeRTOSConfig.h (already set): configUSE_MUTEXES = 1, configUSE_TIME_SLICING = 1 (course default), heap_4.
- Match the I2C speed and the device addresses to your parts; size any page-write buffers to the EEPROM page.
9. Provided Template
You are given Lab17_Template_main.c:
- Given: all includes (note #include "FreeRTOS_PSoC.h"), the bus mutex, an i2c_read_reg() transaction helper, three tasks (Temp high / Display mid / Log low), and a complete main() that starts the bus, creates the mutex, and makes the tasks.
- You write: the xSemaphoreTake/xSemaphoreGive pair around each transaction in all three tasks, plus the Exp02/03 variations.
- Do not change the boot sequence in main().
/* every task, around its whole transaction: */
xSemaphoreTake(busMutex, portMAX_DELAY);
t = i2c_read_reg(TEMP_ADDR, 0x00u); /* START..STOP as one unit */
xSemaphoreGive(busMutex);
/* ... use t here, OUTSIDE the held window ... */
10. Lab Experiments
Exp01 — Basic: protect the bus
Add the take/give around each task's transaction. Run all three tasks together. Expected: every read and write is correct regardless of timing; the bus is never interleaved.
Exp02 — Advanced: see the corruption
Temporarily remove the mutex from the Temp and Log tasks and make the Log write slow and frequent. Read back what the Temp task gets and watch for wrong/garbage values when a switch lands mid-transaction. Flag a mismatch on the LED. Then restore the mutex and confirm it is clean. Expected: observable corruption without the mutex, and a clean bus with it — the concrete reason the lock exists.
Exp03 — Challenge: priority inheritance, or a bus server
Make the Log task hold the bus for a long page write and have the high-priority Temp task try to read. Show (by timing a pin or logging) that the Temp task's wait is bounded because the Log task inherits its priority to finish fast. (Alternative: refactor to a bus-server task that owns the bus and serves requests from a queue, and compare latency.) Expected: evidence of priority inheritance bounding the high-priority wait, or a working bus-server with a latency comparison.
Exp04 — Extension: a re-entrant lock with a recursive mutex
Write a high-level helper that takes the bus lock and calls a lower-level helper that also takes it. With a plain mutex this self-deadlocks; switch the bus lock to a recursive mutex (xSemaphoreCreateRecursiveMutex + …TakeRecursive/…GiveRecursive) and show the nested take now works, releasing only after the matching gives. Expected: a demonstration of the self-deadlock, the recursive-mutex fix, and a statement of why you would still prefer a plain mutex when you do not need re-entrancy.
11. Testing & Verification
| # | Pass condition |
|---|---|
| Exp01 | All three tasks share the bus with correct transactions under all timing. |
| Exp02 | Removing the mutex produces observable corruption; restoring it fixes it. |
| Exp03 | Priority inheritance bounds the high-priority task's wait (or a bus-server serialises with measured latency). |
| Exp04 | A nested take self-deadlocks on a plain mutex and works on a recursive mutex; the trade-off is stated. |
Submit: your protected tasks, your Exp02 corruption evidence, your Exp03 inheritance timing or bus-server comparison, and (extension) your recursive-mutex demonstration.
12. Q&A & Troubleshooting
- Occasional garbage reads → a transaction is unprotected or the lock is per-byte; take once before START, give once after STOP.
- One task starves on the bus → the mutex is held across compute/delay; shrink the held window to the transaction.
- High-priority task still waits too long → you used a binary semaphore (no inheritance) instead of a mutex.
- Deadlock → a task takes the bus mutex and then blocks on another lock a bus holder needs; keep a single, consistent lock order (Lab 09).
- I2C hangs after an error → a transaction aborted without SendStop; always release the bus (and the mutex) even on error paths.
- SPI frames wrong device → CS not inside the mutex-held window; the CS framing is part of the transaction.
13. Analysis Questions & Lab Report
- Why must a bus transaction be atomic? Give a concrete corruption that interleaving causes.
- Where exactly do the take and give go, and why around the whole transaction rather than each byte?
- Why a mutex rather than a binary semaphore for a shared bus? What does priority inheritance buy you here?
- When would you choose a bus-server task over a mutex, and what does it cost?
- (Extension) Why does a plain mutex self-deadlock on a nested take, and when is a recursive mutex the right fix (and when not)?
Your report should include your protected tasks, your corruption evidence, your extension demonstration, and answers to the questions.