System Design with Verilog

Read time: 37 minutes (9305 words)

We do not have time to explore using an industry standard tool to design a system. But, we can take a look at a simple example just to see what this kind of design looks like using a serious hardware description language: Verilog

Icarus-Verilog

Many system designers use commercial tools to do their work, and those tools are hard for a student to obtain. Fortunately, there is a nice open-source implementation of the Verilog language we can use easily. That version is called Icarus Verilog. A bit of research turned up many examples of this program being used in academic projects.

Let’s give it a try, and use Docker to create a test environment.

Note

Docker installs nicely in Linux and on Mac systems. On the PC, you either need to install Docker in a Virtual Machine, or install Windows 10 Professional. This example project was created on my MacBook laptop.

Creating the Test Environment

With Docker, this is pretty easy. Here is the project Dockerfile I created for this test.

Dockerfile
1
2
3
4
5
6
7
8
FROM alpine

RUN apk --no-cache --update add bash
RUN apk --no-cache add build-base autoconf gperf git flex bison
RUN git clone https://github.com/steveicarus/iverilog.git
WORKDIR iverilog
RUN git checkout v10_2
RUN sh autoconf.sh && ./configure && make && make install

This file is pretty simple. I like using Alpine Linux, a very small version of Linux ideally suited for creating Docker Containers.

Here is the Makefile I used for this experiment:

Makefile
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
PROJNAME	:= $(shell basename $(PWD))
UID			:= $(shell id -u)
GID 		:= $(shell id -g)

.PHONY: build
build:
	docker build -t $(PROJNAME) .

.PHONY:	shell
shell:
	docker run --rm -it \
		--user="$(UID):$(GID)" \
		-v "$(PWD)/code":/workdir \
		$(PROJNAME) bash

.PHONY: clean
clean:
	docker rmi $(PROJNAME)
	docker ps -aq | xargs docker rm

With these two files in a project directory, you can build the container by doing this:

$ make build

This takes a while, since the Dockerfile sets up the Linux OS, then clones the GitHub repository containing the Icarus Verilog source code. It also installs the tools needed to compile this code. There is a Makefile in the project code, so building and installing is done with simple make commands.

When this installation work completes, you have a container on your test machine ready to run. For his experiment, I mount my local project directory into the container. When working in the container, my project code will appear in the /iverilog directory.

I can write the Verilog code on my Mac, or inside the container with this setup.

Working in the Container

Here is the command that puts you in the container (Alpine Linux) where you can work in a Linux environment:

$ make shell
docker run --rm -it \
            --user="501:20" \
            -v "/Users/rblack/_acc/cosc2325/verilog/iverilog/code":/workdir \
            iverilog bash
bash-4.4$ pwd
/iverilog

This command starts the container, and mounts the code directory in the host project directory into the container’s file system. I ran the pwd command to show where we are in the Linux file system. This is a nice feature. Any code created in this directory will exist on the host, even if we destroy this container.

If you look closely at the command used to create this session, there is a --rm option which will remove this container after you exit this “shell” session. Since it only takes seconds to fire one up again, I add this option so my system does not have containers around that I am not currently using. The “image” used to create the container is still around, though.

There is one other command that will throw away the “image” as well. Since the only two files I really need to recreate this test environment are the Makefile and the Dockerfile, those are what I keep in a repository on Github.

Automatic Compiling

There is a neat trick you can do with Docker containers that I use for some projects. There is a Python library called watchdog that will monitor a directory for any changes in files that live there. If it detects a change, it can be configured to run any command you like in that environment.

You can start up a Docker container that launches this Python program, which then sits there watching the directory you specify. On my projects, the directory it is watching (inside Linux) is actually the project directory on my laptop. I set up watchdog so that it runs the C++ compiler, actually, make, whenever a file changes on my host machine in the project directory. Then, I can work on a project code file, and simply save it while sitting in the editor working. A few seconds later, I see the result of running make on my project and I did not type in a single command. I told you I am lazy! This is really cool!

I have a similar setup when I am working on a publication, or maybe even a book that is processed by LaTeX. I have a Docker image with the TeX Live project code loaded that system will build a nice PDF file from my source text files. Again, all I do is edit a file and save it, and in a few seconds, a newly created PDF file appear. I usually have that file loaded in a viewer, so I can see the final results very quick;y as I edit files.

Verilog ALU

Let’s set up a simple ALU unit, and build a test system to check its operation.

Here is an image we can use to visualize this module:

This sketch shows the input and output signals we will model.

The ALU we will create supports only the basic operations needed for our simple 8-bit machine. Here is the Verilog code for this component:

