Home Verilog Development on Apple Silicon Macs (M1/M2/M3) - A Step-by-Step Guide to Setting up Verilator, SystemC and GTKWave on macOS
Post
Cancel

Verilog Development on Apple Silicon Macs (M1/M2/M3) - A Step-by-Step Guide to Setting up Verilator, SystemC and GTKWave on macOS

Hardware development often depends on proprietary EDA software coming from Synopsys or Cadence, however, this software is only available on Linux and Windows with license fees unattainable for a student or hobbyist. The open-source tool Verilator allows everyone to simulate Verilog code and interface it with C++. Setting up Verilator on an Intel Mac was pretty straightforward, which is sadly not the case for Apple Silicon Macs. This article is a step-by-step guide on how to set up Verilator and GTKWave on Apple Silicon Macs (M1/M2/M3).

Tool Setup

The first step is to install Apple’s command line utilities, which contain important tools such as git and a compiler. This is done by the following command:

1
xcode-select --install

Next up is installing third-party command line tools via Homebrew. If you already installed Homebrew you can skip the following two steps. If you haven’t installed brew yet than run the following command:

1
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

When brew is installed, it needs to be set up for your terminal environment in the ~/.zshrc file. Open or create the ~/.zshrc file (~ means it is in your home directory) and add the following lines:

1
2
3
# homebrew
export PATH="/opt/homebrew/bin:${PATH}"
eval "$(/opt/homebrew/bin/brew shellenv)"

This makes sure brew is set up every time you open a new terminal window. Now close and reopen your terminal to be able to use brew.

With brew setup, it is time to install cmake and ninja which are used to compile Verilator and SystemC from scratch. Use the brew install command to install those:

1
brew install cmake ninja wget git

The open-source tool GTKWave is also installable through brew but for Apple Silicon Macs a slightly different version has to be installed using the following command:

1
brew install --HEAD randomplum/gtkwave/gtkwave

Sadly, the next two command line tools have to be compiled from scratch because Apple’s version of those don’t play nicely with Verilator. The first is bison (GNU Bison) which is a parser generator that is needed to parse Verilog files by Verilator. Execute the following commands to build bison from scratch and install it under /opt/bison/bison-3.7.91:

1
2
3
4
5
6
7
sudo mkdir -p /opt/bison/bison-3.7.91
wget http://alpha.gnu.org/gnu/bison/bison-3.7.91.tar.xz
tar -xvf bison-3.7.91.tar.xz
cd bison-3.7.91
./configure --prefix=/opt/bison/bison-3.7.91 CFLAGS="-isysroot $(xcrun -show-sdk-path)" CXXFLAGS="-isysroot $(xcrun -show-sdk-path)"
make
sudo make install

After bison was successfully installed, the following has to be added to your ~/.zshrc:

1
2
3
4
5
# bison
export CFLAGS="-I/opt/bison/bison-3.7.91/share/include ${CFLAGS}"
export CXXFLAGS="-I/opt/bison/bison-3.7.91/share/include ${CXXFLAGS}"
export LDFLAGS="-L/opt/bison/bison-3.7.91/lib ${LDFLAGS}"
export PATH="/opt/bison/bison-3.7.91/bin:${PATH}"

The second tool which needs to be built from scratch is flex (Flex) which is a lexical analyzer generator which is also needed by Verilator. Run the following commands to build flex and install it under /opt/flex/flex-2.6.4

1
2
3
4
5
6
7
sudo mkdir -p /opt/flex/flex-2.6.4
wget https://github.com/westes/flex/releases/download/v2.6.4/flex-2.6.4.tar.gz
tar -xvf flex-2.6.4.tar.gz
cd flex-2.6.4
./configure --prefix=/opt/flex/flex-2.6.4 CFLAGS="-isysroot $(xcrun -show-sdk-path)" CXXFLAGS="-isysroot $(xcrun -show-sdk-path)"
make
sudo make install

After the success installation of flex its paths also have to be added to your ~/.zshrc:

