Tantangan Logging pada Sistem Berdaya Rendah

Penerapan logging pada sistem mikroprosesor berdaya rendah (constrained systems)—seperti Game Boy Advance (ARM7TDMI) atau mikrokontroler Cortex-M0/M3—menghadapi batasan ketat: siklus instruksi CPU terbatas, SRAM berkisar belasan hingga ratusan kilobyte, dan ketiadaan sistem operasi standar untuk menangani multithreading.

Penggunaan fungsi standar seperti printf berbasis UART menimbulkan latensi masif akibat operasi I/O blocking. Setiap byte yang dikirim via serial interface lambat menghentikan eksekusi core logic, merusak determinisme waktu real-time, dan memicu heisenbug. Arsitektur logging harus memilih strategi penulisan: In-Memory Ring Buffer (circular buffer di RAM) atau Direct Memory-Mapped I/O (seperti register debug emulator).

Analisis Perbandingan: Ring Buffer vs Memory-Mapped I/O

1. Siklus CPU dan Latensi

Direct Memory-Mapped I/O (MMIO)—contohnya antarmuka logging mGBA atau ARM Semihosting—mengalihkan proses output langsung ke trap debugger atau emulasi host. Penulisan ke MMIO register beroperasi dalam 1 siklus bus write, meminimalkan overhead instruksi CPU dalam firmware target. Sebaliknya, In-Memory Ring Buffer membutuhkan kalkulasi indeks (modulo/bitwise mask), pointer arithmetic, dan penyalinan byte memori ke array.

2. Konsumsi RAM

MMIO logging memanfaatkan register perangkat keras virtual di luar alamat SRAM target, sehingga konsumsi RAM internal adalah nol byte. Ring buffer mengorbankan memori kerja: buffer berukuran 1 KB hingga 4 KB secara permanen memotong ruang stack atau heap yang kritis pada target dengan total RAM 32 KB hingga 256 KB.

3. Risiko Concurrency dan Tearing Data

Ring buffer rentan terhadap data tearing jika logging dipanggil di dalam Interrupt Service Routine (ISR) dan context utama secara bersamaan tanpa proteksi kritis. Direct MMIO yang selesai dalam single instruction STR (store register) cenderung atomik pada level bus, menghilangkan kebutuhan atomic critical sections.

4. Persistensi Crash Dump

Jika target crash atau lockup dalam hardware fault, MMIO langsung mencatat log ke host runner sebelum core berhenti. Pada ring buffer, data tersimpan di RAM dan memerlukan mekanisme cold-boot retention, RAM dumping, atau non-volatile storage write pasca-crash untuk inspeksi pascamortem.

5. Portabilitas Binary tanpa Debugger

Binary dengan MMIO logging emulator (seperti port register 0x4FFF600–0x4FFF700 pada mGBA) gagal atau membeku bila dieksekusi pada silikon fisik tanpa hardware debugger. Sebaliknya, binary dengan in-memory circular buffer berjalan identik pada hardware asli maupun emulator.

Matriks Evaluasi Trade-off

  • Direct MMIO: Nol pemakaian RAM, latensi 1-3 siklus write, crash dump realtime otomatis, namun tidak portabel ke bare metal tanpa probe hardware.
  • In-Memory Ring Buffer: Memerlukan alokasi statis RAM, latensi pemrosesan memori lokal, rentan data overwrite saat overflow, tetapi sepenuhnya portabel pada semua hardware.

Implementasi Circular Ring Buffer dan Opsional MMIO di C

Kode berikut menerapkan circular ring buffer statis berukuran pangkat dua untuk eliminasi operasi modulo, lengkap dengan switch integrasi direct I/O untuk logging mGBA emulator.

#include <stdint.h>
#include <stdbool.h>
#include <string.h>

#define LOG_BUFFER_SIZE 1024 // Wajib pangkat dua
#define LOG_BUFFER_MASK (LOG_BUFFER_SIZE - 1)

// mGBA Emulator Register Map (Akses I/O Direct Memory)
#define REG_MGBA_DEBUG_ENABLE (*(volatile uint16_t*)0x4FFF780)
#define REG_MGBA_DEBUG_FLAGS  (*(volatile uint16_t*)0x4FFF700)
#define REG_MGBA_DEBUG_STRING ((volatile char*)0x4FFF600)

typedef struct {
    uint8_t buffer[LOG_BUFFER_SIZE];
    volatile uint32_t head;
    volatile uint32_t tail;
} LogRingBuffer;

static LogRingBuffer g_log_rb;
static bool g_direct_io_available = false;

// ponytail: inisialisasi hardware MMIO statis. Tambah pengecekan runtime dynamic jika target platform berubah.
void log_init(void) {
    g_log_rb.head = 0;
    g_log_rb.tail = 0;

    // Handshake mGBA logging interface
    REG_MGBA_DEBUG_ENABLE = 0xC0DE;
    if (REG_MGBA_DEBUG_ENABLE == 0x1DEA) {
        g_direct_io_available = true;
    }
}

void log_write_direct_mmio(const char* str, uint8_t level) {
    uint32_t i = 0;
    while (str[i] != '\0' && i < 255) {
        REG_MGBA_DEBUG_STRING[i] = str[i];
        i++;
    }
    REG_MGBA_DEBUG_STRING[i] = '\0';
    REG_MGBA_DEBUG_FLAGS = level | 0x100; // Trigger send flag
}

void log_write_ring_buffer(const uint8_t* data, uint32_t len) {
    for (uint32_t i = 0; i < len; i++) {
        uint32_t next_head = (g_log_rb.head + 1) & LOG_BUFFER_MASK;

        // Jika buffer penuh, drop byte tertua dengan memajukan tail
        if (next_head == g_log_rb.tail) {
            g_log_rb.tail = (g_log_rb.tail + 1) & LOG_BUFFER_MASK;
        }

        g_log_rb.buffer[g_log_rb.head] = data[i];
        g_log_rb.head = next_head;
    }
}

void log_message(const char* msg, uint8_t level) {
    if (g_direct_io_available) {
        log_write_direct_mmio(msg, level);
        return;
    }
    log_write_ring_buffer((const uint8_t*)msg, (uint32_t)strlen(msg));
}

Panduan Pemilihan Arsitektur

  1. Gunakan Direct MMIO saat fase prototyping atau emulasi: Menjaga siklus CPU tetap murni untuk logika target dan memberikan visibilitas langsung atas error tanpa overhead.
  2. Gunakan Ring Buffer untuk target release dan pengujian hardware mandiri: Menjamin executable tidak memicu fault akibat dereferensi unmapped memory region. Alokasikan buffer di segmen RAM non-cacheable jika target menggunakan DMA untuk flush log.