FreeRTOS trên ESP32 - dùng cả hai nhân cho robot vừa WiFi vừa PID

Nhập môn FreeRTOS thực dụng - xTaskCreatePinnedToCore tách vòng PID khỏi WiFi, ưu tiên task, chia sẻ dữ liệu an toàn với volatile/mutex/queue và bẫy watchdog. Trong bài viết này, chúng ta sẽ cùng tìm hiểu chi tiết và ứng dụng thực tế trong robot.
Bài toán mà mẫu millis() không giải nổi
Robot của bạn vừa cân bằng (vòng PID cần nhịp 5 ms ĐỀU TĂM TẮP) vừa phục vụ trang web điều khiển qua WiFi. Vấn đề: stack WiFi thỉnh thoảng chiếm CPU vài chục mili giây — vòng PID thọt nhịp đúng lúc đó, robot khựng, ngã. Mẫu millis (bài Đa nhiệm với millis ở mục Kiến thức) bó tay vì mọi việc vẫn chung một dòng chảy.
ESP32 có lời giải phần cứng: hai nhân CPU và hệ điều hành thời gian thực FreeRTOS chạy sẵn bên dưới mọi chương trình Arduino. Bạn chỉ cần học cách xin thêm "dòng chảy" thứ hai: WiFi ở nhân 0 (nơi Espressif vốn đặt nó), vòng điều khiển độc chiếm nhân 1 — hai thế giới không giẫm chân nhau.
FreeRTOS đã ở đó từ đầu
Sự thật ít người mới biết: code Arduino trên ESP32 luôn chạy trong một task FreeRTOS (task tên "loopTask", nhân 1). WiFi, Bluetooth là các task hệ thống ở nhân 0. Nghĩa là bạn không "bật" FreeRTOS — bạn chỉ tạo thêm task bên cạnh những task đã có.
Từ vựng tối thiểu:
- Task: một hàm chạy như chương trình độc lập, có stack riêng, vòng lặp riêng
- Scheduler: trọng tài chia CPU cho các task theo độ ưu tiên (số to = ưu tiên cao); task ưu tiên cao sẵn sàng chạy là giành CPU ngay
- vTaskDelay: "delay lịch sự" — task ngủ, CPU trao cho task khác (khác hẳn delay() trơ)
- Pinned to core: ghim task vào nhân 0 hoặc 1
Ví dụ chuẩn - tách vòng điều khiển khỏi WiFi
// ---- Task điều khiển: nhân 1, ưu tiên cao, nhịp cứng 5ms ----
void controlTask(void *param) {
TickType_t lastWake = xTaskGetTickCount();
for (;;) {
float angle = readAngleComplementary(0.005);
float output = computePid(angle);
applyMotors(output);
// ngủ đến ĐÚNG mốc 5ms kế tiếp - nhịp tuyệt đối
vTaskDelayUntil(&lastWake, pdMS_TO_TICKS(5));
}
}
// ---- Task báo cáo: nhân 0, ưu tiên thấp, chạy cùng phe WiFi ----
void telemetryTask(void *param) {
for (;;) {
publishStatus(); // MQTT/WebSocket - chậm cũng kệ
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void setup() {
Serial.begin(115200);
setupHardware();
connectWifi();
// hàm tên stack tham số ưu tiên handle NHÂN
xTaskCreatePinnedToCore(controlTask, "control", 4096, NULL, 3, NULL, 1);
xTaskCreatePinnedToCore(telemetryTask,"telemetry",4096, NULL, 1, NULL, 0);
}
void loop() { vTaskDelay(pdMS_TO_TICKS(1000)); } // loop thành người thừaBa lựa chọn có chủ đích trong ví dụ:
- vTaskDelayUntil thay vì vTaskDelay trong vòng điều khiển: nhịp tính từ mốc trước, không cộng dồn thời gian xử lý — đây chính là "nhịp cứng" mà PID cân bằng đòi hỏi (bài Robot tự cân bằng ở mục Kiến thức)
- Ưu tiên 3 vs 1: task điều khiển đè task báo cáo nếu tranh chấp; đừng đặt ưu tiên cao hơn ~5 kẻo chèn cả task hệ thống
- Stack 4096 byte: đủ cho tính toán thường; task có printf chuỗi dài hay JSON nên 8192 — thiếu stack là crash bí ẩn kinh điển
Chia sẻ dữ liệu giữa các task - phần dễ sai nhất
Hai task cùng đọc-ghi một biến là mở cửa cho lỗi tinh vi nhất lập trình: race condition. Ba mức giải quyết theo độ phức tạp dữ liệu:
- Biến đơn (int, float, bool): khai `volatile` — đủ cho cờ lệnh và giá trị đo đơn lẻ trên ESP32 (đọc/ghi 32-bit là nguyên tử). Ví dụ:
volatile float latestAngle;task điều khiển ghi, task báo cáo đọc - Cụm dữ liệu nhiều trường: mutex — khóa khi đọc/ghi cả cụm để không dính "nửa cũ nửa mới":
SemaphoreHandle_t mtx = xSemaphoreCreateMutex();
// ghi (task điều khiển):
xSemaphoreTake(mtx, portMAX_DELAY);
state.angle = angle; state.pwm = pwm; state.timestamp = millis();
xSemaphoreGive(mtx);- Luồng sự kiện (lệnh từ web xuống robot): queue — hàng đợi FIFO an toàn giữa task,
xQueueSendbên nhận lệnh WiFi,xQueueReceivebên điều khiển; không mất lệnh, không chen nhau
Quy tắc chọn nhanh: một số → volatile; một cụm → mutex; một dòng chảy → queue. Vượt quá là đang thiết kế phức tạp hơn robot cần.
Bẫy đặc sản của FreeRTOS trên ESP32
- Task watchdog trigger ("task_wdt: Task watchdog got triggered"): task ưu tiên cao chạy
for(;;)không bao giờ vTaskDelay — task nền không được thở. Mọi vòng lặp task phải có vTaskDelay/vTaskDelayUntil, dù chỉ 1 tick - Stack overflow âm thầm: robot reset ngẫu nhiên kèm "Guru Meditation" — nghi stack trước tiên; đo mức dùng thật bằng
uxTaskGetStackHighWaterMark(NULL)rồi đặt dư 30% - Serial từ nhiều task đan xen chữ: Serial không tự khóa — hai task cùng print là ký tự trộn lẫn; log qua một task riêng nhận queue, hoặc bọc mutex quanh print
- Tưởng 2 nhân = nhanh gấp đôi: song song chỉ lợi khi hai việc thật sự độc lập; chia nhỏ một thuật toán tuần tự ra 2 nhân thường chậm hơn vì chi phí đồng bộ
Khi nào cần FreeRTOS, khi nào millis là đủ?
| Tình huống | Lời khuyên |
|---|---|
| Dò line, tránh vật, xe Bluetooth | millis đủ — thêm FreeRTOS là lễ nghi thừa |
| Robot cân bằng KHÔNG WiFi | millis/micros đủ (một vòng điều khiển duy nhất) |
| Vòng điều khiển nhanh + WiFi/MQTT/camera | FreeRTOS — đúng bài toán sinh ra nó |
| Việc nặng thỉnh thoảng (ghi SD, JSON lớn) cạnh việc nhịp đều | FreeRTOS, đẩy việc nặng sang task/nhân khác |
| Muốn học kỹ năng firmware chuyên nghiệp | FreeRTOS — RTOS là ngôn ngữ chung của ngành nhúng |
Lộ trình học khuyến nghị: thành thạo millis trước (nó dạy tư duy không-chặn), rồi FreeRTOS chỉ với 2 task như ví dụ chuẩn, thêm queue khi có luồng lệnh — dừng ở đó cho đến khi dự án thật đòi hơn.
Câu hỏi thường gặp
FreeRTOS có làm code chậm đi không?
Chi phí chuyển task ~vài µs mỗi lần — không đáng kể ở nhịp robot (ms). Đổi lại nhịp vòng điều khiển ĐỀU hơn hẳn khi hệ thống có WiFi. Cái giá thật là độ phức tạp debug — trả bằng kỷ luật: ít task, dữ liệu chia sẻ tối thiểu, đặt tên task rõ.
Bao nhiêu task là hợp lý cho một robot?
2-4 task tự tạo là vùng đẹp: điều khiển (nhân 1), giao tiếp mạng (nhân 0), có thể thêm log/màn hình. Mỗi "việc" một task nghe hay nhưng 8-10 task là ác mộng đồng bộ — gom việc cùng nhịp vào chung task, trong task vẫn dùng mẫu millis cho các việc con.
Arduino Uno có dùng được FreeRTOS không?
Có bản port nhưng không khuyến khích học trên đó: 2 KB RAM chia stack cho vài task là hết, và Uno chỉ một nhân nên mất luôn lợi thế lớn nhất. Bài toán cần RTOS thường cũng là bài toán cần lên ESP32.
Task hệ thống WiFi nằm nhân nào, có đổi được không?
Mặc định nhân 0 (PRO_CPU); Arduino loopTask ở nhân 1 (APP_CPU). Đổi được qua sdkconfig nhưng đừng — quy ước "mạng nhân 0, ứng dụng nhân 1" là nền của mọi ví dụ, thư viện và lời khuyên cộng đồng; thiết kế của bạn nên xuôi dòng đó.
Tài nguyên tải về
Tải bản PDF của bài viết để in giáo án hoặc gửi trước cho học viên.



