Instruction Set and Addressing Modes
- Anthony Faulise
- Mar 8
- 8 min read
Updated: Mar 17

I think my first tasks are to define my instruction set and addressing modes, i.e.; the "Instruction Architecture".
I’m going to steer clear of a full CISC instruction set. Forget polynomial evaluation, floating point, and single-instruction block transfers or string lookups.
After reviewing manuals for the Motorola 6800, Motorola 68020, Zilog Z-80, Digital PDP-11, and MIPS RISC designs, I ‘ve settled on these instructions as sufficient to do anything I would need:
LD (load)
ADD, ADC - add with and without carry
SUB, SBC - subtract with and without carry
CMP (compare)
AND
OR
NOT
XOR
NEG (negate)
INC
DEC
Rotate (right/left with/without carry)
RR, RRC
RL, RLC
Shift (right/left arithmetic/logical)
SRA (arithmetic new MSB = old MSB), SRL (logical new MSB = 0)
SL
SLB, SRB (shift left/right 1 byte)
BRA (relative jump on a variety of conditions, 1 word instruction)
JMP (absolute jump on variety of conditions)
JSR (jump to subroutine)
IM (set interrupt mask)
SETC (set CCR carry bit)
CLRC (clear CCR carry bit)
RTS (return from subroutine)
SWI (software interrupt)
RTI (return from interrupt)
NOP (no operation)
I also feel my CPU needs to support these addressing modes:
Implicit (no operand)
Immediate (operand follows instruction in program memory)
Register
Indexed (address of operand is immediate address plus offset contained in a register)
Indirect (content of register is address of operand)
Indirect with pre-decrement of register (nice to have, but negotiable)
Indirect with post-increment of register (nice to have, but negotiable)
Doubly Indirect (content of register is the address of the address of the operand)
From my research, there are some addressing modes the M68020 has (and I think the VAX too) that I thought I might like, such as "Address Register Indirect with Index" where the address of an operand is a register, plus a constant times a second register, plus a fixed displacement. This is really convenient for accessing arrays of objects, where the base register is the start of the array, the “constant” is the size of the object, the register that multiplies the constant is the index in the array, and the final displacement is the offset within the object of the member value you want to access. I’ll have to live without it.
I was unsure initially about how many registers I wanted to support. My experience with the M6800 left me feeling that two registers was not enough. The Z-80’s accumulator plus six 8-bit or three 16-bit registers seemed to be the bare minimum. I left the question open, but came to a conclusion from another angle.
I didn’t want to deal with a combination of single-word and multi-word instructions, so I preferred that the instruction word should be able to contain the entire instruction, addressing mode, and source and destination register information. For immediate mode addressing, I was going to have to live with fetching the operand in the word after the instruction. I may be able to squeeze the (8-bit) relative offset of a branch instruction into the 16-bit instruction word if I’m creative. I’ll leave that for later.
My first thought was to have a certain bit-field in the instruction word for the instruction itself, then other bit fields for the addressing mode and register ID of each operand. If I allowed 3-bits for addressing mode and 3-bits for register ID for each of two operands, that would occupy 2 x (3 + 3) = 12 bits of my 16-bit word. That left just 4 bits to encode my 22-plus instructions.
At first, I saw my 8 registers melting away to two, but then I had an insight (OK, "insight" might just have been remembering it from someone else's architecture). Only a few instructions require two operands: LD, ADD, ADC, SUB, SBC, CMP, AND, OR, XOR. That’s 9. I can allow 4 bits to encode those, and if all 4 bits are 1s, say, that could indicate that the instruction was a one-operand instruction, and that the next 6-bits of the instruction word, no longer needed to encode the second operand, can be used to represent the instruction. Six bits is more than enough to encode all the one-operand instructions. And I can use the same trick so that if the middle 6 bits are all 1s, the instruction will be a zero-operand instruction (ie; implicit) and the instruction can be encoded in the bottom 6-bits.
It looks like this:
Instructions | OpCode Bits 15-12 | OpCode Bits 11-6 | OpCode Bits 5-0 |
LD, ADD, SUB, CMP, AND, OR, XOR | 0000 - 1110 | Operand 2 Mode + Reg | Operand 1 Mode + Reg |
NOT, NEG, INC, DEC, ROT*, SHIFT*, BRA | 1111 | 000000 - 001111 | Operand 1 Mode + Reg |
IM | 1111 | 010000 | mmmmmm |
SWI | 1111 | 010001 | mmmmmm |
BRA | 1111 | 1ccc dd | dddddd |
SETC, CLRC, RTS, RTI, NOP | 1111 | 111111 | 000000 - 011111 |
JMP | 1111 | 111111 | 100 ccc |
JSR | 1111 | 111111 | 101 ccc |
"mmmmmm" = interrupt mask value
"ccc" = condition codes to specify conditional branch or jump
IM and SWI permits setting the interrupt mask value in one instruction word by reserving a specific combination of opcode bits to contain the new mask value. I may enforce some privileging system by detecting the top two bits of the PC and restricting access to lower mask values. (Low mask value restricts interrupts to highest priority), so for example, PC must be in the top 6% of memory (PC = 1111 X X X) to access mask values below 001000 and in the next 6% (PC = 1110 XXX) to access the next group of mask values (below 010000). All other PC values could access higher mask values. Attempts to write a non-permitted mask value would throw a mask permission fault interrupt).
BRA (conditional branch) deserves a little explanation. My idea is that if I reserve a particular combination of OpCode bits 11-6 (ie; starting with 1), then the ccc bits can encode the condition I want to test and the dddd dddd bits (8-bits) can encode a +127 to -128 offset). That keeps my branch instruction to one word.
I should make sure my "ccc" condition field is enough to encode what I need:
Code | Condition | CCR |
000 | Always | n/a |
001 | Carry set | C=0 |
010 | Carry clear | C=0 |
011 | Zero | Z=1 |
100 | Not Zero | Z=0 |
101 | Negative | N=1 |
110 | Overflow | V=1 |
111 | Half-carry (?) | H=1 |
At this point, I am satisfied that I can have my full instruction set and all my addressing modes and keep my 8 registers.
I realized I could apply the same bit-saving trick to register addressing, though. What if I split up my 6-bits for addressing mode and register ID differently for different modes?
I'll say that “Register” mode is represented by an addressing mode with the lead-bit 0, and the remaining 5 bits are used to select one of 32 registers.
If the lead bit is 1, that signals that the two next bits (1mm) can encode 3 (not 4) addressing modes (Indirect, Indexed, Doubly Indirect). The remaining 3 bits select one of 8 registers (1mm rrr).
If the first 3 bits are 1, that signals that the next bit (111n) is used to select between two addressing modes (Indirect pre-decrement, Indirect post-increment) but with only 3 register options (111n rr). I have to reserve rr=11 to indicate immediate addressing mode.
Addressing mode 1111 11 indicates immediate mode, which requires no register information.
Now, I have 32 registers that can be used for data, and of those, 8 registers can be used for any of 3 addressing modes. I have a further 2 addressing modes that can be applied to 3 of the registers.
It will make the implementation logic a little more complex, but I gain a lot of registers.
I'll make the registers accessible by the Indirect, Indexed, and Doubly Indirect modes different from the registers accessible by pre-dec and post-inc. That way the programmer won't have to trade off which registers to assign to which modes, although it may encroach on the registers available for general data use.
That leaves me with:
Mode | Operand Format | Addr. Mode Bits | Register Bits | Available Registers |
Register | R4 | 0 | yyyyy: 00000-11111 | R0 - R31 |
Indexed | (R4+disp) | 100 | yyy: 000-111 | R20-R27 |
Indirect | (R4) | 101 | yyy: 000-111 | R20-R27 |
Doubly Indirect | @(R4) | 110 | yyy: 000-111 | R20-R27 |
Indir. Pre-dec | -(R4) | 1110 | yy: 00, 01, 10 | R28-R30 |
Indir. Post-inc | (R4)+ | 1111 | yy: 00, 01, 10 | R28-R30 |
Immediate | 111111 | (None) | None |
Registers 20-27 will function as traditional index registers.
Registers 28-30 are equipped to serve as stack pointers.
Register 30, which is one of the three that support pre-decrement and post-increment can serve as my system stack pointer. It doesn’t need any special functionality, but I’ll notionally reserve it "by convention."
Register 31 isn’t available to the pre-dec and post-inc modes (because I need to reserve yy=11 to signify immediate mode), so I’ll reserve it as the program counter. Having the PC as a general register gives me some flexibility since now code can be self-aware of the PC. Jumps are just an alias for LD to R31. Relative jumps can be implemented by adding an offset to R31. The one thing I won't be able to do is to read a data byte at a relative offset to the PC. (Someone tell me what that's good for if you know. Some processors permit it.)
[Later...] I can see one potential challenge with this system. It would be easiest to route the register select bits from the addressing mode bit field to specific bits of the register address. That means the register ranges for different addressing modes may not be adjacent as they are in the table above. Mapping a 3-bit field so it addresses registers R20-R27 probably won't be convenient. If I simplify the logic, it may mean that there are data-only registers between the ranges of registers used for indexed-type access.
To illustrate, when I use Register Mode, I can just run my 5-bit field to the register address lines.
For Pre-dec and Post-Inc modes, I would prefer that the register address lines are: 111yy, with yy=11 not permitted. This addresses registers R28 - R30.
For Indirect, Indexed, and Doubly Indirect, most convenient would be to set the register address lines as 10yyy, which addresses registers R16-R23. That leaves an "island" of registers, R24-R27 (11000 - 11011) that are data-only registers. This would be an irritant to the programmer (me).
If I want Indirect, Indexed, and Doubly Indirect modes to run from R20 - R27 instead, then my register address lines in this mode need to run from 10100 to 11011, which is not possible to achieve just by routing address bits from the op code operand field to the register address lines. I could do it with some gates to a) invert bit 2 of the op code register select field (y2) on its way to the register address lines, b) make bit 3 of the register address the inverse of bit 2 of the register address lines when any of these three modes are selected, and running the addressing mode register select bits to bits 2, 1, and 0.
That would look like this (y2-y0 are operand mode bits, ra4-ra0 are register address lines):
y2 | y1 | y0 | ra4 | ra3 | ra2 | ra1 | ra0 | Reg |
0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | R20 |
0 | 0 | 1 | 1 | 0 | 1 | 0 | 1 | R21 |
0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 | R22 |
0 | 1 | 1 | 1 | 0 | 1 | 1 | 1 | R23 |
1 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | R24 |
1 | 0 | 1 | 1 | 1 | 0 | 0 | 1 | R25 |
1 | 1 | 0 | 1 | 1 | 0 | 1 | 0 | R26 |
1 | 1 | 1 | 1 | 1 | 0 | 1 | 1 | R27 |
OK, that's not as painful as I expected it to be. It does add a gate delay during resgister select. If that turns out to be a problem, I may have to remove the feature and revert to the system with the "island" of data-only registers.
To sum it all up:
Mode | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
Register | 0 | RA4 | RA3 | RA2 | RA1 | RA0 |
Indexed | 1 | 1 | 1 | RA3, RA2* | RA1 | RA0 |
Indirect | 1 | 0 | 1 | RA3, RA2* | RA1 | RA0 |
Double Indirect | 1 | 1 | 0 | RA3, RA2* | RA1 | RA0 |
Indir Pre-Dec | 1 | 1 | 1 | 0 | RA1 | RA0 |
Indir Post-Inc | 1 | 1 | 1 | 1 | RA1 | RA0 |
Immediate | 1 | 1 | 1 | 1 | 1 | 1 |
Comments