Belajar Embedded Engineer - C/C++ untuk Embedded
Episode 3 of 28

Belajar Embedded Engineer - C/C++ untuk Embedded

Menulis C/C++ dengan mentalitas embedded: pointer & alamat, volatile untuk register dan ISR, pilihan memori static vs heap, serta pola embedded seperti ring buffer dan state machine yang menjadi bahan baku firmware production

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

Pendahuluan

Setelah memahami arsitektur sistem embedded di episode 2 — Flash, RAM, dan peripheral registers — sekarang waktunya mengasah alat utama: C/C++. Ini bukan pelajaran C akademis; ini C dengan lensa embedded, di mana setiap byte dan setiap siklus clock dihitung.

Di dunia desktop, kesalahan kecil sering luput karena resource melimpah. Di MCU dengan RAM 16 KB, malloc yang bocor atau pointer yang salah bisa membuat perangkat hang di lapangan — dan tidak ada tombol restart karena perangkat itu mungkin berada di pompa air di sawah, bukan di meja kerja. Karena itu, kalian harus menulis C yang disiplin: paham di mana data hidup, mengapa volatile penting, dan kapan pola tertentu lebih aman daripada yang lain.

Pointer: Alamat Itu Nyata

Di embedded, pointer bukan abstraksi — ia adalah alamat fisik yang bisa kalian baca di memory map. Membaca register peripheral adalah membaca pointer:

c
#include <stdint.h>
 
#define GPIOA_IDR (*(volatile uint32_t *)0x40020010) /* input data register */
 
uint32_t read_pin_state(void) {
    return GPIOA_IDR & (1U << 5); /* baca status fisik pin 5 */
}

Pola (*(volatile uint32_t *)ALAMAT) adalah idiom universal di firmware: cast konstanta alamat menjadi pointer ke uint32_t, lalu dereference. Tanpa volatile, compiler berhak menganggap nilai register tidak pernah berubah dan meng-cache-nya di register CPU — hasilnya pembacaan basi. Ingat: register peripheral berubah sendiri tanpa sepengetahuan C (karena ISR dan hardware), jadi volatile wajib.

Perbedaan pointer vs array juga krusial: array punya storage sendiri, pointer hanya menunjuk. Kesalahan umum adalah sizeof pada pointer ketika berniat mengukur array — di embedded ini sering jadi buffer overflow halus yang sulit dideteksi.

volatile: Kata Kunci Paling Penting di Embedded

volatile memberi tahu compiler: "jangan berasumsi nilai variabel ini stabil, dan jangan optimasi aksesnya". Tiga kasus yang mengharuskan volatile:

  1. Register peripheral — nilainya berubah oleh hardware.
  2. Variabel yang dibagi dengan ISR — berubah "di belakang" main loop.
  3. Variabel yang dibaca flag hardware (misal status bit).
c
volatile uint32_t tick_count = 0;
 
void SysTick_Handler(void) {
    tick_count++; /* ISR menambah; main loop membaca */
}
 
int main(void) {
    uint32_t last = 0;
    for (;;) {
        if (tick_count != last) { /* tanpа volatile, bisa tak pernah terlihat */
            last = tick_count;
            toggle_led();
        }
    }
}

Contoh nyata dampaknya: tanpа volatile, compiler bisa mengoptimasi tick_count ke register CPU sehingga main loop tidak pernah melihat nilainya berubah. Bug jenis ini hanya muncul di release build — dan memburu-nya di lapangan adalah mimpi buruk.

Memori: Static vs Heap

Di embedded, aturan emasnya: hindari heap (malloc) di kode produksi. Alasan:

AlasanPenjelasan
FragmentationRAM kecil mudah terpecah; malloc gagal di waktu tak terduga
Non-deterministikDurasi malloc tidak pasti — merusak real-time (episode 14)
Tidak ada OSDi bare-metal, malloc bisa berupa implementasi sederhana yang lambat
Hard to debugKebocoran di heap tidak terdeteksi sampai perangkat hang

Sebagai gantinya, gunakan:

  • Variabel static / global untuk data yang hidup sepanjang program.
  • Ring buffer statis untuk data streaming.
  • Memory pool dengan ukuran blok tetap jika memang butuh alokasi dinamis.

Ring buffer adalah pola yang wajib dikuasai — semua firmware UART/serial memakainya:

c
#define RB_SIZE 64
static uint8_t rb[RB_SIZE];
static uint8_t rb_head, rb_tail;
 
int rb_push(uint8_t b) {
    uint8_t next = (uint8_t)((rb_head + 1) % RB_SIZE);
    if (next == rb_tail) return -1;   /* penuh */
    rb[rb_head] = b;
    rb_head = next;
    return 0;
}
 
int rb_pop(uint8_t *out) {
    if (rb_tail == rb_head) return -1; /* kosong */
    *out = rb[rb_tail];
    rb_tail = (uint8_t)((rb_tail + 1) % RB_SIZE);
    return 0;
}

% RB_SIZE dengan ukuran pangkat dua akan dioptimasi compiler menjadi operasi AND, sehingga ring buffer ini praktis gratis di siklus clock.

State Machine: Fondasi Firmware

Firmware nyata jarang berupa "alur linier"; hampir selalu berupa state machine. Contoh nyata: tombol dengan debounce:

State machine tombol
IDLE --tekan--> PRESSING --debounce selesai & masih ditekan--> PRESSED
PRESSED --lepas--> RELEASING --debounce selesai--> IDLE

State machine membuat perilaku deterministik, mudah dites, dan mudah dibaca — tiga hal yang sangat bernilai di firmware yang harus hidup bertahun-tahun. Semua pola ini akan kita pakai ulang: ring buffer di episode 4 (UART), state machine di episode 17 (OTA) dan 22 (DSP pipeline).

Common Pitfalls

  1. Menggunakan malloc/free sembarangan — resep fragmentation dan hang.
  2. Melupakan volatile pada register dan variabel ISR — bug yang muncul hanya di release.
  3. sizeof(pointer) bukan sizeof(array) — overflow halus.
  4. Type mismatch pada bit ops — selalu gunakan suffix U/UL pada literal dan uint32_t yang eksplisit.
  5. Integer overflow tak diperiksa — di embedded, penghitung waktu dan buffer selalu punya batas.

Important

Perbedaan C dan C++ di embedded bukan "mana yang lebih keren", melainkan trade-off. C++ memberi template, RAII, dan constexpr — tapi tanpa sadar bisa menarik heap, RTTI, dan exception yang menambah binary. Untuk MCU kecil, banyak tim memakai C atau C++ dengan subset ketat (no exceptions, no RTTI, no heap). Pilih yang tim kalian jadikan standar — lalu disiplin pada aturannya.

Penutup

Inti yang harus dibawa pulang:

  • Pointer di embedded adalah alamat fisik — register diakses lewat pointer cast + volatile.
  • volatile wajib untuk register dan variabel yang dibagi ISR.
  • Hindari heap; gunakan static, ring buffer, dan memory pool.
  • State machine membuat firmware deterministik dan mudah dites.

Di episode 4 selanjutnya kita keluar dari teori dan menyentuh hardware: MCU & peripherals — GPIO, UART, SPI, I2C, timer, dan interrupt, dengan praktik mengendalikan peripheral nyata di board kalian. Kode C embedded yang barusan kalian kuasai akan menjadi bahasa percakapan dengan hardware. Sampai jumpa di episode 4!