Bahasa: ID

Penampil foto layar sentuh dengan ESP32

Cara saya membuat penampil foto kecil dengan ESP32, layar sentuh 3,2" dan kartu SD, lengkap dengan menu, thumbnail dan geser, di chip tanpa memori tambahan.

Firmware ·

  • ESP32
  • Arduino
  • C++
  • SPI
  • DMA
  • LCD
  • Kartu SD
Layar 3,2 inci menampilkan foto anak kucing, dengan kabel jumper di belakangnya

Spesifikasi utama

PapanESP32 DOIT DevKit V1 (ESP32-WROOM-32, tanpa PSRAM)
LayarTFT 3,2", ILI9341, 320 × 240, SPI
SentuhXPT2046 resistif, di bus SPI yang sama dengan layar
PenyimpananModul kartu microSD, FAT32, di bus SPI sendiri
LibraryLovyanGFX, JPEGDEC, Arduino SD
BuildArduino core untuk ESP32 3.3, arduino-cli dan Makefile
Kecepatan layar34 frame per detik, layar penuh, SPI 40 MHz
Kecepatan SDTulis 1,7 MB/s, baca 1,37 MB/s, SPI 20 MHz

Diagram blok

Diagram kabel. ESP32 terhubung ke layar dan chip sentuh lewat satu bus SPI (VSPI, pin 18, 23 dan 19) dan ke kartu SD lewat bus kedua (HSPI, pin 14, 13 dan 27). Dua tombol di pin 25 dan 26 terhubung ke ground.

Gambaran umum

Saya ingin tahu sejauh mana ESP32, layar sentuh murah 3,2” dan kartu SD bisa bekerja bersama. Hasilnya adalah penampil foto kecil. Ada menu utama, halaman thumbnail yang bisa digeser, dan tampilan layar penuh. Dua tombol bisa melakukan hal yang sama seperti layar sentuh.

ESP32 yang saya pakai tidak punya memori tambahan (PSRAM). Total RAM-nya sekitar 520 KB, dan jauh lebih kecil dalam satu blok. Batas ini menentukan hampir semua keputusan di proyek ini.

Kodenya ditulis dalam C++ dengan Arduino core untuk ESP32. Tidak perlu membaca semuanya untuk mengikuti halaman ini. Setiap potongan kode di bawah pendek dan hanya menunjukkan satu ide.

Komponen dan kabel

Saya memakai tiga chip. ILI9341 menggerakkan layar, XPT2046 membaca sentuhan, dan kartu SD menyimpan foto. Ketiganya memakai SPI, bus sederhana dengan satu kabel clock, satu kabel data keluar dan satu kabel data masuk, ditambah satu kabel “chip select” untuk tiap chip.

ESP32 punya dua bus SPI yang bebas, jadi tiap tugas saya beri bus sendiri.

Komponen Bus Pin (GPIO) Kecepatan
Layar VSPI SCK 18, MOSI 23, MISO 19, CS 5, DC 16, RST 17 40 MHz
Sentuh VSPI (berbagi) CS 33, IRQ 34 1 MHz
Kartu SD HSPI SCK 14, MOSI 13, MISO 27, CS 15 20 MHz
Tombol - 25 dan 26, ke GND -

Layar dan chip sentuh berbagi satu bus. Kartu SD sendirian di bus yang lain. Dengan begitu, membaca file tidak perlu menunggu layar, dan layar tidak perlu menunggu kartu.

Semua pin ada di satu file, pins.h, jadi saya cukup mengubahnya di satu tempat.

// LCD ILI9341 (VSPI)
#define PIN_LCD_SCLK   18
#define PIN_LCD_MOSI   23
#define PIN_LCD_MISO   19
#define PIN_LCD_CS      5

// SD card (HSPI)
#define PIN_SD_SCLK    14
#define PIN_SD_MOSI    13
#define PIN_SD_MISO    27   // not GPIO12, the board won't boot
#define PIN_SD_CS      15

Komentar di sebelah MISO itu penting. GPIO12 adalah pin MISO HSPI yang biasa, tapi juga pin “strapping”. ESP32 membacanya saat dinyalakan untuk memilih tegangan flash. Modul kartu SD menarik jalur itu ke atas, jadi chip akan memilih 1,8 V dan tidak mau menyala. Karena itu saya memasang MISO di GPIO27 sejak awal. Penjelasan lengkapnya ada di tulisan blog ini.

Build dan flash

Saya build dari terminal dengan arduino-cli dan Makefile kecil, jadi tidak perlu membuka Arduino IDE.

$ make build
$ make flash PORT=/dev/ttyUSB0

Menggambar layar per strip

Satu layar penuh berukuran 320 × 240 piksel, dengan 2 byte per piksel. Totalnya 150 KB, lebih besar dari yang bisa diberikan ESP32 dalam satu blok. Jadi saya tidak pernah menyimpan satu frame utuh di memori.

Saya menggambar layar per strip 24 baris, dengan dua buffer kecil masing-masing 15 KB. Selagi CPU mengisi satu strip, hardware SPI mengirim strip lainnya ke layar sendiri (itu DMA). CPU dan bus bekerja bersamaan.

int buf = 0;
for (int y = 0; y < height; y += STRIP_H) {
  fillStrip(strip[buf], y);   // the CPU draws 24 rows
  // waits for the other strip to finish, then sends this one with DMA
  lcd.pushImageDMA(0, y, 320, STRIP_H, strip[buf]);
  buf ^= 1;                   // switch buffers
}

Saya mengujinya dengan animasi “plasma” yang mengubah setiap piksel di setiap frame. Saya mengukur setiap bagian.

  • Dengan SPI 40 MHz saya dapat 34 frame per detik. CPU butuh 9,4 ms per frame, lalu menunggu bus 17 ms. Yang membatasi adalah bus, bukan CPU.
  • Di 80 MHz bisa 66 frame per detik, tapi muncul garis terpotong di layar. Modul dan kabel jumper saya tidak sanggup, jadi saya tetap di 40 MHz.

Menampilkan foto yang lebih besar dari layar

Foto dari kamera sering selebar 4000 piksel, sedangkan layarnya 320. Library JPEGDEC bisa men-decode JPEG di ukuran penuh, ½, ¼ atau ⅛, jauh lebih cepat daripada decode semuanya lalu mengecilkannya. Saya pilih ukuran terkecil yang masih cukup besar.

// largest shrink that still gives at least the size I need
int div = 8;
if (w / 8 < fitW || h / 8 < fitH) div = 4;
if (w / 4 < fitW || h / 4 < fitH) div = 2;
if (w / 2 < fitW || h / 2 < fitH) div = 1;

Lalu saya ubah ke ukuran yang pas baris per baris, dengan perbandingan gambar tetap. Sisa di sekelilingnya menjadi garis hitam. File PNG dan BMP lewat LovyanGFX, yang bisa menyesuaikannya sendiri.

Thumbnail yang cepat dimuat

Halaman thumbnail menampilkan 6 gambar. Men-decode enam JPEG besar setiap kali itu lambat. Jadi saat pertama kali membuat thumbnail (96 × 72), saya menyimpannya di kartu SD dalam folder tersembunyi /.thumbs. Berikutnya saya cukup membaca 14 KB piksel yang sudah jadi.

Setiap thumbnail yang disimpan diawali header kecil. Header itu mengingat ukuran dan tanggal foto aslinya. Jadi kalau saya mengganti foto, thumbnail-nya dibuat ulang.

struct ThumbHeader {
  char     magic[4];   // "THB1"
  uint16_t w, h;
  uint32_t srcSize;    // size and date of the photo
  uint32_t srcTime;    // if one changes, rebuild
};

Kartu SD

Kartu dimulai di 20 MHz. Kalau gagal, misalnya karena kabel panjang, dicoba lagi di 4 MHz.

const uint32_t freqs[] = { 20000000, 4000000 };
for (uint32_t f : freqs) {
  if (SD.begin(PIN_SD_CS, sdSpi, f)) return true;
  SD.end();
}
return false;

Meminta 25 MHz tetap menghasilkan 20 MHz. ESP32 hanya bisa membagi clock 80 MHz menjadi 40, 26,7 atau 20 MHz, dan library SD tidak melewati 25. Jadi 20 MHz adalah batas sebenarnya dengan library ini.

Saya menulis tes kecepatan yang menulis 512 KB, membacanya lagi dan memeriksa setiap byte.

Ukuran blok Tulis Baca
4 KB 1,3 MB/s 1,37 MB/s
16 KB 1,7 MB/s 1,37 MB/s
32 KB 1,7 MB/s 1,37 MB/s

Menulis dengan blok 16 KB atau lebih itu lebih cepat. Kecepatan baca tidak berubah, karena library membaca kartu 512 byte sekali jalan tanpa DMA.

Satu jebakan lagi. Kartu butuh resistor pull-up di MISO, dan banyak modul tidak punya. Di ESP32 core 3.x, pinMode() pada pin yang sudah dipakai bus SPI justru melepasnya dari bus. Jadi saya menyalakan pull-up dengan cara lain.

gpio_pullup_en((gpio_num_t)PIN_SD_MISO);   // not pinMode()

Tombol

Setiap tombol terhubung dari pin ke GND, dengan pull-up internal ESP32. Saat ditekan, pin terbaca LOW. Tombol asli “memantul” beberapa milidetik, jadi saya hanya menerima keadaan baru kalau sudah stabil 20 ms. Tekan singkat dihitung klik, dan 0,8 detik atau lebih dihitung tekan lama.

bool raw = digitalRead(pin) == LOW;
if (raw != lastRaw) { lastRaw = raw; changedAt = millis(); }

if (raw != stable && millis() - changedAt >= 20) {
  stable = raw;   // the level held for 20 ms, it's real
  ...
}

BTN1 pindah ke item berikutnya dan BTN2 membukanya. Menahan BTN2 untuk kembali.

Masalah yang saya temui

  • Layar putih, tanpa gambar. Lampu latar menyala, tapi layar tidak menerima perintah. Ternyata ada kabel jumper yang longgar. Membaca ID ILI9341 lewat MISO (harus menjawab 00 93 41) sekarang jadi pemeriksaan pertama saya.
  • Sentuhan tidak berfungsi. Dua kabel data sentuh tertukar. Saya menemukannya tanpa menekan layar sama sekali. XPT2046 punya sensor suhu bawaan, dan chip yang berfungsi memberi nilai stabil sekitar 830 di suhu ruang. Nilai acak berarti chip tidak menjawab.
  • JPEG progresif tidak bisa dibuka. JPEGDEC hanya membaca JPEG “baseline”, jadi foto harus disimpan dengan format itu sebelum dimasukkan ke kartu.
  • Kartu SD besar. Kartu di atas 32 GB biasanya berformat exFAT, yang tidak didukung di sini. Kartu itu perlu diformat ke FAT32.

Yang saya pelajari

  • Memori menentukan desain. Tanpa PSRAM, satu frame penuh tidak muat. Strip, buffer kecil, dan memori yang hanya dipakai saat sebuah layar terbuka membuat semuanya mungkin.
  • Ukur dulu, jangan menebak. Mengukur waktu setiap bagian menunjukkan bahwa batasnya ada di bus SPI, bukan di CPU. CPU sendiri bisa menggambar sekitar 100 frame per detik.
  • Dua bus lebih baik dari satu. Memisahkan layar dan kartu SD menghilangkan satu jenis waktu tunggu.
  • Pelajari pin-nya dulu. Pin strapping, pin flash dan pin khusus input di ESP32 bisa menghabiskan berjam-jam kalau dipilih asal.
  • Kabel adalah separuh dari debugging. Dua masalah tersulit saya berasal dari kabel, bukan kode. Tes kecil yang membaca ID atau sensor dari setiap chip menghemat banyak waktu.

Selanjutnya

Saya sudah mencoba memutar video pendek (MJPEG) dari kartu. Hasilnya sekitar 25 frame per detik di 256 × 192, dan kecepatan baca kartu yang membatasi. Saya juga sudah menyiapkan pin untuk input analog, sensor I2C dan satu perangkat SPI lagi.

← Kembali ke semua proyek