Panel ILI9341 cukup besar untuk menampilkan grafik sensor yang berguna, tetapi framebuffer RGB565 berukuran 240 × 320 membutuhkan 153.600 byte:

240 * 320 * 2 bytes = 153,600 bytes

Alokasi sebesar itu bukan pilihan awal yang efisien pada microcontroller kecil jika grafik hanya membutuhkan beberapa ratus sampel. Display controller sudah memiliki graphics RAM sendiri. ESP32-C3 dapat menjadikan panel sebagai tujuan gambar, menyimpan hanya state aplikasi yang diperlukan, lalu mengirim pixel yang berubah melalui SPI.

Untuk line graph yang terus diperbarui, state yang berguna bukan salinan kedua seluruh layar. Yang dibutuhkan adalah riwayat sampel dengan ukuran terbatas dan informasi secukupnya untuk menghapus serta menggambar ulang bagian yang berubah.

Pisahkan riwayat sampel dari pixel

Misalkan area plot memiliki lebar 200 pixel dan setiap posisi horizontal mewakili satu sampel. Firmware hanya perlu menyimpan 200 pengukuran untuk membangun kembali trace.

Ring buffer berkapasitas tetap menyatakan batas tersebut secara langsung:

const WIDTH: usize = 200;

struct Samples {
    values: [i16; WIDTH],
    head: usize,
    len: usize,
}

impl Samples {
    const fn new() -> Self {
        Self {
            values: [0; WIDTH],
            head: 0,
            len: 0,
        }
    }

    fn push(&mut self, value: i16) {
        self.values[self.head] = value;
        self.head = (self.head + 1) % WIDTH;
        self.len = self.len.saturating_add(1).min(WIDTH);
    }

    fn get_oldest(&self, index: usize) -> Option<i16> {
        if index >= self.len {
            return None;
        }

        let start = if self.len == WIDTH { self.head } else { 0 };
        Some(self.values[(start + index) % WIDTH])
    }
}

Kebutuhan memorinya tetap:

200 samples * 2 bytes = 400 bytes

Struktur ini tidak membutuhkan heap sehingga cocok untuk firmware #![no_std]. Yang lebih penting, biaya memorinya mengikuti jumlah dan representasi sampel, bukan resolusi display.

Jika sensor secara alami menghasilkan f32, menyimpan float tetap masuk akal. Jika rentang nilainya dapat direpresentasikan dengan integer terkalibrasi, fixed-point atau integer dapat mengurangi penggunaan memori sekaligus membuat aturan scaling lebih eksplisit.

Petakan nilai pengukuran ke area plot

Sistem koordinat display dan satuan sensor berada pada domain yang berbeda. Sebaiknya konversi keduanya ditempatkan dalam satu fungsi.

Untuk plot dengan koordinat atas y0, tinggi h, dan rentang input min..=max, pemetaan linear dapat ditulis sebagai:

normalized = (value - min) / (max - min)
y = y0 + (h - 1) - normalized * (h - 1)

Pengurangan dari sisi bawah diperlukan karena koordinat Y pada display biasanya bertambah ke arah bawah.

Untuk chart sederhana, integer arithmetic menghindari kebutuhan state floating-point:

fn scale_y(value: i32, min: i32, max: i32, y0: i32, height: i32) -> i32 {
    let value = value.clamp(min, max);
    let span = max - min;
    let pixels = height - 1;

    y0 + pixels - ((value - min) * pixels / span)
}

Pemanggil harus memastikan max > min. Clamping juga penting: nilai sensor di luar rentang sebaiknya berhenti di batas grafik, bukan menghasilkan koordinat di luar area plot.

Automatic scaling dapat dipakai pada sensor dengan perubahan rentang besar, tetapi perubahan skala juga mengubah arti pixel lama. Rentang tetap sering lebih mudah dibaca pada embedded dashboard karena posisi vertikal yang sama tetap mewakili nilai fisik yang sama.

Full redraw menghabiskan bandwidth SPI untuk pixel yang tidak berubah

Menghapus dan menggambar ulang seluruh layar 240 × 320 setiap kali ada sampel baru memang sederhana, tetapi jumlah data yang dikirim jauh lebih besar daripada kebutuhan line graph.

Tanpa menghitung command overhead, satu transfer penuh RGB565 membutuhkan:

