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