Analysis of SystemC Framework Principles

Basic structure

Unlike typical third-party libraries, SystemC (abbreviated as SC below) provides a main() function, and by default it is a strong symbol (the weak here was added by me later). This means users cannot define their own main(). Therefore, from the very beginning, SC was not designed to build an API for users to call, but rather as an intrusive asynchronous programming framework, allowing users to write code in a style close to RTL. The core initialization code of SC is as follows:

// sc_main.cpp
/* __attribute__((weak)) */ int
main( int argc, char* argv[] )
{
   return sc_core::sc_elab_and_sim( argc, argv );
}
// ...
int
sc_elab_and_sim( int argc, char* argv[] )
{
    // ...
    try
    {
        pln();
        // Perform initialization here
        sc_in_action = true;
        status = sc_main( argc, &argv_call[0] );
        // Perform cleanup here
        sc_in_action = false;
    }
    catch( ... )
    {
        // ...
    }
}

SC’s approach is to take over the executable entry point main(), requiring users to write their own logic in sc_main() to construct the corresponding hardware module objects, and then explicitly call sc_start() to start the event loop. The event loop will end after the specified time, or more gracefully, the process will naturally exit once all events have been handled. A simple example is as follows:

#include <iostream>
#include <systemc>
SC_MODULE (hello_world) {
  SC_CTOR (hello_world) {
    SC_METHOD(say_hello);
  }
  void say_hello() {
    std::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");
  sc_core::sc_start();
  return 0;
}

This default usage scenario constrains us to compile everything into a single executable, which in turn means it can only run certain cases based on file interfaces. In fact, this idea is very similar to the hardware simulation flow, because SC was originally developed by Synopsys...

Event loop

In essence, the event loop in the SC framework is the process of responding to events and continuously advancing along the time axis. First, here is a concrete example, which will be explained later in combination with the framework code. The following is an SC implementation of a two-input AND gate:

#include <iostream>
#include <systemc>
using namespace sc_core;
SC_MODULE(and2)
{
  sc_in<bool> A, B;       // input signals
  sc_out<bool> F;         // output signal
  void do_and2() { F.write( A.read() && B.read() ); }
  SC_CTOR(and2)           // constructor
  {
    SC_METHOD(do_and2);   // register the corresponding class method as SC_METHOD, i.e., register the method with the SystemC kernel
    sensitive << A << B;  // equivalent to declaring always @(*): automatically call the corresponding method when a signal changes
  }
};
SC_MODULE(monitor) {
  sc_in<bool> clk;
  sc_in<bool> F;
  SC_CTOR(monitor) {
    SC_THREAD(thread);
    sensitive << clk.neg();
  }
  void thread() {
    while (true) {
      std::cout << sc_time_stamp() << ": " << F.read() << std::endl;
      wait();
    }
  }
};
SC_MODULE(testbench)
{
  sc_in<bool> clk;
  sc_out<bool> A, B;
  void thread()
  {
    for (auto& p: {std::make_pair(false, false), std::make_pair(false, true),
                   std::make_pair(true, false), std::make_pair(true, true)}) {
      A.write(p.first);
      B.write(p.second);
      wait();
    }
    sc_stop();
  }
  SC_CTOR(testbench)
  {
    SC_THREAD(thread);
    sensitive << clk.pos();
  }
};
// sc_main in top level function like in C++ main
int sc_main(int argc, char* argv[]) {
  sc_clock clk("Clock", 1, SC_NS, 0.5, 0.0, SC_NS);
  sc_signal<bool> A, B, F;
  and2 a("And");
  testbench tb("TestBench");
  monitor mon("Monitor");
  a << A << B << F;
  mon << clk << F;
  tb << clk << A << B;
  sc_start();
  return 0;
}

Time update

After calling sc_start(), following a series of initialization steps, SC eventually enters sc_simcontext::simulate(), which contains the real main event loop. The main event loop’s primary job is to manage events and the timeline—the simulator’s timeline gradually moves forward as events occur.

The core code in the main event loop is as follows:

bool
sc_simcontext::next_time( sc_time& result ) const
{
    while( m_timed_events->size() ) {
   sc_event_timed* et = m_timed_events->top();
   if( et->event() != 0 ) {
       result = et->notify_time();
       return true;
   }
   delete m_timed_events->extract_top();
    }
    return false;
}
void
sc_simcontext::do_timestep(const sc_time& t)
{
    // ...
    m_curr_time = t;
    m_change_stamp++;
    m_initial_delta_count_at_current_time = m_delta_count;
}
void
sc_simcontext::simulate( const sc_time& duration )
  do {
    // handle all events produced by SC_METHOD and SC_THREAD at the current time point
    crunch();
    // ...
    // next, prepare to advance the timeline, i.e., update the timestamp m_curr_time
    t = m_curr_time;
    do {
      // call next_time() to retrieve the timestamp corresponding to the next event
      if ( !next_time(t) || (t > until_t) ) {
          if ( (t > until_t) || m_prim_channel_registry->async_suspend() ) {
              // requested simulation time completed or no external updates
              goto exit_time;
          }
          // received external updates, continue simulation
          break;
      }
      // update m_curr_time to the next timestamp
      if ( t > m_curr_time ) do_timestep(t);
      // now that the current time has been updated, trigger all events that should occur at this time
      do {
        sc_event_timed* et = m_timed_events->extract_top();
        sc_event* e = et->event();
        delete et;
        if( e != 0 ) {
          e->trigger();
        }
      } while( m_timed_events->size() &&
               m_timed_events->top()->notify_time() == t );
      // if after the time update no runnable events are produced, the timeline can continue to move forward
    } while( m_runnable->is_empty() );
  } while ( t < until_t ); // hold off on the delta for the until_t time.
  // ...
}

