SystemVerilog: Connect interface to pure Verilog modules

SystemVerilog interfaces gives us a very good tool to make the code cleaner and less error prone. If you have a group of ports that go together, you can group them into a single interface and reduce huge amount of code.


Thats great. But if you have a pure verilog module and want it to stay that way. How should we connect the interface to the pure Verilog module? Its easy as well. Just create an instance of a module and an instance of the interface. When you instantiate the verilog module use the interface’s members as module’s ports. Now you can drive your module through the interface or connect the module to another SystemVerilog module that accepts the same interface.


Passing and receiving different SystemVerilog datatypes with C/C++ using DPI

SystemVerilog makes interaction with C/C++ a piece of cake. Now, you can directly call C/C++ functions as functions or tasks in SystemVerilog(aka. Importing) and call SystemVerilog functions or tasks from C/C++(aka. Exporting). In this article, we will see how we can pass and receive different SystemVerilog datatypes through DPI.

Return type of imported C/C++ functions

First thing to note is that imported functions can only return single bit types and 2-state int or double types that are not vectors.

This rule rules out all 4-state types except single bit types

  • logic packed arrays, logic unpacked arrays
  • wire, wire packed arrays, wire unpacked arrays
  • reg, reg packed arrays, reg unpacked arrays

and all vector types

  • bit packed arrays, bit unpacked arrays
  • logic packed arrays, logic unpacked arrays
  • wire packed arrays, wire unpacked arrays
  • reg packed arrays, reg unpacked arrays

So what we are left with are fixed width int and double types:

  • bit
  • byte
  • shortint
  • int
  • shortreal
  • real
  • logic

You can pass these types as arguments and receive these types as return from an imported functions. Here is an example of passing and receiving single bit bit and logic.

import "DPI-C" context function bit and_bit(input bit a, b);
import "DPI-C" context function logic and_logic(input logic a, b);
svBit
and_bit(
    svBit a,
    svBit b) {
	return a && b;
}

svLogic
and_logic(
    svLogic a,
    svLogic b) {
	return a && b;
}

You should have noticed two new types on C/C++ side: svBit and svLogic. These are usually 8 bit unsigned char types but only the lsb bit is used.

Passing and receiving packed arrays

Passing and receiving packed arrays is easier than dealing with unpacked arrays. On C side, packed bit vectors are called svBitVecVal and packed logic vectors are called svLogicVecVal. Bit vectors are plain int types. If the vector size exceeds the size that can be stored inside an integer, the bit vectors are passed as array of svBitVecVal.
Logic vectors are little bit more complicated because they represent 4-state values. svLogicVecVal is a structure with two members aval and bval. aval stores the plain bit representation of logic vector while bval stores whether the corresponding bit is ‘0’, ‘1’ or ‘x’, ‘z’. If the vector size exceeds the size that can be stored inside an integer, the logic vectors are passed as array of svLogicVecVal.

[ada]
import “DPI-C” context function void add_bpv(input bit [3:0] a, b, output bit [3:0] c);
import “DPI-C” context function void add_lpv(input logic [3:0] a, b, output logic [3:0] c);
[/ada]

void
add_bpv(
    const svBitVecVal* a,
    const svBitVecVal* b,
    svBitVecVal* c) {
	*c = *a + *b;
}

void
add_lpv(
    const svLogicVecVal* a,
    const svLogicVecVal* b,
    svLogicVecVal* c) {
	c->aval = a->aval + b->aval;
	c->bval = 0;
}

As you can see, we are using output arguments to receive vector types because imported functions cannot return vector types.

Passing and receiving unpacked vectors

Unpacked arrays are sent as arrays of svBit or scLogic to C. There is no way to directly access the integer represented by putting all the individual bits together without calculating it.

import "DPI-C" context function void and_buv(input bit a[3:0], b [3:0], output bit c [3:0]);
import "DPI-C" context function void or_luv(input logic a[3:0], b [3:0], output logic c [3:0]);
void
and_buv(
    const svBit* a,
    const svBit* b,
    svBit* c) {
	for(int i = 0; i < 4; i++) {
		c[i] = a[i] & b[i];
	}
}
void
or_luv(
    const svLogic* a,
    const svLogic* b,
    svLogic* c) {
	for(int i = 0; i < 4; i++) {
		c[i] = a[i] | b[i];
	}
}

