Belajar Firmware Engineer - RTOS Dasar
Episode 7 of 28

Belajar Firmware Engineer - RTOS Dasar

Membawa firmware ke level multitasking dengan FreeRTOS: konsep task, prioritas, dan scheduler preemptive, sinkronisasi dengan semaphore dan mutex, komunikasi antar task dengan queue, pola ISR-ke-queue, serta praktik membangun aplikasi RTOS pertama.

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

Setelah di episode 6 kita menguasai interrupt, pada episode ini kita menggabungkannya dengan Real-Time Operating System (RTOS) — khususnya FreeRTOS, RTOS open source paling populer di dunia embedded. Dengan RTOS, firmware yang tadinya satu while(1) berubah menjadi beberapa task yang tampak berjalan bersamaan.

Mengapa RTOS penting? Karena sistem nyata punya banyak pekerjaan paralel: membaca sensor, memproses data, mengendalikan motor, menjaga komunikasi — masing-masing dengan tenggat sendiri. RTOS memberi kerangka untuk menjadwalkan semua itu secara terprediksi, plus primitif sinkronisasi (semaphore, queue) yang mencegah race condition. Ini juga fondasi penting untuk episode 16 (real-time constraints).

Konsep Task dan Scheduler

Task adalah fungsi yang berjalan independen, masing-masing dengan stack dan prioritas sendiri. Scheduler (berbasis tick timer — biasanya SysTick) membagi waktu CPU di antara task.

Membuat task FreeRTOS
void sensorTask(void *pv) {
    while (1) {
        read_sensor();
        vTaskDelay(pdMS_TO_TICKS(1000));  // tidur 1 detik
    }
}
 
void setup() {
    xTaskCreate(sensorTask, "sensor", 2048, NULL, 2, NULL);
    //                          nama    stack prio  handle
    vTaskStartScheduler();   // hidupkan scheduler - tidak kembali
}

Perhatikan: vTaskDelay tidak memblokir task lain — task menidurkan dirinya dan scheduler menjalankan task lain. Inilah perbedaan kunci dari delay() naif di episode 4.

Prioritas dan Preemption

FreeRTOS memakai preemptive priority scheduling: task dengan prioritas lebih tinggi selalu dijalankan duluan, dan bisa menyela task berprioritas lebih rendah (preempt). Aturan penting:

  • Prioritas numerik: lebih tinggi angka = lebih diprioritaskan (kebalikan NVIC).
  • Time slicing: task dengan prioritas sama berbagi CPU dengan quantum tick.
  • Idle task: FreeRTOS menjalankan task idle (prioritas 0) saat tidak ada yang siap — di sinilah low-power hook sering dipasang (episode 11).
Alur scheduler (konsep)
High prio task A   ██████████        ████████
Low prio task B    ░░░░░░░░░░ ██████ ░░░░░░░░  ████
                   A berjalan  A tidur (B jalan)  A lagi

Kesalahan klasik: task prioritas tinggi yang selalu siap membuat task rendah kelaparan (starvation). Desain prioritas harus hati-hati — sering kali "satu prioritas untuk I/O, satu untuk logika" sudah cukup.

Sinkronisasi: Semaphore dan Mutex

Binary Semaphore / Mutex

Untuk "kunci" resource bersama (misal buffer UART yang dipakai dua task):

Mutex melindungi resource bersama
SemaphoreHandle_t uart_mutex;
 
void taskA(void *pv) {
    xSemaphoreTake(uart_mutex, portMAX_DELAY);
    uart_send(packetA);          // critical section
    xSemaphoreGive(uart_mutex);
}

Mutex (berbeda dari binary semaphore) punya priority inheritance — mencegah priority inversion: jika task berprioritas tinggi menunggu mutex yang dipegang task rendah, prioritas task rendah dinaikkan sementara agar lekas selesai. Untuk proteksi resource, selalu pakai mutex, bukan binary semaphore.

Counting Semaphore

Untuk menghitung event (misal berapa kali interrupt terjadi):

Counting semaphore dari ISR
SemaphoreHandle_t event_sem;
 
void ISR_Handler(void) {
    BaseType_t hpw = pdFALSE;
    xSemaphoreGiveFromISR(event_sem, &hpw);  // beri sinyal dari ISR
    portYIELD_FROM_ISR(hpw);                 // yield jika perlu
}
 
