Skip to content

Single Source library

Mikhail Moiseev edited this page Feb 27, 2025 · 5 revisions

Introduction

Single Source library consists of communication channels with functional interfaces.

image

Target and Initiator in pair connect two processes in different modules. FIFO connects two processes in the same module as well as serves as a storage for one process. Buffer is a fast version of FIFO to be used in sequential process(es) only. Pipe is used to pipeline computation for multiple cycles. Register adds state for combinational process.

image

The Single Source modules work in two modes: cycle accurate (RTL) and approximate time (TLM). Cycle accurate RTL mode intended for hardware synthesis. In RTL mode the modules provide cycle accurate simulation. Approximate time TLM (Transaction Level Modelling) mode provides fast simulation, intended for functional and performance simulation. Approximate time simulation is data-driven, there are no clock events.

Library files

  • sct_common.h -- includes of all library headers and adds using namespace sct
  • sct_ipc_if.h -- interfaces, general template types and defines
  • sct_initiator.h -- initiator module
  • sct_target.h -- target and combinational target modules
  • sct_prim_signal.h -- primitive channel signal implementation with multiple drivers support
  • sct_signal.h -- signal implementation
  • sct_ports.h -- input and output ports, sc_port for target and initiator
  • sct_fifo.h -- FIFO module
  • sct_prim_fifo.h -- primitive channel FIFO implementation, used as base channel in TLM mode
  • sct_buffer.h -- buffer channel, which is a fast FIFO to use in clocked process(es) only
  • sct_pipe.h -- pipeline register
  • sct_register.h -- register to store METHOD state
  • sct_clock.h -- clock with enable/disable
  • sct_sel_type.h -- integer types sct_int and sct_uint
  • sct_static_log.h -- static logarithm implementation
  • sct_utils.h -- utility functions

Library defines

SCT_TLM_MODE could be provided as compile definition: if SCT_TLM_MODE defined TLM mode is used, RTL mode is used otherwise.

There are multiple options for clock/reset levels. SCT_CMN_TRAITS -- clock edge and reset level, one of six following options:

  • SCT_POSEDGE_NEGRESET -- positive clock edge, negative reset level
  • SCT_POSEDGE_POSRESET -- positive clock edge, positive reset level
  • SCT_NEGEDGE_NEGRESET -- negative clock edge, negative reset level
  • SCT_NEGEDGE_POSRESET -- negative clock edge, positive reset level
  • SCT_BOTHEDGE_NEGRESET -- both clock edges, negative reset level
  • SCT_BOTHEDGE_POSRESET -- both clock edges, positive reset level

By default, positive clock edge and negative reset level are used. That is provided by define SCT_CMN_TRAITS:

#ifndef SCT_CMN_TRAITS
  #define SCT_CMN_TRAITS SCT_POSEDGE_NEGRESET
#endif

If other clock edge/reset levels required, SCT_CMN_TRAITS value should be provided as compile definition.

There is an CMakeLists.txt example where sct_def_traits target has definitions for TLM mode, negative clock edge and positive reset level:

add_executable(sct_def_traits sc_main.cpp)
target_compile_definitions(sct_def_traits PUBLIC -DSCT_TLM_MODE)
target_compile_definitions(sct_def_traits PUBLIC -DSCT_CMN_TRAITS=SCT_NEGEDGE_POSRESET)

Library interfaces

The interfaces contain non-blocking functions except b_put and b_get which are may-blocking.

Interface Functions Comment
sct_put_if bool ready() Return true if the channel is ready to put request
void reset_put() Reset this channel
void clear_put() Clear (remove) request put in this cycle
bool put(const T& data) Non-blocking put request into the channel if it is ready, return ready to request
bool put(const T& data, sc_uint<N> mask) Non-blocking put request into the channel if it is ready, mask used to enable/disable put or choose targets in multi-cast put, return ready to request
void b_put(const T& data) May-blocking put request, could be used in THREAD process only
void addTo(sc_sensitive& s) Add put related signals to process sensitivity
void addTo(sc_sensitive* s, sc_process_handle* p) Add put related signals to process sensitivity
sct_get_if bool request() Return true if the channel has request to get
void reset_get() Reset this channel
void clear_get() Clear (return back) request got in this cycle
T peek() Peek request, return current request data, if no request last data returned
T get() Non-blocking get request and remove it from the channel, return current request data, if no request last data returned
bool get(T& data, bool enable) Non-blocking get request and remove it from the channel if enable is true, return true if there is a request and enable is true
T b_get() May-blocking get request, could be used in THREAD process only
void addTo(sc_sensitive& s) Add get related signals to process sensitivity
void addTo(sc_sensitive* s, sc_process_handle* p) Add get related signals to process sensitivity
void addPeekTo(sc_sensitive& s) Add peek related signal to process sensitivity
sct_fifo_if inherits sct_put_if<T> and sct_get_if<T>
unsigned size() FIFO LENGTH
unsigned elem_num() Number of elements in the channel, value updated last clock edge for METHOD, last DC for THREAD
bool almost_full(unsigned N) Return true if the channel has (LENGTH-N) elements or more, value updated last clock edge for METHOD, last DC for THREAD
bool almost_empty(unsigned N) Return true if the channel has N elements or less, value updated last clock edge for METHOD, last DC for THREAD
void clk_nrst(sc_in<bool>& clk_in, sc_in<bool>& nrst_in) Bind clock and reset to the channel
void addTo(sc_sensitive& s) Add put and get related signal to process sensitivity
void addToPut(sc_sensitive& s) Add put related signals to process sensitivity
void addToGet(sc_sensitive& s) Add get related signals to process sensitivity
void addToGet(sc_sensitive& s) Add get related signals to process sensitivity
sct_in_if const T& read() Read from the signal/register
void addTo(sc_sensitive* s, sc_process_handle* p) Add signals to process sensitivity
sct_inout_if const T& read() Read from the signal/register
void write(const T& val) Write to the signal/register
void addTo(sc_sensitive* s, sc_process_handle* p) Add signals to process sensitivity

Functions addTo, addToPut, addToGet and addPeekTo are used to add the channel to process sensitivity list. Instead of addTo - operator << can be used. Instead of addToPut, addToGet and addPeekTo - operator << with fifo.PUT, << fifo.GET and fifo.PEEK can be used.

Processes

SystemC design with single source library can use method and thread processes created with SC_METHOD and SC_THREAD correspondently. It is recommended to use SCT_METHOD and SCT_THREAD macros instead of them:

  • SCT_METHOD(proc) -- combinational method process, same as SC_METHOD(proc),
  • SCT_METHOD(proc, clk) -- sequential method process with synchronous reset,
  • SCT_METHOD(proc, clk, rst) -- sequential method process with asynchronous reset,
  • SCT_THREAD(proc) -- sequential thread process, same as SC_THREAD(proc),
  • SCT_THREAD(proc, clk) -- sequential thread process with explicit clock,
  • SCT_THREAD(proc, clk, rst) -- sequential thread process with explicit clock/reset.

SCT_METHOD(proc) creates a combinational process operates with single source channels, signals/ports. SCT_METHOD(proc, clk) and SCT_METHOD(proc, clk, rst) creates a sequential method process. SCT_THREAD(proc) creates a sequential process operates with single source channels only. SCT_THREAD(proc, clk) creates a process operates with single source channels and reads single source signals (sct_signal) / ports (sct_in/sct_out). SCT_THREAD(proc, clk, rst) creates a universal sequential process which can access single source channels and/or reads single source signals (sct_signal) / ports (sct_in/sct_out). SC_CTHREAD macro is normally not used.

Difference between sequential method and thread processes is in simulation speed, method process is faster. Thread process allows to use wait() to introduce multiple states, which can simplify the process function code. Sequential method process function should have reset section and sequential logic section, and cannot have wait() calls.

void combMethod() {
   // combinational logic ...
}
void seqMethod() {
   if (rst) {
      // reset logic ...
   } else {
      // sequential logic ...
   }
}
void seqThread() {
   // reset logic
   wait();
   while (true)
      // sequential logic ...
      wait();         
      // optional sequential logic ...
      // multiple wait() calls allowed
   }
}

Clock and reset levels for a process are specified by SCT_CMN_TRAITS if clock/reset are explicitly provided. Otherwise clock and reset levels are taken from channels in the sensitivity list. Channels obtains clock and reset levels from SCT_CMN_TRAITS by default, that can be changed for individual channels. All the channels used in a sequential process should use the same clock and edge as the process sensitive. A channel could have different reset or reset level than the process. If a thread process has reset signal(s), it should have the reset specification with async_reset_signal_is or/and sync_reset_signal_is.

Any process should be sensitive to all single source channels accessed and to all single source signals (sct_signal) / ports (sct_in/sct_out) read in its function code. Combinational method should be also sensitive to all SystemC signals (sc_signal) and ports (sc_in/sc_out) read in its function code. A process is never sensitive to reset.

template <class T>
class MyModule : public sc_module {
   sc_in<bool>      clk{"clk"};
   sct_target<T>    targ{"targ"};
   sct_initiator<T> init{"init"};
   sct_signal<T>    s{"s"};

   explicit MyModule(const sc_module_name& name) : sc_module(name) {
        SCT_METHOD(combMethod);                      // Combinational method, same as SC_METHOD
        sensitive << init;                           // No reset in sensitivity

        SCT_METHOD(seqMethod, clk);                  // Sequential method with synchronous reset
        sensitive << init;                           

        SCT_METHOD(seqMethod, clk, nrst);            // Sequential method with asynchronous reset
        sensitive << init;                           

        SCT_THREAD(seqThread);                       // Sequential thread, sensitive to @sct_target only
        sensitive << targ;                           // Clock and reset taken from @sct_target
        async_reset_signal_is(nrst, 0);              // Reset specification required

        SCT_THREAD(seqThread, clk);                  // Sequential thread, sensitive to @sct_target and @sct_signal
        sensitive << targ << s;                      // Clock is explicitly provided for @sct_signal
        async_reset_signal_is(nrst, 0);              // Reset specification required

        SCT_THREAD(seqThread, clk, nrst);            // Most universal sequential thread, sensitive to @sct_signal here
        sensitive << s;                              // Clock and reset are explicitly provided
        async_reset_signal_is(nrst, 0);              // Reset specification still required
   }
};

If any process sensitive to a channel which is not read inside or not sensitive to a channel which is read inside, error reported by ICSC. The error is reported for single channels and for vector/array of channels, no individual channels in vector/array are considered here.

Target and Initiator

Target and Initiator are channels intended to connect two user defined modules. Initiator implements sct_put_if interface and could be used in one METHOD or THREAD process to put requests. Target implements sct_get_if interface and could be used in one METHOD or THREAD process to get requests which put by the connected Initiator.

Target and initiator instantiation and bind

To connect two modules, Target placed in one modules, Initiator in another one. Target and Initiator should be connected to clock and reset with clk_nrst() function. Target and Initiator are connected to each other with method bind(), called in their common parent module constructor. Both Target and Initiator have method bind(), any of them can be called.

image

struct Producer : public sc_module {
    sc_in<bool>         clk{"clk"};
    sc_in<bool>         nrst{"nrst"};
    sct_initiator<T>    init{"init"};
    explicit Producer (const sc_module_name& name) : sc_module(name) {
        init.clk_nrst(clk, nrst);
    } 
}
struct Consumer : public sc_module {
    sc_in<bool>         clk{"clk"};
    sc_in<bool>         nrst{"nrst"};
    sct_target<T>       targ{"targ"};
    explicit Consumer (const sc_module_name& name) : sc_module(name) {
        targ.clk_nrst(clk, nrst);
    } 
}
struct Top: public sc_module {
    Producer prod{"prod"};
    Consumer cons{"cons"};
    explicit Top(const sc_module_name& name) : sc_module(name) {
        prod.clk(clk); prod.nrst(nrst);
        cons.clk(clk); cons.nrst(nrst);
        // Call bind() method of initiator or bind() method of target
        prod.init.bind(cons.targ);  
    }
}

Target and Initiator have the same template parameters:

template<
    class T,                            // Payload data type 
    class TRAITS = SCT_CMN_TRAITS,  // Clock edge and reset level traits
    bool TLM_MODE = SCT_CMN_TLM_MODE>   // RTL (0) or TLM (1) mode
class sct_initiator {};

template<
    class T,                            // Payload data type 
    class TRAITS = SCT_CMN_TRAITS,  // Clock edge and reset level traits
    bool TLM_MODE = SCT_CMN_TLM_MODE>   // RTL (0) or TLM (1) mode
class sct_target {};

Target and Initiator constructor parameters:

sct_target(const sc_module_name& name,      // Module name -- same as instance variable name
           bool sync_ = 0,                  // Is register required to pipeline request 
           bool always_ready_ = 0);         // Is always ready to get request

sct_initiator(const sc_module_name& name,   // Module name -- same as instance variable name  
           bool sync_ = 0);                 // Is register required to pipeline request  

That is enough to setsync_ = 1 for Target or for Initiator to have register added.

Target and initiator usage

Target and initiator can be used in SystemC method process. The method process should be created with SC_METHOD or SCT_METHOD macro in the module constructor. The method process should have sensitivity list with all the targets/initiators accessed in the process function.

// Initiator and target in method process example
struct Producer : public sc_module {
    sct_initiator<T>         init{"init"};
    explicit Producer (const sc_module_name& name) : sc_module(name) {
       SC_METHOD(initProc); 
       sensitive << init;
    } 
    void initProc {
       // Put data into init
    }
}

