Hi,
here is my solution:
I initialize the registers with zeroes, because it simplifies unit-
testing.
A binary program is unpacked into an array of opcodes (16bit). The
program counter (named "current") counts opcodes, not bytes, so
for a jump instruction the destination address has to be divided by two.
I also wrote a small "disassembler" which dumped the test program as:
000: [6177] V1 = 0x77
002: [6245] V2 = 0x45
004: [7101] V1 = V1 + 0x01
006: [8320] V3 = V2
008: [8121] V1 = V1 | V2
00A: [8122] V1 = V1 & V2
00C: [8233] V2 = V2 ^ V3
00E: [8134] V1 = V1 + V3
010: [8235] V2 = V2 - V3
012: [8106] V1 = V1 >> 1
014: [8327] V3 = V2 - V3
016: [830E] V3 = V3 << 1
018: [64FF] V4 = 0xFF
01A: [C411] V4 = rand() & 0x11
01C: [32BB] skip next if V2 == 0xBB
01E: [1000] goto 000
020: [0000] exit
(Almost looks like ruby code. We really need a "goto" in ruby ;-)
Regards,
Boris
### chip8_emu.rb
class Chip8Emulator
attr_accessor :register
VF = 15 # index of the carry/borrow register
def initialize
@register = [0] * 16 # V0..VF
end
def exec(program)
opcodes = program.unpack('n*')
current = 0 # current opcode index
loop do
opcode = opcodes[current]
# these are needed often:
x = opcode >> 8 & 0x0F
y = opcode >> 4 & 0x0F
kk = opcode & 0xFF
nnn = opcode & 0x0FFF
case opcode >> 12 # first nibble
when 0 then return
when 1 then current = (nnn) / 2 and next
when 3 then current += 1 if @register[x] == kk
when 6 then @register[x] = kk
when 7 then add(x, kk)
when 8
case opcode & 0x0F # last nibble
when 0 then @register[x] = @register[y]
when 1 then @register[x] |= @register[y]
when 2 then @register[x] &= @register[y]
when 3 then @register[x] ^= @register[y]
when 4 then add(x, @register[y])
when 5 then subtract(x, x, y)
when 6 then shift_right(x)
when 7 then subtract(x, y, x)
when 0xE then shift_left(x)
else raise "Unknown opcode: " + opcode.to_s(16)
end
when 0xC then random(x, kk)
else raise "Unknown opcode: " + opcode.to_s(16)
end
current += 1 # next opcode
end
end
def add(reg, value)
result = @register[reg] + value
@register[reg] = result & 0xFF
@register[VF] = result >> 8 # carry
end
def subtract(reg, a, b)
result = @register[a] - @register[b]
@register[reg] = result & 0xFF
@register[VF] = - (result >> 8) # borrow
end
def shift_right(reg)
@register[VF] = @register[reg] & 0x01
@register[reg] >>= 1
end
def shift_left(reg)
@register[VF] = @register[reg] >> 7
@register[reg] = (@register[reg] << 1) & 0xFF
end
def random(reg, kk)
@register[reg] = rand(256) & kk
end
# show all registers
def dump
0.upto(VF) do |reg|
printf("V%1X:%08b\n", reg, @register[reg])
end
end
end
if $0 == __FILE__
ARGV.each do |program|
emu = Chip8Emulator.new
emu.exec(File.read(program))
emu.dump
end
end
### test_chip8_emu.rb
require 'test/unit'
require 'chip8_emu'
class Chip8EmulatorTest < Test::Unit::TestCase
def setup
@emu = Chip8Emulator.new
end
def test_init
assert_equal [0] * 16, @emu.register
end
def test_set_register
@emu.exec("\x60\x42" + "\x63\xFF" + "\x6F\x66" + "\0\0")
assert_equal [66, 0, 0, 255] + [0]*11 + [102], @emu.register
end
def test_jump
@emu.exec("\x10\x04" + "\x00\x00" + "\x60\x42" + "\0\0")
assert_equal [66] + [0]*15, @emu.register
end
def test_skip_next
@emu.exec("\x60\x42" + "\x30\x42" + "\x60\x43" + "\0\0")
assert_equal [66] + [0]*15, @emu.register
end
def test_add_const
@emu.exec("\x60\xFF" + "\x70\x01" + "\0\0")
assert_equal [0]*15 + [1], @emu.register
end
def test_copy
@emu.exec("\x60\x42" + "\x81\x00" + "\0\0")
assert_equal [66]*2 + [0]*14, @emu.register
end
def test_or
@emu.exec("\x60\x03" + "\x61\x05" + "\x80\x11" + "\0\0")
assert_equal [7, 5] + [0]*14, @emu.register
end
def test_and
@emu.exec("\x60\x03" + "\x61\x05" + "\x80\x12" + "\0\0")
assert_equal [1, 5] + [0]*14, @emu.register
end
def test_xor
@emu.exec("\x60\x03" + "\x61\x05" + "\x80\x13" + "\0\0")
assert_equal [6, 5] + [0]*14, @emu.register
end
def test_add
@emu.exec("\x60\x01" + "\x61\x01" + "\x80\x14" + "\0\0")
assert_equal [2, 1] + [0]*14, @emu.register
end
def test_subtract
@emu.exec("\x60\x00" + "\x61\x01" + "\x80\x15" + "\0\0")
assert_equal [255, 1] + [0]*13 + [1], @emu.register
end
def test_subtract2
@emu.exec("\x60\x01" + "\x61\x02" + "\x80\x17" + "\0\0")
assert_equal [1, 2] + [0]*14, @emu.register
end
def test_shift_right
@emu.exec("\x60\xFF" + "\x80\x06" + "\0\0")
assert_equal [0x7F] + [0]*14 + [1], @emu.register
end
def test_shift_left
@emu.exec("\x60\xFF" + "\x80\x0E" + "\0\0")
assert_equal [0xFE] + [0]*14 + [1], @emu.register
end
def test_rand
srand 0
first_rand = rand(256)
srand 0
@emu.exec("\xC0\x0F" + "\0\0")
assert_equal [first_rand & 0x0F] + [0]*15, @emu.register
end
end
### chip8_asm.rb
class Chip8Disassembler
CODES = {
/0000/ => 'exit',
/1(...)/ => 'goto \1',
/3(.)(..)/ => 'skip next if V\1 == 0x\2',
/6(.)(..)/ => 'V\1 = 0x\2',
/7(.)(..)/ => 'V\1 = V\1 + 0x\2',
/8(.)(.)0/ => 'V\1 = V\2',
/8(.)(.)1/ => 'V\1 = V\1 | V\2',
/8(.)(.)2/ => 'V\1 = V\1 & V\2',
/8(.)(.)3/ => 'V\1 = V\1 ^ V\2',
/8(.)(.)4/ => 'V\1 = V\1 + V\2',
/8(.)(.)5/ => 'V\1 = V\1 - V\2',
/8(.)06/ => 'V\1 = V\1 >> 1',
/8(.)(.)7/ => 'V\1 = V\2 - V\1',
/8(.)0E/ => 'V\1 = V\1 << 1',
/C(.)(..)/ => 'V\1 = rand() & 0x\2',
}
def self.code2text hexcode
CODES.each do |re, subs|
if hexcode =~ re
return hexcode.sub(re, subs)
end
end
'???'
end
def self.dump binary
opcodes = binary.unpack "n*"
opcodes.each_with_index do |code, waddr|
code_hex = "%04X" % code
printf("%03X: [%s] %s\n", waddr*2, code_hex, code2text
(code_hex));
end
end
end
binary = File.read(ARGV[0])
Chip8Disassembler.dump(binary)