⏱ FreeRTOS cho lập trình nhúng
Đa nhiệm thời gian thực trên ESP32 / STM32: task, lập lịch, queue, semaphore, mutex, notification, event group, buffer, software timer — và một firmware hoàn chỉnh.
?Khóa học này dành cho ai?
Học sinh, sinh viên đã biết C/C++ cơ bản (xem khóa C++ cơ bản → nâng cao) và đã làm vài dự án Arduino, muốn viết firmware đa nhiệm, phản hồi đúng hạn cho ESP32, STM32: robot, trạm quan trắc, thiết bị IoT, dự án thi Khoa học kỹ thuật.
📖 Lý thuyết theo tài liệu chính thức
Bám theo 62 mục của trang freertos.org, giải thích bằng tiếng Việt kèm ví dụ phần cứng.
📊 Biểu đồ thời gian
Sơ đồ trạng thái, biểu đồ Gantt lập lịch, đảo ưu tiên, ISR → task… vẽ trực quan.
▶ 23 ví dụ chạy thật
Chạy trên FreeRTOS thật (bản mô phỏng trên PC), in mốc thời gian từng ms — kết quả có thể kiểm chứng.
⚙Chạy ví dụ trên máy tính và trên board
- Trên PC (Windows): tải FreeRTOS-Kernel, dùng port
portable/MSVC-MingW+heap_4.c, file cấu hình vàsim.hnhư trong khóa; biên dịch bằng MinGW g++. Linux/macOS dùng portThirdParty/GCC/Posix. - Trên ESP32 (Arduino IDE): chép thân các hàm task, bỏ
vTaskStartScheduler()(đã chạy sẵn), tạo task trongsetup(), thayLOG(...)bằngSerial.printf(...), thay ngắt giả lập bằngattachInterrupt. Lưu ý: ESP32 tính stack bằng byte. - Trên STM32: STM32CubeIDE → bật FREERTOS (CMSIS v2), có thể gọi trực tiếp API gốc
xTaskCreate…
≡Đối chiếu với giáo trình (62 mục)
| STT | Chủ đề trong giáo trình | Học ở bài |
|---|---|---|
| 1–12 | Getting Started, Kernel Overview, Supported Devices, Multitasking/Scheduling Basics, Context Switching, Real Time Applications/Scheduling, Implementation, Building Blocks, Detailed Example, Source Code Organization | Bài 1: Giới thiệu RTOS và FreeRTOS Bài 3: Lập lịch, trạng thái task và ưu tiên |
| 13–17 | Task States, Task Priorities, Implementing a Task, Idle Task and Idle Hook | Bài 2: Task: tạo, tham số, stack, xoá Bài 3: Lập lịch, trạng thái task và ưu tiên Bài 4: Idle task, idle hook và các hàm tiện ích của kernel |
| 18–24 | Co-Routines (status, implementing, priorities, scheduling, limitations) | Bài 12: Queue set, chủ đề nâng cao và dự án tổng hợp |
| 25–26, 51 | Queues | Bài 5: Queue – hàng đợi truyền dữ liệu giữa các task |
| 27–28 | Binary Semaphores, Counting Semaphores | Bài 6: Binary semaphore và counting semaphore |
| 29–30, 55 | Mutexes, Recursive Mutexes, Semaphore/Mutex API | Bài 7: Mutex, đảo ưu tiên, mutex đệ quy |
| 31–36, 50 | Direct To Task Notifications (as binary/counting semaphore, event group, mailbox) | Bài 8: Direct-to-task notification |
| 37–39, 53–54 | Stream & Message Buffers (ISR → task, core → core) | Bài 10: Stream buffer và message buffer |
| 40–45, 56 | Software Timers (daemon task, configuration, one-shot vs auto-reload, resetting) | Bài 11: Software timer |
| 46–49 | Task Creation, Task Control, Task Utilities, RTOS Kernel Control | Bài 2: Task: tạo, tham số, stack, xoá Bài 3: Lập lịch, trạng thái task và ưu tiên Bài 4: Idle task, idle hook và các hàm tiện ích của kernel |
| 52 | Queue Sets | Bài 12: Queue set, chủ đề nâng cao và dự án tổng hợp |
| 57 | Event Groups (flags) | Bài 9: Event group – chờ nhiều sự kiện, đồng bộ nhiều task |
| 58–62 | FreeRTOS-MPU, Co-routines API, Quality Management, Coding/Testing/Style Guide | Bài 12: Queue set, chủ đề nâng cao và dự án tổng hợp |
⌚Lộ trình gợi ý — 12 buổi
1,5 giờ
1,5 giờ
→Khóa liên quan
💻 C++ cơ bản → nâng cao
Nền tảng ngôn ngữ: con trỏ, struct, hàm callback, đa luồng.
🧩 Design Pattern
Observer, State, Command, Object Pool… — rất hợp để tổ chức firmware RTOS.
⏱ Giới thiệu RTOS và FreeRTOS
Hiểu vì sao firmware cần hệ điều hành thời gian thực, FreeRTOS gồm những gì và chuẩn bị môi trường thực hành.
- Phân biệt siêu vòng lặp (super loop) và đa nhiệm với RTOS
- Hiểu “thời gian thực” nghĩa là đúng hạn, không phải chạy nhanh
- Biết các khối xây dựng của FreeRTOS và cấu trúc mã nguồn
- Cài môi trường: ESP32 (Arduino/ESP-IDF), STM32CubeIDE hoặc bản mô phỏng trên PC
1Vấn đề của siêu vòng lặp
Chương trình Arduino thông thường là siêu vòng lặp: loop() làm lần lượt từng việc. Khi một việc dùng delay() hoặc chạy lâu (đọc cảm biến 250 ms, vẽ màn hình 200 ms), mọi việc khác phải chờ — nút nhấn bị bỏ lỡ, động cơ phản hồi chậm.
// Bài 1 – Vì sao cần RTOS? Mô phỏng chương trình "siêu vòng lặp" (super loop) kiểu Arduino
// KHÔNG dùng FreeRTOS: mỗi việc dùng delay() chặn -> nút nhấn bị bỏ lỡ.
#include <cstdio>
unsigned long thoiGian = 0; // đồng hồ giả lập (ms)
void delayMs(unsigned long ms) { thoiGian += ms; } // delay(): CPU đứng chờ, không làm gì khác được
// Nút được nhấn tại các thời điểm này (mỗi lần nhấn giữ 50 ms)
const unsigned long LAN_NHAN[] = {120, 950, 1430, 2210};
bool nutDangNhan() {
for (unsigned long t : LAN_NHAN)
if (thoiGian >= t && thoiGian < t + 50) return true;
return false;
}
int main() {
int phatHien = 0;
std::printf("== Siêu vòng lặp với delay() ==\n");
while (thoiGian < 2500) { // loop()
if (nutDangNhan()) { phatHien++; std::printf("[%4lu ms] phát hiện nút nhấn\n", thoiGian); }
std::printf("[%4lu ms] đọc cảm biến DHT22 (mất 250 ms)\n", thoiGian);
delayMs(250);
std::printf("[%4lu ms] cập nhật màn hình LCD (mất 200 ms)\n", thoiGian);
delayMs(200);
}
std::printf("Nhấn 4 lần, chỉ phát hiện %d lần -> bỏ lỡ %d lần!\n", phatHien, 4 - phatHien);
std::printf("RTOS giải quyết: mỗi việc là một TASK, task đọc nút có ưu tiên cao được chạy ngay.\n");
}
g++ -std=c++17 00-sieu-vong-lap.cpp -o app && ./app== Siêu vòng lặp với delay() == [ 0 ms] đọc cảm biến DHT22 (mất 250 ms) [ 250 ms] cập nhật màn hình LCD (mất 200 ms) [ 450 ms] đọc cảm biến DHT22 (mất 250 ms) [ 700 ms] cập nhật màn hình LCD (mất 200 ms) [ 900 ms] đọc cảm biến DHT22 (mất 250 ms) [1150 ms] cập nhật màn hình LCD (mất 200 ms) [1350 ms] đọc cảm biến DHT22 (mất 250 ms) [1600 ms] cập nhật màn hình LCD (mất 200 ms) [1800 ms] đọc cảm biến DHT22 (mất 250 ms) [2050 ms] cập nhật màn hình LCD (mất 200 ms) [2250 ms] phát hiện nút nhấn [2250 ms] đọc cảm biến DHT22 (mất 250 ms) [2500 ms] cập nhật màn hình LCD (mất 200 ms) Nhấn 4 lần, chỉ phát hiện 1 lần -> bỏ lỡ 3 lần! RTOS giải quyết: mỗi việc là một TASK, task đọc nút có ưu tiên cao được chạy ngay.
Giải thích
- Ví dụ này KHÔNG dùng FreeRTOS: chỉ mô phỏng thời gian để thấy 3/4 lần nhấn nút bị bỏ lỡ.
- Có thể vá bằng
millis()(bài 4 khóa C++), nhưng khi số việc tăng, code thành “mì ống” khó bảo trì — đó là lúc cần RTOS.
2RTOS và FreeRTOS
RTOS (Real-Time Operating System) là hệ điều hành nhỏ cho vi điều khiển, cho phép chia chương trình thành nhiều task chạy “song song”. Thời gian thực nghĩa là đảm bảo phản hồi trong một hạn định (deterministic) — không nhất thiết là nhanh nhất.
Hard real-time
Trễ hạn = hỏng: túi khí ô tô, điều khiển động cơ, máy trợ tim.
Soft real-time
Trễ hạn = giảm chất lượng: phát nhạc, cập nhật màn hình, gửi dữ liệu IoT.
FreeRTOS (Richard Barry, 2003; Amazon tiếp quản 2017) là RTOS phổ biến nhất thế giới: mã nguồn mở MIT, nhân chỉ vài file C, chạy trên hơn 40 kiến trúc (ARM Cortex-M, ESP32 Xtensa/RISC-V, AVR, PIC, RISC-V…). ESP32 chạy FreeRTOS ngay cả khi bạn viết Arduino — setup()/loop() chính là một task.
| Khối xây dựng | Vai trò | Bài |
|---|---|---|
| Task | đơn vị thực thi độc lập, có stack và ưu tiên riêng | 2–4 |
| Queue | gửi dữ liệu giữa các task / từ ngắt | 5 |
| Semaphore, Mutex | báo hiệu sự kiện, bảo vệ tài nguyên chung | 6–7 |
| Task notification | báo hiệu trực tiếp tới task, nhẹ nhất | 8 |
| Event group | chờ nhiều sự kiện, đồng bộ nhiều task | 9 |
| Stream/Message buffer | truyền dòng byte / thông điệp | 10 |
| Software timer | gọi hàm sau một khoảng thời gian / định kỳ | 11 |
3Cấu trúc mã nguồn và cấu hình
Ứng dụng tự cung cấp file FreeRTOSConfig.h — nơi bật/tắt mọi tính năng. Các tuỳ chọn quan trọng:
| Tuỳ chọn | Ý nghĩa | Giá trị hay dùng |
|---|---|---|
configTICK_RATE_HZ | số ngắt tick mỗi giây | 1000 (1 tick = 1 ms); ESP32 Arduino: 1000 |
configMAX_PRIORITIES | số mức ưu tiên | 5–25 (ESP32: 25) |
configUSE_PREEMPTION | cho phép chiếm quyền | 1 |
configTOTAL_HEAP_SIZE | heap cho FreeRTOS | tuỳ RAM chip |
configMINIMAL_STACK_SIZE | stack tối thiểu (đơn vị word*) | 128 trên Cortex-M |
INCLUDE_vTaskDelete… | bật các hàm API tuỳ chọn | 1 |
⚠️ *Trên FreeRTOS gốc, kích thước stack tính bằng word (4 byte trên chip 32 bit). Trên ESP-IDF/Arduino-ESP32, xTaskCreate nhận kích thước bằng byte.
4Chuẩn bị môi trường
| Môi trường | Cách bắt đầu |
|---|---|
| ESP32 (Arduino / ESP-IDF) | Cài board ESP32 trong Arduino IDE → dùng ngay xTaskCreate, xQueueCreate… không cần include gì thêm. ESP-IDF: #include "freertos/FreeRTOS.h", "freertos/task.h". |
| STM32 | STM32CubeIDE → Middleware → FREERTOS (CMSIS-RTOS v2), hoặc thêm FreeRTOS-Kernel thủ công. |
| Mô phỏng trên PC (khóa này) | FreeRTOS-Kernel + port MSVC-MingW (Windows) hoặc ThirdParty/GCC/Posix (Linux/macOS). Toàn bộ ví dụ trong khóa đã chạy thật trên bản mô phỏng, có in mốc thời gian (ms) để quan sát lập lịch. |
Lệnh build một ví dụ trên Windows (MinGW): g++ -std=c++17 -Isim -Ikernel/include -Ikernel/portable/MSVC-MingW vi_du.cpp libfreertos.a -lwinmm -o app. File sim.h cung cấp LOG() (in kèm thời gian) và hàm giả lập ngắt; trên board thật thay LOG bằng Serial.printf/ESP_LOGI.
🔧Áp dụng trong lập trình nhúng
- Trên ESP32-Arduino,
loop()chạy trong taskloopTaskưu tiên 1, stack 8 KB, trên nhân 1. WiFi/Bluetooth chạy ở các task riêng trên nhân 0. - Không nên dùng
delay()dài trong task — dùngvTaskDelay()(trên ESP32 Arduino,delay()thực chất gọivTaskDelay). - Bản quyền MIT cho phép dùng FreeRTOS trong sản phẩm thương mại mà không phải công bố mã nguồn.
✎Bài tập về nhà
🧵 Task: tạo, tham số, stack, xoá
Viết và tạo task đúng cách, truyền tham số, chọn kích thước stack và quản lý task bằng handle.
- Viết hàm task với vòng lặp vô hạn
- Dùng
xTaskCreatevàxTaskCreateStatic - Truyền tham số qua
void* - Đo stack còn trống, xoá task, truy vấn trạng thái
1Task đầu tiên
Một task là một hàm void tenTask(void* thamSo) chứa vòng lặp vô hạn và không bao giờ return (muốn kết thúc thì gọi vTaskDelete(NULL)).
xTaskCreate(hàm, "tên", stack, thamSo, ưuTiên, &handle) tạo task; vTaskStartScheduler() trao quyền điều khiển CPU cho FreeRTOS.
// Bài 2 – Task đầu tiên: tạo 2 task nháy 2 "LED" với chu kỳ khác nhau, chạy song song
#include "sim.h"
void taskLedDo(void* thamSo) {
(void)thamSo;
for (int i = 0; i < 4; i++) { // trên board thật là for (;;) – task không bao giờ return
LOG("LED đỏ %s", i % 2 ? "TẮT" : "BẬT");
vTaskDelay(pdMS_TO_TICKS(500)); // nhường CPU 500 ms (KHÔNG chặn task khác như delay())
}
vTaskDelete(nullptr); // task tự xoá khi xong việc
}
void taskLedXanh(void* thamSo) {
int chuKy = *static_cast<int*>(thamSo); // nhận tham số qua con trỏ void*
for (int i = 0; i < 6; i++) {
LOG("LED xanh %s", i % 2 ? "TẮT" : "BẬT");
vTaskDelay(pdMS_TO_TICKS(chuKy));
}
LOG("Hai task đã chạy song song xong");
ket_thuc();
}
int main() {
static int chuKyXanh = 300; // static: phải còn sống khi task chạy
PRINT("Tạo task...");
xTaskCreate(taskLedDo, "LedDo", configMINIMAL_STACK_SIZE, nullptr, 1, nullptr);
xTaskCreate(taskLedXanh, "LedXanh", configMINIMAL_STACK_SIZE, &chuKyXanh, 1, nullptr);
PRINT("Khởi động scheduler – từ đây FreeRTOS điều khiển CPU");
vTaskStartScheduler(); // không bao giờ trả về nếu đủ bộ nhớ
PRINT("Không đủ heap để tạo idle task!");
}
g++ -std=c++17 -Isim -Ikernel/include -Ikernel/portable/MSVC-MingW 01-hello-task.cpp libfreertos.a -lwinmm -o app && ./appTạo task... Khởi động scheduler – từ đây FreeRTOS điều khiển CPU [ 1 ms] LED đỏ BẬT [ 1 ms] LED xanh BẬT [ 301 ms] LED xanh TẮT [ 501 ms] LED đỏ TẮT [ 601 ms] LED xanh BẬT [ 901 ms] LED xanh TẮT [ 1001 ms] LED đỏ BẬT [ 1201 ms] LED xanh BẬT [ 1501 ms] LED đỏ TẮT [ 1501 ms] LED xanh TẮT [ 1801 ms] Hai task đã chạy song song xong
Giải thích
- Hai task có chu kỳ 500 ms và 300 ms chạy độc lập — không task nào phải chờ task kia.
vTaskDelay(pdMS_TO_TICKS(500))đưa task vào trạng thái Blocked, CPU được nhường cho task khác. Đây là điểm khác biệt quyết định so vớidelay().- Tham số phải còn sống khi task chạy — dùng biến
static/toàn cục, không dùng biến cục bộ củamain()/setup(). - Trên ESP32:
setup()tạo task rồi kết thúc; scheduler đã được khởi động sẵn, không gọivTaskStartScheduler().
2Tham số, task tĩnh, handle và stack
| Hàm | Dùng để |
|---|---|
xTaskCreate | tạo task, cấp stack + TCB từ heap của FreeRTOS |
xTaskCreateStatic | tạo task với stack + TCB do bạn cấp sẵn (không heap — hợp với hệ thống an toàn) |
vTaskDelete(handle) | xoá task (NULL = chính nó); bộ nhớ được idle task giải phóng |
uxTaskGetStackHighWaterMark | stack còn trống ít nhất từ trước tới giờ — dùng để chỉnh kích thước stack |
eTaskGetState, uxTaskGetNumberOfTasks | truy vấn trạng thái, đếm số task |
// Bài 2 – Task: một hàm dùng cho nhiều task (tham số khác nhau), task tạo tĩnh, handle, trạng thái, stack
#include "sim.h"
struct CauHinhCamBien {
const char* ten;
int chuKyMs;
int giaTriGoc;
};
void taskCamBien(void* p) {
const CauHinhCamBien* cfg = static_cast<const CauHinhCamBien*>(p);
for (int i = 0; i < 3; i++) {
LOG("%-8s đọc được %d", cfg->ten, cfg->giaTriGoc + i);
vTaskDelay(pdMS_TO_TICKS(cfg->chuKyMs));
}
LOG("%-8s xong, stack còn trống ít nhất %u word", cfg->ten, (unsigned)uxTaskGetStackHighWaterMark(nullptr));
vTaskDelete(nullptr);
}
// ===== Task tạo TĨNH: bộ nhớ stack và TCB do ta cấp, không dùng heap =====
StackType_t stackTinh[256];
StaticTask_t tcbTinh;
void taskBaoThuc(void*) {
for (;;) {
LOG("BaoThuc bíp! (task tĩnh)");
vTaskDelay(pdMS_TO_TICKS(400));
}
}
const char* tenTrangThai(eTaskState s) {
switch (s) {
case eRunning: return "Running";
case eReady: return "Ready";
case eBlocked: return "Blocked";
case eSuspended: return "Suspended";
case eDeleted: return "Deleted";
default: return "?";
}
}
TaskHandle_t hBaoThuc = nullptr;
void taskQuanLy(void*) {
vTaskDelay(pdMS_TO_TICKS(50));
LOG("QuanLy trạng thái BaoThuc: %s", tenTrangThai(eTaskGetState(hBaoThuc)));
vTaskDelay(pdMS_TO_TICKS(900));
LOG("QuanLy số task đang tồn tại: %u", (unsigned)uxTaskGetNumberOfTasks());
LOG("QuanLy xoá task BaoThuc");
vTaskDelete(hBaoThuc);
vTaskDelay(pdMS_TO_TICKS(500));
LOG("QuanLy số task còn lại: %u (gồm IDLE và Tmr Svc)", (unsigned)uxTaskGetNumberOfTasks());
ket_thuc();
}
int main() {
static const CauHinhCamBien nhiet{"Nhiet", 300, 28};
static const CauHinhCamBien doAm{"DoAm", 450, 60};
xTaskCreate(taskCamBien, "Nhiet", 512, (void*)&nhiet, 1, nullptr); // cùng hàm,
xTaskCreate(taskCamBien, "DoAm", 512, (void*)&doAm, 1, nullptr); // khác tham số
hBaoThuc = xTaskCreateStatic(taskBaoThuc, "BaoThuc", 256, nullptr, 1, stackTinh, &tcbTinh);
xTaskCreate(taskQuanLy, "QuanLy", 512, nullptr, 2, nullptr);
vTaskStartScheduler();
}
g++ -std=c++17 -Isim -Ikernel/include -Ikernel/portable/MSVC-MingW 02-task-tham-so-stack.cpp libfreertos.a -lwinmm -o app && ./app[ 1 ms] Nhiet đọc được 28 [ 1 ms] DoAm đọc được 60 [ 1 ms] BaoThuc bíp! (task tĩnh) [ 51 ms] QuanLy trạng thái BaoThuc: Blocked [ 301 ms] Nhiet đọc được 29 [ 401 ms] BaoThuc bíp! (task tĩnh) [ 451 ms] DoAm đọc được 61 [ 601 ms] Nhiet đọc được 30 [ 801 ms] BaoThuc bíp! (task tĩnh) [ 901 ms] DoAm đọc được 62 [ 901 ms] Nhiet xong, stack còn trống ít nhất 509 word [ 951 ms] QuanLy số task đang tồn tại: 5 [ 951 ms] QuanLy xoá task BaoThuc [ 1351 ms] DoAm xong, stack còn trống ít nhất 509 word [ 1451 ms] QuanLy số task còn lại: 3 (gồm IDLE và Tmr Svc)
Giải thích
- Cùng một hàm
taskCamBientạo được nhiều task nhờ tham số khác nhau (struct cấu hình). - Stack mỗi task trên chip thật thường 1–4 KB. Chạy thử, đọc high water mark, rồi đặt kích thước = dùng thật + 20–30% dự phòng.
- Số task cuối cùng là 3 vì FreeRTOS tự tạo thêm IDLE và Tmr Svc (timer daemon).
🔧Áp dụng trong lập trình nhúng
- ESP32:
xTaskCreatePinnedToCore(ham, "ten", 4096, NULL, 1, &h, 1)— chọn nhân 0 hoặc 1 để chạy task. - Tràn stack là lỗi phổ biến nhất: biến cục bộ lớn (mảng 1 KB),
printfsố thực, đệ quy. BậtconfigCHECK_FOR_STACK_OVERFLOW 2khi phát triển. - Không tạo/xoá task liên tục lúc chạy — tạo hết ở lúc khởi động để tránh phân mảnh heap.
⚠Lỗi thường gặp
- Hàm task chạy hết rồi
return→ crash. - Truyền con trỏ tới biến cục bộ của hàm tạo task.
- Stack quá nhỏ → hành vi kỳ lạ, reset ngẫu nhiên.
✎Bài tập về nhà
📅 Lập lịch, trạng thái task và ưu tiên
Hiểu scheduler chọn task nào chạy, các trạng thái của task và cách điều khiển chúng.
- Nắm 4 trạng thái Running/Ready/Blocked/Suspended
- Hiểu chiếm quyền (preemption), chia lượt (time slicing), hiện tượng “đói” CPU
- Hiểu chuyển ngữ cảnh (context switch) và ngắt tick
- Dùng
vTaskDelayUntil,vTaskSuspend/Resume,vTaskPrioritySet
1Trạng thái task
Trên CPU một nhân, tại mỗi thời điểm chỉ một task ở trạng thái Running. Task chờ thời gian (vTaskDelay) hay chờ sự kiện (queue, semaphore) ở trạng thái Blocked — không tốn chút CPU nào. Đây là lý do RTOS hiệu quả: phần lớn thời gian các task ngủ chờ sự kiện.
2Thuật toán lập lịch
FreeRTOS dùng lập lịch ưu tiên cố định, có chiếm quyền:
- Luôn chạy task Ready có ưu tiên cao nhất (số càng lớn càng ưu tiên; 0 là idle).
- Khi task ưu tiên cao hơn chuyển sang Ready (hết delay, nhận được dữ liệu), nó chiếm quyền ngay lập tức.
- Các task cùng ưu tiên chia lượt mỗi tick (time slicing).
Nếu task ưu tiên cao không bao giờ Blocked (vòng lặp bận), mọi task thấp hơn bị đói CPU (starvation). Quy tắc vàng: task ưu tiên cao phải ngắn và phải chờ sự kiện.
// Bài 3 – Ưu tiên và chiếm quyền (preemption): task ưu tiên cao chen ngang ngay khi sẵn sàng.
// Phần 2: task ưu tiên cao KHÔNG nhường CPU -> task thấp bị "đói" (starvation).
#include "sim.h"
void taskNen(void*) { // ưu tiên 1: việc nền nặng
for (int i = 1; i <= 10; i++) {
banTinhToan(100); // tính toán liên tục 100 ms, không nhường CPU
LOG(" Nen xong khối việc %d", i);
}
vTaskDelete(nullptr);
}
void taskKhanCap(void*) { // ưu tiên 3: phải phản hồi đúng hạn
TickType_t moc = xTaskGetTickCount();
for (int i = 0; i < 3; i++) {
vTaskDelayUntil(&moc, pdMS_TO_TICKS(170));
LOG("KhanCap đọc cảm biến khí gas (chen ngang task nền)");
}
vTaskDelete(nullptr);
}
void taskThamLam(void*) { // ưu tiên 2: chạy liên tục 300 ms không nghỉ
vTaskDelay(pdMS_TO_TICKS(700));
LOG("ThamLam bắt đầu chiếm CPU 300 ms (không gọi vTaskDelay)");
banTinhToan(300);
LOG("ThamLam xong – trong 300 ms đó task Nen (ưu tiên 1) không chạy được");
vTaskDelay(pdMS_TO_TICKS(350));
ket_thuc();
}
int main() {
xTaskCreate(taskNen, "Nen", 512, nullptr, 1, nullptr);
xTaskCreate(taskKhanCap, "KhanCap", 512, nullptr, 3, nullptr);
xTaskCreate(taskThamLam, "ThamLam", 512, nullptr, 2, nullptr);
vTaskStartScheduler();
}
g++ -std=c++17 -Isim -Ikernel/include -Ikernel/portable/MSVC-MingW 03-uu-tien-preemption.cpp libfreertos.a -lwinmm -o app && ./app[ 101 ms] Nen xong khối việc 1 [ 171 ms] KhanCap đọc cảm biến khí gas (chen ngang task nền) [ 201 ms] Nen xong khối việc 2 [ 301 ms] Nen xong khối việc 3 [ 341 ms] KhanCap đọc cảm biến khí gas (chen ngang task nền) [ 401 ms] Nen xong khối việc 4 [ 501 ms] Nen xong khối việc 5 [ 511 ms] KhanCap đọc cảm biến khí gas (chen ngang task nền) [ 602 ms] Nen xong khối việc 6 [ 701 ms] ThamLam bắt đầu chiếm CPU 300 ms (không gọi vTaskDelay) [ 1001 ms] ThamLam xong – trong 300 ms đó task Nen (ưu tiên 1) không chạy được [ 1002 ms] Nen xong khối việc 7 [ 1102 ms] Nen xong khối việc 8 [ 1202 ms] Nen xong khối việc 9 [ 1302 ms] Nen xong khối việc 10
Giải thích
- KhanCap chạy đúng các mốc 170/340/510 ms dù task Nen đang tính toán.
- Từ 701 đến 1001 ms task Nen im lặng hoàn toàn vì ThamLam (ưu tiên 2) chiếm CPU mà không nhường.
3Chuyển ngữ cảnh và ngắt tick
Một bộ timer phần cứng tạo ngắt tick (thường 1 ms). Trong ngắt, kernel tăng bộ đếm tick, đánh thức task hết hạn delay và quyết định có cần đổi task không. Chuyển ngữ cảnh = lưu toàn bộ thanh ghi của task cũ vào stack của nó, nạp thanh ghi của task mới — mất vài micro giây trên Cortex-M.
4Điều khiển task
| Hàm | Tác dụng |
|---|---|
vTaskDelay(n) | ngủ n tick tính từ lúc gọi |
vTaskDelayUntil(&moc, n) | ngủ tới moc + n → chu kỳ chính xác, không trôi (lấy mẫu cảm biến, vòng điều khiển PID) |
vTaskSuspend / vTaskResume | tạm dừng / tiếp tục một task |
vTaskPrioritySet / uxTaskPriorityGet | đổi / đọc ưu tiên lúc chạy |
taskYIELD() | tự nhường CPU cho task cùng ưu tiên |
// Bài 3 – vTaskDelay vs vTaskDelayUntil (chu kỳ chính xác), vTaskSuspend/Resume, đổi ưu tiên lúc chạy
#include "sim.h"
void taskLayMau(void*) {
LOG("== Cách 1: vTaskDelay(100) sau khi xử lý 30 ms ==");
for (int i = 0; i < 4; i++) {
LOG("vTaskDelay lấy mẫu lần %d", i);
banTinhToan(30); // xử lý mất 30 ms
vTaskDelay(pdMS_TO_TICKS(100)); // nghỉ 100 ms TÍNH TỪ BÂY GIỜ -> chu kỳ thực 130 ms (trôi dần)
}
LOG("== Cách 2: vTaskDelayUntil(&moc, 100) ==");
TickType_t moc = xTaskGetTickCount();
for (int i = 0; i < 4; i++) {
LOG("vTaskDelayUntil lấy mẫu lần %d", i);
banTinhToan(30);
vTaskDelayUntil(&moc, pdMS_TO_TICKS(100)); // đánh thức đúng moc + 100 -> chu kỳ đúng 100 ms
}
vTaskDelete(nullptr);
}
TaskHandle_t hNhay = nullptr;
void taskNhayLed(void*) {
vTaskDelay(pdMS_TO_TICKS(1000));
for (;;) {
LOG(" NhayLed nháy (ưu tiên %u)", (unsigned)uxTaskPriorityGet(nullptr));
vTaskDelay(pdMS_TO_TICKS(150));
}
}
void taskDieuKhien(void*) {
vTaskDelay(pdMS_TO_TICKS(1320));
LOG("DieuKhien -> vTaskSuspend(NhayLed): tạm dừng");
vTaskSuspend(hNhay);
vTaskDelay(pdMS_TO_TICKS(400));
LOG("DieuKhien -> vTaskResume(NhayLed) và nâng ưu tiên lên 3");
vTaskPrioritySet(hNhay, 3);
vTaskResume(hNhay);
vTaskDelay(pdMS_TO_TICKS(320));
ket_thuc();
}
int main() {
xTaskCreate(taskLayMau, "LayMau", 512, nullptr, 2, nullptr);
xTaskCreate(taskNhayLed, "NhayLed", 512, nullptr, 1, &hNhay);
xTaskCreate(taskDieuKhien, "DieuKhien", 512, nullptr, 4, nullptr);
vTaskStartScheduler();
}
g++ -std=c++17 -Isim -Ikernel/include -Ikernel/portable/MSVC-MingW 04-delay-until-suspend.cpp libfreertos.a -lwinmm -o app && ./app[ 1 ms] == Cách 1: vTaskDelay(100) sau khi xử lý 30 ms == [ 1 ms] vTaskDelay lấy mẫu lần 0 [ 131 ms] vTaskDelay lấy mẫu lần 1 [ 261 ms] vTaskDelay lấy mẫu lần 2 [ 391 ms] vTaskDelay lấy mẫu lần 3 [ 521 ms] == Cách 2: vTaskDelayUntil(&moc, 100) == [ 521 ms] vTaskDelayUntil lấy mẫu lần 0 [ 621 ms] vTaskDelayUntil lấy mẫu lần 1 [ 721 ms] vTaskDelayUntil lấy mẫu lần 2 [ 821 ms] vTaskDelayUntil lấy mẫu lần 3 [ 1031 ms] NhayLed nháy (ưu tiên 1) [ 1181 ms] NhayLed nháy (ưu tiên 1) [ 1321 ms] DieuKhien -> vTaskSuspend(NhayLed): tạm dừng [ 1721 ms] DieuKhien -> vTaskResume(NhayLed) và nâng ưu tiên lên 3 [ 1721 ms] NhayLed nháy (ưu tiên 3) [ 1871 ms] NhayLed nháy (ưu tiên 3) [ 2021 ms] NhayLed nháy (ưu tiên 3)
Giải thích
- Với
vTaskDelay, chu kỳ thực = 100 + 30 = 130 ms và bị trôi. VớivTaskDelayUntilcác mốc đúng 521, 621, 721, 821. - Trong lúc bị Suspend (1321–1721 ms) NhayLed không in gì.
🔧Áp dụng trong lập trình nhúng
- Gợi ý phân ưu tiên: điều khiển động cơ/an toàn > giao tiếp (UART, CAN) > xử lý dữ liệu > hiển thị > ghi log.
- ESP32 có 2 nhân: scheduler chạy riêng trên mỗi nhân (SMP), nên hai task có thể chạy THỰC SỰ song song.
- Vòng điều khiển (PID, cân bằng robot) luôn dùng
vTaskDelayUntilđể chu kỳ lấy mẫu ổn định.
⚠Lỗi thường gặp
- Đặt mọi task cùng ưu tiên cao nhất “cho chắc”.
- Vòng lặp bận chờ (
while(!co){}) trong task ưu tiên cao. - Dùng
vTaskSuspendđể đồng bộ dữ liệu thay vì semaphore/queue.
✎Bài tập về nhà
😴 Idle task, idle hook và các hàm tiện ích của kernel
Hiểu idle task, dùng idle hook để đo tải CPU/tiết kiệm điện, và các API điều khiển kernel.
- Biết idle task làm gì và vì sao không được chặn
- Viết idle hook đo CPU rảnh
- Dùng vùng găng và tạm dừng scheduler đúng chỗ
- Dùng
vTaskList,pcTaskGetName… để gỡ lỗi
1Idle task và idle hook
Idle task (ưu tiên 0) được tạo tự động khi khởi động scheduler, chạy khi không còn task nào Ready. Nó giải phóng bộ nhớ của task đã bị xoá. Idle hook (vApplicationIdleHook, bật configUSE_IDLE_HOOK) là hàm của bạn được idle task gọi liên tục — dùng để đo tải CPU, cho chip ngủ (__WFI()), nuôi watchdog. Idle hook không bao giờ được chặn.
Tiết kiệm năng lượng nâng cao: tickless idle (configUSE_TICKLESS_IDLE) tắt ngắt tick khi rảnh lâu — ESP32 dùng cho chế độ light sleep tự động.
2Kernel control và tiện ích
| Hàm | Ý nghĩa |
|---|---|
taskENTER_CRITICAL / taskEXIT_CRITICAL | vùng găng: tắt ngắt (tới mức cho phép) — giữ thật ngắn |
vTaskSuspendAll / xTaskResumeAll | tạm dừng scheduler, ngắt vẫn chạy |
vTaskList, uxTaskGetSystemState | liệt kê task, trạng thái, stack còn trống |
xTaskGetTickCount, pcTaskGetName, xTaskGetHandle | đọc tick, tên task, tìm task theo tên |
// Bài 4 – Idle task, idle hook (đo tải CPU), vùng găng, tạm dừng scheduler, liệt kê task
#include "sim.h"
volatile uint32_t demIdle = 0;
// Idle hook: được gọi liên tục khi KHÔNG có task nào khác sẵn sàng.
// Không được chặn (không vTaskDelay). Thường dùng để: đo CPU rảnh, cho chip vào chế độ ngủ.
extern "C" void vApplicationIdleHook(void) {
static TickType_t lanCuoi = 0;
TickType_t now = xTaskGetTickCount();
if (now != lanCuoi) { lanCuoi = now; demIdle++; } // đếm số tick CPU rảnh
}
void taskBan(void*) {
for (int i = 0; i < 3; i++) {
banTinhToan(60); // bận 60 ms
vTaskDelay(pdMS_TO_TICKS(140)); // nghỉ 140 ms -> tải CPU ~30%
}
vTaskDelete(nullptr);
}
volatile int biengChung = 0;
void taskBaoCao(void*) {
vTaskDelay(pdMS_TO_TICKS(20));
char bang[512];
vTaskList(bang); // bảng: Tên Trạng thái Ưu tiên Stack trống Số thứ tự
PRINT("Tên \tTT\tƯT\tStack\tSTT\n%s", bang);
PRINT("(TT: X=đang chạy, R=sẵn sàng, B=bị chặn, S=tạm dừng, D=đã xoá)\n");
// Vùng găng (critical section): tắt ngắt/không chuyển task – phải thật NGẮN
taskENTER_CRITICAL();
biengChung++;
taskEXIT_CRITICAL();
// Tạm dừng scheduler: các task khác không chạy nhưng ngắt vẫn hoạt động
vTaskSuspendAll();
biengChung++;
xTaskResumeAll();
LOG("BaoCao biengChung = %d (sửa an toàn 2 lần)", biengChung);
TickType_t batDau = xTaskGetTickCount();
uint32_t idleBatDau = demIdle;
vTaskDelay(pdMS_TO_TICKS(600));
uint32_t tong = xTaskGetTickCount() - batDau, ranh = demIdle - idleBatDau;
LOG("BaoCao trong %lu ms: CPU rảnh ~%lu ms -> tải CPU ~%lu%%", (unsigned long)tong, (unsigned long)ranh,
(unsigned long)(100 - ranh * 100 / tong));
LOG("BaoCao tên task hiện tại: %s, idle task: %s", pcTaskGetName(nullptr), pcTaskGetName(xTaskGetIdleTaskHandle()));
ket_thuc();
}
int main() {
xTaskCreate(taskBan, "Ban", 512, nullptr, 1, nullptr);
xTaskCreate(taskBaoCao, "BaoCao", 2048, nullptr, 2, nullptr);
vTaskStartScheduler();
}
g++ -std=c++17 -Isim -Ikernel/include -Ikernel/portable/MSVC-MingW 05-idle-hook-tien-ich.cpp libfreertos.a -lwinmm -o app && ./appTên TT ƯT Stack STT BaoCao X 2 2045 2 Ban R 1 509 1 IDLE R 0 253 3 Tmr Svc B 7 509 4 (TT: X=đang chạy, R=sẵn sàng, B=bị chặn, S=tạm dừng, D=đã xoá) [ 21 ms] BaoCao biengChung = 2 (sửa an toàn 2 lần) [ 621 ms] BaoCao trong 600 ms: CPU rảnh ~439 ms -> tải CPU ~27% [ 621 ms] BaoCao tên task hiện tại: BaoCao, idle task: IDLE
Giải thích
- Tải CPU đo được ~27%: khớp với task bận 60 ms trên mỗi chu kỳ ~200 ms.
- Bảng
vTaskList: X = đang chạy, R = Ready, B = Blocked. Cột Stack là high water mark. - Trên board thật, idle hook tính tải CPU bằng cách đếm số vòng lặp trong một giây rồi so với lúc hệ thống rảnh hoàn toàn.
🔧Áp dụng trong lập trình nhúng
- ESP32: Task Watchdog theo dõi idle task — nếu một task ưu tiên cao chạy bận quá lâu làm idle không chạy được, chip báo lỗi "Task watchdog got triggered".
- Vùng găng trên ESP32 đa nhân dùng
portENTER_CRITICAL(&spinlock).
⚠Lỗi thường gặp
- Gọi
vTaskDelaytrong idle hook. - Làm việc nặng (in, chờ) trong vùng găng.
- Quên gọi
xTaskResumeAllsauvTaskSuspendAll.
✎Bài tập về nhà
📬 Queue – hàng đợi truyền dữ liệu giữa các task
Truyền dữ liệu an toàn giữa các task và từ ngắt tới task bằng queue.
- Tạo queue, gửi/nhận có chờ và không chờ
- Gửi struct, gửi lên đầu hàng đợi, peek
- Gửi từ ISR bằng
xQueueSendFromISR - Xử lý khi hàng đợi đầy
1Queue cơ bản
Queue là hàng đợi FIFO có kích thước cố định, an toàn khi nhiều task/ngắt dùng chung. Dữ liệu được sao chép vào queue (không phải con trỏ) — task gửi có thể dùng lại biến ngay.
| API | Ghi chú |
|---|---|
xQueueCreate(soPhanTu, kichThuoc) | tạo hàng đợi |
xQueueSend(q, &x, timeout) | gửi vào cuối; chờ tối đa timeout nếu đầy |
xQueueReceive(q, &x, timeout) | lấy từ đầu; chờ nếu rỗng (portMAX_DELAY = chờ mãi) |
uxQueueMessagesWaiting | số phần tử đang chờ |
// Bài 5 – Queue cơ bản: task sản xuất gửi dữ liệu, task tiêu thụ chờ (bị chặn) cho tới khi có dữ liệu.
// Phần 2: hàng đợi đầy khi bên gửi nhanh hơn bên nhận.
#include "sim.h"
#include "queue.h"
QueueHandle_t qNhietDo;
void taskDocCamBien(void*) { // producer
for (int i = 0; i < 4; i++) {
int t = 280 + i * 3; // nhiệt độ x10
LOG("DocCamBien gửi %d", t);
xQueueSend(qNhietDo, &t, portMAX_DELAY); // sao chép giá trị vào hàng đợi
vTaskDelay(pdMS_TO_TICKS(200));
}
vTaskDelete(nullptr);
}
void taskHienThi(void*) { // consumer
int t;
for (int i = 0; i < 4; i++) {
xQueueReceive(qNhietDo, &t, portMAX_DELAY); // ngủ (Blocked) tới khi có dữ liệu – không tốn CPU
LOG(" HienThi nhận %d -> LCD: %d.%d°C", t, t / 10, t % 10);
}
vTaskDelete(nullptr);
}
QueueHandle_t qNho;
void taskGuiNhanh(void*) {
vTaskDelay(pdMS_TO_TICKS(1000));
LOG("== Phần 2: hàng đợi 3 phần tử, gửi mỗi 50 ms, nhận mỗi 200 ms ==");
for (int i = 1; i <= 8; i++) {
BaseType_t ok = xQueueSend(qNho, &i, 0); // timeout 0: không chờ nếu đầy
LOG("GuiNhanh gửi %d -> %s (đang chờ %u)", i, ok == pdPASS ? "OK" : "ĐẦY, bỏ mất", (unsigned)uxQueueMessagesWaiting(qNho));
vTaskDelay(pdMS_TO_TICKS(50));
}
vTaskDelete(nullptr);
}
void taskNhanCham(void*) {
vTaskDelay(pdMS_TO_TICKS(1000));
int v;
while (xQueueReceive(qNho, &v, pdMS_TO_TICKS(500)) == pdPASS) { // chờ tối đa 500 ms
LOG(" NhanCham nhận %d", v);
vTaskDelay(pdMS_TO_TICKS(200));
}
LOG(" NhanCham hết 500 ms không có dữ liệu -> kết thúc");
ket_thuc();
}
int main() {
qNhietDo = xQueueCreate(5, sizeof(int)); // 5 phần tử, mỗi phần tử sizeof(int) byte
qNho = xQueueCreate(3, sizeof(int));
xTaskCreate(taskDocCamBien, "DocCamBien", 512, nullptr, 2, nullptr);
xTaskCreate(taskHienThi, "HienThi", 512, nullptr, 1, nullptr);
xTaskCreate(taskGuiNhanh, "GuiNhanh", 512, nullptr, 2, nullptr);
xTaskCreate(taskNhanCham, "NhanCham", 512, nullptr, 1, nullptr);
vTaskStartScheduler();
}
g++ -std=c++17 -Isim -Ikernel/include -Ikernel/portable/MSVC-MingW 06-queue-co-ban.cpp libfreertos.a -lwinmm -o app && ./app[ 1 ms] DocCamBien gửi 280 [ 1 ms] HienThi nhận 280 -> LCD: 28.0°C [ 201 ms] DocCamBien gửi 283 [ 201 ms] HienThi nhận 283 -> LCD: 28.3°C [ 401 ms] DocCamBien gửi 286 [ 401 ms] HienThi nhận 286 -> LCD: 28.6°C [ 601 ms] DocCamBien gửi 289 [ 601 ms] HienThi nhận 289 -> LCD: 28.9°C [ 1001 ms] == Phần 2: hàng đợi 3 phần tử, gửi mỗi 50 ms, nhận mỗi 200 ms == [ 1001 ms] GuiNhanh gửi 1 -> OK (đang chờ 1) [ 1001 ms] NhanCham nhận 1 [ 1051 ms] GuiNhanh gửi 2 -> OK (đang chờ 1) [ 1101 ms] GuiNhanh gửi 3 -> OK (đang chờ 2) [ 1151 ms] GuiNhanh gửi 4 -> OK (đang chờ 3) [ 1201 ms] GuiNhanh gửi 5 -> ĐẦY, bỏ mất (đang chờ 3) [ 1201 ms] NhanCham nhận 2 [ 1251 ms] GuiNhanh gửi 6 -> OK (đang chờ 3) [ 1301 ms] GuiNhanh gửi 7 -> ĐẦY, bỏ mất (đang chờ 3) [ 1351 ms] GuiNhanh gửi 8 -> ĐẦY, bỏ mất (đang chờ 3) [ 1402 ms] NhanCham nhận 3 [ 1602 ms] NhanCham nhận 4 [ 1802 ms] NhanCham nhận 6 [ 2502 ms] NhanCham hết 500 ms không có dữ liệu -> kết thúc
Giải thích
- Phần 1: HienThi nhận ngay khi DocCamBien gửi — task nhận ngủ trong lúc chờ.
- Phần 2: hàng đợi 3 phần tử đầy, các giá trị 5, 7, 8 bị bỏ (timeout = 0). Chọn kích thước queue theo tốc độ gửi/nhận tệ nhất.
2Queue chứa struct, nhiều nguồn, từ ngắt
Mẫu thiết kế phổ biến: nhiều task nguồn → một queue → một task xử lý (ghi log, gửi MQTT). Mỗi phần tử là một struct mô tả nguồn và giá trị.
xQueueSendToFront: chen lên đầu — cho tin khẩn.xQueuePeek: xem mà không lấy ra.- Trong ISR chỉ dùng hàm có hậu tố FromISR: không bao giờ chờ, trả về cờ “cần chuyển ngữ cảnh”.
// Bài 5 – Queue chứa struct: nhiều task nguồn gửi về một task ghi log; tin khẩn gửi lên ĐẦU hàng đợi;
// xQueuePeek xem mà không lấy; gửi từ ngắt bằng xQueueSendFromISR.
#include "sim.h"
#include "queue.h"
enum Nguon : uint8_t { NHIET, DO_AM, KHOI, NUT };
const char* TEN_NGUON[] = {"Nhiệt độ", "Độ ẩm", "Khói", "Nút nhấn"};
struct BanTin {
Nguon nguon;
int16_t giaTri;
bool khanCap;
};
QueueHandle_t qLog;
void taskNhiet(void*) {
for (int i = 0; i < 3; i++) {
BanTin b{NHIET, int16_t(28 + i), false};
xQueueSend(qLog, &b, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(150));
}
vTaskDelete(nullptr);
}
void taskDoAm(void*) {
for (int i = 0; i < 3; i++) {
BanTin b{DO_AM, int16_t(60 - i), false};
xQueueSend(qLog, &b, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(150));
}
vTaskDelete(nullptr);
}
void taskKhoi(void*) {
vTaskDelay(pdMS_TO_TICKS(140));
BanTin b{KHOI, 850, true};
xQueueSendToFront(qLog, &b, portMAX_DELAY); // chen lên ĐẦU hàng đợi
vTaskDelete(nullptr);
}
// ISR nút nhấn: dùng phiên bản ...FromISR, không bao giờ chờ
uint32_t isrNut() {
BaseType_t canChuyen = pdFALSE;
BanTin b{NUT, 1, false};
xQueueSendFromISR(qLog, &b, &canChuyen);
return canChuyen; // pdTRUE nếu task đang chờ có ưu tiên cao hơn task bị ngắt
}
void taskPhanCung(void*) { // giả lập: người dùng nhấn nút lúc 300 ms
vTaskDelay(pdMS_TO_TICKS(300));
phatNgat(3);
vTaskDelete(nullptr);
}
void taskGhiLog(void*) {
vTaskDelay(pdMS_TO_TICKS(100)); // ghi log chậm: để bản tin dồn lại trong hàng đợi
BanTin b;
while (xQueuePeek(qLog, &b, pdMS_TO_TICKS(400)) == pdPASS) {
if (b.khanCap) LOG("GhiLog (peek) thấy tin khẩn ở đầu hàng đợi!");
xQueueReceive(qLog, &b, 0);
LOG("GhiLog %s = %d%s", TEN_NGUON[b.nguon], b.giaTri, b.khanCap ? " <<< KHẨN" : "");
vTaskDelay(pdMS_TO_TICKS(40));
}
LOG("GhiLog hàng đợi trống, kết thúc. sizeof(BanTin) = %u byte", (unsigned)sizeof(BanTin));
ket_thuc();
}
int main() {
qLog = xQueueCreate(10, sizeof(BanTin));
dangKyNgat(3, isrNut);
xTaskCreate(taskNhiet, "Nhiet", 512, nullptr, 2, nullptr);
xTaskCreate(taskDoAm, "DoAm", 512, nullptr, 2, nullptr);
xTaskCreate(taskKhoi, "Khoi", 512, nullptr, 3, nullptr);
xTaskCreate(taskPhanCung, "PhanCung", 512, nullptr, 4, nullptr);
xTaskCreate(taskGhiLog, "GhiLog", 1024, nullptr, 1, nullptr);
vTaskStartScheduler();
}
g++ -std=c++17 -Isim -Ikernel/include -Ikernel/portable/MSVC-MingW 07-queue-struct.cpp libfreertos.a -lwinmm -o app && ./app[ 101 ms] GhiLog Nhiệt độ = 28 [ 142 ms] GhiLog (peek) thấy tin khẩn ở đầu hàng đợi! [ 142 ms] GhiLog Khói = 850 <<< KHẨN [ 182 ms] GhiLog Độ ẩm = 60 [ 222 ms] GhiLog Nhiệt độ = 29 [ 262 ms] GhiLog Độ ẩm = 59 [ 302 ms] GhiLog Nút nhấn = 1 [ 342 ms] GhiLog Nhiệt độ = 30 [ 382 ms] GhiLog Độ ẩm = 58 [ 822 ms] GhiLog hàng đợi trống, kết thúc. sizeof(BanTin) = 6 byte
Giải thích
- Tin khói (khẩn) được xử lý trước dù gửi sau các tin nhiệt độ/độ ẩm.
- Struct nên nhỏ (ở đây 6 byte). Dữ liệu lớn: gửi con trỏ tới bộ đệm trong một Object Pool thay vì sao chép cả khối.
- Ngắt nút nhấn gửi thẳng vào cùng queue bằng
xQueueSendFromISR.
🔧Áp dụng trong lập trình nhúng
- UART RX (ESP-IDF): driver UART đẩy sự kiện vào một queue; task của bạn
xQueueReceiveđể xử lý. - ISR trên ESP32 Arduino:
BaseType_t w = pdFALSE; xQueueSendFromISR(q, &v, &w); if (w) portYIELD_FROM_ISR(); - ISR trên ESP32 phải có thuộc tính
IRAM_ATTR.
⚠Lỗi thường gặp
- Gọi
xQueueSend(không FromISR) trong ngắt. - Gửi con trỏ tới biến cục bộ đã chết qua queue.
- Queue quá nhỏ làm mất dữ liệu mà không biết.
✎Bài tập về nhà
🚦 Binary semaphore và counting semaphore
Dùng semaphore để báo hiệu sự kiện (đặc biệt từ ngắt) và quản lý nhóm tài nguyên.
- Hiểu kỹ thuật xử lý ngắt trì hoãn (deferred interrupt processing)
- Dùng binary semaphore ISR → task
- Dùng counting semaphore quản lý N tài nguyên và đếm sự kiện
1Binary semaphore: báo hiệu từ ngắt
Binary semaphore giống một lá cờ 0/1. Mẫu kinh điển: ISR thật ngắn — chỉ xSemaphoreGiveFromISR — rồi một task ưu tiên cao đang chờ xSemaphoreTake thức dậy làm phần việc nặng (đọc I2C, chống dội, tính toán). Nhờ vậy ngắt khoá hệ thống trong thời gian cực ngắn.
// Bài 6 – Binary semaphore: "xử lý ngắt trì hoãn" (deferred interrupt processing).
// ISR chỉ báo hiệu (give) rồi thoát ngay; task xử lý (ưu tiên cao) làm phần việc nặng.
#include "sim.h"
#include "semphr.h"
SemaphoreHandle_t semNut;
volatile uint32_t soLanNgat = 0;
uint32_t isrNutNhan() { // ISR: thật ngắn, không in, không chờ
soLanNgat++;
BaseType_t canChuyen = pdFALSE;
xSemaphoreGiveFromISR(semNut, &canChuyen);
return canChuyen; // = portYIELD_FROM_ISR(canChuyen) trên board thật
}
void taskXuLyNut(void*) {
for (;;) {
if (xSemaphoreTake(semNut, pdMS_TO_TICKS(450)) == pdTRUE) { // ngủ tới khi ISR give (tối đa 450 ms)
LOG("XuLyNut được đánh thức -> đọc chống dội, đổi chế độ quạt (việc nặng ở đây)");
} else {
LOG("XuLyNut 450 ms không có ai nhấn nút -> kết thúc ví dụ");
ket_thuc();
}
}
}
void taskNen(void*) {
for (;;) {
banTinhToan(100); // task nền bận liên tục
LOG(" Nen đang tính toán...");
}
}
void taskPhanCung(void*) { // giả lập người dùng nhấn nút
const uint32_t thoiDiem[] = {250, 520, 900};
TickType_t truoc = 0;
for (uint32_t t : thoiDiem) {
vTaskDelay(pdMS_TO_TICKS(t - truoc));
truoc = t;
LOG("[phần cứng] nút được nhấn -> phát ngắt");
phatNgat(3);
}
vTaskDelete(nullptr);
}
int main() {
semNut = xSemaphoreCreateBinary(); // tạo ra ở trạng thái "trống" (đã bị take)
dangKyNgat(3, isrNutNhan);
xTaskCreate(taskXuLyNut, "XuLyNut", 512, nullptr, 3, nullptr);
xTaskCreate(taskNen, "Nen", 512, nullptr, 1, nullptr);
xTaskCreate(taskPhanCung, "PhanCung", 512, nullptr, 4, nullptr);
vTaskStartScheduler();
}
g++ -std=c++17 -Isim -Ikernel/include -Ikernel/portable/MSVC-MingW 08-binary-semaphore-isr.cpp libfreertos.a -lwinmm -o app && ./app[ 101 ms] Nen đang tính toán... [ 201 ms] Nen đang tính toán... [ 251 ms] [phần cứng] nút được nhấn -> phát ngắt [ 251 ms] XuLyNut được đánh thức -> đọc chống dội, đổi chế độ quạt (việc nặng ở đây) [ 301 ms] Nen đang tính toán... [ 401 ms] Nen đang tính toán... [ 501 ms] Nen đang tính toán... [ 521 ms] [phần cứng] nút được nhấn -> phát ngắt [ 521 ms] XuLyNut được đánh thức -> đọc chống dội, đổi chế độ quạt (việc nặng ở đây) [ 601 ms] Nen đang tính toán... [ 701 ms] Nen đang tính toán... [ 801 ms] Nen đang tính toán... [ 901 ms] [phần cứng] nút được nhấn -> phát ngắt [ 902 ms] XuLyNut được đánh thức -> đọc chống dội, đổi chế độ quạt (việc nặng ở đây) [ 902 ms] Nen đang tính toán... [ 1002 ms] Nen đang tính toán... [ 1102 ms] Nen đang tính toán... [ 1202 ms] Nen đang tính toán... [ 1302 ms] Nen đang tính toán... [ 1352 ms] XuLyNut 450 ms không có ai nhấn nút -> kết thúc ví dụ
Giải thích
- Mỗi lần ngắt, XuLyNut chạy ngay tại thời điểm ngắt (251, 521, 902 ms) dù task Nen đang bận.
xSemaphoreCreateBinary()tạo ra ở trạng thái rỗng — lần take đầu sẽ chờ.- Nếu nhiều ngắt tới trước khi task kịp take, binary semaphore chỉ nhớ 1 lần — dùng counting semaphore hoặc queue nếu cần đếm.
2Counting semaphore
Counting semaphore có bộ đếm 0…max. Hai cách dùng:
- Quản lý tài nguyên: khởi tạo = số tài nguyên; take = mượn, give = trả (2 kênh DMA, 3 slot bộ đệm, 4 chỗ sạc).
- Đếm sự kiện: khởi tạo = 0; ISR give mỗi lần có sự kiện, task take để xử lý từng cái — không mất sự kiện.
// Bài 6 – Counting semaphore: quản lý một NHÓM tài nguyên giống nhau (ở đây: 2 kênh DMA / 2 chỗ sạc)
// và đếm sự kiện (nhiều ngắt dồn lại không bị mất).
#include "sim.h"
#include "semphr.h"
SemaphoreHandle_t semKenh; // 2 kênh truyền dữ liệu dùng chung
void taskGuiDuLieu(void* p) {
const char* ten = static_cast<const char*>(p);
LOG("%s xin một kênh (còn %u kênh rảnh)", ten, (unsigned)uxSemaphoreGetCount(semKenh));
xSemaphoreTake(semKenh, portMAX_DELAY); // giảm bộ đếm; nếu = 0 thì chờ
LOG("%s ĐƯỢC cấp kênh, gửi dữ liệu 300 ms", ten);
vTaskDelay(pdMS_TO_TICKS(300));
LOG("%s trả kênh", ten);
xSemaphoreGive(semKenh); // tăng bộ đếm
vTaskDelete(nullptr);
}
// ===== Đếm sự kiện: encoder phát nhiều xung liên tiếp =====
SemaphoreHandle_t semXung;
uint32_t isrEncoder() {
BaseType_t c = pdFALSE;
xSemaphoreGiveFromISR(semXung, &c);
return c;
}
void taskDemXung(void*) {
vTaskDelay(pdMS_TO_TICKS(800));
LOG("== Phần 2: 5 xung encoder tới dồn dập trong lúc task đang bận ==");
for (int i = 0; i < 5; i++) phatNgat(3);
vTaskDelay(pdMS_TO_TICKS(10));
int dem = 0;
while (xSemaphoreTake(semXung, 0) == pdTRUE) dem++;
LOG("DemXung xử lý được %d/5 xung (counting semaphore không làm mất xung)", dem);
ket_thuc();
}
int main() {
semKenh = xSemaphoreCreateCounting(2, 2); // tối đa 2, ban đầu 2 (cả hai kênh rảnh)
semXung = xSemaphoreCreateCounting(10, 0); // đếm sự kiện: ban đầu 0
dangKyNgat(3, isrEncoder);
static const char* ten[] = {"Task A", "Task B", "Task C", "Task D"};
for (int i = 0; i < 4; i++) xTaskCreate(taskGuiDuLieu, ten[i], 512, (void*)ten[i], 2, nullptr);
xTaskCreate(taskDemXung, "DemXung", 512, nullptr, 1, nullptr);
vTaskStartScheduler();
}
g++ -std=c++17 -Isim -Ikernel/include -Ikernel/portable/MSVC-MingW 09-counting-semaphore.cpp libfreertos.a -lwinmm -o app && ./app[ 1 ms] Task A xin một kênh (còn 2 kênh rảnh) [ 1 ms] Task A ĐƯỢC cấp kênh, gửi dữ liệu 300 ms [ 1 ms] Task B xin một kênh (còn 1 kênh rảnh) [ 1 ms] Task B ĐƯỢC cấp kênh, gửi dữ liệu 300 ms [ 1 ms] Task C xin một kênh (còn 0 kênh rảnh) [ 1 ms] Task D xin một kênh (còn 0 kênh rảnh) [ 301 ms] Task A trả kênh [ 302 ms] Task B trả kênh [ 303 ms] Task C ĐƯỢC cấp kênh, gửi dữ liệu 300 ms [ 303 ms] Task D ĐƯỢC cấp kênh, gửi dữ liệu 300 ms [ 603 ms] Task C trả kênh [ 604 ms] Task D trả kênh [ 801 ms] == Phần 2: 5 xung encoder tới dồn dập trong lúc task đang bận == [ 812 ms] DemXung xử lý được 5/5 xung (counting semaphore không làm mất xung)
Giải thích
- Task C, D phải chờ tới khi A, B trả kênh (301 ms).
- 5 xung encoder dồn dập đều được đếm đủ.
🔧Áp dụng trong lập trình nhúng
- Ngắt GPIO, ngắt UART, ngắt “DMA xong”, ngắt timer đều nên dùng mẫu deferred processing.
- Trên STM32: gọi
xSemaphoreGiveFromISRtrongHAL_GPIO_EXTI_Callback; ưu tiên ngắt phải ≥configMAX_SYSCALL_INTERRUPT_PRIORITY(số lớn hơn = ưu tiên thấp hơn trên Cortex-M) mới được gọi API FreeRTOS. - Task notification (bài 8) là cách nhẹ hơn để làm việc tương tự khi chỉ có một task nhận.
⚠Lỗi thường gặp
- Dùng semaphore để bảo vệ tài nguyên chung (nên dùng mutex — bài 7).
- Xử lý nặng ngay trong ISR.
- Quên kiểm tra giá trị trả về khi take có timeout.
✎Bài tập về nhà
🔐 Mutex, đảo ưu tiên, mutex đệ quy
Bảo vệ tài nguyên dùng chung bằng mutex, hiểu đảo ưu tiên và tránh deadlock.
- Nhận biết race condition trên tài nguyên chung (LCD, UART, I2C, biến toàn cục)
- Dùng mutex đúng: take/give trong cùng task
- Hiểu đảo ưu tiên và cơ chế kế thừa ưu tiên
- Dùng recursive mutex; tránh deadlock
1Mutex bảo vệ tài nguyên
Mutex (mutual exclusion) là “chìa khoá” duy nhất của một tài nguyên: task nào lấy được (xSemaphoreTake) mới được dùng, dùng xong phải trả (xSemaphoreGive). Khác semaphore: mutex có chủ sở hữu — chỉ task đã lấy mới được trả — và có kế thừa ưu tiên.
// Bài 7 – Mutex bảo vệ tài nguyên dùng chung (màn hình LCD).
// Ghi một dòng lên LCD gồm nhiều bước; nếu task khác chen vào giữa -> màn hình bị lẫn chữ.
#include <cstring>
#include "sim.h"
#include "semphr.h"
char lcd[2][17]; // màn hình 2 dòng x 16 ký tự (tài nguyên dùng chung)
SemaphoreHandle_t mutexLcd;
bool dungMutex = false;
void lcdVietDong(int dong, const char* s) { // ghi từng ký tự, mỗi ký tự mất 1 ms (giống LCD thật)
for (int i = 0; i < 16; i++) {
lcd[dong][i] = s[i] ? s[i] : ' ';
vTaskDelay(1); // tạo cơ hội cho task khác chen vào
if (!s[i]) { for (int k = i + 1; k < 16; k++) lcd[dong][k] = ' '; break; }
}
}
void hienThi(const char* d0, const char* d1) {
if (dungMutex) xSemaphoreTake(mutexLcd, portMAX_DELAY); // vào vùng găng
lcdVietDong(0, d0);
lcdVietDong(1, d1);
if (dungMutex) xSemaphoreGive(mutexLcd); // ra vùng găng – CHÍNH task đã take phải give
}
void taskNhietDo(void*) { for (;;) { hienThi("Nhiet do: 28.5C", "Do am : 61%"); vTaskDelay(5); } }
void taskCanhBao(void*) { for (;;) { hienThi("!! CANH BAO !!", "Khoi vuot nguong"); vTaskDelay(7); } }
void chup(const char* tieuDe) {
vTaskSuspendAll(); // chụp nội dung LCD khi không ai đang ghi dở
PRINT("%s\n +----------------+\n |%.16s|\n |%.16s|\n +----------------+", tieuDe, lcd[0], lcd[1]);
xTaskResumeAll();
}
void taskKiemTra(void*) {
vTaskDelay(pdMS_TO_TICKS(53));
chup("KHÔNG có mutex – hai task ghi xen kẽ:");
dungMutex = true;
vTaskDelay(pdMS_TO_TICKS(200));
xSemaphoreTake(mutexLcd, portMAX_DELAY); // chờ tới lúc không ai đang ghi
chup("CÓ mutex – mỗi lần chỉ một task ghi trọn vẹn:");
xSemaphoreGive(mutexLcd);
ket_thuc();
}
int main() {
std::memset(lcd, ' ', sizeof(lcd));
lcd[0][16] = lcd[1][16] = 0;
mutexLcd = xSemaphoreCreateMutex();
xTaskCreate(taskNhietDo, "NhietDo", 512, nullptr, 1, nullptr);
xTaskCreate(taskCanhBao, "CanhBao", 512, nullptr, 1, nullptr);
xTaskCreate(taskKiemTra, "KiemTra", 1024, nullptr, 2, nullptr);
vTaskStartScheduler();
}
g++ -std=c++17 -Isim -Ikernel/include -Ikernel/portable/MSVC-MingW 10-mutex-race.cpp libfreertos.a -lwinmm -o app && ./appKHÔNG có mutex – hai task ghi xen kẽ: +----------------+ |!! CANH BAO !! | |Do am : 61% g| +----------------+ CÓ mutex – mỗi lần chỉ một task ghi trọn vẹn: +----------------+ |!! CANH BAO !! | |Khoi vuot nguong| +----------------+
Giải thích
- Không mutex: dòng 2 của LCD bị lẫn “Do am : 61% g” — chữ “g” còn sót từ “Khoi vuot nguong”.
- Có mutex: mỗi task ghi trọn vẹn 2 dòng rồi mới tới task khác.
2Đảo ưu tiên và kế thừa ưu tiên
Đảo ưu tiên: task Cao chờ khoá do task Thap giữ; task Vua (không liên quan) chiếm CPU của Thap → Cao bị Vua chặn gián tiếp. Sự cố nổi tiếng: robot Mars Pathfinder (1997) liên tục reset vì lỗi này.
Mutex của FreeRTOS có kế thừa ưu tiên: khi Cao chờ, Thap tạm được nâng lên ưu tiên của Cao, làm xong nhanh rồi trả khoá; sau đó trở lại ưu tiên cũ.
// Bài 7 – Đảo ưu tiên (priority inversion) và kế thừa ưu tiên (priority inheritance).
// Thap (1) giữ khoá -> Cao (3) phải chờ khoá -> Vua (2) chen ngang Thap, làm Cao chờ lâu vô lý.
// Binary semaphore: KHÔNG kế thừa ưu tiên. Mutex: Thap được tạm nâng lên ưu tiên 3.
#include "sim.h"
#include "semphr.h"
SemaphoreHandle_t khoa;
TickType_t batDau;
void taskThap(void*) {
xSemaphoreTake(khoa, portMAX_DELAY);
LOG(" Thap (ƯT 1) lấy khoá, ghi thẻ SD 100 ms");
for (int i = 0; i < 100; i++) {
banTinhToan(1);
if (i == 50) LOG(" Thap đang giữ khoá, ưu tiên hiện tại = %u", (unsigned)uxTaskPriorityGet(nullptr));
}
LOG(" Thap trả khoá");
xSemaphoreGive(khoa);
vTaskDelete(nullptr);
}
void taskCao(void*) {
vTaskDelay(pdMS_TO_TICKS(10));
LOG("Cao (ƯT 3) cần khoá -> phải chờ Thap");
xSemaphoreTake(khoa, portMAX_DELAY);
LOG("Cao có khoá sau khi chờ %lu ms", (unsigned long)(xTaskGetTickCount() - batDau - 10));
xSemaphoreGive(khoa);
vTaskDelete(nullptr);
}
void taskVua(void*) {
vTaskDelay(pdMS_TO_TICKS(20));
LOG(" Vua (ƯT 2) không cần khoá, chạy 200 ms");
banTinhToan(200);
LOG(" Vua xong");
vTaskDelete(nullptr);
}
void chayThuNghiem(void* p) {
for (int lan = 0; lan < 2; lan++) {
bool dungMutex = (lan == 1);
khoa = dungMutex ? xSemaphoreCreateMutex() : xSemaphoreCreateBinary();
if (!dungMutex) xSemaphoreGive(khoa); // binary tạo ra ở trạng thái rỗng
LOG("========== Dùng %s ==========", dungMutex ? "MUTEX (có kế thừa ưu tiên)" : "BINARY SEMAPHORE (không kế thừa)");
batDau = xTaskGetTickCount();
xTaskCreate(taskThap, "Thap", 512, nullptr, 1, nullptr);
xTaskCreate(taskCao, "Cao", 512, nullptr, 3, nullptr);
xTaskCreate(taskVua, "Vua", 512, nullptr, 2, nullptr);
vTaskDelay(pdMS_TO_TICKS(450));
vSemaphoreDelete(khoa);
}
(void)p;
ket_thuc();
}
int main() {
xTaskCreate(chayThuNghiem, "DieuPhoi", 1024, nullptr, 4, nullptr);
vTaskStartScheduler();
}
g++ -std=c++17 -Isim -Ikernel/include -Ikernel/portable/MSVC-MingW 11-priority-inversion.cpp libfreertos.a -lwinmm -o app && ./app[ 1 ms] ========== Dùng BINARY SEMAPHORE (không kế thừa) ========== [ 1 ms] Thap (ƯT 1) lấy khoá, ghi thẻ SD 100 ms [ 11 ms] Cao (ƯT 3) cần khoá -> phải chờ Thap [ 21 ms] Vua (ƯT 2) không cần khoá, chạy 200 ms [ 221 ms] Vua xong [ 253 ms] Thap đang giữ khoá, ưu tiên hiện tại = 1 [ 302 ms] Thap trả khoá [ 302 ms] Cao có khoá sau khi chờ 291 ms [ 451 ms] ========== Dùng MUTEX (có kế thừa ưu tiên) ========== [ 451 ms] Thap (ƯT 1) lấy khoá, ghi thẻ SD 100 ms [ 461 ms] Cao (ƯT 3) cần khoá -> phải chờ Thap [ 502 ms] Thap đang giữ khoá, ưu tiên hiện tại = 3 [ 551 ms] Thap trả khoá [ 551 ms] Cao có khoá sau khi chờ 90 ms [ 552 ms] Vua (ƯT 2) không cần khoá, chạy 200 ms [ 752 ms] Vua xong
Giải thích
- Binary semaphore: Cao chờ 291 ms. Mutex: chỉ 90 ms.
- Dòng “ưu tiên hiện tại = 3” chứng minh Thap đã được nâng ưu tiên.
3Mutex đệ quy và deadlock
Recursive mutex cho phép cùng một task lấy khoá nhiều lần (hàm gọi lồng nhau cùng bảo vệ một tài nguyên); phải trả đủ số lần đã lấy. Deadlock xảy ra khi các task chờ nhau mãi mãi — ví dụ A giữ khoá 1 chờ khoá 2, B giữ khoá 2 chờ khoá 1. Phòng tránh: luôn lấy các khoá theo cùng một thứ tự, dùng timeout, giữ khoá thật ngắn.
// Bài 7 – Mutex đệ quy (recursive mutex): một task lấy CÙNG một khoá nhiều lần (hàm gọi lồng nhau).
// Với mutex thường, lần lấy thứ hai sẽ tự khoá chính mình (deadlock).
#include "sim.h"
#include "semphr.h"
SemaphoreHandle_t mutexThuong, mutexDeQuy;
void ghiByte(uint8_t b) { // hàm cấp thấp: tự khoá bus I2C
xSemaphoreTakeRecursive(mutexDeQuy, portMAX_DELAY);
LOG(" ghiByte(0x%02X) – đang giữ khoá", b);
xSemaphoreGiveRecursive(mutexDeQuy);
}
void ghiGoiTin(const uint8_t* d, int n) { // hàm cấp cao: khoá cả gói, rồi gọi hàm cấp thấp (cũng khoá)
xSemaphoreTakeRecursive(mutexDeQuy, portMAX_DELAY);
LOG(" ghiGoiTin: lấy khoá lần 1");
for (int i = 0; i < n; i++) ghiByte(d[i]); // lấy khoá lần 2, 3, ... trong cùng task – hợp lệ
xSemaphoreGiveRecursive(mutexDeQuy); // số lần give phải bằng số lần take
LOG(" ghiGoiTin: trả khoá – task khác giờ mới lấy được");
}
void taskChinh(void*) {
LOG("== Recursive mutex ==");
const uint8_t goi[] = {0x3C, 0xA5, 0x01};
ghiGoiTin(goi, 3);
LOG("== Mutex thường bị lấy 2 lần trong cùng task ==");
xSemaphoreTake(mutexThuong, portMAX_DELAY);
LOG(" lấy lần 1: OK");
BaseType_t ok = xSemaphoreTake(mutexThuong, pdMS_TO_TICKS(200)); // chờ chính mình -> không bao giờ được
LOG(" lấy lần 2 (chờ tối đa 200 ms): %s", ok ? "OK" : "THẤT BẠI – nếu chờ portMAX_DELAY sẽ treo vĩnh viễn (deadlock)");
xSemaphoreGive(mutexThuong);
ket_thuc();
}
int main() {
mutexThuong = xSemaphoreCreateMutex();
mutexDeQuy = xSemaphoreCreateRecursiveMutex();
xTaskCreate(taskChinh, "Chinh", 1024, nullptr, 1, nullptr);
vTaskStartScheduler();
}
g++ -std=c++17 -Isim -Ikernel/include -Ikernel/portable/MSVC-MingW 12-recursive-mutex.cpp libfreertos.a -lwinmm -o app && ./app[ 1 ms] == Recursive mutex == [ 1 ms] ghiGoiTin: lấy khoá lần 1 [ 1 ms] ghiByte(0x3C) – đang giữ khoá [ 1 ms] ghiByte(0xA5) – đang giữ khoá [ 1 ms] ghiByte(0x01) – đang giữ khoá [ 1 ms] ghiGoiTin: trả khoá – task khác giờ mới lấy được [ 1 ms] == Mutex thường bị lấy 2 lần trong cùng task == [ 1 ms] lấy lần 1: OK [ 201 ms] lấy lần 2 (chờ tối đa 200 ms): THẤT BẠI – nếu chờ portMAX_DELAY sẽ treo vĩnh viễn (deadlock)
Giải thích
- Mutex thường bị lấy lần 2 trong cùng task: chờ chính mình → thất bại sau 200 ms (với portMAX_DELAY sẽ treo vĩnh viễn).
🔧Áp dụng trong lập trình nhúng
- Mọi bus dùng chung (I2C, SPI, UART log) cần một mutex nếu nhiều task truy cập.
- Không dùng mutex trong ISR (mutex có chủ sở hữu, ISR không phải task) — dùng semaphore hoặc notification.
- Arduino-ESP32:
Serial.printtừ nhiều task có thể bị lẫn chữ — bọc bằng mutex hoặc gom log về một task.
⚠Lỗi thường gặp
- Take mutex ở task này, give ở task khác.
- Giữ mutex trong lúc
vTaskDelaydài. - Lấy hai mutex theo thứ tự khác nhau ở hai task → deadlock.
✎Bài tập về nhà
🔔 Direct-to-task notification
Dùng notification — cơ chế báo hiệu nhẹ và nhanh nhất của FreeRTOS — thay cho semaphore, event group, mailbox khi chỉ có một task nhận.
- Hiểu notification nằm sẵn trong TCB của mỗi task
- Dùng như binary/counting semaphore
- Dùng như event group (bit) và mailbox (ghi đè)
1Notification thay semaphore
Mỗi task có sẵn một giá trị 32 bit + trạng thái “đang chờ”. Bên gửi tác động trực tiếp lên task nhận (cần handle), không qua đối tượng trung gian → nhanh hơn và không tốn RAM. Giới hạn: chỉ một task nhận; không thể “phát” cho nhiều task.
// Bài 8 – Direct-to-task notification dùng như binary semaphore và counting semaphore.
// Mỗi task có sẵn một "hộp thư" 32 bit: không cần tạo đối tượng, nhanh hơn và tốn ít RAM hơn semaphore.
#include "sim.h"
#include "semphr.h"
TaskHandle_t hXuLy = nullptr;
uint32_t isrAdcXong() { // ngắt "ADC chuyển đổi xong"
BaseType_t c = pdFALSE;
vTaskNotifyGiveFromISR(hXuLy, &c); // tăng giá trị notification của task lên 1
return c;
}
void taskXuLy(void*) {
LOG("== Như BINARY semaphore: ulTaskNotifyTake(pdTRUE, ...) xoá về 0 sau khi nhận ==");
for (int i = 0; i < 2; i++) {
uint32_t n = ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
LOG("XuLy thức dậy, giá trị notification = %lu -> đọc ADC", (unsigned long)n);
}
LOG("== Như COUNTING semaphore: ulTaskNotifyTake(pdFALSE, ...) giảm đi 1 mỗi lần ==");
vTaskDelay(pdMS_TO_TICKS(200)); // trong lúc ngủ, 3 ngắt dồn lại
for (int i = 0; i < 3; i++) {
uint32_t n = ulTaskNotifyTake(pdFALSE, 0);
LOG("XuLy xử lý 1 sự kiện (còn lại %lu)", (unsigned long)(n - 1));
}
LOG("Kích thước: một binary semaphore ~%u byte RAM; notification: 0 byte thêm (nằm sẵn trong TCB)",
(unsigned)sizeof(StaticSemaphore_t));
ket_thuc();
}
void taskPhanCung(void*) {
vTaskDelay(pdMS_TO_TICKS(100)); LOG("[ngắt] ADC xong"); phatNgat(3);
vTaskDelay(pdMS_TO_TICKS(100)); LOG("[ngắt] ADC xong"); phatNgat(3);
vTaskDelay(pdMS_TO_TICKS(100));
for (int i = 0; i < 3; i++) { LOG("[ngắt] ADC xong (dồn)"); phatNgat(3); vTaskDelay(pdMS_TO_TICKS(20)); }
vTaskDelete(nullptr);
}
int main() {
dangKyNgat(3, isrAdcXong);
xTaskCreate(taskXuLy, "XuLy", 1024, nullptr, 2, &hXuLy);
xTaskCreate(taskPhanCung, "PhanCung", 512, nullptr, 3, nullptr);
vTaskStartScheduler();
}
g++ -std=c++17 -Isim -Ikernel/include -Ikernel/portable/MSVC-MingW 13-notify-semaphore.cpp libfreertos.a -lwinmm -o app && ./app[ 1 ms] == Như BINARY semaphore: ulTaskNotifyTake(pdTRUE, ...) xoá về 0 sau khi nhận == [ 101 ms] [ngắt] ADC xong [ 101 ms] XuLy thức dậy, giá trị notification = 1 -> đọc ADC [ 201 ms] [ngắt] ADC xong [ 201 ms] XuLy thức dậy, giá trị notification = 1 -> đọc ADC [ 201 ms] == Như COUNTING semaphore: ulTaskNotifyTake(pdFALSE, ...) giảm đi 1 mỗi lần == [ 301 ms] [ngắt] ADC xong (dồn) [ 321 ms] [ngắt] ADC xong (dồn) [ 341 ms] [ngắt] ADC xong (dồn) [ 401 ms] XuLy xử lý 1 sự kiện (còn lại 2) [ 401 ms] XuLy xử lý 1 sự kiện (còn lại 1) [ 401 ms] XuLy xử lý 1 sự kiện (còn lại 0) [ 401 ms] Kích thước: một binary semaphore ~168 byte RAM; notification: 0 byte thêm (nằm sẵn trong TCB)
Giải thích
ulTaskNotifyTake(pdTRUE, ...): xoá về 0 khi nhận → giống binary semaphore.ulTaskNotifyTake(pdFALSE, ...): giảm 1 → giống counting semaphore, 3 ngắt dồn lại được xử lý đủ 3 lần.
2Notification như event group và mailbox
eAction trong xTaskNotify | Hành vi | Tương đương |
|---|---|---|
eSetBits | OR bit vào giá trị | event group |
eIncrement | tăng 1 | counting semaphore |
eSetValueWithOverwrite | ghi đè giá trị | mailbox (chỉ cần giá trị mới nhất) |
eSetValueWithoutOverwrite | chỉ ghi nếu giá trị cũ đã được đọc | queue 1 phần tử |
// Bài 8 – Task notification dùng như EVENT GROUP (từng bit là một sự kiện) và như MAILBOX (ghi đè giá trị mới nhất)
#include "sim.h"
constexpr uint32_t BIT_WIFI = 1u << 0, BIT_NUT = 1u << 1, BIT_PIN_YEU = 1u << 2;
TaskHandle_t hQuanLy = nullptr, hHienThi = nullptr;
void taskQuanLy(void*) {
for (int i = 0; i < 3; i++) {
uint32_t bits = 0;
// Chờ bất kỳ bit nào; khi thoát xoá hết các bit (0xFFFFFFFF)
xTaskNotifyWait(0, 0xFFFFFFFF, &bits, portMAX_DELAY);
LOG("QuanLy nhận bits = 0x%lX:%s%s%s", (unsigned long)bits,
(bits & BIT_WIFI) ? " [WiFi đã kết nối]" : "", (bits & BIT_NUT) ? " [Nút nhấn]" : "",
(bits & BIT_PIN_YEU) ? " [Pin yếu]" : "");
}
vTaskDelete(nullptr);
}
void taskWifi(void*) { vTaskDelay(pdMS_TO_TICKS(100)); xTaskNotify(hQuanLy, BIT_WIFI, eSetBits); vTaskDelete(nullptr); }
void taskNut(void*) {
vTaskDelay(pdMS_TO_TICKS(250));
xTaskNotify(hQuanLy, BIT_NUT, eSetBits);
xTaskNotify(hQuanLy, BIT_PIN_YEU, eSetBits); // hai sự kiện gần như cùng lúc -> gộp trong một lần nhận
vTaskDelay(pdMS_TO_TICKS(100));
xTaskNotify(hQuanLy, BIT_NUT, eSetBits);
vTaskDelete(nullptr);
}
// ===== Mailbox: chỉ cần giá trị MỚI NHẤT, giá trị cũ chưa đọc thì ghi đè =====
void taskCamBien(void*) {
vTaskDelay(pdMS_TO_TICKS(500));
for (int t = 280; t <= 300; t += 4) {
xTaskNotify(hHienThi, t, eSetValueWithOverwrite);
LOG("CamBien ghi %d vào mailbox", t);
vTaskDelay(pdMS_TO_TICKS(50));
}
vTaskDelete(nullptr);
}
void taskHienThi(void*) {
vTaskDelay(pdMS_TO_TICKS(500));
for (int i = 0; i < 3; i++) {
uint32_t v;
xTaskNotifyWait(0, 0, &v, portMAX_DELAY);
LOG(" HienThi LCD cập nhật chậm (120 ms/lần), đọc giá trị mới nhất: %lu", (unsigned long)v);
vTaskDelay(pdMS_TO_TICKS(120));
}
ket_thuc();
}
int main() {
xTaskCreate(taskQuanLy, "QuanLy", 512, nullptr, 3, &hQuanLy);
xTaskCreate(taskWifi, "Wifi", 512, nullptr, 2, nullptr);
xTaskCreate(taskNut, "Nut", 512, nullptr, 4, nullptr); // ưu tiên cao hơn QuanLy: gửi xong cả 2 bit rồi QuanLy mới chạy
xTaskCreate(taskHienThi, "HienThi", 512, nullptr, 1, &hHienThi);
xTaskCreate(taskCamBien, "CamBien", 512, nullptr, 2, nullptr);
vTaskStartScheduler();
}
g++ -std=c++17 -Isim -Ikernel/include -Ikernel/portable/MSVC-MingW 14-notify-bits-mailbox.cpp libfreertos.a -lwinmm -o app && ./app[ 101 ms] QuanLy nhận bits = 0x1: [WiFi đã kết nối] [ 251 ms] QuanLy nhận bits = 0x6: [Nút nhấn] [Pin yếu] [ 352 ms] QuanLy nhận bits = 0x2: [Nút nhấn] [ 501 ms] CamBien ghi 280 vào mailbox [ 501 ms] HienThi LCD cập nhật chậm (120 ms/lần), đọc giá trị mới nhất: 280 [ 551 ms] CamBien ghi 284 vào mailbox [ 601 ms] CamBien ghi 288 vào mailbox [ 621 ms] HienThi LCD cập nhật chậm (120 ms/lần), đọc giá trị mới nhất: 288 [ 651 ms] CamBien ghi 292 vào mailbox [ 701 ms] CamBien ghi 296 vào mailbox [ 741 ms] HienThi LCD cập nhật chậm (120 ms/lần), đọc giá trị mới nhất: 296 [ 751 ms] CamBien ghi 300 vào mailbox
Giải thích
- Hai sự kiện Nút + Pin yếu tới gần như cùng lúc được gộp thành bits = 0x6 trong một lần nhận.
- Mailbox: LCD chậm chỉ hiển thị giá trị mới nhất (288, 296), các giá trị trung gian bị ghi đè — đúng ý muốn.
🔧Áp dụng trong lập trình nhúng
- ESP-IDF dùng notification rất nhiều trong driver. Trên ESP32 Arduino:
xTaskNotifyGive(handle)từ task,vTaskNotifyGiveFromISRtừ ngắt. - Chọn: chỉ 1 task nhận → notification; nhiều task cùng chờ → semaphore/event group; cần đệm nhiều dữ liệu → queue.
⚠Lỗi thường gặp
- Gửi notification trước khi có handle của task nhận (handle còn NULL).
- Dùng notification khi có nhiều task cần nhận cùng sự kiện.
✎Bài tập về nhà
🎌 Event group – chờ nhiều sự kiện, đồng bộ nhiều task
Chờ tổ hợp nhiều sự kiện (AND/OR) và đồng bộ nhiều task tại một điểm.
- Đặt/xoá/chờ bit với
xEventGroupSetBits,xEventGroupWaitBits - Phân biệt chờ tất cả (AND) và chờ bất kỳ (OR)
- Dùng
xEventGroupSyncđể đồng bộ (rendezvous)
1Event group
Event group là tập các bit sự kiện (24 bit dùng được trên chip 32 bit). Khác notification: nhiều task có thể cùng chờ một event group. Tham số của xEventGroupWaitBits: bit cần chờ, có xoá bit khi thoát không, chờ tất cả hay bất kỳ, timeout.
// Bài 9 – Event group: chờ NHIỀU sự kiện (AND / OR) và đồng bộ nhiều task tại một điểm (rendezvous)
#include <initializer_list>
#include "sim.h"
#include "event_groups.h"
EventGroupHandle_t egKhoiDong, egDongBo;
constexpr EventBits_t BIT_WIFI = 1 << 0, BIT_SD = 1 << 1, BIT_CAM_BIEN = 1 << 2, BIT_LOI = 1 << 3;
constexpr EventBits_t TAT_CA = BIT_WIFI | BIT_SD | BIT_CAM_BIEN;
void taskKhoiTao(void* p) {
auto bit = static_cast<EventBits_t>(reinterpret_cast<uintptr_t>(p));
const char* ten = bit == BIT_WIFI ? "WiFi" : bit == BIT_SD ? "Thẻ SD" : "Cảm biến";
uint32_t thoiGian = bit == BIT_WIFI ? 300 : bit == BIT_SD ? 120 : 200;
vTaskDelay(pdMS_TO_TICKS(thoiGian));
LOG(" %s sẵn sàng -> đặt bit 0x%lX", ten, (unsigned long)bit);
xEventGroupSetBits(egKhoiDong, bit);
vTaskDelete(nullptr);
}
void taskUngDung(void*) {
LOG("UngDung chờ TẤT CẢ: WiFi + SD + cảm biến (xWaitForAllBits = pdTRUE)");
EventBits_t b = xEventGroupWaitBits(egKhoiDong, TAT_CA, pdFALSE, pdTRUE, portMAX_DELAY);
LOG("UngDung bits = 0x%lX -> hệ thống sẵn sàng, bắt đầu ghi log", (unsigned long)b);
LOG("UngDung chờ BẤT KỲ: lỗi hoặc ... (xWaitForAllBits = pdFALSE), tối đa 200 ms");
b = xEventGroupWaitBits(egKhoiDong, BIT_LOI, pdTRUE, pdFALSE, pdMS_TO_TICKS(200));
LOG("UngDung hết giờ, bit lỗi = %s", (b & BIT_LOI) ? "CÓ" : "không");
vTaskDelete(nullptr);
}
// ===== Rendezvous: 3 task (3 động cơ) phải cùng tới vạch xuất phát rồi mới chạy =====
constexpr EventBits_t DC1 = 1 << 0, DC2 = 1 << 1, DC3 = 1 << 2;
void taskDongCo(void* p) {
int so = (int)reinterpret_cast<uintptr_t>(p);
EventBits_t bitCuaToi = 1 << (so - 1);
vTaskDelay(pdMS_TO_TICKS(800 + so * 70)); // mỗi động cơ hiệu chỉnh mất thời gian khác nhau
LOG(" Động cơ %d hiệu chỉnh xong, chờ các động cơ khác", so);
xEventGroupSync(egDongBo, bitCuaToi, DC1 | DC2 | DC3, portMAX_DELAY);
LOG(" Động cơ %d XUẤT PHÁT", so);
if (so == 3) { vTaskDelay(pdMS_TO_TICKS(10)); ket_thuc(); }
vTaskDelete(nullptr);
}
int main() {
egKhoiDong = xEventGroupCreate();
egDongBo = xEventGroupCreate();
xTaskCreate(taskUngDung, "UngDung", 512, nullptr, 3, nullptr);
for (EventBits_t b : {BIT_WIFI, BIT_SD, BIT_CAM_BIEN})
xTaskCreate(taskKhoiTao, "KhoiTao", 512, reinterpret_cast<void*>(uintptr_t(b)), 2, nullptr);
for (int i = 1; i <= 3; i++) xTaskCreate(taskDongCo, "DongCo", 512, reinterpret_cast<void*>(uintptr_t(i)), 2, nullptr);
vTaskStartScheduler();
}
g++ -std=c++17 -Isim -Ikernel/include -Ikernel/portable/MSVC-MingW 15-event-group.cpp libfreertos.a -lwinmm -o app && ./app[ 1 ms] UngDung chờ TẤT CẢ: WiFi + SD + cảm biến (xWaitForAllBits = pdTRUE) [ 121 ms] Thẻ SD sẵn sàng -> đặt bit 0x2 [ 201 ms] Cảm biến sẵn sàng -> đặt bit 0x4 [ 301 ms] WiFi sẵn sàng -> đặt bit 0x1 [ 301 ms] UngDung bits = 0x7 -> hệ thống sẵn sàng, bắt đầu ghi log [ 301 ms] UngDung chờ BẤT KỲ: lỗi hoặc ... (xWaitForAllBits = pdFALSE), tối đa 200 ms [ 501 ms] UngDung hết giờ, bit lỗi = không [ 871 ms] Động cơ 1 hiệu chỉnh xong, chờ các động cơ khác [ 941 ms] Động cơ 2 hiệu chỉnh xong, chờ các động cơ khác [ 1011 ms] Động cơ 3 hiệu chỉnh xong, chờ các động cơ khác [ 1011 ms] Động cơ 3 XUẤT PHÁT [ 1011 ms] Động cơ 1 XUẤT PHÁT [ 1012 ms] Động cơ 2 XUẤT PHÁT
Giải thích
- Ứng dụng chỉ chạy khi cả WiFi, SD, cảm biến đều sẵn sàng (301 ms) — dù chúng xong theo thứ tự khác nhau.
xEventGroupSync: ba động cơ hiệu chỉnh xong ở 871/941/1011 ms nhưng cùng XUẤT PHÁT lúc 1011 ms.
🔧Áp dụng trong lập trình nhúng
- ESP-IDF dùng event group cho WiFi:
WIFI_CONNECTED_BIT,WIFI_FAIL_BIT— ví dụ station chuẩn chờ bằngxEventGroupWaitBits. - Không đặt bit từ ISR trực tiếp: dùng
xEventGroupSetBitsFromISR(nó chuyển việc cho timer daemon).
⚠Lỗi thường gặp
- Quên xoá bit sau khi xử lý → task chạy lặp vô hạn.
- Dùng nhiều hơn 24 bit trên chip 32 bit.
✎Bài tập về nhà
🌊 Stream buffer và message buffer
Truyền dòng byte (UART, âm thanh) và thông điệp độ dài thay đổi từ ngắt tới task, giữa hai nhân CPU.
- Dùng stream buffer cho dữ liệu byte liên tục, hiểu trigger level
- Dùng message buffer cho thông điệp có độ dài thay đổi
- Biết giới hạn: một bên ghi, một bên đọc
1Stream buffer: ISR → task
Stream buffer chuyên chở dòng byte: ghi bao nhiêu byte cũng được, đọc bao nhiêu tuỳ ý. Trigger level: task đang chờ chỉ được đánh thức khi buffer có ít nhất N byte — giảm số lần chuyển ngữ cảnh.
// Bài 10 – Stream buffer: chuyển luồng BYTE từ ngắt (UART nhận dữ liệu GPS) sang task.
// Một bên ghi – một bên đọc, không cần mutex. Task được đánh thức khi đủ "trigger level" byte.
#include <cstring>
#include "sim.h"
#include "stream_buffer.h"
StreamBufferHandle_t sbUart;
const char DU_LIEU_GPS[] = "$GPGGA,21.0285N,105.8542E*4A\n$GPGGA,21.0290N,105.8551E*47\n";
volatile size_t viTriPhat = 0;
uint32_t isrUartRx() { // mỗi ngắt "nhận" 4 byte từ module GPS
BaseType_t c = pdFALSE;
size_t con = std::strlen(DU_LIEU_GPS) - viTriPhat;
size_t n = con < 4 ? con : 4;
xStreamBufferSendFromISR(sbUart, DU_LIEU_GPS + viTriPhat, n, &c);
viTriPhat += n;
return c;
}
void taskPhanCung(void*) { // UART tạo ngắt mỗi 10 ms
while (viTriPhat < std::strlen(DU_LIEU_GPS)) {
phatNgat(3);
vTaskDelay(pdMS_TO_TICKS(10));
}
vTaskDelete(nullptr);
}
void taskDocGps(void*) {
char dong[64];
size_t len = 0;
for (;;) {
char buf[16];
size_t n = xStreamBufferReceive(sbUart, buf, sizeof(buf), pdMS_TO_TICKS(200)); // nhận tối đa 16 byte
if (n == 0) { LOG("DocGps không còn dữ liệu"); ket_thuc(); }
LOG("DocGps nhận khối %2u byte", (unsigned)n);
for (size_t i = 0; i < n; i++) {
if (buf[i] == '\n') { dong[len] = 0; LOG("DocGps >>> câu NMEA hoàn chỉnh: %s", dong); len = 0; }
else if (len < sizeof(dong) - 1) dong[len++] = buf[i];
}
}
}
int main() {
sbUart = xStreamBufferCreate(128, 12); // dung lượng 128 byte, đánh thức task khi có >= 12 byte
dangKyNgat(3, isrUartRx);
xTaskCreate(taskDocGps, "DocGps", 1024, nullptr, 2, nullptr);
xTaskCreate(taskPhanCung, "PhanCung", 512, nullptr, 3, nullptr);
vTaskStartScheduler();
}
g++ -std=c++17 -Isim -Ikernel/include -Ikernel/portable/MSVC-MingW 16-stream-buffer.cpp libfreertos.a -lwinmm -o app && ./app[ 1 ms] DocGps nhận khối 4 byte [ 31 ms] DocGps nhận khối 12 byte [ 61 ms] DocGps nhận khối 12 byte [ 91 ms] DocGps nhận khối 12 byte [ 91 ms] DocGps >>> câu NMEA hoàn chỉnh: $GPGGA,21.0285N,105.8542E*4A [ 121 ms] DocGps nhận khối 12 byte [ 321 ms] DocGps nhận khối 6 byte [ 321 ms] DocGps >>> câu NMEA hoàn chỉnh: $GPGGA,21.0290N,105.8551E*47 [ 521 ms] DocGps không còn dữ liệu
Giải thích
- Ngắt UART mỗi 10 ms đẩy 4 byte; task được đánh thức theo khối 12 byte (trigger level).
- Khối đầu chỉ 4 byte vì dữ liệu đã có sẵn khi task gọi receive; khối cuối 6 byte nhận được sau khi chờ hết timeout 200 ms.
- Task tự ghép byte thành câu NMEA hoàn chỉnh khi gặp ký tự xuống dòng.
2Message buffer: thông điệp nguyên vẹn
Message buffer xây trên stream buffer, mỗi thông điệp được lưu kèm độ dài (sizeof(size_t) byte) → bên nhận luôn nhận trọn một thông điệp. FreeRTOS gợi ý dùng message buffer để trao đổi dữ liệu giữa hai nhân trên chip đa nhân không đối xứng (AMP).
// Bài 10 – Message buffer: gửi các THÔNG ĐIỆP có độ dài thay đổi; bên nhận luôn nhận trọn từng thông điệp.
// Đây cũng là cơ chế FreeRTOS gợi ý để trao đổi dữ liệu giữa 2 nhân (core-to-core) trên chip đa nhân.
#include <cstring>
#include "sim.h"
#include "message_buffer.h"
MessageBufferHandle_t mbLenh;
void taskBluetooth(void*) { // "nhân 0": nhận lệnh từ điện thoại
const char* lenh[] = {"LED ON", "SERVO 90", "PLAY_MELODY twinkle_twinkle_little_star", "STOP"};
for (const char* l : lenh) {
size_t n = xMessageBufferSend(mbLenh, l, std::strlen(l), portMAX_DELAY);
LOG("Bluetooth gửi thông điệp %2u byte: \"%s\"", (unsigned)n, l);
vTaskDelay(pdMS_TO_TICKS(30));
}
vTaskDelete(nullptr);
}
void taskThucThi(void*) { // "nhân 1": thực thi lệnh
vTaskDelay(pdMS_TO_TICKS(100)); // bận lúc đầu -> các thông điệp dồn lại
LOG("ThucThi còn trống %u byte trong buffer", (unsigned)xMessageBufferSpacesAvailable(mbLenh));
char buf[64];
size_t n;
while ((n = xMessageBufferReceive(mbLenh, buf, sizeof(buf) - 1, pdMS_TO_TICKS(200))) > 0) {
buf[n] = 0;
LOG("ThucThi nhận nguyên vẹn %2u byte: \"%s\"", (unsigned)n, buf);
}
LOG("ThucThi hết thông điệp (mỗi thông điệp tốn thêm %u byte lưu độ dài)", (unsigned)sizeof(size_t));
ket_thuc();
}
int main() {
mbLenh = xMessageBufferCreate(200);
xTaskCreate(taskBluetooth, "Bluetooth", 512, nullptr, 2, nullptr);
xTaskCreate(taskThucThi, "ThucThi", 1024, nullptr, 1, nullptr);
vTaskStartScheduler();
}
g++ -std=c++17 -Isim -Ikernel/include -Ikernel/portable/MSVC-MingW 17-message-buffer.cpp libfreertos.a -lwinmm -o app && ./app[ 1 ms] Bluetooth gửi thông điệp 6 byte: "LED ON" [ 31 ms] Bluetooth gửi thông điệp 8 byte: "SERVO 90" [ 61 ms] Bluetooth gửi thông điệp 39 byte: "PLAY_MELODY twinkle_twinkle_little_star" [ 91 ms] Bluetooth gửi thông điệp 4 byte: "STOP" [ 101 ms] ThucThi còn trống 111 byte trong buffer [ 101 ms] ThucThi nhận nguyên vẹn 6 byte: "LED ON" [ 101 ms] ThucThi nhận nguyên vẹn 8 byte: "SERVO 90" [ 101 ms] ThucThi nhận nguyên vẹn 39 byte: "PLAY_MELODY twinkle_twinkle_little_star" [ 101 ms] ThucThi nhận nguyên vẹn 4 byte: "STOP" [ 301 ms] ThucThi hết thông điệp (mỗi thông điệp tốn thêm 8 byte lưu độ dài)
Giải thích
- 4 lệnh có độ dài 6, 8, 39, 4 byte được nhận nguyên vẹn đúng thứ tự.
- Buffer 200 byte còn trống 111 byte sau 4 thông điệp (57 byte dữ liệu + 4 × 8 byte độ dài).
🔧Áp dụng trong lập trình nhúng
- Nhận dữ liệu GPS, RS485/Modbus, mic I2S: ISR/DMA ghi vào stream buffer, task xử lý.
- Chỉ đúng MỘT task/ISR ghi và MỘT task đọc; nhiều bên ghi phải tự bảo vệ bằng vùng găng/mutex.
⚠Lỗi thường gặp
- Nhiều task cùng ghi vào một stream buffer không bảo vệ.
- Đọc message buffer với bộ đệm nhỏ hơn thông điệp → không lấy được gì.
✎Bài tập về nhà
⏲ Software timer
Gọi hàm sau một khoảng thời gian hoặc định kỳ mà không cần tạo task riêng.
- Hiểu timer daemon task và hàng đợi lệnh timer
- Tạo timer one-shot và auto-reload, dùng timer ID
- Đổi chu kỳ, dừng, reset timer — mẫu “timeout không hoạt động”
1Timer daemon và các loại timer
Software timer không chạy trong ngắt: mọi callback được gọi bởi timer daemon task (“Tmr Svc”). Lệnh xTimerStart/Stop/Reset/ChangePeriod được gửi qua timer command queue. Cấu hình: configUSE_TIMERS, configTIMER_TASK_PRIORITY, configTIMER_QUEUE_LENGTH, configTIMER_TASK_STACK_DEPTH.
| Loại | Hành vi | Ví dụ |
|---|---|---|
| One-shot | gọi callback một lần | tắt bơm sau 5 phút, timeout kết nối |
| Auto-reload | gọi lặp lại theo chu kỳ | nháy LED, gửi heartbeat, lấy mẫu chậm |
// Bài 11 – Software timer: one-shot và auto-reload, timer ID, đổi chu kỳ, dừng timer.
// Callback chạy trong task "Tmr Svc" (timer daemon) – KHÔNG được chặn (không vTaskDelay, không chờ lâu).
#include "sim.h"
#include "timers.h"
TimerHandle_t tmrNhay, tmrTatBom, tmrDemNguoc;
void cbNhayLed(TimerHandle_t t) { // auto-reload: gọi lặp lại
static bool led = false;
led = !led;
LOG(" [timer NhayLed] LED %s (chạy trong task: %s)", led ? "BẬT" : "TẮT", pcTaskGetName(nullptr));
(void)t;
}
void cbTatBom(TimerHandle_t) { // one-shot: chỉ gọi một lần
LOG(" [timer TatBom] ĐÃ TƯỚI ĐỦ 500 ms -> tắt bơm");
}
void cbDemNguoc(TimerHandle_t t) { // dùng timer ID để lưu bộ đếm riêng của timer
uintptr_t con = reinterpret_cast<uintptr_t>(pvTimerGetTimerID(t));
LOG(" [timer DemNguoc] còn %u", (unsigned)con);
if (con == 0) { xTimerStop(t, 0); LOG(" [timer DemNguoc] dừng timer"); return; }
vTimerSetTimerID(t, reinterpret_cast<void*>(con - 1));
}
void taskChinh(void*) {
LOG("Chinh bật bơm, khởi động các timer");
xTimerStart(tmrNhay, 0);
xTimerStart(tmrTatBom, 0);
vTaskDelay(pdMS_TO_TICKS(700));
LOG("Chinh đổi chu kỳ nháy LED 200 ms -> 80 ms");
xTimerChangePeriod(tmrNhay, pdMS_TO_TICKS(80), 0);
vTaskDelay(pdMS_TO_TICKS(300));
xTimerStop(tmrNhay, 0);
LOG("Chinh dừng timer nháy; timer TatBom còn hoạt động? %s", xTimerIsTimerActive(tmrTatBom) ? "có" : "không");
xTimerStart(tmrDemNguoc, 0);
vTaskDelay(pdMS_TO_TICKS(450));
ket_thuc();
}
int main() {
// xTimerCreate(tên, chu kỳ, auto-reload?, ID, callback)
tmrNhay = xTimerCreate("NhayLed", pdMS_TO_TICKS(200), pdTRUE, nullptr, cbNhayLed);
tmrTatBom = xTimerCreate("TatBom", pdMS_TO_TICKS(500), pdFALSE, nullptr, cbTatBom);
tmrDemNguoc = xTimerCreate("DemNguoc", pdMS_TO_TICKS(100), pdTRUE, reinterpret_cast<void*>(3), cbDemNguoc);
xTaskCreate(taskChinh, "Chinh", 1024, nullptr, 1, nullptr);
vTaskStartScheduler();
}
g++ -std=c++17 -Isim -Ikernel/include -Ikernel/portable/MSVC-MingW 18-software-timer.cpp libfreertos.a -lwinmm -o app && ./app[ 1 ms] Chinh bật bơm, khởi động các timer [ 201 ms] [timer NhayLed] LED BẬT (chạy trong task: Tmr Svc) [ 401 ms] [timer NhayLed] LED TẮT (chạy trong task: Tmr Svc) [ 501 ms] [timer TatBom] ĐÃ TƯỚI ĐỦ 500 ms -> tắt bơm [ 601 ms] [timer NhayLed] LED BẬT (chạy trong task: Tmr Svc) [ 701 ms] Chinh đổi chu kỳ nháy LED 200 ms -> 80 ms [ 781 ms] [timer NhayLed] LED TẮT (chạy trong task: Tmr Svc) [ 861 ms] [timer NhayLed] LED BẬT (chạy trong task: Tmr Svc) [ 941 ms] [timer NhayLed] LED TẮT (chạy trong task: Tmr Svc) [ 1001 ms] Chinh dừng timer nháy; timer TatBom còn hoạt động? không [ 1101 ms] [timer DemNguoc] còn 3 [ 1201 ms] [timer DemNguoc] còn 2 [ 1301 ms] [timer DemNguoc] còn 1 [ 1401 ms] [timer DemNguoc] còn 0 [ 1401 ms] [timer DemNguoc] dừng timer
Giải thích
- Callback chạy trong task “Tmr Svc” — được in ra bằng
pcTaskGetName(NULL). - Timer ID lưu bộ đếm riêng cho từng timer (đếm ngược 3, 2, 1, 0).
- Callback không được chặn: không
vTaskDelay, không chờ queue lâu — nếu không mọi timer khác cũng bị trễ.
2Reset timer: timeout không hoạt động
xTimerReset khởi động lại thời gian đếm. Mỗi thao tác của người dùng gọi reset → timer chỉ hết hạn khi người dùng ngừng thao tác đủ lâu. Đây là cách làm tắt đèn nền, chế độ ngủ, đăng xuất tự động.
// Bài 11 – Reset timer: tự tắt đèn nền LCD sau 1 giây KHÔNG có thao tác. Mỗi lần nhấn nút -> đếm lại từ đầu.
// Đây là cách làm "timeout không hoạt động" chuẩn trên thiết bị nhúng (màn hình, chế độ ngủ, watchdog mềm).
#include "sim.h"
#include "timers.h"
TimerHandle_t tmrDenNen;
bool denNenBat = false;
void cbTatDenNen(TimerHandle_t) {
denNenBat = false;
LOG(" [timer] 1000 ms không thao tác -> TẮT đèn nền LCD");
}
uint32_t isrNut() { // nhấn nút: bật đèn nền + reset timer (bản FromISR)
BaseType_t c = pdFALSE;
denNenBat = true;
xTimerResetFromISR(tmrDenNen, &c); // nếu timer đang dừng thì khởi động, đang chạy thì đếm lại
return c;
}
void taskNguoiDung(void*) {
const uint32_t lanNhan[] = {100, 600, 1100, 1500, 3200};
TickType_t truoc = 0;
for (uint32_t t : lanNhan) {
vTaskDelay(pdMS_TO_TICKS(t - truoc));
truoc = t;
LOG("Nhấn nút -> bật đèn nền, đếm lại 1000 ms");
phatNgat(3);
}
vTaskDelay(pdMS_TO_TICKS(1200));
ket_thuc();
}
int main() {
tmrDenNen = xTimerCreate("DenNen", pdMS_TO_TICKS(1000), pdFALSE, nullptr, cbTatDenNen);
dangKyNgat(3, isrNut);
xTaskCreate(taskNguoiDung, "NguoiDung", 512, nullptr, 1, nullptr);
vTaskStartScheduler();
}
g++ -std=c++17 -Isim -Ikernel/include -Ikernel/portable/MSVC-MingW 19-timer-reset.cpp libfreertos.a -lwinmm -o app && ./app[ 101 ms] Nhấn nút -> bật đèn nền, đếm lại 1000 ms [ 601 ms] Nhấn nút -> bật đèn nền, đếm lại 1000 ms [ 1101 ms] Nhấn nút -> bật đèn nền, đếm lại 1000 ms [ 1501 ms] Nhấn nút -> bật đèn nền, đếm lại 1000 ms [ 2501 ms] [timer] 1000 ms không thao tác -> TẮT đèn nền LCD [ 3201 ms] Nhấn nút -> bật đèn nền, đếm lại 1000 ms [ 4201 ms] [timer] 1000 ms không thao tác -> TẮT đèn nền LCD
Giải thích
- Các lần nhấn 100/600/1100/1500 ms liên tục đẩy lùi thời điểm tắt; đèn chỉ tắt lúc 2501 ms = 1500 + 1000.
🔧Áp dụng trong lập trình nhúng
- ESP32 Arduino cũng có
esp_timer(độ phân giải µs) và lớpTicker— software timer của FreeRTOS phù hợp khi độ phân giải ms là đủ. - Dùng timer one-shot làm watchdog mềm: mỗi lần nhận gói tin thì reset; hết hạn = mất kết nối.
⚠Lỗi thường gặp
- Gọi hàm chặn trong callback.
- Đặt ưu tiên timer daemon quá thấp → callback bị trễ.
- Gọi
xTimerStarttrong ISR (phải dùng bản FromISR).
✎Bài tập về nhà
🏁 Queue set, chủ đề nâng cao và dự án tổng hợp
Hoàn thiện kiến thức (queue set, quản lý bộ nhớ, co-routine, MPU, chất lượng mã) và ghép tất cả vào một firmware hoàn chỉnh.
- Chờ đồng thời nhiều queue/semaphore bằng queue set
- Chọn mô hình heap (heap_1…heap_5)
- Biết co-routine, MPU, tiêu chuẩn chất lượng và quy ước code của FreeRTOS
- Thiết kế kiến trúc task cho một dự án thật
1Queue set
Queue set cho phép một task chờ trên nhiều queue/semaphore cùng lúc và biết cái nào vừa có dữ liệu (xQueueSelectFromSet). Tuy vậy FreeRTOS khuyên ưu tiên thiết kế đơn giản hơn (gộp về một queue chứa struct có trường “nguồn”).
// Bài 12 – Queue set: một task chờ đồng thời trên NHIỀU queue/semaphore, xử lý cái nào có dữ liệu trước.
#include <initializer_list>
#include "sim.h"
#include "queue.h"
#include "semphr.h"
QueueHandle_t qNhiet, qLenh;
SemaphoreHandle_t semKhanCap;
QueueSetHandle_t tapHop;
void taskNguonNhiet(void*) {
for (int t : {280, 285, 291}) { xQueueSend(qNhiet, &t, 0); vTaskDelay(pdMS_TO_TICKS(120)); }
vTaskDelete(nullptr);
}
void taskNguonLenh(void*) {
vTaskDelay(pdMS_TO_TICKS(60));
char c = 'F'; xQueueSend(qLenh, &c, 0);
vTaskDelay(pdMS_TO_TICKS(150));
c = 'S'; xQueueSend(qLenh, &c, 0);
vTaskDelay(pdMS_TO_TICKS(50));
xSemaphoreGive(semKhanCap);
vTaskDelete(nullptr);
}
void taskXuLy(void*) {
for (;;) {
QueueSetMemberHandle_t ai = xQueueSelectFromSet(tapHop, pdMS_TO_TICKS(300)); // chờ BẤT KỲ thành viên nào
if (ai == qNhiet) {
int t; xQueueReceive(qNhiet, &t, 0);
LOG("XuLy từ qNhiet: %d.%d°C", t / 10, t % 10);
} else if (ai == qLenh) {
char c; xQueueReceive(qLenh, &c, 0);
LOG("XuLy từ qLenh : lệnh '%c'", c);
} else if (ai == semKhanCap) {
xSemaphoreTake(semKhanCap, 0);
LOG("XuLy từ semKhanCap: DỪNG KHẨN CẤP");
} else {
LOG("XuLy 300 ms không có gì -> kết thúc");
ket_thuc();
}
}
}
int main() {
qNhiet = xQueueCreate(4, sizeof(int));
qLenh = xQueueCreate(4, sizeof(char));
semKhanCap = xSemaphoreCreateBinary();
tapHop = xQueueCreateSet(4 + 4 + 1); // tổng sức chứa của các thành viên
xQueueAddToSet(qNhiet, tapHop);
xQueueAddToSet(qLenh, tapHop);
xQueueAddToSet(semKhanCap, tapHop);
xTaskCreate(taskXuLy, "XuLy", 1024, nullptr, 3, nullptr);
xTaskCreate(taskNguonNhiet, "NguonNhiet", 512, nullptr, 2, nullptr);
xTaskCreate(taskNguonLenh, "NguonLenh", 512, nullptr, 2, nullptr);
vTaskStartScheduler();
}
g++ -std=c++17 -Isim -Ikernel/include -Ikernel/portable/MSVC-MingW 20-queue-set.cpp libfreertos.a -lwinmm -o app && ./app[ 1 ms] XuLy từ qNhiet: 28.0°C [ 61 ms] XuLy từ qLenh : lệnh 'F' [ 121 ms] XuLy từ qNhiet: 28.5°C [ 211 ms] XuLy từ qLenh : lệnh 'S' [ 241 ms] XuLy từ qNhiet: 29.1°C [ 261 ms] XuLy từ semKhanCap: DỪNG KHẨN CẤP [ 561 ms] XuLy 300 ms không có gì -> kết thúc
Giải thích
- Dữ liệu từ 3 nguồn khác nhau được xử lý đúng thứ tự thời gian tới.
2Quản lý bộ nhớ, co-routine, MPU, chất lượng
| Mô hình heap | Đặc điểm | Dùng khi |
|---|---|---|
| heap_1 | chỉ cấp phát, không giải phóng | hệ thống tạo mọi thứ lúc khởi động (an toàn nhất) |
| heap_2 | có giải phóng, không gộp khối trống (cũ) | ít dùng |
| heap_3 | bọc malloc/free của thư viện C | đã có malloc an toàn luồng |
| heap_4 | có giải phóng + gộp khối trống | phổ biến nhất (khóa này dùng) |
| heap_5 | như heap_4, trải trên nhiều vùng RAM rời rạc | chip có RAM trong + RAM ngoài |
- Co-routine: dạng “task” siêu nhẹ dùng chung một stack, dành cho chip cực ít RAM; đã lỗi thời và bị loại bỏ trong các bản FreeRTOS mới — thay bằng task + notification.
- FreeRTOS-MPU: dùng bộ bảo vệ bộ nhớ của Cortex-M để task chỉ truy cập vùng nhớ được phép (task “không đặc quyền”), lỗi một task không làm hỏng cả hệ thống.
- Chất lượng: kernel tuân thủ phần lớn MISRA C, có bộ kiểm thử phủ mã; bản SAFERTOS được chứng nhận IEC 61508 SIL3 cho ứng dụng an toàn.
- Quy ước code: tiền tố kiểu trong tên biến (
xBaseType_t/struct,uluint32_t,pvvoid*,pcchar*), tên hàm có tiền tố file (xTaskCreatetrong tasks.c,vQueueDeletetrong queue.c) và kiểu trả về (vvoid,xBaseType_t,pvcon trỏ).
3Dự án: firmware trạm thời tiết đa nhiệm
Dự án dùng gần như mọi cơ chế của khóa: task chu kỳ (vTaskDelayUntil), queue, mutex, event group, task notification từ ngắt, software timer auto-reload và one-shot.
// Bài 12 – DỰ ÁN TỔNG HỢP: firmware trạm thời tiết đa nhiệm
// TaskCamBien (ưu tiên 3, chu kỳ 200 ms, vTaskDelayUntil) --queue--> TaskXuLy (2)
// TaskXuLy: lọc trung bình, cập nhật LCD (mutex), báo động qua event group
// TaskCanhBao (4): chờ bit báo động ISR nút nhấn --notification--> TaskGiaoDien (2): đổi chế độ hiển thị
// Software timer 500 ms: nhịp tim (heartbeat LED) Timer one-shot: kết thúc mô phỏng sau 2,5 giây
#include <cstdio>
#include "sim.h"
#include "queue.h"
#include "semphr.h"
#include "event_groups.h"
#include "timers.h"
struct MauDo { uint32_t thoiDiem; int16_t nhietX10; uint8_t doAm; };
QueueHandle_t qMau;
SemaphoreHandle_t mtxLcd;
EventGroupHandle_t egBaoDong;
TaskHandle_t hGiaoDien;
constexpr EventBits_t BIT_QUA_NHIET = 1 << 0;
volatile int cheDo = 0; // 0: nhiệt độ, 1: độ ẩm
uint32_t soMau = 0, soLanCanhBao = 0;
void lcdIn(const char* s) {
xSemaphoreTake(mtxLcd, portMAX_DELAY);
LOG(" LCD | %-20s |", s);
xSemaphoreGive(mtxLcd);
}
void taskCamBien(void*) {
TickType_t moc = xTaskGetTickCount();
const int16_t nhiet[] = {285, 287, 290, 295, 302, 311, 318, 322, 316, 305, 298, 291};
for (int i = 0;; i = (i + 1) % 12) {
MauDo m{xTaskGetTickCount(), nhiet[i], uint8_t(60 - i)};
xQueueSend(qMau, &m, 0);
vTaskDelayUntil(&moc, pdMS_TO_TICKS(200));
}
}
void taskXuLy(void*) {
int16_t lichSu[3] = {0};
int dem = 0;
MauDo m;
for (;;) {
xQueueReceive(qMau, &m, portMAX_DELAY);
soMau++;
lichSu[dem++ % 3] = m.nhietX10;
int n = dem < 3 ? dem : 3, tong = 0;
for (int i = 0; i < n; i++) tong += lichSu[i];
int tb = tong / n;
char s[32];
if (cheDo == 0) std::snprintf(s, sizeof(s), "T = %d.%d C (TB3)", tb / 10, tb % 10);
else std::snprintf(s, sizeof(s), "H = %u %%", m.doAm);
lcdIn(s);
if (tb >= 310) xEventGroupSetBits(egBaoDong, BIT_QUA_NHIET);
else xEventGroupClearBits(egBaoDong, BIT_QUA_NHIET);
}
}
void taskCanhBao(void*) {
for (;;) {
xEventGroupWaitBits(egBaoDong, BIT_QUA_NHIET, pdFALSE, pdTRUE, portMAX_DELAY);
soLanCanhBao++;
LOG("!! CanhBao nhiệt độ TB >= 31.0 C -> bật còi, gửi SMS");
vTaskDelay(pdMS_TO_TICKS(400)); // nhắc lại mỗi 400 ms khi còn quá nhiệt
}
}
uint32_t isrNutMode() {
BaseType_t c = pdFALSE;
vTaskNotifyGiveFromISR(hGiaoDien, &c);
return c;
}
void taskGiaoDien(void*) {
for (;;) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
cheDo = 1 - cheDo;
LOG("GiaoDien nút MODE -> hiển thị %s", cheDo ? "ĐỘ ẨM" : "NHIỆT ĐỘ");
}
}
void cbNhipTim(TimerHandle_t) { static bool b; b = !b; LOG(" (heartbeat LED %s)", b ? "●" : "○"); }
void cbKetThuc(TimerHandle_t) {
char bang[512];
vTaskList(bang);
PRINT("\n===== Tổng kết sau 2,5 giây =====\nSố mẫu đã xử lý: %lu, số lần cảnh báo: %lu\nTên \tTT\tƯT\tStack\tSTT\n%s",
(unsigned long)soMau, (unsigned long)soLanCanhBao, bang);
ket_thuc();
}
void taskNguoiDung(void*) { // giả lập nhấn nút MODE lúc 700 ms và 1300 ms
vTaskDelay(pdMS_TO_TICKS(700)); phatNgat(3);
vTaskDelay(pdMS_TO_TICKS(600)); phatNgat(3);
vTaskDelete(nullptr);
}
int main() {
qMau = xQueueCreate(8, sizeof(MauDo));
mtxLcd = xSemaphoreCreateMutex();
egBaoDong = xEventGroupCreate();
dangKyNgat(3, isrNutMode);
xTaskCreate(taskCamBien, "CamBien", 512, nullptr, 3, nullptr);
xTaskCreate(taskXuLy, "XuLy", 1024, nullptr, 2, nullptr);
xTaskCreate(taskCanhBao, "CanhBao", 512, nullptr, 4, nullptr);
xTaskCreate(taskGiaoDien, "GiaoDien", 512, nullptr, 2, &hGiaoDien);
xTaskCreate(taskNguoiDung, "NguoiDung", 512, nullptr, 1, nullptr);
xTimerStart(xTimerCreate("NhipTim", pdMS_TO_TICKS(500), pdTRUE, nullptr, cbNhipTim), 0);
xTimerStart(xTimerCreate("KetThuc", pdMS_TO_TICKS(2500), pdFALSE, nullptr, cbKetThuc), 0);
vTaskStartScheduler();
}
g++ -std=c++17 -Isim -Ikernel/include -Ikernel/portable/MSVC-MingW 21-du-an-tram-thoi-tiet.cpp libfreertos.a -lwinmm -o app && ./app[ 1 ms] LCD | T = 28.5 C (TB3) | [ 201 ms] LCD | T = 28.6 C (TB3) | [ 401 ms] LCD | T = 28.7 C (TB3) | [ 500 ms] (heartbeat LED ●) [ 601 ms] LCD | T = 29.0 C (TB3) | [ 701 ms] GiaoDien nút MODE -> hiển thị ĐỘ ẨM [ 801 ms] LCD | H = 56 % | [ 1000 ms] (heartbeat LED ○) [ 1001 ms] LCD | H = 55 % | [ 1201 ms] LCD | H = 54 % | [ 1201 ms] !! CanhBao nhiệt độ TB >= 31.0 C -> bật còi, gửi SMS [ 1301 ms] GiaoDien nút MODE -> hiển thị NHIỆT ĐỘ [ 1401 ms] LCD | T = 31.7 C (TB3) | [ 1500 ms] (heartbeat LED ●) [ 1601 ms] !! CanhBao nhiệt độ TB >= 31.0 C -> bật còi, gửi SMS [ 1601 ms] LCD | T = 31.8 C (TB3) | [ 1801 ms] LCD | T = 31.4 C (TB3) | [ 2000 ms] (heartbeat LED ○) [ 2001 ms] !! CanhBao nhiệt độ TB >= 31.0 C -> bật còi, gửi SMS [ 2001 ms] LCD | T = 30.6 C (TB3) | [ 2201 ms] LCD | T = 29.8 C (TB3) | [ 2401 ms] LCD | T = 29.1 C (TB3) | ===== Tổng kết sau 2,5 giây ===== Số mẫu đã xử lý: 13, số lần cảnh báo: 3 Tên TT ƯT Stack STT Tmr Svc X 7 509 7 IDLE R 0 253 6 CamBien B 3 509 1 GiaoDien B 2 509 4 CanhBao B 4 509 3 XuLy B 2 1021 2
Giải thích
- Cảnh báo bật khi nhiệt độ trung bình 3 mẫu ≥ 31.0 °C và nhắc lại mỗi 400 ms cho tới khi hết quá nhiệt.
- Nút MODE (ngắt) đổi màn hình giữa nhiệt độ và độ ẩm mà không làm gián đoạn việc lấy mẫu.
- Bảng cuối cho thấy mọi task đều đang Blocked (B) — hệ thống phần lớn thời gian ngủ, CPU rảnh để tiết kiệm năng lượng.
✓Tóm tắt
- Task + ưu tiên + chờ sự kiện thay cho delay → hệ thống phản hồi đúng hạn.
- Truyền dữ liệu: queue (nhiều phần tử), stream/message buffer (byte/thông điệp), notification mailbox (giá trị mới nhất).
- Báo hiệu: notification (1 task nhận) → semaphore → event group (nhiều sự kiện / nhiều task).
- Bảo vệ tài nguyên: mutex (có kế thừa ưu tiên), vùng găng ngắn.
- Hẹn giờ: software timer thay vì tạo task chỉ để chờ.