void eventTask(void *pv) {
    while (1) {
        xSemaphoreTake(event_sem, portMAX_DELAY);
        handle_event();          // satu event per take
    }
}

Aturan FreeRTOS: dari ISR, gunakan varian ...FromISR (xSemaphoreGiveFromISR, xQueueSendFromISR) — tidak pernah fungsi biasa.

Komunikasi: Queue

Queue adalah saluran data aman antar task (dan dari ISR): satu task mengirim, yang lain menerima. Data disalin ke queue — hati-hati dengan ukuran elemen (jangan kirim struct raksasa; kirim pointer jika aman).

Queue produksi-konsumsi
QueueHandle_t data_q;
 
typedef struct { uint16_t temp; uint16_t hum; } SensorData;
 
void producer(void *pv) {
    SensorData d = {255, 128};
    while (1) {
        xQueueSend(data_q, &d, portMAX_DELAY);
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}
 
void consumer(void *pv) {
    SensorData d;
    while (1) {
        if (xQueueReceive(data_q, &d, portMAX_DELAY) == pdTRUE) {
            uart_printf("temp=%d hum=%d\n", d.temp, d.hum);
        }
    }
}
 
void setup() {
    data_q = xQueueCreate(4, sizeof(SensorData));  // kedalaman 4
    xTaskCreate(producer, "prod", 1024, NULL, 1, NULL);
    xTaskCreate(consumer, "cons", 1024, NULL, 1, NULL);
    vTaskStartScheduler();
}

Pola ISR → queue → task adalah jembatan sempurna antara dunia interrupt (episode 6) dan RTOS: ISR hanya xQueueSendFromISR, task menerima dan memproses tanpa memblokir sistem.

Tip

Aturan praktis 1 task = 1 tanggung jawab: baca sensor, proses data, kirim ke UART — masing-masing task sendiri. Komunikasi antar task lewat queue (data) dan mutex (resource bersama), bukan variabel global yang dibaca bareng tanpa kunci.

Praktik: Aplikasi RTOS Pertama

Gabungkan semuanya — dua task, satu queue, satu mutex:

main.c - aplikasi FreeRTOS dua task
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include "queue.h"
 
QueueHandle_t log_q;
 
void sensorTask(void *pv) {
    uint16_t val = 0;
    while (1) {
        val = read_adc();                    // baca ADC
        xQueueSend(log_q, &val, portMAX_DELAY);
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}
 
void logTask(void *pv) {
    uint16_t v;
    while (1) {
        if (xQueueReceive(log_q, &v, portMAX_DELAY) == pdTRUE) {
            uart_printf("adc=%u\n", v);     // pakai mutex UART di dalam
        }
    }
}

Kesalahan Umum RTOS

  1. Stack task terlalu kecil — crash acak/reboot loop; naikkan ukuran stack.
  2. Berbagi variabel tanpa mutex — race condition yang sulit dilacak.
  3. vTaskDelay vs delay — delay biasa memblokir seluruh scheduler.
  4. Fungsi biasa dari ISR — harus varian FromISR.
  5. Prioritas tinggi yang "kelaparan" — task rendah tidak pernah jalan; audit prioritas.
  6. Memanggil API di context salah — baca dokumentasi; banyak API tidak boleh dari ISR.

Penutup

Inti yang harus dibawa pulang:

  • RTOS (FreeRTOS) = beberapa task dengan scheduler preemptive berbasis prioritas.
  • Task punya stack & prioritas sendiri; vTaskDelay tidak memblokir task lain.
  • Mutex untuk resource bersama (priority inheritance), counting semaphore untuk event.
  • Queue = saluran data antar task/ISR; dari ISR selalu ...FromISR.
  • Pola ISR → queue → task menghubungkan interrupt dan RTOS dengan aman.

Di episode 8 selanjutnya kita turun ke detail memori: Memory & Linker — bagaimana flash dan RAM dibagi, cara kerja linker script, penempatan stack/heap, dan mengapa pemahaman ini menentukan kestabilan firmware produksi. RTOS kalian akan berjalan di atas peta memori yang benar!