Lập trình ESP32 với FreeRTOS: chạy đa nhiệm thật sự với xTaskCreate

Lập trình ESP32 với FreeRTOS

Từ đầu serie tới giờ, mọi bài đều viết theo mô hình quen thuộc: mọi việc dồn hết vào 1 vòng loop(). Cách này đơn giản nhưng có giới hạn — nếu 1 việc trong loop() bị chặn (blocking) lâu, mọi việc khác cũng phải chờ theo. Bài này chúng ta sẽ lập trình ESP32 với FreeRTOS — hệ điều hành thời gian thực chạy sẵn bên dưới mọi chương trình Arduino trên ESP32, cho phép chạy nhiều tác vụ thật sự song song.

Đây là một bài thực hành trong Serie Lập trình ESP32 từ A tới Z.

FreeRTOS đã chạy sẵn, bạn chỉ chưa để ý

Sự thật thú vị: ngay cả những bài void loop() đơn giản nhất trước giờ, bên dưới ESP32 vẫn đang chạy FreeRTOS — hàm setup()/loop() mà Arduino cung cấp thực chất chỉ là 1 task FreeRTOS được framework tự tạo sẵn cho bạn (thường chạy trên core 1). ESP32 có 2 nhân CPU vật lý, nghĩa là ngoài task loop() mặc định đó, bạn hoàn toàn có thể tự tạo thêm task khác chạy song song, tận dụng cả 2 core thay vì chỉ dùng 1.

Khi nào cần tới FreeRTOS task?

Nếu chương trình bạn có 2 việc độc lập, không việc nào nên làm chậm việc kia — ví dụ vừa đọc cảm biến liên tục, vừa xử lý kết nối mạng nặng — nhồi hết vào 1 loop() với delay() sẽ khiến việc này chặn việc kia. Tạo 2 task riêng, mỗi task lo 1 việc, là cách giải quyết đúng bản chất thay vì cố căn thời gian delay() cho khớp.

Lập trình ESP32 với FreeRTOS: taskBlink chạy trên core 0 nháy LED GPIO26 200ms, taskHeartbeat chạy trên core 1 nháy LED GPIO27 1 giây

Lập trình ESP32 với FreeRTOS

Viết hàm task

void taskBlink(void *parameter) {
  pinMode(ledBlink, OUTPUT);

  for (;;) {
    digitalWrite(ledBlink, HIGH);
    vTaskDelay(200 / portTICK_PERIOD_MS);
    digitalWrite(ledBlink, LOW);
    vTaskDelay(200 / portTICK_PERIOD_MS);
  }
}

Một task FreeRTOS là 1 hàm có vòng lặp vô hạn for (;;) riêng của nó — không bao giờ được return, y hệt bản chất của loop() nhưng là bản riêng, độc lập. Trong task mình dùng vTaskDelay() (đơn vị tick): trong lúc chờ, task này chuyển sang trạng thái bị chặn (blocked) và nhường CPU cho task khác chạy.

Thực ra trên ESP32, hàm delay() quen thuộc của Arduino cũng gọi vTaskDelay() ở bên dưới, nên nó cũng nhường CPU chứ không chặn cứng như trên Arduino Uno. Mình vẫn viết vTaskDelay() cho rõ ý đồ, và để code dễ mang sang project ESP-IDF thuần sau này.

Lưu ý: Cái thực sự chặn cứng CPU là vòng lặp chờ bận (busy-wait), ví dụ while (digitalRead(pin) == HIGH) {} không có delay bên trong. Task nào viết như vậy sẽ chiếm trọn core của nó, các task cùng core có độ ưu tiên thấp hơn không bao giờ được chạy, và sau vài giây ESP32 có thể bị watchdog reset.

Tạo và ghim task vào core cụ thể

xTaskCreatePinnedToCore(
  taskBlink,      // ham task
  "TaskBlink",    // ten task (de debug)
  2048,           // kich thuoc stack (byte)
  NULL,           // tham so truyen vao task
  1,              // do uu tien
  NULL,           // handle cua task
  0               // ghim vao core 0
);

