├── .gitignore ├── tox.ini ├── test_binaries ├── hello.bin ├── example.bin ├── hello2.bin └── ique_fibonacci.bin ├── terminals ├── curses-bold_terminal.py ├── debug_terminal.py ├── pygame_terminal.py ├── qt_terminal.py └── curses_terminal.py ├── .gitmodules ├── LICENSE ├── example.dasm16 ├── emuplugin.py ├── tests.py ├── README.md ├── disasm.py ├── plugins ├── terminalplugin.py └── debuggerplugin.py ├── asm.py ├── dcpu16.py └── asm_pyparsing.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.obj 3 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E265,E501 3 | -------------------------------------------------------------------------------- /test_binaries/hello.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtauber/dcpu16py/HEAD/test_binaries/hello.bin -------------------------------------------------------------------------------- /test_binaries/example.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtauber/dcpu16py/HEAD/test_binaries/example.bin -------------------------------------------------------------------------------- /test_binaries/hello2.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtauber/dcpu16py/HEAD/test_binaries/hello2.bin -------------------------------------------------------------------------------- /test_binaries/ique_fibonacci.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtauber/dcpu16py/HEAD/test_binaries/ique_fibonacci.bin -------------------------------------------------------------------------------- /terminals/curses-bold_terminal.py: -------------------------------------------------------------------------------- 1 | import curses_terminal 2 | 3 | 4 | class Terminal(curses_terminal.Terminal): 5 | style_bold = True 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "examples"] 2 | path = examples 3 | url = git://github.com/jtauber/DCPU-16-Examples.git 4 | [submodule "dcpu16os"] 5 | path = dcpu16os 6 | url = git://github.com/jtauber/dcpu16os.git 7 | -------------------------------------------------------------------------------- /terminals/debug_terminal.py: -------------------------------------------------------------------------------- 1 | WIDTH = 80 2 | HEIGHT = 24 3 | 4 | 5 | class Terminal: 6 | width = WIDTH 7 | height = HEIGHT 8 | keys = [] 9 | 10 | def __init__(self, args): 11 | pass 12 | 13 | def update_character(self, row, column, character, color=None): 14 | print("TERMINAL (%d,%d:'%s') %s" % (column, row, chr(character), str(color))) 15 | 16 | def show(self): 17 | pass 18 | 19 | def updatekeys(self): 20 | pass 21 | 22 | def redraw(self): 23 | pass 24 | 25 | def quit(self): 26 | pass 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 James Tauber and contributors. 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /example.dasm16: -------------------------------------------------------------------------------- 1 | ; Try some basic stuff 2 | SET A, 0x30 ; 7c01 0030 3 | SET [0x1000], 0x20 ; 7de1 1000 0020 4 | SUB A, [0x1000] ; 7803 1000 5 | IFN A, 0x10 ; c00d 6 | SET PC, crash ; 7dc1 001a [*] 7 | 8 | ; Do a loopy thing 9 | SET I, 10 ; a861 10 | SET A, 0x2000 ; 7c01 2000 11 | :loop SET [0x2000+I], [A] ; 2161 2000 12 | SUB I, 1 ; 8463 13 | IFN I, 0 ; 806d 14 | SET PC, loop ; 7dc1 000d [*] 15 | 16 | ; Call a subroutine 17 | SET X, 0x4 ; 9031 18 | JSR testsub ; 7c10 0018 [*] 19 | SET PC, crash ; 7dc1 001a [*] 20 | 21 | :testsub SHL X, 4 ; 9037 22 | SET PC, POP ; 61c1 23 | 24 | ; Hang forever. X should now be 0x40 if everything went right. 25 | :crash SET PC, crash ; 7dc1 001a [*] 26 | 27 | ; [*]: Note that these can be one word shorter and one cycle faster by using the short form (0x00-0x1f) of literals, 28 | ; but my assembler doesn't support short form labels yet. 29 | -------------------------------------------------------------------------------- /emuplugin.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import imp 3 | from os.path import join, basename, splitext, dirname 4 | 5 | PLUGINS_DIR = join(dirname(__file__), "plugins") 6 | 7 | 8 | def importPlugins(dir=PLUGINS_DIR): 9 | # http://tinyurl.com/cfceawr 10 | return [_load(path).plugin for path in glob.glob(join(dir, "[!_]*.py"))] 11 | 12 | 13 | def _load(path): 14 | # http://tinyurl.com/cfceawr 15 | name, ext = splitext(basename(path)) 16 | return imp.load_source(name, path) 17 | 18 | 19 | class BasePlugin: 20 | """ 21 | Plugin module to interface with a cpu core. 22 | 23 | 24 | Signaling a shutdown should be done via raising SystemExit within tick() 25 | """ 26 | 27 | # Specify if you want to use a different name class name 28 | name = None 29 | 30 | # List in the format of [(*args, *kwargs)] 31 | arguments = [] 32 | 33 | # Set in __init__ if you do not wish to have been "loaded" or called 34 | loaded = True 35 | 36 | def tick(self, cpu): 37 | """ 38 | Gets called at the end of every cpu tick 39 | """ 40 | pass 41 | 42 | def shutdown(self): 43 | """ 44 | Gets called on shutdown of the emulator 45 | """ 46 | pass 47 | 48 | def memory_changed(self, cpu, address, value, oldvalue): 49 | """ 50 | Gets called on a write to memory 51 | """ 52 | pass 53 | 54 | def __init__(self, args=None): 55 | self.name = self.__class__.__name__ if not self.name else self.name 56 | 57 | # Assign this to your plugin class 58 | plugin = None 59 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | import nose.tools as nose 2 | import os 3 | import subprocess 4 | 5 | 6 | ASSEMBLY_OUTPUT = "__test_output.obj" 7 | SOURCE_DIR = "examples" 8 | BINARY_DIR = "test_binaries" 9 | 10 | 11 | def tearDownModule(): 12 | if os.path.exists(ASSEMBLY_OUTPUT): 13 | os.remove(ASSEMBLY_OUTPUT) 14 | 15 | 16 | def example(name): 17 | return os.path.join(SOURCE_DIR, name + ".asm") 18 | 19 | 20 | def check_path(assembler, path): 21 | code = subprocess.call([assembler, path, ASSEMBLY_OUTPUT]) 22 | nose.assert_equal(code, 0, "Assembly of {0} failed!".format(path)) 23 | 24 | assert path.endswith(".asm") 25 | binary = os.path.join(BINARY_DIR, os.path.basename(path)[:-4] + ".bin") 26 | if os.path.exists(binary): 27 | with open(ASSEMBLY_OUTPUT, "rb") as testing, open(binary, "rb") as tested: 28 | nose.assert_equal(testing.read(), tested.read(), "Produced and tested binaries differ!") 29 | 30 | 31 | # asm.py 32 | def test_example_asm(): 33 | check_path("./asm.py", "example.asm") 34 | 35 | 36 | def test_hello_asm(): 37 | check_path("./asm.py", example("hello")) 38 | 39 | 40 | def test_hello2_asm(): 41 | check_path("./asm.py", example("hello2")) 42 | 43 | 44 | def test_fibonacci_asm(): 45 | check_path("./asm.py", example("ique_fibonacci")) 46 | 47 | 48 | # asm_pyparsing.py 49 | def test_example_pyparsing(): 50 | check_path("./asm_pyparsing.py", "example.asm") 51 | 52 | 53 | def test_hello_pyparsing(): 54 | check_path("./asm_pyparsing.py", example("hello")) 55 | 56 | 57 | def test_hello2_pyparsing(): 58 | check_path("./asm_pyparsing.py", example("hello2")) 59 | 60 | 61 | def test_fibonacci_pyparsing(): 62 | check_path("./asm_pyparsing.py", example("ique_fibonacci")) 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Python implementation of Notch's DCPU-16. 2 | 3 | Complete with assembler, disassembler, debugger and video terminal implementations. 4 | 5 | See http://0x10c.com/doc/dcpu-16.txt for specification of the CPU. 6 | 7 | Notch apparently started doing a 6502 emulator first. Given I did one in 8 | Python it only seems fitting I now 9 | do a DCPU-16 implementation in Python too :-) 10 | 11 | 12 | ## Status 13 | 14 | Runs a number of example programs successfully. Should be feature-complete at the CPU level. 15 | 16 | A dissassembler and (two) assemblers are also included as well as the emulator. The emulator 17 | includes a debugger. 18 | 19 | * `./asm.py example.dasm16 example.obj` will assemble Notch's example to object code 20 | * `./disasm.py example.obj` will disassemble the given object code 21 | * `./dcpu16.py example.obj` will execute it (but won't show anything without extra options) 22 | 23 | There is also an experimental pyparsing-based assembler `./asm_pyparsing.py` 24 | contributed by Peter Waller. You'll need to `pip install pyparsing` to run it. 25 | 26 | `./dcpu16.py` takes a number of options: 27 | 28 | * `--debug` runs the emulate in debug mode, enabling you to step through each instruction 29 | * `--trace` dumps the registers and stack after every step (implied by `--debug`) 30 | * `--speed` outputs the speed the emulator is running at in kHz 31 | * `--term TERM` specifies a terminal to use for text output (`null`, `debug`, `curses`, `pygame` or `qt`) 32 | 33 | I'm working on an operating system for the DCPU-16 at 34 | [https://github.com/jtauber/dcpu16os](https://github.com/jtauber/dcpu16os) and also plan an 35 | implementation of Forth at some point. 36 | 37 | ## Examples 38 | 39 | Now see [https://github.com/jtauber/DCPU-16-Examples](https://github.com/jtauber/DCPU-16-Examples) 40 | -------------------------------------------------------------------------------- /terminals/pygame_terminal.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | 4 | class Terminal: 5 | 6 | COLORS = [ 7 | (0, 0, 0), 8 | (255, 0, 0), 9 | (0, 255, 0), 10 | (255, 255, 0), 11 | (0, 0, 255), 12 | (255, 0, 255), 13 | (0, 255, 255), 14 | (255, 255, 255) 15 | ] 16 | 17 | def __init__(self, args): 18 | self.width = args.width 19 | self.height = args.height 20 | self.keys = [] 21 | pygame.font.init() 22 | self.font = pygame.font.match_font("Monospace,dejavusansmono") 23 | self.font = pygame.font.get_default_font() if not self.font else self.font 24 | self.font = pygame.font.Font(self.font, 12) 25 | self.cell_width = max([self.font.metrics(chr(c))[0][1] for c in range(0, 128)]) 26 | self.cell_height = self.font.get_height() 27 | win_width = self.cell_width * args.width 28 | win_height = self.cell_height * args.height 29 | self.screen = pygame.display.set_mode((win_width, win_height)) 30 | 31 | def update_character(self, row, column, character, color=None): 32 | if not color or (not color[0] and not color[1]): 33 | fgcolor = self.COLORS[7] 34 | bgcolor = self.COLORS[0] 35 | else: 36 | fgcolor = self.COLORS[color[0]] 37 | bgcolor = self.COLORS[color[1]] 38 | surf = pygame.Surface((self.cell_width, self.cell_height)) 39 | surf.fill(pygame.Color(*bgcolor)) 40 | char = self.font.render(chr(character), True, fgcolor) 41 | surf.blit(char, (1, 1)) 42 | self.screen.blit(surf, (column * self.cell_width, row * self.cell_height)) 43 | 44 | def show(self): 45 | pass 46 | 47 | def updatekeys(self): 48 | events = pygame.event.get(pygame.KEYDOWN) 49 | for e in events: 50 | key = e.unicode 51 | if key: 52 | self.keys.insert(0, ord(e.unicode)) 53 | 54 | def redraw(self): 55 | pygame.display.flip() 56 | 57 | def quit(self): 58 | pass 59 | -------------------------------------------------------------------------------- /disasm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import print_function 4 | 5 | import struct 6 | import sys 7 | import argparse 8 | 9 | 10 | INSTRUCTIONS = [None, "SET", "ADD", "SUB", "MUL", "DIV", "MOD", "SHL", "SHR", "AND", "BOR", "XOR", "IFE", "IFN", "IFG", "IFB"] 11 | IDENTIFERS = ["A", "B", "C", "X", "Y", "Z", "I", "J", "POP", "PEEK", "PUSH", "SP", "PC", "O"] 12 | 13 | 14 | class Disassembler: 15 | 16 | def __init__(self, program, output=sys.stdout): 17 | self.program = program 18 | self.offset = 0 19 | self.output = output 20 | 21 | def next_word(self): 22 | w = self.program[self.offset] 23 | self.offset += 1 24 | return w 25 | 26 | def format_operand(self, operand): 27 | if operand < 0x08: 28 | return "%s" % IDENTIFERS[operand] 29 | elif operand < 0x10: 30 | return "[%s]" % IDENTIFERS[operand % 0x08] 31 | elif operand < 0x18: 32 | return "[0x%02x + %s]" % (self.next_word(), IDENTIFERS[operand % 0x10]) 33 | elif operand < 0x1E: 34 | return "%s" % IDENTIFERS[operand % 0x10] 35 | elif operand == 0x1E: 36 | return "[0x%02x]" % self.next_word() 37 | elif operand == 0x1F: 38 | return "0x%02x" % self.next_word() 39 | else: 40 | return "0x%02x" % (operand % 0x20) 41 | 42 | def next_instruction(self): 43 | offset = self.offset 44 | w = self.next_word() 45 | 46 | operands, opcode = divmod(w, 16) 47 | b, a = divmod(operands, 64) 48 | 49 | if opcode == 0x00: 50 | if a == 0x01: 51 | first = "JSR" 52 | else: 53 | return 54 | else: 55 | first = "%s %s," % (INSTRUCTIONS[opcode], self.format_operand(a)) 56 | 57 | asm = "%s %s" % (first, self.format_operand(b)) 58 | binary = " ".join("%04x" % word for word in self.program[offset:self.offset]) 59 | return "%-40s ; %04x: %s" % (asm, offset, binary) 60 | 61 | def run(self): 62 | while self.offset < len(self.program): 63 | print(self.next_instruction(), file=self.output) 64 | 65 | 66 | if __name__ == "__main__": 67 | parser = argparse.ArgumentParser(description="DCPU-16 disassembler") 68 | parser.add_argument("-o", help="Place the output into FILE instead of stdout", metavar="FILE") 69 | parser.add_argument("input", help="File with DCPU object code") 70 | args = parser.parse_args() 71 | 72 | program = [] 73 | if args.input == "-": 74 | f = sys.stdin 75 | else: 76 | f = open(args.input, "rb") 77 | word = f.read(2) 78 | while word: 79 | program.append(struct.unpack(">H", word)[0]) 80 | word = f.read(2) 81 | 82 | output = sys.stdout if args.o is None else open(args.o, "w") 83 | d = Disassembler(program, output=output) 84 | d.run() 85 | -------------------------------------------------------------------------------- /terminals/qt_terminal.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from PyQt4 import QtGui 3 | from PyQt4.QtCore import Qt 4 | 5 | # Ensure that the QT application does not try to handle (and spam) the KeyboardInterrupt 6 | import signal 7 | signal.signal(signal.SIGINT, signal.SIG_DFL) 8 | 9 | 10 | class Terminal(QtGui.QWidget): 11 | COLORS = [ 12 | (0, 0, 0), 13 | (255, 0, 0), 14 | (0, 255, 0), 15 | (255, 255, 0), 16 | (0, 0, 255), 17 | (255, 0, 255), 18 | (0, 255, 255), 19 | (255, 255, 255) 20 | ] 21 | 22 | def __init__(self, args): 23 | self.width = args.width 24 | self.height = args.height 25 | self.keys = [] 26 | self.app = QtGui.QApplication(sys.argv) 27 | super(Terminal, self).__init__() 28 | 29 | self.font = QtGui.QFont("Monospace", 10) 30 | self.font.setStyleHint(QtGui.QFont.TypeWriter) 31 | font_metrics = QtGui.QFontMetrics(self.font) 32 | self.cell_width = font_metrics.maxWidth() + 2 33 | self.cell_height = font_metrics.height() 34 | win_width = self.cell_width * args.width 35 | win_height = self.cell_height * args.height 36 | 37 | self.pixmap_buffer = QtGui.QPixmap(win_width, win_height) 38 | self.pixmap_buffer.fill(Qt.black) 39 | 40 | self.resize(win_width, win_height) 41 | self.setMinimumSize(win_width, win_height) 42 | self.setMaximumSize(win_width, win_height) 43 | self.setWindowTitle("DCPU-16 terminal") 44 | 45 | self.app.setQuitOnLastWindowClosed(False) 46 | self.closed = False 47 | 48 | def update_character(self, row, column, character, color=None): 49 | char = chr(character) 50 | x = column * self.cell_width 51 | y = row * self.cell_height 52 | 53 | qp = QtGui.QPainter(self.pixmap_buffer) 54 | qp.setFont(self.font) 55 | if not color or (not color[0] and not color[1]): 56 | fgcolor = self.COLORS[7] 57 | bgcolor = self.COLORS[0] 58 | else: 59 | fgcolor = self.COLORS[color[0]] 60 | bgcolor = self.COLORS[color[1]] 61 | qp.fillRect(x, y, self.cell_width, self.cell_height, QtGui.QColor(*bgcolor)) 62 | qp.setPen(QtGui.QColor(*fgcolor)) 63 | qp.drawText(x, y, self.cell_width, self.cell_height, Qt.AlignCenter, char) 64 | qp.end() 65 | 66 | def closeEvent(self, e): 67 | self.closed = True 68 | 69 | def keyPressEvent(self, e): 70 | for c in str(e.text()): 71 | self.keys.insert(0, ord(c)) 72 | 73 | def updatekeys(self): 74 | pass 75 | 76 | def redraw(self): 77 | if self.closed: 78 | raise SystemExit 79 | self.update() 80 | self.app.processEvents() 81 | 82 | def quit(self): 83 | self.app.quit() 84 | 85 | def paintEvent(self, event): 86 | qp = QtGui.QPainter(self) 87 | qp.drawPixmap(0, 0, self.pixmap_buffer) 88 | qp.end() 89 | -------------------------------------------------------------------------------- /terminals/curses_terminal.py: -------------------------------------------------------------------------------- 1 | import curses 2 | 3 | 4 | class Terminal: 5 | style_bold = False 6 | keymap = {'A': 0x3, 'C': 0x2, 'D': 0x1} 7 | 8 | def setup_colors(self): 9 | curses.start_color() 10 | curses.use_default_colors() 11 | self.colors = {} 12 | self.colors[(0, 0)] = 0 13 | self.colors[(7, 0)] = 0 14 | self.color_index = 1 15 | self.win.bkgd(curses.color_pair(0)) 16 | 17 | def __init__(self, args): 18 | if args.debug: 19 | print("Curses conflicts with debugger") 20 | raise SystemExit 21 | self.win = curses.initscr() 22 | self.win.nodelay(1) 23 | self.win_height, self.win_width = self.win.getmaxyx() 24 | 25 | curses.curs_set(0) 26 | curses.noecho() 27 | self.width = args.width 28 | self.height = args.height 29 | self.keys = [] 30 | self.setup_colors() 31 | 32 | def get_color(self, fg, bg): 33 | if (fg, bg) not in self.colors: 34 | curses.init_pair(self.color_index, fg, bg) 35 | self.colors[(fg, bg)] = self.color_index 36 | self.color_index += 1 37 | 38 | return self.colors[(fg, bg)] 39 | 40 | def update_character(self, row, column, character, color=None): 41 | try: 42 | pair = 0 43 | if color: 44 | pair = self.get_color(*color) 45 | color = curses.color_pair(pair) 46 | if self.style_bold: 47 | color |= curses.A_BOLD 48 | self.win.addch(row, column, character, color) 49 | except curses.error: 50 | pass 51 | 52 | def show(self): 53 | color = curses.color_pair(self.get_color(3, -1)) 54 | 55 | if self.win_width > self.width: 56 | try: 57 | s = "." * (self.win_width - self.width) 58 | for y in range(self.height): 59 | self.win.addstr(y, self.width, s, color) 60 | except curses.error: 61 | pass 62 | 63 | if self.win_height > self.height: 64 | try: 65 | s = "." * (self.win_width) 66 | for y in range(self.height, self.win_height): 67 | self.win.addstr(y, 0, s, color) 68 | except curses.error: 69 | pass 70 | 71 | def updatekeys(self): 72 | try: 73 | # XXX: this is probably a bad place to check if the window has 74 | # resized but there is no other opportunity to do this 75 | win_height, win_width = self.win.getmaxyx() 76 | if win_height != self.win_height or win_width != self.win_width: 77 | self.win_height, self.win_width = win_height, win_width 78 | self.show() 79 | 80 | while(True): 81 | char = self.win.getkey() 82 | if len(char) == 1: 83 | c = self.keymap[char] if char in self.keymap else ord(char) 84 | self.keys.insert(0, c) 85 | except curses.error: 86 | pass 87 | 88 | def redraw(self): 89 | self.win.refresh() 90 | 91 | def quit(self): 92 | curses.endwin() 93 | -------------------------------------------------------------------------------- /plugins/terminalplugin.py: -------------------------------------------------------------------------------- 1 | from emuplugin import BasePlugin 2 | import importlib 3 | import sys 4 | import time 5 | import os 6 | import re 7 | 8 | START_ADDRESS = 0x8000 9 | MIN_DISPLAY_HZ = 60 10 | 11 | 12 | class TerminalPlugin(BasePlugin): 13 | """ 14 | A plugin to implement terminal selection 15 | """ 16 | 17 | arguments = [ 18 | (["--term"], dict(action="store", default="null", help="Terminal to use (e.g. null, pygame)")), 19 | (["--geometry"], dict(action="store", default="80x24", help="Geometry given as `width`x`height`", metavar="SIZE"))] 20 | 21 | def processkeys(self, cpu): 22 | keyptr = 0x9000 23 | for i in range(0, 16): 24 | if not cpu.memory[keyptr + i]: 25 | try: 26 | key = self.term.keys.pop() 27 | except IndexError: 28 | break 29 | cpu.memory[keyptr + i] = key 30 | 31 | def tick(self, cpu): 32 | """ 33 | Update the display every .1s or always if debug is on 34 | """ 35 | if self.debug or not self.time or (time.time() - self.time >= 1.0 / float(MIN_DISPLAY_HZ)): 36 | self.time = time.time() 37 | self.term.redraw() 38 | self.term.updatekeys() 39 | if self.term.keys: 40 | self.processkeys(cpu) 41 | 42 | def memory_changed(self, cpu, address, value, oldval): 43 | """ 44 | Inform the terminal that the memory is updated 45 | """ 46 | if START_ADDRESS <= address <= START_ADDRESS + self.term.width * self.term.height: 47 | row, column = divmod(address - START_ADDRESS, self.term.width) 48 | ch = value % 0x0080 49 | ch = ord(' ') if not ch else ch 50 | fg = (value & 0x4000) >> 14 | (value & 0x2000) >> 12 | (value & 0x1000) >> 10 51 | bg = (value & 0x400) >> 10 | (value & 0x200) >> 8 | (value & 0x100) >> 6 52 | self.term.update_character(row, column, ch, (fg, bg)) 53 | 54 | def shutdown(self): 55 | """ 56 | Shutdown the terminal 57 | """ 58 | self.term.quit() 59 | 60 | def __init__(self, args): 61 | """ 62 | Create a terminal based on the term argument 63 | """ 64 | if args.term == "null": 65 | self.loaded = False 66 | return 67 | BasePlugin.__init__(self) 68 | self.time = None 69 | sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "terminals"))) 70 | try: 71 | terminal = importlib.import_module(args.term + "_terminal") 72 | except ImportError as e: 73 | print("Terminal %s not available: %s" % (args.term, e)) 74 | raise SystemExit 75 | self.debug = args.debug 76 | 77 | m = re.match(r"(\d+)x(\d+)", args.geometry) 78 | if m is None: 79 | print("Invalid geometry `%s`" % args.geometry) 80 | args.width, args.height = 80, 24 81 | else: 82 | args.width = int(m.group(1)) 83 | args.height = int(m.group(2)) 84 | 85 | self.term = terminal.Terminal(args) 86 | self.name += "-%s" % args.term 87 | self.term.show() 88 | 89 | plugin = TerminalPlugin 90 | -------------------------------------------------------------------------------- /plugins/debuggerplugin.py: -------------------------------------------------------------------------------- 1 | from emuplugin import BasePlugin 2 | import dcpu16 3 | 4 | try: 5 | raw_input 6 | except NameError: 7 | # Python3 raw_input was renamed to input 8 | raw_input = input 9 | 10 | 11 | class DebuggerPlugin(BasePlugin): 12 | """ 13 | A plugin to implement a debugger 14 | """ 15 | 16 | def __init__(self, args): 17 | """ 18 | Enable debugger if args.debug is True 19 | """ 20 | BasePlugin.__init__(self) 21 | self.loaded = args.debug 22 | self.debugger_breaks = set() 23 | self.debugger_in_continue = False 24 | 25 | def tick(self, cpu): 26 | self.cpu = cpu 27 | if not self.debugger_in_continue or cpu.memory[dcpu16.PC] in self.debugger_breaks: 28 | self.debugger_in_continue = False 29 | while True: 30 | try: 31 | command = [s.lower() for s in raw_input("debug> ").split()] 32 | except EOFError: 33 | # Ctrl-D 34 | print("") 35 | raise SystemExit 36 | try: 37 | if not command or command[0] in ("step", "st"): 38 | break 39 | elif command[0] == "help": 40 | help_msg = """Commands: 41 | help 42 | st[ep] - (or simply newline) - execute next instruction 43 | g[et]
|% - (also p[rint]) - print value of memory cell or register 44 | s[et]
|% - set value of memory cell or register to 45 | b[reak]
[...] - set breakpoint at given addresses (to be used with 'continue') 46 | cl[ear]
[...] - remove breakpoints from given addresses 47 | c[ont[inue]] - run without debugging prompt until breakpoint is encountered 48 | 49 | All addresses are in hex (you can add '0x' at the beginning) 50 | Close emulator with Ctrl-D 51 | """ 52 | print(help_msg) 53 | elif command[0] in ("get", "g", "print", "p"): 54 | self.debugger_get(*command[1:]) 55 | elif command[0] in ("set", "s"): 56 | self.debugger_set(*command[1:]) 57 | elif command[0] in ("break", "b"): 58 | if len(command) < 2: 59 | raise ValueError("Break command takes at least 1 parameter!") 60 | self.debugger_break(*command[1:]) 61 | elif command[0] in ("clear", "cl"): 62 | self.debugger_clear(*command[1:]) 63 | elif command[0] in ("continue", "cont", "c"): 64 | self.debugger_in_continue = True 65 | break 66 | else: 67 | raise ValueError("Invalid command!") 68 | except ValueError as ex: 69 | print(ex) 70 | 71 | @staticmethod 72 | def debugger_parse_location(what): 73 | registers = "abcxyzij" 74 | specials = ("pc", "sp", "o") 75 | if what.startswith("%"): 76 | what = what[1:] 77 | if what in registers: 78 | return 0x10000 + registers.find(what) 79 | elif what in specials: 80 | return (dcpu16.PC, dcpu16.SP, dcpu16.O)[specials.index(what)] 81 | else: 82 | raise ValueError("Invalid register!") 83 | else: 84 | addr = int(what, 16) 85 | if not 0 <= addr <= 0xFFFF: 86 | raise ValueError("Invalid address!") 87 | return addr 88 | 89 | def debugger_break(self, *addrs): 90 | breaks = set() 91 | for addr in addrs: 92 | addr = int(addr, 16) 93 | if not 0 <= addr <= 0xFFFF: 94 | raise ValueError("Invalid address!") 95 | breaks.add(addr) 96 | self.debugger_breaks.update(breaks) 97 | 98 | def debugger_clear(self, *addrs): 99 | if not addrs: 100 | self.debugger_breaks = set() 101 | else: 102 | breaks = set() 103 | for addr in addrs: 104 | addr = int(addr, 16) 105 | if not 0 <= addr <= 0xFFFF: 106 | raise ValueError("Invalid address!") 107 | breaks.add(addr) 108 | self.debugger_breaks.difference_update(breaks) 109 | 110 | def debugger_set(self, what, value): 111 | value = int(value, 16) 112 | if not 0 <= value <= 0xFFFF: 113 | raise ValueError("Invalid value!") 114 | addr = self.debugger_parse_location(what) 115 | self.cpu.memory[addr] = value 116 | 117 | def debugger_get(self, what): 118 | addr = self.debugger_parse_location(what) 119 | value = self.cpu.memory[addr] 120 | print("hex: {hex}\ndec: {dec}\nbin: {bin}".format(hex=hex(value), dec=value, bin=bin(value))) 121 | 122 | plugin = DebuggerPlugin 123 | -------------------------------------------------------------------------------- /asm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import print_function 4 | 5 | import struct 6 | import re 7 | import sys 8 | import argparse 9 | import os 10 | import codecs 11 | 12 | 13 | def disjunction(*lst): 14 | "make a uppercase/lowercase disjunction out of a list of strings" 15 | return "|".join([item.upper() for item in lst] + [item.lower() for item in lst]) 16 | 17 | 18 | BASIC_INSTRUCTIONS = disjunction("SET", "ADD", "SUB", "MUL", "DIV", "MOD", "SHL", "SHR", "AND", "BOR", "XOR", "IFE", "IFN", "IFG", "IFB") 19 | GENERAL_REGISTERS = disjunction("A", "B", "C", "X", "Y", "Z", "I", "J") 20 | ALL_REGISTERS = disjunction("A", "B", "C", "X", "Y", "Z", "I", "J", "POP", "PEEK", "PUSH", "SP", "PC", "O") 21 | 22 | 23 | def operand_re(prefix): 24 | return """ 25 | ( # operand 26 | (?P<""" + prefix + """register>""" + ALL_REGISTERS + """) # register 27 | | 28 | (\[\s*(?P<""" + prefix + """register_indirect>""" + GENERAL_REGISTERS + """)\s*\]) # register indirect 29 | | 30 | (\[\s*(0x(?P<""" + prefix + """hex_indexed>[0-9A-Fa-f]{1,4}))\s*\+\s*(?P<""" + prefix + """hex_indexed_index>""" + GENERAL_REGISTERS + """)\s*\]) # hex indexed 31 | | 32 | (\[\s*(?P<""" + prefix + """decimal_indexed>\d+)\s*\+\s*(?P<""" + prefix + """decimal_indexed_index>""" + GENERAL_REGISTERS + """)\s*\]) # decimal indexed 33 | | 34 | (\[\s*(?P<""" + prefix + """label_indexed>\w+)\s*\+\s*(?P<""" + prefix + """label_indexed_index>""" + GENERAL_REGISTERS + """)\s*\]) # label indexed 35 | | 36 | (\[\s*(0x(?P<""" + prefix + """hex_indirect>[0-9A-Fa-f]{1,4}))\s*\]) # hex indirect 37 | | 38 | (\[\s*(?P<""" + prefix + """decimal_indirect>\d+)\s*\]) # decimal indirect 39 | | 40 | (0x(?P<""" + prefix + """hex_literal>[0-9A-Fa-f]{1,4})) # hex literal 41 | | 42 | (\[\s*(?P<""" + prefix + """label_indirect>\w+)\s*\]) # label indirect 43 | | 44 | (?P<""" + prefix + """decimal_literal>\d+) # decimal literal 45 | | 46 | (?P<""" + prefix + """label>\w+) # label+ 47 | ) 48 | """ 49 | 50 | line_regex = re.compile(r"""^\s* 51 | (:(?P