The Sparta Modeling Framework
Loading...
Searching...
No Matches
Communication, Events, and Scheduling

In addition to providing a simulation command line infrastructure (see Simulator Configuration) as well as resource creation (see resources) in a tree form for organization (see trees), Sparta provides a series of classes and ordering specifically dedicated for resource to resource communication as well as internal resource communication. This communication can be timed or untimed as desired.

The first point of Sparta's communication philosophy lies behind the timing aspects of Sparta: There are scheduling phases (sparta::SchedulingPhase) that allows a user to count on certain ordering between resources and another. This is different from prior versions of Sparta where every scheduleable type of event was in a single phase and required the user to provide explicit orderings.

Ordering within a phase, however, is still supported, allowing a modeler to specify the order in which events are scheduled/fired within that phase. For example, a modeler may desired eventA which reads from a Port to be scheduled before eventB which might act on the data read.

The classes that support communication and scheduling are:

The below sections detail each. To build the examples documented in this section, please see the README.md for directions/prerequisite for creating a build directory. Then, build in the example/Documentation/communication directory:

cd sparta/build-dir # Please see the README.md for creation
cd example/Documentation/communication
make
Macros for handling exponential backoff.

Ports

Ports are the mechanism in which a resource communicates a message to another resource (for example, via the method sparta::DataOutPort::send). The two resources do not know about each other and don't need to. Typically, during sparta::app::Simulation::finalizeTree (calls sparta::app::Simulation::bindTree_) phase, any sparta::Port derived classes that are constructed with a sparta::PortSet are bound together using sparta::bind(sparta::Port, sparta::Port) methods. An example of how this is done is found in details section of sparta::DataOutPort as well as the example core simulator's ExampleSimulator::buildTree_ (see Core Example Using Sparta) method (source found in example/CoreExample/src).

For the receiver of the data, the receiver would have to register a callback on the sparta::DataInPort via the sparta::DataInPort::registerConsumerHandler. This callback must be a member of the containing class or another persistent class in simulation.

Example of a device receiving data can be found in example/Documentation/communication/Ports_example.hpp:

#include <cinttypes>
#include <memory>
#define ILOG(msg) \
if(SPARTA_EXPECT_FALSE(info_logger_)) { \
info_logger_ << msg; \
}
//
// Basic device parameters
//
class MyDeviceParams : public sparta::ParameterSet
{
public:
MyDeviceParams(sparta::TreeNode* n) :
sparta::ParameterSet(n)
{
auto a_dumb_true_validator = [](bool & val, const sparta::TreeNode*)->bool {
// Really dumb validator
if(val == true) {
return true;
}
return false;
};
my_device_param.addDependentValidationCallback(a_dumb_true_validator,
"My device parameter must be true");
}
PARAMETER(bool, my_device_param, true, "An example device parameter")
};
//
// Example of a Device in simulation
//
class MyDevice : public sparta::Unit
{
public:
// Typical and expected constructor signature if this device is
// build using sparta::ResourceFactory concept
MyDevice(sparta::TreeNode * parent_node, // The TreeNode this Devive belongs to
const MyDeviceParams * my_params);
// Name of this resource. Required by sparta::ResourceFactory. The
// code will not compile without it
static const char * name;
private:
// A data in port that receives uint32_t
// The callback to receive data from a sender
void myDataReceiver_(const uint32_t & dat);
};
// Defined name
const char * MyDevice::name = "my_device";
// Implementation
// Construction
MyDevice::MyDevice(sparta::TreeNode * my_node,
const MyDeviceParams * my_params) :
sparta::Unit(my_node, name),
a_delay_in_(&unit_port_set_, "a_delay_in", 1) // Receive data one cycle later
{
// Tell SPARTA to ignore this parameter
my_params->my_device_param.ignore();
// Register the callback
a_delay_in_.
registerConsumerHandler(CREATE_SPARTA_HANDLER_WITH_DATA(MyDevice, myDataReceiver_, uint32_t));
}
// This function will be called when a sender with a DataOutPort
// sends data on its out port. An example would look like:
//
// a_delay_out.send(1234);
//
void MyDevice::myDataReceiver_(const uint32_t & dat)
{
ILOG("I got data: " << dat);
}
File that defines Data[In,Out]Port<DataT>
A set of sparta::Parameters per sparta::ResourceTreeNode.
#define PARAMETER(type, name, def, doc)
Parameter declaration.
File that defines the PortSet class.
#define CREATE_SPARTA_HANDLER_WITH_DATA(clname, meth, dataT)
Basic Node framework in sparta device tree composite pattern.
File that defines the Unit class, a common grouping of sets and loggers.
DataInPort receives data from sender using a DataOutPort.
Definition DataPort.hpp:289
Generic container of Parameters.
Node in a composite tree representing a sparta Tree item.
Definition TreeNode.hpp:205
The is the base class for user defined blocks in simulation.
Definition Unit.hpp:38

