CHIP-8 Mimarisi ve Ruby Emülatörü

By | 3 Ocak 2015

2015’deki ilk blog yazım olarak bunu öngörmemiştim, zira eski oyun konsollarına olan ilgim sadece oyun oynamak üzerine idi. Ta ki, yukarıdan vahiy gönderen bir push servisine yakalanmış gibi, “Niye emülatör yazmıyorum ki?” diye düşünene kadar.

Öncelikle söylemem lazım, benim şahsi favorim bir çok N64 hayranının aksine NES ve SNES ikilisi oluyor. Gerek mimarilerinin göreceli kolaylığı ve güzelliği, gerekse de efsanevi oyunlarıyla (Benim için bu efsane oyunlar, Mario ve Zelda serileri oluyor) beni benden alıyorlar. Tamam kabul ediyorum, o konsollar çıktığında ben portakaldaki vitamin bile değildim ama çakma konsollar ve emülatörler ile durumu kurtardık yine de. Bu sebeplerden dolayı, başta NES mimarisine ve emülatörüne giriştim fakat, göreceli olarak daha kolay olan mimarisi bile feleğimi şaşırttı. Onu öğrenirken, bir yandan da daha eski ve cidden daha kolay bir mimaride olan CHIP-8’i öğrenmeye başladım. Pong ve Space Invaders’ın platformu olan meşhur CHIP-8’den bahsediyorum.

chip-8-si

Bilmeyenler için, CHIP-8 aslında gerçek bir oyun konsolu değil. Daha basit bir şekilde aslında, yorumlanan (interpreted) bir programlama dili ve bu dil ile yazılan programlar, CHIP-8 sanal bilgisayarı üstünde çalışıyor. Genellikle de oyunlar için kullandığından, çok rahat bir şekilde sanal bir oyun konsolu olduğu söylenebilir.

Dilin ilk kullanımı 70’lerin ortasına dayandığından, en baba ve eski bazı oyunlar (örnekteki gibi Pong ve Space Invaders) bu platform üzerinde geliştirildi. Bu oyunların niteliklerini de biliyorsanız ve hatırlayacak olursanız, platformun oldukça basit bir yapısı olduğunu tahmin edebilirsiniz.

Genel Mimari

Bellek:

CHIP-8 dili, 4 KB’lık bir belleği adresleyebiliyor. 0x000’dan başlayıp, 0xFFF’e kadar giden bu bellek bölümünün ilk 512 baytlık kısmı (0x000 – 0x200) yorumlayıcıya ayrıldığından, dışarıdan kullanılamıyor (Bazı varyasyonlarında 0x600’e kadar gidiyor.) Sonraki 3.5 KB’lık kısım ile programın kullanabileceği alan olarak duruyor.

Register’lar:

  • 16 tane genel amaçlı (V0…VF) 8 bitlik
  • Adres tutucu (I = Index) 16 bitlik
  • 2 tane özel amaçlı 8 bitlik: Gecikme (delay) ve ses sayaçları
  • Program sayacı (PC) 16 bitlik
  • Yığın işaretçisi (SP = Stack Pointer) 8 bit
  • Ve bir takım, varsayılan olarak dışarıdan erişilemeyen register’lar

Buna ek olarak, sistem 16 tane 2 baytlık veriyi tutabilecek bir yığın (stack) yapısına sahip. Bu şekilde en fazla 16 tane iç içe alt yordam kullanılabiliyor.

Girdi:

Sistemin girdisi 16 tuşluk bir klavyeden yapılıyor. Emülasyonda kolayca 0x0’dan 0xF’e kadar haritalanabilir.

Görüntü ve Ses:

Görüntü için 64 * 32 = 2048 piksellik siyah-beyaz bir ekran kullanıyor. Genel çizim için sprite’lar kullanılıyor ve bunların boyutu 15 bayta kadar çıkabiliyor. Dolayısıyla bir sprite en fazla 8 x 15 = 120 piksellik bir alan işgal edebiliyor.

Ses için ise, ses sayacı sıfıra vurduğunda tek bir belirli tonda ses çıkartılıyor. Yani genel olarak baktığımızda sistemin görüntü ve ses mimarisi 2048 piksel ve tek bir ses tonundan daha fazlasına ihtiyaç duymuyor. Bu yüzden implementasyonu oldukça kolay.

Ek olarak fontların oluşturulması için, yine sprite mantığında 5 tane bayt (120 piksel) kullanılıyor. Genel tablo için, şuraya gözatabilirsiniz.

Talimatlar (instructions):

Sistemde genel olarak 35 tane (bazı kaynaklarda 36 deniyor.) talimat (instruction) var. Tüm bu talimatlar, 2 bayt boyutunda ve Big Endian olarak, yani en önemli bayt en başta bulunacak şekilde çalışıyor. Bu kadar az sayıda talimat olduğundan dolayı, hepsini Wikipedia’da bile listelenmiş olarak bulabilirsiniz.

Emülatöre Giriş

