top of page

Planning the Instruction Controller

  • Writer: Anthony Faulise
    Anthony Faulise
  • Mar 17
  • 14 min read

Updated: Apr 1




Lookout! On 3/26, while working on the design of the Instruction Controller, I discovered an error in my logic in this post. I've pasted in a correction. Look for "Do Over."


Today, I’m going to focus on the CPU Instruction Controller. The Instruction Controller will orchestrate its subsystems to fetch an instruction, get any needed operands, perform the instruction itself, potentially store a result, and queue up the next instruction.


Full disclosure, this post is the result of a week or two of thinking, note taking, and revising.


Overview


My initial vision of the Instruction Controller (IC) is a sequencer that will:

  1. Load the PC to the Address Latch

  2. Ask the Bus Controller to fetch an instruction and store it in the Instruction Register

  3. Have the Addressing Mode Decoder examine the Instruction Register to see if we need an operand, or skip to 7.

  4. Load the MicroCode PC with the start address of the routine to fetch the first operand according to the Addressing Mode. The MicroCode Sequencer will execute until the first operand is stored in the appropriate ALU latch.

  5. Have the Addressing Mode Decoder examine the Instruction Register again to see if we need an additional operand, or skip to 7, leaving any computed or fetched indirect address in the Address Scratch register.

  6. Load the MicroCode PC with the start address of the routine to fetch the second operand according to the Addressing Mode. The MicroCode Sequencer will execute until the second operand is stored in the appropriate ALU latch. This step will potentially overwrite the computed or fetched indirect address in the Address Scratch register with a new address, which will serve as the destination of the result, if appropriate.

  7. Have the Addressing Mode Decoder examine the Instruction Register again and route the appropriate bits to the Instruction Decoder.

  8. The Instruction Decoder looks up the start address of the MicroCode routine for the subject instruction.

  9. The Controller turns control over to the MicroCode Sequencer, which executes the necessary instructions to perform the actual data manipulation. The MicroCode will store the result of any computation, possibly making use of the address in the Address Scratch register or the Register Address latch, before returning.

  10. When the MicroCode signals it is done, the Controller will then return to step 1 above.


Since the MicroCode for the actual instruction may take several cycles, I thought I might try to pipeline operations a little and start the instruction fetch and Addressing Mode Decoder steps as soon as the Controller turns control over to the MicroCode Sequencer for the instruction itself. Whoever finishes first would have to wait.


Hmm. It sounds nice, but as I type this I see a potential problem. If the result of the current instruction alters one of the operands of the subsequent instruction, that would be a problem. In theory, I could check to see if the addresses are the same and then pause the pre-fetch logic if they are, or even detect if the operand >changed<. Or it could be more trouble than it’s worth.  I’m voting for the latter.


Maybe the Controller could at least pre-fetch the next instruction. Ah, but what if there’s a branch? I could pre-fetch the instruction as long as the current instruction isn’t a branch, jump, RTS, RTI, etc. OK, KISS for now.


Allright, in any case, I see the Instruction Sequencer (IS) using a shift register to sequence the steps as:


  1. Route PC to AR, fetch instruction to IR, evaluate IR with AMD logic on the fly, preload the IS if needed to jump ahead to 3 or 4

  2. Load Operand 2 (src) to ALU-Left, evaluate IR with AMD logic on the fly

  3. Load Operand 1 (dest / src+dest) to ALU-RIGHT

    1. Start here for 1-operand instructions

  4. Lookup Instruction MC Start Address

    1. Start here for 0-operand instructions

  5. Start MC for instruction processing

    1. Instruction processing is responsible for writing result of any instruction to dest

    2. Instruction processing is responsible for incrementing PC

  6. When instruction MC is done, signal IS to jump back to 1


Addressing Mode Decoder


The Addressing Mode Decoder (AMD) unit needs to do a few computations:

  • Examine the instruction and determine the number of operands, this decides when to turn control over to the instruction MicroCode and which ALU input latch each operand is routed to

  • Examine an operand field (6-bits) and determine the MicroCode start address for the routine to handle fetching the operand

  • Examine an operand field (6-bits) and determine how to route some of the operand field bits to the Register Address bus

  • For certain instructions, IM, SWI, BRA, JMP, JSR, latch or gate some of the operand-field bits to make them available to the MicroCode.

  • Examine the IR and route appropriate bits to the Instruction Decoder to determine the MicroCode start address for the actual instruction itself.


Let’s look at each computation in turn.

Number of Operands

There are fifteen permitted instructions that require two operands. I’ve designed these so that if the high 4 bits of the IR are 0000 - 1110, there are 2 operands. 


If the high 4 bits of the IR are 1, then we examine the next 6 bits, IR11 - IR6. If they are 000000 - 001111, we have a 1-operand instruction. 


If IR11-IR10 are 01 or 1X, we have 0-operand instruction, but we need to parse the OpCode bits in a special way. 


Finally, if IR15-IR6 are 1, then we have a 0-operand instruction without special treatment of the operand field.


Two Operand = NAND(IR15-IR12) = OR(IR15*, IR14*, IR13*, IR12*)


One Operand = AND(IR15, IR14, IR13, IR12, IR11*, IR10*)


Zero Operand = AND(IR15, IR14, IR13, IR12, B11) OR AND(IR15, IR14, IR13, IR12, IR10)


Each of these expressions has 2 gate delays, which is the best we can do.

Operand Fetch MicroCode Start Address

To actually fetch the operand and latch it in a register or one of the two ALU operand latches, we will turn control over to the MicroCode. There will be a separate MicroCode routine to handle each Addressing Mode. We need to examine the Addressing Mode and determine the start address in MicroCode to handle it. That is this task.


We will use a small ROM to examine the addressing mode bits and lookup the MicroCode starting address. In principle, we could just route all 16 bits of the IR to the Instruction Decoder to find the start address, but because there is so much redundancy, that is inefficient. We would need a 64Kx16 memory to look up just 8 distinct Addressing Modes. Better to reduce it with some logic.


Note that we’ll need to do this for each operand, so we’ll need logic to select the Addressing Mode select bits for each of the operands in sequence when we have two operands, and just for the one operand when we have just one.


As a reminder, here are the addressing modes and how the bits of the Operand field signal the mode.



Mode Name

OP5

OP4

OP3

OP2

OP1

OP0

Addr Mode ID

Register

0

X

X

X

X

X

0 = 000

Indexed

1

0

0

X

X

X

1 = 001

Indirect

1

0

1

X

X

X

2 = 010

Doubly Indir

1

1

0

X

X

X

3 = 011

Indir Pre-Dec

1

1

1

0

X

X

4 = 100

Indir Post-Inc

1

1

1

1

X

X

5 = 101

Immediate

1

1

1

1

1

1

6 = 110



We’ll map the OP bits to the Addressing Mode ID to look up the MC start address of the routine to handle the Addressing Mode. We need to generate a 3-bit value. Here are the Karnaugh maps to generate the low 3 bits of the MC start-address lookup. We’ll deal with the upper bits later.



Mode Lookup Bit 2

OP2-0:

000

001

011

010

110

111

101

100

 OP5-3: 000

0

0

0

0

0

0

0

0

001

0

0

0

0

0

0

0

0

011

0

0

0

0

0

0

0

0

010

0

0

0

0

0

0

0

0

110

0

0

0

0

0

0

0

0

111

1

1

1

1

1

1

1

1

101

0

0

0

0

0

0

0

0

100

0

0

0

0

0

0

0

0



Mode Lookup Bit 1

OP2-0:

000

001

011

010

110

111

101

100

 OP5-3: 000

0

0

0

0

0

0

0

0

001

0

0

0

0

0

0

0

0

011

0

0

0

0

0

0

0

0

010

0

0

0

0

0

0

0

0

110

1

1

1

1

1

1

1

1

111

0

0

0

0

0

1

0

0

101

1

1

1

1

1

1

1

1

100

0

0

0

0

0

0

0

0



Mode Lookup Bit 0

OP2-0:

000

001

011

010

110

111

101

100

 OP5-3: 000

0

0

0

0

0

0

0

0

001

0

0

0

0

0

0

0

0

011

0

0

0

0

0

0

0

0

010

0

0

0

0

0

0

0

0

110

1

1

1

1

1

1

1

1

111

0

0

0

0

1

0

1

1

101

0

0

0

0

0

0

0

0

100

1

1

1

1

1

1

1

1


So:

  • MLookupBit2: OP5 AND OP4 AND OP3

  • MLookupBit1: AND(OP5, OP4, OP3*) OR AND(OP5, OP4*, OP3) OR AND(OP5, OP4, OP3, OP2, OP1, OP0)

  • MLookupBit0: AND(OP5, OP4, OP3*) OR AND(OP5, OP4*, OP3*) OR AND(OP5, OP4, OP3, OP2, ,OP1, OP0*) OR AND(OP5, OP4, OP3, OP2, OP1*)


Register Address Bit Routing

