Memprogram game untuk perangkat yang disematkan di ESP32

Bagian 0: motivasi


pengantar


Saya mencari proyek hobi yang bisa saya kerjakan di luar tugas utama saya untuk melarikan diri dari situasi di dunia. Saya sebagian besar tertarik pada pemrograman game, tapi saya juga suka sistem embedded. Sekarang saya bekerja di perusahaan game, tetapi sebelumnya saya terutama terlibat dalam mikrokontroler. Meskipun pada akhirnya saya memutuskan untuk mengubah jalur saya dan masuk ke industri game, saya masih suka bereksperimen dengan mereka. Jadi mengapa tidak menggabungkan kedua hobi?

Odroid pergi


Saya memiliki Odroid Go , yang akan menarik untuk dimainkan. Intinya adalah ESP32 - mikrokontroler yang sangat populer dengan fungsionalitas standar MK (SPI, I2C, GPIO, timer, dll.), Tetapi juga dengan WiFi dan Bluetooth, yang membuatnya menarik untuk membuat perangkat IoT.

Odroid Go melengkapi ESP32 dengan banyak periferal, mengubahnya menjadi mesin game portabel yang mengingatkan Gameboy Color: layar LCD, speaker, palang kontrol, dua tombol tambahan utama dan empat, baterai dan pembaca kartu SD.

Kebanyakan orang membeli Odroid Go untuk menjalankan emulator dari sistem 8-bit lama. Jika benda ini mampu meniru permainan lama, itu juga akan mengatasi peluncuran game asli yang dirancang khusus untuk itu.


Keterbatasan


Resolusi 320x240

Layar hanya memiliki ukuran 320x240, jadi kami sangat terbatas dalam jumlah informasi yang ditampilkan di layar pada saat yang bersamaan. Kita perlu mempertimbangkan dengan cermat game apa yang akan kita buat dan sumber daya apa yang digunakan.

Warna 16-bit

Layar mendukung warna 16-bit per piksel: 5 bit untuk merah, 6 bit untuk hijau, dan 5 untuk biru. Untuk alasan yang jelas, sirkuit seperti itu biasanya disebut RGB565. Hijau menjadi sedikit lebih merah dan biru, karena mata manusia lebih baik membedakan antara gradasi hijau daripada biru atau merah.

Warna 16-bit berarti kita hanya memiliki akses ke 65 ribu warna. Bandingkan ini dengan warna standar 24-bit (8 bit per warna), memberikan 16 juta warna.

Kurangnya GPU

Tanpa GPU, kami tidak dapat menggunakan API seperti OpenGL. Saat ini, GPU yang sama biasanya digunakan untuk rendering game 2D seperti untuk game 3D. Hanya alih-alih objek, segi empat digambar, di mana tekstur bit ditumpangkan. Tanpa GPU, kita harus meraster setiap piksel dengan CPU, yang lebih lambat tapi lebih sederhana.

Dengan resolusi layar 320x240 dan warna 16-bit, ukuran buffer bingkai total adalah 153.600 byte. Ini berarti bahwa setidaknya tiga puluh kali per detik kita perlu mengirimkan 153.600 byte ke layar. Ini pada akhirnya dapat menyebabkan masalah, jadi kita harus lebih pintar saat merender layar. Misalnya, Anda dapat mengubah warna yang diindeks menjadi palet sehingga untuk setiap piksel Anda perlu menyimpan satu byte, yang akan digunakan sebagai indeks palet 256-warna.

4 MB

ESP32 memiliki RAM internal 520 KB, sementara Odroid Go menambahkan 4 MB RAM eksternal. Tetapi tidak semua memori ini tersedia untuk kita, karena sebagian digunakan oleh ESP32 SDK (lebih lanjut tentang ini nanti). Setelah menonaktifkan semua kemungkinan fungsi luar dan memasukkan fungsi utama saya, ESP32 melaporkan bahwa kita dapat menggunakan 4.494.848 byte. Jika di masa depan kita membutuhkan lebih banyak memori, maka nanti kita dapat kembali memangkas fungsi yang tidak perlu.

Prosesor 80-240 MHz

CPU dikonfigurasi pada tiga kemungkinan kecepatan: 80 MHz, 160 MHz dan 240 MHz. Bahkan maksimum 240 MHz jauh dari kekuatan lebih dari tiga gigahertz komputer modern yang biasa kita gunakan untuk bekerja. Kita akan mulai dari 80 MHz dan melihat sejauh mana kita bisa melangkah. Jika kita ingin game bekerja dengan daya baterai, maka konsumsi daya harus rendah. Untuk melakukan ini, alangkah baiknya untuk menurunkan frekuensinya.

Debugging buruk

Ada beberapa cara untuk menggunakan debugger dengan perangkat yang disematkan (JTAG), tetapi, sayangnya, Odroid Go tidak memberi kami kontak yang diperlukan, jadi kami tidak dapat menelusuri kode dalam debugger, seperti yang biasanya terjadi. Ini berarti bahwa debugging dapat menjadi proses yang sulit, dan kami harus secara aktif menggunakan debugging di layar (menggunakan warna dan teks), dan juga menampilkan informasi ke konsol debugging (yang, untungnya, mudah diakses melalui USB UART).

Kenapa semua masalah?


Mengapa bahkan mencoba membuat game untuk perangkat yang lemah ini dengan semua batasan yang tercantum di atas, dan tidak menulis apa pun untuk PC desktop? Ada dua alasan untuk ini:

Keterbatasan menstimulasi kreativitas

Ketika Anda bekerja dengan sistem yang memiliki seperangkat peralatan tertentu, yang masing-masing memiliki keterbatasannya sendiri, itu membuat Anda memikirkan bagaimana cara terbaik memanfaatkan kelebihan dari keterbatasan ini. Jadi kami lebih dekat dengan pengembang game sistem lama, misalnya, Super Nintendo (tapi itu masih jauh lebih mudah bagi kami daripada bagi mereka).

Pengembangan tingkat rendah itu menyenangkan

Untuk menulis game dari awal untuk sistem desktop biasa, kita harus bekerja dengan konsep mesin tingkat rendah standar: rendering, fisika, pengenalan tabrakan. Tetapi ketika menerapkan semua ini pada perangkat yang disematkan, kita juga harus berurusan dengan konsep komputer tingkat rendah, misalnya, menulis driver LCD.

Seberapa rendah perkembangannya?


Ketika datang ke tingkat rendah dan membuat kode Anda sendiri, Anda harus menggambar perbatasan di suatu tempat. Jika kami mencoba menulis game tanpa pustaka untuk desktop, maka perbatasan kemungkinan merupakan sistem operasi atau API lintas platform seperti SDL. Dalam proyek saya, saya akan menarik garis pada penulisan hal-hal seperti driver SPI dan bootloader. Bersama mereka lebih banyak siksaan daripada kesenangan.