struct Consumer : public sc_module {
    sct_target<T>       targ{"targ"};   
    explicit Consumer (const sc_module_name& name) : sc_module(name) {
       SC_METHOD(targProc); 
       sensitive << targ;
    } 
    void targProc{
       // Get data from targ
    }
}

Target and initiator can be used in clocked thread process. Clocked thread process should be created with SC_THREAD or SCT_THREAD macro, but not with SC_CTHREAD. The thread process should have sensitivity list with all the targets/initiators accessed in the process function as for method process. If the thread process has reset signal, it should have the reset specification with async_reset_signal_is or/and sync_reset_signal_is.

// Initiator and target in thread process example
struct Producer : public sc_module {
    sct_initiator<T>         init{"init"};
    explicit Producer (const sc_module_name& name) : sc_module(name) {
       SC_THREAD(initProc); 
       sensitive << init;
       async_reset_signal_is(nrst, 0);
    } 
    void initProc {
       // Reset init to set default values
       wait();
       while(true) {
          // Put data into init 
          wait();
       }
    }
}

struct Consumer : public sc_module {
    sct_target<T>       targ{"targ"};   
    explicit Consumer (const sc_module_name& name) : sc_module(name) {
       SC_THREAD(targProc); 
       sensitive << targ;
       async_reset_signal_is(nrst, 0);
    } 
    void targProc{
       // Reset init to set default values
       wait();
       while(true) {
          // Get data from targ
          wait();
       }
    }
}

There are three kinds of connections which could be organized:

  • Combinational,
  • Buffered,
  • Buffered with FIFO.

Combinational connection

In combinational connection request part of connection contains core_req and core_data signals, which could be used directly or through the pipelining register (specified with second parameter of Target/Initiator constructor). There is no back-pressure signal, so Target process should be always ready to get request. Initiator process does not need to check ready to put request (method ready() always returns true).

image

Combinational connection is provided with last parameter of sct_target<> constructor or with using special target class sct_comb_target<>.

In combinational connection put data into Initiator can be done without checking if the Initiator is ready.

// Initiator and always ready target in method process example
struct Producer : public sc_module {
    sct_initiator<T>         init{"init"};
    explicit Producer (const sc_module_name& name) : sc_module(name) {
       SC_METHOD(initProc); sensitive << init;
    } 
    void initProc {
       T val = getSomeValue();     // Put at every path, reset is not required 
       init.put(val);              // Do not check ready() as connected Target is always ready
    }
}

struct Consumer : public sc_module {
    // Combinational target
    sct_comb_target<T>       targ{"targ"};   
    explicit Consumer (const sc_module_name& name) : sc_module(name) {
       SC_METHOD(targProc); sensitive << targ;
    } 
    void targProc{
       T val;
       if (targ.get(val)) {     // Get at every path, reset is not required
           doSomething(val);        
       }
    }
}

In thread process it needs to reset Initiator and Target in the reset section.

// Initiator and always ready target in thread process example
struct Producer : public sc_module {
    sct_initiator<T>         init{"init"};
    explicit Producer (const sc_module_name& name) : sc_module(name) {
       SC_THREAD(initProc); sensitive << init;
       async_reset_signal_is(nrst, 0);
    } 
    void initProc {
       init.reset_put();              // Reset is required in thread process
       wait();
       while(true) {
          T val = getSomeValue();     // Put every cycle
          init.put(val);              // Do not check ready() as connected Target is always ready
          wait();
       }
    }
}

struct Consumer : public sc_module {
    sct_comb_target<T>       targ{"targ"};   
    explicit Consumer (const sc_module_name& name) : sc_module(name) {
       SC_THREAD(targProc); sensitive << targ;
       async_reset_signal_is(nrst, 0);
    } 
    void targProc{
       targ.reset_get();              // Reset is required in thread process
       wait();
       while(true) {
          if (targ.request()) {         
              doSomething(targ.get());
          }
          wait();
       }
    }
}

Using Target and Initiator in method and thread process looks very similar. In the next sections examples using method and thread process will be mixed.

Buffered connection

In buffered connection core_ready signal is used as backpressure when Target is not ready to get request. This connection called buffered as it has the buffer register inside Target or Initiator to store one request if Target is not ready. THis kind of connection is the most common and used as default one.

Request part of the connection contains core_req and core_data signals, which could be used directly or through the pipelining register (specified with second parameter of Target/Initiator constructor). The pipelining register is additional to the buffer register. Response part contains core_ready signal which is passed through register to avoid combinational loop. If target process is method this register explicitly added, if it is thread this register is implicitly provided by the process.

image

struct Producer : public sc_module {
    sct_initiator<T>         init{"init"};
    explicit Producer (const sc_module_name& name) : sc_module(name) {
       SC_METHOD(initProc); sensitive << init;
    } 
    void initProc {
       init.reset_put();            // Reset required as put is done at some path only  
       if (init.ready()) {          // Check ready required as target could be not ready 
          init.put(getSomeValue());
       }
    }
}

struct Consumer : public sc_module {
    sct_target<T>       targ{"targ"};
    explicit Consumer (const sc_module_name& name) : sc_module(name) {
       SC_THREAD(targProc); sensitive << targ;
       async_reset_signal_is(nrst, 0);
    } 
    void targProc {
       targ.reset_get();
       wait();
       while(true) {
          if (targ.request()) {      
              doSomething(targ.get()); 
          }
          wait(); 
       }
    }
}

Buffered connection with FIFO

The buffered connection with FIFO provides additional buffer to store requests until their processed by the target process. FIFO can be added to Target with add_fifo() method:

template<unsigned LENGTH>                 // FIFO size (maximal number of elements)
void add_fifo(bool sync_valid = 0,        // Is register required to pipeline core_req and core_data
              bool sync_ready = 0,        // Is register required to pipeline core_ready 
              bool init_buffer = 0);      // Initialize all the elements with zeros 
                                          // First element to get is always initialized to zero 

image

template<class T>
struct A : public sc_module {
    sct_target<T>       run{"run"}; 
    explicit A(const sc_module_name& name) : sc_module(name) {
        run.clk_nrst(clk, nrst);
        run.template add_fifo<2>(1, 1);  // Add FIFO with 2 element and registers in request/response
    }
}

Initiator-to-Target Protocol

The discussed protocol considers buffered connection w/o FIFO. Request is taken by Target when core_req and core_ready both are high. Target can return it to the target process immediately or store the request in the buffer. Initiator sets new request when the previous one has been taken.

The first diagram below represents Target and Initiators accessed in thread processes. The second diagram represents Target and Initiators accessed in method processes.

image

image

Signal and ports

Signal can be used for inter-process communication between processes in the same module. For communication between processes in different modules input/output ports are used together with signal.

