Designing a trivial CPU, part 2: machine code
Go to the first post in this series.
Introduction
I’m designing a small CPU. In the last post, I picked (fairly
arbitrarily) bit-size, registers, addressing modes, and an instruction
set. To recap, that’s: 8 bits; four registers (x
, y
, i
and j
);
four additional indirect modes ((i)
, (j)
, k
and (k)
), where
k
is an 8-bit constant.
The instructions are summarised below:
Instruction | Operation | Addressing (r' ) |
Opcodes |
---|---|---|---|
MOV r, r' |
r = r' | regs | 16 |
LDR r, ind |
r = ind | indir | 16 |
STR ind, r |
ind = r' | (i) , (j) , (k) |
16 |
ADD r, r' |
r = r + r' | regs, indir | 32 |
SUB r, r' |
r = r - r' | regs, indir | 32 |
ADC r, r' |
r = r + r’ + C | regs, indir | 32 |
SBC r, r' |
r = r - r’ + C | regs, indir | 32 |
INC r' |
r = r + 1 | regs | 4 |
DEC r' |
r = r - 1 | regs | 4 |
JMP cc k |
pc = k if cc | — | 4 |
ASR r' |
C = r & 1; r = r >>> 1 | regs | 4 |
LSR r' |
C = r & 1; r = r >> 1 | regs | 4 |
ROR r' |
C = r & 1; r = r >> 1 | r << 7 | regs | 4 |
RRC r' |
C = r & 1; r = r >> 1 | C << 7 | regs | 4 |
IN r, (k) |
r = (k) | regs | 4 |
OUT (k), r |
(k) = r | regs | 4 |
Todays’ post is about fitting the instruction set above into the 256 available opcodes.
Mapping to opcodes: consistency
The opcodes of the processor are 8 bits wide. Each distinct
instruction (such as ADC x, (i)
) maps to one opcode. The question
is: for each instruction, which one?
What we want to do here is ensure that the process of decoding the
instructions is as simple as possible. This means that similar
functionality should be grouped together in some way. For example,
similar instructions should share as many common bits as possible. The
target register (r
) should be encoded in the same place every time,
and in the same way, and so on.
With that in mind, we can paint in quite a few features very quickly. Arbitrarily, let’s put the register/operand information in the LSBs of the instruction, leaving the MSBs to indicate which instruction it is.
Arithmetic
The ADD
and SUB
instructions account for 128 opcodes – half of
the available space – so let’s put them in the upper half of the
opcode space, with opcodes like 1xxx xxxx
. They each have a
destination register, one of four, and two kinds of source (either the
four registers, or the four indirect modes). So we could put the
destination register in bits 2-3, the kind of source in bit 4, and the
source in bits 0-1. That leaves us with bits 5 and 6 to encode whether
it’s ADD
or SUB
, and whether it’s using the carry bit from a
previous operation:
Instruction | 7 | 6 | 5 | 4 | 3-2 | 1-0 |
---|---|---|---|---|---|---|
op r, r' |
1 | add/sub | carry | 0 | r |
r' |
op r, ind |
1 | add/sub | carry | 1 | r |
ind |
I’m going to leave INC and DEC for later, as they’re much smaller.
Register and memory moves
The next biggest class of operation is the move operations – between
registers, or to and from RAM. These account for 44 opcodes between
them. Let’s put them all in the first opcodes, 00xx xxxx
. Note that
LDR
and STR
both use indirect addressing, and MOV
works with two
registers. We’ve previously used bit 4 to indicate indirect
addressing, so LDR
and STR
can both have bit 4 set, and MOV
have
it clear.
Instruction | 7 | 6 | 5 | 4 | 3-2 | 1-0 |
---|---|---|---|---|---|---|
MOV r, r' |
0 | 0 | 0 | 0 | r |
r' |
LDR r, ind |
0 | 0 | 0 | 1 | r |
ind |
unused | 0 | 0 | 1 | 0 | – | – |
STR ind, r |
0 | 0 | 1 | 1 | r |
ind |
Shifts
Next, we have the four right shift/rotate instructions. These all
operate on a single register, with no indirect addressing. There’s
four instructions, and four registers. We’ve generally been using bit
4 = 0
for register-based instructions, and conveniently the unuse
block above is one of those, so we can fit these instructions in
there:
Instruction | 7 | 6 | 5 | 4 | 3-2 | 1-0 |
---|---|---|---|---|---|---|
ASR r |
0 | 0 | 1 | 0 | r |
00 |
LSR r |
0 | 0 | 1 | 0 | r |
01 |
ROR r |
0 | 0 | 1 | 0 | r |
10 |
RRC r |
0 | 0 | 1 | 0 | r |
11 |
This means that, we’ve filled up all the opcodes 1xxx xxxx
and 00xx xxxx
, leaving 01xx xxxx
unallocated so far.
The rest
We’ve still got INC
/DEC
(8 opcodes), IN
/OUT
(8 opcodes), and
JMP
(8 opcodes). I’m going to fit the first four into the same
block: 0100 xxxx
. This makes sense for the arithmetic ops, as this
is a “register” block, with bit 4 clear. It’s less obvious that it’s
appropriate for the I/O ops, given that they both use a (k)
indirection. We might have to revisit this later.
Finally, there’s the JMP
instruction. That takes two parameters: the
condition code, cc
, and an 8-bit constant address to jump to, k
.
That’s 4 opcodes (one for each of the conditions).
Filling all of these in, we have:
Instruction | 7 | 6 | 5 | 4 | 3-2 | 1-0 |
---|---|---|---|---|---|---|
INC r /DEC r |
0 | 1 | 0 | 0 | r |
0 i/d |
IN r, (k) |
0 | 1 | 0 | 0 | r |
10 |
OUT (k), r |
0 | 1 | 0 | 0 | r |
11 |
JMP cc k |
0 | 1 | 1 | 1 | 00 | cc |
This leaves a few ranges unused:
Instruction | 7 | 6 | 5 | 4 | 3-2 | 1-0 |
---|---|---|---|---|---|---|
unused | 0 | 1 | 0 | 1 | – | – |
unused | 0 | 1 | 1 | 0 | – | – |
unused | 0 | 1 | 1 | 1 | 01 | – |
unused | 0 | 1 | 1 | 1 | 10 | – |
unused | 0 | 1 | 1 | 1 | 11 | – |
Overview and conclusion
There are 16 blocks of opcodes, corresponding to the four MSBs of the opcode space. We’ve allocated 12 of those blocks fully, and two more partially. We can summarise them in this table:
xx00 | xx01 | xx10 | xx11 | |
---|---|---|---|---|
00xx | MOV |
LDR |
shift | STR |
01xx | misc | JMP |
||
10xx | ADD |
ADD |
ADC |
ADC |
11xx | SUB |
SUB |
SBC |
SBC |
A more detailed table, showing all of the opcodes, is available as a spreadsheet.
The next post is going to be about the structure of the hardware, and how it gets controlled to execute these instructions.