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.

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).
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.
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.
FreeRTOS memakai preemptive priority scheduling: task dengan prioritas lebih tinggi selalu dijalankan duluan, dan bisa menyela task berprioritas lebih rendah (preempt). Aturan penting:
High prio task A ██████████ ████████
Low prio task B ░░░░░░░░░░ ██████ ░░░░░░░░ ████
A berjalan A tidur (B jalan) A lagiKesalahan 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.
Untuk "kunci" resource bersama (misal buffer UART yang dipakai dua task):
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.
Untuk menghitung event (misal berapa kali interrupt terjadi):
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.
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).
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.
Gabungkan semuanya — dua task, satu queue, satu mutex:
#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
}
}
}vTaskDelay vs delay — delay biasa memblokir seluruh scheduler.FromISR.Inti yang harus dibawa pulang:
vTaskDelay tidak memblokir task lain....FromISR.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!