1
2
3
4
5
# flex
export CFLAGS="-I/opt/flex/flex-2.6.4/include ${CFLAGS}"
export CXXFLAGS="-I/opt/flex/flex-2.6.4/include ${CXXFLAGS}"
export LDFLAGS="-L/opt/flex/flex-2.6.4/lib ${LDFLAGS}"
export PATH="/opt/flex/flex-2.6.4/bin:${PATH}"

The next tool you need for Verilator is SystemC which is a set of C++ classes and macros for event-driven simulation mostly used for hardware system-level modeling. First you need to set $SYSTEMC_HOME environment variable which points to the installation location of SystemC. Add the following to your ~/.zshrc and make sure to restart your terminal afterward or run source ~/.zshrc:

1
2
# systemc
export SYSTEMC_HOME="[PATH_TO_SYSTEMC]"

To download SystemC from GitHub, compile and install it execute the following commands:

1
2
3
4
5
6
7
8
git clone https://github.com/accellera-official/systemc $SYSTEMC_HOME
cd $SYSTEMC_HOME
git checkout 2.3.3
mkdir build
cd build
cmake -GNinja -DENABLE_PTHREADS=ON -DENABLE_PHASE_CALLBACKS_TRACING=OFF -DCMAKE_CXX_STANDARD=17 ..
ninja
ninja install

After installing bison, flex, and SystemC it is finally time to install Verilator. As a first step, the path to the Verilator repository needs to be added to your ~/.zshrc and make sure to restart your terminal afterward or run source ~/.zshrc:

1
export VERILATOR_ROOT="[PATH_TO_VERILATOR]"

Then, clone Verilator from GitHub, checkout the correct version, and compile it using autoconf and make:

1
2
3
4
5
6
git clone https://github.com/verilator/verilator $VERILATOR_ROOT
cd $VERILATOR_ROOT
git checkout v4.224
autoconf
./configure
make

Lastly, the Verilator bin and include directories must be added to your PATH variable to be found by other tools:

1
2
export PATH="$VERILATOR_ROOT/bin:$PATH"
export VERILATOR_INC_DIR="$VERILATOR_ROOT/include"

Restart your terminal or use source ~/.zshrc such that the changes take effect.

This concludes installing Verilator using bison, flex, and SystemC.

Verilog Test Module

To check if everything works correctly, create a new directory at an arbitrary location:

1
2
mkdir [PATH_TO_TEST_PROJECT]
cd [PATH_TO_TEST_PROJECT]

