CHDL Tutorial 0: Das Blinkenlights

by chad

CHDL and its associated standard library has grown by leaps and bounds over the past year, thanks in part to a decision by my employers to adopt it as a research vehicle. CHDL code now looks quite different than it did a year ago. There is a conditional assignment system that simplifies the creation of state machines. The chdl/statemachine.h API has been deprecated, there is now basic support for buses and tri-state drivers, and simulation speed is soon to receive a huge boost.

Because CHDL development has arrived at a point where I want to actively encourage others to try it, use it, and hopefully contribute to it, I have decided to immediately begin releasing tutorial and technical information related to CHDL in particular and digital electronics deisn in general through this web site. By doing this I hope to educate visitors interested in digital electronics, promote CHDL as a platform for hardware development, and thoroughly document the CHDL interfaces and internals.

This first set of articles, which I hope to make available in rotation with at least two other sets, is devoted entirely to the use of CHDL; written to an audience expected to consist primarily of amateur electical engineers, but with nothing that an experienced professional or engineering student would not be expected to understand. Throughout this tutorial I have included exercises for the reader. Most of these will require the consultation of other references to complete. The truly interested reader is likely to ditch these and do something they find fun instead, but I think of them as milestones. If you think you would be unable to complete an exercise, reading past it may be unwise.

Getting CHDL

It is important to note that CHDL is released under the GNU LGPL. This means that any derivative of CHDL must remain under this license, but does not extend to software linking (statically or dynamically) against the library. This means that, if you want to use CHDL on a commercial hardware project, binary simulation applications and libraries can be released under whatever license you feel like. This will satisfy all of the electrical engineers in your company with a list of patents on their CVs.

Previous articles included some instructions for getting and installing CHDL, but for the sake of completeness, slightly more detailed instructions will be included here. It is expected that anybody reading this is running a relatively recent Linux or Mac OS and has some level of comfort obtaining and using software. The only requirements are a recent G++ or Clang compiler that supports C++ 11 and GNU Make, but a waveform viewer program like GTKWave would be useful too, for viewing output. A large number of engineers primarily use Windows. CHDL will likely compile with a little coaxing in CygWin, which also provides a reasonable development environment.

CHDL is a library, and using it well requires some knowledge of how libraries work on your platform of choice. For Linux, there are many places to start, but see this guide for a tutorial.

Exercise: Write a library in C++ that contains a single function, “say_hi” that prints “Hello, world!” to the screen. Compile and link it as a shared object and write a program, in a separate directory, that calls this function once and exits. If you feel you can do this, you are certainly well-prepared to install and use CHDL!

Downloading the Source

The most recent CHDL source can be obtained from GitHub, either by using the Git client:

$ git clone https://github.com/cdkersey/chdl.git

or as a .zip archive.

Building CHDL

In the CHDL source directory, type make. This will invoke GNU Make, which will read the Makefile and automatically build the source files and then link them into libchdl.so. This is the CHDL library, ready to use. If make errors out, please send a full bug report to chad@cdkersey.com.

Installing CHDL (optional)

To make CHDL available system-wide, run sudo make install. This is not strictly necessary, but if /usr/local/lib is in your library search path, linking against an installed CHDL becomes somewhat more straightforward for any user on the system.

Building and Running the Tests

CHDL includes both a set of tests and a set of example designs. As a first step, just to make sure everything has built correctly, run some tests. They provide an automated way to verify that CHDL is running correctly.

chdl$ cd test
test$ make
g++ -std=c++11 -O2 test_adder.cpp test_common.h -lchdl -o test_adder
g++ -std=c++11 -O2 test_mult.cpp test_common.h -lchdl -o test_mult
g++ -std=c++11 -O2 test_logic.cpp test_common.h -lchdl -o test_logic
g++ -std=c++11 -O2 test_shift.cpp test_common.h -lchdl -o test_shift
test$ LD_LIBRARY_PATH=../ make run
./test_adder > test_adder.run 2> test_adder.opt
./test_mult > test_mult.run 2> test_mult.opt
./test_logic > test_logic.run 2> test_logic.opt
./test_shift > test_shift.run 2> test_shift.opt

This last command will take a few seconds but should complete with no errors.

Exercise: Build and install CHDL on your machine, and run all of the tests.

Creating a CHDL Project