Jadi, kita akan menggunakan ESP-IDF, yang pada dasarnya adalah SDK untuk ESP32. Kita dapat mengasumsikan bahwa ia memberi kita beberapa utilitas yang biasanya disediakan oleh sistem operasi , tetapi sistem operasi tidak bekerja di ESP32 . Sebenarnya, MK ini menggunakan FreeRTOS, yang merupakan sistem operasi real-timetapi ini bukan OS nyata. Ini hanya perencana. Kemungkinan besar, kita tidak akan berinteraksi dengannya, tetapi pada intinya ESP-IDF menggunakannya.

ESP-IDF menyediakan kami dengan API untuk periferal ESP32 seperti SPI, I2C, dan UART, serta pustaka runtime C, jadi ketika kita memanggil sesuatu seperti printf, ia sebenarnya mentransfer byte melalui UART untuk ditampilkan pada monitor antarmuka serial. Itu juga memproses semua kode startup yang diperlukan untuk menyiapkan mesin sebelum memanggil titik peluncuran game kami.

Dalam posting ini saya akan menyimpan majalah pengembangan di mana saya akan berbicara tentang hal-hal menarik yang menurut saya dan menjelaskan aspek yang paling sulit. Saya tidak punya rencana dan kemungkinan besar saya akan membuat banyak kesalahan. Semua ini saya buat karena minat.

Bagian 1: membangun sistem


pengantar


Sebelum kita dapat mulai menulis kode untuk Odroid Go, kita perlu mengkonfigurasi ESP32 SDK. Ini berisi kode yang memulai ESP32 dan memanggil fungsi utama kami, serta kode periferal (misalnya, SPI) yang akan kita perlukan ketika kita menulis driver LCD.

Espressif menyebut ESP-IDF SDK-nya ; kami menggunakan versi stabil terbaru v4.0 .

Kami dapat mengkloning repositori sesuai dengan instruksi mereka (dengan bendera rekursif ), atau cukup mengunduh zip dari halaman rilis.

Tujuan pertama kami adalah aplikasi Hello World bergaya minimal yang diinstal di Odroid Go yang membuktikan pengaturan lingkungan build yang benar.

C atau C ++


ESP-IDF menggunakan C99, jadi kami akan memilihnya juga. Jika diinginkan, kita bisa menggunakan C ++ (ada kompiler C ++ di ESP32 toolchain), tetapi untuk sekarang, kita akan tetap menggunakan C.

Sebenarnya, saya suka C dan kesederhanaannya. Tidak peduli berapa banyak saya menulis kode dalam C ++, saya tidak pernah berhasil mencapai momen menikmatinya.

Orang ini meringkas pikiran saya dengan cukup baik.

Selain itu, jika perlu, kami dapat beralih ke C ++ kapan saja.

Proyek minimal


IDF menggunakan CMake untuk mengelola sistem pembangunan. Itu juga mendukung Makefile, tetapi tidak digunakan lagi di v4.0, jadi kami hanya akan menggunakan CMake.

Minimal, kita memerlukan file CMakeLists.txt dengan deskripsi proyek kami, folder utama dengan file sumber dari titik masuk ke dalam permainan, dan file CMakeLists.txt lain di dalam main , yang berisi daftar file sumber.

CMake perlu referensi variabel lingkungan yang memberitahukan di mana harus mencari IDF dan toolchain. Saya kesal karena saya harus menginstalnya kembali setiap kali saya memulai sesi terminal baru, jadi saya menulis skrip export.sh . Ini menetapkan IDF_PATH dan IDF_TOOLS_PATH, dan juga sumber ekspor IDF yang menetapkan variabel lingkungan lainnya.

Hal ini cukup untuk pengguna script untuk mengatur IDF_PATH dan IDF_TOOLS_PATH variabel .

IDF_PATH=
IDF_TOOLS_PATH=


if [ -z "$IDF_PATH" ]
then
	echo "IDF_PATH not set"
	return
fi

if [ -z "$IDF_TOOLS_PATH" ]
then
	echo "IDF_TOOLS_PATH not set"
	return
fi


export IDF_PATH
export IDF_TOOLS_PATH

source $IDF_PATH/export.sh

CMakeLists.txt di root:

cmake_minimum_required(VERSION 3.5)

set(COMPONENTS "esptool_py main")

include($ENV{IDF_PATH}/tools/cmake/project.cmake)

project(game)

Secara default, sistem build akan membangun setiap komponen yang mungkin ada di dalam $ ESP_IDF / komponen , yang akan menghasilkan lebih banyak waktu kompilasi. Kami ingin mengkompilasi sekumpulan komponen minimal untuk memanggil fungsi utama kami, dan menghubungkan komponen tambahan nanti jika perlu. Ini adalah untuk apa variabel KOMPONEN .

CMakeLists.txt di dalam main :

idf_component_register(
	SRCS "main.c"
    INCLUDE_DIRS "")

Segala sesuatu yang dia lakukan - tak terhingga satu detik ditampilkan di monitor antarmuka serial "Hello World". VTaskDelay menggunakan FreeRTOS untuk menunda .

File main.c sangat sederhana:

#include <stdio.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>


void app_main(void)
{
	for (;;)
	{
		printf("Hello World!\n");
		vTaskDelay(1000 / portTICK_PERIOD_MS);
	}

	// Should never get here
	esp_restart();
}

Perhatikan bahwa fungsi kita disebut app_main , bukan main . Fungsi utama digunakan oleh IDF untuk persiapan yang diperlukan, dan kemudian itu membuat tugas dengan fungsi app_main kami sebagai titik masuk.

Tugas hanyalah blok yang dapat dieksekusi yang dapat dikelola FreeRTOS. Meskipun kita tidak perlu khawatir tentang ini (atau mungkin tidak sama sekali), penting untuk dicatat di sini bahwa permainan kita berjalan dalam satu inti (ESP32 memiliki dua inti), dan dengan setiap iterasi dari for loop, tugas menunda eksekusi selama satu detik. Selama penundaan ini, penjadwal FreeRTOS dapat mengeksekusi kode lain yang sedang menunggu untuk dieksekusi (jika ada).

Kita dapat menggunakan kedua inti, tetapi untuk sekarang, mari kita batasi diri kita sendiri.

Komponen


Bahkan jika kita mengurangi daftar komponen ke minimum yang diperlukan untuk aplikasi Hello World (yang esptool_py dan utama ), karena konfigurasi rantai ketergantungan, ia masih mengumpulkan beberapa komponen lain yang tidak kita butuhkan. Ia mengumpulkan semua komponen ini:

app_trace app_update bootloader bootloader_support cxx driver efuse esp32 esp_common esp_eth esp_event esp_ringbuf
esp_rom esp_wifi espcoredump esptool_py freertos heap log lwip main mbedtls newlib nvs_flash partition_table pthread
soc spi_flash tcpip_adapter vfs wpa_supplicant xtensa

Banyak dari mereka cukup logis ( bootloader , esp32 , freertos ), tetapi mereka diikuti oleh komponen yang tidak perlu karena kami tidak menggunakan fungsi jaringan: esp_eth, esp_wifi, lwip, mbedtls, tcpip_adapter, wpa_supplicant . Sayangnya, kami masih dipaksa untuk merakit komponen-komponen ini.