For most of the addressing modes, I need to route register select information from the operand field to the register memory address lines (RA). Most of the development for this topic is in the post “Instruction Set and CPU Architecture.”


The logic is:


RA4 = OP5 OR OP4


For RA3:

Conceptually:


RA3

OP2, 1, 0:

000


001


011


010


110


111


101


100

OP5, 4, 3: 000

OP3

OP3

OP3

OP3

OP3

OP3

OP3

OP3

001

OP3

OP3

OP3

OP3

OP3

OP3

OP3

OP3

011

OP3

OP3

OP3

OP3

OP3

OP3

OP3

OP3

010

OP3

OP3

OP3

OP3

OP3

OP3

OP3

OP3

110

OP2

OP2

OP2

OP2

OP2

OP2

OP2

OP2

111

1

1

1

1

1

1

1

1

101

OP2

OP2

OP2

OP2

OP2

OP2

OP2

OP2

100

OP2

OP2

OP2

OP2

OP2

OP2

OP2

OP2


In detail:

RA3

OP2, 1, 0:

000


001


011


010


110


111


101


100

OP5, 4, 3: 000

0

0

0

0

0

0

0

0

001

1

1

1

1

1

1

1

1

011

1

1

1

1

1

1

1

1

010

0

0

0

0

0

0

0

0

110

0

0

0

0

1

1

1

1

111

1

1

1

1

1

1

1

1

101

0

0

0

0

1

1

1

1

100

0

0

0

0

1

1

1

1


RA3 = AND(OP5, OP4, OP3) OR AND(OP5*, OP3) OR AND(OP5, OP2)


For RA2:

Conceptually:


RA2

OP2, 1, 0:

000


001


011


010


110


111


101


100

OP5, 4, 3: 000

OP2

OP2

OP2

OP2

OP2

OP2

OP2

OP2

001

OP2

OP2

OP2

OP2

OP2

OP2

OP2

OP2

011

OP2

OP2

OP2

OP2

OP2

OP2

OP2

OP2

010

OP2

OP2

OP2

OP2

OP2

OP2

OP2

OP2

110

OP2*

OP2*

OP2*

OP2*

OP2*

OP2*

OP2*

OP2*

111

1

1

1

1

1

1

1

1

101

OP2*

OP2*

OP2*

OP2*

OP2*

OP2*

OP2*

OP2*

100

OP2*

OP2*

OP2*

OP2*

OP2*

OP2*

OP2*

OP2*



In detail:


RA2

OP2, 1, 0:

000


001


011


010


110


111


101


100

OP5, 4, 3: 000

0

0

0

0

1

1

1

1

001

0

0

0

0

1

1

1

1

011

0

0

0

0

1

1

1

1

010

0

0

0

0

1

1

1

1

110

1

1

1

1

0

0

0

0

111

1

1

1

1

1

1

1

1

101

1

1

1

1

0

0

0

0

100

1

1

1

1

0

0

0

0


RA2 = AND(OP5, OP4, OP3) OR AND(OP5*, OP2) OR AND(OP5, OP2*)


For RA1 and RA0:

RA1 = OP1

RA0 = OP0


Special Bits Capture

See below.


Instruction Start Address Lookup

We will use a small ROM (the Instruction Decoder, ID) to look up the starting address in the MicroCode to either fetch an operand or to perform an instruction. We don’t yet know how many address lines we’ll need to select from the ID, but let’s refer to them as IDA0, IDA1, …


To conserve resources, we would like the lookups in the ID to be relatively compact, meaning that there should not be large groups of input addresses that are unused. That just wastes ROM real-estate.


  • For operand fetch, we use logic to map the Addressing Mode to 3 bits: MLookupBit2, 1, and 0

    • Map MLB2 to IDA2, MLB1 to IDA1, MLB0 to IDA0.

    • We’ll have to figure out IDA3… later

  • For 2-operand instructions, we can route IR15-IR12 to IDA3-IDA0

    • We’ll have to figure out IDA4… later

  • For 1-operand instructions, we can route IR9-IR6 to IDA3-IDA0

    • We’ll have to figure out IDA4… later

  • For most 0-operand instructions, we can route IR3-IR0 to IDA3-IDA0

  • For 0-operand instructions IM, SWI, BRA, JMP, and JSR, we will want to find a way to map to IDA3-IDA0 as well

  • I haven’t mentioned it yet, but I’ll also need some routines in the MicroCode to store the result of instruction operations according to the addressing mode of the destination operand (or the single operand). I can probably reuse MLB2-MLB0, but will need to map the options to a different block of the ID.


