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