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?
- Parallel Execution: Jalankan pembacaan sensor, komunikasi WiFi, dan display update secara bersamaan.
- Deterministic Timing: Respons event kritis (interrupt, sensor alarm) dalam waktu yang terprediksi.
- Resource Management: Kelola memori, peripheral, dan akses data dengan aman antar task.
- Scalability: Tambah fitur baru tanpa menulis ulang kode existing.
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 |
|---|---|
| Task | Unit eksekusi yang memiliki stack dan prioritas sendiri |
| Scheduler | Menentukan task mana yang berjalan berdasarkan prioritas |
| Semaphore | Mekanisme sinkronisasi antar task |
| Mutex | Mekanisme untuk melindungi resource bersama |
| Queue | FIFO buffer untuk komunikasi antar task |
| Timer | Callback yang dijalankan secara periodik |
| Event Group | Bit 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).
/* * 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
// 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 |
|---|---|---|
| pvTaskCode | void(*)(void*) | Fungsi yang menjadi body task |
| pcName | const char* | Nama task (maks 16 karakter) |
| usStackDepth | uint32_t | Ukuran stack dalam bytes |
| pvParameters | void* | Parameter yang dikirim ke fungsi task |
| uxPriority | UBaseType_t | Prioritas task (0-25) |
| pxCreatedTask | TaskHandle_t* | Handle task (untuk kontrol nanti) |
| xCoreID | BaseType_t | Core yang digunakan (0, 1, atau tskNO_AFFINITY) |
Mengontrol Task
// 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);
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
- β Sinkronisasi antar task
- β Sinkronisasi ISR β Task
- β Tanpa ownership
- β Tidak ada priority inheritance
- β Proteksi resource bersama
- β Ada ownership
- β Priority inheritance
- β Tidak bisa dari ISR
Implementasi Mutex untuk Serial
// 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
// 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).
// 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));
}
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.
// 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 KB | Heap, stack, global variable |
| IRAM (Instruction RAM) | ~128 KB | Kode yang sering dieksekusi, ISR |
| Cache | ~64 KB | Cache untuk flash access |
| WiFi/BT RAM | ~70 KB | Dedikasi untuk protocol stack |
Best Practices Memory Management
// 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 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
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β 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 β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// 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));
}
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: