Control: The last step!

Read time: 22 minutes (5564 words)

The machine is wired and ready for action. All we need to do not is fire the beast up and let it run.

Well, that will happen when we find the right way to control the stages.

We know we need to activate the components to get internal processing taken care of, and we need to activate wires to move data around in our machine. A few small details will set up for a simple solution to the control problem (another 3am idea).

Tick Revisited

We originally designed every part in our machine using a single base class: Component. That class set up the structure common to all components. Every part has a set or inputs, and a set of outputs. The required tick method tells the part to perform whatever internal processing is needed to converts inputs to outputs.

Simple.

Control Signals

Our design uses a tick method with no parameters. Perhaps it makes sense to send down a single set of bits that can be used to tell the part the missing control signal data needed to process correctly.

For some parts, there is no need to additional signals, what they do is defined and does not change (like a basic boolean part, or an incrementer. Other parts need additional signals to decide what to do. All of our multiplexors work this way, as does the alu. Our memory units have additional controls to select read or write.

Let’s review our inventory, and see what additional signals we need:

MUX2

  • sel: 1 bit to choose which input is to be routed to the output.

MUX4

  • sel: two bits to choose one of tha available four inputs to route to the output.

PC

  • none: this unit needs no control.

IM

  • none: nothing needed here as well, thi sis a read-only device

DEC

  • none: This unit needs no control.

RF

  • rw: selects read or write actions

ALU

  • This unit needs controls, but those come from the decoder.

STK

  • rw: selects push or pop actions

DM

  • rw: selects read or write

IO – * rw: selects read or write

Looking back, it seems that we need at most two additional bits to decide what to do internally. If we add a single 8-bit parameter to the tick method, we can set up a simple control system that will let us get this system moving.

include/Component.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Copyright 2018 Roie R. Black
#pragma once

#include <string>
#include <vector>

#include "Component.h"
#include "Pin.h"


class Component {
 public:
    bool debug;
    // constructor
    explicit Component(std::string n);

    // mutators
    void add_in_pin(std::string name);
    void add_out_pin(std::string name);
    void tick(int ctrl);
    void set_pin_val(Pin * p, uint16_t val);
    void set_debug(void);

    // accessors
    std::string get_name(void);
    Pin * get_in_pin(std::string n);
    Pin * get_out_pin(std::string n);
    uint16_t get_pin_val(Pin * p);
    void dump(void);

 private:
    std::string name;
    std::vector<Pin *> in_pins;
    std::vector<Pin *> out_pins;
};

The base Component.cpp file needs a tweak as well, this is an easy fix.

Modifying all part classes also needs to happen. Once that is done, we modify the internal processing so we control what happens based on that new signal (which is really arriving at the part from the control unit). We will not add wires to make thies signals “move”!

Controlling Wires

We can set up a big simplification in the wire management by simply noting that wires only need to be activated when something changes on their inputs. Our current design does not pay attention to this, but it would be an easy fix to check the current output pin value and compare that with our new output values. If an output pin does not change, there is no need to activate that wire.

What we can do is activate all wires attached to output pins at the end of a tick on that component. That will simplify our controller logic.

Building The Controller

Here is a plan:

Set up another control file, much like the one used to generate our machine. This file will list the steps to be taken by identifying the part we need to activate, and the needed new signal value. Since what happens when the machine works through the four stages is completely defined by the instruction we are processing at the moment, we will create an array (actually a vector) holding the sequence of components we need to activate, in order, to process that instruction.

Here is a control file, showing the basic idea:

examples/avrsim.seq
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Fetch
	mux2.reset
	pc.0
	im.0
	END

Decode
	dec.0
	rf.0
	END
EOR
	mux2.1
	mux3.2
	alu.0
	END
OUT
	io.0
	END
IN
	io.1
	mux6.2
	rf.1
	END

In this example, the part to trigger is named, and that part should have been created in the net list we used to build our machine.

The dot followed by a number provides the signal value needed to this activation of that part.

Let’s work through the first two stages, since those do not really do anything different for any instruction. In fact, we can simply set up those two stages as extra “instructions” and record the actions needed for each. We will need to set up the controller so it “calls” these extra instructions on each machine cycle.

There is nothing in the fetch or decode stages that needs information from the controller. Perhaps we should just invent two new “instructions” to tell the machine to process those two actions.

The real issue is how to handle the last two stages.

Microcode

We have defined a list of steps we need to take so that data moves through our machine properly for each instruction we process. W need a way to save that list, so we can use it to manage the machine.

C++ supports the idea of an object that has no methods. The concept is an old one in programming, and we call an object of this kind a struct. Here are two struct data types we can use to set up our control system

include/microcode.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Copyright 2019 Roie R. Black
#pragma once
#include <string>
#include <vector>
#include "Component.h"

struct uOp {
    Component * part;
    int ctrl;
};

struct uCode {
    std::string name;
    std::vector<uOp *>uops;
};

We can use these structs to create a vector of instruction codes, each containing a name and another vector with the actual micro-operations we need to perform, in the correct order. Each micro-instruction (uOp holds a pointer to the part to be triggered, and the control value needed for that step. All of this can be set up using our text file above, with the help of yet another ugly parsing routine,

Note

Rather that paste that code here, it is in the CPUfactory3 repository, in the COntroller class implementation.

The final countdown!

Once we have this control system in place, all we need to do it start up the machine, load a program, activate the reset method, and start the machine running.

Note

The machine is running on my test system, but the code is not quite ready for prime time. There was a bit of major surgery needed to add the new tick mechanism to the system, and I am still not ready to release all the code. That should be done by next class!