240 * 320 * 2 = 153,600 bytes per frame

Jika dilakukan sepuluh kali per detik, payload pixel saja mencapai:

1,536,000 bytes/s

Traffic sebenarnya juga mencakup command display dan operasi gambar lain. Frame rate praktis bergantung pada clock SPI, overhead driver, bentuk transaksi, dan perangkat lain yang memakai bus yang sama.

Sensor yang diambil sekali per detik tidak membutuhkan beban seperti itu. Bahkan plot yang lebih cepat biasanya hanya mengubah area sempit atau satu segmen baru pada setiap update.

Dua strategi redraw inkremental

Ada dua pendekatan yang berguna untuk membuat scrolling chart tanpa framebuffer.

Pendekatan pertama mempertahankan posisi X dan menggambar ulang seluruh area plot dari ring buffer ketika sampel baru masuk. Cara ini tetap tidak membutuhkan framebuffer sebesar layar, tetapi semua pixel grafik digambar ulang. Implementasinya sederhana dan cocok jika sample rate rendah.

Pendekatan kedua menggunakan sweep display. Write cursor bergerak dari kiri ke kanan. Sebelum menggambar segmen terbaru, firmware menghapus kolom atau strip sempit berikutnya, lalu menggambar segmen baru di sana. Setelah mencapai sisi kanan, cursor kembali ke kiri.

Secara konseptual:

old trace                 new sample
    |                          |
    v                          v
+--------------------+    +--------------------+
|      /\      /     |    |      /\      /     |
|  /\/  \_/\/       | -> |  /\/  \_/\/  \    |
|            ^       |    |             ^      |
+------------|-------+    +-------------|------+
             cursor                     cursor

Jumlah pekerjaan gambar per sampel menjadi terbatas. Tampilan ini tidak menggeser semua titik lama ke kiri, sehingga perilakunya lebih menyerupai sweep oscilloscope daripada chart yang seluruh isinya terus bergeser.

Grafik yang benar-benar bergeser ke kiri memerlukan redraw area plot dari sampel yang tersimpan, pemakaian fitur controller secara hati-hati jika sesuai, atau penyimpanan data pixel di tempat lain. Ring buffer menyelesaikan riwayat sampel; ring buffer tidak memindahkan pixel yang sudah berada di display.

Menghapus garis membutuhkan pengetahuan tentang area di bawahnya

Incremental drawing membawa masalah kecil tetapi penting: menggambar garis baru mudah, sedangkan pixel lama akan tetap terlihat jika tidak dihapus secara eksplisit.

Menghapus strip sempit sebelum dipakai ulang sering lebih sederhana daripada mencoba menggambar tepat di atas garis lama dengan warna background. Strip tersebut dapat mencakup seluruh tinggi plot:

plot height = 160 px
strip width = 2 px
RGB565 payload = 160 * 2 * 2 = 640 bytes

Jumlah ini jauh lebih kecil daripada transfer satu layar penuh.

Ada konsekuensinya. Jika grid line atau label melintasi strip tersebut, proses clear ikut menghapusnya. Firmware perlu menggambar kembali dekorasi statis di area yang dibersihkan, atau menempatkan label di luar area plot yang berubah.

Batas layout yang berguna kemudian terlihat seperti ini:

+--------------------------+
| title / numeric reading  |  updated separately
+--------------------------+
|                          |
|      plot rectangle      |  incremental redraw
|                          |
+--------------------------+
| fixed labels             |  rarely redrawn
+--------------------------+

Memperlakukan setiap area berdasarkan frekuensi perubahannya membuat traffic display lebih mudah diprediksi.

embedded-graphics cocok untuk layer drawing

Pada Rust, embedded-graphics menyediakan geometri, warna, teks, dan drawing primitives melalui DrawTarget. Driver ILI9341 yang kompatibel dapat mengekspos panel melalui abstraksi tersebut, sedangkan ESP HAL menyediakan implementasi SPI dan GPIO di bawahnya.

Layer-nya kira-kira:

application graph state
        |
embedded-graphics primitives
        |
ILI9341 display driver
        |
SPI + GPIO traits / HAL
        |
ESP32-C3

