Pendahuluan
(Tautan ke kode sumber dan proyek KiCAD disediakan di akhir artikel.)Meskipun kami dilahirkan di era 8-bit, komputer pertama kami adalah Amiga 500. Ini adalah mesin 16-bit yang hebat dengan grafis dan suara yang luar biasa, menjadikannya hebat untuk bermain game. Platforming telah menjadi genre game yang sangat populer di komputer ini. Banyak dari mereka sangat berwarna-warni dan memiliki paralaks yang sangat halus. Ini dimungkinkan berkat programmer berbakat yang dengan cerdik menggunakan prosesor Amiga untuk meningkatkan jumlah warna layar. Lihatlah LionHeart misalnya!
Lionheart on Amiga. Gambar statis ini tidak menampilkan keindahan gambar.Sejak 90-an, elektronik telah banyak berubah, dan sekarang ada banyak mikrokontroler kecil yang memungkinkan Anda untuk membuat hal-hal luar biasa.
Kami selalu menyukai game platform, dan hari ini, hanya dengan beberapa dolar, Anda dapat membeli Raspberry Zero, menginstal Linux, dan “cukup mudah” menulis game platform yang penuh warna.
Tapi tugas ini bukan untuk kita - kita tidak ingin menembak burung pipit dari meriam!
Kami ingin menggunakan mikrokontroler dengan memori terbatas, dan bukan sistem yang kuat pada chip dengan GPU terintegrasi! Dengan kata lain, kami menginginkan kesulitan!
Ngomong-ngomong, tentang kemungkinan video: beberapa orang berhasil memeras semua jus dari mikrokontroler AVR dalam proyek mereka (misalnya, dalam proyek Uzebox atau Craft dari pengembang lft). Namun, untuk mencapai ini, mikrokontroler AVR memaksa kita untuk menulis dalam assembler, dan meskipun beberapa gim sangat bagus, Anda akan menghadapi keterbatasan serius yang tidak memungkinkan Anda membuat gim dalam gaya 16-bit.
Oleh karena itu, kami memutuskan untuk menggunakan mikrokontroler / papan yang lebih seimbang, yang memungkinkan kami untuk menulis kode sepenuhnya dalam C.
Dia tidak sekuat Arduino Due, tetapi tidak selemah Arduino Uno. Menariknya, "Jatuh Tempo" berarti "dua," dan "Uno" berarti "satu." Microsoft mengajarkan kami untuk menghitung dengan benar (1, 2, 3, 95, 98, ME, 2000, XP, Vista, 7, 8, 10), dan Arduino juga menggunakan cara ini! Kami akan menggunakan Arduino Zero, yang berada di tengah antara 1 dan 2!
Ya, menurut Arduino, 1 <0 <2.
Secara khusus, kami tidak tertarik pada papan itu sendiri, tetapi pada seri prosesornya. Arduino Zero memiliki mikrokontroler seri ATSAMD21 dengan Cortex M0 + (48 MHz), memori flash 256 KB dan RAM 32 KB.
Meskipun Cortex M0 + 48-MHz secara signifikan mengungguli MC68000 7-MHz yang lama dalam kinerja, Amiga 500 memiliki RAM 512 KB, sprite perangkat keras, papan permainan ganda terintegrasi, Blitter (mesin transfer blok berbasis gambar DMA dengan sistem pengenalan tabrakan pixel-akurasi bawaan) dan transparansi) dan Tembaga (coprocessor raster yang memungkinkan Anda untuk melakukan operasi dengan register berdasarkan posisi sapuan untuk menciptakan banyak efek yang sangat indah). SAMD21 tidak memiliki semua perangkat keras ini (dengan pengecualian yang agak sederhana dibandingkan dengan Blitter DMA), jadi banyak yang akan ditampilkan secara terprogram.
Kami ingin mencapai parameter berikut:
- Resolusi 160 x 128 piksel pada layar SPI 1,8 inci.
- Grafik dengan 16 bit per piksel;
- Frame rate tertinggi. Setidaknya 25 fps pada 12 MHz SPI, atau 40 fps pada 24 MHz;
- lapangan bermain ganda dengan scroll parallax;
- semuanya ditulis dalam C. Tidak ada kode assembler;
- Pixel-akurat pengakuan tabrakan;
- hamparan layar.
Tampaknya mencapai tujuan-tujuan ini cukup sulit. Ya, terutama jika kita menolak kode asm!
Misalnya, dengan warna 16-bit, ukuran layar 160 × 128 piksel akan membutuhkan 40 KB untuk buffer layar, tetapi kami hanya memiliki 32 KB RAM! Dan kita masih perlu menggulir paralaks pada bidang permainan ganda dan banyak lagi, dengan frekuensi setidaknya 25/40 fps!
Tapi tidak ada yang mustahil bagi kita, kan?
Kami menggunakan trik dan fungsi bawaan ATSAMD21! Sebagai "perangkat keras", kami menggunakan
uChip , yang dapat dibeli di
Toko Itaca .
uChip: jantung dari proyek kami!Ini memiliki karakteristik yang sama dengan Arduino Zero, tetapi jauh lebih sedikit, dan juga lebih murah daripada Arduino Zero asli (ya, Anda dapat membeli Arduino Zero palsu seharga $ 10 pada AliExpress ... tetapi kami ingin membangun yang asli). Ini akan memungkinkan kami untuk membuat konsol portabel kecil. Anda dapat mengadaptasi proyek ini untuk Arduino Zero dengan mudah, hanya hasilnya akan cukup rumit.
Kami juga membuat papan uji kecil yang mengimplementasikan konsol portabel untuk orang miskin. Detail di bawah!
Kami tidak akan menggunakan kerangka kerja Arduino. Itu tidak cocok ketika datang untuk mengoptimalkan dan mengelola peralatan. (Dan jangan bicara tentang IDE!)
Pada artikel ini, kami akan menjelaskan bagaimana kami sampai pada versi final game, menjelaskan semua optimasi dan kriteria yang digunakan. Gim itu sendiri belum lengkap, tidak memiliki suara, level, dll. Namun, ini dapat digunakan sebagai titik awal untuk berbagai jenis permainan!
Selain itu, ada banyak lagi opsi optimasi, bahkan tanpa assembler!
Jadi, mari kita mulai perjalanan kita!
Kesulitan
Sebenarnya, proyek ini memiliki dua aspek kompleks: pengaturan waktu dan memori (RAM dan penyimpanan).
Memori
Mari kita mulai dengan memori. Pertama, alih-alih menyimpan gambar tingkat besar, kami menggunakan ubin. Bahkan, jika Anda dengan hati-hati menganalisis sebagian besar platformer, Anda akan melihat bahwa mereka dibuat dari sejumlah kecil elemen grafis (ubin) yang diulang berkali-kali.
Turrican 2 di Amiga. Salah satu game platform terbaik sepanjang masa. Anda dapat dengan mudah melihat ubin di dalamnya!Dunia / tingkat tampaknya beragam berkat berbagai kombinasi ubin. Ini menghemat banyak memori pada drive, tetapi tidak memecahkan masalah buffer bingkai besar.
Trik kedua yang kami gunakan adalah mungkin karena kekuatan komputasi UC yang agak besar dan keberadaan DMA! Alih-alih menyimpan semua data frame dalam RAM (dan mengapa ini diperlukan?) Kami akan membuat adegan di setiap frame dari awal. Secara khusus, kami akan terus menggunakan buffer, tetapi sedemikian rupa sehingga sesuai dalam satu blok horizontal grafik data dengan tinggi 16 piksel.
Pengaturan waktu - CPU
Ketika seorang insinyur perlu membuat sesuatu, dia pertama-tama memeriksa untuk melihat apakah ini mungkin. Tentu saja, pada awalnya kami melakukan tes ini!
Jadi, kita membutuhkan setidaknya 25 fps pada layar 160 × 128 piksel. Itu adalah 512.000 piksel / s. Karena mikrokontroler beroperasi pada frekuensi 48 MHz, kami memiliki setidaknya 93 siklus clock per piksel. Nilai ini turun menjadi 58 siklus jika kita bertujuan 40 fps.
Faktanya, mikrokontroler kami mampu memproses hingga 2 piksel dalam satu waktu, karena setiap piksel membutuhkan 16 bit, dan ATSAMD21 memiliki bus internal 32-bit, yaitu, kinerjanya akan lebih baik!
Nilai 93 clock cycle memberi tahu kita bahwa tugas itu sepenuhnya bisa dilakukan! Bahkan, kita dapat menyimpulkan bahwa CPU sendiri dapat menangani semua tugas rendering tanpa DMA. Kemungkinan besar, ini benar, terutama ketika bekerja dengan assembler. Namun, kodenya akan sangat sulit ditangani. Dan di C itu harus sangat dioptimalkan! Sebenarnya, Cortex M0 + tidak ramah-C seperti Cortex M3, dan tidak memiliki banyak instruksi (bahkan tidak memuat / menghemat dengan kenaikan / pra-kenaikan / pengurangan berikutnya!), Yang harus dilaksanakan dengan dua atau lebih instruksi sederhana.
Mari kita lihat apa yang perlu kita lakukan untuk menggambar dua bidang bermain (dengan asumsi kita sudah tahu koordinat x dan y, dll.).
- Hitung lokasi piksel latar depan dalam memori flash.
- Dapatkan nilai piksel.
- Jika transparan, hitung posisi piksel latar belakang dalam blitz.
- Dapatkan nilai piksel.
- Hitung lokasi target.
- Simpan piksel ke buffer.
Selain itu, untuk setiap sprite yang dapat masuk ke buffer, operasi berikut harus dilakukan:
- Hitung posisi piksel sprite dalam memori flash.
- Mendapatkan nilai piksel.
- Jika tidak transparan, maka hitung lokasi buffer tujuan.
- Menyimpan piksel dalam buffer.
Semua operasi ini tidak hanya tidak diimplementasikan sebagai instruksi ASM tunggal, tetapi setiap instruksi ASM membutuhkan dua siklus ketika mengakses RAM / memori flash.
Selain itu, kami masih belum memiliki logika permainan (yang, untungnya, membutuhkan sedikit waktu, karena dihitung sekali per frame), pengenalan tabrakan, pemrosesan buffer, dan instruksi yang diperlukan untuk mengirim data melalui SPI.
Sebagai contoh, ini adalah pseudo-code dari apa yang harus kita lakukan (untuk saat ini, kita asumsikan bahwa permainan tidak memiliki scrolling, dan lapangan bermain memiliki latar belakang warna yang konstan!) Hanya untuk latar depan.
Biarkan cameraY dan cameraX menjadi koordinat sudut kiri atas layar di dunia game.
Biarkan xTilepos dan yTilepos menjadi posisi petak saat ini di peta.
xTilepos = cameraX / 16;
Jumlah instruksi untuk 2560 piksel (160 x 16) adalah sekitar 16rb, mis. 6 per piksel. Bahkan, Anda bisa menggambar dua piksel sekaligus. Ini membagi dua jumlah instruksi aktual per pixel, yaitu, jumlah instruksi level tinggi per pixel adalah sekitar 3. Namun, beberapa instruksi level tinggi ini akan dibagi menjadi dua atau lebih instruksi assembler, atau memerlukan setidaknya dua siklus untuk menyelesaikan karena mereka mengakses ke memori. Juga, kami tidak mempertimbangkan mengatur ulang pipa CPU karena melompat dan menunggu status untuk memori flash. Ya, kami masih jauh dari siklus 58-93 yang kami miliki, tetapi kami masih perlu mempertimbangkan latar belakang lapangan bermain dan sprite.
Meskipun kami melihat bahwa masalah dapat diselesaikan pada satu CPU, DMA akan jauh lebih cepat. Akses memori langsung menyisakan lebih banyak ruang untuk sprite layar atau efek grafis yang lebih baik (misalnya, kita dapat menerapkan alpha blending).
Kami akan melihat bahwa untuk mengonfigurasi DMA untuk setiap ubin, kami membutuhkan kurang dari 100 instruksi C, mis. Kurang dari 0,5 per piksel! Tentu saja, DMA masih harus melakukan jumlah transfer yang sama dalam memori, tetapi peningkatan alamat dan transmisi dilakukan tanpa campur tangan CPU, yang dapat melakukan hal lain (misalnya, menghitung dan merender sprite).
Menggunakan timer SysTick, kami menemukan bahwa waktu yang diperlukan untuk menyiapkan DMA untuk seluruh blok, dan kemudian untuk menyelesaikan DMA, adalah sekitar 12k clock cycle. Catatan: siklus jam! Bukan instruksi level tinggi! Jumlah siklus cukup tinggi untuk hanya 2560 piksel, mis. 1.280 kata 32-bit. Bahkan, kami mendapatkan sekitar 10 siklus per kata 32-bit. Namun, Anda perlu mempertimbangkan waktu yang diperlukan untuk menyiapkan DMA, serta waktu yang diperlukan DMA untuk memuat deskriptor transfer dari RAM (yang pada dasarnya berisi pointer dan jumlah byte yang ditransfer). Selain itu, selalu ada semacam perubahan bus memori (sehingga CPU tidak dapat diam tanpa data), dan memori flash memerlukan setidaknya satu keadaan tunggu.
Pengaturan waktu - SPI
Kemacetan lain adalah SPI. Apakah 12 MHz cukup untuk 25 fps? Jawabannya adalah ya: 12 MHz sesuai dengan sekitar 36 frame per detik. Jika kita menggunakan 24 MHz, maka batasnya akan berlipat ganda!
Omong-omong, spesifikasi layar dan mikrokontroler mengatakan bahwa kecepatan SPI maksimum masing-masing adalah 15 dan 12 MHz. Kami menguji dan memastikan bahwa itu dapat ditingkatkan ke 24 MHz tanpa masalah, setidaknya dalam "arah" yang kita butuhkan (mikrokontroler menulis ke layar).
Kami akan menggunakan tampilan SPI 1,8 inci yang populer. Kami memastikan bahwa ILI9163 dan ST7735 beroperasi secara normal dengan frekuensi 12 MHz (setidaknya dengan 12 MHz. Telah diverifikasi bahwa ST7735 beroperasi dengan frekuensi hingga 24 MHz). Jika Anda ingin menggunakan tampilan yang sama seperti dalam tutorial "Cara memutar video di Arduino Uno", kami sarankan Anda memodifikasinya jika Anda ingin menambahkan dukungan SD di masa depan. Kami menggunakan versi kartu SD sehingga kami memiliki banyak ruang untuk elemen lain, seperti suara atau level tambahan.
Grafik
Seperti yang telah disebutkan, permainan menggunakan ubin. Setiap level terdiri dari ubin yang diulang sesuai tabel, yang kami sebut "gameMap". Seberapa besar masing-masing ubin? Ukuran setiap ubin sangat mempengaruhi konsumsi memori, detail, dan fleksibilitas (dan, seperti yang akan kita lihat nanti, kecepatan juga). Ubin yang terlalu besar akan membutuhkan pembuatan ubin baru untuk setiap variasi kecil yang kita butuhkan. Ini akan memakan banyak ruang di drive.
Dua ubin berukuran 32 × 32 piksel (kiri dan tengah), yang berbeda dalam sebagian kecil (bagian kanan atas piksel adalah 16 × 16). Karena itu, kita perlu menyimpan dua ubin berbeda dengan ukuran 32 × 32 piksel. Jika kita menggunakan ubin 16 × 16 piksel (di sebelah kanan), maka kita hanya perlu menyimpan dua ubin 16 × 16 (ubin yang sepenuhnya putih dan ubin di sebelah kanan). Namun, saat menggunakan 16 × 16 ubin, kami mendapatkan 4 elemen peta.Namun, ubin lebih sedikit per layar diperlukan, yang meningkatkan kecepatan (lihat di bawah) dan mengurangi ukuran peta (yaitu jumlah baris dan kolom dalam tabel) dari setiap level. Ubin terlalu kecil menciptakan masalah sebaliknya. Tabel peta semakin besar dan kecepatan melambat. Tentu saja, kita tidak akan membuat keputusan bodoh. misalnya, pilih ubin dengan ukuran 17 × 31 piksel. Teman setia kami - derajat dua! Ukuran 16 × 16 hampir menjadi "aturan emas", digunakan dalam banyak game, dan kami akan memilihnya!
Layar kami memiliki ukuran 160 × 128. Dengan kata lain, kita membutuhkan 10 × 8 ubin per layar, mis. 80 entri dalam tabel. Untuk level besar 10 × 10 layar (atau 100 × 1 layar), hanya 8.000 catatan akan diperlukan (16 KB jika kita menggunakan 16 bit untuk merekam. Kemudian kita akan menunjukkan mengapa kita memutuskan untuk memilih 16 bit untuk merekam).
Bandingkan ini dengan jumlah memori yang kemungkinan akan ditempati oleh gambar besar di seluruh layar: 40 KB * 100 = 4 MB! Ini gila!
Mari kita bicara tentang sistem rendering.
Setiap frame harus mengandung (dalam urutan gambar):
- grafik latar belakang (bidang bermain kembali)
- grafik level itu sendiri (foreground).
- sprite
- teks / hamparan teratas.
Secara khusus, kami akan melakukan operasi berikut secara berurutan:
- Menggambar latar belakang + latar depan (ubin)
- menggambar ubin tembus + sprite + overlay atas
- mengirim data dengan SPI.
Latar belakang dan ubin yang sepenuhnya buram akan ditarik oleh DMA. Ubin yang sepenuhnya buram adalah ubin yang tidak memiliki piksel transparan.
Ubin sebagian transparan (kiri) dan benar-benar buram (kanan). Dalam ubin sebagian transparan, beberapa piksel (di kiri bawah) transparan, dan karenanya latar belakang terlihat melalui area ini.Ubin, sprite, dan overlay yang sebagian transparan tidak dapat dibuat secara efektif oleh DMA. Faktanya, sistem DMA chip ATSAMD21 hanya menyalin data, dan tidak seperti Blitter komputer Amiga, itu tidak memeriksa transparansi (ditentukan oleh nilai warna). Semua elemen transparan sebagian digambar oleh CPU.
Data kemudian ditransmisikan ke layar menggunakan DMA.
Membuat saluran pipa
Seperti yang Anda lihat, jika kami melakukan operasi ini secara berurutan dalam satu buffer, itu akan memakan banyak waktu. Bahkan, ketika DMA sedang berjalan, CPU tidak akan sibuk kecuali menunggu DMA selesai! Ini adalah cara yang buruk untuk mengimplementasikan mesin grafis. Selain itu, ketika DMA mengirim data ke perangkat SPI, itu tidak menggunakan bandwidth penuh. Bahkan, bahkan ketika SPI beroperasi pada frekuensi 24 MHz, data ditransmisikan hanya pada frekuensi 3 MHz, yang cukup kecil. Dengan kata lain, DMA tidak digunakan untuk potensi penuh: DMA dapat melakukan tugas-tugas lain tanpa benar-benar kehilangan kinerja.
Itulah sebabnya kami mengimplementasikan pipeline, yang merupakan pengembangan dari ide buffering ganda (kami menggunakan tiga buffer!). Tentu saja, pada akhirnya, operasi selalu dilakukan secara berurutan. Tetapi CPU dan DMA secara bersamaan melakukan tugas yang berbeda, tanpa (terutama) saling mempengaruhi.
Inilah yang terjadi pada saat yang bersamaan:
- Buffer digunakan untuk menggambar data latar belakang menggunakan saluran DMA 1;
- Di buffer lain (yang sebelumnya diisi dengan data latar belakang), CPU menggambar sprite dan ubin transparan sebagian;
- Kemudian buffer lain (yang berisi blok data horizontal penuh) digunakan untuk mengirim data ke tampilan melalui SPI menggunakan saluran DMA 0. Tentu saja, buffer yang digunakan untuk mengirim data melalui SPI sebelumnya diisi dengan sprite sedangkan SPI mengirim blok sebelumnya dan sementara buffer lain penuh dengan ubin.
DMA
Sistem DMA chip ATSAMD21 tidak dapat dibandingkan dengan Blitter, namun demikian ia memiliki fitur-fiturnya yang bermanfaat. Berkat DMA, kami dapat memberikan kecepatan refresh yang sangat tinggi, meskipun memiliki lapangan bermain ganda.
Konfigurasi transfer DMA disimpan dalam RAM, dalam “deskriptor DMA”, memberi tahu DMA bagaimana dan di mana ia harus melakukan transfer saat ini. Deskriptor ini dapat digabungkan bersama: jika ada koneksi (mis. Tidak ada pointer nol), maka setelah transfer selesai, DMA akan secara otomatis menerima deskriptor berikutnya. Melalui penggunaan banyak deskriptor, DMA dapat melakukan "transfer kompleks" yang berguna ketika, misalnya, buffer sumber adalah urutan segmen yang tidak berdampingan dari byte yang berdekatan. Namun, butuh waktu untuk mendapatkan dan menulis deskriptor, karena Anda perlu menyimpan / memuat 16 byte deskriptor dari RAM.
DMA dapat bekerja dengan data dengan panjang yang berbeda: byte, setengah kata (16 bit) dan kata-kata (32 bit). Dalam spesifikasinya, panjang ini disebut "beat size". Untuk SPI, kami dipaksa untuk menggunakan transfer byte (meskipun spesifikasi REVD saat ini menyatakan bahwa chip ATSAMD21 SERCOM memiliki FIFO, yang, menurut Microchip, dapat menerima data 32-bit, pada kenyataannya, tampaknya mereka tidak memiliki FIFO. Spesifikasi REVD juga menyebutkan Register SERCOM CTRLC, yang tidak ada di file header dan di bagian deskripsi register. Untungnya, tidak seperti AVR, ATSAMD21 setidaknya memiliki register data transfer buffer, sehingga tidak akan ada jeda dalam pengiriman!). Untuk menggambar ubin, kami, tentu saja, menggunakan 32 bit. Ini memungkinkan Anda untuk menyalin dua piksel per ketukan. Chip ATSAMD21 DMA juga memungkinkan setiap sumber mengalahkan untuk meningkatkan sumber atau alamat tujuan dengan jumlah ukuran ketukan yang tetap.
Dua aspek ini sangat penting dan menentukan cara kita menggambar ubin.
Pertama, jika kami membuat satu pixel per beat (16 bit), kami akan membagi dua throughput sistem kami. Kami tidak dapat menolak bandwidth penuh!
Namun, jika kita menggambar dua piksel per ketukan, bidang permainan akan dapat menggulir hanya sejumlah piksel, yang akan menyebabkan pergerakan halus. Untuk menangani ini, Anda dapat menggunakan buffer yang dua atau lebih piksel lebih besar. Saat mengirim data ke layar, kita akan menggunakan offset yang benar (0 atau 1 piksel), tergantung pada apakah kita perlu memindahkan "kamera" dengan jumlah piksel genap atau ganjil.
Namun, demi kesederhanaan, kami menyediakan ruang untuk 11 ubin penuh (160 + 16 piksel), dan bukan untuk 160 + 2 piksel. Pendekatan ini memiliki satu keuntungan besar: kami tidak perlu menghitung dan memperbarui alamat penerima dari setiap deskriptor DMA (ini akan membutuhkan beberapa instruksi, yang dapat menghasilkan terlalu banyak perhitungan per ubin). Tentu saja, kami hanya akan menggambar jumlah minimum piksel, yaitu, tidak lebih dari 162. Ya, pada akhirnya, kami akan menghabiskan sedikit memori tambahan (dengan mempertimbangkan tiga buffer, ini adalah sekitar 1500 byte) untuk kecepatan dan kesederhanaan. Anda juga dapat melakukan optimasi lebih lanjut.

