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

Separated the frontend commandline stuff from the actual assembler

This commit is contained in:
Safiire 2015-02-24 16:43:50 -08:00
parent dfda652a51
commit 3dc1c22aae
6 changed files with 229 additions and 197 deletions

View File

@ -4,109 +4,10 @@
## ##
## Usage: ./assembler_6502.rb <infile.asm> ## Usage: ./assembler_6502.rb <infile.asm>
## ##
## This is a pretty straightfoward assembler, that is currently set up ## This the front end of the Assembler, just processes commandline arguments
## to produce iNES ROM formatted binaries from simple assembly listings. ## and passes them to the actual assembler.
## 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.
require 'yaml'
require 'ostruct'
require 'optparse'
require_relative 'lib/directive'
require_relative 'lib/assembler' require_relative 'lib/assembler'
require_relative 'lib/instruction' require_relative 'lib/front_end'
require_relative 'lib/label'
module Assembler6502 Assembler6502::FrontEnd.new(ARGV).run
#####
## 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] <input_file.asm>"
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

View File

@ -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 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 ## The Main Assembler
class Assembler class Assembler
## Custom exceptions ## Custom exceptions
class INESHeaderNotFound < StandardError; end class INESHeaderNotFound < StandardError; end
class MapperNotSupported < 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) def self.from_file(infile, outfile)
assembler = self.new(File.read(infile)) assembler = self.new(File.read(infile))
byte_array = assembler.assemble byte_array = assembler.assemble
@ -61,14 +29,71 @@ module Assembler6502
end end
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) def initialize(assembly_code)
@ines_header = nil @ines_header = nil
@assembly_code = assembly_code @assembly_code = assembly_code
end 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 ## Run the assembly process into a virtual memory object
def assemble_in_virtual_memory def assemble_in_virtual_memory
@ -152,59 +177,6 @@ module Assembler6502
memory memory
end 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
end end

89
lib/front_end.rb Normal file
View File

@ -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] <input_file.asm>"
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

49
lib/memory.rb Normal file
View File

@ -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

12
lib/module_functions.rb Normal file
View File

@ -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

9
lib/opcodes.rb Normal file
View File

@ -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