Menulis firmware C/C++ yang benar untuk mikrokontroler: memahami batasan flash dan RAM, pola non-blocking, dasar-dasar RTOS seperti task dan queue, serta praktik membangun firmware multi-tugas untuk perangkat IoT.

Setelah di episode 3 kita berhasil membaca sensor dan menggerakkan aktuator, pada episode ini kita belajar menulis firmware yang benar — bukan sekadar kode yang berfungsi, tapi kode yang bisa bertahan di perangkat dengan sumber daya sangat terbatas.
Mengapa firmware berbeda dari pemrograman biasa? Di laptop, kalian bisa mengalokasikan memori GB dengan santai. Di ESP32, kalian hanya punya 4 MB flash untuk program dan ±320 KB RAM untuk data. malloc yang bocor, stack yang meluap, atau delay() yang memblokir akan membuat perangkat hang di lapangan — dan kalian tidak bisa menekan Ctrl+C untuk melihat errornya.
Setiap program yang berjalan di MCU memakai tiga area memori:
| Area | Fungsi | Keterbatasan di ESP32 |
|---|---|---|
| Flash | Menyimpan program dan data konstan | 4 MB — semua kode harus muat |
| RAM | Menyimpan variabel saat runtime | ±320 KB — heap + stack berbagi |
| Stack | Konteks fungsi yang sedang berjalan | Per task ±8 KB (default) |
Praktik yang wajib diingat sejak sekarang:
static atau alokasi sekali saat setup.malloc/new bisa gagal di perangkat kecil.static char logBuffer[256]; // dialokasikan sekali di RAM statis
void logTelemetry(float t, float h) {
snprintf(logBuffer, sizeof(logBuffer), "{\"t\":%.2f,\"h\":%.2f}", t, h);
// aman: buffer dialokasikan di setup, bukan di stack tiap pemanggilan
}Perhatikan snprintf dengan sizeof(logBuffer) — selalu batasi panjang output agar tidak melebihi buffer.
delay() menghentikan seluruh program. Jika firmware sedang menunggu 2 detik, ia tidak bisa memantau button, tidak bisa membaca sensor lain, dan tidak bisa merespons jaringan. Solusinya adalah pola non-blocking memakai millis():
#include <Arduino.h>
unsigned long lastRead = 0;
const unsigned long interval = 2000;
void setup() {
Serial.begin(115200);
}
void loop() {
unsigned long now = millis();
if (now - lastRead >= interval) {
lastRead = now;
Serial.println("baca sensor..."); // tugas berkala
}
// tugas lain tetap jalan: baca button, proses data, dll.
}Dengan pola ini, loop() terus berputar cepat dan setiap tugas dijadwalkan berdasarkan waktu. Untuk sistem yang lebih kompleks, kita naik ke level berikutnya: RTOS.
RTOS (Real-Time Operating System) memungkinkan mikrokontroler menjalankan beberapa task secara "bersamaan" dengan penjadwalan berbasis prioritas dan preemption. ESP32 menggunakan FreeRTOS — tersedia langsung di Arduino/PlatformIO.
Analoginya: sebelumnya satu koki mengerjakan semua hidangan satu per satu. Dengan RTOS, setiap hidangan ditangani koki sendiri (task) yang dijadwalkan bergantian dengan cepat, sehingga semua tampak berjalan paralel.
Konsep inti FreeRTOS yang wajib dipahami:
| Konsep | Fungsi |
|---|---|
| Task | Fungsi independen yang berjalan dengan prioritas & stack sendiri |
| Queue | Antrean data aman antar task (produksi–konsumsi) |
| Semaphore/Mutex | Kunci agar task tidak saling menabrak resource bersama |
| vTaskDelay | Delay yang tidak memblokir task lain |
Mari bangun firmware yang menjalankan dua tugas sekaligus: satu membaca sensor, satu menampilkan status ke serial.
#include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
void sensorTask(void *pv) {
float t = 26.5f;
while (1) {
t += 0.1f; // simulasi pembacaan sensor
Serial.printf("sensor: %.1f C\n", t);
vTaskDelay(pdMS_TO_TICKS(1000)); // sleep tanpa blokir task lain
}
}
void statusTask(void *pv) {
while (1) {
Serial.println("status: firmware OK");
vTaskDelay(pdMS_TO_TICKS(3000));
}
}
void setup() {
Serial.begin(115200);
xTaskCreate(sensorTask, "sensor", 2048, NULL, 1, NULL);
xTaskCreate(statusTask, "status", 2048, NULL, 1, NULL);
}
void loop() {
vTaskDelay(pdMS_TO_TICKS(1000)); // setup done, biar RTOS yang jalan
}sensor: 26.5 C
sensor: 26.6 C
status: firmware OK
sensor: 26.7 CPerhatikan outputnya: task sensor mencetak tiap detik, status tiap 3 detik — keduanya berjalan "bersamaan". pdMS_TO_TICKS() mengubah milidetik ke tick RTOS.
Jika task sensor harus mengirim data ke task lain (misalnya task pengiriman jaringan), gunakan queue:
QueueHandle_t dataQueue;
typedef struct {
float temperature;
float humidity;
} Telemetry;
void sensorTask(void *pv) {
Telemetry t = {26.5f, 60.0f};
while (1) {
t.temperature += 0.1f;
xQueueSend(dataQueue, &t, 0); // kirim ke queue, tak menunggu
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
void sendTask(void *pv) {
Telemetry t;
while (1) {
if (xQueueReceive(dataQueue, &t, portMAX_DELAY) == pdTRUE) {
Serial.printf("kirim: %.1f C, %.1f %%\n", t.temperature, t.humidity);
}
}
}xQueueSend memproduksi data, xQueueReceive mengonsumsinya. portMAX_DELAY membuat task konsumen menunggu hingga ada data — tanpa membuang CPU.
Tip
Mulailah setiap task dari yang paling sederhana dan naikkan kompleksitas bertahap. Aturan praktis: 1 task = 1 tanggung jawab (baca sensor, kirim data, kontrol aktuator). Jika sebuah task perlu menunggu, gunakan vTaskDelay atau queue, bukan delay().
volatile untuk flag sederhana.delay() di dalam task — memblokir task lain; selalu pakai vTaskDelay.sprintf tanpa batas bisa menimpa memori; pakai snprintf.Inti yang harus dibawa pulang:
delay() — pakai pola non-blocking millis() untuk tugas berkala.Di episode 5 selanjutnya, kita akan menghubungkan firmware ini ke dunia luar: communication protocols — bagaimana MQTT, CoAP, dan HTTP bekerja, model publish/subscribe, dan praktik membangun komunikasi MQTT pertama dari ESP32 ke broker lokal. Firmware kita akan mulai "berbicara"!