[lnkForumImage]
TotalShareware - Download Free Software

Confronta i prezzi di migliaia di prodotti.
Asp Forum
 Home | Login | Register | Search 


 

Forums >

comp.lang.ruby

[QUIZ] Chip-8 (#88

James Gray

7/28/2006 5:50:00 PM

The three rules of Ruby Quiz:

1. Please do not post any solutions or spoiler discussion for this quiz until
48 hours have passed from the time on this message.

2. Support Ruby Quiz by submitting ideas as often as you can:

http://www.rub...

3. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps everyone
on Ruby Talk follow the discussion. Please reply to the original quiz message,
if you can.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

by Ethan Price

CHIP-8 was an interpreted language used in the 1970's for basic games like Pong.
While it is technically an interpreted language, it defines many components a
real computer has, such as registers and a processor. Today, like many other
gaming consoles, it is kept alive by emulators allowing it to be played on most
any modern computer. Emulation is where raw operation codes (the most low-level
way of feeding instructions to a computer) are translated by another computer,
giving a totally different system the ability to run programs not designed for
it.

Your job is to make an emulator. It will only cover some basic parts of it, but
all emulators have to start somewhere. I have written a simple program, and will
give you a list of all the opcodes I used and what they are supposed to do. We
won't worry about graphics right now, just simple operations. Four things need
to be done:

1. Make the emulator read the file. All opcodes are four digit hex numbers,
but those will need to be split.
2. Set up the registers and other assorted things so they can be modified.
3. Interpret the function of the opcode in Ruby.
4. Dump all the registers so you can check and make sure everything went well.

Like I said, all opcodes are four digit long hex numbers. So you read four hex
digits worth of data, process it, then move on to the next four digits. There
are 16 registers (areas where data can be stored). They are named V0 through VF,
and are all eight bits wide. VF is usually used as a carry for math operations.

Here is the list of opcodes you will need to interpret:

NNN is an address
KK is an 8 bit constant
X and Y are two 4 bits constants

0000 This is nothing. If you see this, it should mean that you are at the end
of the file and have nothing left to read, so you therefore need to exit
gracefully.

1NNN Jump to the address NNN of the file

3XKK Skip next instruction if VX == KK

6XKK VX = KK

7XKK VX = VX + KK (Note: The documentation does not mention this, but I would
assume that VF would act as a carry if needed. The
included program will not need a carry for this
function, so providing for one is purely optional.)

8XY0 VX = VY

8XY1 VX = VX OR VY

8XY2 VX = VX AND VY

8XY3 VX = VX XOR VY

8XY4 VX = VX + VY, Sets VF to 1 if there is a carry, 0 if there isn't

8XY5 VX = VX - VY, VF is set to 0 if there is a borrow, 1 if there isn't.

8X06 VX = VX SHIFT RIGHT 1 (VX=VX/2), VF is set to the value of the least
significant bit of VX before the shift.

8XY7 VX = VY - VX, VF is set to 0 if there is a borrow, 1 if there isn't.

8X0E VX = VX SHIFT LEFT 1 (VX=VX*2), VF is set to the value of the most
significant bit of VX before the shift.

CXKK VX = Random number AND KK

Lets explain how to read these opcodes. Take opcode 1NNN for example. Since the
1NNN starts with 1, we know that no matter what comes after it we will be doing
a jump instruction. Same with the other codes. For the opcode 8XY2, we know if
an opcode starts with 8 and ends in 2 it will be the operation "VX = VX AND VY".

The variables NNN, KK, X and Y are a little different. They can represent any
hex number (0-F), and also are used to note how big of chunks to read them in.
For NNN, you will want to read those as a three digit hex number. Lets do an
example opcode (in hex): 1234. Since it starts with 1, we know it is a "Jump to
address NNN" command. The three numbers after that (the NNN part), are 234, so
we know to jump to location 234 in the file (this is a hex number, not decimal).
KK and X/Y are similar, just for different sized chunks.

While I'm on the subject, let's talk about addresses in detail. Your current
address in the file is how many bytes you are from the beginning of the file. So
if I am at address 008 in the file, I am eight bytes away from the beginning of
the file, and therefore the next byte I read will be byte nine. Say we had the
opcode 100F, that means we would jump to after byte 16 (it is a hex remember).

If your wondering how the carries work, here are some examples. First off,
addition. Say we add these two numbers (opcode 8xy4):

11111111 <--This is VX
+
00000001 <--This is VY

Obviously this would equal 100000000, but that is too big for one register. So
what we do is make VF equal 00000001, and then roll over to 00000000. Sort of
like an old car odometer. When it hits 99,999 miles, it goes back to 00,000
miles, except we are keeping track of the fact that it rolled over. Subtraction
is similar (opcode 8xy5):

00000000 <-- This is VX
-
00000001 <-- This is VY

Since the registers can only deal with positive numbers, we have to keep it
positive. So what we do is "borrow", making VX equal to 100000000. All of a
sudden our subtraction works:

100000000 <--VX
-
00000001 <--VY
=
11111111

If we do this though, we have to make sure VF is zero to indicate that a borrow
was needed. If a borrow was not needed, VF gets set to 00000001, since you
didn't need the extra.

Now for shifts. To sum it up, if you shift left, the far left number gets taken
away, and a zero added to the far right. So 10101111 would become 01011110,
because we got rid of the far left number and added a zero to the far right.
Shift right is the same except going the other way (get rid of far right number,
add 0 to the left). As you can see, a number disappears when you shift. VF is
set to the number that disappears (obviously it can only be a 1 or 0).

A full list of opcodes can be had at David Winter's page, under section 1.5:

http://www.pdc.kth.se/%7Elfo/chip8...

He also has a full explanation of everything, so if check there for more
details. If you want to write code for an opcode that I didn't list, by all
means go for it.

Be sure to start reading the attached program from the beginning. To my
understanding in a real Chip-8 game up to address 200 was reserved for machine
code which we wont worry about.

The register values you should get are:

V1:01000101
V2:10111011
V3:11101100
V4:this number should be random, so do multiple runs to make sure it changes
VF:00000000

A note: If everything is working as it is supposed to, you should run straight
through the file once. If you are looping, I would start with double checking
all your math operations.

Good Luck!

16 Answers

James Gray

7/28/2006 5:58:00 PM

0

On Jul 28, 2006, at 12:50 PM, Ruby Quiz wrote:

> by Ethan Price
>
> CHIP-8 was an interpreted language used in the 1970's for basic
> games like Pong.

I know this quiz is a bit longer (to read) than we normally prefer,
it is not hard however and I encourage people to try it. I solved it
myself in just over 100 lines of code.

The length mainly comes from me pestering Ethan to clarify details,
which he did a great job of.

James Edward Gray II


James Gray

7/28/2006 6:55:00 PM

0

On Jul 28, 2006, at 12:50 PM, Ruby Quiz wrote:

> I have written a simple program

It was just pointed out to me that I didn't attach the program. You
can find it at:

http://www.rubyquiz.com...

James Edward Gray II


Jacob Fugal

7/28/2006 9:41:00 PM

0

On 7/28/06, Daniel Martin <martin@snowplow.org> wrote:
> > While I'm on the subject, let's talk about addresses in detail. Your
> > current address in the file is how many bytes you are from the
> > beginning of the file. So if I am at address 008 in the file, I am
> > eight bytes away from the beginning of the file, and therefore the
> > next byte I read will be byte nine. Say we had the opcode 100F, that
> > means we would jump to after byte 16 (it is a hex remember).
>
> I'm not sure you mean this. I think you mean either "jump to before
> the 16th byte" or meant to use opcode 1010.
>
> Looking at your program, I see an easy way to make this explanation
> easier:
>
> Opcode 0100 will send you back to the beginning, so that the next
> instruction read and executed is the first instruction of the file.
> Opcode 0102 will send you back to the second instruction in the file
> (remember that instructions are four hex digits - or two bytes -
> long). Other addresses follow.

I think you mean 1000 and 1002 in the description above. :)

So, should an absolute jump to N make it so that the Nth (0-based
index) instruction is the next executed, or the (N+1)th? You're
example above seems to put it both ways. 1000 sends you back to the
beginning, so the "0th" instruction (first instruction in the file) is
the next executed. But then 1002 should make it so that the "2nd"
instruction (third instruction in the file) is the next executed,
right? Maybe this is what you meant by "second" anyways...

Jacob Fugal

James Gray

7/28/2006 10:26:00 PM

0

On Jul 28, 2006, at 3:15 PM, Daniel Martin wrote:

>> 2. Set up the registers and other assorted things so they can be
>> modified.
>
> I'll note that the spec. nowhere tells us what the initial, power-on
> values of the registers are. My personal recommendation is that
> people intialize the registers with random values, so that bad chip-8
> programs would be caught. As an addition, it might be nice to accept
> command line arguments to initialize the registers.

I would think nils would be more likely to blow up with a problem in
the later math operations.

James Edward Gray II

Jacob Fugal

7/28/2006 11:11:00 PM

0

On 7/28/06, Ethan Price <nahteecirp@gmail.com> wrote:
> On 7/28/06, Jacob Fugal <lukfugl@gmail.com> wrote:
> > On 7/28/06, Daniel Martin <martin@snowplow.org> wrote:
> > > Opcode 0100 will send you back to the beginning, so that the next
> > > instruction read and executed is the first instruction of the file.
> > > Opcode 0102 will send you back to the second instruction in the file
> > > (remember that instructions are four hex digits - or two bytes -
> > > long). Other addresses follow.
> >
> > So, should an absolute jump to N make it so that the Nth (0-based
> > index) instruction is the next executed, or the (N+1)th? You're
> > example above seems to put it both ways. 1000 sends you back to the
> > beginning, so the "0th" instruction (first instruction in the file) is
> > the next executed. But then 1002 should make it so that the "2nd"
> > instruction (third instruction in the file) is the next executed,
> > right? Maybe this is what you meant by "second" anyways...
>
> Here is how it is supposed to work. Say we have a file that reads in hex:
> 1234ABCD5678
> Opcode 1000 would send us to before 1234, so after jumping the first
> instruction we would read is 1234. Opcode 1002 would send us to after 1234
> and before ABCD (were moving forward 2 bytes, which equals 4 hex digits
> worth of data). 1004 would send us to after ABCD and before 5678, and so on.

Ah, gotcha. I was thinking in instruction addressing, rather than
memory addressing. That probably reflects the fact that I read and
decompile all the instructions before beginning execution. This makes
sense now. So, regarding the initial quiz description, 100F would send
us F (15) bytes forward from the start of the file, or only 7.5
instructions, and you'd have your instruction pointer in the middle of
an instruction. Is that valid? Or was it indeed a typo as Daniel
suggested?

Jacob Fugal

Boris Prinz

7/30/2006 6:12:00 PM

0

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)



Alexandru E. Ungur

7/30/2006 6:36:00 PM

0

Hi,

This one was a lot of fun :)
Not that the others weren't but I must admit I forgot
how much I liked assembler... Thanks for reminding me :)

Now, once the emulator was done, wasn't too much fun to
watch the same program over and over, so I wrote a small
'assembler' to be able to create programs as well :)

I rewrote the Chip8Test file as well in this pseudo
assembler, then 'compiled' it back, and the run it.
I also added a few more opcodes, almost all except those
used for the actual graphical part.

The files are:
chip8.rb - the emulator
cc8.rb - the 'assembler'
Chip8Test.c8 - the original test program, in 'source' :)
AnotherTest.c8 - another test program
cc8-quickref.txt - a quick reference to the 'assembly' lang
used in the .c8 tests files.

The way it works is:
ruby cc8.rb Chip8Test.c8 will create a Chip8Test.bin file
which can then be run with chip8.rb

Great quiz!!

Have a nice day everyone,
Alex

Sander Land

7/30/2006 6:55:00 PM

0