Now that we have verified that CHDL is working, it’s time to build something! We will start with the simplest sequential circuit imaginable: a 1-bit toggle that alternates values on each clock cycle. This will allow us to focus our attention on the fundamentals of CHDL without being distracted by the complexity of the design.

Starting a Project

CHDL designs are just C++ programs, so feel free to use whatever environment you normally use for editing and compiling C++ and take the following as a detailed step-by-step suggestion.

We start by creating a new directory for our project. Somewhere in the filesystem:

$ mkdir 0_blinkenlights
$ cd 0_blinkenlights

We then create our code file and a Makefile:

0_blinkenlights$ touch Makefile blinkenlights.cpp

Makefile should be initialized with the following text:

CXXFLAGS ?= -std=c++11
LDLIBS ?= -lchdl
blinkenlights: blinkenlights.cpp
clean:
        rm -f blinkenlights

Remember that in makefiles, tab characters have syntactic meaning, so be sure to indent using tabs instead of spaces wherever indentation is needed.

Now initialize blinkenlights.cpp with:

#include <iostream>
#include <fstream>
#include <chdl/chdl.h>
using namespace std;
using namespace chdl;
int main() {
  return 0;
}

This is the most basic, empty C++ program using CHDL. It includes the proper header files, brings the CHDL namespace into file scope and then does nothing. Before you build or run the test program, make sure the CHDL library is in both your library and header search path.

A test build/run should complete successfully but do nothing interesting:

0_blinkenlights$ make
g++ -std=c++11 blinkenlights.cpp -lchdl -o blinkenlights
0_blinkenlights$ ./blinkenlights
0_blinkenlights$

Building and Simulating a Simple Design

Now that we have our boilerplate code in place, it’s time to build something! To the top of the main() function, add the following code:

node x;
x = Reg(!x);
TAP(x);
ofstream vcd("blinkenlights.vcd");
run(vcd, 100);

The first three statements create a signal called x and connect it to the output of a D flip-flop. The input of this D flip-flop is the complement of its output. This is our strobe. This signal is then tapped, making it visible as simulation output. The next pair of statements create an output file called “blinkenlights.vcd” and then run a simulation. This file can be viewed in GTKWave or another waveform viewer.

Compile and run the program again. There will again be no output to the console, but instead of doing nothing, the program will generate an output file called “blinkenlights.vcd”.

0_blinkenlights$ make
g++ -std=c++11 blinkenlights.cpp -lchdl -o blinkenlights
0_blinkenlights$ ./blinkenlights
0_blinkenlights$ ls
blinkenlights blinkenlights.cpp blinkenlights.vcd Makefile
0_blinkenlights$ gtkwave blinkenlights.vcd

Unless otherwise specified, the register is initialized to 0, leading to the following waveform:

This circuit can be thought of as a frequency divider dividing the (implicit, global) clock signal’s frequency by two. It is easy to start imagining a multitude of circuits that could be built and simulated in a similar fashion, but the reader is cautioned to read on first to get a better understanding of what this code does.

An Analysis of the Example

Only three lines of the example program can be thought of as an actual “design”:

node x;
x = Reg(!x);
TAP(x);

Let’s examine these lines in depth. (If you would like even more depth, consider reading the companion CHDL Internals articles.) The first line declares a variable named x of type node. This is similar to a node in the circuit sense. It is a Boolean logic signal that can take on only the values ‘0’ or ‘1’. It has a default value, and the value in a node can be overwritten.

The second statement overwrites the initial value of the node x with the output of a register. It also highlights a few important things about CHDL syntax and style. Remember that, in C++, arguments are evaluated before the function is called, and the function is called before its result is assigned. So how can !x mean “not this register’s output” when this register’s output has not been assigned yet. The reason for this is simple. Assignments to nodes are effective globally. If two objects of type node refer to the same signal, when the source of that signal changes, it changes for all node objects that pointed to the same signal. After all, they represent the same node in the circuit!

The second statement also demonstrates a few other things. All of the usual C++ logical and equality operators (||, &&, ==, !=, !) have been overloaded to work with CHDL nodes, creating a notation for implementing combinational logic. Also, it points out a characteristic of function names in CHDL. Uppercase initial characters (usually followed by camelCase) are reserved for functions that implement hardware. In this example, Reg() is a function that takes a node argument and returns a node result. Finally, it demonstrates an important fact about sequential logic in CHDL. Clock signals are implicit. All registers have an implied clock input that is connected to a global clock signal.

