บทความ: ESP32 FreeRTOS — ทำ Multitasking สำหรับโปรเจกต์ IoT ขั้นสูง

เรียนรู้วิธีการใช้งาน FreeRTOS บน ESP32 เพื่อสร้างโปรเจกต์ IoT ที่ทำงานหลายอย่างพร้อมกัน พร้อมตัวอย่างโค้ด Tasks, Queues, Semaphores และ Timers ภาษาไทย

📅 17 มีนาคม 2026⏱️ 15 นาที🎯 ระดับขั้นสูง

🚀 แนะนำ

เคยเจอปัญหานี้ไหม? คุณเขียนโค้ด 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!

ดูโปรเจกต์ตัวอย่าง →