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:
parent
dfda652a51
commit
3dc1c22aae
@ -4,109 +4,10 @@
|
||||
##
|
||||
## Usage: ./assembler_6502.rb <infile.asm>
|
||||
##
|
||||
## 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] <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
|
||||
Assembler6502::FrontEnd.new(ARGV).run
|
||||
|
160
lib/assembler.rb
160
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
|
||||
|
89
lib/front_end.rb
Normal file
89
lib/front_end.rb
Normal 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
49
lib/memory.rb
Normal 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
12
lib/module_functions.rb
Normal 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
9
lib/opcodes.rb
Normal 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
|
Loading…
Reference in New Issue
Block a user