Belajar Firmware Engineer - C untuk Firmware
Episode 3 of 28

Belajar Firmware Engineer - C untuk Firmware

Menguasai C dalam konteks firmware: pointer dan alamat, memory-mapped I/O yang mengakses register hardware, kualifikasi volatile dan const, pola bit manipulation, serta embedded C patterns seperti fixed-width types dan struct-over-register untuk menulis kode firmware yang aman.

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

Pendahuluan

Setelah di episode 2 kita memahami memory map dan proses boot, pada episode ini kita masuk ke senjata utama firmware: bahasa C. Bukan C untuk aplikasi biasa — melainkan C yang berbicara langsung dengan hardware: membaca dan menulis alamat memori, memanipulasi bit, dan menulis kode yang perilakunya terdefinisi di bawah interrupt.

Mengapa ini penting? Karena hampir semua kesalahan firmware — pin tidak nyala, variabel tiba-tiba berubah, kode dibatalkan kompiler — berakar pada pemahaman yang dangkal tentang pointer, volatile, dan pola C embedded. Kuasai episode ini, dan semua episode berikutnya (register, driver, RTOS) akan terasa jauh lebih mudah.

Pointer: Bahasa Alamat

Pointer menyimpan alamat — bukan nilai. Di firmware, pointer adalah cara kita menunjuk ke lokasi memori tertentu:

Pointer dasar
uint32_t value = 42;        // variabel di RAM
uint32_t *ptr = &value;     // ptr = alamat value
*ptr = 7;                   // tulis 7 ke alamat value
 
// *ptr untuk membaca/menulis, &value untuk mengambil alamat

Bedakan tiga hal yang sering tertukar: value (nilai), &value (alamat), dan *ptr (isi di alamat yang ditunjuk). Pointer ke void (void *) menyimpan alamat tanpa tipe — sering dipakai di driver generic.

Memory-Mapped I/O: Register sebagai Alamat

Peripheral hardware diekspos sebagai register pada alamat tetap. Mengaksesnya = membaca/menulis alamat tersebut sebagai variabel bertipe tertentu. Misalnya pada STM32, bit RCC->AHB2ENR atau pada chip lain:

MMIO - menulis register via pointer
#define GPIOA_BASE 0x48000000UL     // contoh alamat GPIOA (STM32L4)
#define GPIOA_MODER (GPIOA_BASE + 0x00UL)
 
*(volatile uint32_t *)GPIOA_MODER = 0x1;   // tulis mode GPIO

Pola *(volatile uint32_t *)ALAMAT = NILAI adalah inti MMIO: cast alamat ke pointer, lalu dereference untuk membaca/menulis. Vendor menyediakan definisi register ini lewat CMSIS/STM32 HAL (misal GPIOA->MODER), tapi prinsipnya tetap satu: register adalah lokasi memori.

volatile: Jangan Biarkan Kompiler "Berdalih"

volatile memberi tahu kompiler: nilai di alamat ini bisa berubah di luar kendali program — oleh hardware, interrupt, atau DMA. Tanpa volatile, kompiler boleh mengoptimalkan pembacaan:

Tanpa volatile - berbahaya
uint32_t status = *ptr_register;   // baca sekali
while (status == 0) {
    // kompiler bisa membaca register SEKALI saja,
    // lalu loop selamanya - register tidak pernah dibaca ulang
}
Dengan volatile - benar
volatile uint32_t status = *ptr_register;
while (status == 0) {
    // setiap iterasi membaca register asli
}

Aturan praktis: volatile untuk register hardware, variabel bersama ISR, dan flag yang diubah DMA. Jangan menempel volatile di semua tempat — ia melumpuhkan optimasi. Kita akan mendalaminya lagi di episode 6 (interrupt-safe code).

Warning

volatile bukan pengganti sinkronisasi (mutex/atomic) — ia hanya mencegah optimasi baca/tulis. Untuk data yang dibagi antara ISR dan task RTOS dengan operasi multi-langkah, tetap butuh critical section. Jangan menganggap volatile menyelesaikan semua race condition.

Bit Manipulation: Pola yang Dipakai Setiap Hari

Register dikendalikan bit per bit. Empat pola dasar:

Pola set, clear, toggle, dan test
reg |=  (1U << n);      // set bit n
reg &= ~(1U << n);      // clear bit n
reg ^=  (1U << n);      // toggle bit n
if (reg & (1U << n)) {} // test apakah bit n = 1
 
// tulis beberapa bit sekaligus (field)
reg &= ~(0x3U << 8);    // clear bit 8-9
reg |=  (0x2U << 8);    // set bit 8-9 ke 0b10