Of course, we can see that we will need to generate the upper bits of IDA to avoid collisions among the different groups of instructions that all want to use IDA3-IDA0. The 2-, 1-, and 0-operand instructions all want 4-bits each, so let’s allocate 16 addresses to each major block, and map the blocks as follows:



Lookup Type

IDA6-IDA4

IDA3-IDA0

Operand Fetch

000

0, MLB2, MLB1, MLB0

2-Operand Instruction

001

IR15, IR14, IR13, IR12

1-Operand Instruction

010

IR9, IR8, IR7, IR6

0-Operand Instruction

011

IR3, IR2, IR1, IR0

Special 0-Operand

100

(Logic Mapping)

Result Save

101

0, MLB2, MLB1, MLB0

Unused

110

n/a

Reserved

111

(Logic Mapping)


Bottom line: I need 128 addresses in the IM. The width of the IM will be determined by the size of the MicroCode ROM, which we don't yet know.


As I was typing this out, it occurred to me that I could use the “Reserved” block of lookups (IDA6-4: 111) to lookup MicroCode to handle RESET, and Interrupt handling. I’ll keep that in my back pocket for later.


Finally, I need to design some logic to map the special 0-operand instructions (IW, SWI, BRA, JMP, JSR) to a compact list of values for IDA3-IDA0. Let’s tackle that.


Instruction

IR

Desired IDA3-0

IM

1111 0100 00mm mmmm

0000

SWI

1111 0100 01mm mmmm

0001

BRA

1111 1ccc dddd dddd

0010

JMP

1111 1111 1110 0ccc

0011

JSR

1111 1111 1110 1ccc

0100


I observed two possible optimizations here. 


First, if I reorganize the bits of BRA to move the condition codes to the low bits, it will make it easier to use the condition code bits because they will always be in the same place and I won’t need logic to select them from two different places.


Second, if I set the desired IDA for JMP and JSR differently, I can map IR3 to IDA0 to differentiate between JMP and JSR with less logic. Using the same observation, I see that I can likely map IR6 to IDA0 to differentiate between IM and SWI.


OK, so here’s the redesigned map for IDA3-0.



Instruction

IR

Desired IDA3-0

IM

1111 0100 00mm mmmm

0000

SWI

1111 0100 01mm mmmm

0001

BRA

1111 1ddd dddd dccc

0010

JMP

1111 1111 1110 0ccc

0100

JSR

1111 1111 1110 1ccc

0101


I didn't bother with a Karnaugh map on this one, just eyeballed it. Here’s the logic to control the IDA3-0 and the routing of the bit fields for ccc, dddd dddd, and mmm mmm.


IDA3 = 0

IDA2 = AND(IM15-IM5, IM4*)

IDA1 = AND(IM15-11)

IDA0 = AND(IM15-IM12, IM11*, IM10, IM9*, IM8*, IM7*, IM6) OR AND(IM15-IM5, IM4*, IM3)

MMMGate = AND(IM15-12, IM11*, IM10, IM9*, IM8*, IM7*)

DDDGate = AND(IM15-11)

CCCGate = AND(IM15-IM5, IM4*)


At the cost of one additional gate of propagation delay, it would be possible to re-use AND(IM15-IM12), AND(IM11*, IM10, IM9*, IM8*, IM7*), and AND(IM11-IM5). Also note that some of the ANDs are so wide that I will have to cascade two AND gates.


Whoopsie! Do-Over Required

