# -*- coding: utf-8 -*-
#
# Copyright © 2019 Simon Forman
#
# This file is part of PythonOberon
#
# PythonOberon is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PythonOberon is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with PythonOberon. If not see <http://www.gnu.org/licenses/>.
#
'''
Emulated Hardware
========================================
RAM map::
Addr (bytes)
╔════════════╤══════════════╗
║ 0x00000000 Start of RAM ║
║ . │ ║
║ 0x000E7F00 DISPLAY_START║
║ . + 0x18000 bytes║
║ . or 0x6000 words║
║ . or 1024x768x1px ║
║ 0x000FFF00 end of display║
║ . │ ║
║ 0x00180000 MemSize ║
╟────────── ┼ ────────────╢
║ │ ║
║ Empty ║
╟────────── ┼ ────────────╢
║ 0xFFFFF800 ROMStart ║
║ . │ ║
║ . IO_RANGE ║
║ 0xFFFFFFC0 | clock ║
║ 0xffffffc4 | switches/LEDs║
║ 0xffffffc8 | serial data ║
║ 0xffffffcc | serial status║
║ 0xffffffd0 | SPI data ║
║ 0xffffffd4 | SPI control ║
║ 0xffffffd8 | mouse ║
║ . │ ║
╚════════════╧══════════════╝
'''
import pdb, sys
from array import array
from time import time
from struct import unpack
from pprint import pformat
from .disassembler import dis
from .util import (
bint,
blong,
python_int_to_signed_int,
signed_int_to_python_int,
)
IO_RANGE = 0xFFFFFFC0
ROMStart = 0xFFFFF800 // 4
MemSize = 0x00180000
MemWords = MemSize // 4
[docs]def log(message, *args):
pass
## print message % args
## print >> stderr, message % args
[docs]class Trap(Exception):
pass
[docs]class RISC(object):
'''
The RISC processsor.
This class is designed for ease of introspection rather than efficiency.
'''
def __init__(self, rom, ram, PC=ROMStart):
self.rom = rom
self.ram = ram
self.PC = self.pcnext = PC
self.R = [0] * 16
self.H = 0
self.N = self.Z = self.C = self.OV = False
self.io_ports = {}
self.switches = 0
[docs] def cycle(self):
'''Run one cycle of the processor.'''
self.PC = self.pcnext
self.decode(self.fetch())
if not self.p:
self.register_instruction()
elif self.q:
self.branch_instruction()
else:
self.ram_instruction()
[docs] def fetch(self):
'''
Load an instruction from RAM or ROM and return it. Raise
:py:exc:`Trap` if ``PC`` goes out of bounds or if the machine
enters a certain kind of infinite loop (this is a way for code
running on the emulated chip to signal *HALT*.)
'''
if self.PC < MemWords:
instruction = self.ram[self.PC << 2]
elif ROMStart <= self.PC < (ROMStart + len(self.rom)):
instruction = self.rom[self.PC - ROMStart]
else:
raise Trap('Fetch from bad address 0x%08x' % (self.PC,))
if (
instruction == 0xE7FFFFFF
): # REPEAT UNTIL False i.e. halt loop.
raise Trap('REPEAT-UNTIL-False-ing')
return instruction
[docs] def decode(self, instruction):
'''
Decode the instruction and set various field and flag member values
of the emulator object.
'''
self.IR = IR = bint(instruction)
self.p = IR[31]
self.q = IR[30]
self.u = IR[29]
self.v = IR[28]
self.w = IR[16]
self.op = IR[20, 16]
self.ira = IR[28, 24]
self.irb = IR[24, 20]
self.irc = IR[4, 0]
self.cc = IR[27, 24]
self.imm = IR[16, 0]
self.off = IR[20, 0]
self.jmp = IR[24, 0]
self.C0 = self.R[self.irc]
self.MOV = (not self.p) and (self.op == 0)
self.LSL = (not self.p) and (self.op == 1)
self.ASR = (not self.p) and (self.op == 2)
self.ROR = (not self.p) and (self.op == 3)
self.AND = (not self.p) and (self.op == 4)
self.ANN = (not self.p) and (self.op == 5)
self.IOR = (not self.p) and (self.op == 6)
self.XOR = (not self.p) and (self.op == 7)
self.ADD = (not self.p) and (self.op == 8)
self.SUB = (not self.p) and (self.op == 9)
self.MUL = (not self.p) and (self.op == 10)
self.DIV = (not self.p) and (self.op == 11)
self.LDR = self.p and (not self.q) and (not self.u)
self.STR = self.p and (not self.q) and self.u
self.BR = self.p and self.q
[docs] def register_instruction(self):
'''
Increment ``PC`` and set a register from the ALU.
'''
self.pcnext = self.PC + 1
self.set_register(self.Arithmetic_Logical_Unit())
[docs] def Arithmetic_Logical_Unit(self):
'''
Enact the ALU of the RISC chip.
'''
B = self.R[self.irb]
# Here's how negative immediate values are stored in the instruction and
# regenerated in the cpu. In the ORGX module the instruction is created
# like so:
#
# PROCEDURE Put1(op, a, b, im: LONGINT);
# BEGIN (*emit format-1 instruction, -10000H <= im < 10000H*)
# IF im < 0 THEN INC(op, 1000H) END ; (*set v-bit*)
# code[pc] := (((a+40H) * 10H + b) * 10H + op) * 10000H + (im MOD 10000H); INC(pc)
# END Put1;
#
# If the immediate value is negative the V bit in the instruction is set.
# In any event the immediate value has its high sixteen bits masked off
# (modulus 0x10000 effects this.) For example, -23 looks like this,
# bit-wise:
#
# 11111111111111111111111111101001
#
# And after truncating the high bits:
#
# 1111111111101001
#
# This is the bit pattern that gets stored in the instruction immediate
# field.
#
# The statement immediately below reconstructs the negative 32-bit value
# if the V bit is set in the instruction, otherwise it simply passes
# through the given immediate value. (This happens, of course, only if
# the Q bit is set, otherwise C1 is just set to C0.)
C1 = self.C1 = (
(
(0b11111111111111110000000000000000 | self.imm)
if self.v
else self.imm
)
if self.q
else self.C0
)
if self.MOV:
# (q ? (~u ? {{16{v}}, imm} : {imm, 16'b0}) :
if self.q:
res = C1 if not self.u else (self.imm << 16)
else:
# (~u ? C0 : ... )) :
if not self.u:
res = self.C0
else:
# ... (~irc[0] ? H : {N, Z, C, OV, 20'b0, 8'b01010000})
if not self.irc[0]:
res = self.H
else:
res = (
self.N << 31
| self.Z << 30
| self.C << 29
| self.OV << 28
| 80
)
# Bit-wise logical operations
elif self.LSL:
res = B << (C1 & 31)
elif self.ASR:
C1 &= 31
res = B >> C1
if bint(B)[31]:
res |= (2**C1 - 1) << (32 - C1) # Extend sign bit.
elif self.ROR:
C1 &= 31
lost_bits = bint(B)[C1:0]
res = (B >> C1) | (lost_bits << (32 - C1))
elif self.AND:
res = B & C1
elif self.ANN:
res = B & (0xFFFFFFFF ^ C1)
elif self.IOR:
res = B | C1
elif self.XOR:
res = B ^ C1
# For the arithmetical operators we must convert to Python ints to
# correctly handle negative numbers.
elif self.ADD:
B = signed_int_to_python_int(B)
C1 = signed_int_to_python_int(C1)
res = B + C1 + (self.u and self.C)
self.C = res < B
res = self._check_overflow(res)
elif self.SUB:
B = signed_int_to_python_int(B)
C1 = signed_int_to_python_int(C1)
res = B - C1 - (self.u and self.C)
res = self._check_overflow(res)
self.C = res > B
elif self.MUL:
B = signed_int_to_python_int(B)
C1 = signed_int_to_python_int(C1)
product = B * C1
self.product = blong(python_int_to_signed_int(product, 64))
res = self.product[32:0]
elif self.DIV:
B = signed_int_to_python_int(B)
C1 = signed_int_to_python_int(C1)
res, remainder = divmod(B, C1)
res = python_int_to_signed_int(res)
self.remainder = python_int_to_signed_int(remainder)
else:
raise Trap('We should never get here!')
return res
def _check_overflow(self, res, bits=33):
try:
return python_int_to_signed_int(res)
except ValueError:
self.OV = True
return blong(python_int_to_signed_int(res, bits))[32:0]
self.OV = False
[docs] def set_register(self, value):
'''
Set ``A`` register and ``N``, ``Z``, and ``H``.
'''
value = value if isinstance(value, bint) else bint(value)
self.R[self.ira] = value[32:0]
self.N = value[31]
self.Z = value == 0
self.H = (
self.product[64:32]
if self.MUL
else self.remainder
if self.DIV
else self.H
)
[docs] def branch_instruction(self):
'''
Branch instruction.
'''
S = self.N ^ self.OV
T = (
(self.cc == 0) & self.N
| (self.cc == 1) & self.Z
| (self.cc == 2) & self.C
| (self.cc == 3) & self.OV
| (self.cc == 4) & (self.C | self.Z)
| (self.cc == 5) & S
| (self.cc == 6) & (S | self.Z)
| (self.cc == 7)
)
if self.IR[27]:
T = not T
if not T:
self.pcnext = self.PC + 1
return
if self.v: # Save link
self.R[15] = (self.PC + 1) << 2
if self.u:
offset = signed_int_to_python_int(self.jmp, width=24)
self.pcnext = int(offset + self.PC + 1)
else:
self.pcnext = self.C0 >> 2
[docs] def ram_instruction(self):
'''
RAM read/write instruction.
'''
self.addr = addr = int(
self.R[self.irb] + self._sign_extend_offset()
)
if addr >= IO_RANGE:
self.io(addr - IO_RANGE)
elif self.LDR:
value = self.ram.get_byte(addr) if self.v else self.ram[addr]
self.set_register(value)
elif self.v:
self.ram.put_byte(addr, self.R[self.ira] & 255)
else:
self.ram[addr] = self.R[self.ira]
self.pcnext = self.PC + 1
def _sign_extend_offset(self):
off = bint(self.off & 0xFFFFF)
if off[19]:
off = signed_int_to_python_int(off | 0xFFF00000)
return off
[docs] def io(self, port):
'''
I/O instruction.
'''
device = self.io_ports.get(port)
if not device:
raise Trap('no device at port 0x%x (aka %i)' % (port, port))
if self.LDR:
self.set_register(device.read())
else:
device.write(self.R[self.ira])
[docs] def dump_mem(self, to_file=None, number=10, syms=None):
if to_file is None:
to_file = sys.stdout
if self.PC < MemWords:
self.dump_ram(to_file=to_file, number=number, syms=syms)
else:
self.dump_rom(to_file=to_file, number=number)
[docs] def dump_ram(
self, to_file=None, location=None, number=10, syms=None
):
'''
Debug function, print a disassembly of a span of RAM.
'''
if to_file is None:
to_file = sys.stdout
label_len = 16
if location is None:
location = self.PC
if syms is None:
syms = {}
lower = max((0, location - number))
for i in range(lower, location + number):
label = '%16s' % syms.get(i, ' ' * label_len)[:label_len]
h = '>' if i == location else ' '
i <<= 2
instr = self.ram[i]
print(f'{h} {label} {i:05x} 0x{instr:08x} {dis(instr)}', file=to_file)
#print(h, label, hex(i), hex(self.ram[i]), dis(self.ram[i]), file=to_file)
[docs] def dump_rom(self, to_file, location=None, number=10):
'''
Debug function, print a disassembly of a span of ROM.
'''
if location is None:
location = self.PC - ROMStart
lower = max((0, location - number))
upper = min((len(self.rom), location + number + 1))
for i in range(lower, upper):
h = '>' if i == location else ' '
print(
'%s rom[0x%x] %s' % (h, i, dis(self.rom[i])),
file=to_file,
)
[docs] def view(self):
'''
Debug function, print current instruction.
'''
if self.PC >= MemSize:
return
kw = self.__dict__.copy()
kw['A'] = self.R[self.ira]
# print '- ' * 40
print('PC: 0x%(PC)04x ---' % kw, dis(int(self.IR)))
if self.STR:
print(
' Storing',
'[0x%(addr)04x] <- R%(ira)i = 0x%(A)08x' % kw,
)
elif self.LDR:
print(
' Loading', 'R%(ira)i <- [0x%(addr)04x]' % kw
)
# Print the registers.
# for i in range(0, 16, 2):
# reg0, reg1 = self.R[i], self.R[i + 1]
# print 'R%-2i = 0x%-8x' % (i + 1, reg1),
# print 'R%-2i = 0x%-8x' % (i, reg0)
# print
[docs] def brief_view(self):
'''
Debug function, print crude state of chip.
'''
return (
'0x%08x : 0x%08x'
' %i %i %i %i %i %i %i %i'
' %i %i %i %i %i %i %i'
' 0x%x'
) % (
(self.PC, self.IR)
+ tuple(map(signed_int_to_python_int, self.R[:-1]))
+ (self.R[-1],)
)
[docs]class ByteAddressed32BitRAM(object):
'''
Represent a 32-bit wide RAM chip that is byte-addressed.
E.g. addresses 0-3 are the first four bytes, or one (32-bit) word.
'''
BYTE_MASKS = (
0b11111111111111111111111100000000,
0b11111111111111110000000011111111,
0b11111111000000001111111111111111,
0b00000000111111111111111111111111,
)
def __init__(self):
# Use a dict rather than some array. Might be woth exploring other
# datastructures...
self.store = array('I', MemWords * b'\0\0\0\0')
assert self.store.itemsize == 4
[docs] def get(self, addr):
'''
Return a (32-bit) word. Address must be word-aligned.
'''
word_addr, byte_offset = divmod(addr, 4)
assert not byte_offset, repr(addr)
return self.store[word_addr]
__getitem__ = get
[docs] def put(self, addr, word):
'''
Set a (32-bit) word. Address must be word-aligned.
'''
word_addr, byte_offset = divmod(addr, 4)
assert not byte_offset, repr(addr)
self.store[word_addr] = word
__setitem__ = put
[docs] def get_byte(self, addr):
'''
Return a byte. Address need not be word-aligned.
'''
word_addr, byte_offset = divmod(addr, 4)
word = self.store[word_addr]
return (word >> (8 * byte_offset)) & 0xFF
[docs] def put_byte(self, addr, byte):
'''
Set a byte. Address need not be word-aligned.
'''
# if isinstance(byte, str):
# byte = ord(byte[:1])
if not (0 <= byte < 0x100):
raise ValueError("byte out of range: %i" % (byte,))
word_addr, byte_offset = divmod(addr, 4)
n = 8 * byte_offset # How many bits to shift.
byte <<= n
word = self.store[word_addr]
if word: # merge word and shifted byte
# AND mask with the memory word to clear the bits for the
# pre-shifted byte and OR the result with it.
byte |= word & self.BYTE_MASKS[byte_offset]
self.put(word_addr << 2, byte)
def __len__(self):
return (4 * (1 + max(self.store))) if self.store else 0
def __repr__(self):
return pformat(self.store)
[docs]class Disk(object):
'''
Disk
(I cribbed most of this from
`pdewacht/oberon-risc-emu <https://github.com/pdewacht/oberon-risc-emu>`_
. I'm not exactly sure how it works but it does work, well enough to
load the Oberon OS from the disk image.)
'''
SECTOR_SIZE = 512
SECTOR_SIZE_WORDS = SECTOR_SIZE // 4
STRUCT_FORMAT = '<%iI' % SECTOR_SIZE_WORDS
diskCommand, diskRead, diskWrite, diskWriting = list(range(4))
def __init__(self, image_file):
self.state = self.diskCommand
self.rx_buf = [None] * self.SECTOR_SIZE_WORDS
self.rx_idx = 0
self.tx_buf = [None] * (self.SECTOR_SIZE_WORDS + 2)
self.tx_cnt = 0
self.tx_idx = 0
self.file = image_file
self.read_sector()
self.offset = 0x80002 if self.tx_buf[0] == 0x9B1EA38D else 0
[docs] def read(self):
if self.tx_idx >= 0 and self.tx_idx < self.tx_cnt:
log('disk_read from buffer 0x%x', self.tx_buf[self.tx_idx])
return self.tx_buf[self.tx_idx]
log('disk_read from default 0xFF')
return 255
[docs] def write(self, word):
log('disk_write 0x%x', word)
self.tx_idx += 1
if self.state == self.diskCommand:
if (0xFF & word) == 0xFF and self.rx_idx == 0:
log('disk_write PASS 0x%x', word)
return
log(
'disk_write diskCommand 0x%x to rx_buf[%i]',
word,
self.rx_idx,
)
self.rx_buf[self.rx_idx] = word
self.rx_idx += 1
if self.rx_idx == 6:
## pdb.set_trace()
self.run_command()
self.rx_idx = 0
elif self.state == self.diskRead:
if self.tx_idx == self.tx_cnt:
self.state = self.diskCommand
log('disk_write diskRead -> diskCommand')
self.tx_cnt = 0
self.tx_idx = 0
elif self.state == self.diskWrite:
if word == 254:
self.state = self.diskWriting
log('disk_write diskWrite -> diskWriting')
elif self.state == self.diskWriting:
if self.rx_idx < 128:
self.rx_buf[self.rx_idx] = word
self.rx_idx += 1
if self.rx_idx == 128:
self.write_sector()
if self.rx_idx == 130:
self.tx_buf[0] = 5
self.tx_cnt = 1
self.tx_idx = -1
self.rx_idx = 0
self.state = self.diskCommand
log('disk_write diskWriting -> diskCommand')
[docs] def run_command(self):
cmd, a, b, c, d = self.rx_buf[0:5]
a, b, c, d = (n & 0xFF for n in (a, b, c, d))
arg = (a << 24) | (b << 16) | (c << 8) | d
log('run_command ' + ' '.join(map(hex, (cmd, arg))))
if cmd == 81:
self.state = self.diskRead
self.tx_buf[0] = 0
self.tx_buf[1] = 254
self._seek(arg)
## pdb.set_trace()
self.read_sector(2)
self.tx_cnt = 2 + 128
elif cmd == 88:
self.state = self.diskWrite
self._seek(arg)
self.tx_buf[0] = 0
self.tx_cnt = 1
else:
self.tx_buf[0] = 0
self.tx_cnt = 1
self.tx_idx = -1
def _seek(self, arg):
log('#' * 100)
a = (arg - self.offset) * self.SECTOR_SIZE
log('seeking to %i (0x%x)', arg, a)
self.file.seek(a)
[docs] def read_sector(self, into=0):
data = self.file.read(self.SECTOR_SIZE)
self.tx_buf[into:] = unpack(self.STRUCT_FORMAT, data)
[docs] def write_sector(self):
log('write sector %r', self.rx_buf)
# data = pack(self.STRUCT_FORMAT, self.rx_buf)
# self.file.write(data)
[docs]class Mouse(object):
'''Mouse'''
def __init__(self):
self.value = 0
[docs] def read(self):
return self.value
[docs] def write(self, word):
raise NotImplementedError
[docs] def set_coords(self, x, y):
self.value = self.value & 0xFF000000 | x | (y << 12)
[docs]class Clock(object):
'''clock'''
def __init__(self, now=None):
self.reset(now)
[docs] def read(self):
return self.time() - self.start_time
[docs] def write(self, word): # RESERVED
raise NotImplementedError
[docs] def reset(self, now=None):
self.start_time = now or self.time()
[docs] def time(self):
'''Return int time in ms.'''
return int(round(1000 * time()))
[docs]class LEDs(object):
'''LEDs'''
def __init__(self):
self.switches = 0
[docs] def read(self):
return self.switches
[docs] def write(self, word):
print('LEDs', bin(word)[2:])
[docs]class FakeSPI(object):
'''SPI'''
def __init__(self):
self.things = {}
self.current_thing = None
self.data = DataControl(self)
[docs] def register(self, index, thing):
self.things[index] = thing
[docs] def read(self):
log('FakeSPI Control Read: 0x1')
return 1
[docs] def write(self, word):
log('FakeSPI Control Write: 0x%x', word)
word %= 4
try:
self.current_thing = self.things[word]
log('Setting SPI device to %s', self.current_thing)
except KeyError:
log('No SPI device %i', word)
self.current_thing = None
[docs]class Keyboard:
'''
pdewacht/oberon-risc-emu/blob/master/src/sdl-ps2.c
'''
def __init__(self, initial_keys=()):
self.buffer = list(initial_keys)
[docs] def read(self):
return self.buffer.pop(0) if self.buffer else 0
[docs]class DataControl(object):
def __init__(self, spi):
self.spi = spi
[docs] def read(self):
if self.spi.current_thing:
data = self.spi.current_thing.read()
else:
data = 0xFF
log('FakeSPI Data Read: 0x%x', data)
return data
[docs] def write(self, word):
log('FakeSPI Data Write: 0x%x', word)
if self.spi.current_thing:
self.spi.current_thing.write(word)
[docs]class SerialStatus(object):
def __init__(self, ser):
self.ser = ser
[docs] def read(self):
return 1
[docs] def write(self, word):
2 / 0
[docs]class Serial(object):
def __init__(self, input_file):
self.input_file = input_file
self.status = SerialStatus(self)
[docs] def read(self):
next_byte = self.input_file.read(1)
if not next_byte:
raise RuntimeError('Out of serial input.')
# print(f'serial port read: {(next_byte)}')
return ord(next_byte)
[docs] def write(self, word):
if word & 0xFFFFFF00:
# There are bits in the high bytes!
print(f'\nwoot! 0x{word:08x}')
else:
print(chr(word & 0xFF), end='')
sys.stdout.flush() # damnit