Mở đầu: khi while(1) không còn đủ gọn
Từ Bài 1, mọi demo của series đều nằm gọn trong một vòng lặp while(1) duy nhất — gọi tuần tự
vài FSM, đọc vài cảm biến, cập nhật vài LED. Đây gọi là super-loop, và với phần lớn sản
phẩm nhỏ, nó là kiến trúc HOÀN TOÀN ĐÚNG ĐẮN — đơn giản, dễ hiểu, không có race condition nào giữa các
"task" vì chúng chưa bao giờ chạy đồng thời cả.
Nhưng super-loop có một trần rõ ràng: thêm 5-6 FSM cùng chạy, rồi một việc "hơi nặng" len vào giữa chừng — chu kỳ quét bắt đầu phình ra, độ trễ giữa các lần một task được chạy không còn đều đặn nữa. Bài này định nghĩa và ĐO chính xác hiện tượng đó (gọi là jitter), viết một cooperative scheduler gọn trong 30 dòng để quản lý nó có kỷ luật hơn, và chỉ ra pitfall trung tâm khiến nó vẫn có thể sụp đổ — cùng cách chữa KHÔNG CẦN nhảy thẳng lên RTOS.
1. Trần của super-loop
Verify bằng số thật trên VMCU: 3 task chạy trong super-loop — LED nhanh (chu kỳ 100ms), LED chậm (chu kỳ 500ms), quét nút kiểu Bài 5 (chu kỳ 20ms) — mỗi task giả định chỉ tốn 1ms để chạy xong. Đo jitter (độ lệch giữa thời điểm LẼ RA phải chạy và thời điểm THỰC SỰ chạy) của từng task trong 5 giây mô phỏng: jitter tối đa chỉ 0-2ms cho cả ba — gần như hoàn hảo, đúng như kỳ vọng khi mọi task đều "nhẹ".
Giờ thêm một "task tham" — chiếm 300ms mỗi lần chạy (chu kỳ 1000ms), mô phỏng một thao tác nặng như ghi log dài hay tính toán phức tạp. Verify: jitter tối đa của CẢ BA task còn lại vọt từ 0-2ms lên gần 300ms — đúng bằng thời gian task tham chiếm dụng. Đây chính là trần của super-loop: một task chạy-đến-hết-lượt (run-to-completion) không hề biết "nhường bớt" cho ai, nên khi nó dài, MỌI task khác phải đợi.
| Tiêu chí | Super-loop / Cooperative đủ dùng | Nên cân nhắc RTOS |
|---|---|---|
| Số task | Vài task, độ ưu tiên gần bằng nhau | Nhiều task, ưu tiên rất khác nhau |
| Deadline | Mềm, dao động vài ms không sao | Cứng, có task PHẢI phản hồi tức thời |
| Đội ngũ | 1 người, cần đơn giản dễ debug | Nhiều người, cần cô lập module rõ ràng |
2. Cooperative scheduler
Thay vì gọi tay từng task theo thứ tự cố định trong while(1), một
cooperative scheduler giữ một BẢNG task — mỗi task gồm: hàm cần gọi, chu kỳ mong muốn, và
mốc thời gian lần chạy gần nhất. Mỗi vòng quét, scheduler chỉ cần hỏi từng task "đã tới hạn chưa?" (so
millis() hiện tại với mốc + chu kỳ), và gọi nếu đúng.
Chữ "hợp tác" (cooperative) mang một hàm ý QUAN TRỌNG: mỗi task PHẢI tự giác chạy xong rồi trả quyền điều khiển lại ngay — không có cơ chế nào ép nó nhường CPU giữa chừng. Scheduler hoàn toàn TIN TƯỞNG mọi task không "tham" (không chạy quá lâu). Đây chính là giả định sẽ bị phá vỡ ở Mục 4.
3. Viết scheduler 30 dòng
Đây là "hệ điều hành" đầu tiên bạn tự viết trong series — và rất nhiều sản phẩm thương mại thực tế dừng lại đúng ở mức độ phức tạp này, không cần gì hơn:
typedef struct {
void (*fn)(void); // ham can goi khi den han
uint32_t period_ms; // chu ky mong muon
uint32_t last_ms; // moc thoi gian lan chay gan nhat
} task_t;
#define NUM_TASKS 3
task_t tasks[NUM_TASKS] = {
{ led_fast_task, 100, 0 },
{ led_slow_task, 500, 0 },
{ button_scan_task, 20, 0 },
};
void scheduler_tick(void) {
uint32_t now = millis(); // Bai 4
for (int i = 0; i < NUM_TASKS; i++) {
if (now - tasks[i].last_ms >= tasks[i].period_ms) {
tasks[i].fn(); // chay-den-het-luot, KHONG bi ngat boi task khac
tasks[i].last_ms = now;
}
}
}
int main(void) {
setup();
while (1) {
scheduler_tick(); // toan bo "he dieu hanh" nam trong 1 dong nay
}
}
Phép trừ now - tasks[i].last_ms tận dụng đúng tính chất cuộn vòng (wrap-around) của
uint32_t đã học ở Bài 4 — vẫn cho kết quả ĐÚNG dù millis() có cuộn qua
$2^{32}$ hay không, không cần xử lý đặc biệt gì thêm.
4. Pitfall trung tâm: một task tham kéo trễ tất cả
Vì scheduler hoàn toàn "hợp tác" (Mục 2), nó không có cách nào tự bảo vệ khỏi một task vi phạm lời hứa "chạy nhanh rồi trả CPU". Verify lại con số Mục 1: task tham 300ms/1000ms đã kéo jitter của CẢ BA task LÀNH khác lên gần 300ms — dù bản thân chúng không hề thay đổi gì. Đây là pitfall trung tâm của cả chương: tính đúng đắn của một task phụ thuộc vào hành vi của NHỮNG task khác — một module tưởng chừng độc lập lại có thể phá vỡ cả hệ thống.
Giải pháp KHÔNG CẦN nhảy thẳng lên RTOS: chẻ việc dài thành FSM nhiều bước — đúng kỹ thuật máy trạng thái hữu hạn của Bài 5. Thay vì một hàm chạy liền 300ms, chia thành nhiều bước nhỏ (ví dụ 30 bước × 10ms), mỗi lần scheduler gọi tới chỉ chạy ĐÚNG MỘT bước rồi trả quyền điều khiển ngay — công việc vẫn hoàn thành sau vài chu kỳ, nhưng không còn chiếm dụng CPU liên tục nữa.
Một kỹ thuật đi kèm hữu ích: đo WCET (Worst-Case Execution Time — thời gian chạy tệ nhất) của từng task bằng cách bấm giờ (hoặc đếm chu kỳ CPU trên phần cứng thật) qua nhiều lần chạy với dữ liệu đầu vào khác nhau — biết trước "tham" tới đâu trước khi nó gây sự cố trong sản phẩm thật.
5. Thực hành: 3 task + task tham + đo jitter
Demo dưới đây chạy cooperative scheduler thật trên VMCU: chọn 1 trong 3 kịch bản và xem jitter của từng task thay đổi ra sao:
Kịch bản (5 giây mô phỏng, 3 task: LED nhanh 100ms, LED chậm 500ms, quét nút 20ms)
So sánh 3 kịch bản: bật "Có task tham" xem jitter mọi task vọt lên gần 300ms; bật "Đã chẻ thành FSM" xem jitter quay lại y hệt kịch bản "Không task tham" — pitfall được chữa mà không cần RTOS.
// TRUOC: 1 buoc chan CPU nguyen 300ms - moi task khac phai doi
void task_tham_CHAN(void) {
for (int i = 0; i < 300; i++) { do_mot_don_vi_viec(); delay_1ms(); }
}
// SAU: che thanh FSM 30 buoc x 10ms - scheduler goi lai nhieu lan, moi lan 1 buoc
typedef enum { STEP_0, STEP_1, /* ... */ STEP_29, STEP_DONE } step_t;
static step_t buoc_hien_tai = STEP_DONE;
void task_tham_FSM(void) {
if (buoc_hien_tai == STEP_DONE) return; // chua co viec, tra CPU ngay
do_mot_don_vi_viec(); // chi 1 don vi viec (~10ms)
buoc_hien_tai = (buoc_hien_tai == STEP_29) ? STEP_DONE : buoc_hien_tai + 1;
} // <- TRA VE ngay, khong chan CPU lau hon 10ms moi lan goi
import { simulateSuperLoop, jitterStats } from './vmcu.js';
const baseTasks = [
{ name: 'led_fast', periodMs: 100, workMs: 1 },
{ name: 'led_slow', periodMs: 500, workMs: 1 },
{ name: 'button_scan', periodMs: 20, workMs: 1 },
];
// Kich ban 'greedy': them 1 task chiem 300ms moi 1000ms
const events = simulateSuperLoop({
tasks: [...baseTasks, { name: 'task_tham', periodMs: 1000, workMs: 300 }],
durationMs: 5000,
});
console.log(jitterStats(events, 'led_fast')); // { maxJitter: ~290, ... }
console.log(jitterStats(events, 'button_scan')); // { maxJitter: ~292, ... }
// Kich ban 'split': doi workMs 300 -> 10 (che thanh FSM) - jitter ve lai 0-2ms
Tóm lược
- ✅ Super-loop có trần thật — verified: task tham 300ms/1000ms kéo jitter mọi task khác từ 0-2ms lên gần 300ms. Vẫn là kiến trúc ĐÚNG cho đa số sản phẩm nhỏ — dùng bảng tiêu chí Mục 1 để quyết định khi nào cần nâng cấp.
-
✅ Cooperative scheduler: mảng
{fn, period, last}+ vòng lặp gọi task đến hạn, xây trênmillis()Bài 4 — chỉ ~30 dòng, "hệ điều hành" đầu tiên bạn tự viết. "Hợp tác" nghĩa là TIN mọi task không tham. - ✅ Pitfall trung tâm: 1 task tham (run-to-completion dài) kéo trễ TẤT CẢ task khác — tính đúng đắn của một task phụ thuộc vào hành vi của những task khác.
- ✅ Chữa KHÔNG CẦN RTOS: chẻ việc dài thành FSM nhiều bước nhỏ (Bài 5) — verified: jitter quay lại ĐÚNG mức ban đầu (0ms/1ms/2ms), không sai một ly.
Trắc nghiệm ôn tập
Câu 1
Verified: không có task tham, jitter tối đa của mọi task chỉ 0-2ms; thêm một task chiếm 300ms mỗi giây khiến jitter mọi task khác vọt lên gần 300ms — dù bản thân các task đó không đổi gì. Vì sao?
Câu 2
Chữ "hợp tác" (cooperative) trong "cooperative scheduler" mang ý nghĩa gì?
Câu 3
Verified: chẻ task tham từ 1 bước 300ms thành 30 bước × 10ms (giữ nguyên chu kỳ tổng 1000ms) đưa jitter của các task khác quay về ĐÚNG mức ban đầu (0-2ms). Vì sao cách này hiệu quả mà không cần RTOS?
Câu 4
Theo bảng tiêu chí Mục 1, khi nào nên cân nhắc nâng cấp từ super-loop/cooperative scheduler lên RTOS?
Tải file code thực hành minh họa bài học
File JavaScript VMCU — engine MCU ảo dùng xuyên suốt cả 16 bài, Bài 13 vừa thêm mô phỏng
cooperative scheduler (simulateSuperLoop) và đo jitter (jitterStats), kèm
self-test đối chiếu đúng mọi hành vi trong bài (chạy node vmcu.js, không cần cài thêm gì):
Bình luận