Emülatörün nasıl çalıştığını şu sayfadaki Python örneğini çalışarak öğrendim. O örnekteki sistem emülasyonunda, girdi ve çıktı aygıtları olarak Pyglet kütüphanesi kullanılıyor. Yani aslında sadece temel işlemci ve belleği sanallaştırmamız (aslında zaten sanal da) yeterli oluyor.

İşe hemen bir sınıf oluşturarak ve nitelikleri tanımlayarak başladım.

Ardından bu niteliklere, başlangıç değerlerini vermem gerekti. Mimarideki genel özellikleri göz önünde bulundurarak, niteliklere başlangıç değerlerini verdim. Ekstra olarak font şemasındaki onaltılık değerleri de ekledim.


Son değişkeni tanımladıktan sonra “oha falan olma hali” olduysa aldırmayın, gayet basit bir açıklaması var. Emülatörü hazırladıktan sonra, oyun dosyalarını programa yükleyeceğiz. Bu oyun dosyaları (genellikle konsollar için ROM olarak söylenir.) CHIP-8 mimarisinde çalışacak şekilde hazırlanmış programlar oluyor ve içlerinde talimatlar hazır olarak geliyor. Hangi onaltılık değerin hangi talimata (daha doğrusu burada methota) denk geldiğini bu hash ile belirliyoruz. Tabi bunlar (onaltılık kodlar) sallamasyon değerler değil, yazının sonunda paylaşacağım kaynaklardan, cidden değerler ile talimatların önceden belirli olarak eşleştirildiğini göreceksiniz.

Sonraki adım, verilen dosyayı (ROM’u) belleğe yüklemek oluyor. Hemen metodu tanımlayalım:

Temel olarak yaptığmız şey, dosyayı ikilik (binary) olarak açıp okumak ve tüm değerleri, 0x200’den sonraki bellek bölgelerine yerleştirmek.

Ardından sanal makinenin devir (cycle) sistemini oluşturan metodu yazmamız lazım:

Bu metodu dikkatlice incelediyseniz, talimatı çalıştıracak olan, özel bir metot yazmamız lazım. Bunu diğerleri gibi public yapmak yerine, private olarak yapmayı tercih ettim şahsen ben:

Ne olur ne olmaz diye, olası bir exception’dan kurtulmak için de, tek satırlık bir rescue kullandım. Ruby’nin bu tek satırlık işlem yaptırabilen metotlarının hastasıyım :)

Ek olarak, ufak bir metotla da hangi tuşun basıldığını bulmamız gerecek. Yine private altından yazalım:

Şimdi ise en başa bela kısım geliyor. Başlangıçta, çalıştırılacak talimatları haritalamıştık. Şimdi ise o talimatların metotlarını yazmamız gerekecek. Hazır olun, 35 tane metot geliyor, bunları da private‘ın altına koyun:

Tüm bu metotların ne yaptığını açıklayamayacağım, kaynaklardan bulabilirsiniz. Sadece implementasyonları Ruby’ye aktardım. Yine de sadece metota bakarak bile ne yaptığını kestirebilirsiniz. Bir de dikkat ettiyseniz, metot isimlerinin başına alt çizgi “_” koyduk. Çünkü Ruby’de metot isimleri, sayı ile başlayamaz. Talimat haritalandırmasını düzgün yapmamız gerektiğinden, hepsinin başına bir alt çizgi koyarak, isimleri aynı tutabildik. Ha bu arada, talimat isimleri, orjinal CHIP-8 talimat isimleriyle aynı değil, söylemiş olayım. Ama yine de bi’şey farketmiyor.

Peki bundan sonrasında ne olacak? Sınıftan yeni bir örnek oluşturacağız, ROM dosyasını yükleyeceğiz ve döngü ile çalıştıracağız. Hemen public bir metot yazalım:

Bu metodu çağırdığımızda, sonsuza kadar, 0.1 saniye aralıklarla, devir yapıp talimat çalıştıracak. Çalıştığını görebilmek için de, cycle metodunun içine bir puts ekleyip, hangi talimatın çalıştırıldığını görelim:

Son olarak da örneklemeyi yapıp, ROM’u yükleyelim ve çalıştıralım. Ben Pong oyununun ROM’unu kullandım, ROM’ları şuradan indirebilirsiniz. Dosyayı, Ruby dosyasıyla aynı yere koyun.

Eğer herhangi bir yeri kaçırdıysanız, Ruby dosyasının tam hali burada. Dosyayı çalıştırın ve konsol çıktılarına dikkatlice gözatın. Şöyle bir şey çıkacak:

Eğer hiçbir şey yapmazsanız, gerçek bir oyun olduğundan dolayı, asla sonlanmayacak ve sürekli talimatları çalıştıracaktır. Paylaştığım linkteki, PONG dışındaki diğer oyunları da denerseniz benzer sonuçlar elde edeceksiniz.

Bir sonraki yazıda, Gosu kütüphanesini kullanarak, girdi-çıktı sistemini de implemente edip, gerçekten emülatörü oynanabilir hale getireceğim. Gosu kütüphanesinde yaşadığım anormal bir hata yüzüden olmadı, ama garip bir şekilde Python SFML’de düzgün çalışıyor.

Kaynaklar