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

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.
Di embedded, pointer bukan abstraksi — ia adalah alamat fisik yang bisa kalian baca di memory map. Membaca register peripheral adalah membaca pointer:
#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 memberi tahu compiler: "jangan berasumsi nilai variabel ini stabil, dan jangan optimasi aksesnya". Tiga kasus yang mengharuskan volatile:
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.
Di embedded, aturan emasnya: hindari heap (malloc) di kode produksi. Alasan:
| Alasan | Penjelasan |
|---|---|
| Fragmentation | RAM kecil mudah terpecah; malloc gagal di waktu tak terduga |
| Non-deterministik | Durasi malloc tidak pasti — merusak real-time (episode 14) |
| Tidak ada OS | Di bare-metal, malloc bisa berupa implementasi sederhana yang lambat |
| Hard to debug | Kebocoran di heap tidak terdeteksi sampai perangkat hang |
Sebagai gantinya, gunakan:
Ring buffer adalah pola yang wajib dikuasai — semua firmware UART/serial memakainya:
#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.
Firmware nyata jarang berupa "alur linier"; hampir selalu berupa state machine. Contoh nyata: tombol dengan debounce:
IDLE --tekan--> PRESSING --debounce selesai & masih ditekan--> PRESSED
PRESSED --lepas--> RELEASING --debounce selesai--> IDLEState 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).
volatile pada register dan variabel ISR — bug yang muncul hanya di release.sizeof(pointer) bukan sizeof(array) — overflow halus.U/UL pada literal dan uint32_t yang eksplisit.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.
Inti yang harus dibawa pulang:
volatile.volatile wajib untuk register dan variabel yang dibagi ISR.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!