[Edit 3/26/25: I am such a numbskull. The design above won’t work!  I can’t believe I didn’t see the problem. This is embarrassing.


Clearly, there are values of dddd dddd and ccc that make BRA indistinguishable from JMP and JSR. That means the instruction decoder can’t tell which instruction to execute. That’s a problem.


I need to reassign the distinguishing upper bits of the third hex digit from the right of the Instruction Word so that the Instruction Decoder can distinguish between the instructions, regardless of what the operand is.


Since BRA needs the most auxiliary bits, let me start fixing it by making the op code for BRA:

  • 1111 0ddd dddd dccc


Then Special Instructions IM, SWI, JMP, and JSR can be (respectively):

  • 1111 1000 00 mmm mmm

  • 1111 1000 01 mmm mmm

  • 1111 1000 10 000 ccc

  • 1111 1000 11 000 ccc


This approach keeps the bits that distinguish the instruction class all together and clusters the bits that differentiate the specific instruction within the class all together too.


The standard 1-Operand instructions can be:

  • 1111 11xx xxpp pppp

    • Where xxxx encode one of 16 instructions, which should be enough

    • pp pppp encode the operand


Finally, Zero-Operand instructions can be:

  • 1111 111111 00 xxxx


To summarize:


Instruction Word

Instruction

ID6-ID4

Instruction / Instruction Group

0000 aaaaaa bbbbbb

  • 1110 aaaaaa bbbbbb

LD, ADC, ADD, AND, CMP, OR, SUB, SBC, XOR

001

2-operand instructions

1111 0 dddddddd ccc

BRA

100

Special

1111 100000 mmmmmm

  • 1111 100000 mmmmmm

IM

100

Special

1111 100001 mmmmmm

  • 1111 100001 mmmmmm

SWI

100

Special

1111 100010 000ccc

JMP

100

Special

1111 100011 000ccc

JSR

100

Special

1111 110000 bbbbbb

  • 1111 110111 bbbbbb

NOT, NEG, INC, DEC, ROT*, SHIFT*

010

1-operand instructions

1111 111111 00 0000

  • 1111 111111 00 1111

RTS, SWI, RTI, NOP, STC, CLC, etc.

011

0-operand instructions


aaaaaa = Source operand bits

bbbbbb = Destination operand bits

ccc = Condition code for branches and conditional jumps

dddddd = Displacement (-128 - +127) for relative branches

mmmmmm = bit mask for interrupts


OK, now that seems to be sorted, let me rebuild the logic that will detect the instruction group (for the high bits of the Instruction Decoder) and route the bit fields that distinguish the specific instructions to the low bits of the Instruction Decoder.


Here is the logic for the high bits of the Instruction Decoder lookup table.

Note that ID6-4 = 000 means we are looking up the subroutine to fetch an operand according to the Addressing Mode. The Instruction Sequencer state machine will force ID6-ID4 = 000 during those states and will drive ID3-ID0 from the Addressing Mode Decoder.


ID6 = AND(IW15, IW14, IW13, IW12, IW10*) OR 

AND(IW15, IW14, IW13, IW12, IW11, IW10*)

(BY observation)


ID5 = AND(IW15, IW14, IW13, IW12, IW11, IW10, IW9*) OR

AND(IW15, IW14, IW13, IW12, IW11, IW10, IW9, IW8, IW7, IW6, IW5*, IW4*)

(By Karnaugh map)


ID4 = NAND(IW15, IW14, IW13, IW12) OR

AND(IW15, IW14, IW13, IW12, IW11, IW10, IW9, IW8, IW7, IW6, IW5*, IW4*)

(By observation)


Let’s see how I will route the instruction bits to the low bits of the ID.



Instruction Group

ID6-ID4

Source for ID3-ID0

Operand Fetch

000

AMD

2-Operand

001

IR15-IR12

1-Operand

010

IR9-IR6

0-Operand

011

IR3-IR0


I’ll use four 4-1 multiplexers to do this. I’ll need to select parts with an Output Enable function so I can suppress the outputs when ID6-ID4 is 100.


For the Special instructions, there is no “instruction field” in the IW, so we need to synthesize a compact set of values for ID3-ID0 based on the IR bits. 


First, I lay out the table of the information I have to work with. Reflecting, I can see that bits IW9-6 map directly to the instruction, except for BRA.


Instruction Word

Instruction

ID3-ID0

1111 0 dddddddd ccc

BRA

xxxx

1111 100000 mmmmmm

  • 1111 100000 mmmmmm

IM

0000

1111 100001 mmmmmm

  • 1111 100001 mmmmmm

SWI

0001

1111 100010 000ccc

JMP

0010

1111 100011 000ccc

JSR

0011


So, I could implement a little logic to map BRA to a value like 0100 and I’d be set.


ID3 = 0


ID2 = AND(IW11*, IW10*) OR

AND(IW11*, IW10)


ID1 = AND(IW11, IW10*, IW9*, IW8*, IW7)


ID0 = AND(IW11, IW10*, IW9*, IW8*, IW6)


I generated these with four Karnaugh maps for ID3, ID2, ID1, and ID0 with inputs: IW11, 10, 9, 8, 7, 6.


I can probably wire-OR these to the outputs of the 4-1 multiplexers I mentioned above, or use a buffer with a tri-state output.


END OF THE DO-OVER

One Final Comment

At the start of the project, I set as one of my objectives to use only TTL. I’m seeing now that using PLDs (22V10, 16V8, etc.) will reduce my chip-count (and wiring work) by a LOT. I think I’m going to back down on my initial commitment not to use PLDs. And, I’ve never used one before, so that will be fun.


Comentarios

Obtuvo 0 de 5 estrellas.
Aún no hay calificaciones

Agrega una calificación
bottom of page