Sei sulla pagina 1di 74

Making a Gameboy

Emulator in Ruby

Colby Swandale
0xColby

What is an
emulator?

In computing, an emulator is
hardware or software that
enables one computer system
(called the host) to behave like
another computer system (called
the guest). An emulator typically
enables the host system to run
software or use peripheral
devices designed for the guest
system.
https://en.wikipedia.org/wiki/Emulator

Nintendo Gameboy
Developed

by Nintendo Japan
Released April 1989
Sold 118.69 million units (includes GBC)
Featured Games: Tetris, Super Mario
Land, Pokemon Red & Blue
LCD Monochrome display (160x144
pixels)
15 Hours Battery Life

CPU PPU Memory Cartridge

CPU

CPU

Sharp LR35902 (made specifically for the GB)


4.19Mhz clockspeed
8 bit processor
16 bit memory bus
Similar to the Zilog z80 & Intel 8080 processor

Registers

CPU: Registers
A

L
SP
PC

Store values to be operated on and


stores results of operations

Very quick to read/write

Physically stored in the CPU

Have general and special purposes

A - L: 1 byte

SP, PC: 2 bytes

CPU

class CPU
end

CPU

class CPU
def initialize
@a, @b, @c, @d, @e, @h, @l, @f = 0x00
@pc, @sp = 0x0000
end
end

Instructions

CPU: Instructions

LD A, B

ADD A,B

SUB D

AND B

XOR B

OR H

RLA

DEC BC

PUSH HL

CALL 0x2BC6

NOP

LD D,0x15

LD 0x15,A

POP BC

EI

HALT

CPU: Opcode Table


0

LD A, B

ADD A,B

SUB D

AND B

XOR B

OR H

RLA

DEC BC

PUSH HL

CALL 0x2BC6

NOP

LD D,0x15

LD 0x15,A

POP BC

EI

HALT

CPU: Opcode Table

class CPU
OPCODE = [
:nop, :ld_bc_d16, :ld_bc_a, :inc_bc,
:inc_b, :dec_b, :ld_b_d8, :rlca, :ld_a16_sp,
:add_hl_bc, :ld_a_bc, :dec_bc, :inc_c, :dec_c,
:ld_c_d8, :rrca, :stop_0, :ld_de_d16, :ld_de_a,
:inc_de, :inc_d, :dec_d, :ld_d_d8, :rla, :jr_r8,
:add_hl_de, :ld_a_de, :dec_de, :inc_e, :dec_e,
:ld_e_d8, :rra,
...
end

CPU: LD B,C

class CPU
def ld_b_c
@b = @c
end
end

CPU: INC B

class CPU
def inc_b
result = @b + 1
@b = result & 0xFF
end
end

CPU: LD C,d8

class CPU
def ld_c_d8
@c = $mmu[@pc]
@pc += 1
end
end

Fetch and Execute

CPU: Fetch And Execute

Memory
Read Byte
Program
Counter

Fetch Next
Instruction

Interpret
Instruction

Execute
Instruction

CPU: Tick

class CPU
def tick
operation_index = $mmu[@pc]
@pc += 1
self.public_send OPCODE[operation_index]
end
end

CPU: Timing
Instruction

Cycles

NOP

LD A,A

CALL (a16)

16

AND (d8)

INC D

CPU: Timing

class CPU
def tick
operation_index = $mmu[@pc]
@pc += 1
self.public_send OPCODE[operation_index]
@cycles = OPCODE_TIMING[operation_index]
end
end

Memory

Controlled

by the Memory
Management Unit
64 KB Storage
65,535 (0xFFFF) address space

MMU: Memory Map

0x0

0xFFFF
Game Program

Video

General

IO

Memory Management Unit

class MMU
MEMORY_SIZE = 65_536 # addresses
def initialise(game_program)
@game_program = game_program
@memory = Array.new MEMORY_SIZE, 0
end
end

MMU
class MMU
def [](i)
case i
when 0x0000...0x8000 # ROM Bank 0 + n
# read from cartridge
when 0x8000...0xA000 # Video RAM
@memory[i]
when 0xA000...0xC000 # RAM Bank
# read from cartridge
when 0xC000..0xFFFF # RAM, Sprites, IO, Stack
@memory[i]
end
end
end

MMU
class MMU
def []=(i, v)
case i
when 0x0000...0x8000 #
# write to cartridge
when 0x8000...0xA000 #
@memory[i] = v
when 0xA000...0xC000 #
# write to cartridge
when 0xC000..0xFFFF #
@memory[i] = v
end
end
end

ROM Bank 0 + n
Video RAM
RAM Bank
RAM, Sprites, IO, Stack

MMU

$mmu = MMU.new

Picture
Processing Unit

GPU: Reading Lines

GPU: Memory

0x0

0xFFFF
Game Program

Video

General

IO

PPU

class PPU
FRAMEBUFFER_SIZE = 23_040 # 160 x 144 (screen size)
def initialize
@framebuffer = Array.new FRAMEBUFFER_SIZE, 0
@mode = :vertical_blank
@modeclock = 0
end
end

PPU: Modes

Sprite Read

Video Read

Horizontal Blank

Vertical Blank

PPU: Modes

Sprite Read

Video Read

Horizontal Blank

Vertical Blank

PPU: Modes

Sprite Read

Video Read

Horizontal Blank

Vertical Blank

PPU: Modes

Sprite Read

Video Read

Horizontal Blank

Vertical Blank

PPU: Modes

Sprite Read

Video Read

Horizontal Blank

Vertical Blank

PPU: Modes
class PPU
def tick(cycles)
@modeclock += cycles
case @mode
when :horizontal_blank
hblank if @modeclock >= 80
when :vertical_blank
vblank if @modeclock >= 172
when :sprite_read
oam if @modeclock >= 204
when :vram_read
vram if @modeclock >= 4560
end
end
end

Tile System

PPU: Tile System

Not

enough memory for


a frame buffer
8x8 pixels = 1 title

PPU: Tile System


0

Screen

Screen

160 x 144 Display


60hz refresh rate
Monochrome display (black,
white, light grey, dark grey)

Screen
module Waterfoul
class Screen
def initialize
SDL.InitSubSystem SDL::INIT_VIDEO
@buffer = FFI::MemoryPointer.new :uint32, SCREEN_WIDTH * SCREEN_HEIGHT
@window = SDL.CreateWindow 'waterfoul', 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, SDL::SDL_WINDOW_RESIZABLE
@renderer = SDL.CreateRenderer @window, -1, 0
SDL.SetHint "SDL_HINT_RENDER_SCALE_QUALITY", "2"
SDL.RenderSetLogicalSize @renderer, WINDOW_WIDTH, WINDOW_HEIGHT
@texture = SDL.CreateTexture @renderer, SDL::PIXELFORMAT_ARGB8888, 1, SCREEN_WIDTH, SCREEN_HEIGHT
end
def render(framebuffer)
@buffer.write_array_of_uint32 framebuffer
SDL.UpdateTexture @texture, nil, @buffer, SCREEN_WIDTH * 4
SDL.RenderClear @renderer
SDL.RenderCopy @renderer, @texture, nil, nil
SDL.RenderPresent @renderer
end
end
end

Cartridge

Cartridge

29 different cartridge types


Up to 2Mb ROM space
Up to 32Kb of external memory
supports external hardware i.e: RTC, RAM,
Rumble
Controlled by the Memory Bank Controller

Cartridge: Memory

64kb
Available RAM

Game Program
0x0

0xFFFF

0xFFFFFF

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

16KB

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

Cartridge: Memory

Game Program

Video

General

IO

Cartridge: Memory

0x0

0x4000
Bank 0

0x8000
Bank n

Cartridge: Banking
0x0000 0x4000 0x4001 0x8000

Program
Read Byte

Read
Byte
MMU

Cartridge
Controller

10

11

12

13

14

15

16

17

18

19

20

21

22

23

Cartridge: Banking
0x0000 0x4000

Program
Write Byte

Change
Bank

Write
Byte
MMU

Cartridge
Controller

12

13

14

15

16

17

18

19

20

21

22

23

10
11
0x4001 0x8000

require 'forwardable'
class Cartridge
extend Forwardable
CARTRIDGE_TYPE_MEM_LOC = 0x147
def_delegators :@mbc, :[], :[]=
def initialize(program)
cartridge_type = program[CARTRIDGE_TYPE_MEM_LOC]
@mbc = cartrdige_controller(cartridge_type, program)
end
def cartrdige_controller type, rom
controller_const(type).new rom
end
end

class Cartridge
def controller_const(controller_byte)
case controller_byte
when 0x00, 0x8, 0x9
MBC::ROM
when 0x1, 0x2, 0x3
MBC::MBC1
when 0x5, 0x6
MBC::MBC2
when 0xF, 0x10, 0x11, 0x12, 0x13
MBC::MBC3
when 0x15, 0x16, 0x17
MBC::MBC4
when 0x19, 0x1B, 0x1C, 0x1D, 0x1E
MBC::MBC5
end
end
end

Cartridge

class MBC::MBC1
EXTERNAL_RAM_SIZE = 0x2000
def initialize(program)
@rom_bank = 1
@ram_bank = 1
@game_program = program
@ram = Array.new EXTERNAL_RAM_SIZE, 0
end
end

Cartridge
class MBC::MBC1
def [](i)
case i
when 0x0...0x4000 # ROM Bank 0
@game_program[i]
when 0x4000...0x8000 # ROM Bank n
addr = i - 0x4000
offset = @rom_bank * 0x4000
@game_program[offset + addr]
end
end
end

Cartridge
class MBC::MBC1
def []=(i,v)
case i
when 0x2000...0x4000
@rom_bank = v
when 0x4000...0x6000
@ram_bank = v
when 0xA000...0xC000
offset = @ram_bank * 0x8000
@ram[offset + addr] = v
end
end
end

Updating the
MMU

Updating MMU

class MMU
MEMORY_SIZE = 65536 # bytes
def initialise(game_program)
@memory = Array.new MEMORY_SIZE, 0
@cartridge = Cartridge.new game_program
end
end

Updating MMU
class MMU
def [](i)
case i
when 0x0000...0x8000
@cartridge[i]
when 0x8000...0xA000
@memory[i]
when 0xA000...0xC000
@cartridge[i]
when 0xC000..0xFFFF
@memory[i]
end
end
end

# ROM Bank 0 + n
# Video RAM
# RAM Bank
# RAM, Sprites, IO, Stack

Bringing Everything
Together

Emulator

class Emulator
end

Emulator

class Emulator
def initialize
@cpu = CPU.new
@ppu = PPU.new
@screen = Screen.new
$mmu = MMU.new
end
end

class Emulator
def initialize(rom_path)
game_program = File.binread(rom_path).bytes
@cpu = CPU.new
@ppu = PPU.new
@screen = Screen.new
$mmu = MMU.new(game_program)
end
def run
loop do
@cpu.tick
@ppu.tick @cpu.cycles
@screen.render @ppu.framebuffer if @gpu.can_render?
end
end
end

What I Didnt
Talk About

Input Controls

Memory Registers
CLI

Interrupts
Link Cable

Timer

Sound

SDL
Boot ROM

colby-swandale/waterfoul

Thank You!

Sources
Gameboy Opcode Table: http://www.pastraiser.com/cpu/gameboy/
gameboy_opcodes.html
Gameboy CPU Manual: http://marc.rawer.de/Gameboy/Docs/GBCPUman.pdf
Gameboy Pandocs: http://marc.rawer.de/Gameboy/Docs/GBCPUman.pdf

Potrebbero piacerti anche