alu.v
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
module alu(clk, opc, in1, in2, enable, res);

    `include "parameters.vh"
    input wire clk, enable;
    input wire [2:0] opc;
    input wire [7:0] in1, in2;
    output reg [7:0] res;

    always @(posedge clk) begin
        if (enable) begin
            case (opc)
                `ADD: res <= in1 + in2;
                `SUB: res <= in1 - in2;
                `AND: res <= in1 & in2;
                `OR:  res <= in1 | in2;
                `XOR: res <= in1 ^ in2;
                `NOT: res <= !in2;
            endcase
        end
    end

endmodule

The definition of the instructions is placed in an “include” file so it can be used in multiple places in a project. Here is that code:

parameters.vh
1
2
3
4
5
6
`define ADD     3'd0
`define SUB     3'd1
`define AND     3'd2
`define OR      3'd3
`define XOR     3'd4
`define NOT     3'd5

This file just assigns a value as a three bit code, for each ALU instruction.

This is really a description of the machine, and how it will work. The description is made up of a list of input and output signals the component will require.

The internal behavior is inside an always block. Even though this component can be constructed as a combinational component, this one is triggered by a clock signal, and does its work on every “rising edge” of that clock signal. (We have been calling that the start of the “tick” phase of the clock.)

Notice how similar this is to the C language. We use a simple “case” statement to lay out what happens, depending on the particular opcode the device sees. Also note that the enable signal determines if the ALU generates an output or now.

This is a very simple way to lay out the digital component we need. The supporting development environment probably included tool that can take this file and generate a real circuit, even one we could send to manufacturing. Studying this code is far easier than staring at an electronic circuit diagram, which is why designing hardware has turned into a software project!

Compiling the Code

Here is a simple Makefile that will compile this code:

Makefile
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
PROG	:= test
SRCS	:= $(wildcard *.v)

# tools
IV		:= iverilog
VVP		:= vvp

.PHONY: all
all:	$(PROG)

$(PROG):	$(SRCS)
	$(IV) -o $@ $^

.PHONY: run
run:	$(PROG)
	$(VVP) $(PROG)

.PHONY: clean
clean:
	rm -f $(PROG)

Note

This code will be processed inside of the container I set up. I usually edit in one window, and have the container running in a second window. Actually, I have iverilog installed on my Mac as well as inside the Linux container, so I can test in either environment.

$ make
iverilog -o test alu.v

This works, and no errors are detected. However, the resulting component cannot be “run” in the traditional sense. That single component cannot operate by itself. We can, however, set up a “test fixture” that will connect to the inputs and outputs of this component and “exercise” it to see how it works.

ALU Test Bench

Verilog projects often provide a “test bench” tool, which is just another Verilog file, only this one will “instantiate” the ALU, and run a set of commands through it, to verify that we see the corrrect output.

Here is a “test bench” that will do this job:

alu_tb.v
 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
module test_alu;

    `include "parameters.vh"

    reg clk;
    wire enable;
    wire [0:2] opc;
    wire [0:7] in1, in2;
    wire [0:7] expected;
    wire [0:7] res;
  
    alu alu1(
        .clk (clk), 
        .opc (opc), 
        .in1 (in1), 
        .in2 (in2), 
        .res (res),
        .enable (enable)
    );

    initial begin
        clk = 0;
    end

    always 
        #5 clk = !clk;
    
    wire [7:0] total = 6;
    reg [7:0] num = 0;
    
    wire [0:2 + 8*3 + 1] data [0:7];
    assign data[0] = { `ADD, 1'b1,  8'd5,  8'd7,  8'd12 };
    assign data[1] = { `SUB, 1'b1,  8'd15, 8'd4,  8'd11 };
    assign data[2] = { `AND, 1'b1,  8'd9,  8'd12, 8'd8 };
    assign data[3] = { `OR,  1'b1,  8'd9,  8'd12, 8'd13 };
    assign data[4] = { `XOR, 1'b1,  8'd9,  8'd12, 8'd5 };
    assign data[5] = { `NOT, 1'b1,  8'd1,  8'd00, 8'd1 };
    
    assign { opc, enable, in1, in2, expected } = data[num];
    
    always @(negedge clk) begin
        if (num < total)
            $display("opc = %d, in1 = %d, in2 = %d, expected = %d, res = %d, ok = %d",
                    opc, in1, in2, expected, res, (expected == res));
        else
            $stop;
        num <= num + 1;
    end

endmodule

And here is a run of the test bench code, which exercises the defined ALU:

$ make run
vvp test
opc = 0, in1 =   5, in2 =   7, expected =  12, res =  12, ok = 1
opc = 1, in1 =  15, in2 =   4, expected =  11, res =  11, ok = 1
opc = 2, in1 =   9, in2 =  12, expected =   8, res =   8, ok = 1
opc = 3, in1 =   9, in2 =  12, expected =  13, res =  13, ok = 1
opc = 4, in1 =   9, in2 =  12, expected =   5, res =   5, ok = 1
opc = 5, in1 =   1, in2 =   0, expected =   1, res =   1, ok = 1
** VVP Stop(0) **

According to this output, the ALU is functioning properly, but this “test” is far from adequate for validating a real design. Stil, this example shows the basic approach to testing.

Testing a Counter

I found another tutorial: Art of Writing Testbenchs that uses a simple counter. This example builds an output display that shows the results visually on an oscilloscope like display.

Here is the counter code:

counter.v
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
module counter (clk, reset, enable, count);
    input clk, reset, enable;
    output [3:0] count;
    reg [3:0] count;                                   

    always @ (posedge clk)
    if (reset == 1'b1) begin
        count <= 0;
    end else if ( enable == 1'b1) begin
        count <= count + 1;
    end

endmodule  

And here is the test bench code:

counter_tb.v
 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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
module counter_tb;

    reg clk, reset, enable;
    wire [3:0] count;
    reg dut_error;

    counter U0 (
        .clk    (clk),
        .reset  (reset),
        .enable (enable),
        .count  (count)
    );

    event reset_enable;
    event terminate_sim;

    initial begin
        $display ("###################################################");
        clk = 0;
        reset = 0;
        enable = 0;
        dut_error = 0;
    end

    always
        #5 clk = !clk;

    initial begin
        $dumpfile ("counter.vcd");
        $dumpvars;
    end


    initial @ (terminate_sim)  begin
        $display ("Terminating simulation");
        if (dut_error == 0) begin
            $display ("Simulation Result : PASSED");
        end
        else begin
            $display ("Simulation Result : FAILED");
        end
        $display ("###################################################");
        #1 $finish;
    end

    event reset_done;

    initial forever begin
        @ (reset_enable);
        @ (negedge clk)
        $display ("Applying reset");
        reset = 1;
        @ (negedge clk)
        reset = 0;
        $display ("Came out of Reset");
        -> reset_done;
    end

    initial begin
        #10 -> reset_enable;
        @ (reset_done);
        @ (negedge clk);
        enable = 1;
        repeat (5)
        begin
            @ (negedge clk);
        end
        enable = 0;
        #5 -> terminate_sim;
    end


    reg [3:0] count_compare;

    always @ (posedge clk)
    if (reset == 1'b1)
        count_compare <= 0;
    else if ( enable == 1'b1)
        count_compare <= count_compare + 1;

    always @ (negedge clk)
    if (count_compare != count) begin
        $display ("DUT ERROR AT TIME%d",$time);
        $display ("Expected value %d, Got Value %d", count_compare, count);
        dut_error = 1;
        #5 -> terminate_sim;
    end

endmodule

Here is the output from this test code:

$ vvp test
###################################################
VCD info: dumpfile counter.vcd opened for output.
Applying reset
Came out of Reset
Terminating simulation
Simulation Result : PASSED
###################################################

Obviously, this output is not very helpful, even though it says everything worked correctly. We need to SEE the results:

Install GTKwave

A standard tool to visualise what is going on in the digital world is GTKwave an open-source tool available for all platforms. Installing it is as simple as this on Linux:

$ sudo apt-get install gtkwave

The display I show here was produced with the same tool, installed on my MacBook.

VCD Files

The data file needed to generate the display we will see is a fairly simple text file listing every signal we decide we want to see, and the “time” when that signal changed value. Since we are simulating things here, the times are set by the test code. The C++ code needed to generate data files in the right format are included in the Appendix (see VCD Trace Files).

Here is the counter test run:

../_images/counter-test.png

Obviously, the times are not very realistic. A simple counter can go really fast, unless the clock is really slow, as it seems to be here.

GTKWave can display many signals, and those signals can span a long run of the simulation. You can scroll through the timeline of the simulation and study what happened in your system.

In this example, the counter does not actually count until the enable signal is set. Once that happens, the count begins and increases properly.

Notice that GTKwave shows the actual value on the output, which is actually an 8-bit bus. The clock, on the other hand, is a simple digital signal alternating from zero to one.

Verilog is a complex language, because it supports building real systems available in the commercial marketplace. This example shows how easy it can be to create something and test it. My goal is to work with this tool and try to get our attiny85sim code into a form that can be dropped onto my BASYS3 FGPA board. The tools needed to get this task done are a bit more complex to install than I want to use in this class, but I may find a way that makes sense to try.

More Information

If you are interested in finding out more about Verilog, here is a tutorial, found on the Internet:

Some searching will turn up a lot of examples of projects using this tool.