Skip to content
Browse files

first draft of working emulator

  • Loading branch information...
1 parent 90f5f28 commit 24416dd5fdfa27c38b089e87af5dca534c355b6b @Deradon committed Apr 11, 2012
View
11 TODO
@@ -11,3 +11,14 @@
Register, Word and Literal are all ducktyped to behave similiar,
check if we can clean it up.
+1.4 DualScreen (*MultiScreen)
+require 'dcpu16'
+cpu = DCPU16::CPU.new
+screen = DCPU16::Screen.new(cpu.memory)
+screen2 = DCPU16::Screen.new(cpu.memory, :x_offset => 40, :memory_offset => 0x9000)
+cpu.memory.write(0x8000, 0x30)
+cpu.memory.write(0x9000, 0x30)
+
+1.5 Screen
+ * RefreshRate
+
View
32 bin/dcpu16.rb
@@ -0,0 +1,32 @@
+#!/usr/bin/env ruby
+
+begin
+ require 'dcpu16'
+rescue LoadError
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __FILE__))
+ require 'dcpu16'
+end
+
+if ARGV.length == 1
+ filename = ARGV[0]
+ if filename.end_with?(".o")
+ dump = File.open(filename, "rb").read.unpack("S>*")
+ else
+ require 'tempfile'
+ file = File.open(filename)
+ assembler = DCPU16::Assembler.new(file.read)
+ tmp_file = Tempfile.new("obj")
+ assembler.write(tmp_file.path)
+ dump = tmp_file.read.unpack("S>*")
+ end
+ debugger = DCPU16::Debugger.new(dump)
+ debugger.run
+elsif ARGV.length == 2
+ file = File.open(ARGV[0])
+ assembler = DCPU16::Assembler.new(file.read)
+ assembler.write(ARGV[1])
+else
+ puts "Usage: dcpu16 <input> <output>"
+ exit
+end
+
View
2 examples/array/screen.arr
@@ -0,0 +1,2 @@
+[0x7c21, 0x0030, 0x8011, 0x0911, 0x8000, 0x8412, 0x05fe, 0x0300, 0x7dc1, 0x0003, 0x8422, 0x09fe, 0x007b, 0x7dc1, 0x0002,0000]
+
View
20 examples/dasm16/screen.dasm16
@@ -0,0 +1,20 @@
+; Patrick Helm, deradon87@gmail.com
+;
+; Flash Screen with chars (Benchmark)
+;
+; Output:
+; * Cycle: 519002 (Cycle needed to finish)
+; * Clock: 100000 (Clock-Speed of CPU [defined])
+; * HZ: 99999.02 (Clock-Speed of CPU [real])
+
+SET C, 0x30 ; Init OuterLoop
+:outer_loop SET B, 0 ; Init InnerLoop
+:inner_loop SET [0x8000+B], C ;draw
+ADD B, 1
+IFG 0x0300, B
+SET PC, inner_loop ;jmp inner_loop
+
+ADD C,1
+IFG 0x7B, C
+SET PC, outer_loop ;jmp outer_loop
+
View
2 lib/dcpu16.rb
@@ -1,7 +1,9 @@
require 'dcpu16/assembler'
require 'dcpu16/cpu'
+require 'dcpu16/debugger'
require 'dcpu16/literal'
require 'dcpu16/memory'
+require 'dcpu16/screen'
require 'dcpu16/version'
module DCPU16
View
278 lib/dcpu16/assembler.rb
@@ -1,33 +1,275 @@
# WIP
+# Taken from: https://github.com/dcpu16/dcpu16-rb/blob/master/asm.rb
module DCPU16
class Assembler
+ class Instruction
+ attr_accessor :inst, :a, :b
+ attr_reader :num, :line
+
+ def initialize(num, line)
+ @num, @line = num, line
+ @inst, @a, @b = 0, 0, 0
+ @extensions = []
+ @references = []
+ end
+
+ def extend(value)
+ # Check value against max int
+ @extensions << value
+ end
+
+ def reference(label)
+ @references[@extensions.size] = label
+ @extensions << 0 # Placeholder
+ end
+
+ def resolve(labels)
+ @references.each_with_index do |label, i|
+ next unless label
+ location = labels[label]
+ error("Cannot find label #{label} in #{labels.keys.join(", ")}") unless location
+ @extensions[i] = location
+ end
+ end
+
+ def word
+ @inst + (@a << 4) + (@b << 10)
+ end
+
+ def words
+ [word] + @extensions.dup
+ end
+
+ def bytes
+ words.map{|w| [w].pack('n') }.join("")
+ end
+
+ def size
+ @extensions.size + 1
+ end
+
+ def hex(w)
+ "%04x" % w
+ end
+
+ def to_s
+ "#{@num}: #{words.map{|w| hex(w)}.join(" ")}" # ; #{line}"
+ end
+
+ def error(message)
+ raise Exception.new("Line #{num}: #{message}\n#{line}")
+ end
+ end
+
+
+
+ def self.declare(map, start, string)
+ count = start
+ string.split(" ").each do |token|
+ map[token] = count
+ count += 1
+ end
+ end
+
+ HEX_RE = /^0x[0-9a-fA-F]+$/
+ INT_RE = /^\d+$/
+ REG_RE = /^[A-Z]+$/
+ LABEL_RE = /^[a-z_]+$/
+ INDIRECT_RE = /^\[.+\]/
+ INDIRECT_OFFSET_RE = /^[^+]+\+[^+]+$/
+
+ EXT_PREFIX = 0
+
+ INDIRECT = 0x08
+ INDIRECT_OFFSET = 0x10
+ INDIRECT_NEXT = 0x1e
+ NEXT = 0x1f
+ LITERAL = 0x20
+
+ INSTRUCTIONS = {}
+ EXTENDED_INSTRUCTIONS = {}
+ VALUES = {}
+
+ declare(INSTRUCTIONS, 1, "SET ADD SUB MUL DIV MOD SHL SHR AND BOR XOR IFE IFN IFG IFB")
+ declare(EXTENDED_INSTRUCTIONS, 1, "JSR")
+ declare(VALUES, 0, "A B C X Y Z I J")
+ declare(VALUES, 0x18, "POP PEEK PUSH SP PC O")
+
attr_reader :input
- def initialize(text)
- @input = text
+ def initialize(input)
+ @body = []
+ @label_these = []
+ @input = input
+ self.assemble
+ end
+
+ def clean(line)
+ line.gsub(/;.*/, "").gsub(/,/, " ").gsub(/\s+/, " ").strip
+ end
+
+ def dehex(token)
+ return token.hex.to_s if HEX_RE === token
+ token
+ end
+
+ def parse_value(token, op)
+ token = dehex(token)
+
+ case token
+ when INT_RE
+ value = token.to_i
+ return LITERAL + value if value <= 31
+ op.extend value
+ return NEXT
+
+ when REG_RE
+ return VALUES[token]
+
+ when LABEL_RE
+ op.reference(token)
+ return NEXT
+
+ when INDIRECT_RE
+ inner = dehex(token[1..-2])
+ case inner
+ when INT_RE
+ value = inner.to_i
+ op.extend value
+ return INDIRECT_NEXT
+
+ when REG_RE
+ reg = VALUES[inner]
+ op.error("Can't use indirect addressing on non-basic reg #{reg}") unless reg <= VALUES["J"]
+ return INDIRECT + reg
+
+ when LABEL_RE
+ op.reference(inner)
+ return INDIRECT_NEXT
+
+ when INDIRECT_OFFSET_RE
+ offset, reg = inner.split("+").map{|x| x.strip }
+ offset = dehex(offset)
+
+ op.error("Malformed indirect offset value #{inner}") unless INT_RE === offset && REG_RE === reg
+ value = offset.to_i# + VALUES[reg]
+ op.extend value
+
+ return INDIRECT_OFFSET + VALUES[reg]
+ end
+ else
+ op.error("Unrecognized value #{token}")
+ end
end
- def dump
- raise "TODO #{caller.first}"
- lines = []
- @input.each_line do |line|
- empty = (line =~ /^\s*$/)
- comment = (line =~ /^\s*;+.*$/)
+ def parse_op(op, tokens)
+ inst_name = tokens.shift
+
+ inst_code = INSTRUCTIONS[inst_name]
+ if inst_code
+ op.inst = inst_code
+ op.a = parse_value(tokens.shift, op)
+ op.b = parse_value(tokens.shift, op)
+ return
+ end
+
+ inst_code = EXTENDED_INSTRUCTIONS[inst_name]
+ if inst_code
+ op.inst = EXT_PREFIX
+ op.a = inst_code
+ op.b = parse_value(tokens.shift, op)
+ end
+
+ raise Exception.new("No such instruction: #{inst_name}") unless inst_code
+ end
+
+ def assemble#(text)
+ text = @input
+ labels = {}
+
+ num = 0
+ location = 0
+ text.each_line do |line|
+ num += 1
+ op = Instruction.new(num, line)
+
+ cleaned = clean(line)
+ next if cleaned.empty?
+
+ tokens = cleaned.split(/\s/)
+ op.error("Wrong number of tokens - #{tokens}") unless (2..4) === tokens.size
+
+ labels[tokens.shift[1..-1]] = location if tokens[0].start_with?(":")
+ parse_op(op, tokens)
+
+ @body << op
+ location += op.size
+ end
+
+ @body.each {|op| op.resolve(labels) }
+
+ #display
+ end
- next if empty || comment
+ def display
+ @body.each { |op| puts op }
+ end
- line.gsub!(/^\s*([^;]*).*$/) { $1 } # Strip some spaces
- regex = /^(:\w*)?\s*(\w*)\s*([^,\s]*),?\s?([^\s]*).*$/
- match = line.match(regex)
+ def dump#(filename)
+ @body
+# File.open(filename, "w") do |file|
+# @body.each {|inst| file.write(inst.bytes) }
+# end
+ end
- line = { :label => match[1],
- :op => match[2],
- :a => match[3],
- :b => match[4] }
- puts line
- lines << line
+ def write(filename)
+ File.open(filename, "w") do |file|
+ @body.each {|inst| file.write(inst.bytes) }
end
end
end
end
+
+#if __FILE__ == $PROGRAM_NAME
+# asm = Assembler.new
+# filename = "#{ARGV.first || "out.s"}.o"
+# asm.assemble ARGF
+# asm.dump filename
+#end
+
+
+
+
+
+
+
+
+
+#attr_reader :input
+#def initialize(text)
+# @input = text
+#end
+
+#def dump
+# lines = []
+# @input.each_line do |line|
+# empty = (line =~ /^\s*$/)
+# comment = (line =~ /^\s*;+.*$/)
+
+# next if empty || comment
+
+# line.gsub!(/^\s*([^;]*).*$/) { $1 } # Strip some spaces
+
+# regex = /^(:\w*)?\s*(\w*)\s*([^,\s]*),?\s?([^\s]*).*$/
+# match = line.match(regex)
+
+# line = { :label => match[1],
+# :op => match[2],
+# :a => match[3],
+# :b => match[4] }
+# puts line
+# lines << line
+# end
+#end
+
View
13 lib/dcpu16/cpu.rb
@@ -108,6 +108,19 @@ def step
notify_observers(self)
end
+ def to_s
+ <<EOF
+###########
+### CPU ###
+
+* Cycle: #{cycle}
+# Clock: #{clock_cycle}
+* HZ: #{hz}
+* Last: #{last_instruction.inspect}
+* Reg.: #{registers.inspect}
+EOF
+ end
+
end
end
View
31 lib/dcpu16/debugger.rb
@@ -0,0 +1,31 @@
+module DCPU16
+ class Debugger
+ attr_reader :cpu, :screen
+ def initialize(dump = [])
+ @cpu = DCPU16::CPU.new(dump)
+ @screen = DCPU16::Screen.new(@cpu.memory)
+
+ @cpu.add_observer(self)
+ end
+
+ def run
+ clear_screen
+ begin
+ @cpu.run
+ rescue DCPU16::Instructions::Reserved => e
+ puts @cpu.to_s
+ end
+ end
+
+ # Observer
+ def update(cpu)
+ #cpu
+ end
+
+ # Clears screen for output
+ def clear_screen
+ print @screen.to_s
+ end
+ end
+end
+
View
155 lib/dcpu16/screen.rb
@@ -0,0 +1,155 @@
+require 'observer'
+
+# Text VRAM starts at 0x8000 and is word-based (16bits per address cell)
+# like other memory and is in the same address space as the rest of the processor uses.
+
+# The low 8 bits determine the character shown.
+
+# The high 8 bits determine the color;
+# the highest 4 are the foreground and the lowest 4 are the background.
+
+# The color nibbles use (from LSB to MSB) blue, green, red, highlight.
+# (Normal 16color from PCs, though probably with darkyellow instead of brown)
+
+# The screen appears to be 32 characters by 12 lines, with a character cell of 4x8 pixels,
+# suggesting possible graphics modes of 128x96 pixels at that resolution.
+
+# require 'dcpu16'; cpu = DCPU16::CPU.new; screen = DCPU16::Screen.new(cpu.memory); cpu.memory.write(0x8000, 0x30)
+
+
+# TODO: OUTPUT
+# require 'dcpu16'; debugger = DCPU16::Debugger.new
+module DCPU16
+ class Screen
+ include Observable
+
+ X_OFFSET = 2
+ Y_OFFSET = 2
+
+ attr_reader :width, :height, :memory_offset, :chars
+
+ def initialize(memory, options = {})
+ @memory = memory
+ @width = options[:width] || 32
+ @height = options[:height] || 12
+ @memory_offset = options[:memory_offset] || 0x8000
+
+ @x_offset = options[:x_offset] || X_OFFSET
+ @y_offset = options[:y_offset] || Y_OFFSET
+
+
+ # Initial Screen dump
+ @chars = []
+ @height.times do |h|
+ @width.times do |w|
+ offset = h * @width + w
+ value = @memory.read(@memory_offset + 2*offset).value
+ @chars[offset] = Char.new(value, w + @x_offset, h + @y_offset)
+ end
+ end
+
+ @memory.add_observer(self)
+# print self
+ end
+
+ def size
+ @size ||= @width * @height
+ end
+
+ def memory_offset_end
+ @memory_offset_end ||= 0x82FE#@memory_offset + size*2 - 2
+ end
+
+ def to_s
+ return @to_s if @to_s
+
+ @to_s = "\e[?1049h\e[17;1H"
+ @to_s << chars.join
+ @to_s << frame
+ end
+
+ # Use a fancy border around console
+ def frame
+ return @frame if @frame
+
+ chars = []
+ # 4 corners
+ chars << Char.new(0x23, @x_offset - 1, @y_offset - 1) # TopLeft
+ chars << Char.new(0x23, @x_offset + @width, @y_offset - 1) # TopRight
+ chars << Char.new(0x23, @x_offset - 1, @y_offset + @height) # BottomLeft
+ chars << Char.new(0x23, @x_offset + @width, @y_offset + @height) # BottomRight
+
+ # horiz
+ @width.times { |x| chars << Char.new(0x2d, x + @x_offset, @y_offset - 1) }
+ @width.times { |x| chars << Char.new(0x2d, x + @x_offset, @y_offset + @height) }
+
+ # vertical
+ @height.times { |y| chars << Char.new(0x7c, @x_offset - 1, y + @y_offset) }
+ @height.times { |y| chars << Char.new(0x7c, @x_offset + @width, y + @y_offset) }
+ @frame = ""
+ @frame << chars.join
+ end
+
+ # Callback from observed memory
+ def update(offset, value)
+ return unless (memory_offset..memory_offset_end).include?(offset)
+# @to_s = nil
+
+ diff = (offset - @memory_offset) / 2
+ h = diff / @width
+ w = diff % @width
+ @chars[diff] = Char.new(value, w + @x_offset, h + @y_offset)
+ print @chars[diff]
+# changed
+# notify_observers(self)
+# print @chars[diff]
+ end
+ end
+
+ # DOC
+ # Inspired by: https://github.com/judofyr/rcpu/blob/master/lib/rcpu/libraries.rb
+ class Char
+ attr_reader :output
+ def initialize(value, x, y)
+ @char = (value & 0x007F).chr
+# @bg_color = (value >> 8) & 0x0F
+# @fg_color = value >> 12
+
+ args = []
+ args << (value >> 15)
+ if value > 0x7F
+ args << color_to_ansi(value >> 12) + 30
+ args << color_to_ansi(value >> 8) + 40
+ end
+
+ @char = " " if @char.ord.zero?
+ @color = "\e[#{args*';'}m"
+ @output = "\e7\e[#{y};#{x}H#{@color}#{@char}\e8"
+ end
+
+ def to_s
+ @output
+ end
+
+ def color_to_ansi(bit)
+ ((bit & 1) << 2) | (bit & 2) | ((bit & 4) >> 2)
+ end
+ end
+end
+
+
+
+#The high 8 bits determine the color; the highest 4 are the foreground and the lowest 4 are the background
+
+#args = []
+#args << (value >> 15)
+#if value > 0x7F
+# args << color_to_ansi(value >> 12) + 30
+# args << color_to_ansi(value >> 8) + 40
+#end
+
+#char = " " if char.ord.zero?
+
+#color = "\e[#{args*';'}m"
+#print "\e7\e[#{rows+1};#{cols+1}H#{color}#{char}\e8"
+
View
20 spec/fixtures/example.dasm16
@@ -0,0 +1,20 @@
+; Patrick Helm, deradon87@gmail.com
+;
+; Flash Screen with chars (Benchmark)
+;
+; Output:
+; * Cycle: 519002 (Cycle needed to finish)
+; * Clock: 100000 (Clock-Speed of CPU [defined])
+; * HZ: 99999.02 (Clock-Speed of CPU [real])
+
+SET C, 0x30 ; Init OuterLoop
+:outer_loop SET B, 0 ; Init InnerLoop
+:inner_loop SET [0x8000+B], C ;draw
+ADD B, 1
+IFG 0x0300, B
+SET PC, inner_loop ;jmp inner_loop
+
+ADD C,1
+IFG 0x7B, C
+SET PC, outer_loop ;jmp outer_loop
+
View
5 spec/spec_helper.rb
@@ -1,2 +1,7 @@
require 'dcpu16'
+# Don't run broken specs
+RSpec.configure do |c|
+ c.filter_run_excluding :broken => true
+end
+
View
8 spec/unit/assembler_spec.rb
@@ -1,10 +1,14 @@
require 'spec_helper.rb'
describe DCPU16::Assembler do
- let(:asm) { File.open(File.join('spec', 'fixtures', 'example.asm')).read }
+ let(:asm) { File.open(File.join('spec', 'fixtures', 'example.dasm16')).read }
subject { DCPU16::Assembler.new(asm) }
its(:input) { should == asm }
-# its(:dump) { should be_a_kind_of(Array) }
+ its(:dump) { should be_a_kind_of(Array) }
+ specify do
+# puts subject.dump
+ puts subject.dump.first.bytes.length
+ end
end
View
51 spec/unit/screen_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+require 'unit/support/screen_observer'
+
+describe DCPU16::Screen do
+ let(:memory) { DCPU16::Memory.new }
+ subject { DCPU16::Screen.new(memory) }
+
+ describe "initialize" do
+ specify { expect { DCPU16::Screen.new }.to raise_error(ArgumentError) }
+ specify { expect { DCPU16::Screen.new(memory) }.to_not raise_error }
+ its(:width) { should == 32 }
+ its(:height) { should == 12 }
+ its(:memory_offset) { should == 0x8000 }
+ its(:chars) { subject.length.should == 32*12 } # width * height + newlines
+
+ context "when called with options" do
+ let(:options) { {:width => 40, :height => 200, :memory_offset => 0x9000} }
+ subject { DCPU16::Screen.new(memory, options) }
+
+ its(:width) { should == 40 }
+ its(:height) { should == 200 }
+ its(:memory_offset) { should == 0x9000 }
+ its(:chars) { subject.length.should == 40 * 200 }
+ end
+ end
+
+ describe "#to_s" do
+ context "in initial state" do
+ pending
+# let(:expected) { String.new(" "*32 + "\n") * 12 }
+# its(:to_s) { should == expected }
+ end
+ end
+
+ describe "observable" do
+ let(:observer) { DCPU16::ScreenObserver.new }
+ before(:all) { subject.add_observer(observer) }
+
+ specify { subject.should_receive(:update).tap { memory.write(0x8000, 40) } }
+ context "memory outside of screen changed" do
+ specify { observer.should_not_receive(:update).tap { memory.write(0x7FFF, 40) } }
+ specify { observer.should_not_receive(:update).tap { memory.write(0x8000 + 0x180, 40) } }
+ end
+ context "memory of screen changed" do
+ pending "Disabled Observer right now!"
+# specify { observer.should_receive(:update).tap { memory.write(0x8000, 40) } }
+# specify { observer.should_receive(:update).tap { memory.write(0x82FE, 40) } }
+ end
+ end
+end
+
View
13 spec/unit/support/screen_observer.rb
@@ -0,0 +1,13 @@
+module DCPU16
+ class ScreenObserver
+ attr_reader :screen
+
+ def initialize
+ end
+
+ def update(screen)
+ @screen
+ end
+ end
+end
+

0 comments on commit 24416dd

Please sign in to comment.
Something went wrong with that request. Please try again.