I implemented pretty much all the opcodes and used Tk to display the
graphics as curses didn't work on my machine.
It can run the games from the site James posted
(http://www.pdc.kth.se/%7Elfo/chip8/CH...).
I used one Win32 function (GetKeyState) as I didn't know a compatible
way to check if a key is pressed.

Controls: q for quit, normal controls mapped just like on the site
with the explanation of the opcodes.

I also put the code on Pastie, as Gmail will probably mess up my code
(again): http://pastie.cab...



require 'enumerator'
require 'tk'

require 'Win32API'
keystate = Win32API.new("user32", "GetKeyState", 'I', 'I')
# virtual keycodes, mapping keys like
http://www.pdc.kth.se/%7Elfo/chip8... does
VK = {'q' => 81, 0 => 110, 1 => 103, 2 => 104, 3 => 105, 4 => 100, 5
=> 101, 6 => 102, 7 => 97, 8 => 98, 9 => 99, 0xA => 96, 0xB => 13, 0xC
=> 111, 0xD => 106, 0xE => 109, 0xF => 107}

$key_pressed = proc{|key|
keystate.call(VK[key]) & ~1 != 0 # higher bit set = key currently pressed
}


class Integer
def lo; self & 0xF end # low nibble of byte
def hi; (self >> 4).lo end # high nibble of byte
def bcd_byte
[self / 100, (self/10) % 10, self % 10].map{|n| n.chr}.join
end
end

class Chip8Emulator
COLORS = ['#000000','#ffffff']
FONT_TO_BIN = {' ' => 0,'#' => 1}
FONT = ["#### # #### #### # # #### #### #### #### #### ## ###
### ### #### #### ",
"# # ## # # # # # # # # # # # # # # #
# # # # # ",
"# # # #### ### #### #### #### # #### #### #### ###
# # # ### ### ",
"# # # # # # # # # # # # # # # # #
# # # # # ",
"#### ### #### #### # #### #### # #### #### # # ###
### ### #### # "
].map{|l| l.split('').enum_slice(5).to_a }.transpose.map{|lines|
lines.map{|line| (line.map{|c| FONT_TO_BIN[c] }.join +
'000').to_i(2).chr }.join
}.join # FONT is now the encoded font: 5 bytes for 0, 5
bytes for 1, etc total 80 bytes

def initialize(code)
@ip = 0x200
@mem = FONT + "\0" * (@ip - 80) + code + "\0" * 4096 # ensure
4kb mem + program at start
@regs = "\0" * 160
@I = 0
@stack = []
init_screen
end

def +(a,b)
a += b
@regs[0xF] = a > 0xFF ? 1 : 0
a & 0xFF
end

def -(a,b)
a -= b
@regs[0xF] = a < 0 ? 0 : 1
(a + 0x100) & 0xFF
end

def get_keypress
sleep 0.01 until k = (0..0xF).find{|k| $key_pressed.call(k) }
k
end

def init_screen
@screen = Array.new(32) {Array.new(64,0) }
@img_screen = TkPhotoImage.new('width'=>64,'height'=>32)
@img_screen.put( Array.new(32) {Array.new(64,COLORS[0]) } )
update_screen
end

def draw_sprite(x,y,size)
x %= 64
y %= 32
@regs[0xF] = 0
img_update = Array.new(size){ Array.new(8) }
@mem[@I,size].split('').each_with_index{|b,dy|
ypos = (y+dy) % 32
(0..7).each{|dx|
chr = b[0][7-dx]
xpos = (x+dx) % 64
@regs[0xF] = 1 if(chr==1 && @screen[ypos][xpos]==1) # collision
col = @screen[ypos][xpos] ^= chr
img_update[dy][dx] = COLORS[col]
}
}
@img_screen.put(img_update, :to => [x,y] )
update_screen
end

def update_screen
$tkscreen.copy(@img_screen,:zoom=>10)
end

def fetch
@instr = @mem[@ip,2]
@ip += 2
end

def execute
x = @instr[0].lo
y = @instr[1].hi
opt = @instr[1].lo
kk = @instr[1]
nnn = @instr[0].lo << 8 | @instr[1]
case @instr[0].hi
when 0
case kk
when 0xE0 then init_screen
# 00E0 Erase the screen
when 0xEE then @ip = @stack.pop
# 00EE Return from a CHIP-8 sub-routine
else return nil
end
when 1 then @ip = nnn
# 1NNN Jump to the address NNN of the file
when 2
# 2NNN Call CHIP-8 sub-routine at NNN
@stack.push @ip
@ip = nnn
when 3 then @ip += 2 if @regs[x] == kk
# 3XKK Skip next instruction if VX == KK
when 4 then @ip += 2 if @regs[x] != kk
# 4XKK Skip next instruction if VX != KK
when 5 then @ip += 2 if @regs[x] == @regs[y]
# 5XY0 Skip next instruction if VX == VY
when 6 then @regs[x] = kk
# 6XKK VX = KK
when 7 then @regs[x] = self.+(@regs[x],kk)
# 7XKK VX = VX + KK
when 8
case opt
when 0 then @regs[x] = @regs[y]
# 8XY0 VX = VY
when 1 then @regs[x] |= @regs[y]
# 8XY1 VX = VX OR VY
when 2 then @regs[x] &= @regs[y]
# 8XY2 VX = VX AND VY
when 3 then @regs[x] ^= @regs[y]
# 8XY3 VX = VX XOR VY
when 4 then @regs[x] = self.+(@regs[x],@regs[y])
# 8XY4 VX = VX + VY
when 5 then @regs[x] = self.-(@regs[x],@regs[y])
# 8XY5 VX = VX - VY
when 6 then @regs[0xF], @regs[x] = @regs[x][0], @regs[x]
>> 1 # 8X06 VX = VX SHIFT RIGHT 1 VF = least significant bit
when 7 then @regs[x] = self.-(@regs[y],@regs[x])
# 8XY7 VX = VY - VX
when 0xE then @regs[0xF], @regs[x] = @regs[x][7], @regs[x]
<< 1 # 8X0E VX = VX SHIFT LEFT 1, VF = most significant bit
else return nil
end
when 9 then @ip += 2 if @regs[x] != @regs[y]
# 9XY0 Skip next instruction if VX != VY
when 0xA then @I = nnn
# ANNN I = NNN
when 0xB then @ip = nnn + @regs[0]
# BNNN Jump to NNN + V0
when 0xC then @regs[x] = kk & rand(0xFF)
# CXKK VX = Random number AND KK
when 0xD then draw_sprite(@regs[x],@regs[y],opt)
# DXYN Draws a sprite at (VX,VY) starting at M(I). VF =
collision.
when 0xE
case kk
when 0x9E then @ip +=2 if $key_pressed.call @regs[x]
# EX9E Skip next instruction if key VX pressed
when 0xA1 then @ip +=2 unless $key_pressed.call @regs[x]
# EXA1 Skip next instruction if key VX not pressed
else return nil
end
when 0xF
case kk
when 0x07 then @regs[x] = @delay_timer
# FX07 VX = Delay timer
when 0x0A then @regs[x] = get_keypress
# FX0A Waits a keypress and stores it in VX
when 0x15 then @delay_timer = @regs[x]
# FX15 Delay timer = VX
when 0x18 then
# FX18 Sound timer = VX, not implemented as it doesn't do
anything except beep
when 0x1E then @I += @regs[x]
# FX1E I = I + VX
when 0x29 then @I = 5 * @regs[x]
# FX29 I points to the 4 x 5 font sprite of hex char in VX (
font at start of mem, 5 bytes per char)
when 0x33 then @mem[@I,2] = @regs[x].bcd_byte
# FX33 Store BCD representation of VX in M(I)...M(I+2)
when 0x55 then @mem[@I,x+1] = @regs[0..x]
# FX55 Save V0...VX in memory starting at M(I)
when 0x65 then @regs[0..x] = @mem[@I,x+1]
# FX65 Load V0...VX from memory starting at M(I)
else return nil
end
else return nil
end
return true
end