image

Signal and output port implement sct_inout_if, and can be written by one process. Signal, input and output ports implement sct_in_if, and can be read by one or mode processes.

template<
    class T, bool TLM_MODE = SCT_CMN_TLM_MODE>
class sct_signal {};
template<
    class T, bool TLM_MODE = SCT_CMN_TLM_MODE>
class sct_in {};
template<
    class T, bool TLM_MODE = SCT_CMN_TLM_MODE>
class sct_out {};

Using signal and input/output ports in thread process requires to have clock/reset for these channels which provided with SCT_THREAD macro:

SCT_THREAD(proc, clk, rst);  /// Used if the process sensitive to signals/ports only  
SCT_THREAD(proc, clk);       /// Used if the process sensitive to signals/ports and other channels

In this example sigThread sensitive to signals only:

sct_signal<T>   s{"s"};
MyModule(const sc_module_name& name) : sc_module(name) {  
   SCT_THREAD(sigThread, clk, nrst);    // Clock edge/reset level taken from SCT_CMN_TRAITS
   sensitive << s;                      // Only signal `s` is read inside the process
   async_reset_signal_is(nrst, 0);
}

sc_vector of sct_signal, sct_in and sct_out supported. Binding of while vector to another vector is supported.

class A : public sc_module {
    sc_vector<sct_out<T>>      resp{"resp", 3};
};
class Top {
    A   a{"a"};
    sc_vector<sct_signal<T>>   resp{"resp", 3};

    Top (const sc_module_name& name) : sc_module(name) {        
        a.resp(resp);   // All vector elements bound
    }
}

In RTL mode sct_signal is based on sc_signal, sct_in/sct_out are based on sc_in/sc_out.

Level-enable signal and ports

Level enable signal is often used by one process to make another process does some actions. The signal is usually boolean type. It can have a specific level (usually high) for one or several cycle that means some action to be done. Such behavior work well in cycle accurate mode, but for fast simulation mode requires event notification at every cycle. To support this event notification sct_signal and sct_in/sct_out have second template parameter ENABLE_EVENT. Also special level-enable signal and ports of boolean type are introduced: sct_enable_signal, sct_enable_in, and sct_enable_out. Level-enable singal in fast simulation mode notifies event to wake a sensitive thread process every clock cycle which is taken from the process. Combinational method process cannot be sensitive for such a signal or port.

In the following example producer process send multiple sequenced event.

SC_MODULE(MyModule) {
    sct_target<unsigned>      SC_NAMED(targ);
    SC_CTOR(MyModule) {
        SCT_THREAD(producer, clk, nrst); 
        sensitive << enable << targ;
        async_reset_signal_is(nrst, 0);
        SCT_THREAD(consumer, clk, nrst); 
        sensitive << enable;
        async_reset_signal_is(nrst, 0);
    }
    sct_enable_signal   SC_NAMED(enable);
    void producer() {
        targ.reset_get(); enable = 0;
        wait();
        while (true) {
            unsigned N = targ.b_get();
            enable = 1;
            while (N--) wait();
            enable = 0;
            wait();
        }
    }
    void consumer() {
        wait();
        while (true) {
            if (enable) {...};  // Do something
            wait();
}}}

FIFO

The FIFO can be used for inter-process communication between processes in the same module and for storing requests inside one process. Also the FIFO could be used inside of Target as an extended buffer.

image

The FIFO implements sct_fifo_if. FIFO has size template parameter which is a positive number.

template<
    class T, 
    unsigned LENGTH,                    // Size (maximal number of elements)
    class TRAITS = SCT_CMN_TRAITS,      // Clock edge and reset level traits
    bool TLM_MODE = SCT_CMN_TLM_MODE>   // RTL (0) or TLM (1) mode
>
class sct_fifo {};

The FIFO can have combinational or registered request (core_req and core_data) and response (core_ready) kind which specified in constructor parameters.

sct_fifo(const sc_module_name& name, 
         bool sync_valid = 0,       // Request path has synchronous register 
         bool sync_ready = 0,       // Response path has synchronous register  
         bool use_elem_num = 0,     // Element number/Almost full or empty used 
         bool init_buffer = 0)      // Initialize all buffer elements with zeros in reset
                                    // First element to get is always initialized to zero 

Using synchronous register in request path (sync_valid) not allowed if put process is sequential thread or method. Using synchronous register in response path (sync_ready) not allowed if get process is sequential thread or method. That is required to have equivalent behavior in the generated SV code.

Minimal FIFO size required

Minimal FIFO size to provide full throughput depends on process types and request/response kind. In the table below minimal required FIFO sizes to provide full throughput are given.

Using FIFO in method process(es) with both sync_valid and sync_ready set to 0 is prohibited as that results in combinational loop. Using FIFO in one method process is allowed with sync_valid and sync_ready both set to 1 only. If sync_valid or sync_ready set to 0, such FIFO can be used in two different method processes.

Initiator process Target process sync_valid sync_ready Minimal FIFO size
method method 0 0 prohibited
method method 0 1 1, two processes
method method 1 0 1, two processes
method method 1 1 2
method thread 0 0 1
method thread 1 0 2
method thread 0 1 not supported
method thread 1 1 not supported
thread method 0 0 1
thread method 0 1 2
thread method 1 0 not supported
thread method 1 1 not supported
thread thread 0 0 2
thread thread 0 1 not supported
thread thread 1 0 not supported
thread thread 1 1 not supported

Using FIFO for inter-process communication

FIFO could be used for processes communication instead of set of signals. FIFO has only one writer and one reader process, in comparison with sct_signal which could be read in multiple processes. For 1:N communication array or sc_vector of FIFOs could be used.

struct Top : public sc_module {
    sct_fifo<T, 2>      fifo{"fifo", 0, 1};     // Pipelining register for response
    explicit Top(const sc_module_name& name) : sc_module(name) {
        fifo.clk_nrst(clk, nrst);
        SC_THREAD(producerProc); sensitive << fifo.PUT;   // Process puts to FIFO
        async_reset_signal_is(nrst, 0);
        SC_METHOD(consumerProc); sensitive << fifo.GET;   // Process gets from FIFO   
    } 
}

void producerProc() {
    fifo.reset_put();
    wait();
    while (true) {
       if (fifo.ready()) {           // If FIFO is ready put next value
          fifo.put(getSomeVal());
       }
       wait();
    }
}
void consumerProc() {
    fifo.reset_get();
    T val;
    if (fifo.get(val)) {
       doSomething(val);
    }
}

One process stores requests in FIFO