Gunakan unsigned (1U) agar shift dan perbandingan aman. Untuk menghindari "magic number", pakai macro atau bitfield yang jelas namanya.

Embedded C Patterns

Fixed-Width Types

int tidak dijamin ukurannya antar kompiler. Firmware memakai tipe dengan lebar pasti dari <stdint.h>:

TipeLebarDipakai Untuk
uint8_t / int8_t8 bitByte, karakter, register kecil
uint16_t16 bitCounter, sensor 12-bit
uint32_t32 bitAlamat, register 32-bit, timestamp millis
uint64_t64 bitWaktu/detik, presisi tinggi

Selalu pakai <stdint.h> di kode firmware — tidak ada alasan memakai int mentah.

Struct over Register

Untuk register dengan banyak field, peta alamatnya ke struct dan akses lewat member (cara CMSIS bekerja):

Pola struct-over-register
typedef struct {
    volatile uint32_t MODER;   // offset 0x00
    volatile uint32_t OTYPER;  // offset 0x04
    volatile uint32_t OSPEEDR; // offset 0x08
    volatile uint32_t PUPDR;   // offset 0x0C
    volatile uint32_t IDR;     // offset 0x10
    volatile uint32_t ODR;     // offset 0x14
} GPIO_TypeDef;
 
#define GPIOA ((GPIO_TypeDef *)GPIOA_BASE)
GPIOA->MODER = (1U << 0);   // set pin PA0 sebagai output

Pastikan urutan dan offset member persis dengan manual — inilah mengapa struct ini didefinisikan vendor, bukan ditulis manual.

State Machine

Kontrol firmware nyaris selalu berupa state machine — peta antara "tunggu", "proses", "kirim":

State machine sederhana
typedef enum { ST_IDLE, ST_READ, ST_PROCESS, ST_SEND } state_t;
state_t state = ST_IDLE;
 
void tick(void) {
    switch (state) {
        case ST_IDLE:   if (btn_pressed()) state = ST_READ; break;
        case ST_READ:   read_sensor(); state = ST_PROCESS; break;
        case ST_PROCESS: process_data(); state = ST_SEND; break;
        case ST_SEND:   send_result(); state = ST_IDLE; break;
    }
}

Praktik: Kode Firmware Pertama

Gabungkan semuanya — MMIO + volatile + bit manipulation — untuk menyalakan LED via register, tanpa HAL:

LED blink register-level (konsep)
#define LED_PIN 5U                     // LED di pin 5
#define GPIOA ((GPIO_TypeDef *)GPIOA_BASE)
 
static void delay(volatile uint32_t count) {
    while (count--) {}                 // delay kasar, loop kosong
}
 
int main(void) {
    clock_enable_gpio();               // aktifkan clock GPIO (episode 4)
    GPIOA->MODER &= ~(3U << (2*LED_PIN));
    GPIOA->MODER |=  (1U << (2*LED_PIN)); // set mode output
    while (1) {
        GPIOA->ODR ^= (1U << LED_PIN);    // toggle LED
        delay(500000);
    }
}

Tip

Biasakan menulis test kecil untuk setiap pola: jalankan di board atau simulator, amati hasil, lalu pindah ke konsep berikutnya. Pemahaman pointer + volatile + bit manipulation adalah 80% kemampuan menulis driver — bagian yang tersisa adalah membaca datasheet.

Kesalahan Umum C Firmware

  1. Lupa volatile di register/flag ISR — kondisi while tak pernah berubah di tingkat kompiler.
  2. Shift dengan int bertanda1 << 31 undefined behavior; selalu 1U.
  3. Menulis struct-over-register dengan offset salah — akses register yang salah tanpa error (berbahaya).
  4. magic number tanpa nama — makro/definisi register dari datasheet menyelamatkan debugging.

Penutup

Inti yang harus dibawa pulang:

  • Pointer = alamat; MMIO = membaca/menulis alamat register via pointer dereference.
  • volatile = sinyal "nilai bisa berubah di luar program" — wajib untuk register, flag ISR, dan DMA.
  • Bit manipulation dengan 1U untuk set/clear/toggle/test.
  • Pola embedded: fixed-width types (stdint.h), struct-over-register, dan state machine.
  • Praktik: LED blink register-level tanpa HAL.

Di episode 4 selanjutnya kita naik satu level: MCU Programming — bagaimana clock dipersiapkan, cara kerja GPIO, dan praktik blink register-level penuh dengan datasheet sebagai panduan. Dari sini kalian mulai "menyetir" hardware sungguhan!

Belajar Firmware Engineer - C untuk Firmware | Belajar Firmware Engineer