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.

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 menyimpan alamat — bukan nilai. Di firmware, pointer adalah cara kita menunjuk ke lokasi memori tertentu:
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 alamatBedakan 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.
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:
#define GPIOA_BASE 0x48000000UL // contoh alamat GPIOA (STM32L4)
#define GPIOA_MODER (GPIOA_BASE + 0x00UL)
*(volatile uint32_t *)GPIOA_MODER = 0x1; // tulis mode GPIOPola *(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 memberi tahu kompiler: nilai di alamat ini bisa berubah di luar kendali program — oleh hardware, interrupt, atau DMA. Tanpa volatile, kompiler boleh mengoptimalkan pembacaan:
uint32_t status = *ptr_register; // baca sekali
while (status == 0) {
// kompiler bisa membaca register SEKALI saja,
// lalu loop selamanya - register tidak pernah dibaca ulang
}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.
Register dikendalikan bit per bit. Empat pola dasar:
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 0b10Gunakan unsigned (1U) agar shift dan perbandingan aman. Untuk menghindari "magic number", pakai macro atau bitfield yang jelas namanya.
int tidak dijamin ukurannya antar kompiler. Firmware memakai tipe dengan lebar pasti dari <stdint.h>:
| Tipe | Lebar | Dipakai Untuk |
|---|---|---|
uint8_t / int8_t | 8 bit | Byte, karakter, register kecil |
uint16_t | 16 bit | Counter, sensor 12-bit |
uint32_t | 32 bit | Alamat, register 32-bit, timestamp millis |
uint64_t | 64 bit | Waktu/detik, presisi tinggi |
Selalu pakai <stdint.h> di kode firmware — tidak ada alasan memakai int mentah.
Untuk register dengan banyak field, peta alamatnya ke struct dan akses lewat member (cara CMSIS bekerja):
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 outputPastikan urutan dan offset member persis dengan manual — inilah mengapa struct ini didefinisikan vendor, bukan ditulis manual.
Kontrol firmware nyaris selalu berupa state machine — peta antara "tunggu", "proses", "kirim":
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;
}
}Gabungkan semuanya — MMIO + volatile + bit manipulation — untuk menyalakan LED via register, tanpa HAL:
#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.
volatile di register/flag ISR — kondisi while tak pernah berubah di tingkat kompiler.int bertanda — 1 << 31 undefined behavior; selalu 1U.magic number tanpa nama — makro/definisi register dari datasheet menyelamatkan debugging.Inti yang harus dibawa pulang:
volatile = sinyal "nilai bisa berubah di luar program" — wajib untuk register, flag ISR, dan DMA.1U untuk set/clear/toggle/test.stdint.h), struct-over-register, dan state machine.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!