CHIP-8 Emulator

Spiele, Demos, Grafikzeug und anderes unterhaltendes.
Benutzeravatar
NeoXiD
Beiträge: 6
Registriert: 17.10.2011 18:51
Computerausstattung: http://www.sysprofile.de/id157572
Wohnort: Schweiz
Kontaktdaten:

CHIP-8 Emulator

Beitrag von NeoXiD »

Hallo zusammen :)

Da mich schon seit längerer Zeit die Emulation eines Systems interessiert, habe ich dieses Wochenende beschlossen ein kleines 1-Tages Projekt zu starten. Hierbei handelt es sich um einen CHIP-8 Emulator ( en.wikipedia.org/wiki/CHIP-8 ), welcher sich perfekt für ein erstes Projekt eignet, da er nur über 35 Opcodes verfügt. Das ganze ist natürlich nicht perfektioniert, was aber bei diesem System auch nicht wirklich möglich ist.

Zuerst hier eine RAR mit kostenlosen Spielen (Originalspiele dürfen, wie bei all den Emulatoren für NES/SNES und co. -nicht- mitgeliefert werden): http://www.snapserv.net/dl/CHIP-8_ROMs.rar
All diese Spiele wurden von David Winter entwickelt (http://www.pong-story.com/chip8/) - Wenn also eine kurze Beschreibung zu einem Spiel von Nöten ist, findet man diese teilweise am Anfang der Seite. Der CHIP-8 hatte ein Hex-Keypad und ich habe die Tastenbelegung so gestaltet:
Bild
Nun noch der Source des Emulators, welcher mit ~500 Zeilen meiner Meinung nach eher kurz ist:

Code: Alles auswählen

EnableExplicit
InitSprite()
InitKeyboard()

#EMUSPEED = 3     ; 0 = normal, higher = faster

CompilerIf #PB_Compiler_OS <> #PB_OS_Windows
  Procedure _Beep(D1, D2) : EndProcedure
  #MB_ICONERROR = 0
CompilerEndIf

;- Structures
Structure Chip8System
  ; CPU and memory related
  opcode.u        ; Stored opcode (2bytes unsigned)
  memory.a[4096]  ; Available memory (4096bytes unsigned)
  register.a[16]  ; General-purpose registers (V0 - VE, VF = Carry flag)
  I.u             ; Index register (from 0x000 - 0xFFF)
  pc.u            ; Program counter (from 0x000 - 0xFFF)
  
  ; Graphics and sound related
  gfx.a[64 * 32]  ; 64x32 screen
  delayTimer.a    ; Delay timer 60Hz (can be used by applications)
  soundTimer.a    ; Sound timer 60Hz (beeps when timer reaches one)
  
  ; I/O data (keyboard)
  key.a[16]       ; Hex-based keyboard from 0x0 - 0xF
  
  ; Internal data
  stack.u[16]     ; Stack data (up to 16 levels of nesting)
  sp.a            ; Stack pointer
  
  ; Application data
  refresh.a       ; Refresh screen?
  scrsprite.l     ; Screen sprite
  refreshaddr.l   ; Refresh address (no flicker)
EndStructure

;- Procedure: nonBlockingBeep
Procedure nonBlockingBeep(Frequency.l)
  Beep_(Frequency, 250)
EndProcedure

;- Procedure: chip8_initializeSystem
Procedure chip8_initializeSystem(*self.Chip8System)
  ; Check for object
  If *self = 0 : MessageRequester("Error", "Invalid system object.", #MB_ICONERROR) : EndIf
  
  ; Initialize CPU
  With *self
    \opcode = 0   ; Reset current opcode
    \I = 0        ; Reset input register
    \pc = $200    ; Program counter starts at 0x200
    \sp = 0       ; Reset stack pointer
    \refresh = 1  ; Refresh the screen
  EndWith
  
  ; Clear display, stack, keybuffer and memory
  FillMemory(@*self\gfx, 64 * 32, 0)
  FillMemory(@*self\stack, 32, 0)
  FillMemory(@*self\key, 16, 0)
  FillMemory(@*self\memory, 4096, 0)
  
  ; Load fontset
  CopyMemory(?chip8Fontset, @*self\memory, 80)
  
  ; Reset timers
  *self\delayTimer = 0
  *self\soundTimer = 0
EndProcedure

;- Procedure: chip8_loadROM
Procedure chip8_loadROM(*self.Chip8System, fileName.s)
  Protected fileID.l
  
  ; Check for object
  If *self = 0
    MessageRequester("Error", "Invalid system object.", #MB_ICONERROR)
    End
  EndIf
  
  ; Check if ROM exists
  If FileSize(fileName) < 0
    MessageRequester("Error", "Can not load ROM: " + fileName, #MB_ICONERROR)
    End
  EndIf
  
  ; Check if ROM is too big
  If FileSize(fileName) > 3584
    MessageRequester("Error", "Invalid ROM (too big): " + fileName, #MB_ICONERROR)
  EndIf
  
  ; Read ROM into 0x200
  fileID = ReadFile(#PB_Any, fileName)
  ReadData(fileID, @*self\memory[$200], Lof(fileID))
  CloseFile(fileID)
EndProcedure

;- Procedure: chip8_emulateCycle
Procedure chip8_emulateCycle(*self.Chip8System)  
  ; Fetch next opcode
  *self\opcode = *self\memory[*self\pc] << 8 | *self\memory[*self\pc + 1]
  
  ; Decode opcode
  Select *self\opcode & $F000
    Case $0000  ; RCA1802 chip
      Select *self\opcode & $00FF
        Case $00E0  ; 00E0: Clears the screen
          FillMemory(@*self\gfx, 64 * 32, 0)
          *self\refresh = 1
          *self\pc + 2
        Case $00EE  ; 00EE: Returns from subroutine
          *self\sp - 1
          *self\pc = *self\stack[*self\sp]
          *self\pc + 2
        Default
          MessageRequester("Error", "Invalid opcode [0x0000]: 0x" + RSet(Hex(*self\opcode), 4, "0"), #MB_ICONERROR)
          End
      EndSelect
      
    Case $1000  ; 1NNN: Jumps to address NNN
      *self\pc = *self\opcode & $0FFF
      
    Case $2000  ; 2NNN: Calls subroutine at NNN
      ; Store address in stack
      If *self\sp > 15
        MessageRequester("Error", "Stack overflow.", #MB_ICONERROR)
        End
      EndIf
      *self\stack[*self\sp] = *self\pc
      *self\sp + 1
      
      ; Call subroutine
      *self\pc = *self\opcode & $0FFF
      
    Case $3000  ; 3XNN: Skips the next instruction if VX equals NN
      Define register.a, value.a
      register = (*self\opcode & $0F00) >> 8
      value = *self\opcode & $00FF
      
      ; Check if register VX equals NN
      If *self\register[register] = value
        *self\pc + 4
      Else
        *self\pc + 2
      EndIf
      
    Case $4000  ; 4XNN: Skips the next instruction if VX doesn't equal NN
      Define register.a, value.a
      register = (*self\opcode & $0F00) >> 8
      value = *self\opcode & $00FF
      
      ; Check if register VX doesn't equal NN
      If *self\register[register] <> value
        *self\pc + 4
      Else
        *self\pc + 2
      EndIf
      
    Case $5000  ; 5XY0: Skips the next instruction if VX equals VY
      Define registerX.a, registerY.a
      registerX = (*self\opcode & $0F00) >> 8
      registerY = (*self\opcode & $00F0) >> 4
      
      ; Check if register VX equals VY
      If *self\register[registerX] = *self\register[registerY]
        *self\pc + 4
      Else
        *self\pc + 2
      EndIf
      
    Case $6000  ; 6XNN: Sets VX to NN
      Define register.a, value.a
      register = (*self\opcode & $0F00) >> 8
      value = *self\opcode & $00FF
      *self\register[register] = value
      *self\pc + 2
      
    Case $7000  ; 7XNN: Adds NN to VX
      Define register.a, value.a
      register = (*self\opcode & $0F00) >> 8
      value = *self\opcode & $00FF
      *self\register[register] + value
      *self\pc + 2
      
    Case $8000  ; Arithmethic stuff
      Define registerX.a, registerY.a
      registerX = (*self\opcode & $0F00) >> 8
      registerY = (*self\opcode & $00F0) >> 4
      
      Select *self\opcode & $000F
        Case $0000  ; 8XY0: Sets VX to the value of VY
          *self\register[registerX] = *self\register[registerY]
          *self\pc + 2
          
        Case $0001  ; 8XY1: Sets VX to "VX or VY"
          *self\register[registerX] | *self\register[registerY]
          *self\pc + 2
          
        Case $0002  ; 8XY1: Sets VX to "VX and VY"
          *self\register[registerX] & *self\register[registerY]
          *self\pc + 2
          
        Case $0003  ; 8XY2: Sets VX to "VX xor XY"
          *self\register[registerX] ! *self\register[registerY]
          *self\pc + 2
          
        Case $0004  ; 8XY4: Adds VY to VX - VF is carry
          If *self\register[registerY] > ($FF - *self\register[registerX])
            *self\register[$F] = 1
          Else
            *self\register[$F] = 0
          EndIf
          *self\register[registerX] + *self\register[registerY]
          *self\pc + 2
          
        Case $0005  ; 8XY5: Subtracts VY to VX - VF is borrow
          If *self\register[registerY] > *self\register[registerX]
            *self\register[$F] = 0
          Else
            *self\register[$F] = 1
          EndIf
          *self\register[registerX] - *self\register[registerY]
          *self\pc + 2
          
        Case $0006  ; 8XY6: Shifts VX right by one
          *self\register[$F] = *self\register[registerX] & $1
          *self\register[registerX] >> 1
          *self\pc + 2
          
        Case $0007  ; 8XY5: Sets VX to VY minus VX
          If *self\register[registerX] > *self\register[registerY]
            *self\register[$F] = 0
          Else
            *self\register[$F] = 1
          EndIf
          *self\register[registerX] = *self\register[registerY] - *self\register[registerX]
          *self\pc + 2
          
        Case $000E  ; 8XYE: Shifts VX left by one
          *self\register[$F] = *self\register[registerX] >> 7
          *self\register[registerX] << 1
          *self\pc + 2
          
        Default
          MessageRequester("Error", "Invalid opcode [0x8000]: 0x" + RSet(Hex(*self\opcode), 4, "0"), #MB_ICONERROR)
          End
      EndSelect
      
    Case $9000  ; 9XY0: Skips the next instruction if VX doesn't equal VY
      Define registerX.a, registerY.a
      registerX = (*self\opcode & $0F00) >> 8
      registerY = (*self\opcode & $00F0) >> 4
      
      ; Check if register VX doesn't equal VY
      If *self\register[registerX] <> *self\register[registerY]
        *self\pc + 4
      Else
        *self\pc + 2
      EndIf
      
    Case $A000  ; ANNN: Sets I to the address NNN
      *self\I = *self\opcode & $0FFF
      *self\pc + 2
      
    Case $B000  ; BNNN: Jumps to the address NNN plus V0
      *self\pc = (*self\opcode & $0FFF) + *self\register[0]
      *self\pc + 2
      
    Case $C000  ; CXNN: Sets VX To a random number And NN
      Define register.a, value.a
      register = (*self\opcode & $0F00) >> 8
      *self\register[register] = Random($FF) & (*self\opcode & $00FF)
      *self\pc + 2
      
    Case $D000  ; DXYN: Draws a sprite at coordinate (VX, VY)
      Define X.u, Y.u, height.u, pixel.u
      Define yLine.a, xLine.a
      ; Detect parameters
      X = *self\register[(*self\opcode & $0F00) >> 8]
      Y = *self\register[(*self\opcode & $00F0) >> 4]
      height = *self\opcode & $000F
      
      ; Draw sprite
      *self\register[$F] = 0    ; Reset collision register
      For yLine = 0 To height - 1
        pixel = *self\memory[*self\I + yLine]
        For xLine = 0 To 7
          If (pixel & ($80 >> xLine)) <> 0
            ; Mark collision if pixel is already set
            If (X + xLine + ((Y + yLine) * 64)) > 64 * 32 - 1 : Continue : EndIf
            If (X + xLine + ((Y + yLine) * 64)) < 0 : Continue : EndIf
            If *self\gfx[(X + xLine + ((Y + yLine) * 64))] = 1
              *self\register[$F] = 1
            EndIf
            
            ; Draw pixel
            *self\gfx[X + xLine + ((Y + yLine) * 64)] ! 1
          EndIf
        Next xLine
      Next yLine
      *self\refresh = 1
      *self\pc + 2
      
    Case $E000  ; I/O (keyboard)
      Define key.a
      key = *self\register[(*self\opcode & $0F00) >> 8]
      
      Select *self\opcode & $00FF
        Case $009E: ; EX9E: Skips the next instruction if the key stored in VX is pressed
          If *self\key[key] <> 0
            *self\pc + 4
          Else
            *self\pc + 2
          EndIf
          
        Case $00A1: ; EXA1: Skips the next instruction if the key stored in VX isn't pressed
          If *self\key[key] = 0
            *self\pc + 4
          Else
            *self\pc + 2
          EndIf 
          
        Default
          MessageRequester("Error", "Invalid opcode [0xE000]: 0x" + RSet(Hex(*self\opcode), 4, "0"), #MB_ICONERROR)
          End
      EndSelect
      
    Case $F000  ; Opcode group
      Define register.a
      register = (*self\opcode & $0F00) >> 8
      
      Select *self\opcode & $00FF
        Case $0007  ; FX07: Sets VX to the value of the delay timer
          *self\register[register] = *self\delayTimer
          *self\pc + 2
          
        Case $000A  ; FX0A: A key press is awaited, and then stored in VX
          Define keyPress.a = #False, i.l
          
          For i = 0 To 15
            If *self\key[i] <> 0
              *self\register[register] = i
              keyPress = #True
            EndIf
          Next i
          
          If keyPress <> 0 : *self\pc + 2 : EndIf
         
        Case $0015  ; FX15: Sets the delay timer to VX
          *self\delayTimer = *self\register[register]
          *self\pc + 2
          
        Case $0018  ; FX18: Sets the sound timer to VX
          *self\soundTimer = *self\register[register]
          *self\pc + 2
          
        Case $001E  ; FX1E: Adds VX to I
          If (*self\I + *self\register[register]) > $FFF
            *self\register[$F] = 1
          Else
            *self\register[$F] = 0
          EndIf
          *self\I + *self\register[register]
          *self\pc + 2
          
        Case $0029  ; FX29: Sets I to the location of the sprite (character) in VX
          *self\I = *self\register[register] * $5
          *self\pc + 2
          
        Case $0033  ; FX33: Stores the binary-coded decimal representation of VX at address I
          *self\memory[*self\I] = *self\register[register] / 100
          *self\memory[*self\I + 1] = (*self\register[register] / 10) % 10
          *self\memory[*self\I + 2] = (*self\register[register] % 100) % 10
          *self\pc + 2
          
        Case $0055  ; FX55: Stores V0 to VX in memory starting at address I
          CopyMemory(@*self\register, @*self\memory[*self\I], register + 1)
          *self\I + (register + 2)
          *self\pc + 2
          
        Case $0065  ; FX65: Fills V0 To VX With values from memory at address I
          CopyMemory(@*self\memory[*self\I], @*self\register, register + 1)
          *self\I + (register + 2)
          *self\pc + 2
          
        Default
          MessageRequester("Error", "Invalid opcode [0xF000]: 0x" + RSet(Hex(*self\opcode), 4, "0"), #MB_ICONERROR)
          End
      EndSelect
      
    Default
      MessageRequester("Error", "Invalid opcode: 0x" + RSet(Hex(*self\opcode), 4, "0"), #MB_ICONERROR)
      End
  EndSelect
  
  ; Update timers
  If *self\delayTimer > 0
    *self\delayTimer - 1
  EndIf
  
  If *self\soundTimer > 0
    If *self\soundTimer = 1
      CreateThread(@nonBlockingBeep(), 1000)
    EndIf
    *self\soundTimer - 1
  EndIf
EndProcedure

;- Procedure: chip8_startMainLoop
Procedure chip8_startMainLoop(*self.Chip8System)
  Protected running.a = #True
  Protected windowID.l, windowEvent.l
  Protected xLine.a, yLine.a
  
  ; Check for object
  If *self = 0
    MessageRequester("Error", "Invalid system object.", #MB_ICONERROR)
    End
  EndIf
  
  ; Initialize graphics
  windowID = OpenWindow(#PB_Any, 0, 0, 512, 256, "PChip8", #PB_Window_ScreenCentered | #PB_Window_SystemMenu)
  OpenWindowedScreen(WindowID(windowID), 0, 0, 512, 256, 0, 0, 0, #PB_Screen_NoSynchronization)
  
  ; Create screen sprite
  *self\scrsprite = CreateSprite(#PB_Any, 512, 256)
  
  ; Game-specific anti flicker if supported
  Define antiFlickerRate.l = 50
  
  While running = #True
    ClearScreen(0)
    ExamineKeyboard()
    
    *self\key[$0] = KeyboardPushed(#PB_Key_X)
    *self\key[$1] = KeyboardPushed(#PB_Key_1)
    *self\key[$2] = KeyboardPushed(#PB_Key_2)
    *self\key[$3] = KeyboardPushed(#PB_Key_3)
    *self\key[$4] = KeyboardPushed(#PB_Key_Q)
    *self\key[$5] = KeyboardPushed(#PB_Key_W)
    *self\key[$6] = KeyboardPushed(#PB_Key_E)
    *self\key[$7] = KeyboardPushed(#PB_Key_A)
    *self\key[$8] = KeyboardPushed(#PB_Key_S)
    *self\key[$9] = KeyboardPushed(#PB_Key_D)
    *self\key[$A] = KeyboardPushed(#PB_Key_Z)
    *self\key[$B] = KeyboardPushed(#PB_Key_C)
    *self\key[$C] = KeyboardPushed(#PB_Key_4)
    *self\key[$D] = KeyboardPushed(#PB_Key_R)
    *self\key[$E] = KeyboardPushed(#PB_Key_F)
    *self\key[$F] = KeyboardPushed(#PB_Key_V)
    
    ; Emulate one or multiple cycle(s)
    Define i.l
    For i = 0 To #EMUSPEED : chip8_emulateCycle(*self) : Next i
    
    ; Refresh screen if necessary
    Define keep.a = 0
    If *self\refresh = 1
      StartDrawing(SpriteOutput(*self\scrsprite))
      For yLine = 0 To 31
        For xLine = 0 To 63
          If *self\gfx[xLine + (yLine * 64)] = 1
            Box(xLine * 8, yLine * 8, 8, 8, RGB(255, 255, 255))
          Else
            Define color.l = Point(xLine * 8, yLine * 8)
            If color > RGB(antiFlickerRate, antiFlickerRate, antiFlickerRate)
              Box(xLine * 8, yLine * 8, 8, 8, RGB(Red(color) - antiFlickerRate, Green(color) - antiFlickerRate, Blue(color) - antiFlickerRate))
              keep = 1  
            Else
              Box(xLine * 8, yLine * 8, 8, 8, RGB(0, 0, 0))
            EndIf
          EndIf
        Next xLine
      Next yLine
      StopDrawing()
      If keep = 0 : *self\refresh = 0 : EndIf
    EndIf
    DisplaySprite(*self\scrsprite, 0, 0)
    
    ; Handle window events
    windowEvent = WindowEvent()
    If windowEvent = #PB_Event_CloseWindow
      running = #False
    EndIf

    FlipBuffers()
    Delay(0)
  Wend
EndProcedure

;- Main loop
Define emulator.Chip8System
chip8_initializeSystem(emulator)
chip8_loadROM(emulator, OpenFileRequester("CHIP-8 ROM auswählen", "", "CHIP-8 ROM (*.c8)|*.c8", 0))
chip8_startMainLoop(emulator)

DataSection
  chip8Fontset:
  Data.a $F0, $90, $90, $90, $F0 ; 0
  Data.a $20, $60, $20, $20, $70 ; 1
  Data.a $F0, $10, $F0, $80, $F0 ; 2
  Data.a $F0, $10, $F0, $10, $F0 ; 3
  Data.a $90, $90, $F0, $10, $10 ; 4
  Data.a $F0, $80, $F0, $10, $F0 ; 5
  Data.a $F0, $80, $F0, $90, $F0 ; 6
  Data.a $F0, $10, $20, $40, $40 ; 7
  Data.a $F0, $90, $F0, $90, $F0 ; 8
  Data.a $F0, $90, $F0, $10, $F0 ; 9
  Data.a $F0, $90, $F0, $90, $90 ; A
  Data.a $E0, $90, $E0, $90, $E0 ; B
  Data.a $F0, $80, $80, $80, $F0 ; C
  Data.a $E0, $90, $90, $90, $E0 ; D
  Data.a $F0, $80, $F0, $80, $F0 ; E
  Data.a $F0, $80, $F0, $80, $80 ; F
EndDataSection
Da es bei CHIP-8 keinerlei Opcodes gibt, welche den Frame-Sync in irgendeiner Art und Weise steuern, und auch keinerlei Dokumentation über die originale Taktgeschwindigkeit verfügbar ist, muss die Konstante #EMUSPEED am Anfang je nach Spiel angepasst werden, um ein bestmögliches Resultat zu erzielen. Ausserdem versuchte ich, durch einen leichten "Nachglüh-Effekt" das Flackern etwas einzudämmen, was natürlich trotzdem nicht komplett möglich ist. Das ganze war ausserdem wie bereits erwähnt nur ein Lernprojekt, um jetzt eine etwas grössere Spielekonsole zu emulieren, bzw. dafür einen Emulator zu coden. Danke fürs Lesen und noch einen schönen Abend.
Zuletzt geändert von NeoXiD am 02.07.2012 16:17, insgesamt 1-mal geändert.
Gruss
NeoXiD

Bild
Benutzeravatar
Danilo
-= Anfänger =-
Beiträge: 2284
Registriert: 29.08.2004 03:07

Re: CHIP-8 Emulator

Beitrag von Danilo »

Mir ist ein kleiner Fehler aufgefallen:

Code: Alles auswählen

    *self\key[$E] = KeyboardPushed(#PB_Key_F)
    *self\key[$E] = KeyboardPushed(#PB_Key_V)
Das muss bestimmt $F für #PB_Key_V sein.

Sonst läuft aber alles wunderbar hier.
cya,
...Danilo
"Ein Genie besteht zu 10% aus Inspiration und zu 90% aus Transpiration" - Max Planck
Benutzeravatar
NeoXiD
Beiträge: 6
Registriert: 17.10.2011 18:51
Computerausstattung: http://www.sysprofile.de/id157572
Wohnort: Schweiz
Kontaktdaten:

Re: CHIP-8 Emulator

Beitrag von NeoXiD »

Danilo hat geschrieben:Mir ist ein kleiner Fehler aufgefallen:

Code: Alles auswählen

    *self\key[$E] = KeyboardPushed(#PB_Key_F)
    *self\key[$E] = KeyboardPushed(#PB_Key_V)
Das muss bestimmt $F für #PB_Key_V sein.

Sonst läuft aber alles wunderbar hier.
Oha, vielen Dank für den kleinen Bugreport. Weil so gut wie kein Spiel die Taste "$F" benutzt, fiel mir das ganze gar nicht erst auf. Ist nun fixed :)
Gruss
NeoXiD

Bild
Mesa
Beiträge: 16
Registriert: 03.05.2012 18:23

Re: CHIP-8 Emulator

Beitrag von Mesa »

Für weitere Informationen:
Ich machte einen Emulator chip8 im Dezember 2001 auf der Französisch-Forum.
Dies ist tatsächlich eine Übersetzung eines C-Emulator übersetzt in PureBasic.
(Text von Google übersetzt)

http://www.purebasic.fr/french/viewtopi ... p8#p137132

Mesa.
Antworten