Passing and receiving mixed packed and unpacked arrays

As expected, mixed packed and unpacked arrays are represented as arrays of svBitVecVal or svLogicVecVal. If the size of the packed dimension is greater than what can fit inside an integer, mixed packed and unpacked arrays are represented as arrays of arrays of svBitVecVal or svLogicVecVal.

import "DPI-C" context function void add_bpuv(input bit [0:3]a[0:3], b [0:3], output bit [0:3]c [3:0]);
void
add_bpuv(
    const svBitVecVal* a,
    const svBitVecVal* b,
    svBitVecVal* c) {
	for(int i = 0; i < 4; i++) {
		c[i] = a[i] + b[i];
	}
}

Passing and receiving structs

Structs are passed can be passed and received directly. Individual members are representing based on their data type as discussed above.

typedef struct {
shortint mem1;
logic [7:0] mem2;
bit [7:0] mem3;
} struct_t;

import "DPI-C" context function void add_struct(input struct_t a, b, output struct_t c);
void
add_struct(
    const struct_t* a,
    const struct_t* b,
    struct_t* c) {
	c->mem1 = a->mem1 + b->mem1;
	c->mem2[0].aval = a->mem2[0].aval + b->mem2[0].aval;
	c->mem2[0].bval = 0;
	c->mem3[0] = a->mem3[0] + b->mem3[0];
}

Here is the full verilog and c code.


Crack and install Modelsim on linux

Note: Piracy is crime. Please don’t use or encourage pirated software. If you would like to evaluate Modelsim, try the evaluation version. If you still haven’t changed your mind, read on :P.

1) Install the required dependencies

2) Download modelsim 10.1c for linux and its corresponding crack.

3) Mount the downloaded modelsim iso and install modelsim.

mkdir /tmp/modelsim/
mount -o loop path-to-iso /tmp/modelsim
cd /tmp/modelsim
./install.linux

Note:Install modelsim under ~/modelsim/ directory. If you choose another location, please make sure you exchange the default location with your preferred location in the steps below.

4) Execute this script to crack modelsim

Comment the first two lines in the generated license.dat

#SERVER xxxx xxxxxxxxxxxx 27001
#VENDOR mgcld D:FEATURE mgc_s mgcld 2020.00 1-jan-2021 999 0 TS_OK

5) Fix libfreetype problem (Only required if you face this problem)
Try to start vsim

cd ~/modelsim/modeltech/linux_x86_64/
./vsim

If vsim reports the following error, your distro’s freetype library doesn’t play well with modelsim.

** Fatal: Read failure in vlm process (0,0)
Segmentation fault (core dumped)

Use the following script to compile custom freetype

6) Modelsim is ready to use! One last step, add these commands to ~/.bashrc to avoid executing every time you start a new terminal

Linux and Modelsim!!!! Ain’t it fun?

VHDL: std_logic/std_ulogic comparision

VHDL has a lot of things that don’t work as you expect it to work. One of those things are comparison operators.

Comparison of scalar std_ulogic family signals

Comparison of scalar built-in types like std_logic and std_ulogic work as expected.

Comparison of std_ulogic_vector

This is where the shit hits the fan. Your natural intuition would be to try something like this:

This won’t work because VHDL is a strongly types language. You cannot compare between std_ulogic_vector and integer types directly. If you want to compare an integer with a std_ulogic_vector, you have to either convert the integer to std_ulogic_vector or convert std_ulogic_vector to integer before comparing.

Another thing you can do to make this work is to express both of them in std_ulogic_vector.

Modelsim: compile and simulate VHDL from command line

Modelsim has a pretty clumsy and ugly user interface(atleast in linux). Moreover, command line gives more control and makes automation easier. Lets see how we can simulate VHDL project using modelsim command line tools. Before starting, make sure you have modelsim’s bin directory in your PATH. To demonstrate, I will reuse the fileio example. Lets assume you have the above vhdl files in a project directory. In the command line, change to the project directory.

First we have to create a work library:

vlib work

Now, compile the VHDL files:

vcom fileio.vhd gen.vhd

