Enabling pthread in your C++ Verilator Testbench

Form multiple reasons you could need a verilator simulation using multi-threading in C++. This article is a simple tutorial on how to create a simple multi-threaded ready project using Verilator. In this project i gonna use Ubuntu 18.04.

Download and build latest verilator version

First of all you should build the latest version from veripool git repository, once the distro package won’t supply the latest stable version. In Ubuntu 18.04, the package manager states that the current supplied version is 3.916. The latest stable version at this moment available in the Veripool repository is the 4.012 version. Following the instructions available at Veripool website 1, I’ve installed the dependencies by issuing following command:

sudo apt-get install git make autoconf g++ flex bison libfl-dev

After this, I’ve cloned, built and installed verilator stable in the machine.

git clone http://git.veripool.org/git/verilator
cd verilator
git checkout stable
autoconf
./configure
make
sudo make install

If the process completed correctly, you would be able to use verilator tool.

Creating the project

The project folder structure should be the following:

project_dir/
├── src/
|   └── my_top_module.v
├── test/
|   └── my_top_module_testbench.cpp
├── build/
└── Makefile

To this we should create this structure by populating the structure issuing following commands:

mkdir -p project_dir/{src,test,build}
touch src/my_top_module.v
touch test/my_top_module_testbench.cpp
touch Makefile

The project Makefile should be as follows:

mkfile_path	:= $(abspath $(lastword $(MAKEFILE_LIST)))
mkfile_dir	:= $(dir $(mkfile_path))
top_module := my_top_module
cflags := '-std=c++1y'
ldflags := '-lpthread'

.PHONY: prebuild build test clean

all: build

test:
	build/V$(top_module)

build: prebuild
	+$(MAKE) -C build -f V$(top_module).mk V$(top_module)

prebuild:
	verilator -CFLAGS $(cflags) -LDFLAGS $(ldflags) --Mdir $(mkfile_dir)/build --cc -Wall --top-module $(top_module) -I$(mkfile_dir)/src $(mkfile_dir)/src/$(top_module).v  --exe $(mkfile_dir)/test/$(top_module)_testbench.cpp

clean:
	rm -rf $(mkfile_dir)/build

A dummy example of a verilog module will be used to demostrate using c++ multithreaded testbench my_top_module.v:

module my_top_module(input clk, input rst);

reg [7:0] counter;

always@(posedge clk)
begin
    if (rst == 0)
    begin
        counter <= 0;
    end
	else if (counter <= 10)
    begin
		counter <= counter + 1;
		$display("counter: %d", counter);
    end
	else
    begin
		$finish;
	end
end

endmodule

After this, populate the C++ testbench code with a multi-threaded testbench code.

#include <iostream>
#include <chrono>
#include <thread>
#include <functional>
#include <atomic>
#include <mutex>
#include <condition_variable>

#include "Vmy_top_module.h"
#include "verilated.h"

std::atomic_uint64_t seconds;
std::mutex second_sync;

#include <mutex>
#include <condition_variable>

class Semaphore
{

  public:
    Semaphore(int count_ = 0)
        : count(count_)
    {
    }

    inline void notify(/* int tid */)
    {
        std::unique_lock<std::mutex> lock(mtx);
        count++;
        cv.notify_one();
    }

    inline void wait(/*int tid*/)
    {
        std::unique_lock<std::mutex> lock(mtx);
        while (count == 0)
        {
            cv.wait(lock);
        }
        count--;
    }

    inline void checkNotify(std::function<void(void)> callback)
    {

        std::unique_lock<std::mutex> lock(mtx);

        if (count > 0)
        {
            callback();
            count--;
        }
    }

  private:
    std::mutex mtx;
    std::condition_variable cv;
    int count;
};

Semaphore s;
Semaphore endOfSimulationSemaphore;

void tmr(void)
{
    bool endOfSimulation = false;
    while (!endOfSimulation)
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
        seconds++;
        s.notify();
        endOfSimulationSemaphore.checkNotify([&]() { endOfSimulation = true; });
    }
}

void ml(void)
{

    Vmy_top_module *top = new Vmy_top_module;

    while (!Verilated::gotFinish())
    {
        s.wait();
        if (seconds.load() == 0)
        {
            top->clk = 0;
            top->rst = 0;
        }
        else
        {
            top->clk = ~top->clk;
            top->rst = 1;
        }

        top->eval();
    }

    endOfSimulationSemaphore.notify();

    delete top;
}

int main(int argc, char **argv, char **env)
{

    Verilated::commandArgs(argc, argv);

    auto tmr_thread = std::thread(tmr);
    auto ml_thread = std::thread(ml);

    tmr_thread.join();
    ml_thread.join();

    return 0;
}

Compiling the project

After completing the steps above you gonna be able to build and test your code by issuing following commands:

make
make test

at the end this would be

comments powered by Disqus