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