In principle, the most straightforward way to manage discrete events on a timeline is to use a priority queue to maintain the timestamps corresponding to a series of events, and then pop events from it for execution. In fact, SC is implemented exactly this way. From the specific implementation of the next_time(t) method that updates the timestamp, it is clear that it updates the current time to the time point corresponding to the next event in the priority queue, and then triggers the event associated with that time point to see whether runnable events can be produced.

Event handling

As mentioned above, at each time point, the main event loop calls sc_simcontext::crunch() to process all runnable tasks (i.e., all ready SC_METHOD and SC_THREAD), thereby updating various events and states at the current time. This method is also running a small event loop, but during this loop the timeline does not advance. In other words, when SC handles process events, it is only responsible for executing combinational logic, while sequential logic can be implemented by combinational logic in combination with wait() calls.

The event loop inside sc_simcontext::crunch() can roughly be divided into three phases, and its core code is as follows:

inline void
sc_simcontext::crunch( bool once ) {
  while ( true ) {
    // EVALUATE PHASE
    // ...
    while( true ) {
        // execute all SC_METHOD
        m_runnable->toggle_methods();
        sc_method_handle method_h = pop_runnable_method();
        while( method_h != 0 ) {
            empty_eval_phase = false;
            if ( !method_h->run_process() ) { goto out; }
            method_h = pop_runnable_method();
        }
        // execute all SC_(C)THREAD; the logic is similar and omitted
        // ...
        // executed tasks may trigger new tasks and put them into the ready list; but if not, exit the inner loop.
        if( m_runnable->is_empty() ) { break; }
    }
    // UPDATE PHASE
    // here it actually calls a member function of a global registry to update the state of all sc_signal
    // this process will produce some triggerable events and attach them to m_delta_events
    m_prim_channel_registry->perform_update();
    SC_DO_PHASE_CALLBACK_(update_done);
    // ...
    // NOTIFICATION PHASE:
    // if there are events to trigger, trigger them
    int size = m_delta_events.size();
    if ( size != 0 ) {
        sc_event** l_events = &m_delta_events[0];
        int i = size - 1;
        // trigger all events in reverse order; at this point new tasks will be put into the ready list m_runnable
        do { l_events[i]->trigger(); } while( -- i >= 0 );
        m_delta_events.clear();
    }
    // if after the NOTIFICATION phase completes, no more tasks can be run, exit the loop
    if( m_runnable->is_empty() ) { break; }
    // ...
  }
}
  1. EVALUATE: Execute all tasks that can currently run in the ready queue, until no tasks can run. In software, a “task” corresponds to all non-blocked user-space threads and triggered SC_METHOD callbacks.
  2. UPDATE: Update all signals (sc_signal) whose data changed during the evaluation phase, and push their corresponding events (sc_event) into a queue m_delta_events.
  3. NOTIFICATION: Trigger the events in the queue. At this time, due to callbacks produced by the events, new tasks become runnable; these tasks are added to the ready queue, and then the flow returns to step 1 to re-execute runnable logic. If no new tasks can be executed, the event loop exits.

During system initialization—i.e., in the context of user-written sc_main()—when executing the constructor of and2, the system registers the do_and2() function, producing an sc_method_process object. This object is essentially a user-space thread descriptor, and it will be placed into the ready task queue waiting to run. After sc_start() begins, all user-space threads in the ready list start running in a random order: all SC_METHOD are executed once by default, and all SC_THREAD are triggered to start at the appropriate time according to their sensitive lists, after which they keep executing their own loops.

At some moment, for example at the beginning of 1ns, the event generation and handling process in the whole system is as follows:

  1. In the main event loop, due to the advancement of the timeline, the SC_THREAD in testbench finishes its wait() and becomes active, so this task is added to the ready list awaiting execution.
  2. In the evaluation phase of sc_simcontext::crunch(), the ready task list is traversed and all runnable tasks are executed—namely, this SC_THREAD.
  3. Executing this task causes two sc_signals, A and B, to change. Therefore, in the update phase the corresponding signal data is updated; during this process, the events (sc_event) corresponding to these changed signals are placed into a queue named m_delta_events.
  4. In the notification phase, the m_delta_events queue is checked, and all events in it are triggered (trigger()) in reverse order.
  5. At this point, because the signal update triggers the SC_METHOD in and2, it becomes runnable and is added to the ready queue. This happens at the current moment, corresponding to combinational logic.
  6. After the notification phase ends, the first round of the loop ends. The ready queue is found to be non-empty, so the flow returns to the evaluation phase to execute this SC_METHOD.
  7. After the SC_METHOD finishes, it modifies the signal F. But since no task is listening to this signal, no new runnable tasks are produced. The loop ends here.

As can be seen, the SC event-loop kernel is a very minimal implementation. At the very bottom of the framework, only two kinds of events cause user-space threads to be suspended: wait() calls, and waiting for sc_event events—and wait() calls are essentially implemented on top of sc_event. Therefore, based on the primitives provided at the lower layer, SC wraps a full set of upper-layer frameworks with very thorough decoupling between layers, thereby enabling feature-rich interface components; and by leveraging the strong expressive power of C++, it is suitable for hardware modeling at various levels.

comments powered by Disqus
Published:
2018-08-15
Category:
Tag: