Multi-task Firmware with FreeRTOS
Course 1 · Module 4 Suggested time: about 3.5–4 hours (reading + a multi-task lab on the board) Format: a hands-on lesson — creating and coordinating FreeRTOS tasks alongside the TESA Firmware SDK
Lab · Cheatsheet · ← Table of Contents · ← M03 · M05 →
Note: which firmware the code in this lesson is written for (checked on 2026-09-26)
The C code in this lesson calls the API of the TESAIoT Bitstream firmware, called “TESA Firmware SDK” in the original, which is published as a ready-made HEX file (
tesaiot-bitstream-<version>.hex) alongside Bitstream Studio in the TESAIoT_Hackathon lab pack. The source code of this firmware is not yet public. Functions such ascm55_initialize,cm55_start_scheduler,led_controller_toggle,cm55_button_on_pressedandcm55_i2c_manager_i2c_locktherefore have no header you can open or build yourself. Read the snippets as concepts and a calling order. The calls to FreeRTOS and the Infineon PDL (such asxTaskCreate,vTaskDelay,Cy_GPIO_*) are ordinary public APIs.If you want code you can read and build from open source, see tesaiot-pse84-devkit-sdk (Apache-2.0), which is a different codebase with different API names. Examples already checked to do the same job as this lesson (commit
ef72c1b):
proj_cm55/main.candproj_cm33_ns/main.c— create a task withxTaskCreate, then callvTaskStartScheduler()directly frommain(nocm55_start_schedulerwrapper)proj_cm33_ns/examples/sensors/05_auto_push_task.c— a FreeRTOS task that reads a sensor periodically under a mutex, with an adjustable rateproj_cm33_ns/examples/sensors/02_read_imu.c— locking the bus once per sample (sensor_i2c_lock/sensor_i2c_unlock)
Objectives (Learning Outcomes)
Section titled “Objectives (Learning Outcomes)”By the end of this lesson you should be able to:
- Explain the role of an RTOS and splitting work into Tasks
- Design a Task with a stack, a priority, and a basic understanding of the scheduler
- Use Queues, Mutexes, Binary Semaphores, Event Groups to communicate and prevent race conditions
- Consider timing and the restrictions inside an ISR / tick hook
- Build a multi-task system working with the Driver API from M03
- Do a multi-task hands-on exercise on the real board
About the snippets in this lesson The C examples are drawn from the TESA Firmware SDK (FreeRTOS on the CM55) and presented purely as snippets. See more examples on the TESAIoT Developer Hub (Domain: System / Embedded / Real-Time)
Read alongside this chapter
Section titled “Read alongside this chapter”| Document | Use when |
|---|---|
| TESAIoT Developer Hub | The course’s multi-task / system examples |
| FreeRTOS — xTaskCreate | Creating a task |
| FreeRTOS — vTaskDelay | Delaying while freeing the CPU |
| FreeRTOS — xQueueCreate | A queue for passing data between tasks |
| FreeRTOS — Queues overview | The copy-by-value concept |
| FreeRTOS — Mutex / Semaphore | Locking a shared resource |
| FreeRTOS — Event Groups | Status flags between tasks |
| M03 — GPIO and Peripherals | led_controller_*, cm55_button_*, the I²C lock |
| TESAIoT_Hackathon | HEX / Flasher, when using the lab pack |
| Bitstream Studio | Watching telemetry once several tasks send data (extra) |
1. Why RTOS for Edge Products
Section titled “1. Why RTOS for Edge Products”In M03, the code usually sat in a single loop: read a button → UART → ADC. As the system grows (several sensors, connectivity, UI, Edge AI), a single loop causes:
- A slow task blocking work that must respond quickly
- Difficulty separating responsibilities and testing them
The RTOS (Real-Time Operating System) used on this course’s platform is FreeRTOS — creating several Tasks that take turns running by priority.
| Concept | Short meaning |
|---|---|
| Task | A function that runs independently, with its own stack |
| Scheduler | Chooses which task gets the CPU |
| Priority | A higher number = more important (in FreeRTOS) |
| Blocking | A task waiting on a queue/delay → gives the CPU to another task |
Key phrase Split work by responsibility and timing — not just by splitting C files.
2. How the Firmware Starts FreeRTOS
Section titled “2. How the Firmware Starts FreeRTOS”A standard app on the CM55 does not call vTaskStartScheduler() from main directly; it uses:
#include "cm55_init.h"
int main(void){ (void)cm55_initialize(system_ready_callback, NULL); (void)cm55_start_scheduler(); /* wraps vTaskStartScheduler(); does not return */}| Step | Meaning |
|---|---|
cm55_initialize(...) |
Bring-up + creating the starting task(s) on the main stack (the scheduler is not running yet) |
| Callback / code after init | Register hooks, create more tasks, per the project’s design |
cm55_start_scheduler() |
Starts the scheduler — usually does not return to main |
In the lab: create your tasks at the point the project designates (after init / in the callback), then let the scheduler run.
3. Tasks, Stack, Priority, Delay
Section titled “3. Tasks, Stack, Priority, Delay”3.1 Create a periodic task (LED heartbeat pattern)
Section titled “3.1 Create a periodic task (LED heartbeat pattern)”#include "FreeRTOS.h"#include "task.h"#include "led_controller.h"
#define BLINK_STACK_WORDS (256U)#define BLINK_PERIOD_MS (500U)
static void blink_task(void *arg){ (void)arg; for (;;) { led_controller_toggle(LED_RED); vTaskDelay(pdMS_TO_TICKS(BLINK_PERIOD_MS)); }}
void lab_start_blink_task(void){ TaskHandle_t handle = NULL; if (xTaskCreate(blink_task, "LAB_BLINK", BLINK_STACK_WORDS, NULL, tskIDLE_PRIORITY + 1U, &handle) != pdPASS) { /* create failed — check the heap / stack / priority */ }}xTaskCreate parameter |
Meaning |
|---|---|
| The task function | Must loop forever, or delete itself when a one-shot job is done |
| Name | A short string for debugging |
| Stack (words) | Too small → overflow; too large → wastes RAM |
| Priority | Higher = can preempt others sooner once ready to run |
| Handle | Keep it if you’ll delete/notify the task later |
Read more: xTaskCreate · vTaskDelay
3.2 Two tasks, two rates
Section titled “3.2 Two tasks, two rates”/* Task A: 250 ms — "fast heartbeat" */vTaskDelay(pdMS_TO_TICKS(250));
/* Task B: 500 ms — LED blink */vTaskDelay(pdMS_TO_TICKS(500));Notice that both run “simultaneously, from the system’s point of view”, with no busy-waiting in a single loop.
3.3 Priority rules of thumb (course)
Section titled “3.3 Priority rules of thumb (course)”| Guideline | Example in the lab |
|---|---|
| Background work / blinking | tskIDLE_PRIORITY + 1 |
| Button response / light UI work | Slightly higher than blink |
| Critical work (an IPC / watchdog path in a real product) | Higher — don’t raise everything to the maximum |
In a real product, an overly high priority for heavy work can starve other important work — in the lab, change it in small steps and observe the effect.
4. Queues — Pass Data Between Tasks
Section titled “4. Queues — Pass Data Between Tasks”A FreeRTOS queue copies data by the item’s size; it does not automatically pass a pointer. Read the overview: Queues · xQueueCreate
4.1 Producer / consumer (UART log drain pattern)
Section titled “4.1 Producer / consumer (UART log drain pattern)”The SDK’s approach: don’t printf from an ISR — enqueue it and let a task pull it and print it.
#include "queue.h"
typedef struct { char line[64];} log_item_t;
static QueueHandle_t s_log_q;
static void log_drain_task(void *arg){ (void)arg; log_item_t item; for (;;) { if (xQueueReceive(s_log_q, &item, portMAX_DELAY) == pdPASS) { printf("%s", item.line); } }}
void lab_log_queue_init(void){ s_log_q = xQueueCreate(8, sizeof(log_item_t)); (void)xTaskCreate(log_drain_task, "LAB_LOG", 512, NULL, tskIDLE_PRIORITY + 2U, NULL);}
void lab_log_post(const char *msg){ log_item_t item = {0}; /* copy with a length limit — lab example */ for (size_t i = 0; i + 1U < sizeof(item.line) && msg[i] != '\0'; ++i) { item.line[i] = msg[i]; } (void)xQueueSend(s_log_q, &item, 0);}4.2 From ISR to task
Section titled “4.2 From ISR to task”BaseType_t xHigherPriorityTaskWoken = pdFALSE;(void)xQueueSendFromISR(s_log_q, &item, &xHigherPriorityTaskWoken);portYIELD_FROM_ISR(xHigherPriorityTaskWoken);The SDK’s button (cm55_button_*) uses an ISR/bridge + queue approach on the IRQ path — the lab may use the button callback in task context first, and practise FromISR once ready.
5. Mutex and Semaphores — Protect Shared Resources
Section titled “5. Mutex and Semaphores — Protect Shared Resources”5.1 Mutex around a shared bus (I²C)
Section titled “5.1 Mutex around a shared bus (I²C)”From M03: a shared I²C bus must be locked
#include "semphr.h"
static SemaphoreHandle_t s_bus_mtx;
void lab_bus_lock_init(void){ s_bus_mtx = xSemaphoreCreateMutex();}
void lab_bus_lock(void){ (void)xSemaphoreTake(s_bus_mtx, portMAX_DELAY);}
void lab_bus_unlock(void){ (void)xSemaphoreGive(s_bus_mtx);}In the real SDK, some I²C locking uses a poll + vTaskDelay(1) pattern instead of portMAX_DELAY, to avoid a situation where the scheduler isn’t ready yet — in a basic lab, Take(... portMAX_DELAY) is fine once you know the scheduler is running.
Call it through the course’s wrapper when one exists:
cm55_i2c_manager_i2c_lock();/* sensor / HAL transfer */cm55_i2c_manager_i2c_unlock();The product’s UART uses a recursive mutex to serialise printf — know that an ordinary mutex vs a recursive one are different cases.
Read more: Mutexes
5.2 Binary semaphore — wait for an event
Section titled “5.2 Binary semaphore — wait for an event”static SemaphoreHandle_t s_evt;
void lab_evt_init(void){ s_evt = xSemaphoreCreateBinary();}
/* Task waits */(void)xSemaphoreTake(s_evt, pdMS_TO_TICKS(1000));
/* Other task or ISR signals */(void)xSemaphoreGive(s_evt);/* From ISR: xSemaphoreGiveFromISR(s_evt, &xHigherPriorityTaskWoken); */Use this when you need “a signal has occurred”, not to send a large payload — if you must send data, use a queue.
6. Event Groups — Ready Flags
Section titled “6. Event Groups — Ready Flags”#include "event_groups.h"
#define READY_BIT (1U << 0)
static EventGroupHandle_t s_ready;
void lab_ready_init(void){ s_ready = xEventGroupCreate();}
void lab_mark_ready(void){ (void)xEventGroupSetBits(s_ready, READY_BIT);}
BaseType_t lab_wait_ready(TickType_t ticks){ EventBits_t bits = xEventGroupWaitBits( s_ready, READY_BIT, pdFALSE, pdFALSE, ticks); return ((bits & READY_BIT) != 0) ? pdTRUE : pdFALSE;}The SDK uses this pattern for an “I²C is ready” flag before a sensor starts working. Read more: Event Groups
7. Timing Constraints and Real-Time Behavior
Section titled “7. Timing Constraints and Real-Time Behavior”| Topic | Practice in this course |
|---|---|
| A task’s period | Use vTaskDelay / vTaskDelayUntil, per the project’s convention |
| Long work at high priority | Split it into a lower-priority task, or break it into short pieces |
| ISR | Keep it short · no printf · no I²C · no blocking mutex |
| Tick hook | Never call a driver that needs a lock, or slow I/O |
| Heap / queue depth | depth × sizeof(item) uses RAM — creating too large a queue may return NULL |
Never in an ISR / tick hook
xSemaphoreTake(blocking),vTaskDelay, I²C,printfUse*FromISRand let a task do the heavy work instead
8. Worked Multi-Task Scenario
Section titled “8. Worked Multi-Task Scenario”The task: blink an LED at a fixed period + a button press enqueues a message into a log queue + another task prints over UART
blink_task → led_controller_toggle + vTaskDelaybutton callback → xQueueSend(log_q, "btn\r\n")log_drain_task → xQueueReceive + printfBuilding on M03 Lab D: read the ADC in a slow task + adjust the PWM in the same task, or send the value through a queue
9. Module Summary
Section titled “9. Module Summary”- The course uses FreeRTOS — started through
cm55_initialize/cm55_start_scheduler - Tasks + delay are the foundation; separate the timing of work with several tasks
- Queues pass data; Mutexes lock resources; Binary semaphores / Event groups signal state
- Respect ISR rules and priority so the system doesn’t “quietly hang” or starve
- Next, M05 will use several tasks with a sensor / Edge AI pipeline
Next Steps
Section titled “Next Steps”- Do the exercise: Lab
- Keep the summary sheet: Cheatsheet
- When ready, continue to M05 — Sensor Data and Edge AI Preparation (M05 lesson)
References and Further Reading
Section titled “References and Further Reading”Course portals
Section titled “Course portals”FreeRTOS
Section titled “FreeRTOS”- xTaskCreate
- vTaskDelay
- xQueueCreate
- Queues overview
- Mutexes
- Event Groups
- FreeRTOS Kernel Book (GitHub)
Prior modules
Section titled “Prior modules”Check your understanding
Section titled “Check your understanding”Three short questions in quiz.yaml, one per objective of this lesson. Try answering them yourself first, then compare with the answer key and explanations in the file.
Continue hands-on at Lab: multi-task firmware with FreeRTOS
Lab · Cheatsheet · ← Table of Contents · ← M03 · M05 →
Examples on the TESAIoT Developer Hub
Section titled “Examples on the TESAIoT Developer Hub”Try the real thing on the TESAIoT Dev Kit: open examples on the Developer Hub to read the code, download it, or flash ready-made firmware.
- EP07 — Final WiFi Manager — combines scan + profile + connect + auto-retry + a ping watchdog into a complete WiFi manager, with a state machine on screen and auto-connect from a saved profile
- EP07 — SensorHub Final — a course-closing project: a dashboard combining all 4 sensors (DPS368, SHT4x, BMI270, BMM350) + a stereo PDM microphone on a single screen
Review questions
Answer on your own first, then open the answer.
-
ตามหัวข้อ 3.3 งาน background เช่นกระพริบ LED ควรใช้ priority ใด (Objective 1)
- สูงกว่างานตอบสนองปุ่ม
- `tskIDLE_PRIORITY + 1`
- priority สูงสุดของระบบ
- เท่ากับงาน IPC วิกฤต
Show answer
Answer: B. `tskIDLE_PRIORITY + 1`
Priority rules of thumb: งาน background ใช้ `tskIDLE_PRIORITY + 1` และอย่ายกทุกอย่างขึ้นสูงสุด
-
ต้องการส่ง “ข้อมูล” จาก task ปุ่มไปยัง task ที่พิมพ์ UART ควรใช้กลไกใด (Objective 2)
- Mutex
- Event group
- Tick hook
- Queue
Show answer
Answer: D. Queue
Module Summary ข้อ 3: Queue ส่งข้อมูล, Mutex ล็อกทรัพยากร, Binary semaphore / Event group ส่งสัญญาณสถานะ
-
ข้อใดห้ามทำใน ISR หรือ tick hook ตามบทเรียน (เลือกได้หลายข้อ) (Objective 3)
- เรียก `portYIELD_FROM_ISR`
- เรียก `vTaskDelay`
- เรียก `printf`
- เรียก `xQueueSendFromISR`
- อ่านเซ็นเซอร์ผ่าน I²C
Show answer
Answer: B. เรียก `vTaskDelay` · C. เรียก `printf` · E. อ่านเซ็นเซอร์ผ่าน I²C
กรอบ “ห้ามใน ISR / tick hook” ในหัวข้อ 7: ห้าม blocking take, `vTaskDelay`, I²C และ `printf` ให้ใช้ `*FromISR` แล้วให้ task ทำงานหนักแทน
Cite this lesson
If you teach from this lesson or reuse it in slides or documents, credit it with the text below. If you changed it, add (adapted) after the title.
"Multi-task Firmware with FreeRTOS" from TESA Open Knowledge by the Thai Embedded Systems Association (TESA), https://github.com/tesaiot/tesa-qualification-program, licensed under CC BY-NC 4.0
Thai attribution: "เฟิร์มแวร์หลาย task ด้วย FreeRTOS" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0
This lesson adapts the source below; keep its credit too.
https://github.com/drsanti/TESAIoT-Courses/blob/287c21814ba8c75f693136616dcd270349a15966/C1/M04/README.md · Original content by Asst. Prof. Dr. Santi Nuratch (ผศ.ดร.สันติ นุราช), KMUTT. Course 1 (C1/) of drsanti/TESAIoT-Courses. TESA funded the work and holds the rights; published here under CC BY-NC 4.0. The upstream repository carries no licence file. Text kept faithful; structure, front matter, quizzes and notes added by TESA Open Knowledge.
TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0
Content is licensed CC BY-NC 4.0. Reuse it non-commercially and credit the Thai Embedded Systems Association (TESA) every time. · How to cite TESA