Wiring the Stages

Read time: 12 minutes (3030 words)

There is a simple pattern that you will use to wire up the internal structure of each stage. It looks like this:

w1.attach_driven(inv1.get_out_pin("OUT"));
    w1.attach_drives(inv2.get_in_pin("IN"));

Basically, you need to study the stage diagram, identify all the wires in that stage, and make the connections needed.

Note

In the old days, I use to print out the diagram, and use a marker to highlight each connection I made until the circuit was actually constructed. I still have my original diagrams from the very first computer I designed from scratch back in 1978.

Rather than build each stage completely, we will use test-driven-development to put together our machine. We will use a top-down approach, beginning with a top-level class that encapsulates the entire machine. We will call that class the AVRsim. The constructor for this class will put together the entire machine (actually, it will just create stage classess, each of which is responsible for wiring up the needed internal structure).

Since we will want to run our machine either in a command line mode, or in a graphics mode, we will use pass the guienabled flag into this class through a run method.

Here is the class header file:

include/AVRsim.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// Copyright 2019 Roie R. Black
#pragma once
#include <string>
#include "Fetch.h"
#include "Decode.h"
#include "Execute.h"
#include "Store.h"

class AVRsim {
 public:
    // constructor
    explicit AVRsim(std::string name);

    // mutators
    void run(bool guienabled);
 private:
    std::string name;
    void build(void);
};

And, here is our traditional first test case for this clas:

tests/test_avsim.cpp
1
2
3
4
5
6
7
8
9
// Copyright 2019 Roie R. Black
#include <catch.hpp>
#include "AVRsim.h"

const std::string simname = "avrsim";
TEST_CASE("test AVRsim constructor", "[machine]"){
    AVRsim avrsim(simname);
    REQUIRE(avrsim.get_name() == simname);
}

Here is enough of an implementation to get started:

lib/AVRsim.cpp
 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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// Copyright 2019 Roie R. Black
#include <iostream>
#include <string>
#include "AVRsim.h"

AVRsim::AVRsim(std::string n) {
    name = n;
    build();
}

void AVRsim::run(bool guienabled) {
    std::cout 
        << "AVRsim running (gui enabled: " 
        << guienabled 
        << std::endl;
}

void AVRsim::build(void) {
    Fetch fetch("fetch");
    Decode decode("decode");
    Execute execute("execute");
    Store store("store");

    // connect the stages
    w1.attach_driven(fetch.get_out_pin("PCout"));
    w1.attach_drives(decode.get_in_pin("PCin"));
    w2.attach_driven(decode.get_out_pin("PCout"));
    w2.attach_drives(execute.get_in_pin("PCin"));
    w3.attach_driven(execute.get_out_pin("PCout"));
    w3.attach_drives(store.get_in_pin("PCin"));
    w4.attach_driven(store.get_out_pin("PCout"));
    w4.attach_drives(fetch.get_in_pin("PCin"));
}

uint16_t AVRsim::get_pcnext(void) {
    Pin * pcnext = fetch.get_in_pin("PCin");
    if (debug) std::cout << "pcnext" << pcnext << std::endl;
    return pcnext ? pcnext->get_val() : 0;
}

void AVRsim::tick(void) {
    fetch.tick();
    w1.tock();
    decode.tick();
    w2.tock();
    execute.tick();
    w3.tock();
    store.tick();
    w4.tock();
    fetch.tick();
    w1.tock();
    decode.tick();
    w2.tock();
}

Wiring the Stages

We can create simple classes that encapsulate the functions of each stage. These should inherit from our standard Component class.

Here is an example, for the Fetch Unit:

include/Fetch.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// Copyright 2019 Roie R> Black
#pragma once
#include <string>
#include "Component.h"
#include "MUX2.h"
#include "PC.h"
#include "IM.h"
#include "INC.h"

class Fetch : public Component {
 public:
    explicit Fetch(std::string name);
    void build(void);
    void tick(void);

 private:
    std::string name;
};



Program Counter Updating

The Program Counter is the key part of the Fetch stage. We will pass the value used to fetch an instruction through to the decode stage where it will be updated depending on the size f the current instruction. After that, the PC will flow into the other stages, eventually ending up back at the entry point to the Fetch Stage, where it will be used for the next instruction fetch.

We can wire up our entire machine using a simple pattern, and verify that we get a new PC when we get back to another fetch.

Here is the Fetch Unit implementation we need for this test:

lib/Fetch.cpp
 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
37
38
// Copyright 2019 Roie R. Black
#include <iostream>
#include "Fetch.h"
#include "MUX2.h"
#include "Wire.h"
#include "PC.h"
#include "IM.h"

