Initial commit

This commit is contained in:
Hrvoje Cavrak 2015-12-07 17:21:31 +01:00
commit dfab5b4e04
6 changed files with 390 additions and 0 deletions

BIN
ORAO13.ROM Normal file

Binary file not shown.

52
README.MD Normal file
View file

@ -0,0 +1,52 @@
Orao Emulator
============
[Orao](https://en.wikipedia.org/wiki/Orao_%28computer%29) is a Croatian 8-bit
computer used primarily in elementary schools, as part of a computer literacy
initiative.
This is an attempt of an emulator written in Python 2, licensed under the terms
of the GNU General Public License, version 2 or later (GPLv2+).
It implements the 6502 CPU and all peripherals without external dependencies,
and it's meant to be as short as possible (267 SLOC). Sometimes it cuts
corners with readability and it's not PEP-8 compliant, but it wouldn't be
fun to have an emulator 2000 lines of boring, by the book code. Would it? :-)
Additional Requirements
------------------------------
1. Pygame
2. Numpy
Instructions
-------------
Run with python orao.py, enter BASIC using "BC". When asked about the memory
size, press ENTER unless you don't want BASIC to use the entire available
memory.
WAV files containing programs / games should be located in the wav folder.
To save a BASIC program, use SAVE "filename" - a corresponding FILENAME.WAV
will be created. Load it back using LOAD "filename".
DMEM "filename", ADDR, LENGTH can also be used to save a fragment of memory,
beginning at address ADDR and of length LENGTH (in decimal). LMEM "filename"
loads it back.
Clicking the POWER button does a warm reboot. You can then go back to BASIC
with BW to keep the memory intact, or re-initialize with BC. The enclosed PDF
describes the monitor mode and commands, but it is in Croatian only (Google
Translate can help).
Try LMEM "TETRIS" and run it using LNK4096. You can get double quotes by
pressing shift + 2. Erase by using the left arrow.
It *should* be cross-platform.
Known bugs
----------
Pygame and Pulseaudio crash occasionally.

338
orao.py Normal file
View file

