This chapter describes the required steps for generating C code with itemis CREATE. Furthermore, all components of the generated code will be described in detail and each configurable generator feature will be explained.
Table of content:
We will use the following example to explain the code generation, the generated code and the integration with client code. The example model describes a light switch with two states and the following behavior:
The ‘C Code Generation’ example can be found in the example wizard:
File ->
New ->
Example... ->
CREATE Statechart Examples ->
Getting Started – Code Generation ->
C Code Generation
Statechart example model
Generating C code from a statechart requires a generator file (.sgen). It must at least specify the
create::c generator, reference a statechart and define the
targetProject and
targetFolder in the
Outlet feature. By specifying these attributes, C state machine code can be generated.
Example:
GeneratorModel for create::c {
statechart LightSwitch {
feature Outlet {
targetProject = "org.yakindu.sct.examples.codegen.c"
targetFolder = "src-gen"
}
}
}
You can create a generator model with the CREATE Statechart generator model wizard by selecting File → New → Code generator model.
The code generation is performed automatically whenever the statechart or the generator file is modified. See also chapter Running a generator for more information.
Generated code files can be categorized into base, api and library files.
The base source file is the implementation of the state machine model. It is generated into the folder defined by the targetFolder parameter. The file name is derived from the statechart name, but can be overridden by the moduleName parameter.
API files are the header files that expose the state machine’s API. They are generated into the apiTargetFolder or, if that one is not defined, into the targetFolder .
Library files are independent of the concrete state machine model. They are generated into the libraryTargetFolder , or if that one is not defined, into the targetFolder .
The generated header file (here LightSwitch.h) contains fundamental functions to initialize, enter, and exit a state machine.
In the header file, the function names are made up of the state machine name followed by the name of the respective functionality. For the light switch example, these functions are generated as follows:
/*! Initializes the LightSwitch state machine data structures. Must be called before first usage.*/
extern void lightSwitch_init(LightSwitch* handle);
/*! Activates the state machine. */
extern void lightSwitch_enter(LightSwitch* handle);
/*! Deactivates the state machine. */
extern void lightSwitch_exit(LightSwitch* handle);
State machines that use the cycle-based execution scheme will additionally expose a runCycle() method:
/*! Performs a 'run to completion' step. */
extern void lightSwitch_run_cycle(LightSwitch* handle);
The
run_cycle() method is used to trigger a run-to-completion step in which the state machine evaluates arising events and computes possible state changes. For event-driven statecharts, this method is called automatically when an event is received. For cycle-based statecharts, this methods needs to be called explicitly in the client code. See also chapter
Execution schemes. Somewhat simplified, a run-to-completion cycle consists of the following steps:
Furthermore, the header file declares methods to check whether the state machine is active, final or whether a specific state is active:
/*!
* Checks whether the state machine is active.
* A state machine is active if it was entered. It is inactive if it has not been entered at all or if it has been exited.
*/
extern sc_boolean lightSwitch_is_active(const LightSwitch* handle);
/*!
* Checks if all active states are final.
* If there are no active states then the state machine is considered being inactive. In this case this method returns false.
*/
extern sc_boolean lightSwitch_is_final(const LightSwitch* handle);
/*! Checks if the specified state is active. */
extern sc_boolean lightSwitch_is_state_active(const LightSwitch* handle, LightSwitchStates state);
false
.
The client code can read and write state machine variables and raise state machine events. The getters and setters for each variable and event are also contained in the header file. The function names are matching the following pattern:
[
statechart_name]
_
[
interface_name]
_
[
set
|
get
|
raise
]
_
[
variable_name |
event_name]
A statechart can also define an unnamed interface. In this case, the interface name is ommitted from the function name.
For the example model above, the two statechart interfaces user and light are transformed into the following API functions:
/*! Gets the value of the variable 'brightness' that is defined in the interface scope 'light'. */
extern sc_integer lightSwitch_light_get_brightness(const LightSwitch* handle);
/*! Sets the value of the variable 'brightness' that is defined in the interface scope 'light'. */
extern void lightSwitch_light_set_brightness(LightSwitch* handle, sc_integer value);
/*! Raises the in event 'on_button' that is defined in the interface scope 'user'. */
extern void lightSwitch_user_raise_on_button(LightSwitch* handle);
/*! Raises the in event 'off_button' that is defined in the interface scope 'user'. */
extern void lightSwitch_user_raise_off_button(LightSwitch* handle);
/*! Returns the observable for the out event 'on' that is defined in the interface scope 'light'. */
extern sc_observable* lightSwitch_light_get_on(LightSwitch* handle);
/*! Returns the observable for the out event 'off' that is defined in the interface scope 'light'. */
extern sc_observable* lightSwitch_light_get_off(LightSwitch* handle);
In this example code you can see the following:
Bringing it all together, the state machine header file declares the following API functions for the light switch example:
extern void lightSwitch_init(LightSwitch* handle);
extern void lightSwitch_enter(LightSwitch* handle);
extern void lightSwitch_exit(LightSwitch* handle);
extern void lightSwitch_raise_time_event(LightSwitch* handle, sc_eventid evid);
extern void lightSwitch_user_raise_on_button(LightSwitch* handle);
extern void lightSwitch_user_raise_off_button(LightSwitch* handle);
extern sc_integer lightSwitch_light_get_brightness(const LightSwitch* handle);
extern void lightSwitch_light_set_brightness(LightSwitch* handle, sc_integer value);
extern sc_observable* lightSwitch_light_get_on(LightSwitch* handle);
extern sc_observable* lightSwitch_light_get_off(LightSwitch* handle);
extern sc_boolean lightSwitch_is_active(const LightSwitch* handle);
extern sc_boolean lightSwitch_is_final(const LightSwitch* handle);
extern sc_boolean lightSwitch_is_state_active(const LightSwitch* handle, LightSwitchStates state);
The following code snippet demonstrates how to use the state machine API:
/*! Instantiates the state machine */
LightSwitch lightSwitch;
/*! Initializes the state machine, in particular all variables are set to a proper value */
lightSwitch_init(&lightSwitch);
/*! Enters the state machine; from this point on the state machine is ready to react on incoming event */
lightSwitch_enter(&lightSwitch);
/*! Raises the On event in the state machine which causes the corresponding transition to be taken */
lightSwitch_user_raise_on_button(&lightSwitch);
/*! Prints the value of the brightness variable */
printf("Brightness: %d.\n", lightSwitch_light_get_brightness(&lightSwitch));
/*! Exit the state machine */
lightSwitch_exit(&lightSwitch);
There are basically two ways to access outgoing events, getters and observables. The desired option can be enabled in the generator model, see OutEventAPI .
The getter mechanism is straight forward and simply allows to check if an outgoing event is raised by calling an is_raised function that returns a boolean:
/*! Checks if the out event 'on' that is defined in the interface scope 'light' has been raised. */
extern sc_boolean lightSwitch_light_is_raised_on(const LightSwitch* handle);
/*! Checks if the out event 'off' that is defined in the interface scope 'light' has been raised. */
extern sc_boolean lightSwitch_light_is_raised_off(const LightSwitch* handle);
The observable mechanism is more complex to set up, but it allows to get notified whenever the event is raised. Thus, the client code does not need to check the event status explicitly. The client code basically passes on a pointer to a function that is called when the outgoing event is raised.
First of all, we need to specify the callback function:
/*! This function will be called by raising the out event light.on */
static void on_light_on(LightSwitch *o) {
printf("Light is on.\n");
}
Then, we need to instantiate an observer, initialize it with the callback function, and subscribe it to the out event observable:
/*! Instantiate observer for the out events */
sc_single_subscription_observer lightOnObserver;
/*! Initialize the observer with the callback function
sc_single_subscription_observer_init(lightOnObserver, lightSwitch, (sc_observer_next_fp) on_light_on)
/*! Subscribe the observer to the out event observable */
sc_single_subscription_observer_subscribe(lightOnObserver, &lightSwitch->ifaceLight.on);
With that code, our
on_light_on() function will be called whenever the out event
on in interface
light is raised by the state machine.
itemis CREATE support operations that are executed by a state machine as actions, but are implemented by client-side code.
As a simple example a function myOp can be defined in the definition section of the LightSwitch example:
interface:
operation myOp()
For state machines that define operations in their interface(s), the code generator adds corresponding functions to the
required.h header file (in our example
LightSwitch_required.h):
extern void lightSwitch_myOp( LightSwitch* handle);
This function has to be implemented and linked with the generated code, so that the state machine can use it.
If a statechart uses timing functionality or external operations, or tracing is activated in the generator model, an additional header file is generated. Its name matches the following pattern:
[
statechart_name]
_required.h
This header file declares functions the client code has to implement externally.
The light switch example model is such a time-controlled state machine as it uses an after clause at the transition from state On to state Off. Conseuquently, the code generator produces the LightSwitch_required.h header file which declares two function to set and unset a timer:
/*! This function has to set up timers for the time events that are required by the state machine. */
/*!
This function will be called for each time event that is relevant for a state when a state will be entered.
\param evid An unique identifier of the event.
\time_ms The time in milliseconds
\periodic Indicates the the time event must be raised periodically until the timer is unset
*/
extern void lightSwitch_set_timer(LightSwitch* handle, const sc_eventid evid, const sc_integer time_ms, const sc_boolean periodic);
/*! This function has to unset timers for the time events that are required by the state machine. */
/*!
This function will be called for each time event that is relevant for a state when a state will be left.
\param evid An unique identifier of the event.
*/
extern void lightSwitch_unset_timer(LightSwitch* handle, const sc_eventid evid);
Basically the proper time handling has to be implemented by the developer, because timer functions generally depend on the hardware target used. So for each hardware target the client code must provide a function to set a timer and another function to unset it. These functions have to be implemented externally and have to be linked to the generated code.
A state machine calls the set_timer() function to tell the timer service that it has to start a timer for the given time event identifier and raise it after the period of time specified by the time_ms parameter has expired. It is important to only start a timer thread or a hardware timer interrupt within the set_timer() function and avoid any time-consuming operations like extensive computations, sleeping or waiting. Never call the statechart API functions from within these functions! Otherwise the state machine execution might hang within the timer service or might not show the expected runtime behavior.
If the parameter periodic is false, the timer service is supposed to raise the time event only once. If periodic is true, the timer service is supposed to raise the time event every time_ms milliseconds.
The state machine calls the function unset_timer() to notify the timer service to unset the timer for the given event ID.
In order to notify the state machine about the occurence of a time event after a period of time has expired, the raise_time_event() function – defined in the header file of the state machine – needs to be called on the state machine. In the case of the light switch example it is named lightSwitch_raise_time_event(LightSwitch* handle, sc_eventid evid) (in file LightSwitch.h).
The time event is recognized by the state machine and will be processed during the next run cycle.
You can conclude that in order to process the time events raised by the timing service without too much latency, the runtime environment has to call the state machine’s run_cycle() function as frequently as needed. Consider for example a time event which is raised by the timer service after 500 ms. However, if the runtime environment calls the state machine’s run_cycle() function only once per 1000 ms, the event will quite likely not be processed at the correct points in time.
The C code generator can create a default implementation of the timer service (software timer).
To generate the default timer service class, set the timerService feature in the generator model to true. Example:
GeneratorModel for create::c {
statechart LightSwitch {
/* … */
feature GeneralFeatures {
timerService = true
timerServiceTimeType = ""
}
}
}
The default timer service consists of a header file named
sc_timer_service.h and a corresponding source file
sc_timer_service.c. The header file defines the timer service API implemented by the source file:
/*! Initializes a timer service with a set of timers. */
extern void sc_timer_service_init(sc_timer_service_t *tservice, sc_timer_t *timers, sc_integer count, sc_raise_time_event_fp raise_event);
/*! Updates all timers. */
extern void sc_timer_service_proceed(sc_timer_service_t *timer_service, const sc_integer time_ms);
/*! Starts a timer with the specified parameters. */
extern void sc_timer_set(sc_timer_service_t *timer_service, void *handle, const sc_eventid evid, const sc_integer time_ms, const sc_boolean periodic);
/*! Cancels a timer for the specified time event. */
extern void sc_timer_unset(sc_timer_service_t *timer_service, const sc_eventid evid);
The
init() function intializes the timer service with a data structure that holds the individual timers, and a pointer to the state machine’s
raise_time_event() function. In order to start or cancel a timer, the functions
sc_timer_set() and
sc_timer_unset() can be invoked. However, the timer service does not know anything about the current time. Its internal clock needs to be pushed forward manually by calling the
sc_timer_service_proceed() function. This function checks if an already started timer is expired, and if so, raises the corresponding time event on the state machine.
The following code snippet demonstrates how the timer service can be used:
#include <sys/time.h>
#include "../src-gen/LightSwitch.h"
#include "../src-gen/sc_timer_service.h"
/* ! As we make use of time triggers (after & every)
* we make use of a generic timer implementation
* and need a defined number of timers. */
#define MAX_TIMERS 4
//! We allocate the desired array of timers.
static sc_timer_t timers[MAX_TIMERS];
//! The timers are managed by a timer service. */
static sc_timer_service_t timer_service;
unsigned long current_time = 0;
unsigned long last_time = 0;
unsigned long elapsed_time = 0;
unsigned long get_ms() {
struct timeval tv;
unsigned long ms;
gettimeofday(&tv, 0);
ms = tv.tv_sec * 1000 + (tv.tv_usec / 1000);
return ms;
}
int main(int argc, char **argv) {
/*! Instantiates the state machine */
LightSwitch lightSwitch;
/*! Initializes the timer service */
sc_timer_service_init(&timer_service, timers, MAX_TIMERS,
(sc_raise_time_event_fp) &lightSwitch_raise_time_event);
/*! Initializes the state machine, in particular all variables are set to a proper value */
lightSwitch_init(&lightSwitch);
/*! Enters the state machine; from this point on the state machine is ready to react on incoming event */
lightSwitch_enter(&lightSwitch);
/*! Application loop */
last_time = get_ms();
while (1) {
current_time = get_ms();
elapsed_time = current_time - last_time;
last_time = current_time;
/*! push the timer service clock forward */
sc_timer_service_proceed(&timer_service, elapsed_time);
// interact with the state machine, raise events etc..
}
return 0;
}
/*! This function will be called for each time event in LightSwitch when a state is entered. */
void lightSwitch_set_timer(LightSwitch *handle, const sc_eventid evid, const sc_integer time_ms, const sc_boolean periodic) {
sc_timer_set(&timer_service, handle, evid, time_ms, periodic);
}
/*! This function will be called for each time event in LightSwitch when a state will be left. */
void lightSwitch_unset_timer(LightSwitch *handle, const sc_eventid evid) {
sc_timer_unset(&timer_service, evid);
}
The generated state machine code will never allocate any memory. Allocating the memory needed for the statechart structure is a responsibility of the client code. This also includes dimensioning the size of the in event queues for event driven state machines.
Beside the general code generator features, there are language specific generator features, which are listed in the following chapter.
The Outlet feature specifies target project and target folder for the generated artifacts. It is a required feature and has the parameters as described in Outlet feature .
The C code generator extends this feature by the following parameter:
Api Target Folder
Example:
apiTargetFolder = "api-gen"
The IdentifierSettings feature allows the configuration of module names and identifier character length.
Example:
feature IdentifierSettings {
moduleName = "MyStatechart"
statemachinePrefix = "myStatechart"
separator = "_"
headerFilenameExtension = "h"
sourceFilenameExtension = "c"
}
Please note that the maxIdentifierLength option, which existed in older versions of itemis CREATE, has been removed in favor of a statechart annotation that is only available in the C/C++ domain, see @ShortIdentifiers.
Module Name
Statemachine Prefix
Separator
Header Filename Extension
Source Filename Extension
The GeneratorOptions feature allows the configuration of additional aspects concerning the behavior of the generated code.
Example:
feature GeneratorOptions {
userAllocatedQueue = true
metaSource = true
}
User Allocated Queue
Meta Source
The Tracing feature enables the generation of tracing callback functions.
Example:
feature Tracing {
enterState = true
exitState = true
generic = true
}
Enter State
Exit State
Generic
The YET Tracer is a C generator extension, which allows the use of the YET Tracing run option. The generated C code implements the callback funtions, which fit into the generated architecture of the generic tracing feature. To use the YET Tracer, please also supply a C generator with generic Tracing feature enabled.
GeneratorModel for create::c::yet {
...
}
The Includes feature allows to change how include statements are generated.
Example:
feature Includes {
useRelativePaths = false
generateAllSpecifiedIncludes = true
}
Use Relative Paths
Generate all Specified Includes
The GeneralFeatures feature allows to configure additional services to be generated along with the state machine. Per default, all parameters are false, meaning to disable the corresponding features, respectively.
GeneralFeatures is an optional feature.
Example:
feature GeneralFeatures {
timerService = true
timerServiceTimeType = ""
}
Timer Service
Timer Service Type