I always have been amazed how fast 0x10c game skyrocketed, created a whole community of DCPU-16 programmers and then suddenly died leaving lots of content just like… abandoned. The DCPU-16 architecture inspired me to write an emulator for it which eventually became its own virtual CPU architecture called V16 (Virtual 16-bit VM, duh).
Version history
DCPU emulator
When I firstly read about cancelled 0x10c game and DCPU-16, I really started looking forward to write a nice emulator for it so that I can write programs and possibly write an operating system for it. However, while the idea was growing and changing, other projects such as Refraction and Thorn started to take shape and I eventually forgot about that.
V16v1
The first non-DCPU version had a lot of just wrong stuff that was making the instruction format hard to understand and bad in general: it still had pointer access within operands. This was obviously a bad idea because instructions could grow up to six words in size!
After a Habr post I decided to purge everything and put the project on hold for a while.
V16v2: V16RISC
After some time I decided to get back to implementing a virtual machine but this time with a better architecture and instruction set: and I choose to make a RISC-like machine with 16-bit words as minimal addressable units with 1 to 3 word instruction length.
However after some time I was left unsatisfied with the results of my work: the VM core was fine but the assembler written in JavaScript was a pain in the ass for me since I wanted this thing to be buildable and runnable without any dependencies on a UNIX-like system.
So after some hesitation I decided to archive the repo and again start working on a better architecture.
V16v3
The final version is more like the first one. However a lot of stuff that was similar to DCPU-16 is now missing. This includes:
- Pointer math in operands. Now V16RISC’s memory read/write instructions are used.
- Complicated system of constants. The only flag operands have is
IMM
flag which tells the runtime that a constant value should be used instead of a register reference.
The project’s repo contains four different projects:
- The V16 library: the runtime’s core. Has no input or output functionality.
- V16ASM: a simple 500 LoC assembler written in C.
- V16DASM: a simple disassembler.
- V16EXEC: a terminal-based runtime implementation.
- V16SYS: a graphical runtime with other virtual hardware that should represent an authentic DCPU computer.
A bit of docs
Instruction format
If the VM needs an another value, it reads the value at memory[PC++]
.
Every instruction is at least one word wide. This word has the following structure:
Bits | Value |
---|---|
15-10 | The opcode |
9 | “A” operand IMM flag |
8-5 | “A” operand register number |
4 | “B” operand IMM flag |
3-0 | “B” operand register number |
The IMM
flag means that a register number should be ignored and a constant value must be used. A constant value cannot be written to so instructions that write to a specific operand won’t do anything because the runtime will silently ignore them.
Registers
There are 16 registers that can be accessed from any instruction. This means you can set any value to the program counter without any additional instructions like JMP
.
Register index | Common name | Description |
0x00 |
R0 |
General-purpose register |
0x01 |
R1 |
General-purpose register |
0x02 |
R2 |
General-purpose register |
0x03 |
R3 |
General-purpose register |
0x04 |
R4 |
General-purpose register |
0x05 |
R5 |
General-purpose register |
0x06 |
R6 |
General-purpose register |
0x07 |
R7 |
General-purpose register |
0x08 |
R8 |
General-purpose register |
0x09 |
R9 |
General-purpose register |
0x0A |
RI |
Counter (acts as a GPR) |
0x0B |
RJ |
Counter (acts as a GPR) |
0x0C |
IA |
Interrupt routine address |
0x0D |
OF |
Overflow |
0x0E |
SP |
Stack pointer |
0x0F |
PC |
Program counter |
Communicating between devices
Again, built-in device support is in common with DCPU-16. But unlike DCPU-16 V16 uses x86-like IO ports to do this.
IOR $0x00FF, %R0 # Read port 0x00FF to R0
IOW %R0, $0x000F # Write R0 to port 0x000F
This approach, however, can be a bit slow if a device decides to do some time-consuming actions so be aware that sending and receiving data is a possible CPU time eliminator.
The runtime library provides two callback functions for implementations to provide their own logic to communicate between devices and the CPU:
// value is guaranteed to be non-NULL.
static bool IMPL_ioread(V16_vm_t *vm, uint16_t port, uint16_t *value)
{
// We have a hit!
if(port == 0x00FF) {
value[0] = (uint16_t)getchar();
return true;
}
// If no value was written (eg. no device owns the specified port)
return false;
}
static void IMPL_iowrite(V16_vm_t *vm, uint16_t port, uint16_t value)
{
// Writing at 0x00FF will result in putchar()
if(port == 0x00FF) {
putchar(value & 0xFF);
return;
}
}
Memory access
But how can I write stuff to the memory if I have no pointer math in instructions?!
Well to counter that, two RISC-ish instructions are introduced:
MRD $0x8000, %R0 # Read memory[0x8000] to R0
MWR %R0, $0x8A00 # Write R0 to memory[0x8A00]
Instruction set
There are up to 64 possible instructions.
The following table shows all the defined instructions. All the logical operations (in descriptions) are in C syntax. All the opcodes that are not shown here are reserved and treated as invalid (the execution stops).
Opcode | Mnemonic | A mode | B mode | Description |
---|---|---|---|---|
0x00 |
NOP |
N/A | N/A | Do nothing |
0x01 |
HLT |
N/A | N/A | Halt and wait for interrupts |
0x02 |
PTS |
READ | N/A | Push to stack |
0x03 |
PFS |
WRITE | N/A | Pull from stack |
0x04 |
CAL |
READ | N/A | Call a subroutine |
0x05 |
RET |
N/A | N/A | Return from a subroutine |
0x06 |
IOR |
READ | WRITE | Read from an IO port |
0x07 |
IOW |
READ | READ | Write to an IO port |
0x08 |
MRD |
READ | WRITE | Read memory |
0x09 |
MWR |
READ | READ | Write memory |
0x0A |
CLI |
N/A | N/A | Disable interrupts |
0x0B |
STI |
N/A | N/A | Enable interrupts |
0x0C |
INT |
READ | N/A | Trigger an interrupt |
0x0D |
RFI |
N/A | N/A | Return from interrupt routine |
0x20 |
IEQ |
READ | READ | Skip next if !(B == A)
|
0x21 |
INE |
READ | READ | Skip next if !(B != A)
|
0x22 |
IGT |
READ | READ | Skip next if !(B > A)
|
0x23 |
IGE |
READ | READ | Skip next if !(B >= A)
|
0x24 |
ILT |
READ | READ | Skip next if !(B < A)
|
0x25 |
ILE |
READ | READ | Skip next if !(B <= A)
|
0x30 |
MOV |
READ | READWRITE | B = A |
0x30 |
ADD |
READ | READWRITE | B = B + A |
0x30 |
SUB |
READ | READWRITE | B = B - A |
0x30 |
MUL |
READ | READWRITE | B = B * A |
0x30 |
DIV |
READ | READWRITE |
B = B / A or B = 0
|
0x30 |
MOD |
READ | READWRITE |
B = B % A or B = A
|
0x30 |
SHL |
READ | READWRITE | B = B << A |
0x30 |
SHR |
READ | READWRITE | B = B >> A |
0x30 |
AND |
READ | READWRITE | B = B & A |
0x30 |
BOR |
READ | READWRITE | B = B | A |
0x30 |
XOR |
READ | READWRITE | B = B ^ A |
0x30 |
NOT |
READWRITE | N/A | B = ~B |
0x30 |
INC |
READWRITE | N/A | B = B + 1 |
0x30 |
DEC |
READWRITE | N/A | B = B - 1 |
Interrupts
When the attention is needed to a device, an interrupt is triggered. There’s an interrupt queue of 256 interrupts. If the queue grows over 256, the execution stops.
Interrupt routine is a subroutine which returns using RFI
instruction. When this routine is called, the value of R0 register is pushed to the stack and replaced with an argument. The routine address is stored in the IA register.
Assembly syntax
Since the assembler is written in just two days, it lacks a lot of stuff like directives. However it is already possible to write some cool code with it. The basic syntax is:
[label:] <mnemonic> [<prefix>argument[, <prefix>argument]] [# comment]
Name | Description |
---|---|
label | A current PC position right before an instruction starts. |
mnemonic | An opcode translated into human-readable form. |
prefix | An operand prefix. $ for constants, % for registers. |
argument | An identifier (label’s name, register’s name) or a number. |
comment | Ignored. |
Example code (link):
# ASCIItest.S - prints the ASCII table
# in seven colors, than freezes.
mov $0x8000, %r0
mov $0x0800, %r1
loop:
ige $0x0FFF, %r1
mov $end, %pc
mwr %r1, %r0
inc %r0
inc %r1
mov $loop, %pc
end:
mov $end, %pc
Hardware devices
LPM-25
LPM25 is a 320x200 pixel color display compatible with V16 hardware. The display is split into 4x8 pixel cells which displays a colored character making a 80x25 grid of characters.
LPM25 has no internal video memory: instead it uses the internal V16 memory in read-only mode.
The display reads from the memory at constant rate of 50 times per second (50 Hz).
LPM25 reads text data from memory at TEXT_OFF address. Each text cell consists of three parts:
Bits | Description |
---|---|
0-3 | Background color. |
4-7 | Foreground color. |
8-15 | 8-bit ASCII code. |
The glyps for characters are also read from memory at CHAR_OFF address. Each glyph is a 32-bit big-endian value divided by eight quartets representing a row in the glyph (The default ‘A’ character is equal to 0x04AAEAAA).
LPM-25 provides a hardware cursor that looks like a character with inverted colors.
The cursor offset can be set via CUR_OFF
register. Cursor can blink with interval set via CUR_BLINK
register. If the blink interval is set to zero, cursor is always visible.
LPM-25 uses a bunch of IO ports to communicate with the CPU:
Port number | Port name | Description |
---|---|---|
0x1F01 |
TEXT_OFF |
Text data pointer. |
0x1F02 |
CHAR_OFF |
Charset pointer. |
0x1F03 |
CUR_POS |
Cursor position. |
0x1F04 |
CUR_BLINK |
Cursor blink interval (in milliseconds). |
Generic keyboard
When a key is pressed, the device triggers an interrupt with argument value of 0x000F
.
Within the interrupt handler, a value can be read from port 0x000F
. This value can be:
Value | Description |
0xFF01 |
Backspace |
0xFF02 |
Return (Enter) |
0xFF03 |
Insert |
0xFF04 |
Delete |
0xFF05 |
Up arrow |
0xFF06 |
Down arrow |
0xFF07 |
Left arrow |
0xFF08 |
Right arrow |
0xFF09 |
Shift |
0xFF0A |
Ctrl |
0x00** |
8-bit ASCII code |
What’s next?
At that point I have no plans of actively continuing the development but I consider writing this VM and utilities a big experience in C programming ;)