From 3dc1c22aaea8c1b241b7a173add823ac6b107ac7 Mon Sep 17 00:00:00 2001 From: Safiire Date: Tue, 24 Feb 2015 16:43:50 -0800 Subject: [PATCH] Separated the frontend commandline stuff from the actual assembler --- assembler_6502.rb | 107 +-------------------------- lib/assembler.rb | 160 +++++++++++++++++----------------------- lib/front_end.rb | 89 ++++++++++++++++++++++ lib/memory.rb | 49 ++++++++++++ lib/module_functions.rb | 12 +++ lib/opcodes.rb | 9 +++ 6 files changed, 229 insertions(+), 197 deletions(-) create mode 100644 lib/front_end.rb create mode 100644 lib/memory.rb create mode 100644 lib/module_functions.rb create mode 100644 lib/opcodes.rb diff --git a/assembler_6502.rb b/assembler_6502.rb index 2929aed..f7b9ef1 100755 --- a/assembler_6502.rb +++ b/assembler_6502.rb @@ -4,109 +4,10 @@ ## ## Usage: ./assembler_6502.rb ## -## This is a pretty straightfoward assembler, that is currently set up -## to produce iNES ROM formatted binaries from simple assembly listings. -## It is good at knowing which addressing modes are and are not allowed for -## each instruction, and contains some examples of correct syntax. -## -## Parsing is done by Regular Expression, because, well the language is -## so regular, it actually took less time than anything else I've tried -## to parse in the past, including Scheme using parsec. -## -## It handles labels, and does a two pass assembly, first assembling -## the byte codes, and then going back and filling in the proper addresses -## where labels were used. -## -## I have used this to compile some code for the NES, and it ran correctly -## on FCEUX, got it to make some sounds, etc. -## -## Some Todos: -## - I need to add the #<$800 and #>$800 style operators to select the -## MSB and LSB of immediate values during assembly. -## - I may make this into a Rubygem -## - Maybe I can put some better error messages. -## - I should just make a 6502 CPU emulator probably now too. +## This the front end of the Assembler, just processes commandline arguments +## and passes them to the actual assembler. - -require 'yaml' -require 'ostruct' -require 'optparse' -require_relative 'lib/directive' require_relative 'lib/assembler' -require_relative 'lib/instruction' -require_relative 'lib/label' +require_relative 'lib/front_end' -module Assembler6502 - - ##### - ## Load in my OpCode definitions - MyDirectory = File.expand_path(File.dirname(__FILE__)) - OpCodes = YAML.load_file("#{MyDirectory}/data/opcodes.yaml") - - #### - ## Clean up a line of assembly - def sanitize_line(asm_line) - sanitized = asm_line.split(';').first || "" - sanitized.strip.chomp - end - module_function :sanitize_line - - - #### - ## Run the assembler using commandline arguments - def run - options = {:out_file => nil} - parser = OptionParser.new do |opts| - opts.banner = "Usage: #{$0} [options] " - - opts.on('-o', '--outfile filename', 'outfile') do |out_file| - options[:out_file] = out_file; - end - - opts.on('-h', '--help', 'Displays Help') do - puts opts - exit - end - end - parser.parse!(ARGV) - - ## For now let's just handle one file at a time. - if ARGV.size != 1 - STDERR.puts "Can only assemble one input file at once :(" - exit(1) - end - input_file = ARGV.shift - - ## Make sure the input file exists - unless File.exists?(input_file) - STDERR.puts "Input file #{input_file} does not exist" - exit(1) - end - - ## Maybe they didn't provide an output file name, so we'll guess - if options[:out_file].nil? - ext = File.extname(input_file) - options[:out_file] = input_file.gsub(ext, '') + '.nes' - end - - if options.values.any?(&:nil?) - STDERR.puts "Missing options try --help" - exit(1) - end - - begin - Assembler6502::Assembler.from_file(input_file, options[:out_file]) - rescue StandardError => error - STDERR.puts("Assemble Failed!") - STDERR.puts(error.class) - if error.message - STDERR.puts(error.message) - end - exit(1) - end - end - module_function :run - -end - - Assembler6502.run +Assembler6502::FrontEnd.new(ARGV).run diff --git a/lib/assembler.rb b/lib/assembler.rb index 2d2f2f1..2bb93a5 100644 --- a/lib/assembler.rb +++ b/lib/assembler.rb @@ -1,57 +1,25 @@ +require_relative 'module_functions' +require_relative 'opcodes' +require_relative 'memory' +require_relative 'directive' +require_relative 'instruction' +require_relative 'label' module Assembler6502 - #### - ## Let's simulate the entire 0xFFFF addressable memory space - ## In the NES, and create reading and writing methods for it. - class MemorySpace - INESHeaderSize = 0x10 - ProgROMSize = 0x4000 - CharROMSize = 0x2000 - - #### - ## 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 + ## Custom exceptions class INESHeaderNotFound < StandardError; end class MapperNotSupported < StandardError; end #### - ## Assemble from a file to a file + ## 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 @@ -61,14 +29,71 @@ module Assembler6502 end end + #### - ## Assemble 6502 Mnemomics and .directives into a program + ## 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 @@ -152,59 +177,6 @@ module Assembler6502 memory 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 - end end diff --git a/lib/front_end.rb b/lib/front_end.rb new file mode 100644 index 0000000..55b9769 --- /dev/null +++ b/lib/front_end.rb @@ -0,0 +1,89 @@ +require 'optparse' + +module Assembler6502 + + #### + ## This class handles the front end aspects, + ## parsing the commandline options and running the assembler + class FrontEnd + + #### + ## Initialize with ARGV commandline + def initialize(argv) + @options = {:output_file => nil} + @argv = argv.dup + end + + + #### + ## Run the assembler + def run + ## First use the option parser + parser = create_option_parser + parser.parse!(@argv) + + ## Whatever is leftover in argv the input files + if @argv.size.zero? + STDERR.puts("No input files") + exit(1) + end + + ## Only can assemble one file at once for now + if @argv.size != 1 + STDERR.puts "Can only assemble one input file at once :(" + exit(1) + end + + input_file = @argv.shift + + ## Make sure the input file exists + unless File.exists?(input_file) + STDERR.puts "Input file #{input_file} does not exist" + exit(1) + end + + ## Maybe they didn't provide an output file name, so we'll guess + if @options[:output_file].nil? + ext = File.extname(input_file) + @options[:output_file] = input_file.gsub(ext, '') + '.nes' + end + + if @options.values.any?(&:nil?) + STDERR.puts "Missing options try --help" + exit(1) + end + + begin + Assembler6502::Assembler.from_file(input_file, @options[:output_file]) + rescue StandardError => error + STDERR.puts("Assemble Failed!") + STDERR.puts(error.class) + if error.message + STDERR.puts(error.message) + end + exit(1) + end + end + + private + + #### + ## Create a commandline option parser + def create_option_parser + OptionParser.new do |opts| + opts.banner = "Usage: #{$0} [options] " + + opts.on('-o', '--outfile filename', 'outfile') do |output_file| + @options[:output_file] = output_file; + end + + opts.on('-h', '--help', 'Displays Help') do + puts opts + exit + end + end + end + + end + +end diff --git a/lib/memory.rb b/lib/memory.rb new file mode 100644 index 0000000..17e8e2d --- /dev/null +++ b/lib/memory.rb @@ -0,0 +1,49 @@ +module Assembler6502 + + #### + ## Let's use this to simulate a virtual address space, by default + ## we simulate the 64KB of addressable space on the NES + class MemorySpace + + + #### Some constants, the size of PROG and CHAR ROM + INESHeaderSize = 0x10 + ProgROMSize = 0x4000 + CharROMSize = 0x2000 + + + #### + ## Create a completely zeroed memory space, 2**16 by default + def initialize(size = 2**16) + @memory = Array.new(size, 0x0) + end + + + #### + ## Read from memory + ## TODO: This could use some boundry checking + def read(address, count) + @memory[address..(address + count - 1)] + end + + + #### + ## Write to memory + ## TODO: This could use some boundry checking + 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 + +end + diff --git a/lib/module_functions.rb b/lib/module_functions.rb new file mode 100644 index 0000000..130fc8c --- /dev/null +++ b/lib/module_functions.rb @@ -0,0 +1,12 @@ + +module Assembler6502 + + #### + ## This cleans up a line, removing whitespace and newlines + def sanitize_line(asm_line) + sanitized = asm_line.split(';').first || "" + sanitized.strip.chomp + end + module_function :sanitize_line + +end diff --git a/lib/opcodes.rb b/lib/opcodes.rb new file mode 100644 index 0000000..c3d2095 --- /dev/null +++ b/lib/opcodes.rb @@ -0,0 +1,9 @@ +require 'yaml' + +module Assembler6502 + + ## Load OpCode definitions into this module + MyDirectory = File.expand_path(File.dirname(__FILE__)) + OpCodes = YAML.load_file("#{MyDirectory}/../data/opcodes.yaml") + +end