I’ve spent a lot of time hashing out the instruction set for the CPU I’m designing. My first shot at it didn’t work out too well for a number of reasons. For one, I originally tried to keep the number of opcodes with immediates down to a bare minimum.
Originally the only instruction I had with an immediate was push. To do conditional branches, I had an instruction called ‘istrue’ which would execute the next instruction if true and if false it would skip over it. That only left one instruction for the branch, not enough space to setup an address on the stack easily. There were only two ways to do it, write a really convoluted procedure which made loops practically impossible to understand or abuse the address stack and jump indirectly via the return instruction. The indirect jump via return method required setup in advance and careful attention to tear it back down if the condition was false or you end up accumulating garbage on the address stack or inadvertently jump back into the loop somewhere else down the line.
I’ve gone from around 30 instructions to 51. 15 instructions now have an immediate. I added a few instructions to make it easier to deal with variables in memory so there doesn’t have to be as much stack shuffling. I got rid of the ‘istrue’ conditional branch and now have seperate call and jmp instructions that can take values either off the stack or as an immediate with branching based on a boolean sitting on the data stack.
You can now store with or without an immediate either byte or word sized values. Also added instructions for incrementing and decrementing memory directly.
The new opcode map is here on the left. The stack columns have an In and Out section and each section have ‘ia,ib,ic’ and ‘oa,ob,oc’ fields. The ia-c fields are the inputs and the oa-c fields are the outputs. A,B, and C refer to elements on the stack with A being the top of the stack, B being below and so on. The code field is the byte representing the opcode. Fields i0-i5 are the bytes that make up the immediate associated with the opcode.
Fields are marked either a, d, or b which represent an address, data or a boolean. The in fileds represent what is being taken off of the stack and the out fields represent what is being placed onto the stack. For instance, drop takes an item off of the stack and leaves nothing on the stack after it executes. dup takes one item off of the data stack and leaves two identical items on the stack. a2d takes an item off of the address stack and leaves it on the data stack. ret takes an item off of the address stack and leaves nothing. (it assigns that item to the instruction pointer) fetch takes an item (address) off of the data stack and leaves an item on the datastack (the word at that address). calli doesn’t take any value off either stack, it has an immediate (a procedure address) and leaves an item on the address stack (the return address, which is IP + next opcode address).
Overall, this has been a lot more difficult to design than the VM’s I’ve written in the past. In those cases everything was 32bit, since none of the hardware was real I didn’t need to worry about things like reading and writing to different widths. (SFR mapped devices can be triggered on reads and writes). Communicating with the VM was done though mapped memory spaces and code complexity wasn’t much of an issue since most of the code was generated from python programs. Debugging with the VM was easy, it won’t be on the FPGA.
It’s starting to crystallize into something that could be useful. I’ll probably have to add a few more opcodes, change some others, but I think it’s going in the right direction.