struct Top : public sc_module {
    sc_in<bool>         clk{"clk"};
    sc_in<bool>         nrst{"nrst"};
    sct_fifo<T, 5>      fifo{"fifo"};
    explicit Top(const sc_module_name& name) : sc_module(name) {
        fifo.clk_nrst(clk, nrst);
        SC_THREAD(storeProc); sensitive << fifo;  // Process puts and gets to FIFO
        async_reset_signal_is(nrst, 0);
    }
}

void storeProc() {
    fifo.reset();
    wait();
    while (true) {
       if (fifo.ready()) {
          fifo.put(getSomeValue());
       }
       wait(); 
       if (fifo.request()) {
          doSomething(fifo.get());
       }
    }
}

Buffer

image

Buffer is a channel kind of FIFO to be used in single or two sequential processes. Buffer implements sct_fifo_if interface, the same as FIFO. Buffer differs from FIFO in higher simulation speed which is achieved by implementation it as primitive channel (inheritor of sc_prim_channel). Buffer has one common implementation for cycle accurate and approximate time modes.

Buffer size can be 2 elements or more.

template<
    class T, 
    unsigned LENGTH,                    // Size (maximal number of elements)
    class TRAITS = SCT_CMN_TRAITS       // Clock edge and reset level traits
>
class sct_buffer {};

Buffer constructor has the same parameters as FIFO has. Parameters sync_valid and sync_ready should be 0 (false).

sct_buffer(const sc_module_name& name, 
           bool sync_valid = 0,       // Request path has synchronous register 
           bool sync_ready = 0,       // Response path has synchronous register  
           bool use_elem_num = 0,     // Element number/Almost full or empty used 
           bool init_buffer = 0)      // Initialize all buffer elements with zeros in reset
                                      // First element to get is always initialized to zero 

Buffer can be used by one process to store data and for inter-process communication between two processes. peek() function of Buffer can be called from any process including combinational method process.

Pipeline register

image

Pipeline register (sct_pipe) is intended to pipeline combinational logic and enable re-timing feature of a logic synthesis tool. In generated SystemVerilog code it is replaced with a component from a logic synthesis tool and an external library. Pipeline register can be used in one method or thread process as well as put in one process and get in other process. Pipeline register is normally added to sensitivity list of process where put or get done.

Pipeline register supports put bubbles and get backpressure. If there is no get, but some empty registers, they are shifted to provide next request put.

The pipeline register implements sct_fifo_if. It has size template parameter which is a positive number.

template<
    class T, 
    unsigned N,                         // Number of pipeline registers, one or more
    class TRAITS = SCT_CMN_TRAITS,      // Clock edge and reset level traits
    bool TLM_MODE = SCT_CMN_TLM_MODE>   // RTL (0) or TLM (1) mode
>
class sct_pipe {};

The pipeline register can have input or output registers, which are not used for re-timing.

sct_pipe(const sc_module_name& name, 
         bool addInReg,                // Add input register not moved by re-timing 
         bool addOutReg,               // Add output register not moved by re-timing
         const std::string& rtlName)   // Pipeline register instantiated component name

Typical use case for pipeline register is combinational logic re-timing in method process.

void methProc() {
    run.reset_get();
    resp.reset_put();
    pipe.reset();

    if (pipe.ready() && run.request()) {
        T data = compute(run.get());     // Heavy computation to be pipelined
        pipe.put(data);
    }
  
    if (pipe.request() && resp.ready()) {
        resp.put(pipe.get());
    }
}

Register

image

Register is used to add state for METHOD process. Register is written in one method process and could be read in the same or other method process(es). Register is normally added to sensitivity list of process where it is read. Register can be read in thread process.

Register has the same template parameters as Target/Initiator:

template<
    class T,                            // Payload data type 
    class TRAITS = SCT_CMN_TRAITS,      // Clock edge and reset level traits
    bool TLM_MODE = SCT_CMN_TLM_MODE>   // RTL (0) or TLM (1) mode
class sct_register {};

Register has the following methods:

// Reset register, set it value to stored at last clock edge
void reset();
// Write new value to register
void write(const T& data);
// Read value stored at last clock edge
T read();
// To skip using read()
operator T ();

Register can initiate a new request. That means an output request can depend on register state.

sct_target<T>       targ{"targ"};
sct_register<T>     cntr{“cntr”};
explicit A(const sc_module_name& name) : sc_module(name) {
    targ.clk_nrst(clk, nrst);
    cntr.clk_nrst(clk, nrst);
    SC_METHOD(checkProc); sensitive << targ << cntr;
}

void checkProc() {
    cntr.reset();
    // Register accumulates received data up to N
    if (cntr.read() > N) {
        cntr.write(0); 
    } else 
    if (targ.get(data)) {
        cntr.write(cntr.read()+data); 
    }
}

Clock, clock gate and clock gate signal

sct_clock<> is implementation of clock source (generator) like sc_clock with enable/disable control.

    /// Enable clock activity, clock is enabled after construction 
    void enable();   
    /// Disable clock activity, can be called at elaboration phase to disable
    /// clock at simulation phase start
    void disable();    
    /// Register clock gate signals/ports to control clock activity.
    /// If any of the signals/ports is high, then clock is enabled
    void register_cg_enable(sc_signal_inout_if<bool>& enable);
    /// Get clock period    
    const sc_time& period() const;

Clock gate cell sct_clock_gate_cell and clock signal sct_clk_signal should be used together to connect clock input to gated clock source. sct_clk_signal is special signal without DC delay in written value becomes readable.

image

The code example illustrates using sct_clock_gate_cell and sct_clk_signal.

SC_MODULE(A) {
    sc_in_clk               SC_NAMED(clk);
    sc_in<bool>             SC_NAMED(nrst); 
    sc_in<bool>             SC_NAMED(clk_enbl);
    sct_clk_signal          SC_NAMED(clk_out);
    sct::sct_clk_gate_cell  SC_NAMED(clk_gate);
    sc_in<bool>             SC_NAMED(clk_in);

    explicit A(const sc_module_name& name) : sc_module(name) {
        clk_gate.clk_in(clk);        // Clock input
        clk_gate.enable(clk_enbl);   // Gate clock input 
        clk_gate.clk_out(clk_out);   // Gated clock output    
        clk_in(clk_out);
        
        SCT_THREAD(thrdProc, clk_in, nrst);   // Use clock input bound to gated clock
        async_reset_signal_is(nrst1, 0);
}};

Clock gate cells can be sequentially connected to each other, gated clock output of one cell bound to clock input of anther cell.

In TLM mode is all thread processes are created with SC_THREAD/SCT_THREAD macros, clock source(s) can be disabled. Disabling sct_clock allows to speed simulation:

sct_clock<>     clk{"clk", 1, SC_NS};
explicit A(const sc_module_name& name) : sc_module(name) {
    if (SCT_CMN_TLM_MODE) {
         clk.disable();
    }
}

Reset

Reset section

In thread process reset logic initializes registers, local variables and output signals. This logic should be placed in reset section (code scope before first wait()).

