ESP32 & ESP8266

FreeRTOS pada ESP32: Multitasking Real-time

TOKEN

Kuasai multitasking pada ESP32 dengan FreeRTOS β€” dari konsep dasar task hingga proyek concurrent sensor reading

1. Apa Itu RTOS?

RTOS (Real-Time Operating System) adalah sistem operasi yang dirancang untuk memproses data dan merespons event dalam waktu yang deterministik. Berbeda dengan OS umum seperti Windows atau Linux yang mengoptimalkan throughput, RTOS menjamin bahwa tugas kritis selesai dalam batas waktu yang ditentukan (deadline).

ESP32 memiliki dual-core prosesor, dan FreeRTOS sudah terintegrasi di dalam ESP-IDF (Espressif IoT Development Framework) secara default. Bahkan ketika Anda menggunakan Arduino IDE, FreeRTOS tetap berjalan di background β€” Anda hanya perlu memanfaatkannya secara eksplisit.

Mengapa FreeRTOS Penting untuk ESP32?

Arsitektur FreeRTOS pada ESP32
πŸ“±
Arduino App
Kode User Anda
βš™οΈ
ESP-IDF / Arduino API
Hardware Abstraction
↓
πŸ”„
FreeRTOS Kernel
Task Scheduler & IPC
πŸ“‘
WiFi / BT Stack
Berjalan di Core 0
↓
⚑
Core 0 (Protocol)
WiFi, BLE, System Tasks
⚑
Core 1 (App)
User Tasks Default
⚠️ Peringatan

Secara default, WiFi dan Bluetooth ESP32 berjalan di Core 0, sementara task user (termasuk setup() dan loop()) berjalan di Core 1. Ini menghindari konflik dengan protokol stack yang sensitif terhadap timing.

2. Dasar-dasar FreeRTOS

FreeRTOS adalah RTOS ringan open-source yang paling banyak digunakan di dunia. Pada ESP32, FreeRTOS sudah terintegrasi dan menggunakan konfigurasi SMP (Symmetric Multi-Processing) untuk memanfaatkan dual-core.

Konsep Inti FreeRTOS

Konsep Deskripsi
TaskUnit eksekusi yang memiliki stack dan prioritas sendiri
SchedulerMenentukan task mana yang berjalan berdasarkan prioritas
SemaphoreMekanisme sinkronisasi antar task
MutexMekanisme untuk melindungi resource bersama
QueueFIFO buffer untuk komunikasi antar task
TimerCallback yang dijalankan secara periodik
Event GroupBit flag untuk sinkronisasi multi-event

Prioritas Task

FreeRTOS menggunakan preemptive priority-based scheduling. Task dengan prioritas lebih tinggi selalu mendapat kesempatan eksekusi terlebih dahulu. Jika dua task memiliki prioritas yang sama, scheduler akan bergantian menjalankan keduanya (time-slicing).

C++ β€” Prioritas Task di ESP32
/*
 * Prioritas Task pada ESP32 FreeRTOS:
 *
 * Prioritas 0  : Idle Task (lowest)
 * Prioritas 1  : Default Arduino loop()
 * Prioritas 2-24 : User tasks
 * Prioritas 25 : Timer task
 *
 * Semakin tinggi angka = semakin tinggi prioritas
 *
 * Tips: Gunakan prioritas 1-5 untuk task normal,
 *       prioritas >10 untuk task kritis
 */

3. Task Management

Task adalah unit eksekusi fundamental dalam FreeRTOS. Setiap task memiliki stack memory sendiri, priority level, dan state (Running, Ready, Blocked, Suspended).

Membuat Task Baru

C++ β€” Membuat FreeRTOS Task
// Membuat Task pada FreeRTOS ESP32
// BeebaneLabs - https://beebanelabs.pages.dev

// Deklarasi handle task
TaskHandle_t TaskSensorHandle;
TaskHandle_t TaskDisplayHandle;

// Task 1: Membaca sensor setiap 2 detik
void TaskSensor(void *pvParameters) {
  (void) pvParameters;

  for (;;) {  // Infinite loop (task tidak pernah return)
    float suhu = readTemperature();
    float kelembaban = readHumidity();

    Serial.print("[Sensor] Suhu: ");
    Serial.print(suhu);
    Serial.print("Β°C, Kelembaban: ");
    Serial.print(kelembaban);
    Serial.println("%");

    vTaskDelay(pdMS_TO_TICKS(2000)); // Delay 2 detik
  }
}

// Task 2: Update display setiap 500ms
void TaskDisplay(void *pvParameters) {
  (void) pvParameters;

  for (;;) {
    updateOLEDDisplay();
    vTaskDelay(pdMS_TO_TICKS(500));
  }
}

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

  // Buat Task Sensor di Core 1, prioritas 2
  xTaskCreatePinnedToCore(
    TaskSensor,        // Fungsi task
    "TaskSensor",      // Nama task (untuk debug)
    4096,              // Stack size (bytes)
    NULL,              // Parameter
    2,                 // Prioritas
    &TaskSensorHandle, // Handle
    1                  // Core (0 atau 1)
  );

  // Buat Task Display di Core 1, prioritas 1
  xTaskCreatePinnedToCore(
    TaskDisplay,
    "TaskDisplay",
    2048,
    NULL,
    1,
    &TaskDisplayHandle,
    1
  );

  // loop() tetap berjalan sebagai task default
}

void loop() {
  // Task utama (loop) juga bisa digunakan
  // Biasanya untuk logika non-kritis
  vTaskDelay(pdMS_TO_TICKS(1000));
}

Parameter xTaskCreatePinnedToCore

Parameter Tipe Deskripsi
pvTaskCodevoid(*)(void*)Fungsi yang menjadi body task
pcNameconst char*Nama task (maks 16 karakter)
usStackDepthuint32_tUkuran stack dalam bytes
pvParametersvoid*Parameter yang dikirim ke fungsi task
uxPriorityUBaseType_tPrioritas task (0-25)
pxCreatedTaskTaskHandle_t*Handle task (untuk kontrol nanti)
xCoreIDBaseType_tCore yang digunakan (0, 1, atau tskNO_AFFINITY)

Mengontrol Task

C++ β€” Task Control Functions
// Menangguhkan task (pause)
vTaskSuspend(TaskSensorHandle);

// Melanjutkan task (resume)
vTaskResume(TaskSensorHandle);

// Menghapus task
vTaskDelete(TaskSensorHandle);

// Mengubah prioritas task
vTaskPrioritySet(TaskSensorHandle, 5);

// Mendapatkan prioritas saat ini
UBaseType_t prio = uxTaskPriorityGet(TaskSensorHandle);

// Mendapatkan informasi task (untuk debugging)
char* taskList = (char*)malloc(1024);
vTaskList(taskList);
Serial.println("Nama       Status  Prio  Stack  Num");
Serial.println(taskList);
free(taskList);
πŸ’‘ Tips

Gunakan vTaskList() untuk debugging β€” fungsi ini menampilkan semua task aktif beserta status (Running/Ready/Blocked/Suspended), prioritas, sisa stack, dan nomor task. Sangat berguna untuk memantau kesehatan sistem.

4. Semaphore & Mutex

Ketika beberapa task mengakses resource yang sama (misalnya Serial port, I2C bus, atau shared variable), kita membutuhkan mekanisme sinkronisasi untuk menghindari race condition. FreeRTOS menyediakan Semaphore dan Mutex untuk tujuan ini.

Binary Semaphore vs Mutex

🚦
Binary Semaphore
Sinkronisasi
  • βœ… Sinkronisasi antar task
  • βœ… Sinkronisasi ISR β†’ Task
  • βœ… Tanpa ownership
  • ❌ Tidak ada priority inheritance
πŸ”’
Mutex
Proteksi Resource
  • βœ… Proteksi resource bersama
  • βœ… Ada ownership
  • βœ… Priority inheritance
  • ❌ Tidak bisa dari ISR

Implementasi Mutex untuk Serial

C++ β€” Mutex Serial Print
// Mutex untuk melindungi Serial port
// BeebaneLabs - https://beebanelabs.pages.dev

SemaphoreHandle_t serialMutex;

void safePrint(const char* message) {
  if (xSemaphoreTake(serialMutex, portMAX_DELAY) == pdTRUE) {
    Serial.println(message);
    xSemaphoreGive(serialMutex);
  }
}

void Task1(void *pvParameters) {
  for (;;) {
    safePrint("[Task1] Membaca sensor suhu...");
    float temp = readTemperature();

    char buf[64];
    sprintf(buf, "[Task1] Suhu: %.1fΒ°C", temp);
    safePrint(buf);

    vTaskDelay(pdMS_TO_TICKS(2000));
  }
}

void Task2(void *pvParameters) {
  for (;;) {
    safePrint("[Task2] Membaca sensor kelembaban...");
    float hum = readHumidity();

    char buf[64];
    sprintf(buf, "[Task2] Kelembaban: %.1f%%", hum);
    safePrint(buf);

    vTaskDelay(pdMS_TO_TICKS(3000));
  }
}

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

  // Buat mutex
  serialMutex = xSemaphoreCreateMutex();

  if (serialMutex == NULL) {
    Serial.println("Gagal membuat mutex!");
    while(1);
  }

  xTaskCreatePinnedToCore(Task1, "Task1", 4096, NULL, 2, NULL, 1);
  xTaskCreatePinnedToCore(Task2, "Task2", 4096, NULL, 2, NULL, 1);
}

void loop() {
  vTaskDelay(pdMS_TO_TICKS(1000));
}

Counting Semaphore untuk Sinkronisasi ISR

C++ β€” Counting Semaphore dengan ISR
// Counting Semaphore untuk interrupt handling
SemaphoreHandle_t interruptSemaphore;

// ISR (Interrupt Service Routine)
void IRAM_ATTR buttonISR() {
  BaseType_t xHigherPriorityTaskWoken = pdFALSE;
  xSemaphoreGiveFromISR(interruptSemaphore,
                        &xHigherPriorityTaskWoken);
  if (xHigherPriorityTaskWoken) {
    portYIELD_FROM_ISR();
  }
}

void TaskButton(void *pvParameters) {
  int pressCount = 0;
  for (;;) {
    // Tunggu semaphore dari ISR
    if (xSemaphoreTake(interruptSemaphore,
                       portMAX_DELAY) == pdTRUE) {
      pressCount++;
      Serial.printf("Tombol ditekan! Count: %d\n", pressCount);
    }
  }
}

void setup() {
  Serial.begin(115200);
  interruptSemaphore = xSemaphoreCreateBinary();

  pinMode(0, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(0), buttonISR, FALLING);

  xTaskCreatePinnedToCore(TaskButton, "Button", 2048,
                          NULL, 3, NULL, 1);
}

void loop() {
  vTaskDelay(portMAX_DELAY);
}

5. Queue: Komunikasi Antar Task

Queue adalah mekanisme utama untuk komunikasi antar task dalam FreeRTOS. Queue bersifat thread-safe (aman untuk multi-task) dan menyediakan buffering data dengan FIFO (First In, First Out).

C++ β€” Queue untuk Sensor Data
// Queue untuk komunikasi antar task
// BeebaneLabs - https://beebanelabs.pages.dev

// Struct untuk data sensor
typedef struct {
  float temperature;
  float humidity;
  float pressure;
  unsigned long timestamp;
} SensorData_t;

QueueHandle_t sensorQueue;

// Task Producer: Membaca sensor dan kirim ke queue
void TaskSensorRead(void *pvParameters) {
  SensorData_t data;

  for (;;) {
    data.temperature = readTemperature();
    data.humidity = readHumidity();
    data.pressure = readPressure();
    data.timestamp = millis();

    // Kirim data ke queue (non-blocking)
    if (xQueueSend(sensorQueue, &data, pdMS_TO_TICKS(100))
        != pdTRUE) {
      Serial.println("Queue penuh! Data dibuang.");
    }

    vTaskDelay(pdMS_TO_TICKS(1000));
  }
}

// Task Consumer: Membaca data dari queue dan proses
void TaskDataProcess(void *pvParameters) {
  SensorData_t receivedData;

  for (;;) {
    // Tunggu data dari queue (blocking)
    if (xQueueReceive(sensorQueue, &receivedData,
                      portMAX_DELAY) == pdTRUE) {
      Serial.printf("Suhu: %.1fΒ°C, Kelembaban: %.1f%%, "
                     "Tekanan: %.1fhPa @ %lu ms\n",
                     receivedData.temperature,
                     receivedData.humidity,
                     receivedData.pressure,
                     receivedData.timestamp);

      // Proses data: kirim ke MQTT, simpan ke SD, dll
      if (receivedData.temperature > 35.0) {
        triggerAlarm("Suhu tinggi!");
      }
    }
  }
}

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

  // Buat queue dengan kapasitas 10 elemen
  sensorQueue = xQueueCreate(10, sizeof(SensorData_t));

  if (sensorQueue == NULL) {
    Serial.println("Gagal membuat queue!");
    while(1);
  }

  // Buat producer dan consumer task
  xTaskCreatePinnedToCore(TaskSensorRead, "Producer", 4096,
                          NULL, 2, NULL, 1);
  xTaskCreatePinnedToCore(TaskDataProcess, "Consumer", 4096,
                          NULL, 2, NULL, 1);
}

void loop() {
  vTaskDelay(pdMS_TO_TICKS(1000));
}
ℹ️ Producer-Consumer Pattern

Pattern Producer-Consumer dengan Queue adalah pattern paling umum dalam embedded FreeRTOS. Producer menghasilkan data, Consumer memprosesnya. Ini memisahkan concerns dan memungkinkan kedua task berjalan pada kecepatan berbeda tanpa kehilangan data.

6. Software Timer

FreeRTOS menyediakan software timer yang memungkinkan Anda menjalankan callback function secara periodik atau one-shot tanpa membuat task khusus. Timer berjalan di task daemon khusus (timer task) dengan prioritas tinggi.

C++ β€” Software Timer FreeRTOS
// Software Timer pada FreeRTOS
// BeebaneLabs - https://beebanelabs.pages.dev

TimerHandle_t heartbeatTimer;
TimerHandle_t watchdogTimer;
int watchdogCounter = 0;

// Callback: Heartbeat (periodik setiap 5 detik)
void heartbeatCallback(TimerHandle_t xTimer) {
  // Blink LED sebagai heartbeat
  digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
  Serial.printf("[Heartbeat] Alive! Uptime: %lu detik\n",
                millis() / 1000);
}

// Callback: Watchdog check (periodik setiap 30 detik)
void watchdogCallback(TimerHandle_t xTimer) {
  watchdogCounter++;
  if (watchdogCounter > 3) {
    Serial.println("[Watchdog] System hang detected! Restart...");
    ESP.restart();
  }
}

// Reset watchdog (panggil dari task utama)
void feedWatchdog() {
  watchdogCounter = 0;
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_BUILTIN, OUTPUT);

  // Buat timer heartbeat (periodik)
  heartbeatTimer = xTimerCreate(
    "Heartbeat",           // Nama timer
    pdMS_TO_TICKS(5000),   // Period: 5 detik
    pdTRUE,                // Auto-reload (periodik)
    (void*)0,              // ID timer
    heartbeatCallback      // Callback function
  );

  // Buat timer watchdog (periodik)
  watchdogTimer = xTimerCreate(
    "Watchdog",
    pdMS_TO_TICKS(30000),  // Period: 30 detik
    pdTRUE,
    (void*)1,
    watchdogCallback
  );

  // Mulai timer
  xTimerStart(heartbeatTimer, 0);
  xTimerStart(watchdogTimer, 0);
}

void loop() {
  // Lakukan pekerjaan utama
  doMainWork();

  // Feed watchdog setiap iterasi
  feedWatchdog();

  delay(100);
}

7. Memory Management

ESP32 memiliki 520 KB SRAM yang dibagi menjadi beberapa segmen. Memahami memory management sangat penting karena alokasi yang buruk dapat menyebabkan stack overflow atau heap exhaustion.

Pembagian Memory ESP32

Segmen Ukuran Fungsi
DRAM (Data RAM)~320 KBHeap, stack, global variable
IRAM (Instruction RAM)~128 KBKode yang sering dieksekusi, ISR
Cache~64 KBCache untuk flash access
WiFi/BT RAM~70 KBDedikasi untuk protocol stack

Best Practices Memory Management

C++ β€” Memory Monitoring
// Monitoring penggunaan memory
// BeebaneLabs - https://beebanelabs.pages.dev

void printMemoryInfo() {
  Serial.println("=== Memory Info ===");

  // Free heap
  Serial.printf("Free Heap: %d bytes\n", ESP.getFreeHeap());
  Serial.printf("Min Free Heap: %d bytes\n",
                ESP.getMinFreeHeap());
  Serial.printf("Max Alloc Heap: %d bytes\n",
                ESP.getMaxAllocHeap());

  // PSRAM (jika ada)
  if (psramFound()) {
    Serial.printf("Free PSRAM: %d bytes\n",
                  ESP.getFreePsram());
  }

  // Stack watermark per task
  Serial.printf("Task 'Sensor' stack high water mark: %d\n",
    uxTaskGetStackHighWaterMark(TaskSensorHandle));
}

// Best practices:
// 1. Hindari malloc()/free() β€” gunakan static allocation
// 2. Monitor stack usage per task
// 3. Gunakan PSRAM untuk data besar (>100KB)
// 4. Hindari String class Arduino β€” gunakan char array
// 5. Pre-allocate buffer di setup(), bukan di loop()

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

  // Static allocation (lebih aman)
  static uint8_t buffer[1024];

  // Stack size yang cukup untuk task
  // Minimum 2048 bytes, rekomendasi 4096+ untuk task kompleks
  xTaskCreatePinnedToCore(
    TaskSensor, "Sensor",
    4096,       // Stack 4KB β€” cukup untuk kebanyakan task
    NULL, 2, NULL, 1
  );
}

void loop() {
  // Print memory info setiap 10 detik
  static unsigned long lastPrint = 0;
  if (millis() - lastPrint > 10000) {
    printMemoryInfo();
    lastPrint = millis();
  }
  vTaskDelay(pdMS_TO_TICKS(1000));
}
⚠️ Stack Overflow

Stack overflow adalah masalah paling umum dalam FreeRTOS. Gejalanya: reboot acak, hang, atau data corrupt. Solusinya: (1) Tambah ukuran stack, (2) Kurangi variabel lokal besar, (3) Aktifkan configCHECK_FOR_STACK_OVERFLOW di FreeRTOSConfig.h untuk debugging.

8. Proyek: Concurrent Sensor Reading

Sekarang kita akan menggabungkan semua konsep yang telah dipelajari ke dalam satu proyek praktis: Weather Station yang membaca 3 sensor secara concurrent, memproses data, dan menampilkan ke OLED β€” semuanya berjalan paralel.

Arsitektur Proyek

Diagram: Weather Station Concurrent Architecture
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚               ESP32 Weather Station                β”‚
  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
  β”‚                                                    β”‚
  β”‚  Core 1 (User Tasks):                              β”‚
  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚
  β”‚  β”‚TaskSensorβ”‚  β”‚TaskMQTT  β”‚  β”‚TaskDisplay   β”‚     β”‚
  β”‚  β”‚ Prio: 3  β”‚  β”‚ Prio: 2  β”‚  β”‚ Prio: 1      β”‚     β”‚
  β”‚  β”‚ 4096 stk β”‚  β”‚ 8192 stk β”‚  β”‚ 4096 stk     β”‚     β”‚
  β”‚  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β–²β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β–²β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚
  β”‚       β”‚              β”‚               β”‚             β”‚
  β”‚       β–Ό              β”‚               β”‚             β”‚
  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚
  β”‚  β”‚        Queue: sensorDataQueue            β”‚      β”‚
  β”‚  β”‚        (10 x SensorData_t)               β”‚      β”‚
  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚
  β”‚                                                    β”‚
  β”‚  Core 0 (System): WiFi, BLE, TCP/IP Stack          β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
