Memecah firmware menjadi task yang tampak berjalan bersamaan dengan FreeRTOS: memahami scheduler, prioritas dan konteks switching, lalu menyinkronkan antar-task dengan semaphore dan mengirim data antar-task dengan queue, ditutup praktik aplikasi FreeRTOS sederhana

Di episode 6 kalian menulis firmware bare-metal: satu main, satu infinite loop, semuanya berjalan satu per satu. Itu cukup untuk perangkat sederhana. Tapi perangkat modern harus melakukan banyak hal "sekaligus": membaca sensor, mengirim data, merespons tombol, memperbarui display. Solusinya bukan banyak thread bertenaga OS, melainkan RTOS (Real-Time Operating System) — dan FreeRTOS adalah pemain paling dominan di dunia MCU.
Konsep kuncinya: CPU tetap satu, tapi RTOS menciptakan ilusi paralelisme dengan menjadwalkan task secara bergantian — sangat cepat sehingga tampak serentak. Ini bukan bingung; ini konteks switching dengan presisi clock. Episode ini membangun fondasi: task, scheduler, prioritas, semaphore, dan queue.
RTOS adalah OS minimal untuk MCU dengan dua janji utama:
Berbeda dengan Linux, FreeRTOS tidak punya proteksi memory antar process, MMU, atau driver yang lengkap. Ia hanya "kernel kecil" (beberapa ribu baris) yang menempati flash beberapa KB dan RAM beberapa KB.
Scheduler bekerja dengan mengandalkan satu interrupt: SysTick (atau tick timer). Setiap 1 ms, tick interrupt membangunkan scheduler yang memutuskan task mana yang mendapat CPU berikutnya. Itulah mengapa RTOS "mencuri" satu tick dari kalian — trade-off yang layak untuk konkurensi.
Task adalah fungsi dengan infinite loop dan stack sendiri:
#include "FreeRTOS.h"
#include "task.h"
void vTaskSensor(void *pv) {
for (;;) {
uint16_t adc = read_adc_channel(0);
sensor_queue_send(&adc); /* kirim data ke task lain */
vTaskDelay(pdMS_TO_TICKS(100)); /* tidur 100 ms — tidak buang CPU */
}
}
void vTaskBlink(void *pv) {
for (;;) {
toggle_led();
vTaskDelay(pdMS_TO_TICKS(500));
}
}
int main(void) {
xTaskCreate(vTaskSensor, "sensor", 256, NULL, 2, NULL);
xTaskCreate(vTaskBlink, "blink", 128, NULL, 1, NULL);
vTaskStartScheduler(); /* setelah ini, kontrol diserahkan ke RTOS */
for (;;) {} /* seharusnya tidak pernah sampai di sini */
}Kata kunci yang harus dipahami:
256 adalah jumlah words (1024 byte). Terlalu kecil → stack overflow diam-diam; terlalu besar → RAM terbuang.2 dan 1. Scheduler selalu menjalankan task berprioritas tertinggi yang siap (siap = tidak sedang delay atau menunggu queue).vTaskDelay — task tidur dan melepaskan CPU; tanpa ini task akan "monopoli" CPU.configCHECK_FOR_STACK_OVERFLOW dan sediakan handler; crash karena stack task adalah bug nomor satu aplikasi FreeRTOS pemula.Saat dua task berbagi resource (misalnya LCD yang sama), tanpa proteksi keduanya bisa saling menimpa output. Mutex mengunci resource agar hanya satu task yang memakainya dalam satu waktu:
SemaphoreHandle_t lcd_mutex;
void vTaskLog(void *pv) {
for (;;) {
xSemaphoreTake(lcd_mutex, portMAX_DELAY); /* tunggu sampai dapat */
lcd_print("log entry");
xSemaphoreGive(lcd_mutex); /* lepaskan kunci */
vTaskDelay(pdMS_TO_TICKS(1000));
}
}Semaphore biner sedikit berbeda — bukan untuk melindungi resource, melainkan untuk memberi sinyal: task A menunggu tanda bahwa event telah terjadi. Kesalahan paling umum: memakai semaphore untuk eksklusi (harusnya mutex) dan sebaliknya. Aturan praktisnya: eksklusi → mutex; event/sinyal → semaphore biner atau event group.
Queue adalah cara aman mengirim data antar task — produsen menaruh, konsumen mengambil, dan semuanya disinkronkan oleh kernel:
QueueHandle_t sensor_q;
void sender_task(void *pv) { /* produsen */
uint16_t sample = read_adc();
xQueueSend(sensor_q, &sample, 0);
vTaskDelay(pdMS_TO_TICKS(50));
}
void consumer_task(void *pv) { /* konsumen */
uint16_t v;
while (xQueueReceive(sensor_q, &v, portMAX_DELAY) == pdTRUE) {
process_sample(v); /* dipanggil saat ada data */
}
}
int main(void) {
sensor_q = xQueueCreate(8, sizeof(uint16_t)); /* 8 slot */
xTaskCreate(sender_task, "sender", 256, NULL, 1, NULL);
xTaskCreate(consumer_task, "consumer", 256, NULL, 2, NULL);
vTaskStartScheduler();
for (;;) {}
}Queue melakukan dua hal sekaligus: mengirim data dan menyinkronkan. Konsumen yang memanggil xQueueReceive dengan portMAX_DELAY akan tidur sampai ada data — tidak membuang CPU. Ini pola yang persis seperti ring buffer dari episode 3, tapi dengan keamanan konkurensi yang ditangani kernel.
Important
Aturan emas RTOS: jangan berbagi data dengan variabel global tanpa proteksi. Dua task yang menulis variabel yang sama tanpa mutex/queue menghasilkan race condition yang hanya muncul kadang-kadang di lapangan — bug paling sulit dilacak di seluruh embedded. Kebiasaan pertama saat desain: "data ini milik task mana, dan bagaimana task lain mendapatkannya?" Jika jawabannya "langsung baca global", desain ulang dengan queue atau mutex.
vTaskDelay(0) berlebihan — boleh untuk yield, tapi jangan dipakai sebagai taktik sembunyi.FromISR (dibahas tuntas di episode 8).Inti yang harus dibawa pulang:
FromISR variant di interrupt.Di episode 8 selanjutnya kita masuk ke jantung konkurensi: interrupts & concurrency — merancang ISR yang singkat, mengatur prioritas interrupt, dan menulis kode yang aman dijalankan bersamaan dengan task RTOS. Di sinilah firmware yang baik dibedakan dari firmware yang cuma "jalan". Sampai jumpa di episode 8!