Semua buffer blok 16-baris (tanpa deskriptor) terlihat dalam animasi GIF ini. Di sebelah kanan adalah apa yang sebenarnya ditampilkan. 32 frame pertama ditampilkan dalam GIF, di mana kami memindahkan 1 piksel ke kanan di setiap frame. Area hitam buffer adalah bagian yang tidak diperbarui, dan isinya tetap dari operasi sebelumnya. Saat layar menggulirkan jumlah bingkai yang ganjil, lebar 162 piksel ditarik ke dalam buffer. Namun, kolom pertama dan terakhir dari mereka (yang disorot dalam animasi) dibuang. Ketika nilai gulir kelipatan 16 piksel, operasi draw dalam buffer dimulai dari kolom pertama (x = 0).
Bagaimana dengan pengguliran vertikal?
Kami akan menghadapinya setelah kami menunjukkan metode menyimpan ubin di memori flash.
Cara menyimpan ubin
Pendekatan naif (yang cocok untuk kita jika kita hanya merender melalui CPU) adalah menyimpan ubin dalam memori flash sebagai urutan warna piksel. Pixel pertama dari baris pertama, kedua, dan seterusnya, hingga keenam belas. Kemudian kita menyimpan piksel pertama dari baris kedua, baris kedua, dan seterusnya.
Mengapa keputusan seperti itu naif? Karena dalam kasus ini, DMA hanya dapat membuat 16 piksel per deskriptor DMA! Oleh karena itu, kita memerlukan 16 deskriptor, yang masing-masing memerlukan 4 + 4 operasi akses memori (yaitu, untuk mentransfer 32 byte - 8 operasi membaca memori + 8 operasi penulisan memori - DMA harus melakukan 4 lebih banyak pembacaan + 4 penulisan). Ini sangat tidak efisien!
Bahkan, untuk setiap deskriptor, DMA hanya dapat menambah sumber dan alamat tujuan dengan jumlah kata yang tetap. Setelah menyalin baris pertama ubin ke buffer, alamat penerima tidak boleh ditambah 1 kata, tetapi dengan nilai yang menunjuk ke baris buffer berikutnya. Ini tidak mungkin karena setiap deskriptor transmisi hanya mengindikasikan kenaikan transmisi beat, yang tidak dapat diubah.
Akan jauh lebih pintar untuk mengirim dua piksel pertama dari setiap baris ubin secara berurutan, yaitu, piksel 0 dan 1 dari garis 0, piksel 0 dan 1 dari garis 1, dll., Hingga piksel 0 dan 1 dari garis 15. Kemudian kami mengirim piksel 2 dan 3 dari garis 0, dan seterusnya.
Bagaimana ubin disimpan?Pada gambar di atas, setiap angka menunjukkan urutan piksel 16-bit disimpan dalam larik ubin.
Ini dapat dilakukan dengan deskriptor, tetapi kita membutuhkan dua hal:
- Ubin harus disimpan sehingga saat menambah sumber dengan satu kata, kami selalu menunjuk ke posisi piksel yang benar. Dengan kata lain, jika (r, c) adalah piksel dalam baris r dan kolom c, maka kita perlu menyimpan piksel (0,0) (0,1) (1,0) (1,1) (2,0) secara berurutan (2.1) ... (15.0) (15.1) (0.2) (0.3) (1.2) (1.3) ...
- Lebar buffer harus 256 piksel (bukan 160)
Tujuan pertama sangat mudah dicapai: cukup ubah urutan data, Anda dapat melakukan ini saat mengekspor grafik ke file c (lihat gambar di atas).
Masalah kedua dapat diselesaikan karena DMA memungkinkan Anda untuk meningkatkan alamat penerima setelah setiap ketukan dengan 512 byte. Ini memiliki dua konsekuensi:
- Kami tidak dapat mengirim data menggunakan deskriptor tunggal melalui blok SPI. Ini bukan masalah yang sangat serius, karena pada akhirnya kita membaca satu deskriptor hingga 160 piksel. Dampak kinerja akan minimal.
- Blok harus memiliki ukuran 256 * 2 * 16 byte = 8 KB, dan akan ada banyak "ruang yang tidak digunakan" di dalamnya.
Namun, ruang ini masih dapat digunakan, misalnya, untuk deskriptor.
Faktanya, masing-masing deskriptor berukuran 16 byte. Kami membutuhkan setidaknya 10 * 8 (dan sebenarnya 11 * 8!) Penjelas untuk ubin dan 16 penjelas untuk SPI.
Itu sebabnya semakin banyak ubin, semakin tinggi kecepatannya. Bahkan, jika kita menggunakan, misalnya, ubin 32 x 32, maka kita akan membutuhkan lebih sedikit deskriptor per layar (320 bukannya 640). Ini akan mengurangi pemborosan sumber daya.
Tampilkan blok data
Buffer blok, deskriptor, dan data lainnya disimpan dalam tipe struktur, yang kami namakan displayBlock_t.
displayBlock adalah array 16 elemen displayLineData_t. Data DisplayLine mengandung 176 piksel ditambah 80 kata. Dalam 80 kata ini, kami menyimpan deskriptor tampilan atau data tampilan bermanfaat lainnya (menggunakan gabungan).
Karena kami memiliki 16 baris, setiap ubin pada posisi X menggunakan 8 DMA deskriptor pertama (0 hingga 7) dari garis X. Karena kami memiliki maksimum 11 ubin (garis tampilan adalah 176 piksel lebar), ubin hanya menggunakan deskriptor DMA pertama 11 baris data. Deskriptor 8–9 dari semua baris dan deskriptor 0–9 dari baris 11–15 gratis.
Dari jumlah tersebut, deskriptor 8 dan 9 dari baris 0..7 akan digunakan untuk SPI.
Deskriptor 0..9 baris 11-15 (hingga 50 deskriptor, meskipun kami hanya akan menggunakan 48 di antaranya) akan digunakan untuk bidang permainan latar belakang.
Gambar di bawah menunjukkan strukturnya.
Lapangan bermain latar belakang
Latar belakang bermain ditangani secara berbeda. Pertama, jika kita membutuhkan pengguliran yang halus, maka kita harus kembali ke format dua-pixel, karena latar depan dan latar belakang akan menggulir dengan kecepatan yang berbeda. Karena itu, beat akan setengah jalan. Meskipun ini tidak menguntungkan dalam hal kecepatan, pendekatan ini memfasilitasi integrasi. Kami hanya memiliki sedikit deskriptor yang tersisa, sehingga ubin kecil tidak dapat digunakan. Selain itu, untuk menyederhanakan pekerjaan dan dengan cepat menambahkan paralaks, kita akan menggunakan "sektor" yang panjang.
Latar belakang diambil hanya jika setidaknya ada satu piksel yang sebagian transparan. Ini berarti bahwa jika hanya ada satu ubin transparan, latar belakang akan digambar. Tentu saja, ini adalah pemborosan bandwidth, tetapi menyederhanakan semuanya.
Bandingkan bidang latar belakang dan permainan depan:
- Di latar belakang, sektor digunakan, yang ubin panjang disimpan dengan cara "naif".
- Latar belakang memiliki peta sendiri, tetapi secara horizontal berulang. Berkat ini, lebih sedikit memori yang digunakan.
- Latar belakang memiliki paralaks untuk setiap sektor.
Lapangan bermain depan
Seperti yang dikatakan, di setiap blok kami memiliki hingga 11 ubin (10 ubin penuh, atau 9 ubin penuh dan 2 file parsial). Masing-masing ubin ini, jika tidak ditandai sebagai transparan, DMA ditarik. Jika tidak sepenuhnya buram, maka ditambahkan ke daftar, yang akan dianalisis nanti, saat merender sprite.
Kami terhubung bersama dua bidang bermain
Deskriptor bidang bermain latar belakang (yang selalu dihitung) dan bidang bermain depan membentuk daftar yang sangat panjang. Bagian pertama menggambar bidang bermain latar belakang. Bagian kedua menggambar ubin di latar belakang. Panjang bagian kedua mungkin variabel, karena deskriptor DMA dari ubin transparan sebagian dikeluarkan dari daftar. Jika blok hanya berisi ubin buram, maka DMA dikonfigurasi sebagai berikut. untuk memulai langsung dari deskriptor pertama dari ubin pertama.
Sprite dan ubin dengan transparansi
Ubin dengan transparansi dan sprite diproses hampir sama.
Analisis piksel ubin / sprite dilakukan. Jika hitam, maka transparan, dan karena itu ubin latar belakang tidak berubah. Jika bukan hitam, maka piksel latar belakang diganti oleh piksel sprite / ubin.Pengguliran vertikal
Saat bekerja dengan pengguliran horizontal, kami menggambar hingga 11 ubin, meskipun saat menggambar 11 ubin, yang pertama dan terakhir hanya sebagian digambar. Render parsial semacam itu dimungkinkan karena fakta bahwa setiap deskriptor menggambar dua kolom ubin, sehingga kita dapat dengan mudah mengatur awal dan akhir daftar tertaut.Ketika bekerja dengan pengguliran vertikal, kita perlu menghitung register penerima dan volume transmisi. Mereka harus diatur beberapa kali per frame. Untuk menghindari kerepotan ini, kita cukup menggambar hingga 9 blok penuh per frame (8 jika scrolling merupakan kelipatan 16).Peralatan
Seperti yang kami katakan, inti dari sistem ini adalah uChip. Bagaimana dengan yang lainnya?Ini adalah diagram! Beberapa aspeknya layak disebut.Kunci
Untuk mengoptimalkan penggunaan I / O, kami menggunakan sedikit trik. Kami akan memiliki 4 bus sensor L1-L4, dan satu kawat LC umum. 1 dan 0 secara bergantian diterapkan pada kawat biasa. Dengan demikian, bus sensor akan secara bergantian ditarik ke bawah atau ke atas dengan bantuan resistor pull-up internal. Dua kunci terhubung antara masing-masing bus utama dan bus umum. Dioda dimasukkan secara seri dengan dua tombol ini. Masing-masing dioda ini diaktifkan dalam arah yang berlawanan, sehingga setiap kali hanya satu kunci yang "dibaca".Karena tidak ada pengontrol keyboard bawaan (dan tidak ada pengontrol keyboard bawaan menggunakan metode yang menarik ini), delapan tombol dengan cepat disurvei pada awal setiap frame. Karena input harus ditarik ke atas dan ke bawah, kita tidak dapat (dan tidak ingin) menggunakan resistor eksternal, jadi kita perlu menggunakan yang terintegrasi, yang dapat memiliki resistansi yang cukup tinggi (60 kOhm). Ini berarti bahwa ketika bus umum mengubah status, dan bus data mengubah status tarik naik / turunnya, Anda harus memasukkan beberapa penundaan sehingga resistor tarik naik / turun bawaan mengubah kontrak dan mengatur kapasitansi liar ke level yang diinginkan. Tapi kami tidak mau menunggu! Oleh karena itu, kami menempatkan bus umum dalam keadaan impedansi tinggi (sehingga tidak ada perbedaan pendapat), dan pertama-tama ubah bus sensor ke nilai logis 1 atau 0,untuk sementara mengkonfigurasi mereka sebagai output. Kemudian mereka dikonfigurasi sebagai input dengan menarik ke atas atau ke bawah. Karena resistansi keluaran berada pada urutan puluhan Ohm, keadaan berubah dalam beberapa nanodetik, yaitu, ketika bus sensor beralih kembali ke input, maka sudah dalam keadaan yang diinginkan. Setelah itu, bus umum beralih ke output dengan polaritas yang berlawanan.Ini sangat meningkatkan kecepatan pemindaian dan menghilangkan kebutuhan untuk penundaan / instruksi nop.Koneksi SPI
Kami menghubungkan SD dan layar sehingga mereka berkomunikasi satu sama lain tanpa mentransfer data ke ATSAMD21. Ini bisa bermanfaat jika Anda ingin memutar video.Resistor yang menghubungkan MISO dan MOSI harus rendah. Jika terlalu besar, maka SPI tidak akan berfungsi, karena sinyalnya akan terlalu lemah.Optimalisasi dan pengembangan lebih lanjut
Salah satu masalah terbesar adalah penggunaan RAM. Tiga blok menempati masing-masing 8 KB, hanya menyisakan 8 KB per tumpukan dan variabel lainnya. Saat ini, kami hanya memiliki 1,3 KB RAM gratis + 4 KB tumpukan (4 KB per tumpukan - ini banyak, mungkin kami akan menguranginya).Namun, Anda dapat menggunakan blok dengan ketinggian tidak 16, tetapi 8 piksel. Ini akan meningkatkan pemborosan sumber daya pada deskriptor DMA, tetapi hampir mengurangi separuh jumlah memori yang ditempati oleh buffer blok (perhatikan bahwa jumlah deskriptor tidak akan berubah jika kita terus menggunakan 16 × 16 ubin, jadi kita harus mengubah struktur blok). Ini dapat membebaskan sekitar 7,5 KB RAM, yang akan sangat berguna untuk mengimplementasikan fungsi-fungsi seperti kartu yang dapat dimodifikasi dengan rahasia atau menambahkan suara (walaupun suara dapat ditambahkan bahkan dengan 1 KB RAM).Masalah lain adalah sprite, tetapi modifikasi ini lebih sederhana untuk dilakukan, dan Anda hanya perlu fungsi createNextFrameScene () untuk itu. Faktanya, kami membuat dalam RAM sebuah array yang sangat besar dengan kondisi semua sprite. Kemudian, untuk setiap sprite, kami menghitung apakah posisinya berada di area layar, dan kemudian menghidupkannya dan menambahkannya ke daftar render.Sebagai gantinya, Anda dapat melakukan optimasi. Misalnya, di gameMap Anda tidak hanya dapat menyimpan nilai ubin, tetapi juga bendera yang menunjukkan transparansi ubin, yang diatur dalam editor. Ini akan memungkinkan kami untuk dengan cepat memeriksa apakah ubin harus dirender: DMA atau CPU. Itu sebabnya kami menggunakan catatan 16-bit untuk kartu ubin. Jika kita berasumsi bahwa kita memiliki satu set 256 ubin (saat ini kita memiliki kurang dari 128 ubin, tetapi ada cukup ruang pada memori flash untuk menambahkan yang baru), maka ada 7 bit gratis yang dapat digunakan untuk keperluan lain. Tiga dari tujuh bit ini dapat digunakan untuk menunjukkan apakah sprite / objek sedang disimpan. Sebagai contoh:
0b000 =
0b001 =
0b010 =
0b011 =
0b100 =
0b101 =
0b110 =
0b111 = , , .
Kemudian Anda dapat membuat tabel bit dalam RAM di mana setiap bit berarti apakah (misalnya, musuh) terdeteksi / apakah (misalnya, bonus) diambil / apakah objek tertentu diaktifkan (switch). Pada level 10 × 10 layar, ini akan membutuhkan 8000 bit, mis. RAM 1 KB. Bit direset ketika musuh terdeteksi atau bonus diambil.Dalam createNextFrameScene (), kita harus memeriksa bit yang sesuai dengan ubin di area yang terlihat saat ini. Jika mereka memiliki nilai 1:- Jika ini bonus, tambahkan saja ke daftar sprite untuk rendering.
- Jika ini adalah musuh, buat sprite dinamis dan setel ulang benderanya. Dalam bingkai berikutnya, adegan akan berisi sprite dinamis hingga musuh meninggalkan layar atau terbunuh.
Pendekatan ini memiliki kelemahan.- -, ( ). .
- -, 80 , , . , 32 . , «/» ( «», .. 0!). «», «» ( ).
- -, . ( ), . , .
- -, , , , . , , . , , , , !
- , (, Unreal Tournament , ).
Namun demikian, dengan cara ini kita dapat menyimpan dan memproses sprite pada level yang jauh lebih efisien.Namun, teknik ini lebih relevan dengan "logika permainan" daripada mesin grafis dari permainan.Mungkin di masa depan kita akan mengimplementasikan fungsi ini.Untuk meringkas
Kami harap Anda menikmati artikel pengantar ini. Kita perlu menjelaskan lebih banyak aspek yang akan menjadi topik artikel mendatang.Sementara itu, Anda dapat mengunduh kode sumber lengkap game! Jika Anda suka, maka Anda dapat mendukung secara finansial artis ansimuz , yang menggambar semua gambar dan memberikannya kepada dunia secara gratis. Kami juga menerima donasi .Game belum selesai. Kami ingin menambahkan suara, banyak level, objek yang dapat Anda gunakan untuk berinteraksi dan sejenisnya. Anda dapat membuat modifikasi sendiri! Kami berharap dapat melihat game baru dengan grafik dan level baru!Kami akan segera merilis editor peta, tetapi untuk saat ini terlalu sederhana untuk menunjukkannya kepada komunitas!Video
(Catatan: karena pencahayaan yang buruk, video direkam pada frame rate yang jauh lebih rendah! Sebentar lagi kami akan memperbarui video sehingga Anda dapat memperkirakan kecepatan penuh pada 40 fps!)Terima kasih
Grafik permainan (dan ubin yang ditunjukkan pada beberapa gambar) diambil dari aset gratis "Sunny Land" yang dibuat oleh ansimuz .Bahan yang bisa diunduh
Kode sumber proyek adalah dalam domain publik, yaitu disediakan gratis. Kami membagikannya dengan harapan itu akan bermanfaat bagi seseorang. Kami tidak menjamin bahwa karena bug / kesalahan dalam kode tidak akan ada masalah!Diagramskematik proyek KiCadProyek Atmel Studio 7 (sumber)