C++ β€” weather_station_concurrent.ino
// Weather Station - Concurrent Sensor Reading
// BeebaneLabs - https://beebanelabs.pages.dev
// Proyek gabungan: FreeRTOS Task + Queue + Mutex + Timer

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHT.h>
#include <WiFi.h>
#include <PubSubClient.h>

// ========== Konfigurasi Pin ==========
#define DHT_PIN       4
#define DHT_TYPE      DHT22
#define BMP_SDA       21
#define BMP_SCL       22
#define LED_PIN       2

// ========== WiFi & MQTT ==========
const char* WIFI_SSID     = "NamaWiFi";
const char* WIFI_PASS     = "PasswordWiFi";
const char* MQTT_SERVER   = "broker.hivemq.com";
const int   MQTT_PORT     = 1883;

// ========== Object Global ==========
DHT dht(DHT_PIN, DHT_TYPE);
Adafruit_SSD1306 display(128, 64, &Wire, -1);
WiFiClient espClient;
PubSubClient mqttClient(espClient);

// ========== FreeRTOS Objects ==========
typedef struct {
  float temperature;
  float humidity;
  float pressure;
  float altitude;
  unsigned long timestamp;
} SensorData_t;

QueueHandle_t sensorQueue;
SemaphoreHandle_t displayMutex;
SemaphoreHandle_t mqttMutex;
TimerHandle_t heartbeatTimer;

// ========== Heartbeat LED ==========
void heartbeatCb(TimerHandle_t xTimer) {
  digitalWrite(LED_PIN, !digitalRead(LED_PIN));
}

// ========== Task 1: Sensor Reading ==========
void TaskSensorRead(void *pvParameters) {
  SensorData_t data;

  for (;;) {
    // Baca DHT22
    data.temperature = dht.readTemperature();
    data.humidity = dht.readHumidity();

    // Baca BMP280 (simulated)
    data.pressure = 1013.25 + random(-10, 10) / 10.0;
    data.altitude = 44330.0 * (1.0 - pow(data.pressure / 1013.25, 0.1903));
    data.timestamp = millis();

    if (!isnan(data.temperature) && !isnan(data.humidity)) {
      // Kirim ke queue
      if (xQueueSend(sensorQueue, &data,
                     pdMS_TO_TICKS(100)) != pdTRUE) {
        Serial.println("[Sensor] Queue penuh!");
      }
    } else {
      Serial.println("[Sensor] Error membaca DHT!");
    }

    vTaskDelay(pdMS_TO_TICKS(2000));
  }
}

// ========== Task 2: Display Update ==========
void TaskDisplayUpdate(void *pvParameters) {
  SensorData_t displayData;
  bool hasData = false;

  for (;;) {
    // Coba ambil data terbaru dari queue (non-blocking)
    SensorData_t tempData;
    while (xQueueReceive(sensorQueue, &tempData, 0) == pdTRUE) {
      displayData = tempData;
      hasData = true;
    }

    if (hasData && xSemaphoreTake(displayMutex,
                                  pdMS_TO_TICKS(100)) == pdTRUE) {
      display.clearDisplay();
      display.setTextSize(1);
      display.setTextColor(SSD1306_WHITE);

      display.setCursor(0, 0);
      display.println("== Weather Station ==");

      display.setCursor(0, 16);
      display.printf("Suhu:    %.1f C", displayData.temperature);

      display.setCursor(0, 28);
      display.printf("Kelemb:  %.1f %%", displayData.humidity);

      display.setCursor(0, 40);
      display.printf("Tekanan: %.1f hPa", displayData.pressure);

      display.setCursor(0, 52);
      display.printf("Up: %lu s", millis() / 1000);

      display.display();
      xSemaphoreGive(displayMutex);
    }

    vTaskDelay(pdMS_TO_TICKS(500));
  }
}

