A retro-inspired, 16-bit emulator along the lines of the microcomputer era.
Project uses CMake as its build system, but the entire emulator is a single C++ file with no dependencies; it could just
as easily be compiled manually. Run the following commands from the project root to install it to your PATH
on a Linux
system:
mkdir build && cd build && cmake .. -DCMAKE_BUILD_TYPE=Release && cmake --build . && sudo cmake --install .
bedrock <disk0-path> <disk1-path>
Both paths are mandatory, but either disk can be left "disconnected" by passing --
as its path.
The emulator's ISA is a 16-bit, word-addressed load/store architecture with a separate I/O bus address space. 16 registers are supported, along with 16 opcodes. All instructions are one word (16 bits) wide, and follow the same format:
+-----------------------------------------------+
| MSB LSB |
+-----------+-----------+-----------+-----------+
| 4 bits | 4 bits | 4 bits | 4 bits |
+-----------+-----------+-----------+-----------+
| op | dst | src1 | src0 |
+-----------+-----------+-----------+-----------+
Conceptually, the contents of the memory address space and bus address space can be considered as arrays mem
and
bus
, each consisting of 2^16 words. Importantly, bus and memory addresses refer to words, not bytes. Similarly,
registers may be conceptualized as a 16-word array regs
. Opcodes 0x5
-0x8
zero-extend their operands to 32 bits and
only store the low-order word of the result in the destination register. The high-order word is stored in the internal
hi
register, overwriting any previous value. Finally, a 16-bit program counter pc
contains the memory address from
which the next instruction word will be read. At startup, memory, registers, hi
, and pc
are all initialized to zero.
In each execution cycle, the emulator reads the instruction at memory[pc]
, increments pc
, then executes the fetched
instruction word. The following table of opcodes (field op
of the instruction word) will use these conceptual arrays
and a C-like syntax to explain the effects of each instruction.
Opcode Effect
0x0 if (regs[src1]) { uint16_t old_pc = pc; pc = regs[src0]; regs[dst] = old_pc; }
0x1 regs[dst] = hi;
0x2 regs[dst] = src1 << 4 | src0;
0x3 regs[dst] = memory[regs[src0]];
0x4 memory[regs[src0]] = regs[src1];
0x5 hi, regs[dst] = regs[src0] + regs[src1];
0x6 hi, regs[dst] = regs[src0] - regs[src1];
0x7 hi, regs[dst] = regs[src0] * regs[src1];
0x8 hi, regs[dst] = regs[src1] ? regs[src0] / regs[src1] : 0xffffffff;
0x9 regs[dst] = regs[src0] << src1;
0xa regs[dst] = regs[src0] >> src1;
0xb regs[dst] = regs[src0] & regs[src1];
0xc regs[dst] = regs[src0] | regs[src1];
0xd regs[dst] = ~regs[src0];
0xe regs[dst] = bus[regs[src0]];
0xf bus[regs[src0]] = regs[src1];
Bus address 0x0
supports serial I/O routed through stdin
/stdout
. The upper 8 bits are ignored on writes and set to
zero on read. Writing to the address will immediately write the lower byte to stdout
. Reading from the address will
block until a byte is available on stdin
, then returns that byte.
Bus addresses 0x1
-0x3
correspond to the disk0 controller, and 0x4
-0x6
, to disk1. In order, the three bus
addresses that a single controller covers are the following control registers:
Bus Offset Control Register
+0x0 Command/Size
+0x1 Sector
+0x2 Address
Reading from +0x0
will return the number of 512-byte sectors detected in the disk; this value will be zero if no disk
is present. +0x1
and +0x2
are read/write and persist their values. Writing to +0x0
will not change the stored size
value, but sends a command to the controller. Writing 0x0
to it will trigger a disk read, and 0x1
, a disk write. All
other commands are ignored. Whether read or write, the sector being operated on is the sector numbered by +0x1
, and
the memory base address, that in +0x2
. For example, if we wrote 0x100
to bus address 0x3
, 0x2
to 0x2
, and
0x0
to 0x1
, disk0's controller would then read sector 0x2
into memory starting at address 0x100
. Similarly for
writes.
Writing a non-zero value to bus address 0x7
will cause the emulator to immediately exit. The address will always
return zero when read.
All other bus addresses are read-only (will remain unchanged by bus writes) and will always return 0x0
if read from.
The lower 40 words of the address space (addresses 0x00
-0x27
) are read-only (will remain unchanged by store
operations) and contain the emulator firmware. Upon startup, the emulator will read the size field of disk0. If
non-zero, it will read sector 0x0
to address 0x0
, then immediately jump to 0x28
. Otherwise, it enters the
bare-bones "interactive bootstrap assembler," meant to be the simplest possible environment in which an operator could
manually bring up the machine. It will accept machine-code instructions in four-digit lowercase hexadecimal, one word
per line. Entering a final, empty line will cause it to jump to the start of the assembled code. Words are written into
memory starting at address 0x28
. Regardless of how control eventually reached 0x28
, the contents of registers are
not generally predictable, though deterministic. As a simple example, the following sequence input into the interactive
assembler, followed by pressing enter twice to enter a blank line, will immediately exit the emulator:
2007
f000