This section describes a little of the operation of the SystemC simulation kernel, and then relates this to the behaviour of primitive channels.
Most modelling languages, VHDL for example, use a simulation kernel. The purpose of the kernel is to ensure that parallel activities (concurrency) are modelled correctly. In the case of VHDL, a fundamental decision was made that the behaviour of the simulation should not depend on the order in which the processes are executed at each step in simulation time. This section starts by describing what happens in VHDL, because SystemC mimics some key aspects of the VHDL simulation kernel, but also allows other models of computation to be defined. Once VHDL is understood, it is possible to extend the discussion to incorporate the more general simulation kernel of SystemC.
For instance, suppose in SystemC there are two SC_THREADs, both sensitive to a trigger.
SC_THREAD(proc_1); sensitive << Trig.pos(); SC_THREAD(proc_2); sensitive << Trig.pos();
When the trigger changes from low to high, which process will run first? More importantly, does it matter? In the analogous situation using VHDL, you really do not care. This is because in VHDL, communication between processes is done via signals, and process execution and signal update are split into two separate phases.
The VHDL simulation kernel executes each process in turn, but any resulting changes on signals do not happen instantaneously. The same is true of SystemC and sc_signal. To be precise, the assignments are scheduled to happen in the future, meaning when all currently active processes have been evaluated and have reached a point where they need to suspend and wait for some event to occur.
It is possible that no simulated time has passed. If that is the case, and there are pending updates for signals, the processes that react to those signals will run again, without time having passed. This is known as a "delta cycle" and has the effect of determining unambiguously the order in which communicating processes are to execute in cases where no simulation time is passing.
SystemC can do this too, but can also model concurrency, communication and time and in other ways.
When signals in VHDL or sc_signals in SystemC are used to communicate between processes, the simulation is deterministic; it will behave the same on any simulation tool.
In SystemC however, the language allows non-determinism. For instance, suppose a variable declared in a class is accessed from two different SC_THREADs, as described above. Here is an example
SC_MODULE(nondet) { sc_in Trig; int SharedVariable; void proc_1() { SharedVariable = 1; cout << SharedVariable << endl; } void proc_2() { SharedVariable = 2; cout << SharedVariable << endl; } SC_CTOR(nondet) { SC_THREAD(proc_1); sensitive << Trig.pos(); SC_THREAD(proc_2); sensitive << Trig.pos(); } };
In this example, which SC_THREAD will run first is undefined - there is no way of telling which will run first.
For hardware modelling, this is unacceptable. But for software modelling, this might represent a system where a shared variable is used, and where non-determinism is not important - all that is necessary is to guarantee that two processes cannot simultaneously access the variable.
Software engineers use concepts such as mutual exclusion (mutex) or semaphores to cope with such situations.
After looking at the background, it is now possible to summarise the operation of the SystemC simulation kernel.
The SystemC simulation kernel supports the concept of delta cycles. A delta cycle consists of an evaluation phase and an update phase. This is typically used for modelling primitive channels that cannot change instantaneously, such as sc_signal. By separating the two phases of evaluation and update, it is possible to guarantee deterministic behaviour (because a primitive channel will not change value until the update phase occurs - it cannot change immediately during the evaluation phase).
However, SystemC can model software, and in that case it is useful to be able to cause a process to run without a delta cycle (i.e. without executing the update phase). This requires events to be notified immediately (immediate notification). Immediate notification may cause non-deterministic behaviour.
Notification of events is achieved by calling the notify() method of class sc_event.
There are three cases to consider for the notify method.
The notify() method cancels any pending notifications, and carries out various checks on the status of existing notifications.
The behaviour of the simulation kernel can now be described
Note the functions update() and request_update(). The kernel provides these functions specifically for modelling primitive channels such as sc_signal. update() actually runs during the update phase if it was requested by calling request_update() during the evaluation phase.
So how do you write a primitive channel? It is actually surprisingly straightforward! Firstly, all primitive channels are based on the class sc_prim_channel - you can think of this as the primitive channel equivalent of sc_module for hierarchical channels.
Here is the code of a FIFO channel, which is a drastically simplified version of the "built-in" sc_fifo channel. It is simplified in that it only provides blocking methods, and it is not a template class (it only works on type char).
Here is the interface, fifo_if.h
#include "systemc.h" class fifo_out_if : virtual public sc_interface { public: virtual void write(char) = 0; // blocking write virtual int num_free() const = 0; // free entries protected: fifo_out_if() { }; private: fifo_out_if (const fifo_out_if&); // disable copy fifo_out_if& operator= (const fifo_out_if&); // disable }; class fifo_in_if : virtual public sc_interface { public: virtual void read(char&) = 0; // blocking read virtual char read() = 0; virtual int num_available() const = 0; // available // entries protected: fifo_in_if() { }; private: fifo_in_if(const fifo_in_if&); // disable copy fifo_in_if& operator= (const fifo_in_if&); // disable = };
Basically, there is a read and write method, both of which are blocking, i.e. they suspend if the FIFO is empty (read) or full (write).
Here is the code for the first part of the channel.
#include "systemc.h" #include "fifo_if.h" class fifo : public sc_prim_channel, public fifo_out_if, public fifo_in_if { protected: int size; // size char* buf; // fifo buffer int free; // free space int ri; // read index int wi; // write index int num_readable; int num_read; int num_written; sc_event data_read_event; sc_event data_written_event; public: // constructor explicit fifo(int size_ = 16) : sc_prim_channel(sc_gen_unique_name("myfifo")) { size = size_; buf = new char[size]; reset(); } ~fifo() //destructor { delete [] buf; }
The next few functions are used to calculate if space is available and how much is free. The algorithm uses a circular buffer, accessed by the write index (wi) and the read index (ri).
int num_available() const { return num_readable - num_read; } int num_free() const { return size - num_readable - num_written; }
Here is the blocking write function. Note that if num_free() returns zero, the function calls wait(data_read_event). This is an example of dynamic sensitivity. The thread from which write is called will be suspended, until the data_read_event is notified.
void write(char c) // blocking write { if (num_free() == 0) wait(data_read_event); num_written++; buf[wi] = c; wi = (wi + 1) % size; free--; request_update(); }
Once the process resumes after data_read_event, it stores the character in the circular buffer, and then calls request_update(). request_update() makes sure that the simulation kernel calls update() during the update phase of the kernel.
Here is the reset function that clears the FIFO.
void reset() { free = size; ri = 0; wi = 0; }
And here is the read function. Behaviour is similar to the write function, except this time the process blocks if there is no space available (the FIFO is full).
void read(char& c) // blocking read { if (num_available() == 0) wait(data_written_event); num_read++; c = buf[ri]; ri = (ri + 1) % size; free++; request_update(); }
For convenience, here is a "shortcut" version of read, so we can use
char c = portname->read();
syntax.
char read() // shortcut read function { char c; read(c); return c; }
And finally the update() method itself. This is called during the update phase of the simulation kernel. It checks to see if data was read or written during the evaluation phase, and then calls notify(SC_ZERO_TIME) as appropriate to tell the blocked read() or write() functions that they can proceed.
void update() { if (num_read > 0) data_read_event.notify(SC_ZERO_TIME); if (num_written > 0) data_written_event.notify(SC_ZERO_TIME); num_readable = size - free; num_read = 0; num_written = 0; } };
The top level of the design looks very similar to the hierarchical channel. Here it is (from main.cpp)
#include "systemc.h" #include "producer.h" #include "consumer.h" #include "fifo.h" int sc_main(int argc, char* argv[]) { sc_clock ClkFast("ClkFast", 1, SC_NS); sc_clock ClkSlow("ClkSlow", 500, SC_NS); fifo fifo1; producer P1("P1"); P1.out(fifo1); P1.Clock(ClkFast); consumer C1("C1"); C1.in(fifo1); C1.Clock(ClkSlow); sc_start(5000, SC_NS); return 0; }
Note how there is no need to give the FIFO primitive channel a name, due to the use of sc_gen_unique_name().
This chapter has shown a glimpse of writing a primitive channel. There are many more details, and you might want to look at the source code for sc_signal or sc_fifo to see more information.
A particular thing to note is the use of dynamic sensitivity - note how the blocked read and write functions are actually overriding the static sensitivity to the clock signal.