MC6000 in hardware - Part 1: The assembler
SHENZHEN-IO is an interactive circuit building and programming puzzle game with a programmable microcontroller called the MC6000, it has an extremely simple instruction set and no memory besides 2 registers that can only store numbers from -999 to 999.
Each instruction consists of a label, condition, instruction, and comment:
foo: +mov 50 x2 # puts 50 to XBus 2
Conditions can either be +
, -
, or blank and control if the instruction should execute after a comparison. Labels are optional and used to tell where the jmp instruction should jump to, this is just sugar to make things easier to keep track of. Registers consist of acc, bak, and 6 virtual registers coresponding to the 6 I/O ports on the MC6000. The game comes with a more in-depth manual with a language specification here: https://u.pxtst.com/QAgvo8UJR6fah.pdf
The first step to implementing this in actual hardware is to lay out the machine code:
Registers
000 | acc | 001 | dat | 010 | p0 | 011 | p1 |
100 | x0 | 101 | x1 | 110 | x2 | 111 | x3 |
You may notice the lack of the register null
which does nothing when you write to it and returns 0 when you read from it. I did not include it because it can simply be replaced with the literal 0 except when writing to it with MOV
, because of this I added the flag E
to the instruction SLX
which when 1 will eat a value from the bus and do nothing with it.
Condition codes
00 | always execute |
01 | only execute on - flag |
10 | only execute on + flag |
11 | only execute once |
Internally there are two execution flags, +
and -
which are set by the test instructions TEQ
, TGT
, TLT
, TCP
. Every instruction but TCP
sets the flags in a differential as in only either + or - can be true at the same time but TCP
will disable both flags when both operands are equal.
Because I want to reduce instruction size I've made it so only the first operand of test instructions can be immediate values, because of this things have to be shifted around when assembling:
TGT acc 69 -> TLT 69 acc
TEQ 69 69 -> TST 1 0
TCP acc 42 -> TPC 42 acc
You may notice I've added 2 extra test instructions: TST
and TPC
.
TST
takes 2 operands, +
and -
and sets the coresponding flags directly, this is always emitted when both operands of a test instruction are immediates.
TPC
is the same as TCP
but with the operands reversed, this happens when the second operand is an immediate.
Register/Immediate values
10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | |
1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | reg | Regsiter | ||
immediate | Immediate |
Immediate values can range from -999 to 999 and are encoded with two's complement and because it doesn't completely fill the 11 bits (0 - 2048) we can store a register number without adding an extra bit to signal whether or not it's a register. To easily tell the difference between the two you simply check if the first 8 bits are 10000000 which means it's <= -1017.
Register/Select values
3 | 2 | 1 | 0 | |
1 | reg | Regsiter | ||
0 | imm | Selection |
Register/Digit values
4 | 3 | 2 | 1 | 0 | |
1 | reg | Regsiter | |||
0 | immediate | Digit |
Register/Digit values work similarly to Register/Immediate values but store single digits from 0 to 9 but requires a flag to differentiate registers from immediates. If the digit is out of bounds it should be encoded as 0b1111 which will be interpreted as a no-op.
Instructions
18 | 17 | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | |
cond | 0 | 0 | 0 | R/I arg | reg | MOV | |||||||||||||
cond | 0 | 1 | 0 | 0 | 0 | line | JMP | ||||||||||||
cond | 0 | 1 | 0 | 0 | 1 | R/I sleep amount | SLP | ||||||||||||
cond | 0 | 1 | 0 | 1 | 0 | E | xpin | SLX | |||||||||||
cond | 0 | 1 | 0 | 1 | 1 | R/I value | ADD | ||||||||||||
cond | 0 | 1 | 1 | 0 | 0 | reg | SUB | ||||||||||||
cond | 0 | 1 | 1 | 0 | 1 | R/I value | MUL | ||||||||||||
cond | 0 | 1 | 1 | 1 | 1 | 0 | NOT | ||||||||||||
cond | 0 | 1 | 1 | 1 | 0 | 0 | R/S selection | DGT | |||||||||||
cond | 0 | 1 | 1 | 1 | 0 | 1 | R/D value | R/S selection | DST | ||||||||||
cond | 1 | 0 | 0 | R/I arg | reg | TEQ | |||||||||||||
cond | 1 | 0 | 1 | R/I arg | reg | TGT | |||||||||||||
cond | 1 | 1 | 0 | R/I arg | reg | TLT | |||||||||||||
cond | 1 | 1 | 1 | R/I arg | reg | TCP | |||||||||||||
cond | 0 | 0 | 1 | R/I arg | reg | TPC | |||||||||||||
cond | 0 | 1 | 1 | 1 | 1 | 1 | + | - | TST |
Using this layout I made an assembler and disassembler in Dart:
https://dartpad.dartlang.org/1398b0d59ce1f7292c5d5d1064b591b5