The third line of the design simply illustrates the easiest way to make signals visible in simulation. Signals are non-visible by default and must be explicitly exposed as taps. The TAP(x) preprocessor macro creates a tap with the same name as the signal passed to it. Because this is used to name the signal, the argument should only be a C++ identifier. If you would like to create taps on expressions, either give the expression a name first or rename it by using the tap() function instead:

tap("new_name_for_x", x);

This also offers an example of the capitalization scheme. Because tap(), like run() represents a utility function and not a piece of hardware, it is given a lowercase name. Compare to the function version of the ! operator, Inv() in the following alternate way to write the second line:

x = Reg(Inv(x));

Because a introduction to CHDL would be criminally incomplete without mentioning literals, let’s examine a third way to implement this:

x = Reg(Xor(x, Lit(1)));

Lit(1) creates a node that has the fixed value of 1, and Lit(0) does the same thing for 0. (The Xor() function here was chosen instead of its equivalent != operator for the sake of clarity.)

Those are the basics. There’s quite a bit you can do with only this information, and in fact this can be used as the basis for a quite complex simulation system. The only limitation being that taps are single bit signals. All of the extra functionality provided to the designer in CHDL could be implemented using only these basic elements, and indeed, moving it to a library separate from the CHDL core has been considered.

Exercise: Create a second node, y, that blinks half as fast as x.

Netlists and Optimization

Simulation is useful, but it would be nice to be able to fabricate these designs or at least load them into an FPGA. In order to do this there has to be a way to dump the design out of CHDL. Indeed there is. Add the following lines somewhere after the design and before the return statement in your main function:

ofstream netl(“blinkenlights.nand”);
print_netlist(netl);

When you build and run your program again, you will receive the following output in a file called “blinkenlights.nand”:

inputs
outputs
  x 2
inout
design
  litX 0
  inv 2 1
  reg 1 2

Because it was tapped, x is listed as a 1-bit output. Each line in the design section corresponds to a single gate. Its inputs are listed first followed by its output, as node indices. This one should make sense for the most part. The register’s output is inverted and fed back into the register’s input. The output is also tapped as x. But what’s this litX here on node 0. Its value is unused. If you guess that it must be the leftover initial value of x before it was reassigned, you would be correct. But it’s not used. Shouldn’t we be able to get rid of it?

Of course we should, and we can. A set of simple optimizations is built into CHDL to handle cases of unused (dead) nodes, duplicated logic, and simple constant folding. To invoke it, call the optimize() function with no arguments. If we add the following line:

optimize();

just after the last line of our design, we get the following output (on stderr) when we run the program:

 Before optimization: 3
 After dead node elimination: 2
 After contraction: 2
 After combining literals: 2
 After redundant expression elimination: 2
 After tri-state merge: 2

The numbers represent the number of nodes in the circuit after each optimization step. Because of the simplicity of our design, only the single dead node elimination was necessary. Our netlist is now a little more compact:

inputs
outputs
  x 1
inout
design
  inv 1 0
  reg 0 1

Verilog Output

This ad-hoc netlist format, while easy enough to parse by software, is of no use to us if we would like to actually, say, run this design on an FPGA. For that we are going to need to use CHDL’s Verilog output feature. Somewhere after the call to optimize() and before return, add the following lines to your file:

ofstream verilog("blinkenlights.v");
print_verilog("blink", verilog);

The first argument to print_verilog() is the name of the Verilog module being output, as a string. Compile, run, and inspect the output. Nothing too terribly interesting, all of the nodes are wires or registers whose names start with __x. Something is missing, though. If you look at the top of the Verilog output file, you will notice that the module has a single input called phi for the clock signal and no output. Instead, x is made an internal signal so that it can be easily viewed in simulation. How do we create an output, then? Luckily, there’s a macro for that. Simply replace TAP(x) with OUTPUT(x). The variable x is now a module output. Success!

Moving Forward

This tutorial has hopefully provided a starting point for anybody interested in using CHDL to develop hardware. We’re still a long way from building floating point units and microprocessors, but perhaps not as far away as the reader expects. Next time, we will use a more complex design to explore CHDL’s other hardware design features. Until then, there is plenty to try!

Exercise: Get CHDL-generated Verilog to compile as a top-level design in your FPGA toolchain of choice.

Exercise: Build a simple ripple-carry adder that takes two arrays of nodes as input and returns an array of nodes as output.

Exercise: Use the adder from the previous exercise to build a binary counter.