def run
Thread.new {
@key_timer = @delay_timer = 0
loop{
sleep 1.0 / 60
@delay_timer -= 1 if @delay_timer > 0
@key_timer -= 1 if @key_timer > 0
@key_pressed = nil if @key_timer == 0
exit! if $key_pressed.call('q')
}
}

loop {
fetch
break unless execute
}
puts "Halted at instruction %02X%02X " % [@instr[0],@instr[1]]
end

end


$tkscreen = TkPhotoImage.new('width'=>640,'height'=>320)
TkLabel.new(nil, 'image' => $tkscreen ).pack
Thread.new{ Tk.mainloop }

Chip8Emulator.new( File.open(ARGV[0],'rb').read).run if ARGV[0]

Doug.Fort

7/30/2006 7:16:00 PM

0


require 'logger'

class OpcodeGenerator
include Enumerable

OPCODE_SIZE = 2

def initialize(source_file)
@source_file = source_file
end

def each
while true
text = @source_file.read(OPCODE_SIZE)
return if text.nil?
yield text.unpack('S')[0]
end
end

def seek(address)
@source_file.seek address
end

def skip_next
@source_file.seek(OPCODE_SIZE, IO::SEEK_CUR)
end

end

class Chip_8_Emulator

REGISTER_COUNT = 0x16,
REGISTER_PATTERN = /V([0-9A-F])/
REGISTER_MASK = 0xFF
CARRY_MASK = 0x100
LEFT_SHIFT_MASK = 0x80
RIGHT_SHIFT_MASK = 0x1
CARRY_REGISTER = 0xF

