Sie sind auf Seite 1von 14

SystemC Tutorial

1. Introduction to SystemC
SystemC library has been developed to simulate architectures. It is a library of C++ classes,
global functions, data types and a simulation kernel. By using C/C++ development tools and
the SystemC library, executable models can be created in order to simulate, validate, and
optimize the system being designed. The executable model is essentially a C++ program
that exhibits the same behavior as the system when executed.
2. Getting Started
When you start to read a programming language book, it starts with a hello world
example program to understand the language. Once you understand the basic program, you
can do something more interesting in that language. We take the same approach and write a
hello world example in SystemC.

We need to install SystemC, before we can actually write a program. SystemC installation is
sometimes horrible, therefore we decided to provide you a precompiled binary of
SystemC 2.2 which can execute on Mac OS X (PPC and x86), Linux(32 & 64 bit x86) and
Solaris (sun4u). You can copy the given directory to your machine and can execute
SystemC programs.

/home/jianfu/systemc

If you want to install SystemC yourself on your machine, you are referred to
(http://www.systemc.org) to download the source code and compile it.

We also provide the framework for this lab course, which has a Makefile that is configured
to use the above SystemC installation. It also has some helper functions to read addresses
from the provide trace files and print the required results in the suggested format. You can
copy the given directory to your machine.

/home/jianfu/aca2011

Now we have the SystemC installed and we have the Makefile to compile a C++ program
using SystemC library. Lets first compile the hello_world example and see the output.
While you are in aca2011 directory, type make hello_world. This will compile the
program to generate the binary which you can then execute by typing ./hello_world.bin.
See the output given below.

$ make hello_world
$ ./hello_world.bin

SystemC 2.2.0 --- Nov 24 2009 16:53:40
Copyright (c) 1996-2006 by all Contributors
ALL RIGHTS RESERVED
Hello World.


Congratulations! You successfully execute your first program in SystemC. You might want
to have a look at Makefile to see how to compile a C++ program with SystemC library. The
hello_world program is given below.

// All systemc modules should include systemc.h header file
#include "systemc.h"
// Hello_world is module name
SC_MODULE (hello_world)
{
SC_CTOR (hello_world)
{
// Nothing in constructor
}
void say_hello()
{
//Print "Hello World" to the console.
cout << "Hello World.\n";
}
};

// sc_main in top level function like in C++ main
int sc_main(int argc, char* argv[])
{
hello_world hello("HELLO");
// Print the hello world
hello.say_hello();
return(0);
}

It is very similar to C++, isnt it? Yes there is something new: sc_main, SC_MODULE, and
SC_CTOR.

Every C/C++ program starts execution from main() function. However if SystemC library is
used then the program starts execution from sc_main(). main() function is called internally
in SystemC library. The arguments of sc_main() are similar to main() in C/C++.

SystemC programs consist of a set of one or more modules. They provide the ability to
describe structure and are similar to classes in C++. SC_MODULE(hello_world) define a
class which has a constructor SC_CTOR(hello_world) and a function say_hello().
3. Using SystemC
Modules
Modules are the basic building blocks in SystemC to partition a design. A module in basic
contains ports, internal data, constructors and functions to work on the ports. They are
defined as
SC_MODULE(module_name)
{
//module body
}

Ports
Ports pass data to and from the processes of a module to the external world. They are
declared with in, out, or inout. They are declared as
sc_in<bool> reset;
sc_inout<int> data;

Signals
Ports are used for the external communication and signals are used for the communication
inside the module. They can be declared as
sc_signal<bool> reset;

Process
Processes are functions that can be made to run continuously by making them as threads or
run whenever an event occurs.

Constructor
Same as in C++ and can be defined as
SC_MODULE(module_name)
{
SC_CTOR(module_name)
{
//body
}
}
4. CPU-Memory Example

We now have an idea of how a simple SystemC program looks like and how to execute it.
Lets write another program and this will help you to start your assignments during this lab
course.

In this example we simulate CPU connected directly to memory through a number of ports.
When the simulation starts, CPU reads addresses from the given trace file and forward them
to memory to read or write data. The memory receives the request from CPU and performs
the operation. CPU waits for the operation to be completed, and then issues the next
instruction. Let starts from looking at the code and try to understand it.

#include <systemc.h>
#include <iostream>
#include "aca2011.h"

#define DEBUG
using namespace std;

SC_MODULE(Memory)
{
public:
enum Function
{
FUNC_READ,
FUNC_WRITE
};

enum RetCode
{
RET_READ_DONE,
RET_WRITE_DONE,
};
sc_in<bool> Port_CLK;
sc_in<Function> Port_Func;
sc_out<RetCode> Port_Done;
sc_in<int> Port_Addr;
sc_inout_rv<32> Port_Data;

SC_CTOR(Memory)
{
SC_THREAD(execute);
sensitive << Port_CLK.pos();
dont_initialize();
}
~Memory()
{

}
private:
void execute()
{
while (true)
{
wait(Port_Func.value_changed_event());
Function f = Port_Func.read();
int addr = Port_Addr.read();
// Simulate Memory read/write delay
wait(100);

if (f == FUNC_READ)
{
// We return a fack data to CPU which is read from the requested
// address of memory
int data = 0;
Port_Data.write( data );
Port_Done.write( RET_READ_DONE );

stats_readhit(0);
#ifdef DEBUG
cout<<"@"<<sc_time_stamp()<<" Memory reads at address("<<addr<<"). "<<endl;
#endif
Port_Data.write("ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ");
}
else
{
stats_writehit(0);
#ifdef DEBUG
cout<<"@"<<sc_time_stamp()<<" Memory writes at address("<<addr<<"). "<<endl;
#endif
Port_Done.write( RET_WRITE_DONE );
}
}
}
};

SC_MODULE(CPU)
{
public:
sc_in<bool> Port_CLK;
sc_out<Memory::Function> Port_MemFunc;
sc_in<Memory::RetCode> Port_MemDone;
sc_out<int> Port_MemAddr;
sc_inout_rv<32> Port_MemData;

SC_CTOR(CPU)
{
SC_THREAD(execute);
sensitive << Port_CLK.pos();
dont_initialize();
}
private:
void execute()
{
TraceFile::Entry tr_data;
Memory::Function f;
// Loop until end of tracefile
while(!tracefile_ptr->eof())
{
// Get the next action for the processor in the trace
if(!tracefile_ptr->next(0, tr_data))
{
cerr << "Error reading trace for CPU" << endl;
break;
}
int addr = tr_data.addr;
int data = 0;

switch(tr_data.type)
{
case TraceFile::ENTRY_TYPE_READ:
f = Memory::FUNC_READ;

Port_MemAddr.write(addr);
Port_MemFunc.write(f);
#ifdef DEBUG
cout<<"@"<<sc_time_stamp()<<" CPU reads data from address("<<addr<<").
"<<endl;
#endif
break;

case TraceFile::ENTRY_TYPE_WRITE:
f = Memory::FUNC_WRITE;
//Write a random data to Memory
data = rand();
Port_MemData.write(data);
Port_MemAddr.write(addr);
Port_MemFunc.write(f);

#ifdef DEBUG
cout<<"@"<<sc_time_stamp()<<" CPU writes data at address("<<addr<<").
"<<endl;
#endif

Port_MemData.write("ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ");
break;

case TraceFile::ENTRY_TYPE_BAR:
#ifdef DEBUG
cout<<"Memory Barrier .. synchronize all processors"<<endl;
#endif
continue;
//break;
case TraceFile::ENTRY_TYPE_END:
cout<<"The trace for this file is
terminated"<<endl;
break;

default:
cerr << "CPU 0 "<<" ERROR" << endl;
exit(0);

}
wait(Port_MemDone.value_changed_event());

// Advance one cycle in simulated time
wait();
}

// Finished the tracefile, now stop the simulation
sc_stop();
}
};

int sc_main(int argc, char* argv[])
{
try
{
// Initialize the trace file
init_tracefile(&argc, &argv);

if(num_cpus > 1)
{
cout<<"This test program can handle only single cpu's trace
file"<<endl;
exit(0);
}
// Initialize statistics counters
stats_init();

// Instantiate Modules
Memory mem("main_memory");
CPU cpu("cpu");

// The clock that will drive the CPU and Memory
sc_clock clk;
mem.Port_CLK(clk);
cpu.Port_CLK(clk);

// Signals
sc_buffer<Memory::Function> sigMemFunc;
sc_buffer<Memory::RetCode> sigMemDone;
sc_signal<int> sigMemAddr;
sc_signal_rv<32> sigMemData;

// Connecting module ports with signals
mem.Port_Func(sigMemFunc);
mem.Port_Done(sigMemDone);
mem.Port_Addr(sigMemAddr);
mem.Port_Data(sigMemData);

cpu.Port_MemFunc(sigMemFunc);
cpu.Port_MemDone(sigMemDone);
cpu.Port_MemAddr(sigMemAddr);
cpu.Port_MemData(sigMemData);

// Start Simulation
sc_start();

// Print statistics after simulation finished
stats_print();
stats_cleanup();
}
catch (exception& e)
{
cerr << e.what() << endl;
}

return 0;
}



Memory Module
The Memory Module simulates a Random Access Memory. CPU issues instruction to read
or write data at given addresses and this module performs the requested operation.

The first elements of the modules declaration are two enumeration types. The first is used
to identify the read or write request by CPU and the second is used to send signal back to
CPU when it is done with reading or writing.

The next five lines are the declarations of ports the module has in order to communicate
with other modules. Ports are objects through which a module can be connected into one or
more channels. The ports used in the example are: sc_in, sc_out and sc_inout_rv. These
ports can handle data of any C++ or SystemC data-type.

SC_CTOR is the constructor of the module. As a module has processes, which are member
functions or threads registered as processes to the simulation kernel and executed by it when
an event is triggered. The first line of the constructors code does exactly that. It registers to
the simulation kernel that the Memory module has a process which is a thread, the code of
which is the execute member function.


Processes have a list of events on which they are sensitive. If an event happens on a
process sensitivity list, they get woken up. The second line of the constructors code
sensitive << Port_CLK.pos() creates that list for the process registered by the previous
SC_THREAD statement. It specifies that this process will execute on the event when the
signal on the Port_CLK port goes positive (i.e. once per clock cycle).

Events are the basic synchronization objects. They are used to synchronize between
processes. Processes are triggered or caused to run based on their sensitivity to events. That
means that every time a module member function is registered as a process, the events on
which it is sensitive are also defined.

Process gets executed automatically in constructor even if event in sensitivity list does not
occur yet. To prevent this unintentional execution, use dont_initialize() function as shown
in example.

The execute member function defines the code for the thread that will run for this module,
as defined in the constructor. The thread continuously waits for the Port_Func to get written
and then serves the request. It reads the address and data if its a memory read, then waits
100 cycles to simulate memory latency and finally writes the result back before waiting on
the Port_Func again.

Note that a string of 32 Zs is written to the 32-bit bi-directional data port a cycle after we
write the data to the port. This has to be done because bi-directional channels, which have
multiple writers, attempt to resolve conflicts when multiple writers are writing values. Since
we have now written a value, we need to reset our end of the channel in the next cycle to
allow the other end to write values for the next request.
CPU Module
This module simulates a CPU that has the appropriate ports to connect to our Memory
module and make read/write requests for random addresses. In this module we uses sc_in
port for every sc_out port of the Memory module, and a sc_out for every sc_in. The
constructor and execute member function of the module is similar to the one of the Memory
module.

tracefile_ptr is a global pointer to the trace file to read addresses from. Functions
init_tracefile() and stat_init() and stat_print() are explained in Appendix B.
sc_main()

In the sc_main() function the structural elements of the system are created and connected
throughout the system hierarchy. This is facilitated by the C++ class object construction
behavior. When a module comes into existence all sub-modules it contains are also created.

We create four channels of type sc_signal and sc_buffer and connect the two modules
through their ports to these signals. We also create an instance of the sc_clock class and
connects that to the Port_CLK ports of the CPU and Memory modules. The sc_clock class
is a predefined primitive channel derived from the class sc_signal and is intended to model
the behavior of a digital clock signal. In effect an instance of sc_clock triggers an event in
regular intervals (there are constructor overloads that can be used to set certain properties to
the clock. When a modules port is connected to the clock the process sensitive to the port is
executed.
After all modules have been created and connections have been established the simulation
can start by calling the function sc_start() where the simulation kernel takes control and
executes the processes of each module depending on the events occurring at every
simulated cycle. In the first cycle all the processes are executed at least once unless
otherwise stated with a call to the function dont_initialize() when the process is registered.
Time starts at 0 and moves forward only. Time increments are based on the default time
unit and the time resolution. Once the simulation starts there must be a terminating point
where you call sc_stop() function. In our example code we call sc_stop() when we reach
the end of the tracefile.


7. Conclusion

Until now we have seen how a basic simulator can be built using SystemC and the basic
aspects of the framework. A complete implementation of this example can be downloaded
from:

http://www.science.uva.nl/~jianfu/aca2011/aca2011.html

More detailed information on the topics described here and many others can be found in the
document: 1666-2005 IEEE Standard SystemC

Language Reference Manual, which can be


downloaded from http://standards.ieee.org/getieee/1666/ (via http://www.systemc.org/) or at the
assignment site.

If you encounter any problem in the example code or reading incorrect data from given
trace files please send an email to j.fu@uva.nl.

















Appendix A:
Implementing a Bus
In your assignment you will have to create a bus to connect all the caches with. This
appendix gives a short overview of how to create a bus.

1. A bus could simply be represented by a group of signals.
For instance, the address bus could be represented with
sc_signal_rv<32> BusAddress;

The corresponding port in cache module can be constructed as
sc_inout_rv<32> Port_BusAddress;

By connecting all the Port_BusAddress in cache with BusAddress signal, all the cache
modules could read from or write to the address bus. It is important to notice that the
main memory delay has to be handled at some places, probably in cache.

2. A bus can also be packed into a Bus class which implementing a bus interface Bus_if.

A simple bus interface is:
class Bus_if : public virtual sc_interface
{
public:
virtual bool read(int addr) = 0;
virtual bool write(int addr, int data) = 0;
};

An example of a bus class is:
class Bus : public Bus_if, public sc_module
{
public:
// ports
sc_in<bool> Port_CLK;
sc_signal_rv<32> Port_BusAddr;

public:
SC_CTOR(Bus)
{
// Handle Port_CLK to simulate delay
// Initialize some bus properties
}

virtual bool read(int addr)
{
// Bus might be in contention
Port_BusAddr.write(addr);
return true;
};

virtual bool write(int addr, int data)
{
// Handle contention if any
Port_BusAddr.write(addr);
// Data does not have to be handled in the simulation
return true;
}
}

Within the cache, a bus port with bus interface can be represented with
sc_port<Bus_if> Port_Bus;

The following statement in cache can put a read request on bus.
Port_Bus->read(address);

The above code is not complete, so please add more code to simulate your bus. You
could add as many signals and ports as needed. Please notice that in task 3, the bus
might also need to transfer some information about cache line state information, because
if cache A wants to read data which is at Modified state in cache B, cache B has the
obligation to write the data back to main memory before cache A can read from it.

Appendix B:
ACA Helper Functions

Trace files
The TraceFile class is a data type that represents a file that contains traces of memory
requests from a programs execution. Instances of this class can be used to read those traces
from trace files in order to drive a memory simulation. A trace file might contain traces
from multiple processors if it has been created from a multi-processor system. The file
aca2011.h contains the declaration of the TraceFile class while the file aca2011.cpp
contains its implementation. These two files must be part of programs that use the TraceFile
class. We also provide a structure to collect statistics from the simulation. Below is brief
explanation of using the trace file and how to store the statistics to be displayed at the end of
simulation.

Opening a trace file
A function named init_tracefile is provided which takes the commandline
arguments and creates a TraceFile object from the tracefile specified in the first
argument. The signature of this function is given as:

void init_tracefile(int* argc, char** argv[])

The created TraceFile object can then be accessed by the global pointer
tracefile_ptr. This function also sets the global variable num_cpus to indicate how
many CPU traces are present in the opened tracefile. Furthermore, as argc and argv are
passed by reference, the function removes the first argument so that the rest of the
arguments can be parsed afterwards.

Alternatively, the tracefile can be opened manually by creating a TraceFile object. The
TraceFile class has only one public constructor with the following signature:

TraceFile(const char* filename);

The parameter filename is a C-style string with the name of the file. On object creation
(either on stack or dynamically using new) the file will be opened. If an error occurred
when opening the file, a runtime_error is thrown.

On object destruction the file will be closed, if it wasnt already. You can explicitly close a
file and free up resources before the object is destructed by using the close member
function.

Reading from a trace file
Reading a trace for a processor from the file can be achieved using the next member
function (after the file has been opened). The signature of this function is the following:

bool next(uint32_t pid, Entry& e);

The parameter pid is the id/index of the processor for which the next trace-entry will be
read and the e parameter is a reference to a structure of type TraceFile::Entry which
will receive the entry read from the file. The function will return false if an error occurred.
If there is no event for the current time step, the function will return true, and the entrys
type will be ENTRY_TYPE_END.

The Entry data type is a struct declared in the public interface of the class. Thus, when
variables of this data type are declared, the name of the class must be prefixed with the
name of the class (e.g. TraceFile::Entry).

In Task1 and 2 whenever you encounter an entry of type ENTRY_TYPE_BAR ignore it. In
these two tasks the reading of the traces does not need to be synchronized between
processors. This means that each processor can call the next function individually. If a
processor calls next when it already reached the end of its trace, it will keep reading
ENTRY_TYPE_END.

In Task 3 we want multiple processors to be synchronized at certain points. Therefore we
have introduced memory barriers in trace files. In the trace file when you encounter an entry
type ENTRY_TYPE_BAR you need to synchronize all processors and then continue further
execution.

The following code is an example of how the functions to handle tracefiles can be used:

// Open and set up the tracefile from cmdline arguments
init_tracefile(&argc, &argv);

TraceFile::Entry tr_data;
Memory::Function f;

// Loop until end of tracefile
while(!tracefile_ptr->eof())
{
// Get the next action for the processor in the trace
if(!tracefile_ptr->next(pid, tr_data))
{
cerr << "Error reading trace for CPU" << endl;
break;
}

switch(tr_data.type)
{
case TraceFile::ENTRY_TYPE_READ:
cout << "P" << pid << ": Read from " << tr_data.addr << endl;
break;
case TraceFile::ENTRY_TYPE_WRITE:
cout << "P" << pid << ": Write to " << tr_data.addr << endl;
break;
case TraceFile::ENTRY_TYPE_BAR:
break;
default:
cerr << "Error got invalid data from Trace" << endl;
exit(0);
}
}

The above code will read the trace file provided at command line using function
init_tracefile(&argc, &argv) and will write all its trace entries to the standard
output stream.

The TraceFile class also contains a number of member functions that return information
about the file or its state. Those functions are shown in Table 1:

Function Signature Operation
bool eof() const;
Returns true when all processors reached
the end of its trace
uint32_t get_proc_count() const;
Returns the number of processors for
which the file contains traces for.
Table 1: Member Functions

How the functions in Table 1 can be used is also demonstrated in the example code above.

Statistics
The aca2011.cpp file comes with functions to gather and print statistics required for the
simulator. These functions for the statistics are shown in the Table 2. These functions are
used in the tutorial example.

Function Signature Operation
void stats_init();
Initialize internal data for the number of
processors required in the trace file.
Requires num_cpus to be set.
void stats_cleanup();
Free the internal statistic data
void stats_print();
Prints the statistics for each processor at
the end of the simulation.
void stats_writehit(uint32_t cpuid);
Increments the writehit for the specified
processor
void stats_writemiss(uint32_t cpuid);
Increments the writemiss for the specified
processor
void stats_readhit(uint32_t cpuid);
Increments the readhit for the specified
processor
void stats_readmiss(uint32_t cpuid);
Increments the readmiss for the specified
processor
Table 2: Functions to calculate statistics

Das könnte Ihnen auch gefallen