📑 เนื้อหาในบทความ
🚀 แนะนำ
เคยเจอปัญหานี้ไหม? คุณเขียนโค้ด ESP32 แล้วอยากให้มันทำงานหลายอย่างพร้อมกัน — อ่านค่าเซ็นเซอร์, ส่งข้อมูลไป Cloud, และตอบสนองปุ่มกด แต่พอลองทำแล้วโปรแกรมกลายเป็นช้าหรือค้าง?
FreeRTOS คือคำตอบ! เป็น Real-Time Operating System (RTOS) ที่มาพร้อมกับ ESP32 ช่วยให้คุณสร้างโปรเจกต์ IoT ที่ซับซ้อนและทำงานหลายอย่างได้อย่างมีประสิทธิภาพ
⚡ ในบทความนี้คุณจะได้เรียนรู้: Tasks, Queues, Semaphores, และ Timers — เครื่องมือหลักของ FreeRTOS ที่จะช่วยให้โปรเจกต์ IoT ของคุณทำงานได้อย่างมืออาชีพ
📖 FreeRTOS คืออะไร?
FreeRTOS (Real-Time Operating System) เป็นระบบปฏิบัติการแบบ Real-Time ที่เบาและใช้งานได้ฟรี ถูกติดตั้งมาพร้อมกับ ESP32 เพื่อให้จัดการงานหลายๆ งานพร้อมกันได้อย่างมีประสิทธิภาพ
💡 ทำไมต้องใช้ FreeRTOS? ปกติ ESP32 ใช้โค้ดแบบ loop() เดียว แต่ FreeRTOS ให้คุณสร้าง "Tasks" หลายๆ ตัวที่ทำงานแบบคู่ขนาน (parallel) ซึ่งเหมาะสำหรับโปรเจกต์ IoT ที่ซับซ้อน
ประโยชน์ของ FreeRTOS:
- Multitasking: ทำงานหลายอย่างพร้อมกันได้อย่างราบรื่น
- Real-Time: รับประกันว่างานสำคัญจะทำงานตรงเวลา
- Efficient: ใช้หน่วยความจำและ CPU อย่างมีประสิทธิภาพ
- Scalable: เพิ่มลดฟีเจอร์ได้ง่าย
📦 ข้อกำหนดเบื้องต้น
Hardware:
- ESP32 Development Board (ใดๆ ก็ได้)
- USB Cable
- คอมพิวเตอร์ (Windows, Mac, หรือ Linux)
Software:
- Arduino IDE หรือ PlatformIO
- ESP32 Board Package
- พื้นฐานการเขียนโปรแกรม C++
⚠️ ระดับความยาก: หัวข้อนี้เหมาะสำหรับผู้ที่มีพื้นฐาน ESP32 แล้ว หากคุณเพิ่งเริ่มต้น แนะนำให้อ่านบทความ ESP32 Getting Started ก่อน
🎯 Tasks (งาน) — หัวใจของ FreeRTOS
Task คือ "งาน" หรือฟังก์ชันที่ทำงานแบบอิสระ แต่ละ Task มี stack memory และ priority ของตัวเอง FreeRTOS จะจัดการสลับไปมาระหว่าง Tasks อย่างรวดเร็วทำให้ดูเหมือนทำงานพร้อมกัน
ตัวอย่าง: สร้าง 2 Tasks พร้อมกัน
// รวม library ที่จำเป็น
#include <Arduino.h>
// Task handles — ใช้อ้างอิง Task
TaskHandle_t Task1Handle = NULL;
TaskHandle_t Task2Handle = NULL;
// LED pins
const int led1Pin = 2; // Built-in LED
const int led2Pin = 4; // External LED
// --- Task 1: กระพริบ LED 1 ทุกๆ 1 วินาที ---
void task1(void *pvParameters) {
Serial.println("Task 1 เริ่มทำงาน");
while(1) {
digitalWrite(led1Pin, HIGH); // เปิด LED
delay(1000);
digitalWrite(led1Pin, LOW); // ปิด LED
delay(1000);
}
}
// --- Task 2: กระพริบ LED 2 ทุกๆ 0.5 วินาที ---
void task2(void *pvParameters) {
Serial.println("Task 2 เริ่มทำงาน");
while(1) {
digitalWrite(led2Pin, HIGH);
delay(500);
digitalWrite(led2Pin, LOW);
delay(500);
}
}
void setup() {
Serial.begin(115200);
pinMode(led1Pin, OUTPUT);
pinMode(led2Pin, OUTPUT);
Serial.println("สร้าง Tasks...");
// สร้าง Task 1 — ใช้งาน CPU core 0
xTaskCreate(
task1, // ฟังก์ชัน Task
"Task1", // ชื่อ Task (สำหรับ debug)
2048, // Stack size (ใน bytes)
NULL, // Parameters
1, // Priority (1 = ต่ำ)
&Task1Handle // Task handle
);
// สร้าง Task 2 — ใช้งาน CPU core 1
xTaskCreatePinnedToCore(
task2, // ฟังก์ชัน Task
"Task2", // ชื่อ Task
2048, // Stack size
NULL, // Parameters
1, // Priority
&Task2Handle, // Task handle
1 // Core ID (0 หรือ 1)
);
Serial.println("Tasks ถูกสร้างแล้ว!");
}
void loop() {
// ไม่ต้องทำอะไรใน loop() เพราะ Tasks ทำงานอยู่
vTaskDelay(1000); // หน่วงเวลา 1 วินาที
}💡 จุดสำคัญ: ESP32 มี 2 CPU cores — คุณสามารถกำหนดให้ Task ทำงานบน core ใดก็ได้ ซึ่งช่วยปรับปรุงประสิทธิภาพได้มาก!
📬 Queues (คิว) — ส่งข้อมูลระหว่าง Tasks
Queue เป็นวิธีที่ปลอดภัยในการส่งข้อมูลระหว่าง Tasks คิวทำงานเหมือน FIFO (First-In-First-Out) — ข้อมูลที่ใส่เข้าไปก่อนจะออกมาก่อน
ตัวอย่าง: ส่งข้อมูล Sensor ผ่าน Queue
#include <Arduino.h>
// Queue handle
QueueHandle_t sensorQueue;
// โครงสร้างข้อมูล sensor
struct SensorData {
float temperature;
float humidity;
unsigned long timestamp;
};
// --- Task 1: อ่านค่า Sensor และส่งเข้า Queue ---
void sensorTask(void *pvParameters) {
SensorData data;
while(1) {
// จำลองการอ่านค่า sensor
data.temperature = random(200, 350) / 10.0; // 20.0 - 35.0 °C
data.humidity = random(400, 800) / 10.0; // 40.0 - 80.0 %
data.timestamp = millis();
// ส่งข้อมูลเข้า Queue (รอ 1000 ticks ถ้า Queue เต็ม)
if (xQueueSend(sensorQueue, &data, 1000) == pdPASS) {
Serial.print("ส่งข้อมูล: ");
Serial.print(data.temperature);
Serial.print("°C, ");
Serial.print(data.humidity);
Serial.println("%");
} else {
Serial.println("❌ Queue เต็ม!");
}
vTaskDelay(2000); // อ่านค่าทุกๆ 2 วินาที
}
}
// --- Task 2: รับข้อมูลจาก Queue และประมวลผล ---
void processingTask(void *pvParameters) {
SensorData receivedData;
while(1) {
// รับข้อมูลจาก Queue (รอ 5000 ticks)
if (xQueueReceive(sensorQueue, &receivedData, 5000) == pdPASS) {
Serial.print("รับข้อมูล: ");
Serial.print(receivedData.temperature);
Serial.print("°C, ");
Serial.print(receivedData.humidity);
Serial.print("% (เวลา: ");
Serial.print(receivedData.timestamp);
Serial.println("ms)");
// ประมวลผลเพิ่มเติม เช่น ส่งไป CynoIoT
// ...
} else {
Serial.println("⏱️ หมดเวลา — ไม่มีข้อมูลใน Queue");
}
}
}
void setup() {
Serial.begin(115200);
// สร้าง Queue — ขนาด 5, แต่ละช่องเก็บ SensorData
sensorQueue = xQueueCreate(5, sizeof(SensorData));
if (sensorQueue == NULL) {
Serial.println("❌ ไม่สามารถสร้าง Queue ได้!");
return;
}
// สร้าง Tasks
xTaskCreate(sensorTask, "SensorTask", 2048, NULL, 2, NULL);
xTaskCreate(processingTask, "ProcessingTask", 2048, NULL, 1, NULL);
Serial.println("เริ่มต้นระบบ!");
}
void loop() {
vTaskDelay(1000);
}✅ ข้อดีของ Queue: ปลอดภัยต่อ race conditions, ง่ายต่อการ debug, และสามารถ buffer ข้อมูลได้
🔐 Semaphores (เซมาฟอร์) — ป้องกันการชนกัน
Semaphore ใช้ควบคุมการเข้าถึงทรัพยากรที่ใช้ร่วมกัน (shared resources) เช่น Serial, I2C, หรือตัวแปร global เพื่อป้องกันการชนกัน (race condition)
ตัวอย่าง: ใช้ Mutex ป้องกัน Serial
#include <Arduino.h>
// Mutex handle
SemaphoreHandle_t serialMutex;
// --- Task 1: พิมพ์ข้อความ ---
void task1(void *pvParameters) {
while(1) {
// ขออนุญาตใช้งาน Serial
if (xSemaphoreTake(serialMutex, 100) == pdTRUE) {
Serial.println("Task 1: กำลังใช้งาน Serial...");
delay(500);
Serial.println("Task 1: เสร็จสิ้น");
// คืน Serial ให้ Task อื่นใช้
xSemaphoreGive(serialMutex);
}
vTaskDelay(1000);
}
}
// --- Task 2: พิมพ์ข้อความ ---
void task2(void *pvParameters) {
while(1) {
if (xSemaphoreTake(serialMutex, 100) == pdTRUE) {
Serial.println("Task 2: กำลังใช้งาน Serial...");
delay(300);
Serial.println("Task 2: เสร็จสิ้น");
xSemaphoreGive(serialMutex);
}
vTaskDelay(1500);
}
}
void setup() {
Serial.begin(115200);
// สร้าง Mutex (สำหรับทรัพยากรที่ใช้ร่วมกัน)
serialMutex = xSemaphoreCreateMutex();
if (serialMutex == NULL) {
Serial.println("❌ ไม่สามารถสร้าง Mutex ได้!");
return;
}
// สร้าง Tasks
xTaskCreate(task1, "Task1", 2048, NULL, 1, NULL);
xTaskCreate(task2, "Task2", 2048, NULL, 1, NULL);
Serial.println("เริ่มต้นระบบ!");
}
void loop() {
vTaskDelay(1000);
}ประเภทของ Semaphores:
- Binary Semaphore: มีค่าได้ 0 หรือ 1 (เหมือนสวิตช์)
- Counting Semaphore: มีค่าได้ 0, 1, 2, ... (เหมือนที่จอดรถ)
- Mutex: Binary Semaphore พิเศษสำหรับ shared resources
⏰ Timers (ตัวจับเวลา) — ทำงานเป็นรอบ
Timer ใช้สำหรับทำงานเป็นรอบ (periodic) เช่น อ่านค่า sensor ทุกๆ 5 นาที, ส่ง heartbeat ไป server, หรือบันทึกข้อมูล
ตัวอย่าง: Timer อ่านค่า Sensor ทุกๆ 5 วินาที
#include <Arduino.h>
// Timer handle
TimerHandle_t sensorTimer;
// --- Timer Callback: ฟังก์ชันที่ถูกเรียกเมื่อ Timer หมดเวลา ---
void onTimerExpired(TimerHandle_t xTimer) {
Serial.println("⏰ Timer หมดเวลา! อ่านค่า sensor...");
// จำลองการอ่านค่า sensor
float temperature = random(200, 350) / 10.0;
Serial.print("อุณหภูมิ: ");
Serial.print(temperature);
Serial.println("°C");
}
void setup() {
Serial.begin(115200);
// สร้าง Timer — ทำงานทุกๆ 5000ms (5 วินาที)
sensorTimer = xTimerCreate(
"SensorTimer", // ชื่อ Timer
pdMS_TO_TICKS(5000), // Period (5000ms)
pdTRUE, // Auto-reload (true = ทำงานซ้ำ)
0, // Timer ID
onTimerExpired // Callback function
);
if (sensorTimer == NULL) {
Serial.println("❌ ไม่สามารถสร้าง Timer ได้!");
return;
}
// เริ่มทำงาน Timer
if (xTimerStart(sensorTimer, 0) == pdPASS) {
Serial.println("✅ Timer เริ่มทำงานแล้ว!");
} else {
Serial.println("❌ ไม่สามารถเริ่ม Timer ได้!");
}
}
void loop() {
// ไม่ต้องทำอะไรใน loop()
vTaskDelay(1000);
}💡 จุดสำคัญ: Timer ทำงานใน context ของ Timer Service Task ดังนั้น callback function ต้องเร็วและไม่ควรใช้ delay()
🏗️ โปรเจกต์ตัวอย่าง: Smart Plant Monitoring System
นี่คือตัวอย่างโปรเจกต์จริงที่รวมทุกอย่างเข้าด้วยกัน — Tasks, Queues, Semaphores, และ Timers
🌱 ฟีเจอร์ของระบบ:
- ✓ อ่านค่า soil moisture ทุกๆ 10 วินาที (Timer)
- ✓ ส่งข้อมูลไปยัง Queue สำหรับประมวลผล
- ✓ ตรวจสอบและควบคุม water pump (Tasks + Semaphore)
- ✓ ส่งข้อมูลไป CynoIoT Platform
โค้ดเต็ม (Full Code):
#include <Arduino.h>
// --- Handles ---
QueueHandle_t sensorQueue;
SemaphoreHandle_t pumpMutex;
TimerHandle_t readTimer;
// --- Pins ---
const int MOISTURE_PIN = 34;
const int PUMP_PIN = 26;
// --- Data structures ---
struct PlantData {
int moisture; // 0-100%
bool needsWater; // true/false
unsigned long time;
};
// --- Task 1: อ่านค่า Sensor (Timer-triggered) ---
void readSensorCallback(TimerHandle_t xTimer) {
PlantData data;
// อ่านค่า soil moisture (0-4095)
int rawValue = analogRead(MOISTURE_PIN);
data.moisture = map(rawValue, 0, 4095, 100, 0); // แปลงเป็น %
data.needsWater = (data.moisture < 30); // ต่ำกว่า 30% ต้องรดน้ำ
data.time = millis();
// ส่งเข้า Queue
if (xQueueSend(sensorQueue, &data, 100) != pdPASS) {
Serial.println("⚠️ Queue เต็ม!");
}
}
// --- Task 2: ควบคุม Water Pump ---
void pumpTask(void *pvParameters) {
PlantData data;
while(1) {
// รับข้อมูลจาก Queue
if (xQueueReceive(sensorQueue, &data, portMAX_DELAY) == pdPASS) {
Serial.print("ความชื้นดิน: ");
Serial.print(data.moisture);
Serial.println("%");
// ถ้าดินแห้ง ให้รดน้ำ
if (data.needsWater) {
// ใช้ Mutex ป้องกันการชนกัน
if (xSemaphoreTake(pumpMutex, 100) == pdTRUE) {
Serial.println("💧 กำลังรดน้ำ...");
digitalWrite(PUMP_PIN, HIGH);
delay(3000); // รดน้ำ 3 วินาที
digitalWrite(PUMP_PIN, LOW);
Serial.println("✅ รดน้ำเสร็จสิ้น");
xSemaphoreGive(pumpMutex);
}
} else {
Serial.println("✨ ดินชุ่มอยู่แล้ว");
}
// ส่งข้อมูลไป CynoIoT (จำลอง)
Serial.println("📡 ส่งข้อมูลไป CynoIoT...");
// TODO: เพิ่มโค้ดส่งไป CynoIoT ที่นี่
}
}
}
// --- Task 3: Status LED (กระพริบเพื่อแสดงสถานะ) ---
void statusTask(void *pvParameters) {
const int STATUS_PIN = 2;
pinMode(STATUS_PIN, OUTPUT);
while(1) {
digitalWrite(STATUS_PIN, HIGH);
vTaskDelay(500);
digitalWrite(STATUS_PIN, LOW);
vTaskDelay(500);
}
}
void setup() {
Serial.begin(115200);
pinMode(MOISTURE_PIN, INPUT);
pinMode(PUMP_PIN, OUTPUT);
digitalWrite(PUMP_PIN, LOW);
Serial.println("🌱 เริ่มต้น Smart Plant Monitoring System");
// สร้าง Queue (ขนาด 5)
sensorQueue = xQueueCreate(5, sizeof(PlantData));
// สร้าง Mutex สำหรับ Pump
pumpMutex = xSemaphoreCreateMutex();
// สร้าง Timer — อ่าน sensor ทุกๆ 10 วินาที
readTimer = xTimerCreate(
"ReadTimer",
pdMS_TO_TICKS(10000),
pdTRUE,
0,
readSensorCallback
);
// สร้าง Tasks
xTaskCreate(pumpTask, "PumpTask", 4096, NULL, 2, NULL);
xTaskCreate(statusTask, "StatusTask", 2048, NULL, 1, NULL);
// เริ่มทำงาน Timer
xTimerStart(readTimer, 0);
Serial.println("✅ ระบบพร้อมใช้งาน!");
}
void loop() {
vTaskDelay(1000);
}💡 Best Practices — เขียนโค้ด FreeRTOS อย่างมืออาชีพ
✅ ใช้ Stack Size ที่เหมาะสม
เริ่มต้นที่ 2048-4096 bytes และปรับตามความจำเป็น ใช้ uxTaskGetStackHighWaterMark() ตรวจสอบการใช้งาน stack
✅ หลีกเลี่ยง delay() ใน Tasks
ใช้ vTaskDelay() แทน delay() เพื่อให้ RTOS สามารถ switch tasks ได้
✅ ใช้ Priority อย่างชาญฉลาด
งานสำคัญ (เช่น safety-critical) ควรมี priority สูงกว่า แต่ระวัง priority inversion
✅ ตรวจสอบ Return Values
ตรวจสอบ pdPASS / pdFAIL ทุกครั้งที่เรียกใช้ Queue, Semaphore, หรือ Timer functions
✅ Watchdog Timer
ใช้ ESP32's built-in watchdog timer เพื่อรีเซ็ตระบบถ้า Task ค้าง
❌ หลีกเลี่ยง Global Variables
ถ้าจำเป็นต้องใช้ ต้องปกป้องด้วย Mutex หรือใช้ Queue แทน
❌ อย่าใช้ Loop() เมื่อใช้ FreeRTOS
ใส่ vTaskDelay() ใน loop() หรือเว้นว่างไว้เลย
🔧 แก้ปัญหาที่พบบ่อย
❓ Task ค้าง/ไม่ทำงาน
สาเหตุ: Stack overflow หรือ priority ไม่ถูกต้อง
วิธีแก้: เพิ่ม stack size, ตรวจสอบ uxTaskGetStackHighWaterMark(), ปรับ priority
❓ Queue เต็มเสมอ
สาเหตุ: Producer ส่งข้อมูลเร็วกว่า Consumer รับ
วิธีแก้: เพิ่มขนาด Queue, ให้ Consumer ทำงานเร็วขึ้น, หรือใช้ timeout
❓ Serial Monitor แสดงข้อความปนกัน
สาเหตุ: Race condition บน Serial port
วิธีแก้: ใช้ Mutex ปกป้อง Serial calls
❓ ESP32 รีเซ็ตเอง (Watchdog Reset)
สาเหตุ: Task ใช้เวลานานเกินไป (blocking)
วิธีแก้: แบ่งงานเป็นส่วนเล็กๆ, ใช้ vTaskDelay(), ปิด watchdog ถ้าจำเป็น
❓ หน่วยความจำไม่พอ
สาเหตุ: Stack sizes ใหญ่เกินไป หรือสร้าง Tasks มากเกินไป
วิธีแก้: ลด stack sizes, ลดจำนวน Tasks, ใช้ xTaskGetFreeHeap() ตรวจสอบ
🎓 สรุป
FreeRTOS บน ESP32 เปิดโลกใหม่ให้กับโปรเจกต์ IoT ของคุณ — ทำให้สามารถสร้างระบบที่ซับซ้อนและทำงานได้อย่างมีประสิทธิภาพ ด้วย Tasks, Queues, Semaphores, และ Timers คุณมีเครื่องมือครบครันสำหรับสร้างโปรเจกต์ระดับมืออาชีพ
🚀 โปรเจกต์ถัดไปที่ควรลอง
- ✓ สร้าง Multi-Sensor IoT Station ด้วย Tasks หลายตัว
- ✓ สร้าง Real-Time Data Logger ด้วย Queues
- ✓ เชื่อมต่อ CynoIoT Platform สำหรับจัดการข้อมูล IoT
- ✓ สร้าง Smart Home System ที่ควบคุมอุปกรณ์หลายตัวพร้อมกัน
พร้อมที่จะสร้างโปรเจกต์ IoT ระดับโปรแล้วหรือยัง?
FreeRTOS ช่วยให้คุณทำสิ่งที่เคยทำไม่ได้กับ ESP32!
ดูโปรเจกต์ตัวอย่าง →