Bagian yang
pertama ,
kedua ,
ketiga .
Sisa mesin
Kode yang kami tulis untuk meniru prosesor 8080 cukup umum dan dapat dengan mudah diadaptasi untuk dijalankan pada mesin apa pun dengan kompiler C. Tetapi untuk memainkan permainan itu sendiri, kita perlu berbuat lebih banyak. Kita harus meniru peralatan seluruh mesin arcade dan menulis kode yang menempelkan fitur spesifik dari lingkungan komputasi kita ke emulator.
(Anda mungkin tertarik melihat
diagram sirkuit mesin.)
Pengaturan waktu
Game ini berjalan pada 2 MHz 8080. Komputer Anda jauh lebih cepat. Untuk mempertimbangkan ini, kita harus membuat semacam mekanisme.
Gangguan
Interupsi dirancang sedemikian rupa sehingga prosesor dapat memproses tugas dengan waktu eksekusi yang tepat, seperti I / O. Prosesor dapat menjalankan program, dan ketika pin interupsi dipicu, ia berhenti menjalankan program saat ini dan melakukan sesuatu yang lain.
Kita perlu mensimulasikan cara mesin arcade menghasilkan interupsi.
Grafik
Space Invaders menggambar grafik ke memorinya di kisaran alamat 0x2400. Pengontrol video perangkat keras nyata akan membaca RAM dan mengontrol tampilan CRT. Program kami harus meniru perilaku ini dengan menampilkan gambar permainan di jendela.
Tombol
Gim ini memiliki tombol fisik yang dibaca program menggunakan perintah IN pada prosesor 8080. Emulator kami harus mengikat input keyboard ke perintah IN ini.
ROM dan RAM
Saya harus mengakui: kita “memotong sudut” dengan membuat buffer memori 16-kilobyte, yang mencakup 16 KB alokasi memori prosesor yang lebih rendah. Faktanya, alokasi memori 2 KB pertama adalah memori read-only (ROM) nyata. Kita perlu menempatkan operasi tulis dalam memori ke dalam suatu fungsi sehingga tidak mungkin menulis ke ROM.
Suara
Sejauh ini kami belum mengatakan apa-apa tentang suara. Space Invaders memiliki skema suara analog lucu yang mereproduksi satu dari 8 suara yang dikendalikan oleh perintah OUT, yang dikirim ke salah satu port. Kami harus mengonversi perintah OUT ini untuk memutar sampel suara di platform kami.
Ini mungkin terlihat seperti banyak pekerjaan, tetapi tidak terlalu buruk, dan kita dapat bergerak secara bertahap. Hal pertama yang ingin kita lakukan adalah melihat layar, yang membutuhkan interupsi, grafik, dan bagian dari pemrosesan perintah IN dan OUT.
Menampilkan dan memperbarui
Dasar-dasarnya
Anda mungkin akrab dengan komponen sistem tampilan video. Di suatu tempat di sistem ada semacam RAM, yang berisi gambar untuk ditampilkan di layar. Dalam hal perangkat analog, ada peralatan yang membaca RAM ini dan mengubah byte menjadi tegangan analog yang dikirim ke monitor.
Pemahaman yang lebih dalam tentang sistem akan membantu kita dalam menganalisis tujuan alokasi memori dan fungsionalitas kode.
Layar analog memiliki persyaratan untuk kecepatan dan waktu penyegaran. Pada waktu tertentu, layar memiliki piksel spesifik yang diperbarui. Gambar yang dikirimkan ke layar diisi titik demi titik, mulai dari sudut kiri atas dan ke kanan atas, kemudian titik pertama dari baris kedua, titik terakhir dari baris kedua, dll. Setelah baris terakhir digambar di layar, pengontrol video dapat menghasilkan Vertical Blank Interrupt (juga dikenal sebagai VBI atau VBL).
Untuk memastikan animasi yang lancar, gambar dalam RAM yang diproses oleh pengontrol video tidak dapat diubah. Jika pembaruan RAM terjadi di tengah bingkai, pemirsa akan melihat bagian dari dua gambar. Ini menghasilkan efek "sobekan" ketika bingkai yang berbeda dari bingkai di bagian bawah ditampilkan di bagian atas layar. Jika Anda pernah melihat garis pemisah, Anda tahu seperti apa bentuknya.
Untuk menghindari kesenjangan, perangkat lunak harus melakukan sesuatu untuk menghindari mentransfer lokasi pembaruan layar. Dan hanya ada satu cara untuk melakukan ini.
VBL dihasilkan setelah akhir baris terakhir, dan biasanya ada sejumlah waktu sebelum menggambar ulang baris pertama. (Ini waktu Vertikal Kosong, dan bisa sekitar 1 milidetik.)
Ketika VBL diterima, program mulai menampilkan layar dari atas.
Setiap garis ditarik sebelum proses pemindaian bingkai terbalik.
CPU selalu unggul, dan karenanya menghindari jeda baris.
Sistem Video Space Invaders
Halaman yang sangat informatif memberi tahu kami bahwa Space Invaders memiliki dua interupsi video. Salah satunya adalah untuk ujung bingkai, tetapi juga menghasilkan interupsi di tengah layar. Halaman ini menjelaskan sistem pembaruan layar - permainan menggambar grafik di bagian atas layar ketika menerima gangguan di tengah layar, dan menggambar grafik di bagian bawah layar ketika menerima gangguan di ujung bingkai. Ini adalah cara yang cukup cerdas untuk menghilangkan jeda baris, dan contoh yang baik dari apa yang dapat dicapai ketika Anda mengembangkan perangkat keras dan perangkat lunak pada saat yang sama.
Kita harus memaksa emulasi mesin kita untuk menghasilkan gangguan seperti itu. Jika kita akan menghasilkan mereka dengan frekuensi 60 Hz, serta mesin Space Invaders, maka game akan ditarik dengan frekuensi yang benar.
Pada bagian selanjutnya, kita akan berbicara tentang mekanisme interupsi dan memikirkan cara meniru mereka.
Tombol dan port
8080 mengimplementasikan I / O menggunakan instruksi IN dan OUT. Ini memiliki 8 port IN dan OUT yang terpisah - port ditentukan oleh byte data dari perintah. Misalnya,
IN 3
akan memasukkan nilai port 3 di register A, dan
OUT 2
akan mengirim A ke port 2.
Saya mengambil informasi tentang tujuan masing-masing port dari situs
Arkeologi Komputer . Jika informasi ini tidak tersedia, kita harus mendapatkannya dengan mempelajari diagram sirkuit, serta membaca dan eksekusi kode langkah-demi-langkah.
:
1
0 (0, )
1 Start
2 Start
3 ?
4
5
6
7 ?
2
0,1 DIP- (0:3,1:4,2:5,3:6)
2 ""
3 DIP- , 1:1000,0:1500
4
5
6
7 DIP-, 1:,0:
3
2 ( 0,1,2)
3
4
5
6 "" ? , ,
(0=a,1=b,2=c ..)
( 3,5,6 1=$01 2=$00
, (attract mode))
Ada tiga cara untuk mengimplementasikan I / O di tumpukan perangkat lunak kami (yang terdiri dari emulator 8080, kode mesin, dan kode platform).
- Cantumkan pengetahuan mesin di emulator 8080 kami
- Cantumkan pengetahuan 8080 emulator dalam kode mesin
- Ciptakan antarmuka formal antara tiga bagian kode untuk memungkinkan pertukaran informasi melalui API
Saya mengesampingkan opsi pertama - cukup jelas bahwa emulator berada di bagian paling bawah dari rantai panggilan ini dan harus tetap terpisah. (Bayangkan Anda perlu menggunakan kembali emulator untuk gim lain, dan Anda akan mengerti apa yang saya maksud.) Dalam kasus umum, mentransfer struktur data tingkat tinggi ke level yang lebih rendah adalah solusi arsitektur yang buruk.
Saya memilih opsi 2. Biarkan saya tunjukkan kode terlebih dahulu:
while (!done) { uint8_t opcode = state->memory[state->pc]; if (*opcode == 0xdb)
Kode ini mengimplementasikan kembali pemrosesan opcodes untuk IN dan OUT di lapisan yang sama, yang memanggil emulator untuk sisa perintah. Menurut pendapat saya, ini membuat kode lebih bersih. Ini mirip dengan override atau subclass untuk dua perintah, yang merujuk ke lapisan otomat.
Kerugiannya adalah kita mentransfer emulasi opcode di dua tempat. Saya tidak akan menyalahkan Anda karena memilih opsi ketiga. Pada opsi kedua, lebih sedikit kode yang diperlukan, tetapi opsi 3 lebih "bersih", tetapi harganya merupakan peningkatan dalam kompleksitas. Ini adalah masalah pilihan gaya.
Shift register
Mesin Space Invaders memiliki solusi perangkat keras yang menarik yang mengimplementasikan perintah bit shift. 8080 memiliki perintah untuk shift 1-bit, tetapi lusinan perintah 8080 akan diperlukan untuk mengimplementasikan shift multi-bit / multi-byte. Perangkat keras khusus memungkinkan game untuk melakukan operasi ini hanya dalam beberapa instruksi. Dengan bantuannya, setiap frame digambar di bidang game, yaitu, ia digunakan berkali-kali per frame.
Saya rasa saya tidak bisa menjelaskannya lebih baik daripada
analisis Arkeologi Komputer yang sangat baik:
; 16- :
; f 0
; xxxxxxxxyyyyyyyy
;
; 4 x y, x, :
; $0000,
; write $aa -> $aa00,
; write $ff -> $ffaa,
; write $12 -> $12ff, ..
;
; 2 ( 0,1,2) 8- , :
; offset 0:
; rrrrrrrr result=xxxxxxxx
; xxxxxxxxyyyyyyyy
;
; offset 2:
; rrrrrrrr result=xxxxxxyy
; xxxxxxxxyyyyyyyy
;
; offset 7:
; rrrrrrrr result=xyyyyyyy
; xxxxxxxxyyyyyyyy
;
; 3 .
Untuk perintah OUT, menulis ke port 2 menetapkan jumlah shift, dan menulis ke port 4 mengatur data dalam register shift. Membaca dengan IN 3 mengembalikan data yang digeser oleh jumlah shift. Di mesin saya, ini diterapkan seperti ini:
-(uint8_t) MachineIN(uint8_t port) { uint8_t a; switch(port) { case 3: { uint16_t v = (shift1<<8) | shift0; a = ((v >> (8-shift_offset)) & 0xff); } break; } return a; } -(void) MachineOUT(uint8_t port, uint8_t value) { switch(port) { case 2: shift_offset = value & 0x7; break; case 4: shift0 = shift1; shift1 = value; break; } }
Keyboard
Untuk mendapatkan respons mesin, kita perlu mengikat input keyboard ke sana. Sebagian besar platform memiliki cara untuk menerima penekanan tombol dan melepaskan acara. Kode platform untuk tombol akan terlihat seperti berikut:
if(PeekMessage(&msg,NULL,0,0,PM_REMOVE)) { if (msg.message==WM_KEYDOWN ) { if ( msg.wParam == VK_LEFT ) MachineKeyDown(LEFT); } else if (msg.message==WM_KEYUP ) { if ( msg.wParam == VK_LEFT ) MachineKeyUp(LEFT); } }
Kode mesin yang menempelkan kode platform ke kode emulator akan terlihat seperti ini:
MachineKeyDown(char key) { switch(key) { case LEFT: port[1] |= 0x20;
Jika mau, Anda dapat menggabungkan kode mesin dan platform sesuka Anda - ini adalah pilihan implementasi. Saya tidak akan melakukan ini karena saya akan port mesin ke beberapa platform yang berbeda.
Gangguan
Setelah mempelajari manual, saya menyadari bahwa 8080 menangani interupsi sebagai berikut:
- Sumber interupsi (eksternal ke CPU) mengatur pin interupsi CPU.
- Ketika CPU mengonfirmasi interupsi diterima, sumber interupsi dapat mengirim opcode apa pun ke bus dan CPU melihatnya. (Paling sering mereka menggunakan perintah RST.)
- CPU menjalankan perintah ini. Jika RST, maka ini adalah analog dari perintah CALL untuk alamat tetap di bagian bawah memori. Ini mendorong PC saat ini ke tumpukan.
- Kode di alamat memori yang lebih rendah memproses apa yang diinginkan interupsi untuk memberitahu program. Setelah pemrosesan selesai, RST berakhir dengan panggilan ke RET.
Peralatan video dari gim ini menghasilkan dua interupsi yang harus kita tiru secara terprogram: ujung bingkai dan tengah bingkai. Keduanya dilakukan pada 60 Hz (60 kali per detik). 1/60 detik adalah 16,6667 milidetik.
Untuk mempermudah bekerja dengan interupsi, saya akan menambahkan fungsi ke emulator 8080:
void GenerateInterrupt(State8080* state, int interrupt_num) {
Kode platform harus mengimplementasikan timer yang dapat kita panggil (untuk saat ini, saya hanya menyebutnya waktu ()). Kode mesin akan menggunakannya untuk memberikan interupsi ke emulator 8080. Dalam kode mesin, ketika penghitung waktu kedaluwarsa, saya akan memanggil GenerateInterrupt:
while (!done) { Emulate8080Op(state); if ( time() - lastInterrupt > 1.0/60.0)
Ada beberapa detail tentang bagaimana 8080 menangani interupsi, yang tidak akan kita tiru. Saya percaya bahwa pemrosesan seperti itu akan cukup untuk tujuan kita.