In this directory, create a Verilog file with the name Buffer.v with the following contents:

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
module Buffer #(
    parameter DATA_WIDTH = 32
)
(
    input clk_i,
    input reset_n_i,

    input[DATA_WIDTH-1:0] data_i,

    output[DATA_WIDTH-1:0] data_o
);

    reg[DATA_WIDTH-1:0] data;

    assign data_o = data;

    always @(posedge clk_i, negedge reset_n_i) begin
        if(reset_n_i == 1'b0) begin
            data <= {DATA_WIDTH{1'b0}};
        end
        else begin
            data <= data_i;
        end
    end
endmodule

This simple Verilog module Buffer delays an input of bit width DATA_WIDTH (default 32) by one clock cycle. This file is also available on GitHub

SystemC Test Bench

As a good design practice, each Verilog module should have its own test bench to ensure the correct functionality. However, rather than writing a test bench in Verilog, as it is customary in other simulation frameworks, the test bench is written in C++ using SystemC. In the same directory as Buffer.v, create a file with the name Buffer_tb.cc and paste the following code into it:

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
#include <systemc.h>
#include <verilated.h>
#include <verilated_vcd_sc.h>

#include "VBuffer.h"

#include <iostream>

int sc_main(int argc, char** argv) {
    Verilated::commandArgs(argc, argv);
    Verilated::traceEverOn(true);

    // get vcd file path from command line arguments
    std::string vcd_file_path;

    if(argc == 2) {
        vcd_file_path = std::string(argv[1]);
    }

    // signals
    sc_clock clk_i{"clk", 1, SC_NS, 0.5, 0, SC_NS, true};
    sc_signal<bool> reset_n_i;

    // input
    sc_signal<uint32_t> data_i;

    // output
    sc_signal<uint32_t> data_o;

    const std::unique_ptr<VBuffer> buffer{new VBuffer{"buffer"}};

    buffer->clk_i(clk_i);
    buffer->reset_n_i(reset_n_i);

    buffer->data_i(data_i);
    buffer->data_o(data_o);

    // start simulation and trace
    std::cout << "VBuffer start!" << std::endl;

    sc_start(0, SC_NS);

    VerilatedVcdSc* trace = new VerilatedVcdSc();
    buffer->trace(trace, 99);

    if(vcd_file_path.empty()) {
        trace->open("VBuffer_tb.vcd");
    } else {
        trace->open(vcd_file_path.c_str());
    }

    // reset
    sc_start(1, SC_NS);
    reset_n_i.write(0);
    sc_start(1, SC_NS);
    reset_n_i.write(1);
    sc_start(1, SC_NS);

    assert(data_o.read() == 0);

    sc_start(1, SC_NS);

    data_i.write(42);
    sc_start(1, SC_NS);

    data_i.write(0);
    sc_start(1, SC_NS);

    // check if output is 42
    assert(data_o.read() == 42);
    sc_start(1, SC_NS);

    // check if output is 0
    assert(data_o.read() == 0);
    sc_start(10, SC_NS);

    buffer->final();

    trace->flush();
    trace->close();

    delete trace;

    std::cout << "VBuffer done!" << std::endl;
    return 0;
}

(The file above is also available on GitHub.)

For someone who has never used C++ before the above code might look a bit intimidating but is actually easy to understand. The first few lines above sc_main tell the C++ compiler where to find the simulation libraries for SystemC and Buffer module while iostream gives us the ability to write something onto the command line.

sc_main is the main method of SystmC which is always called first when running a SystemC program. The two lines following sc_main pass possible command line arguments to the SystemC kernel and enable the tracing of signals, which is critical for debugging using *.vcd files in GTKWave. Under the comments // singals, // input, and // output are the signal declaration that interface the Verilog module. Enclosed in <,> are the data types of the signals which have to match the width of the Verilog signals in Buffer.v.

Afterward, in Line 30 the VBuffer module is instantiated which is the verilated Buffer module. Verilated means the formerly described Verilog module was translated into C++ code by Verilator, how to do this will be explained when compiling everything later on.

The variable buffer is the C++ object representing the Buffer which was instantiated above. To connect the declared SystemC signals with the Buffer the functions with the names of Verilog signals are called and the matching SystemC signals are passed as shown in line 32 to 36.

Next up is a C++ print out which indicates that the simulation starts followed by the actual initialization of the simulation with sc_start(0, SC_NS), the 0 indicates that the simulation should be started but no simulation time should be accumulated. To be able to get a *.vcd trace file a trace has to be instantiated and initialised in lines 43 and 44. In lines 46 to 50 it is checked if a path to a trace file was passed to the SystemC program, if not a default *.vcd file name is set.

Lines 53 to 75 contain the actual test code for the Buffer Verilog module. Using sc_start(x, SC_NS) runs the simulation for x nanoseconds and the .read and .write calls on the SystemC signals either set the signal to the desired value or return the current value of the signal. assert checks if the signals have the intended value.

After all tests have been carried out successfully, buffer->final() is called, which makes sure to gracefully end the simulation of the buffer object. Then the trace is flushed, which means that all contents of the trace are written into the *.vcd file, and closed. Because the trace object was dynamically allocated it needs to be deleted manually using delete.

Line 84 prints the that the simulation has finished, and line 85 ends the SystemC program with return code 0.

Verilation and Compilation

To actually run the Buffer in the SystemC test bench, it needs to be verilated using Verilator and then compiled together with the SystemC test bench code. It is possible to do this only in the command line. However, the commands are quite long such that using a tool which generates those commands and a compilation script is highly recommended. This is where cmake and ninja come into play. cmake is a tool for generating script to compile C++ code while ninja is the tool for which the compilation scripts are generated. If you are familiar with Makefiles than cmake and ninja are Makefiles on steroids.

In the directory where the Buffer.v and Buffer_tb.cc files have been created, create a new file with the name CMakeLists.txt with the following content:

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
cmake_minimum_required(VERSION 3.16)
project(test CXX)

# Find Verilator
find_package(verilator HINTS ${VERILATOR_ROOT} $ENV{VERILATOR_ROOT})

# SystemC dependencies
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)

# Find SystemC using SystemC's CMake integration
find_package(SystemCLanguage PATHS ${SYSTEMC_HOME})

add_executable(VBuffer_tb Buffer_tb.cc)

set_property(
	TARGET VBuffer_tb
	PROPERTY CXX_STANDARD ${SystemC_CXX_STANDARD}
)

verilate(VBuffer_tb SYSTEMC COVERAGE TRACE
	INCLUDE_DIRECTORIES "."
	SOURCES Buffer.v
)

verilator_link_systemc(VBuffer_tb)

(The file above is also available on GitHub.)

The first line of CMakeLists.txt states which version of cmake is the minimum required one. In line 2 the name of the project (test) and programming language of the project is set where CXX stands for C++.

cmake supports plugins which extend the standard cmake with custom commands and instructions on how to compile specific projects. In line 5 it is checked if the Verilator cmake plugin can be found in the directory VERILATOR_ROOT. If the Verilator plugin cannot be found, cmake is terminated with an error. In lines 8 and 12 cmake is instructed to find the SystemC dependencies and the SystemC plugin.

When all plugins have been found, an executable is added on line 14, which is the SystemC test bench program containing the Buffer module. The name of the executable is VBuffer_tb which has the file Buffer_tb.cc as input. Afterward, in lines 16 to 19 it is stated that the executable VBuffer_tb should be compiled with the C++ standard in which SystemC was compiled (in this tutorial C++ standard 17 was set for SystemC).

To turn Verilog code into C++ code the verilate command in lines 21 to 24 is used which states that the generated C++ should be SystemC compatible, should support traces and the C++ code should be generated from the Verilog file Buffer.v.

In the last line it is stated that the executable VBuffer_tb should be linked against SystemC which is necessary to run the simulation.

Now after setting up a CMakeLists.txt file, it is finally time to compile and run the test bench for the Verilog Buffer module. Because cmake generates numerous files, especially when projects get bigger, it is always recommended to create a dedicated build directory for all build artifacts. To do so, create a new directory named build inside the directory which contains all files that have been created so far and use cd to switch into the newly created build directory:

1
2
mkdir build
cd build

Inside the build directory, execute the cmake command with -GNinja option and pointing to the source directory .. (which is the parent directory). -GNinja tells cmake to generate compile scripts using ninja instead of Makefiles. Makefiles are also a valid option. However, the build is much faster using ninja

1
cmake -GNinja ..

When the cmake command ran successfully,use ninja to verilate and compile the Buffer module and the SystemC test bench:

1
ninja

Now the test bench can be executed using the following command, notice that this is the name of the executable from the CMakeLists.txt:

1
./VBuffer_tb

The output from the test bench should be the following:

1
2
3
4
5
        SystemC 2.3.3-Accellera --- Dec 29 2023 09:42:53
        Copyright (c) 1996-2018 by all Contributors,
        ALL RIGHTS RESERVED
VBuffer start!
VBuffer done!

Because the SystemC test bench recorded a trace in the form of a *.vcd file, there should be a VBuffer_tb.vcd inside the build directory. This *.vcd can be viewed using GTKWave which has been installed using brew earlier. To start GTKWave type the following command into a new terminal window:

1
gtkwave

When GTKWave is open, go to Menu->File->Open New Tab ([CMD]+[T]) and navigate to the build directory and open the VBuffer_tb.vcd file. On the top left, you can find the Verilog module hierarchy. Select Buffer and the traced signals will appear on the bottom left. Select all signals and press Append to see the wave form:

GTKWave Screenshot Apple Silicon

The wave form shows the change of all signals over time. It helps to validate that the Buffer module works correctly because the output signal data_o is equal to the input signal data_i delayed by one clock cycle.

This concludes this tutorial on how to develop hardware using Verilog on Apple Silicon Macs. You can find the code for Buffer.v, Buffer_tb.cc, and CMakeLists.txt on GitHub under the following link: https://github.com/k0nze/verilator_systemc_template

This post is licensed under CC BY 4.0 by the author.