def initialize
@register = Array.new(REGISTER_COUNT)
end

def run(opcode_generator)
@register.fill 0
opcode_generator.each do |opcode|
case opcode & 0xF000
when 0x0000 # exit gracefully
$LOG.debug sprintf("0x%04X Exit", opcode)
break
when 0x1000 # Jump to NNNN
address = address_from_opcode opcode
$LOG.debug sprintf("0x%04X Jump to 0x%04X", opcode, address)
opcode_generator.seek address
when 0x3000 # Skip next instruction if VX == KK
x, k = register_and_constant_from_opcode opcode
$LOG.debug sprintf("0x%04X Skip if V%X == 0x%04X", opcode, x,
k)
if @register[x] == k then
opcode_generator.skip_next
end
when 0x6000 # VX = KK
x, k = register_and_constant_from_opcode opcode
$LOG.debug sprintf("0x%04X V%X = 0x%04X", opcode, x, k)
@register[x] = k
when 0x7000 # VX = VX + KK
x, k = register_and_constant_from_opcode opcode
$LOG.debug sprintf("0x%04X V%X = V%X + 0x%04X", opcode, x, x,
k)
@register[x] = short_int_add(@register[x], k)
when 0x8000 # register operations
case opcode & 0x000F
when 0x0 # VX = VY
x, y = register_pair_from_opcode opcode
$LOG.debug sprintf("0x%04X V%X = V%X", opcode, x, y)
@register[x] = @register[y]
when 0x1 # VX = VX OR VY
x, y = register_pair_from_opcode opcode
$LOG.debug sprintf("0x%04X V%X = V%X OR V%X", opcode, x, x,
y)
@register[x] |= @register[y]
when 0x2 # VX = VX AND VY
x, y = register_pair_from_opcode opcode
$LOG.debug sprintf("0x%04X V%X = V%X AND V%X", opcode, x, x,
y)
@register[x] &= @register[y]
when 0x3 # VX = VX AND VY
x, y = register_pair_from_opcode opcode
$LOG.debug sprintf("0x%04X V%X = V%X XOR V%X", opcode, x, x,
y)
@register[x] ^= @register[y]
when 0x4 # VX = VX + VY
x, y = register_pair_from_opcode opcode
@register[x] = short_int_add(@register[x], @register[y])
when 0x5 # VX = VX - VY
x, y = register_pair_from_opcode opcode
$LOG.debug sprintf("0x%04X V%X = V%X - V%X", opcode, x, x, y)
@register[x] = short_int_subtract(@register[x], @register[y])
when 0x6 # VX = VX shift right 1
x = register_from_opcode opcode
$LOG.debug sprintf("0x%04X V%X = V%X shift right 1", opcode,
x, x)
@register[x] = short_int_shift_right(@register[x])
when 0x7 # VX = VY - VX
x, y = register_pair_from_opcode opcode
$LOG.debug sprintf("0x%04X V%X = V%X - V%X", opcode, x, y, x)
@register[x] = short_int_subtract(@register[y], @register[x])
when 0xE # VX = VX shift left 1
x = register_from_opcode opcode
$LOG.debug sprintf("0x%04X V%X = V%X shift left 1", opcode,
x, x)
@register[x] = short_int_shift_left(@register[x])
else
raise RuntimeError, "Unknown register opcode
0x#{opcode.to_s(16)}"
end
register_index = (opcode & 0x0F00) >> 8
value = opcode & 0x00FF
$LOG.debug sprintf("0x%04X V%X = V%X + 0x%04X", opcode,
register_index, register_index, value)
when 0xC000 # VX = Random number AND KK
x, k = register_and_constant_from_opcode opcode
r = rand(0xFF)
$LOG.debug sprintf("0x%04X V%X = random number 0x%04X AND
0x%04X", opcode, x, r, k)
@register[x] = r & k
else
raise RuntimeError, "Unknown opcode 0x#{opcode.to_s(16)}"
end
end
end

def address_from_opcode(opcode)
opcode & 0x0FFF
end

def register_from_opcode(opcode)
(opcode & 0x0F00) >> 8
end

def register_and_constant_from_opcode(opcode)
x = (opcode & 0x0F00) >> 8
k = opcode & 0x00FF

[x, k]
end

def register_pair_from_opcode(opcode)
x = (opcode & 0x0F00) >> 8
y = (opcode & 0x00F0) >> 4

[x, y]
end

def short_int_add(a, b)
sum = a + b
@register[CARRY_REGISTER] = (sum & CARRY_MASK) >> 8
sum & REGISTER_MASK
end

def short_int_subtract(a, b)
difference = (a | CARRY_MASK) - b
@register[CARRY_REGISTER] = (difference & CARRY_MASK) >> 8
difference & REGISTER_MASK
end

def short_int_shift_left(a)
@register[CARRY_REGISTER] = a & LEFT_SHIFT_MASK
(a << 1) & REGISTER_MASK
end

def short_int_shift_right(a)
@register[CARRY_REGISTER] = a & RIGHT_SHIFT_MASK
a >> 1
end

def method_missing(method_id)
match_object = REGISTER_PATTERN.match(method_id.id2name)
if match_object.nil? : raise NoMethodError, method_id.inspect end

@register[match_object[1].hex]
end

end

if __FILE__ == $0

$LOG = Logger.new STDOUT
$LOG.info "program starts"

require 'test/unit'
require 'stringio'

class TestOpcodeGenerator < Test::Unit::TestCase

TEST_DATA = [0x0001, 0x0002, 0x0003]

def test_generator
opcodes = OpcodeGenerator.new StringIO.new(TEST_DATA.pack("S*"))
opcodes.zip(TEST_DATA) {|opcode, test_element| assert_equal
test_element, opcode}
end

def test_seek
opcodes = OpcodeGenerator.new StringIO.new(TEST_DATA.pack("S*"))
opcodes.seek 2
opcodes.zip(TEST_DATA[1,TEST_DATA.length-1]) {|opcode,
test_element| assert_equal test_element, opcode}
end

def test_skip_next
opcodes = OpcodeGenerator.new StringIO.new(TEST_DATA.pack("S*"))
opcodes.seek 2
opcodes.skip_next
opcodes.zip(TEST_DATA[2,TEST_DATA.length-2]) {|opcode,
test_element| assert_equal test_element, opcode}
end

end

class TestChip_8 < Test::Unit::TestCase

# dump of file Chip8Test.html
TEST_DATA =
[0x6177,0x6245,0x7101,0x8320,0x8121,0x8122,0x8233,0x8134,0x8235,0x8106,0x8327,0x830E,0x64FF,0xC411,0x32BB,0x1000,0x0000,].pack('S*')


# V1:01000101
# V2:10111011
# V3:11101100
# V4:this number should be random, so do multiple runs to make sure
it changes
# VF:00000000
def test_emulator

emulator = Chip_8_Emulator.new

emulator.run OpcodeGenerator.new(StringIO.new(TEST_DATA))
assert_equal 0b01000101, emulator.V1
assert_equal 0b10111011, emulator.V2
assert_equal 0b11101100, emulator.V3
first_v4 = emulator.V4
assert_equal 0b00000000, emulator.VF

emulator.run OpcodeGenerator.new(StringIO.new(TEST_DATA))
assert_equal 0b01000101, emulator.V1
assert_equal 0b10111011, emulator.V2
assert_equal 0b11101100, emulator.V3

# note that this test fails sometimes because the domain isn't very big
# assert_not_equal first_v4, emulator.V4

assert_equal 0b00000000, emulator.VF

end

end

end

Aleks Kissinger

7/30/2006 8:19:00 PM

0

Heres my solution. I was more shooting for brevity of code than cool
stuff... Like previous posts, it uses instruction-level addressing,
so jumps are divided by two.

http://pastie.cab...

<snip>