sparta::SyncPorts

To enable communication between components on different Clock boundaries, use sparta::SyncInPort and sparta::SyncOutPort. This type of port is identical to DataIn/OutPort with the exception that data sent on the out port is delayed until the "rising edge" of the receiver's clock. See sparta::SyncOutPort for more information.


Events

Events are mechanism to allow the scheduling of work based on activity within a resource. For example, the receiving of data from an external resource on a port might trigger the need to act upon that data and send it on. Take the previous example in the Port section and add an event based on the receiving of data (found example/Documentation/communication/Events_example.hpp).

#include <cinttypes>
#include <memory>
// Include Event.h
#define ILOG(msg) \
if(SPARTA_EXPECT_FALSE(info_logger_)) { \
info_logger_ << msg; \
}
class MyDeviceParams : public sparta::ParameterSet
{
public:
MyDeviceParams(sparta::TreeNode* n) :
sparta::ParameterSet(n)
{
auto a_dumb_true_validator = [](bool & val, const sparta::TreeNode*)->bool {
// Really dumb validator
if(val == true) {
return true;
}
return false;
};
my_device_param.addDependentValidationCallback(a_dumb_true_validator,
"My device parameter must be true");
}
PARAMETER(bool, my_device_param, true, "An example device parameter")
};
class MyDevice : public sparta::Unit
{
public:
MyDevice(sparta::TreeNode * parent_node,
const MyDeviceParams * my_params);
static const char * name;
private:
// A data in port that receives uint32_t
// The callback to receive data from a sender
void myDataReceiver_(const uint32_t & dat);
// An event to be scheduled in the sparta::SchedulingPhase::Tick
// phase if data is received
sparta::Event<> event_do_some_work_{&unit_event_set_, "do_work_event",
CREATE_SPARTA_HANDLER(MyDevice, doSomeWork_)};
// Method called by the event 'event_do_some_work_'
void doSomeWork_();
};
// Source
const char * MyDevice::name = "my_device";
MyDevice::MyDevice(sparta::TreeNode * my_node,
const MyDeviceParams * my_params) :
sparta::Unit(my_node, name),
a_delay_in_(&unit_port_set_, "a_delay_in", 1) // Receive data one cycle later
{
// Tell SPARTA to ignore this parameter
my_params->my_device_param.ignore();
// Register the callback
a_delay_in_.registerConsumerHandler(CREATE_SPARTA_HANDLER_WITH_DATA(MyDevice, myDataReceiver_, uint32_t));
}
// This function will be called when a sender with a DataOutPort
// sends data on its out port. An example would look like:
//
// a_delay_out.send(1234);
//
void MyDevice::myDataReceiver_(const uint32_t & dat)
{
ILOG("I got data: " << dat);
ILOG("Time to do some work this cycle: "
<< getClock()->currentCycle());
// Schedule doSomeWork_() for THIS cycle -- implicit precedence, BTW!
event_do_some_work_.schedule();
}
// Called from the scheduler; scheduled by the event_do_some_work_
// event.
void MyDevice::doSomeWork_() {
ILOG("Well, it's time to do some work. Cycle:"
<< getClock()->currentCycle());
}
File that defines the Event class.
#define CREATE_SPARTA_HANDLER(clname, meth)
Event is a simple class for scheduling random events on the Scheduler.
Definition Event.hpp:42
sparta::EventSet unit_event_set_
The Unit's event set.
Definition Unit.hpp:114

This isn't a very interesting class as myDataReceiver_() could easily just call doSomeWork_() directly. But, what if MyDevice had two inports that needed to be called before doing some work? That's no problem either. Let's extend the class, but this time adding a second port and changing event_do_some_work_ from a sparta::Event to a sparta::UniqueEvent to ensure it gets called only once when scheduled by both handlers. Code found in example/Documentation/communication/Events_dual_example.hpp.