xTaskCreatePinnedToCore() tạo task mới và chỉ định chạy cố định trên core nào (0 hoặc 1) — dùng bản xTaskCreate() thường (không ghim core) thì FreeRTOS tự chọn core rảnh hơn để chạy. Ghim rõ core hữu ích khi bạn muốn tách biệt hẳn 2 việc nặng ra 2 nhân riêng, tránh tranh chấp CPU.

Lưu ý: Kích thước stack (2048 byte trong ví dụ) cần đủ lớn cho những gì task đó làm — nếu task gọi hàm đệ quy sâu, cấp phát mảng lớn cục bộ, hoặc dùng String nhiều, có thể cần tăng lên 4096 hoặc 8192, nếu không ESP32 sẽ crash với lỗi tràn stack (stack overflow) rất khó dò ra nguyên nhân.

Code đầy đủ

#include <Arduino.h>

const int ledBlink = 26;
const int ledHeartbeat = 27;

void taskBlink(void *parameter) {
  pinMode(ledBlink, OUTPUT);

  for (;;) {
    digitalWrite(ledBlink, HIGH);
    vTaskDelay(200 / portTICK_PERIOD_MS);
    digitalWrite(ledBlink, LOW);
    vTaskDelay(200 / portTICK_PERIOD_MS);
  }
}

void taskHeartbeat(void *parameter) {
  pinMode(ledHeartbeat, OUTPUT);

  for (;;) {
    digitalWrite(ledHeartbeat, HIGH);
    vTaskDelay(1000 / portTICK_PERIOD_MS);
    digitalWrite(ledHeartbeat, LOW);
    vTaskDelay(1000 / portTICK_PERIOD_MS);

    Serial.print("Heartbeat tu core: ");
    Serial.println(xPortGetCoreID());
  }
}

void setup() {
  Serial.begin(115200);

  xTaskCreatePinnedToCore(
    taskBlink,
    "TaskBlink",
    2048,
    NULL,
    1,
    NULL,
    0
  );

  xTaskCreatePinnedToCore(
    taskHeartbeat,
    "TaskHeartbeat",
    2048,
    NULL,
    1,
    NULL,
    1
  );
}

void loop() {
  vTaskDelay(1000 / portTICK_PERIOD_MS);
}

Code đầy đủ mình để trên GitHub, các bạn xem tại 08-FreeRTOS/01_FreeRTOS_Multitask nhé.

Kết quả

Nối 2 LED vào GPIO26 và GPIO27 (qua điện trở hạn dòng), nạp code, mở Serial Monitor ở baudrate 115200.

Sơ đồ đấu nối ESP32 FreeRTOS: LED1 nối GPIO26, LED2 nối GPIO27, mỗi LED qua điện trở 220Ω xuống GND

Log Serial Monitor ESP32 FreeRTOS: task heartbeat chạy trên core 1

LED ở GPIO26 nhấp nháy nhanh (200ms), LED ở GPIO27 nhấp nháy chậm (1 giây) — cả 2 chạy hoàn toàn độc lập, cùng lúc, không cái nào phải chờ cái kia dù cả hai đều dùng vTaskDelay(). Serial Monitor in ra core đang chạy taskHeartbeat (thường là core 1), khác với core 0 nơi taskBlink chạy, chứng minh 2 task đang thực sự dùng 2 nhân CPU vật lý khác nhau, đúng không nào.

Kết

FreeRTOS là nền tảng ẩn phía sau mọi chương trình Arduino trên ESP32 — hiểu và làm chủ được nó, bạn có thể thiết kế những chương trình phức tạp với nhiều tác vụ chạy song song thật sự, thay vì gò ép mọi thứ vào 1 vòng loop() duy nhất và chật vật căn delay() sao cho không việc nào bị trễ.

Nếu thấy bài viết có ích, hãy chia sẻ cho bạn bè cùng học. Có thắc mắc gì cứ để lại bình luận, mình sẽ giải đáp nhé!

Rate this post

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *