1
0
mirror of https://github.com/safiire/n65.git synced 2024-12-12 00:29:03 +00:00

I have added many directives, such as .org, ines header, .dw, .bytes, and made it produce proper Roms launch into the entry point at 0xC000 from the reset vector. I basically rewrote the Assembler class, this could still use some cleaning up though, more unit tests, and to compile some code with CHR segments

This commit is contained in:
Safiire 2015-02-18 03:05:18 -08:00
parent debff220ec
commit ac46228b9f
6 changed files with 285 additions and 71 deletions

View File

@ -1,6 +1,6 @@
# assembler6502
An assembler for the 6502 Chip written in Ruby
An assembler for the 6502 microprocessor written in Ruby
6502 Assembler

View File

@ -105,3 +105,13 @@ module Assembler6502
end
Assembler6502.run
#p Assembler6502::Directive.parse(' .ines {"prog": 1, "char": 0, "mapper": 0, "mirror": 1} ')
#p Assembler6502::Directive.parse(' .org $423C ')
#p Assembler6502::Directive.parse(' .incbin "mario.chr" ')
#p Assembler6502::Directive.parse(' .dw $2FFF ')
#p Assembler6502::Directive.parse(' .bytes $2F, $FF, $2 ')

View File

@ -1,4 +1,9 @@
reset:
; Create an iNES header
.ines {"prog": 1, "char": 0, "mapper": 0, "mirror": 1}
; Here is the start of our code
.org $C000
start:
LDA #$01 ; square 1
STA $4015
LDA #$08 ; period low
@ -9,3 +14,11 @@ reset:
STA $4000
forever:
JMP forever
nmi:
RTI
.org $FFFA ; Here are the three interrupt vectors
.dw nmi ; VBlank non-maskable interrupt
.dw start ; When the processor is reset or powers on
.dw $0 ; External interrupt IRQ

View File

@ -2,94 +2,154 @@
module Assembler6502
####
## An assembler
## Let's simulate the entire 0xFFFF addressable memory space
## In the NES, and create reading and writing methods for it.
class MemorySpace
####
## Create a completely zeroed memory space
def initialize(size = 2**16)
@memory = Array.new(size, 0x0)
end
####
## Read from memory
def read(address, count)
@memory[address..(address + count - 1)]
end
####
## Write to memory
def write(address, bytes)
bytes.each_with_index do |byte, index|
@memory[address + index] = byte
end
end
####
## Return the memory as an array of bytes to write to disk
def emit_bytes
@memory
end
end
####
## The Main Assembler
class Assembler
attr_reader :assembly_code
####
## Assemble from a file to a file
def self.from_file(infile, outfile)
assembler = self.new(File.read(infile))
byte_array = self.create_ines_header + assembler.assemble(0x8000)
byte_array = assembler.assemble
File.open(outfile, 'w') do |fp|
fp.write(byte_array.pack('C*'))
end
end
####
## iNES Header
def self.create_ines_header
[0x4E, 0x45, 0x53, 0x1a, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]
end
####
## Assemble 6502 Mnemomics into a program
def initialize(assembly_code, label_index = {})
## Assemble 6502 Mnemomics and .directives into a program
def initialize(assembly_code)
@ines_header = nil
@assembly_code = assembly_code
@foreign_labels = label_index
end
####
## Assemble the 6502 assembly
def assemble(start_address = 0x600)
program_data = first_pass_parse(@assembly_code, start_address, @foreign_labels)
@foreign_labels.merge!(program_data.labels)
second_pass_resolve(program_data.instructions, @foreign_labels)
rescue => exception
STDERR.puts "Error:\n\t#{exception.message}"
exit(1)
end
## Run the assembly process into a virtual memory object
def assemble_in_virtual_memory
address = 0x0
labels = {}
memory = MemorySpace.new
unresolved_instructions = []
####
## Just a hexdump
def hexdump
assemble.map{|byte| sprintf("%.2x", (byte & 0xFF))}
end
####
## First pass of the assembler just parses each line.
## Collecting labels, and leaving labels in instructions
## as placeholders, you can provide the code's start address,
## or arbitrary labels that are not found the given asm
def first_pass_parse(assembly_code, address = 0x0600, labels = {})
instructions = []
assembly_code.split(/\n/).each do |line|
parsed_line = Assembler6502::Instruction.parse(line, address)
puts "Assembling, first pass..."
@assembly_code.split(/\n/).each do |raw_line|
sanitized = Assembler6502.sanitize_line(raw_line)
next if sanitized.empty?
parsed_line = Assembler6502::Instruction.parse(sanitized, address)
case parsed_line
when INESHeader
fail(SyntaxError, "Already got ines header") unless @ines_header.nil?
@ines_header = parsed_line
puts "\tWriting iNES Header"
memory.write(0x0, parsed_line.emit_bytes)
when Org
address = parsed_line.address
puts "\tMoving to address: $%X" % address
when Label
puts "\tLabel #{parsed_line.label} = $%X" % parsed_line.address
labels[parsed_line.label.to_sym] = parsed_line
when Instruction
instructions << parsed_line
if parsed_line.unresolved_symbols?
puts "\tSaving instruction with unresolved symbols #{parsed_line}, for second pass"
unresolved_instructions << parsed_line
else
puts "\tWriting instruction #{parsed_line} to memory"
memory.write(parsed_line.address, parsed_line.emit_bytes)
end
address += parsed_line.length
when nil
puts "\tAdvanced address to %X" % address
when IncBin
puts "\tI Don't support .incbin yet"
when DW
if parsed_line.unresolved_symbols?
puts "\tSaving .dw directive with unresolved symbols #{parsed_line}, for second pass"
unresolved_instructions << parsed_line
else
puts "\tWriting .dw #{parsed_line.inspect} to memory"
memory.write(address, parsed_line.emit_bytes)
end
address += 2
when Bytes
bytes = parsed_line.emit_bytes
puts "\tWriting raw bytes to memory #{bytes.inspect}"
memory.write(address, bytes)
address += bytes.size
else
fail(SyntaxError, sprintf("%.4X: Failed to parse: #{line}"))
fail(SyntaxError, sprintf("%.4X: Failed to parse: #{parsed_line}", address))
end
end
OpenStruct.new(:instructions => instructions, :labels => labels)
end
####
## The second pass makes each instruction emit bytes
## while also using knowledge of label addresses to
## resolve absolute and relative usage of labels.
def second_pass_resolve(instructions, labels)
instructions.inject([]) do |sum, instruction|
print "Second pass: Resolving Symbols..."
unresolved_instructions.each do |instruction|
if instruction.unresolved_symbols?
instruction.resolve_symbols(labels)
end
puts instruction
sum += instruction.emit_bytes
sum
memory.write(instruction.address, instruction.emit_bytes)
end
puts 'Done'
memory
end
####
## After assembling the binary into the full 16-bit memory space
## we can now slice out the parts that should go into the binary ROM
## I am guessing the ROM size should be 1 bank of 16KB cartridge ROM
## plus the 16 byte iNES header. If the ROM is written into memory
## beginning at 0xC000, this should reach right up to the interrupt vectors
def assemble
virtual_memory = assemble_in_virtual_memory
rom_size = 16 + (0xffff - 0xc000)
nes_rom = MemorySpace.new(rom_size)
nes_rom.write(0x0, virtual_memory.read(0x0, 0x10))
nes_rom.write(0x10, virtual_memory.read(0xC000, 0x4000))
nes_rom.emit_bytes
end
end

