mirror of
https://github.com/safiire/n65.git
synced 2024-12-12 00:29:03 +00:00
183 lines
5.6 KiB
Ruby
183 lines
5.6 KiB
Ruby
require_relative 'module_functions'
|
|
require_relative 'opcodes'
|
|
require_relative 'memory'
|
|
require_relative 'directive'
|
|
require_relative 'instruction'
|
|
require_relative 'label'
|
|
|
|
module Assembler6502
|
|
|
|
|
|
####
|
|
## The Main Assembler
|
|
class Assembler
|
|
|
|
|
|
## Custom exceptions
|
|
class INESHeaderNotFound < StandardError; end
|
|
class MapperNotSupported < StandardError; end
|
|
|
|
|
|
####
|
|
## Assemble from an asm file to a nes ROM
|
|
def self.from_file(infile, outfile)
|
|
assembler = self.new(File.read(infile))
|
|
byte_array = assembler.assemble
|
|
|
|
File.open(outfile, 'w') do |fp|
|
|
fp.write(byte_array.pack('C*'))
|
|
end
|
|
end
|
|
|
|
|
|
####
|
|
## Instantiate a new Assembler with a full asm
|
|
## file as given in a string.
|
|
def initialize(assembly_code)
|
|
@ines_header = nil
|
|
@assembly_code = assembly_code
|
|
end
|
|
|
|
|
|
####
|
|
## New ROM assembly, this is so simplified, and needs to take banks into account
|
|
## This will happen once I fully understand mappers and banks.
|
|
def assemble
|
|
## Assemble into a virtual memory space
|
|
virtual_memory = assemble_in_virtual_memory
|
|
|
|
## First we need to be sure we have an iNES header
|
|
fail(MapperNotSupported, "Mapper #{@ines_header.mapper} not supported") if @ines_header.mapper != 0
|
|
|
|
## First we need to be sure we have an iNES header
|
|
fail(INESHeaderNotFound) if @ines_header.nil?
|
|
|
|
## Now we want to create a ROM layout for PROG
|
|
## This is simplified and only holds max two PROG entries
|
|
prog_rom = MemorySpace.new(@ines_header.prog * MemorySpace::ProgROMSize)
|
|
case @ines_header.prog
|
|
when 0
|
|
fail("You must have at least one PROG section")
|
|
exit(1)
|
|
when 1
|
|
prog_rom.write(0x0, virtual_memory.read(0xc000, MemorySpace::ProgROMSize))
|
|
when 2
|
|
prog_rom.write(0x0, virtual_memory.read(0x8000, MemorySpace::ProgROMSize))
|
|
prog_rom.write(MemorySpace::ProgROMSize, virtual_memory.read(0xC000, MemorySpace::ProgROMSize))
|
|
else
|
|
fail("I can't support more than 2 PROG sections")
|
|
exit(1)
|
|
end
|
|
|
|
## Now we want to create a ROM layout for CHAR
|
|
## This is simplified and only holds max two CHAR entries
|
|
char_rom = MemorySpace.new(@ines_header.char * MemorySpace::CharROMSize)
|
|
case @ines_header.char
|
|
when 0
|
|
when 1
|
|
char_rom.write(0x0, virtual_memory.read(0x0000, MemorySpace::CharROMSize))
|
|
when 2
|
|
char_rom.write(0x0, virtual_memory.read(0x0000, MemorySpace::CharROMSize))
|
|
char_rom.write(MemorySpace::CharROMSize, virtual_memory.read(0x2000, MemorySpace::CharROMSize))
|
|
else
|
|
fail("I can't support more than 2 CHAR sections")
|
|
exit(1)
|
|
end
|
|
|
|
if @ines_header.char.zero?
|
|
@ines_header.emit_bytes + prog_rom.emit_bytes
|
|
else
|
|
@ines_header.emit_bytes + prog_rom.emit_bytes + char_rom.emit_bytes
|
|
end
|
|
end
|
|
|
|
|
|
private
|
|
|
|
####
|
|
## Run the assembly process into a virtual memory object
|
|
def assemble_in_virtual_memory
|
|
address = 0x0
|
|
labels = {}
|
|
memory = MemorySpace.new
|
|
unresolved_instructions = []
|
|
|
|
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 "\tGot iNES Header"
|
|
|
|
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
|
|
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}"
|
|
memory.write(parsed_line.address, parsed_line.emit_bytes)
|
|
end
|
|
address += parsed_line.length
|
|
|
|
when IncBin
|
|
puts "\t Including binary file #{parsed_line.filepath}"
|
|
memory.write(parsed_line.address, parsed_line.emit_bytes)
|
|
address += parsed_line.size
|
|
|
|
when DW
|
|
if parsed_line.unresolved_symbols?
|
|
puts "\tSaving #{parsed_line} directive with unresolved symbols, for second pass"
|
|
unresolved_instructions << parsed_line
|
|
else
|
|
puts "\tWriting #{parsed_line} to memory"
|
|
memory.write(address, parsed_line.emit_bytes)
|
|
end
|
|
address += 2
|
|
|
|
when Bytes
|
|
bytes = parsed_line.emit_bytes
|
|
puts "\tWriting raw #{bytes.size} bytes to #{sprintf("$%X", address)}"
|
|
memory.write(address, bytes)
|
|
address += bytes.size
|
|
|
|
when ASCII
|
|
bytes = parsed_line.emit_bytes
|
|
puts "\tWriting ascii string to memory \"#{bytes.pack('C*')}\""
|
|
memory.write(address, bytes)
|
|
address += bytes.size
|
|
|
|
else
|
|
fail(SyntaxError, sprintf("%.4X: Failed to parse: #{parsed_line}", address))
|
|
end
|
|
end
|
|
|
|
puts "Second pass: Resolving Symbols..."
|
|
unresolved_instructions.each do |instruction|
|
|
if instruction.unresolved_symbols?
|
|
instruction.resolve_symbols(labels)
|
|
end
|
|
puts "\tResolved #{instruction}"
|
|
memory.write(instruction.address, instruction.emit_bytes)
|
|
end
|
|
puts 'Done'
|
|
|
|
memory
|
|
end
|
|
|
|
end
|
|
|
|
end
|