How not to write an emulator – Part 1

Posted on the March 2nd, 2010 under Linux, Technology by admin

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. :)

2 Responses to 'How not to write an emulator – Part 1'

  1. March 3, 2010 at 1:08 am
    nettles
  2. March 3, 2010 at 9:31 pm
    admin

Leave a Reply




XHTML::
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>