Note: The files should be listed in hierarchical order.

To simulate using GUI:

vsim fileio

Note: vsim takes the name of the top level module to be simulated, not the name of the top level module’s VHDL file.

Adding the -c option starts the simulator in command line interactive mode.

vsim -c fileio

You should be in VSIM’s prompt. From here you can type commands to add signal to the wave, run simulation, write to vcd file, etc.

Show available signals

The VSIM command to list all available signals

show -all

You can also list all available signals in an instance

show fileio
show gen_inst

To unambiguously show signals of an instances down the hierarchy

show /fileio/gen_inst

Adding signals to wave

add wave i_a

You can also add signals of an instance down the hierarchy

add wave /fileio/gen_inst/a_i

Run the simulation

To run the complete simulation

run -all

To run for a specified time

run <time>
run 100ps

Follow this blog post to generate VCD waveform from command line.

Automate simulation

The best thing is you don’t have to type these commands every time you launch vsim. You can automate the process by specifying the .do in the vsim command. The .do should contain the list of commands you want to execute to run the simulation.

Here is the updated command to lauch fileio example:

vsim -c -do fileio.do fileio

Modelsim: Generate vcd waveform

You can create vcd waveform using Modelsim and view it later using gtkwave.

This command specifies the name of the vcd file to dump the waveform into:

vcd file <vcd-filename>

Now that we have specified the vcd filename, we have to specify the signals which should be dumped into the vcd file. Lets find what signals are available in the project:

show -all

To add signal to vcd output:

vcd add <path_to_instance>/<signal-name>

You can now run the simulation and check in your working directory for the vcd file. You can open the vcd file using gtkwave.

To demonstrate, I will reuse the fileio example. These commands will create vcd file for fileio example:

vcd file fileio.vcd
vcd add *

To open the generated vcd file using gtkwave

gtkwave fileio.vcd

VHDL: File i/o as testbench stimulus

It is not always practical to hard code the test bench stimulus in VHDL code. This article introduces a way to read the stimulus stored in a text file and feed the DUT.

First, lets look at the DUT. It is a simple 4 bit adder.

Now lets create the test bench for gen.

The process input_gen reads the input from the input file “in.txt” and the process output_gen writes the output to output file “out.txt”.

The file variable inp_file opens the file “in.txt” in read mode and the file variable out_file opens the file “out.txt” in write mode. Both “in.txt” and “out.txt” are stored as text file.

Each line in “in.txt” has one stimulus for all signals as integers. In this example, we have only two signals i_a and i_b. This is the syntax of one line in “in.txt”:

{{i_a stimulus}} {{i_bstimulus}}

Here is an example “in.txt”:

One line is read from “in.txt” every clock cycle using readline function into inline variable. The stimulus for each variable are read serially from inline using read function.

Quartus: Launch test bench in modelsim

To use modelsim as the default simulator in quartus, please follow this blog post.

Step 1: Open settings dialog box by clicking Assignment->Settings menu or by using the keyboard shortcut Ctrl+Shift+E. The settings dialog box should appear.

Step 2: Select EDA Tool Settings->Simulation option in the sidebar.

Step 3: Enable test bench by selecting the option Compile test bench.

test_bench_enable

Step 4: Click Test Benches… button to create test benches. Test benches dialog should appear.

Step 5: Click New button to create new test bench. New Test Bench Settings dialog should appear.

test_bench_dialog

Step 6: In the New Test Bench Settings dialog, provide the name of your test bench using Test bench name.

Step 7: Add all the necessary test bench and implementation files by clicking File name ellipsis button.

new_tb_dialog

Step 8: Click Ok to create new test bench. Test Benches dialog should now list the test bench you created.

Step 9: Click Ok. Click Apply in Settings dialog box.

Step 10: To launch ModelSim simulator, you have to first Compile your design by clicking the Start compilation icon or by using the Ctrl+L shortcut.

Step 11: After the compilation is successfully completed, launch simulation by clicking RTL Simulation icon.

Quartus: set simulator for already existing project

In this post, I will show you how to change simulation tool for an existing project.

Changing simulation tool

Step 1: Open settings dialog box by clicking Assignment->Settings menu or by using the keyboard shortcut Ctrl+Shift+E. The settings dialog box should appear.

