Belajar Embedded Engineer - Bare-Metal Programming
Episode 6 of 28

Belajar Embedded Engineer - Bare-Metal Programming

Membongkar firmware sampai ke tulangnya: startup code yang mempersiapkan C, linker script yang menempatkan kode ke alamat yang benar, dan blink LED register-level tanpa library apa pun, sampai memahami apa yang terjadi sebelum main dipanggil

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

Pendahuluan

Semua episode sebelumnya membangun pemahaman; episode ini membangun kebenaran. Kita akan menulis firmware tanpa satu baris pun library — tanpa HAL, tanpa CMSIS, tanpa startup code bawaan compiler. Dari nol sampai binary yang siap di-flash. Hasilnya: kalian paham persis apa yang terjadi sebelum main() dipanggil.

Mengapa penting? Karena saat production firmware bermasalah — crash di lapangan, tidak boot, stack overflow — pilihan kalian adalah menebak, atau menelusuri alur boot sendiri. Firmware engineer yang hanya tahu memakai library akan terjebak; yang paham bare-metal bisa menebak dengan presisi dari tanda-tanda yang paling kecil.

Alur Boot: Apa yang Terjadi Sebelum main?

100%

Inilah lima langkah yang terjadi sejak listrik menyala:

  1. CPU membaca vector table di awal flash: alamat stack pointer dan alamat reset handler.
  2. Reset handler mengeksekusi startup code.
  3. Startup menyalin .data dari flash ke RAM dan mengosongkan .bss.
  4. C runtime init (jika ada) menyiapkan _start/__libc_init_array.
  5. main() dipanggil — dan di embedded, ia hampir selalu berakhir di infinite loop.

Tanpa startup code yang benar, variabel global tidak terisi, stack tidak terbentuk, dan main bahkan tidak pernah dipanggil. Untuk memprogram MCU bare-metal, kalian menulis semua langkah ini sendiri.

Vector Table & Reset Handler

Vector table adalah daftar alamat handler interrupt; entri pertama adalah nilai awal stack pointer (ditulis dari kiri), entri kedua adalah alamat reset. Definisi dalam assembly atau C:

vector_table.lds — isi awal flash (konsep)
0x08000000 : 0x2001FFF0      # initial SP = puncak RAM
0x08000004 : 0x08000109      # Reset_Handler address (thumb, bit0 = 1)
0x08000008 : NMI_Handler
0x0800000C : HardFault_Handler
...

Bentuk konkretnya di file startup (misal startup_stm32f401.c) berisi array fungsi berisi alamat handler, dengan __attribute__((section(".isr_vector"))) supaya compiler menaruhnya persis di awal flash.

Startup Code dalam C

Setelah reset, eksekusi masuk ke Reset_Handler. Tugasnya hanya tiga: siapkan stack, salin .data, zero .bss — lalu panggil main:

c
extern uint32_t __etext;    /* awal .data di flash   */
extern uint32_t __data_start__, __data_end__;
extern uint32_t __bss_start__, __bss_end__;
 
void Reset_Handler(void) {
    uint32_t *src = &__etext;
    uint32_t *dst = &__data_start__;
    while (dst < &__data_end__) *dst++ = *src++;   /* copy .data flash -> RAM */
    for (dst = &__bss_start__; dst < &__bss_end__; dst++) *dst = 0; /* zero bss */
    main();
    for (;;) {}   /* main tidak pernah kembali; jika kembali, hang aman */
}

Simbol __etext, __data_start__ dan teman-temannya tidak didefinisikan di C — mereka diekspor oleh linker script. Di sinilah kalian melihat jembatan antara kode dan arsitektur: semua ini bagian dari memory map yang kita bahas di episode 2.

Linker Script: Menempatkan Segalanya di Tempatnya

Linker script adalah resep yang memberi tahu linker ke mana harus menaruh setiap section. Ini contoh .ld untuk STM32F401 (flash 512 KB di 0x08000000, RAM 96 KB di 0x20000000):

stm32f401.ld
ENTRY(Reset_Handler)
 
MEMORY {
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 512K
    RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 96K
}
 
SECTIONS {
    .isr_vector : { KEEP(*(.isr_vector)) } > FLASH
    .text       : { *(.text*) *(.rodata*) } > FLASH
    _etext = .;
 
    .data : {
        __data_start__ = .;
        *(.data*)
        __data_end__ = .;
    } > RAM AT> FLASH
 
    .bss (NOLOAD) : {
        __bss_start__ = .;
        *(.bss*) *(COMMON)
        __bss_end__ = .;
    } > RAM
    __stack_top = ORIGIN(RAM) + LENGTH(RAM);
}

Konsep kunci yang harus dipahami:

  1. > FLASH vs AT> FLASH.data berjalan di RAM tapi berasal dari flash (karena nilai awalnya disimpan di flash, lalu disalin startup ke RAM).
  2. _etext — menandai akhir data berisi di flash; itulah alamat sumber yang dipakai startup code.
  3. KEEP pada vector table — mencegah linker membuang section yang tidak terlihat dipakai.
  4. NOLOAD pada .bss — variabel nol tidak perlu disimpan di flash.

Kesalahan umum: lupa menaruh __stack_top atau salah origin → MCU tidak boot tanpa error yang jelas. Selalu verifikasi dengan arm-none-eabi-nm firmware.elf atau readelf -l untuk melihat section.

Warning

Bekerja dengan alamat mentah seperti ini adalah titik paling mudah membuat kesalahan yang tidak terlihat. Selalu flash di board yang murah dan tersedia banyak (Nucleo/Pico), dan jangan langsung mencoba pada hardware mahal. Jika MCU tidak merespons sama sekali setelah flash, periksa urutan vector table dan nilai stack pointer lebih dulu — bukan kode C-nya.

Build & Flash Bare-Metal

Build pipeline tanpa IDE cukup dua langkah — compile lalu link:

Compile, link, flash
arm-none-eabi-gcc -c -mcpu=cortex-m4 -mthumb -O2 -Wall blink.c -o blink.o
arm-none-eabi-ld -T stm32f401.ld blink.o -o blink.elf
arm-none-eabi-objcopy -O binary blink.elf blink.bin
st-flash write blink.bin 0x08000000

Flag -mcpu=cortex-m4 -mthumb memberi tahu compiler target hardware. objcopy mengubah ELF (dengan metadata debug) menjadi binary mentah yang di-flash ke alamat flash. Jika memakai Pico, ganti st-flash dengan alat flash Pico (drag-and-drop UF2).

Uji: blink LED dengan kode register langsung dari episode 4. Jika LED berkedip tanpa satu baris library pun — selamat, kalian baru saja menulis firmware bare-metal pertama, dan sekarang tahu persis cara kerja di balik setiap framework.

Penutup

Inti yang harus dibawa pulang:

  • Sebelum main, ada startup code: siapkan stack, salin .data, zero .bss.
  • Vector table di awal flash menentukan alamat stack dan reset handler.
  • Linker script menentukan penempatan section; .data berjalan di RAM tapi disalin dari flash.
  • Build bare-metal = compile + link + objcopy + flash, tanpa IDE, tanpa library.

Di episode 7 selanjutnya kita menaikkan level: RTOS fundamentals — memecah firmware menjadi task-task yang tampak berjalan bersamaan dengan FreeRTOS, lengkap dengan semaphore dan queue untuk koordinasi. Bare-metal yang kalian kuasai akan menjelaskan mengapa RTOS mencuri satu tick timer dari kalian. Sampai jumpa di episode 7!

Belajar Embedded Engineer - Bare-Metal Programming | Belajar Embedded Engineer