From: Boris Prinz Date: 2006-07-31T03:12:10+09:00 Subject: [Solution] [QUIZ] Chip-8 (#88) 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)