How not to write an emulator – Part 1
In the heydays of the mid-to-late 80’s, I had an Atari 800 XL. And considering that I spent most of my young adolescent days on it, I still have very fond memories of it.
When software emulators of the Atari came out, I wanted to know how it was done. Thinking more about how the CPU and the supporting sound and graphic chipsets gave me a geeky urge to write an emulator for the CPU used by those generation of 8-bit machines. The CPU used was the 6502. So folks, while this work is in progress, I wrote a very basic skeleton of an emulator written completely in Ruby. It does not very much, but I figure if anyone wanted to take a look at bad and unoptimized coding, I shouldn’t give them a reason to look elsewhere.
Download 6502.rb.bz2
Download test binary – displays alphabets from A to Z
#!/usr/bin/ruby
#Written by Farhan Yousaf - March 2010
class CPU6502
attr_accessor :cpu_instance, :cpus, :imagesize, :debug
def initialize
@debug = false
@ip = 0
@imagesize = 0
@pc = 0
@pc_off = 0
@ram = [ ] * 65536
@register = { :A => 0, :X => 0, :Y => 0, :SP => 0xFF, :SR => 0 }
@cpu_instance = 1
@cpus = @cpus.to_i+1
@inst = Array.new
@inst.push [ 0xA2, :immediate, 2, "LDX" ]
@inst.push [ 0xA9, :immediate, 2, "LDA" ]
@inst.push [ 0xA6, :zeropage, 2, "LDX" ]
@inst.push [ 0xB6, :zeropagey, 2, "LDX" ]
@inst.push [ 0xAE, :absolute, 2, "LDX" ]
@inst.push [ 0xBE, :absolutey, 2, "LDX" ]
@inst.push [ 0x8A, :implied, 1, "TXA" ]
@inst.push [ 0x20, :absolute, 3, "JSR" ]
@inst.push [ 0xE8, :absolute, 1, "INX" ]
@inst.push [ 0xE0, :absolute, 2, "CPX" ]
@inst.push [ 0xD0, :absolute, 2, "BNE" ]
@inst.push [ 0x00, :absolute, 2, "BRK" ]
@flag = { :S => 0, :V => 0, :B => 0, :D => 0, :I => 0, :Z =>0, :C => 0 }
@operand=Array.new(2)
end
def display_status
if (@debug)
#printf("PC=%04x SP=%04x A=%02x X=%02x Y=%02x S=%02x C=%d Z=%d\n\n", @pc,@register[:SP],@register[:A],@register[:X],@register[:Y],@flag[:S],@flag[:C]?1:0,@flag[:Z])
end
end
def push(oper1)
#display_status
@ram[@register[:SP]+0x100] = oper1
@register[:SP]-=1
end
def pull
@register[:SP]+=1
@ram[@register[:SP]+0x100]
end
def set_sign(accumulator)
@flag[:S] = accumulator & 0x80 #bit 7 of A
end
def set_zero(accumulator)
# @flag[:Z] = (accumulator==0) ? false:true
if accumulator == 0
@flag[:Z] = 0
else
@flag[:Z] = 1
end
end
def set_carry(accumulator)
@flag[:C] = accumulator
end
def loadi(filen)
@prog = File.open(filen, "rb") { |io| io.read }
@imagesize = @prog.size
end
def runop(opcode, oper1, oper2)
# display_status
case opcode
when 0xA2 #LDX
@register[:X] = oper1
@pc+=2
set_sign(@register[:X])
set_zero(@register[:X])
when 0x8A #TXA
@register[:A] = @register[:X]
@pc+=1
when 0x20 #JSR
@pc = @pc + 3 - 1 #was +3
push((@pc >> 8) & 0xff)
push(@pc & 0xff)
tmp_addr = (oper1 << 8) | oper2
if tmp_addr == 0xFFEE
putc(@register[:A])
@pc = tmp_addr
else
@pc = tmp_addr
end
display_status
@pc = pull
@pc |= (pull << 8) @pc=@pc+1 when 0xE8 #INX @register[:X] = (@register[:X]+1) & 0xff set_sign(@register[:X]) set_zero(@register[:X]) @pc+=1 when 0xE0 #CPX tmp = @register[:X] - oper1 set_carry(@register[:X] >= oper1) #was < 0x100 set_sign(tmp) if (tmp == 0) set_zero(1) else set_zero(0) end @pc+=2 when 0xD0 #BNE display_status @pc+=1 if (@flag[:Z] == 0) if (oper1 > 0x7F)
@pc = @pc - (~(oper1) & 0x00FF)
else
@pc = @pc + (oper1 & 0x00FF)
end
else
@pc=@pc+1
end
when 0xA9 #LDA
set_sign(oper1)
set_zero(oper1)
@register[:A] = oper1
@pc+=2
when 00 #BRK
#puts "************** IN BREAK **************"
@pc+1
exit 0
end
display_status
end
def readmem(pc)
@prog[pc]
end
def decode
@pc=0
while @pc <= (@pc_off + @prog.size)
#puts "inside decode loop - #{@pc} #{@prog.size}"
opcode=readmem(@pc)
@inst.find do |opc,mode,bytes,desc|
if opc == opcode
case bytes
when 2
@operand[0] = readmem(@pc+1)
printf("%04X\t%s #%02X\t\t # %02X%02X -- (%d)\n", @pc,desc, @operand[0], opc, @operand[0], bytes) if @debug
runop(opcode, @operand[0], @operand[1])
when 1
printf("%04X\t%s \t\t #%02X -- (%d)\n", @pc,desc, opc, bytes) if @debug
runop(opcode, @operand[0], @operand[1])
when 3
@operand[0] = readmem(@pc+2)
@operand[1] = readmem(@pc+1)
printf("%04X\t%s $%02X%02X\t\t # %02X%02X%02X -- (%d)\n", @pc,desc, @operand[0], @operand[1], opc, @operand[0],@operand[1], bytes) if @debug
runop(opcode, @operand[0], @operand[1])
end
end
end
end
end
end
if ARGV.empty?
puts "Usage: 6502.rb [image] -d | disassemble"
puts " 6502.rb [image] -r | run"
exit 0
end
myCPU = CPU6502.new
myCPU.loadi(ARGV[0])
myCPU.debug = true if ARGV.find { |a| a == "-d" }
printf("Image length: %d\n", myCPU.imagesize)
myCPU.decode if ARGV.find { |a| a == "-r" }
Comments welcome, but please be gentle. :)
nettles
Wow, I just started looking into doing this. You’re in Google already!
It’s awesome that you did this in Ruby. When it’s time to optimize, you can extract to a C module or just use rubyinline to rewrite to slow stuff. Is this something you plan to continue and post more about?
admin
@nettles, yes, there will be more updates coming as I add more and more opcodes. I think my goal may be to get BBC Micro BASIC fully emulated — which is a good way to test if stuff works. So watch this space.
Performance is not of any concern to me. But you’re right. Converting the code to C would be a trivial task if I wanted more speed. I wonder if there are SDL libraries for Ruby… Hmm…
Farhan