Pemisahan ini penting. Ring buffer dan kode scaling tidak perlu mengetahui peripheral SPI mana yang digunakan. Sebaliknya, driver display tidak perlu mengetahui apakah garis yang digambar mewakili suhu, tegangan, atau akselerasi.

API dan constructor crate dapat berubah antar-release. Firmware sebaiknya mengunci versi crate yang kompatibel dan mengikuti API driver yang dipilih, bukan menyalin kode inisialisasi dari release lain.

Pisahkan timing akuisisi sensor dari timing display

Update display dapat memakan waktu jauh lebih lama daripada membaca register kecil dari sensor I²C. Menggabungkan keduanya dalam satu jalur yang sensitif terhadap timing dapat menimbulkan jitter pada pengambilan sampel.

Batas yang lebih baik adalah:

sensor acquisition -> sample queue/state -> display update

Sisi akuisisi mencatat pengukuran sesuai cadence yang dibutuhkan. Sisi display mengambil state terbaru pada kecepatan yang mampu ditangani layar.

Pemisahan ini semakin penting setelah Wi-Fi ditambahkan. Aktivitas jaringan, akuisisi sensor, dan transfer display memiliki karakteristik latency yang berbeda. Grafik tidak seharusnya diam-diam mengubah sampling interval hanya karena satu transaksi SPI membutuhkan waktu lebih lama.

Dengan async embedded executor seperti Embassy, tanggung jawab tersebut dapat ditempatkan pada task terpisah jika stack ESP32-C3 yang digunakan menyediakan driver yang diperlukan. Firmware sinkron dapat mempertahankan batas arsitektur yang sama dengan timer dan cooperative main loop.

Kepemilikan bus SPI adalah bagian dari desain

Modul ILI9341 dapat berbagi bus SPI dengan peripheral lain seperti kartu microSD. Berbagi MOSI, MISO, dan SCLK tidak berarti kedua perangkat dapat digunakan secara bersamaan. Masing-masing membutuhkan perilaku chip-select yang benar dan akses bus harus diserialisasi.

Karena itu, renderer grafik sebaiknya tidak menahan bus SPI lebih lama daripada yang diperlukan. Full-screen redraw yang besar memperpanjang waktu tunggu peripheral lain. Update sempit mengurangi traffic display sekaligus durasi pemakaian bus.

Ini juga alasan performa display tidak dapat ditentukan hanya dari frekuensi SPI yang dikonfigurasi. Setup transaksi, transisi command/data, pengelolaan chip-select, batching pada driver, dan peripheral lain yang bersaing ikut menentukan throughput yang benar-benar tersedia.

Anggaran memori mengikuti ownership

Framebuffer bukan satu-satunya pemakai RAM. Buffer sensor, network packet, state protocol, task stack, string, dan alokasi driver bersaing menggunakan memori yang sama.

Untuk grafik, anggarannya dapat dibuat kecil dan terlihat jelas:

sample history      400 bytes   (200 * i16)
graph metadata       tens of bytes
temporary geometry   small stack values
framebuffer           0 bytes

Total persisnya bergantung pada layout Rust dan program di sekitarnya, tetapi sifat strukturalnya tetap sama: kebutuhan memori bertambah mengikuti bounded application state, bukan setiap pixel pada panel.

Prinsip yang sama berlaku di luar satu display controller. Selama display dapat menerima drawing command secara langsung dan aplikasi tidak membutuhkan random access terhadap satu gambar off-screen lengkap, firmware tidak harus menyimpan salinan seluruh layar.

Batas yang berguna adalah area plot, bukan seluruh layar

Grafik sensor live pada ESP32-C3 tidak perlu memperlakukan ILI9341 seperti monitor desktop. Panel sudah menyimpan pixel yang sedang terlihat. Firmware cukup mempertahankan informasi yang diperlukan untuk menghasilkan update berikutnya.

Ring buffer berukuran tetap menyimpan riwayat pengukuran. Fungsi scaling khusus memberi arti pixel yang stabil terhadap nilai sensor. Incremental redraw membatasi traffic SPI, sementara pemisahan akuisisi dan rendering mencegah latency display berubah menjadi masalah timing sensor.

Hasilnya memang bukan graphics pipeline serbaguna. Jalur display ini dibentuk untuk satu workload embedded tertentu, dan justru karena itu kebutuhan memori serta kerjanya dapat tetap kecil dan dapat diprediksi.