Improving Our Project

Read time: 40 minutes (10086 words)

We have solved the problem of modeling the simple movement of data from one place to another in the machine. We have a few useful classes and some code that runs. It is time to step back and review our work before moving into the real work of building our simulator.

In the next few steps, we will reorganize the project, than set up a framework that can grow with our development work. The end result should be pretty cool!

Step9: Project “Refactoring”

When you develop code, especially while working on a larger project, such as our simulator, it is appropriate to stop and look at what you have created. The idea is not to change things in any significant way, rather, it is to reorganize things to make what is going on more clear.

Our project is just a single director fill of files that make up our application. We have not done anything to really test the code well, and that is something we need to correct as soon as possible.

Note

The development up to this point has focused on showing you how to make small changes to the code, adding one basic concept at a time. My philosophy here is called “Baby Steps”, something that has proven itself as a good way to develop code. We will enhance that technique soon.

Organizing Project Code

In looking over our code files, we see a major split into their purpose. Only one file, main.cpp focuses on the user of the program. Since that file is the entry point for our entire application, I am going to move it into a separate folder maned src.

All the remaining code files make up the classes we have created for this application. In an ideal world, those classes define basic gadgets we might be able to use in other applications. It is common to call a collection of related classes a library, so I am going to move all class implementation files into a folder named lib.

The class header files are going to be treated differently. They really need to be separated from the implementation files, since they represent the “contract” between users and builders of the classes themselves. To make this distinction clear, we will place all header files in another folder named include.

A significant project needs two other directories: one for project documentation, and another for test code, something we have ignored up to this point.

Here is our new project directory structure:

+- attint85sim/
    |
    +- src\
    |   |
    |   +- main.cpp
    |
    +- lib\
    |   |
    |   +- class.cpp files
    |
    +- include/
    |   |
    |   +- class.h files
    |
    +- docs
    |
    +- tests

With this new layout, the only files remaining at the top level of our project should be these:

  • README.rst - all projects should have one of these
  • Makefile
  • .git
  • .gitignore

Note

I moved machine.hdl into the lib folder, since it is needed only by the Machine class, I modified the path to that file in the Machine.cpp file.

Rebuilding the Makefile

Obviously, making these changes breaks our simple Makefile. We need to modify that a bit for this new structure.