@ -0,0 +1,338 @@
# -*- coding: utf8 -*-
import pygame, numpy, sys, datetime, wave, time
class CPU(object):
CARRY, ZERO, INTERRUPT, DECIMAL, BREAK, UNUSED, OVERFLOW, NEGATIVE = [2**i for i in range(8)]
def __init__(self, memory):
s, self.tape_out, self.filename, self.samples = self, None, None, 0
s.memory, s.tape, s.flipflop, s.last_sound_cycles, s.sndbuf = memory, None, 0, -20000, []
s.cycles, s.pc, s.flags, s.sp, s.a, s.x, s.y = 0, 0xFF89, 48, 0xFF, 0, 0, 0
s._opcodes = { 0x00:(s.BRK,s.no,7), 0x01:(s.ORA,s.ix,6), 0x05:(s.ORA,s.zp,3),
0x06:(s.ASL,s.zp,5), 0x08:(s.PHP,s.no,3), 0x09:(s.ORA,s.im,2), 0x0a:(s.ASL,s.no,2),
0x0d:(s.ORA,s.ab,4), 0x0e:(s.ASL,s.ab,6), 0x10:(s.BPL,s.re,4), 0x11:(s.ORA,s.iy,6),
0x15:(s.ORA,s.zx,4), 0x16:(s.ASL,s.zx,6), 0x18:(s.CLC,s.no,2), 0x19:(s.ORA,s.ay,5),
0x1d:(s.ORA,s.ax,5), 0x1e:(s.ASL,s.ax,7), 0x20:(s.JSR,s.jm,6), 0x21:(s.AND,s.ix,6),
0x24:(s.BIT,s.zp,3), 0x25:(s.AND,s.zp,3), 0x26:(s.ROL,s.zp,5), 0x28:(s.PLP,s.no,4),
0x29:(s.AND,s.im,2), 0x2a:(s.ROL,s.no,2), 0x2c:(s.BIT,s.ab,4), 0x2d:(s.AND,s.ab,4),
0x2e:(s.ROL,s.ab,6), 0x30:(s.BMI,s.re,4), 0x31:(s.AND,s.iy,6), 0x35:(s.AND,s.zx,4),
0x36:(s.ROL,s.zx,6), 0x38:(s.SEC,s.no,2), 0x39:(s.AND,s.ay,5), 0x3d:(s.AND,s.ax,5),
0x3e:(s.ROL,s.ax,7), 0x40:(s.RTI,s.no,6), 0x41:(s.EOR,s.ix,6), 0x45:(s.EOR,s.zp,3),
0x46:(s.LSR,s.zp,5), 0x48:(s.PHA,s.no,3), 0x49:(s.EOR,s.im,2), 0x4a:(s.LSR,s.no,2),
0x4c:(s.JMP,s.jm,3), 0x4d:(s.EOR,s.ab,4), 0x4e:(s.LSR,s.ab,6), 0x50:(s.BVC,s.re,4),
0x51:(s.EOR,s.iy,6), 0x55:(s.EOR,s.zx,4), 0x56:(s.LSR,s.zx,6), 0x58:(s.CLI,s.no,2),
0x59:(s.EOR,s.ay,5), 0x5d:(s.EOR,s.ax,5), 0x5e:(s.LSR,s.ax,7), 0x60:(s.RTS,s.no,6),
0x61:(s.ADC,s.ix,6), 0x65:(s.ADC,s.zp,3), 0x66:(s.ROR,s.zp,5), 0x68:(s.PLA,s.no,4),
0x69:(s.ADC,s.im,2), 0x6a:(s.ROR,s.no,2), 0x6c:(s.JMP,s.id,5), 0x6d:(s.ADC,s.ab,4),
0x6e:(s.ROR,s.ab,6), 0x70:(s.BVS,s.re,3), 0x71:(s.ADC,s.iy,6), 0x75:(s.ADC,s.zx,4),
0x76:(s.ROR,s.zx,6), 0x78:(s.SEI,s.no,2), 0x79:(s.ADC,s.ay,5), 0x7d:(s.ADC,s.ax,5),
0x7e:(s.ROR,s.ax,7), 0x81:(s.STA,s.ix,6), 0x84:(s.STY,s.zp,3), 0x85:(s.STA,s.zp,3),
0x86:(s.STX,s.zp,3), 0x88:(s.DEY,s.no,2), 0x8a:(s.TXA,s.no,2), 0x8c:(s.STY,s.ab,4),
0x8d:(s.STA,s.ab,4), 0x8e:(s.STX,s.ab,4), 0x90:(s.BCC,s.re,4), 0x91:(s.STA,s.iy,6),
0x94:(s.STY,s.zx,4), 0x95:(s.STA,s.zx,4), 0x96:(s.STX,s.zy,4), 0x98:(s.TYA,s.no,2),
0x99:(s.STA,s.ay,5), 0x9a:(s.TXS,s.no,2), 0x9d:(s.STA,s.ax,5), 0xa0:(s.LDY,s.im,2),
0xa1:(s.LDA,s.ix,6), 0xa2:(s.LDX,s.im,2), 0xa4:(s.LDY,s.zp,3), 0xa5:(s.LDA,s.zp,3),
0xa6:(s.LDX,s.zp,3), 0xa8:(s.TAY,s.no,2), 0xa9:(s.LDA,s.im,2), 0xaa:(s.TAX,s.no,2),
0xac:(s.LDY,s.ab,4), 0xad:(s.LDA,s.ab,4), 0xae:(s.LDX,s.ab,4), 0xb0:(s.BCS,s.re,4),
0xb1:(s.LDA,s.iy,6), 0xb4:(s.LDY,s.zx,4), 0xb5:(s.LDA,s.zx,4), 0xb6:(s.LDX,s.zy,4),
0xb8:(s.CLV,s.no,2), 0xb9:(s.LDA,s.ay,5), 0xba:(s.TSX,s.no,2), 0xbc:(s.LDY,s.ax,5),
0xbd:(s.LDA,s.ax,5), 0xbe:(s.LDX,s.ay,5), 0xc0:(s.CPY,s.im,2), 0xc1:(s.CMP,s.ix,6),
0xc4:(s.CPY,s.zp,3), 0xc5:(s.CMP,s.zp,3), 0xc6:(s.DEC,s.zp,5), 0xc8:(s.INY,s.no,2),
0xc9:(s.CMP,s.im,2), 0xca:(s.DEX,s.no,2), 0xcc:(s.CPY,s.ab,4), 0xcd:(s.CMP,s.ab,4),
0xce:(s.DEC,s.ab,3), 0xd0:(s.BNE,s.re,4), 0xd1:(s.CMP,s.iy,6), 0xd5:(s.CMP,s.zx,4),
0xd6:(s.DEC,s.zx,6), 0xd8:(s.CLD,s.no,2), 0xd9:(s.CMP,s.ay,5), 0xdd:(s.CMP,s.ax,5),
0xde:(s.DEC,s.ax,7), 0xe0:(s.CPX,s.im,2), 0xe1:(s.SBC,s.ix,6), 0xe4:(s.CPX,s.zp,3),
0xe5:(s.SBC,s.zp,3), 0xe6:(s.INC,s.zp,5), 0xe8:(s.INX,s.no,2), 0xe9:(s.SBC,s.im,2),
0xea:(s.NOP,s.no,2), 0xec:(s.CPX,s.ab,4), 0xed:(s.SBC,s.ab,4), 0xee:(s.INC,s.ab,6),
0xf0:(s.BEQ,s.re,4), 0xf1:(s.SBC,s.iy,6), 0xf5:(s.SBC,s.zx,4), 0xf6:(s.INC,s.zx,6),
0xf8:(s.SED,s.no,2), 0xf9:(s.SBC,s.ay,5), 0xfd:(s.SBC,s.ax,5), 0xfe:(s.INC,s.ax,7)}
s._kbd = {0x83FE: (112, 240, 185, 39), 0x83FF: (45, 48), # [p đ š ;] [- 0]
0x85FE: (232, 230, 190, 43), 0x85FF: (8, 94), # [č ć ž :] [BS ^]
0x86FE: (102, 104, 103, 110), 0x86FF: (98, 118), # [f h g n] [b v]
0x877E: (100, 97, 115, 122), 0x877F: (120, 99), # [d a s z] [x c]
0x87BE: (108, 106, 107, 109), 0x87BF: (44, 46), # [l j k m] [, .]
0x87DE: (101, 113, 119, 49), 0x87DF: (50, 51), # [e q w l] [2 3]
0x87EE: (111, 105, 117, 55), 0x87EF: (56, 57), # [o i u 7] [8 9]
0x87F6: (114, 121, 116, 54), 0x87F7: (53, 52), # [r y t 6] [5 4]
0x87FA: (282, 283, 284, 285), 0x87FD: (13, 306), # [f1f2f3f4] [cr l_ctrl]
0x87FC: (276, 273, 274, 275), 0x87FB: (32, 304)} # [arrows] [spc l_shift]
s.ticks = {s.im: 1, s.zp: 1, s.zx: 1, s.zy: 1, s.ab: 2, s.ax: 2, s.no: 0,
s.ay: 2, s.jm: 2, s.id: 2, s.ix: 1, s.iy: 1, s.re: 1}
self.screen = pygame.display.set_mode((800, 900))
self.terminal = pygame.Surface((256, 256), pygame.SRCALPHA, depth=32)
self.terminal.fill((255, 255, 255))
self.alphaarray = pygame.surfarray.pixels_alpha(self.terminal)
self.background = pygame.image.load("pozadina.png").convert_alpha()
def get_flag(self, flag): return self.flags & flag != 0
def set_flag(self, flag, boolean):
self.flags = self.flags | flag if boolean else self.flags & ~(flag)
def set_nz(self, src):
self.set_flag(self.ZERO, src & 0xFF == 0)
self.set_flag(self.NEGATIVE, src & 0x80)
return src
def get_word(self, addr): return 256 * self.get_byte(addr + 1) + self.get_byte(addr)
def get_filename(self): return 'wav/{0}.WAV'.format(str(self.memory[592:602]).rstrip())
def speaker(self):
self.flipflop ^= 1
self.samples = int((self.cycles - self.last_sound_cycles) / 22.675)
if 1 < self.samples < 1000: # Ogranici frekventno podrucje
self.sndbuf += [255] * self.samples + [0] * self.samples
self.last_sound_cycles = self.cycles
def get_byte(self, addr):
if addr == 0x87FF: # Adresa ulaza kasetofona
if not self.tape:
self.tape = (255*(ord(j)>128) for i in \
wave.open(self.get_filename()).readframes(2**24) for j in 2*i)
try:
return self.tape.next()
except StopIteration:
self.tape = None
return 0x00
if addr == 0x8800: self.speaker() # Zvucnik
return self.memory[addr] if addr is not None else self.a
def store_byte(self, addr, val):
if addr is None: # Akumulator
self.a = val & 0xFF
return
if addr == 0x8800: self.speaker() # Zvucnik
if 0x6000 <= addr <= 0x7FFF: # Video RAM
y, x = divmod((addr - 0x6000) * 8, 256)
for i in range(8):
self.alphaarray[x+i, y] = 255 if (val>>i) & 1 else 0 # Transparency mask
self.memory[addr] = val & 0xFF
def stack_push(self, value):
self.store_byte(256 + self.sp, value & 0xFF)
self.sp = (self.sp - 1) & 0xFF
def stack_pop(self):
self.sp = (self.sp + 1) & 0xFF
return self.get_byte(256 + self.sp)
def stack_push_word(self, val): map(self.stack_push, [(val >> 8) & 0xFF, val & 0xFF])
def stack_pop_word(self): return self.stack_pop() + (self.stack_pop() << 8)
###########################################################################
# Adresni nacini
def im(self): return self.pc
def zp(self): return self.get_byte(self.pc)
def zx(self): return (self.zp() + self.x) & 0xFF
def zy(self): return (self.zp() + self.y) & 0xFF
def ab(self): return self.get_word(self.pc)
def ax(self): return (self.ab() + self.x) & 0xFFFF
def ay(self): return (self.ab() + self.y) & 0xFFFF
def ix(self): return self.get_word((self.zp() + self.x) & 0xFF)
def iy(self): return (self.get_word(self.zp()) + self.y) & 0xFFFF
def id(self): return self.get_word(self.ab())
def jm(self): return self.ab()
def no(self): return None
def re(self):
loc = self.zp()
addr = loc - 256 * (loc > 127) if loc & self.NEGATIVE else loc
return (self.pc + addr) & 0xFFFF
###########################################################################
# Instrukcije
def TAX(self, d): self.x = self.set_nz(self.a)
def TXA(self, d): self.a = self.set_nz(self.x)
def TAY(self, d): self.y = self.set_nz(self.a)
def TYA(self, d): self.a = self.set_nz(self.y)
def TSX(self, d): self.x = self.set_nz(self.sp)
def TXS(self, d): self.sp = self.x
def LDA(self, addr): self.a = self.set_nz(self.get_byte(addr))
def LDX(self, addr): self.x = self.set_nz(self.get_byte(addr))
def LDY(self, addr): self.y = self.set_nz(self.get_byte(addr))
def STA(self, addr): self.store_byte(addr, self.a)
def STX(self, addr): self.store_byte(addr, self.x)
def STY(self, addr): self.store_byte(addr, self.y)
def AND(self, addr): self.a = self.set_nz(self.get_byte(addr) & self.a)
def ORA(self, addr): self.a = self.set_nz(self.get_byte(addr) | self.a)
def EOR(self, addr): self.a = self.set_nz(self.get_byte(addr) ^ self.a)
def CLC(self, d): self.set_flag(self.CARRY, False)
def SEC(self, d): self.set_flag(self.CARRY, True)
def CLD(self, d): self.set_flag(self.DECIMAL, False)
def SED(self, d): self.set_flag(self.DECIMAL, True)
def CLI(self, d): self.set_flag(self.INTERRUPT, False)
def SEI(self, d): self.set_flag(self.INTERRUPT, True)
def CLV(self, d): self.set_flag(self.OVERFLOW, False)
def INX(self, d): self.x = self.set_nz((self.x + 1) & 0xFF)
def INY(self, d): self.y = self.set_nz((self.y + 1) & 0xFF)
def DEX(self, d): self.x = self.set_nz((self.x - 1) & 0xFF)
def DEY(self, d): self.y = self.set_nz((self.y - 1) & 0xFF)
def INC(self, addr): self.store_byte(addr, self.set_nz(self.get_byte(addr) + 1))
def DEC(self, addr): self.store_byte(addr, self.set_nz(self.get_byte(addr) - 1))
def ASL(self, addr):
operand = self.get_byte(addr)
self.set_flag(self.CARRY, operand & 0x80)
operand = (operand << 1) & 0xFE
self.store_byte(addr, self.set_nz(operand))
def BIT(self, addr):
op = self.get_byte(addr)
self.set_flag(self.ZERO, op & self.a == 0)
self.set_flag(self.NEGATIVE, op & self.NEGATIVE)
self.set_flag(self.OVERFLOW, op & self.OVERFLOW)
def PHP(self, d): self.stack_push(self.flags | self.BREAK | self.UNUSED)
def PHA(self, d): self.stack_push(self.a)
def PLP(self, d): self.flags = self.stack_pop()
def PLA(self, d): self.a = self.set_nz(self.stack_pop())
def NOP(self, d): pass
def ROR(self, addr):
value = self.get_byte(addr) >> 1 | self.get_flag(self.CARRY) * 128
self.set_flag(self.CARRY, self.get_byte(addr) & 1)
self.store_byte(addr, self.set_nz(value))
def ROL(self, addr):
value = self.get_byte(addr) * 2 + self.get_flag(self.CARRY)
self.set_flag(self.CARRY, value > 255)
self.store_byte(addr, self.set_nz(value & 0xFF))
def BRK(self, d):
self.stack_push_word((self.pc + 1) & 0xFFFF)
self.set_flag(self.BREAK, True)
self.PHP(0)
self.SEI(0)
self.pc = self.get_word(0xFFFE) # IRQ
def JMP(self, addr): self.pc = addr - 2
def JSR(self, addr):
if addr == 0xE7B7 and self.pc > 0xC000: # samo i jedino ako rutinu poziva ROM
if not self.tape_out:
self.tape_out = wave.open(self.get_filename(), 'w')
self.tape_out.setparams((1, 1, 44100, 0, 'NONE', 'not compressed'))
self.tape_out.writeframes(chr(255 * self.flipflop) * (1 + (self.y > 15)) * 10)
self.stack_push_word((self.pc + 1) & 0xFFFF)
self.pc = addr - 2
def LSR(self, addr):
self.set_flag(self.NEGATIVE, False)
self.set_flag(self.CARRY, self.get_byte(addr) & 1)
self.store_byte(addr, (self.get_byte(addr) >> 1) & 0x7F)
self.set_flag(self.ZERO, self.get_byte(addr) == 0)
def RTI(self, d):
self.flags = self.stack_pop() | self.BREAK | self.UNUSED
self.pc = self.stack_pop_word()
def RTS(self, d):
self.pc = self.stack_pop_word()
self.pc = (self.pc + 1) & 0xFFFF
def ADDITION(self, arg):
result = (arg & 0XFF) + self.a + self.get_flag(self.CARRY)
self.set_flag(self.OVERFLOW, (~(arg ^ self.a)) & (self.a ^ result) & 0x80)
self.set_flag(self.CARRY, result > 255)
self.a = self.set_nz(result) & 0xFF
def ADC(self, addr): self.ADDITION(self.get_byte(addr))
def SBC(self, addr): self.ADDITION(~self.get_byte(addr))
def COMPARE(self, what, addr):
self.set_flag(self.CARRY, self.set_nz(what - self.get_byte(addr)) >= 0)
def CMP(self, addr): self.COMPARE(self.a, addr)
def CPX(self, addr): self.COMPARE(self.x, addr)
def CPY(self, addr): self.COMPARE(self.y, addr)
def BRANCH(self, addr, flag, condition):
if self.get_flag(flag) is condition:
self.pc = addr
self.cycles += 1 # Ako se grana, to je 1 ekstra ciklus
def BCS(self, addr): self.BRANCH(addr, self.CARRY, True)
def BCC(self, addr): self.BRANCH(addr, self.CARRY, False)
def BEQ(self, addr): self.BRANCH(addr, self.ZERO, True)
def BNE(self, addr): self.BRANCH(addr, self.ZERO, False)
def BMI(self, addr): self.BRANCH(addr, self.NEGATIVE, True)
def BPL(self, addr): self.BRANCH(addr, self.NEGATIVE, False)
def BVS(self, addr): self.BRANCH(addr, self.OVERFLOW, True)
def BVC(self, addr): self.BRANCH(addr, self.OVERFLOW, False)
def step(self):
opcode = self.memory[self.pc]
self.pc = self.pc + 1 & 0xFFFF
instruction, addressing, cycles = self._opcodes[opcode]
instruction(addressing())
self.pc += self.ticks[addressing]
self.cycles += cycles
cpu = CPU(bytearray([0xFF]*0xC000) + bytearray(open('ORAO13.ROM', 'rb').read()))
pygame.mixer.pre_init(44100, 8, 1, buffer=2048)
pygame.init()
ratio, cpu.channel, running = 0, pygame.mixer.Channel(0), True
pygame.time.set_timer(pygame.USEREVENT + 1, 40)
while running:
before, previous_loop_cycles = datetime.datetime.now(), cpu.cycles
time_elapsed = lambda: (datetime.datetime.now()-before).microseconds + 1
for i in range(5000):
cpu.step()
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.MOUSEBUTTONDOWN:
x, y = event.pos
if 650 < x < 700 and 720 < y < 790: # Reset button
cpu.__init__(cpu.memory[:]) # Warm reset
if event.type in [pygame.KEYDOWN, pygame.KEYUP]:
for address, keycodes in cpu._kbd.iteritems():
keys = map(pygame.key.get_pressed().__getitem__, keycodes)
cpu.memory[address] = ~numpy.dot(keys, [16,32,64,128][:len(keys)]) & 0xFF
if event.type == pygame.USEREVENT + 1:
cpu.screen.blit(cpu.background, [0, 0])
cpu.screen.blit(pygame.transform.smoothscale(cpu.terminal, (512, 512)), [150, 140])
pygame.display.set_caption('({0:.2f} MHz) Orao Emulator v0.1'.format(ratio))
pygame.display.update()
cpu.tape_out = None if cpu.cycles - cpu.last_sound_cycles > 20000 else cpu.tape_out
if len(cpu.sndbuf) > 4096 or cpu.sndbuf and cpu.cycles - cpu.last_sound_cycles > 20000:
while cpu.channel.get_queue():
if time_elapsed() > 10000: break
cpu.channel.queue(pygame.sndarray.make_sound(numpy.uint8(cpu.sndbuf)))
cpu.sndbuf = []
overshoot = cpu.cycles - previous_loop_cycles - time_elapsed()
pygame.time.wait((overshoot > 0) * overshoot // 1000) # Pričekaj da budemo cycle exact
ratio = 1.0 * (cpu.cycles - previous_loop_cycles) / time_elapsed()
pygame.quit()

BIN
orao_manual.pdf Normal file

Binary file not shown.

BIN
pozadina.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
wav/TETRIS.WAV Normal file

Binary file not shown.