Choosing the Instruction Set

The EightThirtyTwo ISA – Part 2 – 2019-08-10

In part 1 I talked about why I wanted to design my own microprocessor, and how I’d settled upon an architecture using eight-bit instruction words and eight addressable thirty-two bit registers.

Because our instruction words won’t have room to encode two registers in them, each instruction will only take one operand – so we need a way of supplying a second operand to instructions such as add, xor, load immediate, etc. For this I’ve defined a ninth register, which I’ll call temp. This register isn’t addressable via the instruction word – it’s simply used implictly wherever it’s needed. One of the key decisions to make will be whether the result of arithmetic instructions will go the nominated register or the temp register.

It’s not entirely clear without giving the matter some thought (and ideally writing some test programs) exactly which instructions we need to implement, so initially I’ll list all the possible instructions that come to mind, and then figure out what’s mandatory, what’s optional, and what’s most beneficial in terms of keeping code size down, in the hope that we can hit our target of 25 instructions.

Possible instructions include:

  • Load, move and store instructions: (12 defined here)
    • Load Immedate. (LI) Load a sign-extended six bit value into the temp register. If the previous instruction was also Load Immediate, shift temp’s contents left six bits and “or” in the new immediate value. This way we can build longer values with cascaded LI instructions.
    • Move to Register (MR) – move from temp to a nominated register
    • Move to Temp (MT) – move the contents of a nominated register to temp.
    • EXchanGe (EXG) – exchange the contents of the nominated register with temp.
    • Load Data (LD) – load data from RAM pointed to by the nominated register.
    • STore data (ST) – store data to RAM pointed to by the nominated register
    • Load Data and Increment – (LDINC) load data from RAM pointed to by the nominated register, and post-increment the register.
    • Load Data and Decrement – (LDDEC) load data from RAM pointed to by the nominated register, and post-increment the register.
    • STore data and Increment (STINC) – store data to RAM pointed to by the nominated register and post-increment the register.
    • STore data and Decrement (STDEC) – store data to RAM pointed to by the nominated register and post-decrement the register.
    • LoaD Byte (LDB) – Load byte from RAM pointed to by the nominated register.
    • STore Byte (STB) – Store byte from RAM pointed to by the nominated register.
  • Control flow instructions: (11 instructions)
    • JuMP (JMP) – set PC to temp
    • Jump to SubRoutine. (JSR) – push a return value on the stack, set PC to contents of TEMP. Alternatively, set PC to temp, set temp to PC+1 and make caller responsible for saving / restoring.
    • RELative JuMP (RELJMP) – Add contents of temp to PC
    • RELative Jump to SubRoutine (RELJSR) – as JSR but adding tmp to PC.
    • Jump on Condition Code (JEQ, JNE, JLT, JGT, JLE, JGE) – jump conditional upon Zero and Carry flags.
    • No Operation (NOP).
  • Arithmetic and logic instructions (19 instructions)
    • Add – add temp to nominated register. (Haven’t yet decided whether the result goes to temp or the nominated register.)
    • Add with carry (ADDC) – add temp + carry flag to nominated register.
    • Sub – subtract temp from register.
    • Reverse Sub (SUBR) – subtract register from temp.
    • Compare (CMP) – subtract temp from register, discard result.
    • Sub with carry (SUBC) – subtract temp + carry from register.
    • Negate – (NEG) contents of nominated register.
    • Multiply – (MUL)
    • And
    • Or
    • Xor
    • Not
    • Shift Left
    • Arithmetic Shift Right
    • Logical Shift Right
    • Rotate Left
    • Rotate Right
    • Rotate Left through Carry (ROLC)
    • Rotate Right through Carry (RORC)

So that’s 42 instructions – we need to lose 17 of these. So let’s see what can easily be implemented in terms of other instructions.

  • “Not rn” can be replaced with “li -1, xor rn”
  • “Neg rn” can be replaced with “li -1, xor rn, li 1 add rn”
  • We don’t need both rotate left and right, since the result wraps around; rotating left 8 bits is the same as rotating right 24 bits.
  • Sub could be implemented by negating one of the operands and then adding.
  • “Subr rn” could be replaced by “exch rn, sub rn”
  • Mul is nice to have but is optional for a lot of soft CPU cores.
  • The Load/Store data with increment/decrement instructions could be removed.
  • NOP will no doubt be needed internally for dealing with pipeline stalls and data hazards – but it’s not necessary to expose it at the ISA level. If we really want to do nothing we can use a pair of exch instructions.

That saves us 12 instructions – we need to lose another 5 so we need to think a bit further out of the box.

Suppose we make the Program Counter one of the eight addressable registers: we could then implement jumps by manipulating that register, so could dispense with jmp, jsr, reljmp and reljsr.

The conditional jump instruction could remain if we can encode the conditions into three bits – but another option, and one that’s quite appealing, is to borrow an idea that goes back to the 1950s, but which has gone out of fashion in recent years: Predication.

Instruction sets with predication generally devote a few bits to the instruction encoding for condition flags which will allow or prevent each instruction executing on an individual level depending upon whether they match the CPU’s current condition codes. PA-RISC, IA-64 and ARM32 have comprehensive predication, but MIPS and x86 are basically limited to conditional branches and a Conditional Move instruction.

Obviously we can’t devote encoding space to full predication, but what we could do is define a “Cond” instruction, which will match against condition codes, then enable or disable execution of all instructions until further notice. The result could look like this

cmp r0
cond SGT ; only execute the following instructions if temp was strictly greater than r0
li branchtarget
mv r7 ; r7 is the program counter.
cond EX ; Go back to unconditional execution

The appealing thing about this is that we could implement simple “If…else…” control flow without needing to branch at all.

Next time I’ll write some more code snippets, see how this ISA feels, decide which instructions to keep and whether any simple changes can be made to make it more comfortable to use.

Leave a Reply

Your email address will not be published. Required fields are marked *