Untungnya, tautannya cukup pintar dan tidak memasukkan komponen yang tidak digunakan ke dalam file biner permainan yang sudah jadi. Kami dapat memverifikasi ini dengan membuat ukuran-komponen .

Total sizes:
 DRAM .data size:    8476 bytes
 DRAM .bss  size:    4144 bytes
Used static DRAM:   12620 bytes ( 168116 available, 7.0% used)
Used static IRAM:   56345 bytes (  74727 available, 43.0% used)
      Flash code:   95710 bytes
    Flash rodata:   40732 bytes
Total image size:~ 201263 bytes (.bin may be padded larger)
Per-archive contributions to ELF file:
            Archive File DRAM .data & .bss   IRAM Flash code & rodata   Total
                  libc.a        364      8   5975      63037     3833   73217
              libesp32.a       2110    151  15236      15415    21485   54397
           libfreertos.a       4148    776  14269          0     1972   21165
                libsoc.a        184      4   7909        875     4144   13116
          libspi_flash.a        714    294   5069       1320     1386    8783
                libvfs.a        308     48      0       5860      973    7189
         libesp_common.a         16   2240    521       1199     3060    7036
             libdriver.a         87     32      0       4335     2200    6654
               libheap.a        317      8   3150       1218      748    5441
             libnewlib.a        152    272    869        908       99    2300
        libesp_ringbuf.a          0      0    906          0      163    1069
                liblog.a          8    268    488         98        0     862
         libapp_update.a          0      4    127        159      486     776
 libbootloader_support.a          0      0      0        634        0     634
                libhal.a          0      0    519          0       32     551
            libpthread.a          8     12      0        288        0     308
             libxtensa.a          0      0    220          0        0     220
                libgcc.a          0      0      0          0      160     160
               libmain.a          0      0      0         22       13      35
                libcxx.a          0      0      0         11        0      11
                   (exe)          0      0      0          0        0       0
              libefuse.a          0      0      0          0        0       0
         libmbedcrypto.a          0      0      0          0        0       0
     libwpa_supplicant.a          0      0      0          0        0       0

Yang terpenting, libc mempengaruhi ukuran biner, dan itu tidak masalah.

Konfigurasi proyek


IDF memungkinkan Anda menentukan parameter konfigurasi waktu kompilasi yang digunakan selama perakitan untuk mengaktifkan atau menonaktifkan berbagai fungsi. Kita perlu mengatur parameter yang memungkinkan kita memanfaatkan aspek tambahan dari Odroid Go.

Pertama, Anda perlu menjalankan skrip sumber export.sh agar CMake memiliki akses ke variabel lingkungan yang diperlukan. Selanjutnya, seperti untuk semua proyek CMake, kita perlu membuat folder perakitan dan memanggil CMake darinya.

source export.sh
mkdir build
cd build
cmake ..

Jika Anda menjalankan make menuconfig , sebuah jendela akan terbuka di mana Anda dapat mengonfigurasi pengaturan proyek.

Memperluas memori flash hingga 16 MB


Odroid Go memperluas kapasitas flash drive standar hingga 16 MB. Anda dapat mengaktifkan fitur ini dengan masuk ke Serial flasher config -> Flash size -> 16MB .

Nyalakan RAM SPI eksternal


Kami juga memiliki akses ke tambahan 4 MB RAM eksternal yang terhubung melalui SPI. Anda dapat mengaktifkannya dengan masuk ke Konfigurasi komponen -> ESP32-spesifik -> Dukungan untuk RAM eksternal yang terhubung dengan SPI dan menekan spasi untuk mengaktifkannya. Kami juga ingin dapat mengalokasikan memori secara eksplisit dari RAM SPI; ini dapat diaktifkan dengan masuk ke konfigurasi RAM SPI -> Metode akses RAM SPI -> Jadikan RAM dapat dialokasikan menggunakan heap_caps_malloc .

Turunkan frekuensinya


ESP32 bekerja secara default dengan frekuensi 160 MHz, tapi mari kita turunkan ke 80 MHz untuk melihat seberapa jauh Anda bisa pergi dengan frekuensi clock terendah. Kami ingin game bekerja dengan daya baterai, dan menurunkan frekuensi akan menghemat daya. Anda dapat mengubahnya dengan masuk ke Konfigurasi komponen -> ESP32-spesifik -> frekuensi CPU -> 80MHz .

Jika Anda memilih Simpan , file sdkconfig akan disimpan ke root folder proyek . Kita dapat menulis file ini di git, tetapi memiliki banyak parameter yang tidak penting bagi kita. Sejauh ini, kami puas dengan parameter standar, kecuali untuk yang baru saja kami ubah.

Anda dapat membuat file sdkconfig.defaults sebagai gantinyayang akan berisi nilai yang diubah di atas. Segala sesuatu yang lain akan dikonfigurasi secara default. Selama proses pembuatan, IDF akan membaca sdkconfig.defaults , menimpa nilai yang kita atur, dan menggunakan standar untuk semua parameter lainnya.

Sekarang sdkconfig.default terlihat seperti ini:

# Set flash size to 16MB
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y

# Set CPU frequency to 80MHz
CONFIG_ESP32_DEFAULT_CPU_FREQ_80=y

# Enable SPI RAM and allocate with heap_caps_malloc()
CONFIG_ESP32_SPIRAM_SUPPORT=y
CONFIG_SPIRAM_USE_CAPS_ALLOC=y

Secara umum, struktur asli permainan terlihat seperti ini:

game
├── CMakeLists.txt
├── export.sh
├── main
│   ├── CMakeLists.txt
│   └── main.c
└── sdkconfig.defaults

Bangun dan flash


Proses perakitan dan firmware itu sendiri cukup sederhana.

Kita jalankan make to compile (untuk build paralel, tambahkan -j4 atau -j8 ), buat flash untuk menulis gambar ke Odroid Go, dan buat monitor untuk melihat output dari pernyataan printf .

make
make flash
make monitor

Kami juga dapat menjalankannya dalam satu baris.

make flash monitor

Hasilnya tidak terlalu mengesankan, tetapi itu akan menjadi dasar untuk sisa proyek.


Referensi



Bagian 2: input


pengantar


Kita harus bisa membaca tombol yang ditekan oleh pemain dan umpan silang di Odroid Go.

Tombol



GPIO


Odroid Go memiliki enam tombol: A , B , Pilih , Mulai , Menu dan Volume .

Masing-masing tombol terhubung ke pin General Purpose IO (GPIO) yang terpisah . Pin GPIO dapat digunakan sebagai input (untuk membaca) atau sebagai output (kami menulis kepada mereka). Dalam hal tombol, kita perlu membaca.

Pertama, Anda perlu mengkonfigurasi kontak sebagai input, setelah itu kami dapat membaca statusnya. Kontak di dalam memiliki satu dari dua tegangan (3.3V atau 0V), tetapi ketika membacanya menggunakan fungsi IDF, mereka dikonversi ke nilai integer.

Inisialisasi


Elemen yang ditandai sebagai SW dalam diagram adalah tombol fisik itu sendiri. Ketika tidak ditekan, kontak ESP32 ( IO13 , IO0 , dll.) Terhubung ke 3.3 V; yaitu 3.3 V berarti bahwa tombol tidak ditekan . Logikanya di sini adalah kebalikan dari apa yang diharapkan.

IO0 dan IO39 memiliki resistor fisik di papan tulis. Jika tombol tidak ditekan, maka resistor menarik kontak ke tegangan tinggi. Jika tombol ditekan, maka arus yang

mengalir melalui kontak ke tanah sebagai gantinya, sehingga tegangan 0 akan dibaca dari kontak. IO13 , IO27 , IO32 dan IO33tidak memiliki resistor, karena kontak pada ESP32 memiliki resistor internal, yang kami konfigurasikan untuk mode pull-up.

Mengetahui hal ini, kita dapat mengonfigurasi enam tombol menggunakan GPIO API.

const gpio_num_t BUTTON_PIN_A = GPIO_NUM_32;
const gpio_num_t BUTTON_PIN_B = GPIO_NUM_33;
const gpio_num_t BUTTON_PIN_START = GPIO_NUM_39;
const gpio_num_t BUTTON_PIN_SELECT = GPIO_NUM_27;
const gpio_num_t BUTTON_PIN_VOLUME = GPIO_NUM_0;
const gpio_num_t BUTTON_PIN_MENU = GPIO_NUM_13;

gpio_config_t gpioConfig = {};

gpioConfig.mode = GPIO_MODE_INPUT;
gpioConfig.pull_up_en = GPIO_PULLUP_ENABLE;
gpioConfig.pin_bit_mask =
	  (1ULL << BUTTON_PIN_A)
	| (1ULL << BUTTON_PIN_B)
	| (1ULL << BUTTON_PIN_START)
	| (1ULL << BUTTON_PIN_SELECT)
	| (1ULL << BUTTON_PIN_VOLUME)
	| (1ULL << BUTTON_PIN_MENU);

ESP_ERROR_CHECK(gpio_config(&gpioConfig));

Konstanta yang ditentukan pada awal kode sesuai dengan masing-masing kontak sirkuit. Kami menggunakan struktur gpio_config_t untuk mengkonfigurasi masing-masing dari enam tombol sebagai input pull-up. Dalam kasus IO13 , IO27 , IO32 dan IO33, kita perlu meminta IDF untuk mengaktifkan resistor pull-up dari kontak ini. Untuk IO0 dan IO39 kita tidak perlu melakukan ini karena mereka memiliki resistor fisik, tetapi kita tetap akan melakukannya untuk membuat konfigurasi yang indah.

ESP_ERROR_CHECK adalah makro pembantu dari IDF yang secara otomatis memeriksa hasil semua fungsi yang mengembalikan esp_err_t(sebagian besar IDF) dan menyatakan bahwa hasilnya tidak sama dengan ESP_OK . Makro ini nyaman digunakan untuk fungsi jika kesalahannya kritis dan setelah itu tidak masuk akal untuk melanjutkan eksekusi. Dalam game ini, game tanpa input bukanlah game, jadi pernyataan ini benar. Kami akan sering menggunakan makro ini.

Tombol membaca


Jadi, kami mengkonfigurasi semua kontak, dan akhirnya dapat membaca nilainya.

Tombol angka dibaca oleh fungsi gpio_get_level , tetapi kita perlu membalikkan nilai yang diterima, karena kontak ditarik ke atas, yaitu, sinyal tinggi sebenarnya berarti "tidak ditekan", dan yang rendah berarti "ditekan". Pembalikan mempertahankan logika yang biasa: 1 berarti "ditekan", 0 - "tidak ditekan".

int a = !gpio_get_level(BUTTON_PIN_A);
int b = !gpio_get_level(BUTTON_PIN_B);
int select = !gpio_get_level(BUTTON_PIN_SELECT);
int start = !gpio_get_level(BUTTON_PIN_START);
int menu = !gpio_get_level(BUTTON_PIN_MENU);
int volume = !gpio_get_level(BUTTON_PIN_VOLUME);

Crosspiece (D-pad)



ADC


Menghubungkan salib berbeda dari menghubungkan tombol. Tombol atas dan ke bawah terhubung ke satu pin dari konverter analog-ke-digital (Analog-to-Digital Converter, ADC) , dan tombol kiri dan kanan terhubung ke pin ADC lainnya.

Tidak seperti kontak digital GPIO, yang darinya kita dapat membaca salah satu dari dua keadaan (tinggi atau rendah), ADC mengubah tegangan analog kontinu (mis., Dari 0 V ke 3,3 V) menjadi nilai numerik diskrit (mis., Dari 0 hingga 4095) )

Saya kira desainer Odroid Go melakukannya untuk menghemat pin GPIO (Anda hanya perlu dua pin analog alih-alih empat pin digital). Bagaimanapun, ini sedikit mempersulit konfigurasi dan membaca dari kontak ini.

Konfigurasi


Kontak IO35 terhubung ke sumbu Y dari laba - laba , dan kontak IO34 terhubung ke sumbu X dari laba - laba . Kita melihat bahwa sambungan salib sedikit lebih rumit daripada tombol angka. Setiap sumbu memiliki dua sakelar ( SW1 dan SW2 untuk sumbu Y, SW3 dan SW4 untuk sumbu X), yang masing-masing terhubung ke satu set resistor ( R2 , R3 , R4 , R5 ).

Jika tidak "atas" atau "bawah" ditekan, pin IO35 ditarik ke tanah melalui R3 , dan kami menganggap nilai 0 V. Jika tidak "kiri" atau "kanan" ditekan, hubungi IO34menarik ke tanah melalui R5 , dan kami menghitung nilainya menjadi 0 V.

Jika SW1 ditekan ("atas") , maka dengan IO35 kita menghitung 3.3 V. Jika SW2 ditekan ("bawah") , maka dengan IO35 kita menghitung sekitar 1, 65 V, karena setengah tegangan akan jatuh pada resistor R2 .

Jika SW3 ditekan ("kiri") , maka dengan IO34 kita menghitung 3,3 V. Jika SW4 ditekan ("kanan") , maka dengan IO34 kita juga menghitung sekitar 1,65 V, karena setengah tegangan akan jatuh pada resistor R4 .

Kedua kasus adalah contoh pembagi tegangan.. Ketika dua resistor pada pembagi tegangan memiliki resistansi yang sama (dalam kasus kami - 100K), maka penurunan tegangan akan menjadi setengah dari tegangan input.

Mengetahui hal ini, kita dapat mengonfigurasi crosspiece:

const adc1_channel_t DPAD_PIN_X_AXIS = ADC1_GPIO34_CHANNEL;
const adc1_channel_t DPAD_PIN_Y_AXIS = ADC1_GPIO35_CHANNEL;

ESP_ERROR_CHECK(adc1_config_width(ADC_WIDTH_BIT_12));
ESP_ERROR_CHECK(adc1_config_channel_atten(DPAD_PIN_X_AXIS,ADC_ATTEN_DB_11));
ESP_ERROR_CHECK(adc1_config_channel_atten(DPAD_PIN_Y_AXIS,ADC_ATTEN_DB_11));

Kami mengatur ADC ke lebar 12 bit sehingga 0 V dibaca sebagai 0, dan 3,3 V sebagai 4095 (2 ^ 12). Atenuasi melaporkan bahwa kita tidak perlu melemahkan sinyal sehingga kita mendapatkan rentang tegangan penuh dari 0 V hingga 3,3 V.

Pada 12 bit, kita dapat berharap bahwa jika tidak ada yang ditekan, maka 0 akan dibaca, ketika ditekan ke atas dan ke kiri - 4096, dan sekitar 2048 akan dibaca ketika ditekan ke bawah dan ke kanan (karena resistor mengurangi tegangan hingga setengahnya).

Membaca silang


Membaca salib lebih sulit daripada tombol, karena kita perlu membaca nilai mentah (dari 0 hingga 4095) dan menafsirkannya.

const uint32_t ADC_POSITIVE_LEVEL = 3072;
const uint32_t ADC_NEGATIVE_LEVEL = 1024;

uint32_t dpadX = adc1_get_raw(DPAD_PIN_X_AXIS);

if (dpadX > ADC_POSITIVE_LEVEL)
{
	// Left pressed
}
else if (dpadX > ADC_NEGATIVE_LEVEL)
{
	// Right pressed
}


uint32_t dpadY = adc1_get_raw(DPAD_PIN_Y_AXIS);

if (dpadY > ADC_POSITIVE_LEVEL)
{
	// Up pressed
}
else if (dpadY > ADC_NEGATIVE_LEVEL)
{
	// Down pressed
}

ADC_POSITIVE_LEVEL dan ADC_NEGATIVE_LEVEL adalah nilai dengan margin, memastikan bahwa kami selalu membaca nilai yang benar.

Pemilihan


Ada dua opsi untuk mendapatkan nilai tombol: polling atau interupsi. Kita dapat membuat fungsi pemrosesan input dan meminta IDF untuk memanggil fungsi-fungsi ini ketika tombol ditekan, atau secara manual menyurvei keadaan tombol ketika kita membutuhkannya. Perilaku yang didorong oleh interupsi membuat segalanya menjadi lebih rumit dan sulit untuk dipahami. Selain itu, saya selalu berusaha untuk membuat semuanya sesederhana mungkin. Jika perlu, kita dapat menambahkan interupsi nanti.

Kami akan membuat struktur yang akan menyimpan keadaan enam tombol dan empat arah salib. Kita dapat membuat struktur dengan 10 boolean, atau 10 int, atau 10 int tanpa tanda tangan. Namun, sebagai gantinya, kami akan membuat struktur menggunakan bidang bit .

typedef struct
{
	uint16_t a : 1;
	uint16_t b : 1;
	uint16_t volume : 1;
	uint16_t menu : 1;
	uint16_t select : 1;
	uint16_t start : 1;
	uint16_t left : 1;
	uint16_t right : 1;
	uint16_t up : 1;
	uint16_t down : 1;
} Odroid_Input;

Saat memprogram untuk sistem desktop, bidang bit biasanya dihindari karena portingnya buruk ke mesin yang berbeda, tetapi kami memprogram untuk mesin tertentu dan kami tidak perlu khawatir tentang itu.

Alih-alih bidang, struktur dengan 10 nilai Boolean dengan ukuran total 10 byte dapat digunakan. Pilihan lain adalah satu uint16_t dengan bit shift dan bit masking macro yang dapat mengatur, menghapus , dan memeriksa bit individual. Itu akan berhasil, tetapi itu tidak akan sangat indah.

Bidang bit sederhana memungkinkan kita untuk mengambil keuntungan dari kedua pendekatan: dua byte data dan bidang bernama.

Demo


Sekarang kita dapat polling keadaan input di dalam loop utama dan menampilkan hasilnya.

void app_main(void)
{
	Odroid_InitializeInput();

	for (;;)
	{
		Odroid_Input input = Odroid_PollInput();

		printf(
			"\ra: %d  b: %d  start: %d  select: %d  vol: %d  menu: %d  up: %d  down: %d  left: %d  right: %d",
			input.a, input.b, input.start, input.select, input.volume, input.menu,
			input.up, input.down, input.left, input.right);

		fflush(stdout);

		vTaskDelay(250 / portTICK_PERIOD_MS);
	}

	// Should never get here
	esp_restart();
}

Fungsi printf menggunakan \ r untuk menimpa baris sebelumnya alih-alih menambahkan yang baru. fflush diperlukan untuk menampilkan garis, karena dalam keadaan normal itu diatur ulang oleh karakter baris baru \ n .


Referensi



Bagian 3: tampilan


pengantar


Kita harus dapat membuat piksel pada Odroid Go LCD.

Menampilkan warna pada layar akan lebih sulit daripada membaca status input karena LCD memiliki otak. Layar dikendalikan oleh ILI9341 - driver LCD TFT yang sangat populer pada satu chip.

Dengan kata lain, kita berbicara dengan ILI9341, yang merespons perintah kita dengan mengendalikan piksel pada LCD. Ketika saya mengatakan "layar" atau "tampilan" di bagian ini, sebenarnya saya maksudkan ILI9341. Kami berurusan dengan ILI9341. Ini mengontrol LCD.

SPI


LCD terhubung ke ESP32 melalui SPI (Serial Peripheral Interface) .

SPI adalah protokol standar yang digunakan untuk bertukar data antar perangkat pada papan sirkuit tercetak. Ini memiliki empat sinyal: MOSI (Master Out Slave In) , MISO (Master In Slave Out) , SCK (Clock) dan CS (Pilih Chip) .

Satu perangkat master di bus mengoordinasikan transfer data dengan mengendalikan SCK dan CS. Mungkin ada beberapa perangkat di satu bus, masing-masing akan memiliki sinyal CS sendiri. Ketika sinyal CS pada perangkat ini diaktifkan, ia dapat mengirim dan menerima data.

ESP32 akan menjadi master SPI (master), dan LCD akan menjadi budak SPI. Kita perlu mengkonfigurasi bus SPI dengan parameter yang diperlukan dan menambahkan layar LCD ke bus dengan mengkonfigurasi kontak yang sesuai.



Nama-nama VSPI.XXXX hanya label untuk kontak dalam diagram, tetapi kita dapat melalui kontak sendiri dengan melihat bagian-bagian dari diagram LCD dan ESP32.

  • MOSI -> VSPI.MOSI -> IO23
  • MISO -> VSPI.MISO -> IO19
  • SCK -> VSPI.SCK -> IO18
  • CS0 -> VSPI.CS0 -> IO5

Kami juga memiliki IO14 , yang merupakan pin GPIO yang digunakan untuk menyalakan lampu latar, dan juga IO21 , yang terhubung ke pin DC pada LCD. Kontak ini mengontrol jenis informasi yang kami kirimkan ke layar.

Pertama, konfigurasikan bus SPI.

const gpio_num_t LCD_PIN_MISO = GPIO_NUM_19;
const gpio_num_t LCD_PIN_MOSI = GPIO_NUM_23;
const gpio_num_t LCD_PIN_SCLK = GPIO_NUM_18;
const gpio_num_t LCD_PIN_CS = GPIO_NUM_5;
const gpio_num_t LCD_PIN_DC = GPIO_NUM_21;
const gpio_num_t LCD_PIN_BACKLIGHT = GPIO_NUM_14;
const int LCD_WIDTH = 320;
const int LCD_HEIGHT = 240;
const int LCD_DEPTH = 2;


spi_bus_config_t spiBusConfig = {};
spiBusConfig.miso_io_num = LCD_PIN_MISO;
spiBusConfig.mosi_io_num = LCD_PIN_MOSI;
spiBusConfig.sclk_io_num = LCD_PIN_SCLK;
spiBusConfig.quadwp_io_num = -1; // Unused
spiBusConfig.quadhd_io_num = -1; // Unused
spiBusConfig.max_transfer_sz = LCD_WIDTH * LCD_HEIGHT * LCD_DEPTH;

ESP_ERROR_CHECK(spi_bus_initialize(VSPI_HOST, &spiBusConfig, 1));

Kami mengkonfigurasi bus menggunakan spi_bus_config_t . Penting untuk mengomunikasikan kontak yang kami gunakan dan ukuran maksimum satu transfer data.

Untuk saat ini, kami akan melakukan satu transmisi SPI untuk semua data buffer bingkai, yang sama dengan lebar LCD (dalam piksel) kali tinggi (dalam piksel) dikalikan jumlah byte per piksel.

Lebar adalah 320, tingginya 240, dan kedalaman warna 2 byte (tampilan mengharapkan warna pixel menjadi 16 bit).

spi_handle_t gSpiHandle;

spi_device_interface_config_t spiDeviceConfig = {};
spiDeviceConfig.clock_speed_hz = SPI_MASTER_FREQ_40M;
spiDeviceConfig.spics_io_num = LCD_PIN_CS;
spiDeviceConfig.queue_size = 1;
spiDeviceConfig.flags = SPI_DEVICE_NO_DUMMY;

ESP_ERROR_CHECK(spi_bus_add_device(VSPI_HOST, &spiDeviceConfig, &gSpiHandle));

Setelah menginisialisasi bus, kita perlu menambahkan perangkat LCD ke bus sehingga kita dapat mulai berbicara dengannya.

  • clock_speed_hz — - , SPI 40 , . 80 , .
  • spics_io_num — CS, IDF CS, ( SD- SPI).
  • queue_size — 1, ( ).
  • flags - driver IDF SPI biasanya memasukkan bit kosong dalam transmisi untuk menghindari masalah waktu selama membaca dari perangkat SPI, tetapi kami melakukan transmisi satu arah (kami tidak akan membaca dari tampilan). SPI_DEVICE_NO_DUMMY melaporkan bahwa kami mengonfirmasi transmisi satu arah ini dan kami tidak perlu memasukkan bit kosong.


gpio_set_direction(LCD_PIN_DC, GPIO_MODE_OUTPUT);
gpio_set_direction(LCD_PIN_BACKLIGHT, GPIO_MODE_OUTPUT);

Kita juga perlu mengatur pin DC dan backlight sebagai pin GPIO. Setelah beralih DC, lampu latar akan terus menyala.

Tim


Komunikasi dengan LCD adalah dalam bentuk perintah. Pertama, kita melewatkan byte yang menunjukkan perintah yang ingin kita kirim, dan kemudian kita meneruskan parameter perintah (jika ada). Layar mengerti bahwa byte adalah perintah jika sinyal DC rendah. Jika sinyal DC tinggi, maka data yang diterima akan dianggap sebagai parameter dari perintah yang dikirimkan sebelumnya.

Secara umum, alirannya terlihat seperti ini:

  1. Kami memberikan sinyal rendah ke DC
  2. Kami mengirim satu byte dari perintah
  3. Kami memberikan sinyal tinggi ke DC
  4. Kirim nol atau lebih byte, tergantung pada persyaratan perintah
  5. Ulangi langkah 1-4

Di sini sahabat kami adalah spesifikasi ILI9341 . Ini mencantumkan semua perintah yang mungkin, parameternya dan cara menggunakannya.


Contoh perintah tanpa parameter adalah Tampilan ON . Byte perintah adalah 0x29 , tetapi tidak ada parameter yang ditentukan untuk itu.


Contoh perintah dengan parameter adalah Set Alamat Kolom . Byte perintah adalah 0x2A , tetapi empat parameter yang diperlukan ditentukan untuk itu. Untuk menggunakan perintah, Anda perlu mengirim sinyal rendah ke DC , mengirim 0x2A , mengirim sinyal tinggi ke DC , dan kemudian mentransfer byte dari empat parameter.

Kode perintah itu sendiri ditentukan dalam enumerasi.

typedef enum
{
	SOFTWARE_RESET = 0x01u,
	SLEEP_OUT = 0x11u,
	DISPLAY_ON = 0x29u,
	COLUMN_ADDRESS_SET = 0x2Au,
	PAGE_ADDRESS_SET = 0x2Bu,
	MEMORY_WRITE = 0x2Cu,
	MEMORY_ACCESS_CONTROL = 0x36u,
	PIXEL_FORMAT_SET = 0x3Au,
} CommandCode;

