diff --git a/README.md b/README.md index fbf3f5b..088be0f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/assembler_6502.rb b/assembler_6502.rb index 22f371e..f6138af 100755 --- a/assembler_6502.rb +++ b/assembler_6502.rb @@ -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 ') + + + + diff --git a/beep.asm b/beep.asm index 3fe6ae7..3d502e1 100644 --- a/beep.asm +++ b/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 diff --git a/lib/assembler.rb b/lib/assembler.rb index 88c9012..2a99aec 100644 --- a/lib/assembler.rb +++ b/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 diff --git a/lib/directive.rb b/lib/directive.rb index 2fd6432..c0f69db 100644 --- a/lib/directive.rb +++ b/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 + + + + diff --git a/lib/instruction.rb b/lib/instruction.rb index cf84b02..d48a47a 100644 --- a/lib/instruction.rb +++ b/lib/instruction.rb @@ -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)