Step 2: Select EDA Tool Settings->Simulation option in the sidebar.

Simulation settings
Step 3: In the simulation settings page, select the required simulation tool using Tool name drop down box.

Setting simulation tool path

Now that you have instructed Quartus to use the simulation tool you wanted, you should also inform Quartus where it can find this tool.

Step 1: Open options dialog box by clicking Tools->Options menu. Options dialog box should open

Step 2: In the option dialog box, select General->EDA Tool options in the sidebar.

Step 3: in the EDA Tool options page, set the path to your desired tool.

EDA Tool options

modelling memory using systemC RTL

Let us design a memory module using systemC similar to this diagram,

Ram blackbox diagram

This is the declaration of our memory module class. The capacity of this model is 256 words. You can change it to your needs.
rammem.h

#ifndef MEMORY_H_
#define MEMORY_H_
#include

SC_MODULE(memory)
{
	sc_inout<sc_lv >  data;
	sc_in<sc_lv >     address;
	sc_in             rw;
	sc_in             enable;

	sc_signal<sc_uint >        ramdata[256];

	void entry();

	void memdump()
	{
		FILE *fp = fopen("data/memdump","w");
		int size;
		for (size = 0; size < 255; size++) {
			fprintf(fp, "0x%xn", ramdata[size].read().to_int());
		}
	}

	SC_CTOR(memory)
	{
		SC_METHOD(entry);
		sensitive << enable << rw << address;

		FILE *fp = fopen("data/ram_init","r");
		if(!fp)
		{
			cout << "error. cannot find ram_init." << endl;
		}
		int size=0;
		int mem_word;
		for (size = 0; size < 255; size++) {
			ramdata[size].write( 0x0 );
		}
		size = 0;
		while (fscanf(fp,"%xn", &mem_word) != EOF) {
			ramdata[size].write( mem_word );
			size++;
		}
	}
};

#endif /* MEMORY_H_ */

Here are the member function defections,
rammem.cpp

#include
#include

void memory::entry()
{
	if(enable.read() == 1) {
		cout << "@ " << sc_time_stamp() << endl;
		if(rw.read() == 1) {			//write
			ramdata[address.read().to_int()].write( data.read() );
			cout << "writing data " << data.read() << " at " << address.read() << endl;
		} else {						//read
			data.write(ramdata[address.read().to_int()].read());
			cout << "reading data " << data.read() << " at " << address.read() << endl;
		}
	} else {
		data.write("zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz");
	}
}

The test bench to test our memory module,
main.cpp

#include
#include

int sc_main(int argc, char* argv[]) {
	memory mem("memory");
	sc_set_time_resolution(1, SC_PS);

	sc_trace_file *wf = sc_create_vcd_trace_file("data/memory");
	sc_trace(wf, mem.address, "address");
	sc_trace(wf, mem.data, "data");
	sc_trace(wf, mem.enable, "enable");
	sc_trace(wf, mem.rw, "rw");

	sc_signal   enable, rw;
	sc_signal > data;
	sc_signal<sc_lv > address;

	mem.rw(rw);
	mem.data(data);
	mem.address(address);
	mem.enable(enable);

	enable  = 0;
	data    = 0;
	address = 0;
	rw      = 0;
	sc_start(0, SC_PS);

	rw      = 1;
	address.write(0x10);
	data.write(0x110011);
	enable  = 1;
	sc_start(5, SC_PS);

	enable  = 0;
	sc_start(5, SC_PS);

	address.write(0x12);
	data.write(0x1100);
	enable  = 1;
	sc_start(5, SC_PS);

	address.write(0x8);
	data.write(0x1010);
	sc_start(5, SC_PS);

	rw      = 0;
	address = 0x10;
	sc_start(5, SC_PS);

	address = 0x1;
	sc_start(5, SC_PS);

	address = 0x3;
	sc_start(5, SC_PS);

	enable  = 0;
	sc_start(1, SC_PS);

	mem.memdump();
	sc_close_vcd_trace_file(wf);

	return(0);
}

Here is the output waveform,

Ram memory output

In this post, we have seen how to design ram models in systemC using RTL style. In our next port, let us model memory using systemC TLM.