- Published on
Concurrency & RTOS Fundamentals (ESP32 Context)
- Authors
📘 Foundation Article 2: Concurrency & RTOS Fundamentals (ESP32 Context)
Track: C++ Firmware Engineering Foundations for ESP32 Environment: ESP32 + VSCode + Arduino Community Edition Project Base: IndustrialNode/IndustrialNode.ino Use Case: Generic Sensor + Relay Node
- 📘 Foundation Article 2: Concurrency & RTOS Fundamentals (ESP32 Context)
- 1. Mengapa Concurrency Itu Default di ESP32
- 2. Apa Itu Task (Dalam Konteks Firmware)
- 🔬 Concurrency Practical Lab — Membuat Task Tambahan
- Apa yang Kita Lakukan?
- 3. Scheduler & Preemption
- 4. Shared State & Race Condition
- 5. Queue sebagai Mekanisme Aman
- 6. Mutex & Deadlock (Konsep Dasar)
- 7. Sensor + Relay Node dalam Konteks Concurrency
- 8. Mental Model yang Harus Dikunci di Tahap Foundation
- 🔒 Konsolidasi Final Artikel 2
1. Mengapa Concurrency Itu Default di ESP32
Pada mikrokontroler klasik:
main()
↓
while(true)
Satu stack. Satu eksekusi. Satu flow.
Pada ESP32:
- loop() = FreeRTOS task
- WiFi = task
- TCP/IP = task
- Timer service = task
- IDLE task = selalu ada
- ISR = interrupt context
Artinya:
Bahkan sebelum Anda membuat arsitektur, sistem sudah concurrent.
Ini bukan pilihan desain Anda.
Ini adalah kondisi dasar sistem.
Diagram Model Eksekusi Dasar (IndustrialNode Context)
Core 1:
├─ loopTask (control + sensor read)
├─ user task (jika dibuat)
└─ IDLE
Core 0:
├─ WiFi task
├─ TCP/IP task
├─ Timer task
└─ IDLE
Interrupt:
└─ ISR button / peripheral
Tidak ada bagian firmware yang benar-benar sendirian.
Kenapa Ini Penting Secara Arsitektural?
Karena tanpa sadar concurrency:
- Anda akan memakai global mutable state.
- ISR akan mengubah state langsung.
- MQTT callback akan menyentuh driver langsung.
- Anda akan menyalahkan hardware saat race muncul.
Concurrency awareness bukan teori.
Itu fondasi untuk:
- Dependency discipline
- Memory discipline
- Layering discipline
- Reliability model
2. Apa Itu Task (Dalam Konteks Firmware)
Task adalah:
- Fungsi dengan stack sendiri
- Memiliki prioritas
- Dijadwalkan oleh scheduler
- Bisa Running / Ready / Blocked
Dalam Arduino ESP32:
void loop()
Adalah task bernama loopTask.
Setiap Task Memiliki:
1️⃣ Stack sendiri 2️⃣ Context register sendiri 3️⃣ Prioritas relatif 4️⃣ Scheduling slot
Implikasi nyata:
- Stack overflow tidak terlihat seperti crash biasa.
- Blocking satu task tidak menghentikan sistem.
- Prioritas salah bisa menyebabkan starvation.
Contoh Kasus Nyata (Stack Awareness)
Tambahkan ke loop:
char largeBuffer[4096];
Perhatikan:
- uxTaskGetStackHighWaterMark(NULL) turun drastis.
- Jika terlalu besar → reset.
Engineer sering mengira ini heap.
Padahal ini stack loopTask.
Concurrency + memory model mulai bertemu di sini.
🔬 Concurrency Practical Lab — Membuat Task Tambahan
Sekarang kita masuk praktikal.
Kita akan:
- Membuat task sensor terpisah
- Membuat queue
- Mengirim event ke loopTask
- Mengamati behavior
📂 Struktur Tetap Flat
IndustrialNode/
└── IndustrialNode.ino
Belum masuk layering. Fokus concurrency.
🧪 IndustrialNode.ino (Full Runnable Concurrency Lab)
Copy–paste utuh:
#include <Arduino.h>
// =============================
// Configuration
// =============================
constexpr int LED_PIN = 5;
constexpr int BUTTON_PIN = 18;
constexpr uint32_t SENSOR_TASK_STACK = 4096;
constexpr uint32_t CONTROL_PERIOD_MS = 200;
// =============================
// Queue Definition
// =============================
QueueHandle_t eventQueue;
enum class EventType : uint8_t {
BUTTON,
SENSOR
};
struct Event {
EventType type;
int value;
};
// =============================
// ISR
// =============================
void IRAM_ATTR buttonISR()
{
Event ev;
ev.type = EventType::BUTTON;
ev.value = 1;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendFromISR(eventQueue, &ev, &xHigherPriorityTaskWoken);
if (xHigherPriorityTaskWoken)
portYIELD_FROM_ISR();
}
// =============================
// Sensor Task
// =============================
void sensorTask(void* param)
{
while (true)
{
Event ev;
ev.type = EventType::SENSOR;
ev.value = analogRead(34);
xQueueSend(eventQueue, &ev, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
// =============================
// Setup
// =============================
void setup()
{
Serial.begin(115200);
delay(1000);
pinMode(LED_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP);
eventQueue = xQueueCreate(10, sizeof(Event));
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN),
buttonISR,
FALLING);
xTaskCreatePinnedToCore(
sensorTask,
"SensorTask",
SENSOR_TASK_STACK,
nullptr,
1,
nullptr,
1
);
Serial.println("Concurrency Lab Started");
}
// =============================
// Loop (Control Task)
// =============================
void loop()
{
Event ev;
if (xQueueReceive(eventQueue, &ev, pdMS_TO_TICKS(CONTROL_PERIOD_MS)))
{
if (ev.type == EventType::BUTTON)
{
Serial.println("Button Event");
digitalWrite(LED_PIN, HIGH);
}
else if (ev.type == EventType::SENSOR)
{
Serial.print("Sensor Value: ");
Serial.println(ev.value);
if (ev.value > 2000)
digitalWrite(LED_PIN, HIGH);
else
digitalWrite(LED_PIN, LOW);
}
}
}
Apa yang Kita Lakukan?
- Membuat task tambahan (
sensorTask). - Membuat queue sebagai boundary.
- ISR tidak menyentuh LED.
- sensorTask tidak menyentuh LED.
- Hanya loop (control) yang menyentuh actuator.
Ini sudah:
- Ownership model awal.
- Concurrency boundary awal.
- Determinism meningkat.
Tanpa OOP. Tanpa layering. Murni concurrency model.
3. Scheduler & Preemption
Sekarang kita analisis apa yang benar-benar terjadi.
Saat sensorTask delay:
vTaskDelay(...)
Ia masuk state Blocked.
Scheduler menjalankan task lain.
Jika ISR masuk:
- ISR kirim event.
- Jika controlTask blocked → bisa langsung wake.
Inilah preemption nyata.
Ilustrasi Preemption dalam Node Ini
SensorTask Running
↓
vTaskDelay
↓
ControlTask Running
↑
Button Interrupt
↓
ISR
↓
ControlTask Wake
Ini bukan ilustrasi textbook.
Ini yang terjadi di board Anda sekarang.
4. Shared State & Race Condition
Sampai titik ini kita sudah:
- Punya
sensorTask - Punya ISR
- Punya
loop()sebagai control task - Punya queue
Sekarang kita sengaja buat versi salah untuk melihat race.
Apa Itu Race Condition?
Race terjadi ketika:
- Dua context berbeda
- Mengakses resource yang sama
- Tanpa koordinasi
- Hasil bergantung timing scheduler
Race bukan error sintaks.
Race adalah error model eksekusi.
🔬 Race Lab — Versi Salah (Tanpa Queue)
Ganti seluruh program dengan ini:
#include <Arduino.h>
constexpr int LED_PIN = 5;
constexpr int BUTTON_PIN = 18;
volatile int sharedValue = 0;
void IRAM_ATTR buttonISR()
{
sharedValue++;
}
void sensorTask(void* param)
{
while (true)
{
sharedValue += 10;
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void setup()
{
Serial.begin(115200);
delay(1000);
pinMode(LED_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN),
buttonISR,
FALLING);
xTaskCreatePinnedToCore(
sensorTask,
"SensorTask",
4096,
nullptr,
1,
nullptr,
1
);
}
void loop()
{
if (sharedValue > 50)
{
digitalWrite(LED_PIN, HIGH);
}
else
{
digitalWrite(LED_PIN, LOW);
}
Serial.println(sharedValue);
delay(200);
}
Apa yang Terjadi?
- ISR menambah
sharedValue - sensorTask menambah
sharedValue - loop membaca
sharedValue
Tidak ada proteksi.
sharedValue++ bukan atomik.
Secara internal:
Load
Add
Store
Jika interleave:
Task load
ISR load
Task store
ISR store
Nilai bisa hilang.
Ini race nyata.
Kenapa volatile Tidak Cukup?
volatile hanya memastikan:
- Compiler tidak menyimpan nilai di register.
- Setiap akses benar-benar baca memory.
volatile tidak membuat:
- Operasi atomik.
- Lock.
- Critical section.
Ini salah kaprah umum.
Ilustrasi Race dalam Sensor + Relay Node
ISR Button → relayState = true
MQTT Callback → relayState = false
Control loop → relayState = sensorDecision
Tanpa ownership:
- Relay bisa berkedip.
- State tidak konsisten.
- Bug sporadis.
Race tidak terlihat saat code review cepat.
Race muncul di runtime.
5. Queue sebagai Mekanisme Aman
Kita kembali ke versi benar (queue-based).
Konsep inti:
Jangan berbagi state. Kirim pesan.
ISR tidak boleh ubah actuator.
ISR hanya kirim event.
Diagram Model Aman (IndustrialNode)
ISR
↓
Queue
↓
Control Task
↓
RelayDriver
Satu owner.
Satu domain yang menyentuh hardware.
Mengapa Queue Lebih Aman?
- 1️⃣ Tidak ada shared mutable state lintas context
- 2️⃣ ISR tidak menyentuh business logic
- 3️⃣ Task tunggal mengontrol actuator
- 4️⃣ Event ordering bisa dianalisis
Queue memindahkan problem dari:
- Memory interleaving menjadi
- Message ordering
Dan message ordering lebih deterministik.
🔬 Validasi: Perhatikan Behavior
Kembali ke Concurrency Lab sebelumnya.
Perhatikan:
- ISR kirim event.
- sensorTask kirim event.
- loop yang memutuskan relay.
Tidak ada race.
Tidak ada sharedValue global.
Ownership jelas.
Kapan Queue Tidak Cukup?
Queue cocok untuk:
- Event
- Command
- Signal
Queue tidak cukup untuk:
- Shared SPI bus
- Shared I2C bus
- Shared file system
- Shared large buffer
Di sinilah mutex masuk.
6. Mutex & Deadlock (Konsep Dasar)
Mutex adalah mekanisme locking.
Tujuan:
- Mengamankan resource bersama.
🔬 Deadlock Lab Sederhana
Tambahkan dua mutex:
SemaphoreHandle_t mutexA;
SemaphoreHandle_t mutexB;
Buat dua task:
Task 1:
xSemaphoreTake(mutexA, portMAX_DELAY);
delay(10);
xSemaphoreTake(mutexB, portMAX_DELAY);
Task 2:
xSemaphoreTake(mutexB, portMAX_DELAY);
delay(10);
xSemaphoreTake(mutexA, portMAX_DELAY);
Jika timing tepat:
- Task 1 pegang A.
- Task 2 pegang B.
- Task 1 tunggu B.
- Task 2 tunggu A.
Deadlock.
Tidak crash.
Tidak error log.
Watchdog reset.
Engineer bingung.
Kenapa Deadlock Sering Tidak Disadari?
Karena:
- Resource ownership tidak digambar.
- Urutan lock tidak distandarkan.
- Tidak ada rule sistemik.
Deadlock bukan bug kecil.
Deadlock adalah kegagalan desain.
Rule Mental di Foundation
Di tahap Foundation:
- Hindari mutex jika bisa pakai queue.
- Jangan lock lebih dari satu resource tanpa urutan tetap.
- ISR tidak boleh mengambil mutex biasa.
- Prioritaskan message passing.
Kita belum membekukan rule.
Kita membentuk mental model.
Sampai di sini kita sudah:
- Membuktikan race.
- Membuktikan volatile tidak cukup.
- Membuktikan queue lebih aman.
- Memahami deadlock secara praktis.
7. Sensor + Relay Node dalam Konteks Concurrency
Sekarang kita satukan semua yang sudah diuji:
- ISR
- sensorTask
- loop sebagai control
- Queue
- Race scenario
- Deadlock awareness
Kita tarik ke sistem nyata:
Generic Sensor + Relay Node
Arsitektur Tanpa Model Concurrency (Yang Sering Terjadi)
ISR Button → relay.set()
MQTT Callback → relay.set()
loop() sensor → relay.set()
Semua context menyentuh actuator.
Dampak:
- Relay chatter
- State tidak konsisten
- Sulit debug
- Tidak deterministik
- Scaling impossible
Masalahnya bukan relay. Masalahnya adalah:
Tidak ada ownership domain.
Arsitektur Dengan Model Concurrency Benar
ISR Button
↓
Queue
↓
MQTT Callback
↓
Queue
↓
Control Task (loop)
↓
Relay Driver
Satu owner untuk actuator.
ISR tidak menyentuh hardware. MQTT callback tidak menyentuh hardware. Hanya ControlTask yang boleh.
Ini bukan OOP. Ini concurrency discipline.
🔬 Practical Validation — Tambahkan MQTT Simulation Task
Kita modifikasi program sebelumnya untuk mensimulasikan “network task”.
Tambahkan task berikut:
void networkSimTask(void* param)
{
while (true)
{
Event ev;
ev.type = EventType::BUTTON;
ev.value = 999; // simulate remote command
xQueueSend(eventQueue, &ev, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(3000));
}
}
Tambahkan di setup:
xTaskCreatePinnedToCore(
networkSimTask,
"NetSim",
4096,
nullptr,
1,
nullptr,
0
);
Sekarang sistem punya:
- ISR → Queue
- sensorTask → Queue
- networkSimTask → Queue
- loop → single control owner
Perhatikan behavior:
- Tidak ada race.
- Tidak ada direct actuator write.
- Tidak ada shared mutable state global.
Kenapa Ini Penting Sebelum OOP?
Karena banyak engineer berpikir:
“Saya buat class saja, selesai.”
Salah.
Class tidak menyelesaikan race.
Encapsulation tidak mencegah deadlock.
Dependency injection tidak menyelesaikan interleaving.
Concurrency discipline harus lebih dulu.
OOP hanya alat untuk mengontrol dependency, bukan untuk memperbaiki race.
Contoh Praktis: Event Flag vs Queue (Perbandingan Nyata)
Model Flag (Riskan)
volatile bool buttonPressed = false;
void IRAM_ATTR buttonISR() {
buttonPressed = true;
}
Masalah:
- Masih shared state.
- Scaling sulit.
- Multiple event type sulit.
- Hard to audit.
Model Queue (Deterministik)
Event ev;
ev.type = EventType::BUTTON;
xQueueSendFromISR(eventQueue, &ev, ...);
Keunggulan:
- Event typed.
- Extensible.
- Tidak berbagi state.
- Ordering bisa ditelusuri.
Message-based system adalah fondasi sistem scalable.
8. Mental Model yang Harus Dikunci di Tahap Foundation
Sekarang kita kunci mental model ini secara eksplisit.
1️⃣ Concurrency adalah default
Anda tidak mengaktifkan concurrency. Anda hidup di dalamnya.
2️⃣ ISR bukan bagian dari loop
ISR adalah interrupt context. Bukan function biasa.
3️⃣ Shared mutable state adalah risiko
Semakin banyak global mutable, semakin tinggi probabilitas race.
4️⃣ Queue lebih aman daripada global
Message passing lebih deterministik daripada shared memory.
5️⃣ Mutex bukan solusi utama
Mutex bisa:
- Menyembunyikan desain buruk.
- Menyebabkan deadlock.
- Mengurangi determinism.
Gunakan hanya jika domain resource memang shared.
6️⃣ Scheduling memengaruhi determinism
Delay kecil + network spike = jitter kontrol.
Ini akan sangat relevan ketika kita masuk ke memory discipline dan communication state machine.
🔒 Konsolidasi Final Artikel 2
Setelah Artikel 2, engineer harus:
- Mengerti bahwa concurrency bukan tambahan.
- Mampu membuat task tambahan.
- Mampu membuat queue.
- Mampu menghindari race sederhana.
- Mampu menjelaskan kenapa volatile tidak cukup.
- Mengerti kenapa mutex bisa berbahaya.
- Memahami ownership actuator.
Artikel ini bukan tentang API FreeRTOS.
Artikel ini tentang:
Model berpikir sistem concurrent.
Tanpa model ini:
- Artikel Memory akan terasa teoretis.
- Artikel OOP akan terasa kaku.
- Artikel Layering akan terasa dipaksakan.
- Artikel Reliability akan terasa paranoid.
Dengan model ini:
Semua artikel berikutnya menjadi logis.
Artikel berikutnya:
📘 FreeRTOS Execution Model (ESP32 Context)
Kita akan masuk ke:
- Stack per task secara mendalam
- Heap global vs stack
- Context switching cost
- Core pinning
- Watchdog interaction
- Determinism impact
Karena concurrency tanpa memory awareness akan menjadi sumber kegagalan berikutnya.
Catatan Penyusunan Artikel ini disusun sebagai materi edukasi dan referensi umum berdasarkan berbagai sumber pustaka, praktik lapangan, serta bantuan alat penulisan. Pembaca disarankan untuk melakukan verifikasi lanjutan dan penyesuaian sesuai dengan kondisi serta kebutuhan masing-masing sistem.