Sebagai gantinya, kami dapat menggunakan makro ( #define SOFTWARE_RESET (0x01u) ), tetapi mereka tidak memiliki simbol di debugger dan mereka tidak memiliki cakupan. Mungkin juga untuk menggunakan konstanta statis bilangan bulat, seperti yang kami lakukan dengan kontak GPIO, tetapi berkat enum, sekilas kami dapat memahami data apa yang diteruskan ke fungsi atau anggota struktur: mereka bertipe CommandCode . Kalau tidak, bisa jadi ini adalah uint8_t mentah yang tidak memberitahukan apa pun kepada programmer yang membaca kode.

Meluncurkan


Selama inisialisasi, kita dapat melewati perintah yang berbeda untuk dapat menggambar sesuatu. Setiap perintah memiliki byte perintah, yang akan kita sebut Kode Perintah .

Kami akan menentukan struktur untuk menyimpan perintah peluncuran sehingga Anda dapat menentukan larik mereka.

typedef struct
{
	CommandCode code;
	uint8_t parameters[15];
	uint8_t length;
} StartupCommand;

  • kode adalah kode perintah.
  • parameter adalah larik parameter perintah (jika ada). Ini adalah array statis ukuran 15, karena ini adalah jumlah maksimum parameter yang kita butuhkan. Karena sifat statis array, kami tidak perlu khawatir mengalokasikan array dinamis untuk setiap perintah setiap waktu.
  • panjang adalah jumlah parameter dalam array parameter .

Dengan menggunakan struktur ini, kita dapat menentukan daftar perintah peluncuran.

StartupCommand gStartupCommands[] =
{
	// Reset to defaults
	{
		SOFTWARE_RESET,
		{},
		0
	},

	// Landscape Mode
	// Top-Left Origin
	// BGR Panel
	{
		MEMORY_ACCESS_CONTROL,
		{0x20 | 0xC0 | 0x08},
		1
	},

	// 16 bits per pixel
	{
		PIXEL_FORMAT_SET,
		{0x55},
		1
	},

	// Exit sleep mode
	{
		SLEEP_OUT,
		{},
		0
	},

	// Turn on the display
	{
		DISPLAY_ON,
		{},
		0
	},
};

Perintah tanpa parameter, misalnya, SOFTWARE_RESET , atur daftar parameter initializer menjadi kosong (yaitu, dengan satu nol) dan panjangnya diatur ke 0. Perintah dengan parameter mengisi parameter dan tentukan panjang. Akan lebih bagus jika kita dapat menetapkan panjang secara otomatis dan tidak menulis angka (jika kita membuat kesalahan atau parameternya berubah), tapi saya tidak berpikir itu sepadan dengan masalahnya.

Tujuan sebagian besar tim jelas dari namanya, dengan pengecualian dua.

MEMORY_ACCESS_CONTROL

  • Mode Lansekap: Secara default, tampilan menggunakan orientasi potret (240x320), tetapi kami ingin menggunakan lansekap (320x240).
  • Top-Left Origin: (0,0) , ( ) .
  • BGR Panel: , BGR. , , , , .

PIXEL_FORMAT_SET

  • 16 bits per pixel: 16- .

Ada banyak perintah lain yang dapat dikirim saat startup untuk mengontrol berbagai aspek, seperti gamma. Parameter yang diperlukan dijelaskan dalam spesifikasi LCD itu sendiri (dan bukan pengontrol ILI9341), yang tidak dapat diakses oleh kami. Jika kami tidak mengirimkan perintah ini, maka pengaturan tampilan default digunakan, yang sangat cocok untuk kami.

Setelah menyiapkan berbagai perintah peluncuran, kita dapat mulai mentransfernya ke layar.

Pertama, kita membutuhkan fungsi yang mengirim satu byte perintah ke layar. Jangan lupa bahwa perintah pengiriman berbeda dengan mengirim parameter, karena kita perlu mengirim sinyal rendah ke DC .

#define BYTES_TO_BITS(value) ( (value) * 8 )

void SendCommandCode(CommandCode code)
{
	spi_transaction_t transaction = {};

	transaction.length = BYTES_TO_BITS(1);
	transaction.tx_data[0] = (uint8_t)code;
	transaction.flags = SPI_TRANS_USE_TXDATA;

	gpio_set_level(LCD_PIN_DC, 0);
	spi_device_transmit(gSpiHandle, &transaction);
}

IDF memiliki struktur spi_transaction , yang kami isi ketika kami ingin mentransfer sesuatu melalui bus SPI. Kita tahu berapa banyak bit payload-nya dan mentransfer beban itu sendiri.

Kita dapat meneruskan pointer ke payload, atau menggunakan struktur tx_data struct internal , yang hanya berukuran empat byte, tetapi menyimpan driver dari keharusan mengakses memori eksternal. Jika kita menggunakan tx_data , kita harus mengatur flag SPI_TRANS_USE_TXDATA .

Sebelum mengirimkan data, kami mengirim sinyal rendah ke DC , menunjukkan bahwa ini adalah kode perintah.

void SendCommandParameters(uint8_t* data, int length)
{
	spi_transaction_t transaction = {};

	transaction.length = BYTES_TO_BITS(length);
	transaction.tx_buffer = data;
	transaction.flags = 0;

	gpio_set_level(LCD_PIN_DC, 1);
	spi_device_transmit(SPIHANDLE, &transaction);
}

Melewati parameter mirip dengan mengirim perintah, hanya kali ini kami menggunakan buffer ( data ) kami sendiri dan mengirimkan sinyal tinggi ke DC untuk memberi tahu tampilan bahwa parameter sedang dikirim. Selain itu, kami tidak menetapkan flag SPI_TRANS_USE_TXDATA karena kami melewati buffer kami sendiri.

Kemudian Anda dapat mengirim semua perintah peluncuran.

#define ARRAY_COUNT(value) ( sizeof(value) / sizeof(value[0]) )

int commandCount = ARRAY_COUNT(gStartupCommands);

for (int commandIndex = 0; commandIndex < commandCount; ++commandIndex)
{
	StartupCommand* command = &gStartupCommands[commandIndex];

	SendCommandCode(command->code);

	if (command->length > 0)
	{
		SendCommandData(command->parameters, command->length);
	}
}

Kami berulang-ulang melintasi larik perintah peluncuran, melewati kode perintah terlebih dahulu, dan kemudian parameter (jika ada).

Gambar bingkai


Setelah menginisialisasi tampilan, Anda dapat mulai menggambar di atasnya.

#define UPPER_BYTE_16(value) ( (value) >> 8u )
#define LOWER_BYTE_16(value) ( (value) & 0xFFu )

void Odroid_DrawFrame(uint8_t* buffer)
{
	// Set drawing window width to (0, LCD_WIDTH)
    uint8_t drawWidth[] = { 0, 0, UPPER_BYTE_16(LCD_WIDTH), LOWER_BYTE_16(LCD_WIDTH) };
	SendCommandCode(COLUMN_ADDRESS_SET);
	SendCommandParameters(drawWidth, ARRAY_COUNT(drawWidth));

	// Set drawing window height to (0, LCD_HEIGHT)
    uint8_t drawHeight[] = { 0, 0, UPPER_BYTE_16(LCD_HEIGHT), LOWER_BYTE_16(LCD_HEIGHT) };
	SendCommandCode(PAGE_ADDRESS_SET);
	SendCommandParameters(drawHeight, ARRAY_COUNT(drawHeight));

	// Send the buffer to the display
	SendCommandCode(MEMORY_WRITE);
	SendCommandParameters(buffer, LCD_WIDTH * LCD_HEIGHT * LCD_DEPTH);
}

ILI9341 memiliki kemampuan untuk menggambar ulang setiap bagian layar. Ini mungkin berguna di masa depan jika kita melihat penurunan dalam frame rate. Dalam hal ini, akan mungkin untuk memperbarui hanya bagian yang diubah dari layar, tetapi untuk saat ini kami hanya akan menggambar ulang seluruh layar lagi.

Untuk membuat bingkai, itu membutuhkan pengaturan jendela render. Untuk melakukan ini, kirim perintah COLUMN_ADDRESS_SET dengan lebar jendela dan perintah PAGE_ADDRESS_SET dengan tinggi jendela. Setiap perintah membutuhkan empat byte parameter yang menggambarkan jendela tempat kita akan melakukan rendering.

UPPER_BYTE_16 dan LOWER_BYTE_16- Ini adalah makro bantu untuk mengekstraksi byte tinggi dan rendah dari nilai 16-bit. Parameter dari perintah ini mengharuskan kita untuk membagi nilai 16-bit menjadi dua nilai 8-bit, itulah sebabnya kami melakukan ini.

Rendering dimulai oleh perintah MEMORY_WRITE dan mengirim ke layar semua 153.600 byte frame buffer sekaligus.

Ada cara lain untuk mentransfer frame buffer ke tampilan:

  • Kami dapat membuat tugas FreeRTOS lain (tugas), yang bertanggung jawab untuk mengoordinasikan transaksi SPI.
  • Anda dapat mentransfer bingkai bukan dalam satu, tetapi dalam beberapa transaksi.
  • Anda dapat menggunakan transmisi non-pemblokiran, di mana kami memulai pengiriman, dan kemudian melanjutkan untuk melakukan operasi lain.
  • Anda dapat menggunakan kombinasi metode di atas.

Untuk saat ini, kami akan menggunakan cara paling sederhana: satu-satunya transaksi pemblokiran. Ketika DrawFrame dipanggil, transfer ke layar dimulai dan tugas kami dijeda hingga transfer selesai. Jika nanti kita mengetahui bahwa kita tidak dapat mencapai frame rate yang baik dengan metode ini, maka kita akan kembali ke masalah ini.

RGB565 dan urutan byte


Tampilan khas (misalnya, monitor komputer Anda) memiliki kedalaman 24 bit (1,6 juta warna): 8 bit per merah, hijau, dan biru. Pixel ditulis ke memori sebagai RRRRRRRRGGGGGGGGGBGBBBBBBBBB .

LCD Odroid memiliki kedalaman 16 bit (65 ribu warna): 5 bit merah, 6 bit hijau dan 5 bit biru. Pixel ditulis ke memori sebagai RRRRRGGGGGGGBBBBB . Format ini disebut RGB565 .

#define SWAP_ENDIAN_16(value) ( (((value) & 0xFFu) << 8u) | ((value) >> 8u)  )
#define RGB565(red, green, blue) ( SWAP_ENDIAN_16( ((red) << 11u) | ((green) << 5u) | (blue) ) )

Tentukan makro yang menciptakan warna dalam format RGB565. Kami akan memberinya byte merah, byte hijau dan byte biru. Dia akan mengambil lima bit paling signifikan dari merah, enam bit paling signifikan dari hijau dan lima bit paling signifikan dari biru. Kami memilih bit tinggi karena mengandung lebih banyak informasi daripada bit rendah.

Namun, ESP32 menyimpan data dalam urutan Little Endian , yaitu byte paling signifikan disimpan di alamat memori yang lebih rendah.

Misalnya, nilai 32-bit [0xDE 0xAD 0xBE 0xEF] akan disimpan dalam memori sebagai [0xEF 0xBE 0xAD 0xDE] . Saat mentransfer data ke layar, ini menjadi masalah karena byte paling signifikan akan dikirim terlebih dahulu, dan LCD mengharapkan untuk menerima byte paling signifikan terlebih dahulu. Setel SWAP_ENDIAN_16

makrountuk menukar byte dan menggunakannya dalam makro RGB565 .

Berikut adalah bagaimana masing-masing dari tiga warna utama dijelaskan dalam RGB565 dan bagaimana mereka disimpan dalam memori ESP32 jika Anda tidak mengubah urutan byte.

Merah

11111 | 000000 | 00000? -> 11111000 00000000 -> 00000000 11111000

Hijau

00000 | 111111 | 00000? -> 00000111 11100000 -> 11100000 00000111

Biru

00000 | 000000 | 11111? -> 00000000 00011111 -> 0001111000000

Demo


Kami dapat membuat demo sederhana untuk menonton LCD beraksi. Pada awal bingkai, flush buffer frame menjadi hitam dan menggambar persegi 50x50. Kita dapat memindahkan kotak dengan tanda silang dan mengubah warnanya dengan tombol A , B dan Mulai .

void app_main(void)
{
	Odroid_InitializeInput();
	Odroid_InitializeDisplay();

	ESP_LOGI(LOG_TAG, "Odroid initialization complete - entering main loop");

	uint16_t* framebuffer = (uint16_t*)heap_caps_malloc(320 * 240 * 2, MALLOC_CAP_DMA);
	assert(framebuffer);

	int x = 0;
	int y = 0;

	uint16_t color = 0xffff;

	for (;;)
	{
		memset(framebuffer, 0, 320 * 240 * 2);

		Odroid_Input input = Odroid_PollInput();

		if (input.left) { x -= 10; }
		else if (input.right) { x += 10; }

		if (input.up) { y -= 10; }
		else if (input.down) { y += 10; }

		if (input.a) { color = RGB565(0xff, 0, 0); }
		else if (input.b) { color = RGB565(0, 0xff, 0); }
		else if (input.start) { color = RGB565(0, 0, 0xff); }

		for (int row = y; row < y + 50; ++row)
		{
			for (int col = x; col < x + 50; ++col)
			{
				framebuffer[320 * row + col] = color;
			}
		}

		Odroid_DrawFrame(framebuffer);
	}

	// Should never get here
	esp_restart();
}

Kami mengalokasikan buffer bingkai sesuai dengan ukuran penuh tampilan: 320 x 240, dua byte per piksel (warna 16-bit). Kami menggunakan heap_caps_malloc sehingga dialokasikan dalam memori, yang dapat digunakan untuk transaksi SPI dengan Direct Memory Access (DMA) . DMA memungkinkan periferal SPI untuk mengakses frame buffer tanpa perlu keterlibatan CPU. Tanpa DMA, transaksi SPI membutuhkan waktu lebih lama.

Kami tidak melakukan pemeriksaan untuk memastikan bahwa rendering tidak terjadi di luar batas layar.


Robekan yang kuat terlihat. Dalam aplikasi desktop, cara standar untuk menghilangkan robekan adalah dengan menggunakan beberapa buffer. Misalnya, ketika buffering ganda, ada dua buffer: buffer depan dan belakang. Saat buffer depan ditampilkan, perekaman dilakukan
di belakang. Kemudian mereka mengubah tempat dan proses berulang.

ESP32 tidak memiliki RAM yang cukup dengan kemampuan DMA untuk menyimpan dua frame buffer (4 MB RAM SPI eksternal, sayangnya, tidak memiliki kemampuan DMA), jadi opsi ini tidak cocok.

ILI9341 memiliki sinyal ( TE ) yang memberi tahu Anda ketika VBLANK terjadi sehingga kami dapat menulis ke layar sampai gambar itu diambil. Tetapi dengan Odroid (atau modul tampilan) sinyal ini tidak terhubung, jadi kami tidak dapat mengaksesnya.

Mungkin kita dapat menemukan nilai yang layak, tetapi untuk saat ini kita tidak akan melakukannya, karena sekarang tugas kita hanyalah menampilkan piksel pada layar.

Sumber


Semua kode sumber dapat ditemukan di sini .

Referensi



All Articles