Merancang driver perangkat yang bersih: perbandingan HAL vs bare-metal, lapisan arsitektur driver dari register sampai aplikasi, pola device struct dengan function pointer, abstraksi untuk portabilitas dan pengujian, serta praktik menulis driver sensor I2C pertama.

Setelah di episode 9 kita mahir membedah firmware yang bermasalah, sekarang kita membalik perspektif: merancang kode yang jarang bermasalah. Episode ini membahas device drivers — lapisan yang menjembatani hardware dan aplikasi — dan arsitektur yang membuatnya bersih, portabel, dan mudah diuji.
Mengapa arsitektur driver penting? Karena dalam produk nyata, satu sensor dipakai beberapa tempat, satu driver dipakai lintas board, dan tim QA butuh cara menguji tanpa hardware. Driver yang ditulis asal-asalan — inisialisasi tersebar, register dipanggil langsung di aplikasi — adalah biang bug yang menyebar ke seluruh project.
Pertanyaan pertama setiap project: pakai HAL (Hardware Abstraction Layer vendor) atau register langsung?
| Aspek | Bare-Metal (register) | HAL (STM32 HAL / ESP-IDF) |
|---|---|---|
| Kontrol | Penuh, tiap bit jelas | Dibatasi API vendor |
| Ukuran binary | Kecil, hemat flash/RAM | Lebih besar |
| Kecepatan belajar | Menuntut datasheet | Cepat mulai |
| Debugging | Transparan | Abstraksi bisa menyembunyikan detail |
| Portabilitas | Rendah (per-chip) | Sedang (lintas keluarga) |
| Produksi | Umum di produk kecil | Umum di produk kompleks |
Keduanya sah. Banyak project produksi memakai kombinasi: HAL untuk peripheral kompleks (USB, Ethernet), bare-metal untuk jalur kritis yang butuh kontrol penuh (timing ketat, low power). Yang terpenting: konsisten dan terdokumentasi — bukan dogma.
Driver yang baik punya lapisan yang jelas, masing-masing dengan satu tanggung jawab:
Hasilnya: driver sensor bisa dipakai di board lain tanpa diubah — hanya BSP yang berubah. Ini juga memudahkan mocking untuk test (episode 14).
Cara klasik membuat driver "objek": struct perangkat berisi state dan operasi:
typedef struct {
uint8_t i2c_addr;
uint16_t last_temp_x10; // suhu x10 untuk presisi
int (*init)(Sensor_t *self);
int (*read)(Sensor_t *self);
} Sensor_t;
Sensor_t sensor_lm75_create(uint8_t addr);static int lm75_init(Sensor_t *s) {
// tulis konfigurasi register (misal resolusi)
return i2c_write_byte(s->i2c_addr, LM75_CFG, 0x0);
}
static int lm75_read(Sensor_t *s) {
uint16_t raw = 0;
i2c_read_regs(s->i2c_addr, LM75_TEMP, (uint8_t *)&raw, 2);
raw = __REV16(raw) >> 7; // format 11-bit
s->last_temp_x10 = (int16_t)raw * 5; // 0.5C per bit -> x10
return 0;
}
Sensor_t sensor_lm75_create(uint8_t addr) {
Sensor_t s = { .i2c_addr = addr,
.init = lm75_init, .read = lm75_read };
return s;
}Pola ini memisahkan antarmuka (apa yang bisa dilakukan) dari implementasi (bagaimana caranya) — dasar dari arsitektur driver yang bisa diganti, di-mock, dan diuji. Aplikasi memanggil sensor.read(&s) tanpa tahu chip apa di baliknya.
Satu sensor bisa dipasang via I2C atau SPI di board berbeda. Abstraksi transport membuat driver tidak peduli:
// driver panggil ini - implementasi memilih I2C/SPI
typedef struct {
int (*read_regs)(uint8_t addr, uint8_t reg, uint8_t *buf, uint16_t n);
int (*write_byte)(uint8_t addr, uint8_t reg, uint8_t val);
} Transport_t;
static const Transport_t *trans;Hasilnya: satu driver untuk chip, beberapa binding transport. Ini pola yang dipakai ST HAL dan library komersial — dan cara terbaik menghindari kode duplikat per board.
Tip
Aturan praktis arsitektur driver: driver tidak boleh menyentuh register GPIO langsung. Semua kontak dengan board lewat BSP (pin & clock). Kalau besok LED pindah pin atau pindah board, cukup ubah satu file — bukan seluruh codebase. Konsistensi ini yang membuat firmware produksi bisa dirawat bertahun-tahun.
Integrasikan semua lapisan untuk proyek "smart lamp" kecil:
// bsp.h
#define LED_PIN 5
void bsp_led_init(void); // RCC + GPIO + pin mapping
void bsp_led_set(bool on);
// driver_led.c
void led_init(void) { bsp_led_init(); }
void led_on(void) { bsp_led_set(true); }
void led_off(void) { bsp_led_set(false); }
void led_toggle(void){ static bool s=false; s=!s; bsp_led_set(s); }
// app.c
int main(void) {
led_init();
while (1) {
led_toggle();
delay_ms(500);
}
}Aplikasi tidak tahu register apa pun — ia berbicara dengan driver, driver berbicara dengan BSP. Struktur ini siap dipakai untuk proyek yang jauh lebih besar.
i2c_read gagal harus ketahuan, bukan diam.Inti yang harus dibawa pulang:
Di episode 11 selanjutnya kita mengoptimalkan untuk baterai: Low Power Firmware — sleep modes, wake sources, konsumsi energi, dan desain firmware yang tidur nyenyak dan bangun tepat waktu. Perangkat kalian akan mulai "hidup hemat"!