// ========== Task 3: MQTT Publisher ==========
void TaskMQTTPublish(void *pvParameters) {
  SensorData_t mqttData;

  for (;;) {
    // Ambil data dari queue
    if (xQueuePeek(sensorQueue, &mqttData,
                   portMAX_DELAY) == pdTRUE) {
      if (xSemaphoreTake(mqttMutex,
                         pdMS_TO_TICKS(5000)) == pdTRUE) {
        if (mqttClient.connected()) {
          char payload[128];
          snprintf(payload, sizeof(payload),
            "{\"temp\":%.1f,\"hum\":%.1f,\"pres\":%.1f}",
            mqttData.temperature,
            mqttData.humidity,
            mqttData.pressure);

          mqttClient.publish("beebane/weather", payload);
          Serial.printf("[MQTT] Published: %s\n", payload);
        }
        xSemaphoreGive(mqttMutex);
      }
    }

    vTaskDelay(pdMS_TO_TICKS(10000)); // Publish setiap 10 detik
  }
}

// ========== Setup ==========
void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);

  // Init sensors
  dht.begin();
  Wire.begin(BMP_SDA, BMP_SCL);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.clearDisplay();
  display.display();

  // Buat FreeRTOS objects
  sensorQueue = xQueueCreate(10, sizeof(SensorData_t));
  displayMutex = xSemaphoreCreateMutex();
  mqttMutex = xSemaphoreCreateMutex();

  // Buat heartbeat timer
  heartbeatTimer = xTimerCreate("Heart",
    pdMS_TO_TICKS(1000), pdTRUE, NULL, heartbeatCb);
  xTimerStart(heartbeatTimer, 0);

  // Buat tasks (semua di Core 1)
  xTaskCreatePinnedToCore(TaskSensorRead, "Sensor",
                          4096, NULL, 3, NULL, 1);
  xTaskCreatePinnedToCore(TaskDisplayUpdate, "Display",
                          4096, NULL, 1, NULL, 1);
  xTaskCreatePinnedToCore(TaskMQTTPublish, "MQTT",
                          8192, NULL, 2, NULL, 1);

  Serial.println("Weather Station started!");
}

void loop() {
  // Maintenance task
  static unsigned long lastMemCheck = 0;
  if (millis() - lastMemCheck > 30000) {
    Serial.printf("Free heap: %d bytes\n", ESP.getFreeHeap());
    lastMemCheck = millis();
  }
  vTaskDelay(pdMS_TO_TICKS(5000));
}
πŸ’‘ Tips Proyek

Perhatikan prioritas task: Sensor (3) > MQTT (2) > Display (1). Sensor harus paling tinggi karena timing-critical. MQTT membutuhkan stack lebih besar (8192) karena library WiFi/MQTT cukup boros stack. Display adalah task paling tidak kritis.

9. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial FreeRTOS di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu:

Pertanyaan 1: Di core berapa task user default (loop()) berjalan pada ESP32?

a) Core 0
b) Core 1
c) Kedua core secara bergantian
d) Random core

Pertanyaan 2: Apa perbedaan utama antara Mutex dan Binary Semaphore?

a) Mutex lebih cepat dari Semaphore
b) Mutex memiliki ownership dan priority inheritance
c) Binary Semaphore memiliki priority inheritance
d) Tidak ada perbedaan

Pertanyaan 3: Fungsi apa yang digunakan untuk membuat task yang terikat pada core tertentu?

a) xTaskCreate()
b) xTaskCreatePinnedToCore()
c) vTaskCreate()
d) xTaskSpawn()

Pertanyaan 4: Apa fungsi dari vTaskDelay(pdMS_TO_TICKS(1000))?

a) Menghapus task setelah 1 detik
b) Menangguhkan task selama 1 detik
c) Mengubah prioritas task menjadi 1
d) Mengirim data setiap 1 detik

Pertanyaan 5: Mengapa stack size 8192 direkomendasikan untuk task MQTT tapi hanya 4096 untuk task sensor?

a) MQTT lebih sering dieksekusi
b) Library WiFi/MQTT menggunakan lebih banyak stack
c) MQTT berjalan di Core 0
d) Sensor tidak membutuhkan stack
← Sebelumnya ESP32 Bluetooth BLE Selanjutnya β†’ Kalibrasi Sensor ESP32