Overview

Loosely speaking, SystemC allows a user to write a set of C++ functions (processes) that are executed under control of a scheduler in an order that mimics the pasasge of simulated time and that are synchronized and communicate in a way that is useful for modeling electronic systems containing hardware and embedded software.

The module is the basic buidling block of a system. A module can contain the following:

  • Ports
  • Exports
  • Channels
  • Processes
  • Events
  • Instances of other modules
  • Other data members and functions

They are all implemented as C++ classes. The base class of them is sc_object.

Concepts

Scheduler

At the core of SystemC is a simulation engine containing a process scheduler. Processes are executed in response to the notification of events. Events are notified at specific points in simulated time.

Elaboration and simulation

The execution of SystemC application consists of elaboration and simulation.
During elaboration, the module hierarchy is created, followed by simulation, during which the scheduler runs.

Kernel

The kernel is the part of SystemC class library implementation that provides the core functionality for elaboration and the scheduler.

Processes (threads)

Processes are C++ functions registered with kernel. Processes are used to perform computations and hence to model the functionality of a system. They can be created statically during elaboration or dynamically during simulation.

Sensitivity

The sensitivity of a process identifies the set of events that would cause the scheduler to execute that process should those events be notified. Both static and dynamic sensitivity are provided. Static sensitivity is created at the time the process instance is created. Dynamic sensitivity is created during the execution of the process’s associated function.

Channels and interfaces

Interfaces are abstract classes that declare a set of pure virtual functions. These virtual functions are implemented by Channel. In other words, a channel is said to implement an interface if it defines all of the methods declared in that interface.
Channels serve to encapsulate the mechanism through which processes communicate and hence to model the communication aspects or protocols of a system. It can be used for inter-module communication or inter-process communication within a module.
Since the methods of a interface are virtual functions, they are typically called through an interface. A channel may implement more than one interface, and a single interface may be implemented by more than one channel.

Ports and exports

A port specifies that a particular interface is required by a module, whereas an export spefices that a particular interface is provided by a module.
Ports and exports forward method calls from the processes within a module to channels, to which those ports and exports are bound. Ports can only forward method calls up or out of a module, whereas exports can only forward method calls down or into a module.

Tutorial

Introduction

Combinational example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# include <systemc.h>
SC_MODULE ( and ) {
sc_in<DT> a;
sc_in<DT> b;
sc_out<DT> f;

void func() {
f.write(a.read() & b.read())
}

SC_CTOR( and ) {
// turn the function into a thread
SC_METHOD( func );
// specify sensitivity
sensitive << a << b ;
}
}

DT is a placeholder, such as sc_int<4>, sc_uint<4>

