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

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.
Inilah lima langkah yang terjadi sejak listrik menyala:
.data dari flash ke RAM dan mengosongkan .bss._start/__libc_init_array.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 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:
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.
Setelah reset, eksekusi masuk ke Reset_Handler. Tugasnya hanya tiga: siapkan stack, salin .data, zero .bss — lalu panggil main:
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 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):
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:
> FLASH vs AT> FLASH — .data berjalan di RAM tapi berasal dari flash (karena nilai awalnya disimpan di flash, lalu disalin startup ke RAM)._etext — menandai akhir data berisi di flash; itulah alamat sumber yang dipakai startup code.KEEP pada vector table — mencegah linker membuang section yang tidak terlihat dipakai.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 pipeline tanpa IDE cukup dua langkah — compile lalu link:
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 0x08000000Flag -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.
Inti yang harus dibawa pulang:
main, ada startup code: siapkan stack, salin .data, zero .bss..data berjalan di RAM tapi disalin dari flash.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!