Chương 27. RTOS cơ bản với C++
Mục tiêu
- Hiểu ba khái niệm cốt lõi của RTOS (FreeRTOS, Zephyr...): task, hàng đợi (queue), mutex — qua một bản mô phỏng tối giản chạy được trên host.
- Biết những ràng buộc C++ đặc thù khi lập trình trên RTOS: cấp phát tĩnh và stack theo task, thứ tự khởi tạo toàn cục, API an toàn cho ISR.
- Biết tra đúng tài liệu chính thức thay vì đoán chữ ký API — sách này không thay thế reference manual của FreeRTOS/Zephyr.
Lưu ý quan trọng: ví dụ chạy được của chương này là một bản mô phỏng khái niệm tối giản trên host, không phải FreeRTOS/Zephyr thật (không có MCU/toolchain RTOS trong môi trường biên dịch của sách). Tên hàm và chữ ký API RTOS thật được trích trong phần văn bản là để tham khảo cấu trúc; luôn đối chiếu với tài liệu chính thức cho phiên bản và cấu hình bạn dùng trước khi viết code thật.
Câu chuyện: vòng lặp chính "biết làm mọi thứ"
Một firmware không dùng RTOS có một vòng lặp while(true) xử lý tuần tự: đọc cảm biến, xử lý giao thức, cập nhật màn hình, tất cả trong cùng một hàm khổng lồ. Khi cần thêm một tác vụ mới (ghi log ra thẻ SD, chậm và không đều), nó làm chậm mọi tác vụ khác vì tất cả chia sẻ chung một luồng thực thi tuần tự. RTOS giải quyết bằng cách chia thành các task độc lập, mỗi task có ngăn xếp riêng, được bộ lập lịch (scheduler) luân phiên chạy theo độ ưu tiên — task ghi thẻ SD chậm không còn chặn task đọc cảm biến khẩn cấp.
Kiến thức
1. Ba khái niệm cốt lõi, mô phỏng trên host
// MO PHONG TOI GIAN cac khai niem RTOS (task, hang doi, mutex) tren host, KHONG PHAI FreeRTOS/Zephyr
// that. Muc dich: thay duoc CACH cac khai niem lam viec cung nhau, truoc khi doc API that cua RTOS ban
// chon (ten ham/kieu du lieu chinh xac phai tra trong tai lieu FreeRTOS/Zephyr cho phien ban ban dung).
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstdio>
// Hang doi co dinh, khong heap - giong tinh than xQueueCreate() cua FreeRTOS nhung don gian hoa toi da.
template <typename T, std::size_t N>
class FixedQueue {
public:
bool send(const T& v) {
if (count_ == N) return false; // FreeRTOS that: xQueueSend co the CHO (block) toi khi co cho
buf_[(head_ + count_) % N] = v;
++count_;
return true;
}
bool receive(T& out) {
if (count_ == 0) return false; // FreeRTOS that: xQueueReceive co the CHO toi khi co du lieu
out = buf_[head_];
head_ = (head_ + 1) % N;
--count_;
return true;
}
private:
std::array<T, N> buf_{};
std::size_t head_ = 0, count_ = 0;
};
// Mutex don gian hoa: tren mo phong MOT LUONG nay no khong the that su xay ra tranh chap, nhung API
// va mau RAII (Lock) giong het cach dung xSemaphoreCreateMutex()/xSemaphoreTake()/xSemaphoreGive() that.
class SimpleMutex {
public:
bool try_lock() { if (locked_) return false; locked_ = true; return true; }
void unlock() { locked_ = false; }
private:
bool locked_ = false;
};
class Lock {
public:
explicit Lock(SimpleMutex& m) : m_(m), owned_(m.try_lock()) {}
~Lock() { if (owned_) m_.unlock(); }
explicit operator bool() const { return owned_; }
private:
SimpleMutex& m_;
bool owned_;
};
static FixedQueue<std::uint16_t, 4> g_sensor_queue;
static SimpleMutex g_log_mutex;
static std::uint32_t g_shared_log_count = 0;
void log_line(const char* msg) {
Lock lock(g_log_mutex);
if (!lock) { std::puts(" (bo qua log: dang co task khac ghi log)"); return; }
std::printf(" log #%u: %s\n", static_cast<unsigned>(++g_shared_log_count), msg);
}
// "Task" san xuat: doc cam bien gia lap moi 2 tick, gui vao hang doi.
void producer_task(std::uint32_t tick) {
if (tick % 2 != 0) return;
const auto sample = static_cast<std::uint16_t>(1000 + tick);
if (g_sensor_queue.send(sample)) log_line("da gui mau vao hang doi");
else log_line("hang doi day, bo mau (trong FreeRTOS that: task co the CHO thay vi mat du lieu)");
}
// "Task" tieu thu: rut mau ra khoi hang doi moi tick, neu co.
void consumer_task(std::uint32_t /*tick*/) {
std::uint16_t sample;
if (g_sensor_queue.receive(sample)) std::printf(" consumer: xu ly mau %u\n", static_cast<unsigned>(sample));
}
int main() {
// Dispatcher round-robin don gian: goi lan luot tung task moi "tick", KHONG uu tien, KHONG ngat
// thuc su - FreeRTOS/Zephyr that dung bo lap lich uu tien va ngat phan cung (SysTick) de chuyen task.
for (std::uint32_t tick = 0; tick < 6; ++tick) {
std::printf("tick %u:\n", static_cast<unsigned>(tick));
producer_task(tick);
consumer_task(tick);
}
}
tick 0:
log #1: da gui mau vao hang doi
consumer: xu ly mau 1000
tick 1:
tick 2:
log #2: da gui mau vao hang doi
consumer: xu ly mau 1002
tick 3:
tick 4:
log #3: da gui mau vao hang doi
consumer: xu ly mau 1004
tick 5:
- Task: một hàm chạy "độc lập" về mặt logic. Bản mô phỏng gọi
producer_task/consumer_tasklần lượt mỗi "tick" (round-robin, không ưu tiên) — RTOS thật dùng ngắt phần cứng (thường là SysTick) để chuyển ngữ cảnh giữa các task theo độ ưu tiên, có thể ngắt một task đang chạy để chạy task ưu tiên cao hơn (preemption). - Hàng đợi (queue):
FixedQueuelà bản rút gọn tối đa của khái niệm mà FreeRTOS gọi làxQueueCreate/xQueueSend/xQueueReceive— cấp phát cố định, không heap trong bản mô phỏng. Khác biệt quan trọng: hàng đợi RTOS thật cho phép task CHỜ (block) tới khi có chỗ trống (gửi) hoặc có dữ liệu (nhận), nhường CPU cho task khác trong lúc chờ — bản mô phỏng ở đây chỉ trảfalsengay lập tức vì không có khái niệm "nhường CPU". - Mutex:
SimpleMutex+Lock(RAII, đúng mẫu chương 12) minh hoạ hình dạng củaxSemaphoreCreateMutex/xSemaphoreTake/xSemaphoreGive. Vì bản mô phỏng chạy đơn luồng, nó không thể hiện được tranh chấp thật — mục đích chỉ là quen với việc luôn bọc mutex bằng RAII thay vì lock/unlock tay (dễ quênunlocktrên đường lỗi, giống bài học CS ở chương 25).
2. API thật trông như thế nào (tham khảo, không phải code chạy được)
FreeRTOS thật (kiểm tra đúng phiên bản trong tài liệu chính thức của bạn) có dạng đại khái:
// Tao task: ham than, ten, kich thuoc stack (theo TU, khong phai byte, tren nhieu port), do uu tien, handle ra.
xTaskCreate(vSensorTask, "Sensor", configMINIMAL_STACK_SIZE, nullptr, tskIDLE_PRIORITY + 1, nullptr);
// Hang doi: tao voi so phan tu va kich thuoc moi phan tu; gui/nhan co tham so "cho toi da bao lau" (tick).
QueueHandle_t q = xQueueCreate(4, sizeof(uint16_t));
xQueueSend(q, &sample, portMAX_DELAY); // cho vo han neu day
xQueueReceive(q, &sample, pdMS_TO_TICKS(50)); // cho toi da 50ms
// Mutex: tao, khoa, mo khoa.
SemaphoreHandle_t mtx = xSemaphoreCreateMutex();
xSemaphoreTake(mtx, portMAX_DELAY);
xSemaphoreGive(mtx);
Zephyr RTOS dùng bộ API khác (k_thread_create, k_msgq_put/k_msgq_get, k_mutex_lock/k_mutex_unlock) với cùng ba khái niệm. Chữ ký chính xác, đơn vị tham số (tick hay mili-giây), và các macro cấu hình (configXXX) thay đổi theo phiên bản và file cấu hình dự án — đây là lý do sách không đưa các API này vào ví dụ chạy được: chúng không biên dịch được nếu thiếu đúng bộ header và file cấu hình RTOS của dự án cụ thể.
3. Ràng buộc C++ đặc thù khi chạy trên RTOS
- Cấp phát tĩnh cho task/queue/mutex khi có thể: FreeRTOS có các biến thể
xTaskCreateStatic,xQueueCreateStaticnhận sẵn buffer do bạn cấp phát (mảng tĩnh/.bss) thay vì gọipvPortMalloc(heap riêng của FreeRTOS, cấu hình được nhưng vẫn là heap) — nhất quán với nguyên tắc tránh heap của chương 20/22. - Kích thước stack theo từng task: mỗi task có ngăn xếp riêng, kích thước khai báo lúc tạo task — đo bằng kỹ thuật watermarking (chương 20) cho từng task, không dùng một con số mặc định cho mọi task.
- Thứ tự khởi tạo toàn cục: các đối tượng C++ toàn cục (
static, biến namespace) được khởi tạo trướcmain, tức là trước khi scheduler chạy. Constructor của chúng không được gọi API RTOS (task chưa tồn tại, scheduler chưa chạy) — nếu cần logic phụ thuộc RTOS, đặt trong task đầu tiên, không đặt trong constructor toàn cục (liên hệconstinit, chương 19). - API an toàn cho ISR: mọi API RTOS gọi từ ISR phải dùng biến thể
...FromISR(xQueueSendFromISR,xSemaphoreGiveFromISR...) — gọi nhầm API thường từ ISR là lỗi runtime nghiêm trọng, thường không được trình biên dịch phát hiện (liên hệ chương 24 mục 2 về ràng buộc ISR). - Tránh exception/RTTI trong task: cùng lý do đã nêu ở chương 19/20 — nhiều cấu hình RTOS nhúng build
-fno-exceptions -fno-rtti; kiểm traFreeRTOSConfig.h/cấu hình Zephyr của dự án.
Góc nhúng
- Độ ưu tiên task sai (task log chạy ưu tiên cao hơn task đọc cảm biến khẩn cấp) là lỗi thiết kế phổ biến hơn lỗi code — vẽ sơ đồ ưu tiên trước khi viết task.
- Đo "CPU idle time" (thời gian task rảnh chạy) là cách gián tiếp biết hệ thống có đang quá tải không — nhiều RTOS cung cấp hook đo việc này.
sizeofcủa mỗi task control block + stack cộng dồn nhanh trên MCU RAM nhỏ; đếm tổng RAM cho tất cả task trước khi thêm task mới.
Lỗi thường gặp
- Gọi API RTOS (tạo task, gửi queue...) trong constructor của một đối tượng toàn cục — chạy trước khi scheduler khởi động, hành vi không xác định hoặc treo.
- Gọi API RTOS "thường" (không phải
...FromISR) từ trong ISR. - Chọn kích thước stack cho task theo cảm tính thay vì đo watermark thực tế.
- Chia sẻ dữ liệu giữa hai task mà không qua queue/mutex, dựa vào "chắc là không trùng lúc đâu" — đúng là race condition kinh điển, chỉ lộ ra ngẫu nhiên khi tải cao.
- Dùng cấp phát động không giới hạn (
xTaskCreatevới heap RTOS mặc định) trong hệ thống chạy dài hạn mà không theo dõi phân mảnh heap của chính RTOS.
Bài tập
- Sửa
FixedQueue::sendtrong ví dụ để khi đầy, nó ghi đè phần tử cũ nhất thay vì từ chối — thảo luận đây có phải hành vi mong muốn cho dữ liệu cảm biến hay không (so với dữ liệu lệnh điều khiển). - Thêm một "task" thứ ba tranh chấp
g_log_mutexcùng lúc với task hiện có trong cùng một tick — quan sátLocktừ chối lấy khoá và log dòng "bỏ qua". - Tra tài liệu FreeRTOS chính thức (hoặc Zephyr) để tìm chữ ký đầy đủ của một hàm tạo queue tĩnh (
xQueueCreateStatichoặc tương đương Zephyr) và ghi lại các tham số nó cần so với bản động. - Vẽ sơ đồ (bằng lời hoặc hình) độ ưu tiên hợp lý cho một firmware có ba task: đọc cảm biến 1kHz, xử lý giao thức UART, ghi log ra thẻ SD — giải thích lựa chọn.
Tóm tắt
- RTOS chia firmware thành các task độc lập, đồng bộ qua hàng đợi và mutex, được lập lịch theo độ ưu tiên — giải quyết vấn đề "một vòng lặp lớn làm mọi thứ".
- Nguyên tắc tránh heap, đo stack, và ràng buộc ISR của các chương trước áp dụng trực tiếp: mỗi task cần stack tĩnh đo đạc được, mọi API gọi từ ISR cần biến thể
FromISR. - Chữ ký API RTOS thật phụ thuộc phiên bản/cấu hình — luôn tra tài liệu chính thức, sách chỉ cho khung khái niệm.