Without going into the details, this is the final file. (You should read up on Make and see what all of this means!

Makefile
 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
# Makefile for v0.9.0
TARGET = $(shell basename $(PWD))

# files
SSRCS	:= 	$(wildcard src/*.cpp)
SOBJS	:=	$(SSRCS:.cpp=.o)
LSRCS	:=	$(wildcard lib/*.cpp)
LOBJS	:=	$(LSRCS:.cpp=.o)

# tools
CXX		:= g++
CFLAGS	:= -I include
RM		:= rm -f

.PHONY: all
all:	$(TARGET)

$(TARGET):	$(SOBJS) $(LOBJS)
	$(CXX) -o $@ $^

%.o:	%.cpp
	$(CXX) -c $(CFLAGS) -o $@ $<

.PHONY: run
run:	$(TARGET)
	./$(TARGET) cycsi.hdl

.PHONY: clean
clean:
	$(RM) $(TARGET) $(SOBJS) $(LOBJS)

Note

You should commit this version of the code now. Tag it as version v.0.9.0

Step10: Testing your Code

What does the term “testing” even mean?

For some developers, hopefully only those new to the craft, testing means making sure the program compiles and runs - at least one.

Good question. The right answer is Test Driven Development, a working method that builds up a set of tests as you write your code.

Catch.hpp

There is a nice testing framework for C++ projects that is implemented in a (huge!) single header file. You set up a separate directory to hold all test code, add this header, then create a set of test files that exercise each major component of your code. In fact, you should also add a tool to your tool set that checks to see if every line of code in your program gets tested at some point.

Yes, this sounds like a bunch of work, but it pays off in the end.

Setting Up for Testing

Let’s create the tests directory:

$ cd attiny85sim
$ mkdir tests
$ cd include
$ wget https://raw.githubusercontent.com/CatchOrg/Catch2/master/single_include/catch.hpp

(That looks like something we should teach Make to manage!)

Here is the starting code I place in the tests folder:

test_main.cpp
1
2
3
#define CATCH_CONFIG_MAIN
#include "catch.hpp"

test_sanity.cpp
1
2
3
4
5
#include "catch.hpp"

TEST_CASE( "sanity test", "sanity" ){
    REQUIRE(true);
}

The test_main.cpp file creates a main function for a single test application constructed out of all files in this directory with names starting with test. Those files will all include the catch.hpp header, and then set up simple test blocks that check some aspect of your code.

Normally, we test each method (or function) in each class you write. This kind of testing is called unit testing since we are testing a “unit” of code.

Note

It makes no sense to try to test anything smaller than a function. Single lines of code need a “context” in which to run, and setting that up is far too complex. SO we test functions, which should not need a “context” to run properly. In real life, we NEVER use global variables for anything, making each function a simple gadget to test!

Sanity Testing

I drop a single test file in my tests folder to make sure the testing system is running properly. Once that has been done, you could drop that file out, but it does not hurt anything to leave it in.

Here is my new Makefile, which adds targets to build the test application, and run the tests.

Makefile
 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
# Makefile for v0.9.0
USRTGT = $(shell basename $(PWD))
TSTTGT = test-app

# files
SSRCS	:= 	$(wildcard src/*.cpp)
SOBJS	:=	$(SSRCS:.cpp=.o)
LSRCS	:=	$(wildcard lib/*.cpp)
LOBJS	:=	$(LSRCS:.cpp=.o)

TSRCS	:=  $(wildcard tests/*.cpp)
TSRCS	+=  $(wildcard tests/test_classes/*.cpp)
TOBJS	:= $(TSRCS:.cpp=.o)

LIB		:= $(USRTGT)lib.a
TLIB	:=	Test_$(USRTGT)lib.a

# tools
CXX		:= g++
CFLAGS	:= -I include -std=c++11
AR		:= ar
RM		:= rm -f

.PHONY: all
all:	$(USRTGT) $(TSTTGT)

$(LIB):	$(LOBJS)
	$(AR) rcs $@ $^

$(TLIB):	$(TOBJS)
	$(AR) rcs $@ $^

$(USRTGT):	$(SOBJS) $(LIB) 
	$(CXX) -o $@ $^


$(TSTTGT): $(TOBJS) $(TLIB) $(LIB)
	$(CXX) -o $@ $^

%.o:	%.cpp
	$(CXX) -c $(CFLAGS) -o $@ $<

.PHONY: run
run:	$(USRTGT)
	./$(USRTGT)

.PHONY: test
test:	$(TSTTGT)
	./$(TSTTGT)

.PHONY: clean
clean:
	$(RM) $(USRTGT) $(TSTTGT) $(LIB) $(SOBJS) $(LOBJS) $(TOBJS)

Running Tests

We can now verify that our testing system works:

And run it:

We have not done any real testing yet, but this shows we are ready to add test code.

Testing Wires

Let’s test the Wire class first.

Here is a start that tests the constructor.

test_wire.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include "catch.hpp"
#include "test_classes/Test_Wire.h"

TEST_CASE( "test wire constructor", "wire" ){
    Test_Wire w1;
    REQUIRE(w1.twrite() == -1);
}

TEST_CASE("test wire write","wire") {
    Test_Wire w1;
    w1.tread(5);
    REQUIRE(w1.twrite() == 5);
}
test_classes/test_wire.h
1
2
3
4
5
6
7
8
9
#pragma once
#include "Wire.h"

class Test_Wire : public Wire {
    public:
        int tread(int val);
        int twrite(void);
};

The pattern you see here is used for all tests. We create an object using the class we are testing. Then we exercise that object usind available methods and check the results. For the constructor, we set an initial value for the wire, so we check that a new object created from this class returns that initial value.

Here is the Makefile used for this test:

 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
# Makefile for v0.9.0
USRTGT = $(shell basename $(PWD))
TSTTGT = test-app

# files
SSRCS	:= 	$(wildcard src/*.cpp)
SOBJS	:=	$(SSRCS:.cpp=.o)
LSRCS	:=	$(wildcard lib/*.cpp)
LOBJS	:=	$(LSRCS:.cpp=.o)

TSRCS	:=  $(wildcard tests/*.cpp)
TSRCS	+=	$(wildcard tests/test_classes/*.cpp)
TOBJS	:= $(TSRCS:.cpp=.o)

LIB		:= $(USRTGT)lib.a

# tools
CXX		:= g++
CFLAGS	:= -I include -std=c++11
AR		:= ar
RM		:= rm -f

.PHONY: all
all:	$(USRTGT) $(TSTTGT)

$(LIB):	$(LOBJS)
	$(AR) rcs $@ $^


$(USRTGT):	$(SOBJS) $(LIB) 
	$(CXX) -o $@ $^


$(TSTTGT): $(TOBJS) $(LIB)
	$(CXX) -o $@ $^

%.o:	%.cpp
	$(CXX) -c $(CFLAGS) -o $@ $<

.PHONY: run
run:	$(USRTGT)
	./$(USRTGT)

.PHONY: test
test:	$(TSTTGT)
	./$(TSTTGT)

.PHONY: clean
clean:
	$(RM) $(USRTGT) $(TSTTGT) $(LIB) $(SOBJS) $(LOBJS) $(TOBJS)

To see it work, we run the tests again.

And run it:

Modifying the Wire Class for Testing

We still have a problem in testing the wire class. As shown up to this point, the read and write methods take no parameters, which will make testing them hard. We need to be able to reach into the class, and test things in an isolated way. The wire class now will not work unless it is attached to components so it can transfer data. The test code should be able to place a value on the wire, and confirm that it is what will be delivered. We can do that by creating additional test methods to be used only for testing. We will call these methods tread and twrite.

Here is the modified Wire class:

Wire.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
#pragma once
#include <string>

class Component;

class Wire {
    public:
        Wire();
        void attach_in(Component *c_in);
        void attach_out(Component *c_out);
        void tick( void );
    private:
        int data;
        Component *in;
        Component *out;
        std::string source, dest;
        int read(void);
        int write( void );
    protected:
        int test_read(int val);
        int test_write(void);
};


Wire.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
#include <iostream>
#include <string>
#include "Wire.h"
#include "Component.h"

// constructor
Wire::Wire( void ) {
    data = -1;
    in = NULL;
    out = NULL;
}

// Accessor
int Wire::read(void) {
    // fetch wire value from input side
    data = in->write();
    source = in->get_name();
    return data;
}

int Wire::write( void ) {
    // deliver wire value to output side
    dest = out->get_name();
    out->read(data);
    std::cout << source << "-(" << 
        data << ")->" << dest << std::endl;
    return data;
}

void Wire::attach_in(Component *c_in) {
    in = c_in;
}

void Wire::attach_out(Component *c_out) {
    out = c_out;
}

void Wire::tick( void ) {
    read();
    write();
}

// thses methods are for testing in a derived class
int Wire::test_read(int val) {
    data = val;
    return data;
}

int Wire::test_write(void) {
    return data;
}

Houston, We Have a Problem

We have a fundamental problem here. We have worked pretty hard to make everything in our classes private. That means the only way we can “open up” access to objects from our simulator classes is to get the testing code to be a “friend” of each class, or rewrite the classes to make methods public again.

What is a lowly tester to do?

There are different approaches to solving this problem, Perhaps the strangest is to use the preprocessor to fake the testing system out by recompiling the tested code with the private keyword replaced with public. That means we need a separate build step for testing that builds things just for testing.

Here is an interesting hack I found on a popular website haunted by developers: stackoverflow

class BASE_CLASS {
  protected:
    int your_method(int a, int b);
};

class TEST_CLASS : public BASE_CLASS {
  public:
    int your_method(int a, int b) {
      return BASE_CLASS::your_method(a, b);
    }
}

This is a strange setup. It uses C++ inheritance to create a new class to be used only for testing. The only thing the new class does is expose the private functions so they can be tested. This seems simple enough. It does mean we need to litter our project with new classes used only for testing. That may mean we need to modify our project setup to keep this cruft from getting in our way as we develop the code.

We will get to that. Let’s just try to fix this one test as an experiment.

Here is the new derived Wire class:

test_classes/Test_Wire.cpp
1
2
3
4
5
6
7
8
9
#include "Test_Wire.h"

int Test_Wire::tread(int val) {
            return test_read(val);
}
int Test_Wire::twrite(void) {
return test_write();
}

Note

You should commit this version of the code now. Tag it as version v.0.10.0

Step11: Building Test Equipment

In a real project to build a system, especially a digital system, it is common to set things up and hook up test gear to watch the action when we power things up! (hopefully, no smoke appears!)

Simulating and Oscilloscope

An oscillscope is just a neat piece of test gear used to watch signals moving along a wire over some period of time. We have everything in place to instrument our system, all we need is a little graphics magic!

Python Graphics

We could build this tool using C++, but the code needed to do graphics in C++ is a bit ugly. So, I am going ot build a simple scope display using Python and the standard graphics library distributed with Python: Tkinter.

Getting Started

Although Python is installed as part of most Linux systems, the version installed by default is the older version (probably version 2.7). I am going to build this scope using the latest version of Python 3:

$ sudo apt-get install python3
$ sudo apt-get install python3-pip

Note

That last command is not strictly needed for this project, but it comes in handy for other Python projects (like Sphinx!)

Here is a starter program, that works, but is a bit unpolished at present. You can try it out to test your Python installation. We will fix things soon.

pyvcd.py
 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
from tkinter import *

nSignals = 10
nTicks = 40
tWidth = 15
tHeight = 15
tRise = 2
WIDTH = nTicks * tWidth
HEIGHT = nSignals * (tHeight* 1.5)

master = Tk()
master.title("pyVCD")

def genClock():
    t = []
    v = 0
    for c in range(nTicks):
        t.append(v)
        if v == 0:
            v = 1
        else:
            v = 0
    return t

def drawTrace(w, n, vals):
    """ Y is downware!"""
    yBase = (n + 1) * tHeight * 1.5
    yOld = yBase  
    for t in range(len(vals)):
        x1 = t * tWidth
        x2 = x1 + tWidth
        x3 = x1 + tRise
        y = yBase - vals[t]*tHeight
        if y == yOld: # no change
            w.create_line(x1,y,x3,y, fill="black") # signal continues
        elif y > yOld:  # ascend
            w.create_line(x1,yOld,x3,y,fill="black")   # signal rising
        else: # must be falling
            w.create_line(x1,yOld,x3,y,fill="black")  # signal falling
        w.create_line(x3,y,x2,y, fill="black")  # rest of this time slot
        yOld = y    # save for next time slot
                                                                                                         
def main():
    w = Canvas(master, width=WIDTH, height=HEIGHT)
    txt = Text(master, height=1, width=5)
    w.pack()

    # draw base lines
    for s in range(nSignals):
        x1 = 0
        y1 = s * (tHeight * 1.5)
        x2 = WIDTH
        y2 = y1
        w.create_line(x1,y1,x2,y2, fill="red")
    # draw clock
    v = genClock()
    print(len(v))
    for t in range(len(v)):
        x1 = t*tWidth;
        if (t % 10) == 0:
            w.create_line(x1,0,x1,HEIGHT, fill="blue")
    drawTrace(w,0,v)
    drawTrace(w,1,v)

    mainloop()

if __name__ == '__main__':
    main()

As it stands here, the code is not really displaying real signals, It is just demonstrating the methods we can use ot generate the display.

The output from this example code looks like this:

../_images/pyvcd-demo.png

I did something here to make things a bit more realistic. Signals, in general, do not change cleanly from a 0 to a 1 or visa versa. Instead, it takes some time to accomplish the transition. I set up a variable that allows the signal to change state over a small amount of time, resulting in the signals you see above. This is not really needed, but it was simple to implement.

Our real signals will be comong fron the C++ simulator code. To make this work, we have two approaches we can take. The simple one will be to let the C++ code run, and write output to a file we can then load into this display logic. That will work, and is how many simulators generate these displays In fact, there is a standard format for the data files called Value Change Dump (VCD) that is quite popular. There is also a standard tool, available for all platforms you can use to view these vcd files, that tool is called GTKwave. We can use this here, or we can try another approach.

On a real system, the scope would observe the system as it runs. We might be able to do that if we can couple the Python program to the C++ program. How might we do that?

One answer is to let the two programs communicate over a network connection between the two programs. skind of programming is becoming very common, and exploring the idea is something worth studying.

Note

You should commit this version of the code now. Tag it as version v.0.11.0