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:

  1. Pointer math in operands. Now V16RISC’s memory read/write instructions are used.
  2. 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 ;)