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:
parent
debff220ec
commit
ac46228b9f
@ -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
|
||||
|
||||
|
@ -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 ')
|
||||
|
||||
|
||||
|
||||
|
||||
|
15
beep.asm
15
beep.asm
@ -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
|
||||
|
172
lib/assembler.rb
172
lib/assembler.rb
@ -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
|
||||
|
152
lib/directive.rb
152
lib/directive.rb
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user