View File

@ -1,28 +1,154 @@
require 'json'
module Assembler6502
####
## This class can setup an iNES Header
class INESHeader
####
## Construct with the right values
def initialize(prog = 0x1, char = 0x0, mapper = 0x0, mirror = 0x1)
@prog, @char, @mapper, @mirror = prog, char, mapper, mirror
end
####
## Emit the header bytes, this is not exactly right, but it works for now.
def emit_bytes
[0x4E, 0x45, 0x53, 0x1a, @prog, @char, @mapper, @mirror, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]
end
end
####
## This is an .org directive
class Org
attr_reader :address
####
## Initialized with start address
def initialize(address)
@address = address
end
end
####
## This is to include a binary file
class IncBin
class FileNotFound < StandardError; end
####
## Initialize with a file path
def initialize(filepath)
unless File.exists?(filepath)
fail(FileNotFound, ".incbin can't find #{filepath}")
end
@data = File.read(filepath).unpack('C*')
end
####
## What is the size of the read data?
def size
@data.size
end
####
## Emit bytes
def emit_bytes
@data
end
end
####
## Data Word
class DW
attr_reader :address
class WordTooLarge < StandardError; end
def initialize(value, address)
@value = value
@address = address
end
def unresolved_symbols?
@value.kind_of?(Symbol)
end
def resolve_symbols(labels)
if unresolved_symbols? && labels[@value] != nil
@value = labels[@value].address
end
end
def emit_bytes
fail('Need to resolve symbol in .dw directive') if unresolved_symbols?
[@value & 0xFFFF].pack('S').bytes
end
end
####
## Just a bunch of bytes
class Bytes
def initialize(bytes)
@bytes = bytes.split(',').map do |byte_string|
number = byte_string.gsub('$', '')
integer = number.to_i(16)
fail(SyntaxError, "#{integer} is too large for one byte") if integer > 0xff
integer
end
end
def emit_bytes
@bytes
end
end
####
## This parses an assembler directive
class Directive
#####
## Some directives are:
## .inesprg x ; x * 16KB of PRG code
## .ineschr x ; x * 8KB of CHR data
## .inesmap x ; mapper. 0 = NROM, I don't know the other types
## .inesmir x ; background mirroring, I don't know what this should be so x = 1
## .bank x ; Sets the bank number, there are 8 banks of 8192 bytes = 2**16
## .org $hhhh ; Positions the code at hex address $hhhh
## .incbin "a" ; Assembles the contents of a binary file into current address
## .dw x ; Assemble a 16-bit word at current address, x can be a label
## .bytes a b c ; Assemble a sequence of bytes at the current address
####
## This will return a new Directive, or nil if it is something else.
def self.parse(directive_line)
def self.parse(directive_line, address)
sanitized = Assembler6502.sanitize_line(directive_line)
case sanitized
when /^\.ines (.+)$/
header = JSON.parse($1)
INESHeader.new(header['prog'], header['char'], header['mapper'], header['mirror'])
when /^\.org\s+\$([0-9A-F]{4})$/
Org.new($1.to_i(16))
when /^\.incbin "([^"]+)"$/
IncBin.new($1)
when /^\.dw\s+\$([0-9A-F]{1,4})$/
DW.new($1.to_i(16), address)
when /^\.dw\s+([A-Za-z_][A-Za-z0-9_]+)/
DW.new($1.to_sym, address)
when /^\.bytes\s+(.+)$/
Bytes.new($1)
when /^\./
fail(SyntaxError, "Syntax Error in Directive '#{sanitized}'")
end
end
end
end

View File

@ -112,6 +112,11 @@ module Assembler6502
## Empty lines assemble to nothing
return nil if sanitized.empty?
## Let's see if this line is an assembler directive
directive = Directive.parse(sanitized, address)
return directive unless directive.nil?
## Let's see if this line is a label, and try
## to create a label for the current address
label = Label.parse_label(sanitized, address)