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:
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