sct_out<T> o{"o"};
sct_signal<T> s{"s"};
void thrdProc() {
    // Reset section
    int a = 0;   // Local variable
    s = 0;       // Register 
    o = 0;       // Output 
    wait();
    while (true) {
        ...
        wait(); 
    } 
}

In method process initialization logic initializes local variables and output signals. This logic is normally be placed in the beginning of the process.

sct_out<T> o{"o"};
void methdProc() {
    // Initialization section
    int a = 0;   // Local variable
    o = 0;       // Output 
    ...
    a = i + 1;
    if (s) o = a;
}

Initialization logic in method process could be merged with its behavior logic based on inputs and registers. Such code style can have better simulation performance.

sct_in<T> i{"i"};
sct_out<T> o{"o"};
void methdProc() {
    int a = i+1;      // Local variable
    o = a ? s : 0;    // Output 
    ...
}

The communication channels also need to be reset with specified reset(), reset_get() and reset_put() methods. In thread process every channel used in this process should be initialized in the reset section.

sct_initiator<T>  init{"init"};
sct_target<T>     targ{"targ"};
sct_fifo<T, 2>    fifo{"fifo"};
void thrdProc() {
    init.reset();
    targ.reset();
    fifo.reset_put();  // If FIFO used for put
    fifo.reset_get();  // If FIFO used for get
    fifo.reset();      // If FIFO used for get and put both
    wait();
    while (true) {
        ...
        wait(); 
    } 
}

In method process every channel used in this process is initialized in the beginning of the process or assigned at all execution path in the process code. Having no explicit reset for registers, signals, output ports and synchronizers can improve simulation performance.

sct_initiator<T>  init{"init"};
sct_target<T>     targ{"targ"};
sct_register<T>   reg1{"reg1"};
sct_register<T>   reg2{"reg2"};
void methProc() {
    init.reset();
    reg1.reset();        
    T val = targ.get();  // targ is accessed at all path, no reset required
    if (val > 0) {
        reg1 = val;      // reg1 accessed at some paths only, reset required
        init.put(val);   // init accessed at some paths only, reset required
    }
    reg2 = val + 1;      // reg2 is accessed at all path, no reset required 
}

Reset control

Reset signal can be asserted/de-asserted in TB and DUT processes as well. To have the same simulation time in RTL and TLM modes it needs to follow the rules given in this section.

If reset control thread is in TB, it could control reset based on time period and be non-sensitive to any channels. In this case such a thread should be SC_CTHREAD in RTL mode and SC_THREAD in TLM mode. To avoid extra activation in TLM mode, this thread should wait for a specified time instead of clock events.

SC_MODULE(A) {
   SC_CTOR(A) {
       // Thread not sensitive to anything
       #ifdef SCT_TLM_MODE
          SC_THREAD(resetProc);
       #else
          SC_CTHREAD(resetProc, clk_in.pos());
       #endif
   }
   #define rstWait(N) if (SCT_CMN_TLM_MODE) wait(N, SC_NS); else wait(N);
   void resetProc() {
        nrst = 0; 
        rstWait(3);
        cout << sc_time_stamp() << " " << sc_delta_count() << " de-assert reset\n";
        nrst = 1; 
        rstWait(5);
        ...
   } 
};

If reset control thread is sensitive to any channels, it should be SCT_THREAD and have dont_initialize() in RTL mode. Such a thread can also be a normal test thread which provides stimulus and checks results:

SC_MODULE(A) {
   SC_CTOR(A) {
       // Thread sensitive to SingleSource channels
        SCT_THREAD(resetProc, clk);
        #ifndef SCT_TLM_MODE
            dont_initialize();
        #endif
        sensitive << s;
   }
   sct_signal<unsigned>  s{"s"};
   void resetProc() {
        nrst = 0; 
        while (s.read() < 3) {s = s.read()+1; wait();}
        cout << sc_time_stamp() << " " << sc_delta_count() << " de-assert reset\n";
        nrst = 1; 
   }

Specify clock edge and reset level

Clock edge and reset level normally are the same for the design. To update them for whole design SCT_CMN_TRAITS should be defined:

#define SCT_CMN_TRAITS SCT_NEGEDGE_POSRESET   // Set negative edge and positive reset level

To specify clock edge and reset level for individual library modules, template parameters should be used, for example:

sct_target<T, SCT_NEGEDGE_POSRESET>       run{"run"};
sct_initiator<T, SCT_POSEDGE_NEGRESET>    resp{"resp"};

Design architecture

Array/vector of SingleSource channels

Array of SingleSource channels can be implemented with sc_vector. First parameter of sc_vector is name, second parameter is number of elements (should be a compile time constant). To provide additional parameters to single source channels, it needs to use lambda function as third parameter of sc_vector.

static const unsigned N = 16;
using T = sc_uint<16>;
sc_vector<sct_target<T>>       targ{"targ", N};        // Two parameters 
sc_vector<sct_initiator<T>>    init{"init", N,         // Three parameters
      [](const char* name, size_t i) {                 // Lambda function         
           return sc_new<sct_initiator<T>>(name, 1);   // Initiator with sync register
      }}; 

Target and initiator in top module

Target and Initiator can be instantiated in top module to be connected to the correspondent modules in testbench. Such top module is synthesizable with input/output ports for the Target/Initiator instances.

Top module can contain Target which is not always ready and has no synchronous register. Top module can contain initiator which has no synchronous register. Top module cannot contain MultiTarget or MultiInitiator. Vector (sc_vector) of Target/Initiator in top module is supported. For synchronous register in a top module Target/Initiator externally connected to a testbench, ICSC reports the corresponding error.

image

To connect a testbench Target/Initiator to the correspondent top module Initiator/Target normal bind function is used always except multi-language simulation. For multi-language simulation if DUT is in SystemVerilog and testbench is in SystemC language, the simulation tool generates a special SystemC wrapper for DUT top module. To connect this wrapper to SystemC testbench SCT_BIND_CHANNEL macro should be used. SCT_BIND_CHANNEL macro cannot be applied to Target/Initiator with record type. For synchronous register in a testbench Target/Initiator connected to the DUT SCT_BIND_CHANNEL macro reports the corresponding error at elaboration phase of simulation.

// Include DUT module generated wrapper or SystemC header 
#ifdef RTL_SIM
    #include "DUT.h"          // Multi-language simulation, include generated wrapper
#else 
    #include "MyDut.h"        // SystemC simulation and synthesis, include designed header
#endif

template<class T>
class MyModule : public sc_module {
   DUT                       dut{"dut"}; 
   sct_target<T>             targ{"targ"};
   SC_CTOR(MyModule) {
       // Bind targ to init in dut module 
       #ifdef RTL_SIM
           SCT_BIND_CHANNEL(dut, init, targ);         // Multi-language simulation
       #else 
           targ.bind(dut.init);                       // SystemC simulation and synthesis
       #endif
   }
}

Array of Target/Initiator in top module

Array of Targets/Initiators supported in any module including top module. Instead of C++ array sc_vector should be used (C++ array is not supported). To bind the Targets/Initiators SCT_BIND_CHANNEL macro with 4 parameters is provided.

// Include DUT module generated wrapper or SystemC header 
#ifdef RTL_SIM
    #include "DUT.h"         // Multi-language simulation, include generated wrapper
#else 
    #include "MyDut.h"       // SystemC simulation and synthesis, include designed header
#endif

template<class T, unsigned N>
class MyModule : public sc_module {
   DUT              dut{"dut"}; 
   sc_vector<sct_target<T>>    targ{"targ", N};
   SC_CTOR(MyModule) {
       // Bind all elements of targ to elements of init in dut module 
       #ifdef RTL_SIM
           SCT_BIND_CHANNEL(dut, init, targ, N);   // Multi-language simulation
       #else 
           for (unsigned i = 0; i != N; ++i)  
               targ[i].bind(dut.init[i]);          // SystemC simulation and synthesis
       #endif
   }
}

Hierarchical connection of Target and Initiator

Target and Initiator can be connected through module hierarchy from child module up to parent module. That is possible explicitly or with sc_port of Initiator/Target.

There is an example of explicit binding Target to Initiator through module hierarchy:

template<class T>
struct Child : public sc_module {
    sct_target<T>       run{"run"};
};

template<class T>
struct Parent: public sc_module  {
    Child<T>           child{"child"};
    SC_CTOR(Parent) {}
};

SC_MODULE(Top)  {
    sct_initiator<T>    resp{"resp"};
    Parent<T>           parent{"parent"};
    SC_CTOR(Top) {
        parent.child.run.bind(resp);
}};

Ports (sc_port) of Target/Initiator contain pointer to them. To bind Initiator to Target through ports it needs to use get_instance() method which provides Target/Initiator from its port (see example below).

template<class T>
struct Child : public sc_module {
    sct_target<T>       run{"run"};
};

template<class T>
struct Parent: public sc_module  {
    sc_port<sct_target<T>>       run;   
    Child<T>                     child{"child"}; 
    SC_CTOR(Parent) {
        run(child.run);   // Bind port to child module initiator
}};

SC_MODULE(Top)  {
    sct_initiator<T>    resp{"resp"};
    Parent<T>           parent{"parent"};
    SC_CTOR(Top) {
        resp.bind(parent.run->get_instance());   // get_instance() provides Initiator from its sc_port  
}};

Process which calls Target/Initiator functions should be in the module where Target/Initiator declared. If a process calls Target/Initiator through its port (sc_port<sct_target>/sc_port<sct_initiator>) the process module and target initiator module should be synthesized in the same parent module.

Module interconnect with FIFO

Connection between modular interfaces inside of common parent module can be done with FIFO. One modular interface should have a FIFO instance and other modular interface should have a FIFO port (sc_port< sct_fifo<> >). The same can be done if the FIFO is instantiated in the parent module.

image

template<class T, unsigned N>
struct A : public sc_module, sc_interface {
   sct_fifo<T, N>    SC_NAMED(fifo);
   ...
};

template<class T, unsigned N>
struct B : public sc_module, sc_interface {
   sc_port<sct_fifo<T, N>>    SC_NAMED(fifo_port);
   ...
};

struct Parent : public sc_module{
   A<int, 3>   SC_NAMED(a);
   B<int, 3>   SC_NAMED(b);
   SC_CTOR(Parent) {
      b.fifo_port(a.fifo);    // Bind FIFO port to FIFO instance
   }
};

Cycle accurate and SingleSource code mix

Conventional cycle accurate design modules can be mixed with single source modules without limitations. sct_clock should be used instead of normal sc_clock.

Cycle accurate threads created with SC_CTHREAD macro are activated by clock event. Such processes can use SingleSource channels to communicate to each other and SingleSource threads created with SCT_THREAD macro. Cycle accurate processes should be sensitive to all the SingleSource channels used inside.

template<class T>
class MyModule : sc_module {
    sct_target<T>       in{"in"};
    sct_signal<T>       s{"s"};
    sct_fifo<T, 2>      fifo{"fifo"};

    MyModule(const sc_module_name& name) : sc_module(name) {
        SC_CTHREAD(threadProc, clk);
        sensitive << in << fifo.PUT << s;    // sensitivity to all used channels
        async_reset_signal_is(nrst, 0);
    }
    void threadProc() {
        in.reset_get();
        fifo.reset_put();
        wait();
        while (true) {
            if (in.request()) {
                fifo.put(s.read()); 
                in.get();
            }
            wait();
        }
    }
}

Instead of SC_CTHREAD special SCT_CTHREAD can be used. SCT_CTHREAD supports clock edge with third parameter:

SCT_CTHREAD(proc, clk, clk_edge);  // clk_edge could be 0 -- negedge, 1 -- posedge, 2 -- both edges
SCT_CTHREAD(proc, clk);            // clk_edge is SCT_CMN_TRAITS::CLOCK

Record as data type in SingleSource channels

Record is supported as data type in all SingleSource channels. The record should comply SystemC requirements for records used in signal/port: the record should have default constructor w/o parameters, operator==(), operator<<(std::ostream) and sc_trace() implemented.

struct Rec_t {
    bool enable;
    sc_uint<16> addr;
    // Default constructor
    Rec_t() : enable(false), addr(0) {}
    // Another constructor, optional
    Rec_t(bool enable_, sc_uint<16> addr_) : enable(enable_), addr(addr_) {}    
    bool operator == (const Rec_t& other) const {
        return (enable == other.enable && addr == other.addr && indx == other.indx);
    }
};
namespace std {
inline ::std::ostream& operator << (::std::ostream& os, const Rec_t& r) {
    os << r.enable << r.addr << r.indx; return os;}
}
namespace sc_core {
void sc_trace(sc_trace_file* , const Rec_t& , const std::string&) {}
}
...
sct_target<Rec_t>   run{"run"};
void methProc() {
   run.reset_get();
   if (run.request()) {
      Rec_t data = run.get();   // Get record fields from target
   }
}

See more examples at https://github.com/intel-innersource/frameworks.design.systemc.sct-common/tree/singlsrc_timed/test/records/

TLM mode

Difference between RTL and TLM simulations

RTL simulation provides precise result and is equivalent to simulation of the generated SV. TLM simulation is approximate time, i.e. channels in TLM mode have implementation optimized for speed. TLM FIFO is equivalent to RTL FIFO, so there is no difference. TLM Target/Initiator pair is equivalent to RTL only for default parameters (no always ready, no sync registers, no FIFO added). If always ready used, sync registers, or FIFO added, TLM Target/Initiator pair differs from RTL one. TLM Pipe is implemented as FIFO (sct_prim_fifo) that significantly differs from RTL implementation.

TLM mode can be used whenever exact time of simulation events is not important. TLM mode can be used for functional modelling such as hardware or software debugging. For performance modelling TLM mode can be used if 100% precise result is not required, otherwise RTL mode should be used.

TLM mode process sensitivity

If a design is used in TLM mode, it needs to ensure process sensitivity lists follow special rules. In TLM mode any process is activated by event notification from SingleSource channels and SystemC signals/ports. To catch an event notification, the correspondent process should be waiting for this event. That requires both of the following:

  1. All the events are added into process sensitivity list;
  2. Activated process goes to a next state where an event notification happens in some future.

To satisfy first condition is it enough to add all channels accessed in the process into process sensitivity list. The second condition is more tricky and requires analysis of all inter-process communications. Lets discuss the second condition in more details.

Process activation events can be notified by the process itself and by other processes (for sequential process only). Other process notifies the events independently or in response to our process actions. The means any process activation should notify enough events (no event could be enough) to get notification of its activation events back.

Initiator, Target and FIFO notifies the sequential or combinational process which performs put or get one more time. In the next example threadProc executes infinitely by self-notification. That allows the process puts to non-full FIFO multiple times w/o get from this FIFO.

Sequential process example.

// sct_single_fifo/method_test3.h
void threadProc() {
    fifo.reset_put();
    wait();
    while (true) {
       fifo.b_put(val);    // Activate this process again and again
       wait();
    }
}

Combinational process example.

// sct_single_fifo/method_test3.h
void methPutProc() {
    mfifo.reset_put();
    if (mfifo.ready()) mfifo.put(val);
}

void methGetProc() {
    mfifo.reset_get();
    // Do nothing to let @methPutProc fill the FIFO
}

In the following example there are two processes which activates one to each other through the FIFO. In producerProc the FIFO is accessed to notify the event for consumerProc in state 0 and 1. consumerProc is activated by the FIFO event and access (get) the FIFO which notifies producerProc back. In state 2 where no event is notified, so consumerProc is not activated and producerProc is not activated more -- simulation hangs up.

struct Top : public sc_module {
    sct_fifo<T, 2>      fifo{"fifo", 1};     // Pipelining register for request
    explicit Top(const sc_module_name& name) : sc_module(name) {
        fifo.clk_nrst(clk, nrst);
        SC_THREAD(producerProc); sensitive << fifo.PUT;   // Process puts to FIFO
        async_reset_signal_is(nrst, 0);
        SC_METHOD(consumerProc); sensitive << fifo.GET;   // Process gets from FIFO   
    } 
}

void producerProc() {
    fifo.reset_put();
    wait();      // STATE 0
    while (true) {
       if (fifo.ready()) {           
          fifo.put(getSomeVal());
       }
       wait();   // STATE 1
       // Do nothing, no event notified this cycle
       wait();   // STATE 2
    }
}
void consumerProc() {
    fifo.reset_get();
    if (fifo.request()) {
       doSomething(fifo.get());
    }
}

The next example demonstrates sequential process which activates itself with FIFO channel. Any put and get to/from the FIFO notifies the event to activate the process. In this process any value except 42 is put to the FIFO. If value is 42, nothing is put and no event is notified, so the process is not activated and simulation hangs up.

struct Top : public sc_module {
    sct_fifo<T, 5>      fifo{"fifo"};
    explicit Top(const sc_module_name& name) : sc_module(name) {
        fifo.clk_nrst(clk, nrst);
        SC_THREAD(storeProc); sensitive << fifo;  // Process puts and gets to FIFO
        async_reset_signal_is(nrst, 0);
    }
}

void storeProc() {
    fifo.reset();
    wait();
    while (true) {
       T val = getSomeValue();
       if (fifo.ready() && val != 42) {   
          fifo.put();
       } else {
          // Do nothing if @val is 42, no event notified this cycle
       }  
       wait();
       if (fifo.request()) {
          doSomething(fifo.get());
       }
    }
}

The next example shows how to create a counter process with signal channel only. Using sct_signal instead of sc_signal is required to avoid process is sensitive to it in RTL mode.

struct Top : public sc_module {
    sct_signal<T>      cnt{"cnt"};
    explicit Top(const sc_module_name& name) : sc_module(name) {
        SCT_THREAD(cntProc, clk.pos());     // Second parameter required for RTL mode to know clock edge 
        sensitive << cnt;                   // Process read and write signal
        async_reset_signal_is(nrst, 0);
    }
}

void cntProc() {
    cnt = 0;
    wait();
    while (true) {
       // Updating @cnt notifies the process itself 
       cnt = (cnt.read() != 255) ? cnt.read() + 1 : 0;
       wait();
    }
}

A typical mistake in TLM mode is waiting for negotiation of a channels ready or request. In the example below, if the FIFO is ready no event is notified. As soon as FIFO could be ready more than one cycle, that could leads to simulation hangs up.

void storeProc() {
    fifo.reset();
    wait();
    while (true) {      
       ...
       if (!fifo.ready()) { 
          // Do something that activates other processes
       } else {
          // Do nothing, no event notified this cycle
       }  
       wait();
    }
}

Joining multiple request and ready channel status in one condition normally is correct.

   ...
   if (fifo.ready() && init.request()) { 
       // Do something that activates other processes
   } else {
       // Do nothing, no event notified this cycle
   }  
   ...

As soon as timing diagram in TLM mode can differ from RTL mode, it is prohibited to process logic assumes the specific number of cycles. Instead of that, process logic and process interconnect should based on operations with channels. The following example has a magic wait which can lead to different result in RTL and TLM modes.

   ...
   if (fifo.ready()) {
      wait(3);       // Can lead to incorrect results
      fifo.get();
   }
   ...

Debug

Development and most of the debug is intended to be done in RTL mode. After RTL mode tests passed, TLM mode could be used for faster simulation. If TLM mode behavior differs from RTL mode, it could be debugged with C++ debugger or in commercial simulators.

To debug TLM mode in the simulation tool, EXTR_SIM_DEBUG option should be defined for syscan tool:

syscan ... -cflags "-DEXTR_SIM_DEBUG" ... 

EXTR_SIM_DEBUG option enables debug signals in sct_prim_fifo which implements target, initiator and FIFO in TLM mode. There are core_req, core_ready and core_data signals, similar to signals between target/initiator. sct_prim_register which implements register in TLM mode has curr_val which is value to be read. sct_prim_synchronizer which implements synchronizer in TLM mode has curr_val and next_val. curr_val is value to be read, next_val is just written value.

TLM mode simulation in the simulation tool can be used as normal SystemC simulation.