Sequential example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# include <systemc.h>
SC_MODULE ( and ) {
sc_in<DT> a;
sc_in<DT> b;
sc_in<bool> clk;
sc_out<DT> f;

void func() {
f.write(a.read() & b.read())
}

SC_CTOR( and ) {
// turn the function into a thread
SC_METHOD( func );
// sensitive to positive edge of the clock
sensitive << clk.pos();
// sensitive << clk.neg(); for negative edge
}

SC_CTOR

  • The module constructor
  • Put the name of the module in the paranthesis
  • Inside {} we need to construct the processes and sensitivity list

Port I/O

  • SystemC uses functions to read from sc_in<> or write to sc_out<>
    • .read()
    • .write()
  • Examples
    • x = inp.read();
    • outp.write(val);

Processes (thread)

  • A thread is a function made to act like a hardware process
    • Runs concurrently
    • Sensitive to signals, clock edges, or fixed amounts of simulation time
    • Not called by the user, always active
  • SystemC supports 3 kinds of threads
    • SC_METHOD()
      • Executes once every sensitivity event
      • Runs continuously
      • Analogous to a Verilog @always block
      • Synthesizable
      • Useful for combinational expressions or simple sequential logic that finishes in 1 clock cycle
    • SC_THREAD()
      • Runs only once at start of simulation them suspends itself when done
      • Can contain an infinite loop to execute code at fixed rate of time
      • NOT Synthesizable
      • Useful in testbenches to describe clocks or initial startup signal sequences
    • SC_CTHREAD()
      • C means “clocked”
      • Runs continuously
      • References a clock edge
      • Synthesizable
      • Can take one or more clock cycles to execute a single iteration
      • Used in 99% of all high-level behavioral designs

SC_CTHREAD Clocked Threads

  • SC_THREAD() only support one-cycle operations, not much difference with RTL code
  • SC_CTHREAD()is not limited to one cycle
    • Can contain continuous loops
    • Can contain large blocks of code with operations of controls
    • Good for behavior description

Let’s look at a FIR example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <systemc.h>
SC_MODULE( fir ) {
sc_in<bool> clk;
sc_in<bool> rst;
sc_in< sc_int<16> > inp;
sc_out< sc_int<16> > outp;

void fir_main(); // just the declaration

SC_CTOR( fir ) {
SC_CTHREAD( fir_main, clk.pos() ); // second arg is sensitivity
reset_signal_is(rst, true); // reset signal, true means reset is asserted high
}
};

This is how to define compute with reset:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Coefficients for each FIR
const sc_uint<8> coef[5] = {
18, 77, 108, 28, 21, 34
}

// FIR Main thread
void fir::fir_main() {
// Reset code
// Reset internal variables
// Reset outputs
wait();

while ( true ) {
// Read inputs
// Algorithm code
// Write outputs
wait();
}
}
  • wait() means wait for one clock cycle
  • Everything from the beginning to the first wait() statement will become a reset state in the generated RTL
  • At each clock cycle the simulator checks if the reset is triggered. If it is, the process restarts from the beginning.

Let’s write the FIR filter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void fir::fir_main(void) {
// Reset
outp.write(0);
wait();
while(true) {
for (int i = 5-1; i > 0; i--) {
taps[i] = taps[i-1];
}
taps[0] = inp.read();

sc_int<16> val;
for ( int i = 0; i <5 ; i++) {
val += coef[i] * taps[i];
}
outp.write(val);
wait();
}
}

Testbench

Test environment:

- SYSTEM
  - testbench module
  - design module

Top level system module looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
SC_MODULE( SYSTEM ) {
// Module declarations

// Local signal declarations

SC_CTOR(SYSTEM) {
// Module instance signal connections
}

~SYSTEM()
// Destructor
};

Now we code the actual top level program:

main.cc:

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
#include<systemc.h>
#include "fir.h"
#include "tb.h"

SC_MODULE(SYSTEM) {
// note that we are declaring pointers
tb *tb0;
fir *fir0;

// declare signals, note that clock is special
sc_signal<bool> rst_sig;
sc_signal< sc_int<16> > inp_sig;
sc_signal< sc_int<16> > outp_sig;
sc_clock clk_sig;

// constructor to tie them together
SC_CTOR(SYSTEM)
// first thing we do is parameterize clock signal
: clk_sig ("clk_sig", 10, SC_NS) // this is a copy constructor
// "clk_sig" is the name of the clock
// 10 is the clock period, SC_NS is nano second, the unit of clock
{
// to instantiate modules we use "new"
tb0 = new tb("tb0");
tb0->clk(clk_sig);
tb0->rst(rst_sig);
tb0->inp(inp_sig);
tb0->outp(outp_sig);

fir0 = new fir("fir0");
fir0->clk(clk_sig); // thus the signals are connected
fir0->rst(rst_sig);
fir0->inp(inp_sig);
fir0->outp(outp_sig);
}

// destructor
~SYSTEM() {
// memory housekeeping
delete tb0;
delete fir0;
}
}

SYSTEM *top = NULL;
int src_main(int argc, char* argv[]) // our good'ol arg count and arg vector
{
top = new SYSTEM("top");
sc_start();
return 0;
}

Now we write testbench tb.h:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <systemc.h>
SC_MODULE(tb) {
// note that the directions are inverted
sc_in<bool> clk;
sc_out<bool> rst;
sc_out< sc_int<16> > inp;
sc_in< sc_int<16> > outp;

// source produces the out signal
// sink receives in signals
void source();
void sink();

SC_CTOR(tb) {
SC_CTHREAD(source, clk.pos());
SC_CTHREAD(sink, clk.pos());
}
}

tb.cc
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
#include "tb.h"
void tb::source() {
// Reset
inp.write(0);
rst.write(1);
wait();
rst.write(0);
wait();
// set rst to high, wait one cycle and set it to low, wait another cycle,
// this way we generate a complete reset pulse

sc_int<16> tmp;
// send stimulus to FIR
for (int i = 0; i < 64; i++) {
if (i > 23 && i < 29>)
tmp = 256;
else
tmp = 0;
inp.write(tmp);
wait();
}
}

```c++
void tb::sink() {
sc_int<16> indata;
// read output coming from DUT
for (int i = 0; i < 64; i++) {
indata = outp.read();
wait();
cout << i << " : \t" << indata.to_int() << endl;
// note the to_int()
}

// finally, we stop the simulation
sc_stop();
}

Reference