#include <cinttypes>
#include <memory>
// Include Event.h
#define ILOG(msg) \
if(SPARTA_EXPECT_FALSE(info_logger_)) { \
info_logger_ << msg; \
}
class MyDeviceParams : public sparta::ParameterSet
{
public:
MyDeviceParams(sparta::TreeNode* n) :
sparta::ParameterSet(n)
{
auto a_dumb_true_validator = [](bool & val, const sparta::TreeNode*)->bool {
// Really dumb validator
if(val == true) {
return true;
}
return false;
};
my_device_param.addDependentValidationCallback(a_dumb_true_validator,
"My device parameter must be true");
}
PARAMETER(bool, my_device_param, true, "An example device parameter")
};
class MyDevice : public sparta::Unit
{
public:
MyDevice(sparta::TreeNode * parent_node,
const MyDeviceParams * my_params);
static const char * name;
private:
// A data in port that receives uint32_t from source 1
sparta::DataInPort<uint32_t> a_delay_in_source1_;
// The callback to receive data from the first sender
void myDataReceiverFromSource1_(const uint32_t & dat1);
// A data in port that receives uint32_t from a second source
sparta::DataInPort<uint32_t> a_delay_in_source2_;
// The callback to receive data from a second sender
void myDataReceiverFromSource2_(const uint32_t & dat2);
// An event to be scheduled if data is received, but it's unique!
// This means it can scheduled many times for a given cycle, but
// it's only called once. Also, this event in the
// SchedulingPhase::Tick phase with a delay of 0.
sparta::UniqueEvent<> event_do_some_work_{&unit_event_set_, "do_some_work_event",
CREATE_SPARTA_HANDLER(MyDevice, doSomeWork_)};
// Method called by the event 'event_do_some_work_'
void doSomeWork_();
// The data in question. DO NOT initialize
// The result after getting both data
uint32_t total_data_ = 0;
};
// Source
const char * MyDevice::name = "my_device";
MyDevice::MyDevice(sparta::TreeNode * my_node,
const MyDeviceParams * my_params) :
sparta::Unit(my_node, name),
a_delay_in_source1_(&unit_port_set_, "a_delay_in_source1", 1), // Receive data one cycle later
a_delay_in_source2_(&unit_port_set_, "a_delay_in_source2", 1) // Receive data one cycle later
{
// Tell SPARTA to ignore this parameter
my_params->my_device_param.ignore();
// Register the callbacks. These callbacks are called in the
// Port's SchedulingPhase::PortUpdate phase (which is before
// SchedulingPhase::Tick)
a_delay_in_source1_.registerConsumerHandler(
CREATE_SPARTA_HANDLER_WITH_DATA(MyDevice, myDataReceiverFromSource1_, uint32_t));
a_delay_in_source2_.registerConsumerHandler(
CREATE_SPARTA_HANDLER_WITH_DATA(MyDevice, myDataReceiverFromSource2_, uint32_t));
}
// This function will be called when a sender with a DataOutPort
// sends data on its out port. An example would look like:
//
// a_delay_out_source1_.send(1234);
//
void MyDevice::myDataReceiverFromSource1_(const uint32_t & dat)
{
ILOG("I got data from Source1: " << dat);
ILOG("Time to do some work this cycle: " << getClock()->currentCycle());
// Schedule doSomeWork_() for THIS cycle. Doesn't matter if data
// from Source2 is here yet. Since the event_do_some_work_ is in
// the SchedulingPhase::Tick phase, it will be scheduled for later
// in this cycle. No argument to schedule == 0 cycle delay.
event_do_some_work_.schedule();
sparta_assert(!data1_.isValid());
// Save the data
data1_ = dat;
}
// This function will be called when a sender with a DataOutPort
// sends data on its out port. An example would look like:
//
// a_delay_out_source2_.send(4321);
//
void MyDevice::myDataReceiverFromSource2_(const uint32_t & dat)
{
ILOG("I got data from Source2: " << dat);
ILOG("Time to do some work this cycle: " << getClock()->currentCycle());
// Schedule doSomeWork_() for THIS cycle. Since the
// event_do_some_work_ is in the SchedulingPhase::Tick phase, it
// will be scheduled for later in this cycle.
event_do_some_work_.schedule();
sparta_assert(!data2_.isValid());
// Save the data
data2_ = dat;
}
// Called from the scheduler; scheduled by the event_do_some_work_
// event.
void MyDevice::doSomeWork_() {
ILOG("Well, it's time to do some work. Cycle: " << getClock()->currentCycle());
sparta_assert(data1_.isValid() && data2_.isValid(),
"Hey, we didn't get data1 and data2 before"" this function was called!");
ILOG("Got these values: "
<< data1_.getValue() << " and "
<< data2_.getValue());
total_data_ = data1_.getValue() + data2_.getValue();
data1_.clearValid();
data2_.clearValid();
}
Set of macros for Sparta assertions. Caught by the framework.
#define sparta_assert(...)
Simple variadic assertion that will throw a sparta_exception if the condition fails.
A type of Event that uniquely schedules itself on the schedule within a single time quantum....
Provides a wrapper around a value to ensure that the value is assigned.

Clocks

Sparta's clocking and clock management are contained in sparta::Clock and sparta::ClockManager classes. The main difference to note in Sparta, is that sparta::Clock objects are not event-based objects nor do they constantly "clock" as simulation progresses (like in other frameworks). sparta::Clock objects are simply available to convert simulation time (from the sparta::Scheduler) to clock time based on that clock's frequency. Therefor, in simulation, sparta::Clock objects are passed around as constant objects. In addition, there is no notion of a "rising edge" nor a "falling edge" in simulation (however, the sparta::Clock supports this notion if absolutely needed). Instead, the Clocks are used to simply answer the question, what time (in NS) on the Scheduler should Event X be scheduled?

Using the sparta::ClockManager and the sparta::Clock classes are pretty straight-forward. By default the sparta::ClockManager contains a root clock that runs at the Scheduler frequency: 1 cycle == 1 NS. This is called the master clock (and denotes the "hypercycle"). From the master clock, more clocks can be created and then associated with sparta::TreeNode objects that Device objects hang off of:

sparta::RootTreeNode root_clks("clocks",
"Clock Tree Root",
root_node.getSearchScope());
sparta::Clock::Handle root_clk = cm.makeRoot(&root_clks);
sparta::Clock::Handle clk_1000 = cm.makeClock("clk_1000", root_clk, 1000.0);
sparta::Clock::Handle clk_100 = cm.makeClock("clk_100", root_clk, 100.0);
sparta::Clock::Handle clk_10 = cm.makeClock("clk_10", root_clk, 10.0);
root_tree_node_.setClock(root_clk.get()); // The root is at highest freq
// The device "runs" at 1GHz. I.e. it's events are scheduled at that
// time frame.
my_device_node_.setClock(clk_1000.get());
Manages building a clock tree.
Clock::Handle makeRoot(RootTreeNode *parent=nullptr, const std::string &name="Root")
Construct a root clock.
Clock::Handle makeClock(const std::string &name, const Clock::Handle &parent, const uint32_t &p_rat, const uint32_t &c_rat)
Create a new clock with a given ratio to a parent clock.
TreeNode which represents the root ("top") of a device tree.

Scheduling

sparta::Scheduler

The sparta::Scheduler class simply schedules callback methods for some time in the future. These callbacks are typically scheduled by sparta::Scheduleable class and its derivatives, but the sparta::scheduler is open to anyone who wishes to schedule a callback (but this is highly discouraged).

The callback type is sparta::SpartaHandler, a copyable method delegate that allows a user to specify a function of their class as a callback point. The callback form is expected to be of the following:

void func();

Time in the sparta::Scheduler can be interpreted anyway the user wishes, but the base unit is a sparta::Scheduler::Tick. For most simulation uses, the Tick is considered a PS of time. To convert a Tick to a higher-order unit such as a clock cycle, use a sparta::Clock made from a sparta::ClockManager to perform the conversions.

A typical flow for scheduling events is:

  1. Create an sparta::Scheduleable (or derivative sparta::Event, sparta::UniqueEvent, sparta::PayloadEvent) object with a given handler and a sparta::EventSet (that contains a sparta::Clock)
  2. Schedule the event using the event's schedule method, which will place the event on the sparta::Scheduler at the appropriate Tick using the sparta::Clock it got from the sparta::EventSet
  3. Wait for the sparta::Scheduler to get around to calling the method during the sparta::Scheduler::run call

The sparta::Scheduler is a sparta::RootTreeNode so that it can be seen in a global search scope, and loggers can be attached. For example, try this on the CoreExample:

<build-dir>/example/CoreExample/sparta_core_example -r 1000 -l scheduler debug 1

Or do this in your C++ code:

new sparta::log::Tap(&my_scheduler_, "debug", std::cout);
Logging Tap. Attach to a TreeNode to intercept logging messages from any NotificationSource nodes in ...
Definition Tap.hpp:28

Event Ordering

The sparta::Scheduler has a concept of "phased grouping" that allows a user to specify which callback they want called before another in time. Each SPARTA event type has an associated sparta::SchedulingPhase phase in its template parameter list that the event will always be placed in. In that phase, the event will always come before a "higher priority phase" and always after a "lower priority phase." But, within its assigned phase, the event will still be semi-random with respect to other events. It's "semi-random," meaning order will be indentical between simulation runs, but possibly different once the simulator is modified at the source-code level. This can be annoying.

Ordering within a phase is provided by a Direct Acyclic Graph or sparta::DAG. The DAG uses a class called sparta::Scheduleable that represents a position within the DAG and an ordering group within a SchedulingPhase. By default the sparta::Scheduleable is not in a group and is standalone within its assigned sparta::SchedulingPhase. Once a precedence between two sparta::Scheduleable objects is established, an ordering with assigned. This results in each sparta::Scheduleable being designated into an ordering group by the DAG. Event types (sparta::Event, sparta::UniqueEvent, sparta::PayloadEvent) provide this support. The developer can order an event type to precede another event, but only if the events are in the same SchedulingPhase:

sparta::EventSet my_ev_set;
// These events are created in the SchedulingPhase::Tick by default.
sparta::Event<> my_go_first_event(&my_ev_set, "my_go_first_event",
CREATE_SPARTA_HANDLER(MyClass, myFirstMethod));
sparta::Event<> my_go_second_event(&my_ev_set, "my_go_second_event",
CREATE_SPARTA_HANDLER(MyClass, mySecondMethod));
// Make myFirstMethod always get called before mySecondMethod.
// This can be done since both my_go events fall into the
// SchedulingPhase::Tick
my_go_first_event.precedes(my_go_second_event);
// Likewise, you can do this to set up precedence:
// my_go_first_event >> my_go_second_event;
sparta::Event<sparta::SchedulingPhase::Update> my_update_event(&my_ev_set_, "my_update_event",
CREATE_SPARTA_HANDLER(MyClass, myUpdateMethod));
// COMPILER ERROR! The update event is in a different phase than
// the my_go_first_event -- these events automatically happen in
// order.
my_update_event >> my_go_first_event;
Set of Events that a unit (or sparta::TreeNode, sparta::Resource) contains and are visible through a ...
Definition EventSet.hpp:26

The sparta::Scheduler is responsible for finalizing the DAG. The DAG is finalized when the sparta::Scheduler is finalized through the sparta::Scheduler::finalize method called by the framework. Therefor, all events can only be scheduled after the sparta::scheduler is finalized. It is illegal to schedule events before the dag is finalized because precedence has not been fully established. Any startup work can be scheduled via the sparta::StartupEvent class before sparta::Scheduler finalization.

The expected usage is something like:

sched.finalize();
producer.scheduleMyStuff(); //a method that puts events on the scheduler.
sched.run(100); // will run up to at most 99 ticks
// sched.run(100, true); // will run to exactly 99 ticks

sparta::SysCSpartaSchedulerAdapter

This class will allow a Sparta developer to interoperate a Sparta-based simulator with the SystemC kernel. The general rule of thumb is that the Sparta scheduler is either always equal to or 1 cycle ahead of the SystemC scheduler. The Sparta Scheduler will "sleep" waiting for SysC to catch up the next scheduled Sparta event.

There are two ways to stop simulation using this adapter:

  1. In SystemC, find the event SC_SPARTA_STOP_EVENT_NAME and notify it when SystemC is complete
  2. Register a sparta::Event via registerSysCFinishQueryEvent that is called by Sparta to query the SystemC side

There are some caveats to know about this adapter. See the todo.

Todo:
The Sparta scheduler is on its own SC_THREAD and is put to sleep between scheduled events. For example, if the Sparta scheduler has an event scheduled @ tick 1000, and time is currently 500, the Sparta scheduler thread will wait() for 500 ticks. However, if a SystemC component puts an event on the Sparta scheduler during this sleep window (say at 750 ticks), we do not have a mechanism to wake this thread early.