The EightThirtyTwo ISA – Part 6 – 2019-09-15
In my baby-steps towards a working VBCC backend I now have something complete enough at least to send the text “Hello world!” to a UART. There’s a long way to go yet before it (a) works and (b) produces code that’s even remotely efficient, but it’s a start. In the process I’ve found and addressed some shortcomings in the ISA, and made a few tweaks to the instruction set and encoding.
Most importantly, for C code on a CPU that has relatively few registers, it’s critical that values can be indexed efficiently within a stack frame. The EightThirtyTwo ISA wasn’t well equipped for this. I had originally considered giving r5 a special meaning as an index register, and devoting an instruction each to “load indexed” and “store indexed”, which would add r5 to the nominated register to build an effective address – but it felt clunky and inelegant.
I had already decided to include an “addt” instruction as well as “add”. These instructions would be the same except “addt” places the result in the temp register instead of the nominated register. My original plan was to alter this functionality specifically for r7, so that “addt r[0-6]” would perform straightforward addition, placing the result in temp, but “addt r7” would put the result in r7 and r7’s old contents + 1 in the temp register. This would effectively become a “jsr” instruction, using temp as a link register; the subroutine saves temp to the stack, and unwinds it to r7 when it terminates.
[I’ve since noticed that not special-casing “addt r7” renders it useful for calculating PC-relative addresses of variables, so instead I will special-case “add r7” to place the old value + 1 in temp. Arguably this makes more sense anyway, since in this case we’re just adding functionality for the special case, instead of changing it completely.]
It occurred to me, though, that I’d be able to make better use of “addt” if I were able to load and store using the temp register as an address, instead of having to use a nominated register, so I’ve defined a load and store instruction for this purpose. Together with addt these form a little cluster of instructions that operate in the opposite direction from the main instruction set. Since we’re unlikely to use a calculated address a second time before it gets pushed out of temp, I’ve only defined pre-decrement and post-increment versions of these load and store instructions, since that’s what’s most useful in dealing with the stack. With these instructions we can now save and restore registers in a function prologue / epilogue without marshalling each one in turn into the temp register, like this:
function:
exg r6 // Return address is in temp
stmpdec r6 // save return address
stmpdec r1
stmpdec r2
stmpdec r3
stmpdec r4
mr r6 // saved the return address and four registers in 7 bytes.
...
li 24
addt r6
ldtmpinc r1 // Load a value 24 bytes past the stack - 3 bytes
...
mt r6
ltmpinc r4
ltmpinc r3
ltmpinc r2
ltmpinc r1
ltmpinc r6
exg r6
mr r7 // Restore four registers, and jump to return address - 8 byte
I also realised that I need to deal with the difference between signed and unsigned comparisons: while the binary arithmetic is the same for the two, the semantics of the operands will determine which one we consider to be the greater, and thus how we interpret the carry/borrow bit. It occurred to me that if I add an instruction, “sgn”, that sets a flag causing the next comparison to be treated as signed, I can use that flag in other contexts too. For instance, I can use it to indicate that a shift should be treated as arithmetic rather than logical – thus removing the need to have both “asr” and “lsr” instructions – or possibly indicating that byte or halfword loads should be sign-extended.
The other thing that struck me is that, because r7 is the program counter, many instructions will never be used with r7 as an operand – for example, “and”, “or”, “xor”, “ror”, “shl”, “shr” and several others, I could potentially overload those opcodes, giving me potential encoding space for some zero-operand instructions – such as the suggested “sgn” instruction mentioned above.
The instruction set as it currently stands now looks like this:
Move instructions:
- li – load immediate.
- mr – move temp to register.
- mt – move register to temp.
- exg – exchange register and temp.
Memory load/store instructions:
- ld – load from addres in register, result to temp.
- ldinc – as ld, but postincrements the register.
- ldbinc – as ldinc but reads a single byte.
- ltmpinc – load from address in temp, result to register.
- st – store temp to address in register.
- stdec – as st but predecrements the register.
- sth – store halfword in temp to address in register.
- stbinc – store byte in temp to address in register with postincrement.
- stmpdec – store register contents to address in temp, with predecrement.
Arithmetic instructions:
- add – adds temp to register, result to register. (If register is r7, old contents + 1 are stored in temp.)
- addt – adds temp to register, result to temp.
- sub – subtract temp from register, result to register.
- cmp – subtract temp from register, discard result.
- mul – multiply temp and register, result to register.
Bitwise / Logical instructions:
- and
- or
- xor – exclusive or.
- shl – shift left.
- shr – shift right. If the previous instruction was “sgn” the shift will be arithmetic.
- ror – rotate right.
Miscellaneous instructions:
- cond – begin conditional execution, operand is one of EX, NEX, EQ, NEQ, GE, SGT, LE, SLT. “cond EX” returns to unconditional execution – as does anything that writes to r7. “cond NEX” is currently used to terminate the simulator.
Zero operand, overloaded instructions:
- sgn – indicate that the next instruction (sub, cmp, shr, mul) should treat its operands as signed rather than unsigned.
- ldt – load from address in temp, result to temp.
Next time I’ll look at some hand-crafted EightThirtyTwo assembly codeā¦