Fetch::Fetch(std::string n):Component(n) {
    name = n;

    // create the needed connections
    this->add_in_pin("PCin");
    this->add_out_pin("PCout");

    // assemble this component
    build();
}

void Fetch::build(void) {
    std::cout << "assembling fetch unit" << std::endl;
}

void Fetch::tick(void) {
    Pin *inpin = this->get_in_pin("PCin");
    Pin *outpin = this->get_out_pin("PCout");
    uint16_t ival, oval;

    if (inpin) {
        ival = inpin->get_val();
        oval = ival;
        std::cout << "Fetch:" << oval << std::endl;
    }
    if (outpin) {
        outpin->set_val(oval);
    }
}

All other stages are identical for now, except we will add one to the PC as it passes through the decoder for this next test.

Each of these classes will contain an internal build method, where the internal structure of the stage will be wired. We will ge to that soon enough. For now, we want to focus on the top level structure of our simulator, so we have something running.

Testing a Machine Cycle

Obviously, we need to wire up all four stages to be able to run the next test. Here are the additions we need to do that:

include/AVRsim.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
// Copyright 2019 Roie R. Black
#pragma once
#include <string>
#include <cstdint>
#include "Fetch.h"
#include "Decode.h"
#include "Execute.h"
#include "Store.h"
#include "Wire.h"

class AVRsim {
 public:
    // constructor
    explicit AVRsim(std::string name, bool debug);

    // accessors
    std::string get_name(void);
    uint16_t get_pcnext(void);

    // mutators
    void run(bool guienabled);
    void tick(void);

 private:
    std::string name;
    bool debug;
    void build(void);
};

lib/AVRsim.cpp
 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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// Copyright 2019 Roie R. Black
#include <iostream>
#include <string>
#include "AVRsim.h"
#include "Pin.h"

// create objects for each stage
Fetch fetch("fetch");
Decode decode("decode");
Execute execute("execute");
Store store("store");

// create wires to hook them together
Wire w1("W1");
Wire w2("W2");
Wire w3("W3");
Wire w4("W4");

AVRsim::AVRsim(std::string n, bool dbg) {
    debug = dbg;
    name = n;
    build();
}

std::string AVRsim::get_name(void) {
    return name;
}

void AVRsim::run(bool guienabled) {
    std::cout
        << "AVRsim running (gui enabled: "
        << guienabled
        << ")" << std::endl;
}

void AVRsim::build(void) {
    if (debug) {
        std::cout << "attaching wires" << std::endl;
    }
    w1.attach_driven(fetch.get_out_pin("PCout"));
    w1.attach_drives(decode.get_in_pin("PCin"));
    w2.attach_driven(decode.get_out_pin("PCout"));
    w2.attach_drives(execute.get_in_pin("PCin"));
    w3.attach_driven(execute.get_out_pin("PCout"));
    w3.attach_drives(store.get_in_pin("PCin"));
    w4.attach_driven(store.get_out_pin("PCout"));
    w4.attach_drives(fetch.get_in_pin("PCin"));
}

uint16_t AVRsim::get_pcnext(void) {
    Pin * pcnext = fetch.get_in_pin("PCin");
    if (debug) std::cout << "pcnext" << pcnext << std::endl;
    return pcnext ? pcnext->get_val() : 0;
}

void AVRsim::tick(void) {
    fetch.tick();
    w1.tock();
    decode.tick();
    w2.tock();
    execute.tick();
    w3.tock();
    store.tick();
    w4.tock();
    fetch.tick();
    w1.tock();
    decode.tick();
    w2.tock();
}

The test we will run runs through a full four step cycle, and verifies that the program counter has been incremented

tests/test_avrsim.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Copyright 2019 Roie R. Black
#include <catch.hpp>
#include "AVRsim.h"

const std::string simname = "avrsim";
TEST_CASE("test AVRsim constructor", "[machine]"){
    AVRsim avrsim(simname);
    REQUIRE(avrsim.get_name() == simname);
}

TEST_CASE("test AVRsim PCnext function", "[machine]"){
    AVRsim avrsim(simname);
    uint16_t val = avrsim.get_pcnext();
    avrsim.tick();
    REQUIRE(avrsim.get_pcnext() == val+1);
}

Note

This is a bad test. Normally, you work hard to create tests that will survive untouched as the project eolves. In our real machine, there will be times when the program counter will increment by two, and this test will fail! It will also fail if we run a branch instruction, since then the value of the PC will be defined by that branch instruction.

We have enough of the top-level done now. We want to study how to integrate this machine into our graphics system next.