thinStates C Compiler for StateWORKS
Introduction
The thinStates C Compiler for StateWORKS offers a “thin” way to Finite State Machine software design with the StateWORKS SWStudio.
The generated C source code, models the State Machines’ logic and interconnections with the other objects.
At the moment, there is no “standard library” for all the external objects (e.g. Counters, Timers etc.), but an example is given for the very basic useful objects: Commands, Timers, Digital Inputs, Digital Outputs, Output Functions.
The lack of such a standard library also means there is no standard configuration support for the external objects: for example, in no way the “interval” setting of timers is reflected in the generated C code.
Generation Technique
The key concept in thinStates code generation, is the generation of a .c
file for each VFSM instance.
For example, if we define just only one State Machine in SWStudio (e.g. Button) and then we instantiate it two times (e.g. Button_1 and Button_2), we get two .c
files: swc_vfsm_Button_1.c
and swc_vfsm_Button_2.c
.
In fact, the two .c
files look very similar, but the object names they refer to are different: each one statically refers to the objects that that instance is connected to, in SWStudio.
In our experience, this approach produces the most human-friendly (and fast, as well) source code: it is simple to read, and simple to debug: we can put a breakpoint when a particular instance of state machine changes its state, and inspect the relevant variables if needed, to understand an issue that happens on a single instance.
Generated Modules
For each VFSM instance:
swc_vfsm_name.c
: contains the logic (the state functions) for the vfsm instance, statically linked to the referred objects, and instantiates the state index variable.
For each VFSM definition:
swc_vfsm_name.h
: where name is the name of the VFSM definition, contains:- the enumeration of all the states’ number, starting from 1, numbered as they are in the SWStudio VFSM editor;
- the externs for the logic invocation and state index variable of all the instances of this definition.
Always generated:
swc_config.h
: includes a custom header fileapp_swc_config.h
and all the vfsm include (.h) files.swc_vfsm.c
: provides two functions that invoke all the VFSM instances in the system:uint8_t swc_vfsm_Execute_All_I()
: executes all the actions for all the state machines, and always returns 0.uint8_t swc_vfsm_Execute_All_T()
: executes all the transitions for all the state machines, and returns 1 if one has changed state.
Example: lpc_p2148_example
The example runs on a development board from Olimex: the LPC-P2148, which is based on an NXP LPC2148 (arm7-based) microcontroller.
The board provides the following I/O that are used in our project:
- Two buttons
- One potentiometer
- Two LEDs
- One buzzer
The example project in SWStudio defines the following:
- Two Digital Inputs:
Di_button:001
Di_button:002
- Three Digital Outputs:
Do_buzzer
Do_led:001
Do_led:002
- Two Commands for the state machines:
Cmd_dummy
(for the state machines that we don’t need to command)Cmd_Flasher_Buzzer
(for the Flasher_Buzzer state machine instance)
- Two Output Functions:
OFun_readPotPosition
(divides the pot area in three slots numbered 0, 1, 2)OFun_updateBuzzerTimer
(changes the buzzer on/off period when the buzzer is activated)
- Three Timers:
Ti_button:001
(debouncer for button 1)Ti_button:002
(debouncer for button 2)Ti_buzzer
(defines the on/off period for buzzer activation)
- Three VFSM Definitions:
- Button: changes its state from
NotPressed
toPressed
and vice-versa, reading the button’s digital input and using the timer to perform debouncing. It does not need commands: it always runs. - Flasher: alternates on/off on a digital output, using a timer. It responds to two commands: start and stop.
- Main: it is the main controller: in the
Always
state, it inspects the state of the two buttons’ state machine instances, and turns LEDs 1 and 2 on if the state isPressed
, off if the state isNotPressed
.
In the state logic, it activates the buzzer if both buttons areNotPressed
, after both buttons have beenPressed
and the pot is in the central position.
During buzzer activation, it invokes theOfun_updateBuzzerTimer
to change the on/off period.
Also, if the user presses both buttons and then releases them, the state machine returns to the initial state.
- Button: changes its state from
Now let’s skip SWStudio usage explanation (there are other docs for this), and let’s give a look at the generated code.
Generated Code for Button_1 instance
Let’s immediately look at the state machine Button_1, which gets compiled in swc_vfsm_button_1.c
. We can divide the file in the following parts:
Includes
Just one include here: swc_config.h
, which contains the useful definitions of the whole StateWORKS system, plus – through the user-supplied app_swc_config.h
- the needed realworld’s definitions.
Instantiation of the state index variable, and new state index variable
The global state index variable is swc_vfsm_Button_1_stateIdx
. This variable will always contain the state number in which the state machine is. It is defined as starting from 1, which is the number of the “Init” state. In fact, the number that will be here will always be the same number that SWStudio displays for the states.
The global state index variable is not modified during the transitions. Instead,
swc_vfsm_Button_1_stateIdx_new
is updated. The change will be reflected to
swc_vfsm_Button_1_stateIdx
, only after all state transitions will be calculated.
This will provide state index stability during the transitions of all the VFSMs, regardless of the order of execution of the transitions’ code.
Both variables are of type swc_vfsm_stateidx_t
, so that the user can choose the preferred type for it (unsigned char
will be good if we will have a maximum of 255 states in our largest state machine, for example).
Definition of Virtual Inputs
In this section, each #define defines a name that is composed as VI_name, where name is the name of the Virtual Input, as it is defined in the Inputs table in SWStudio. This name, that is defined in the VFSM editor, will be the same for both the instances Button_1 and Button_2, but the two translations will be different.
In fact, it is in the translations that we perform the static link of this VFSM instance with the connected objects, which – for the virtual inputs – are: Ti_button_001,Di_button_001
. Note that :
in object names, is always translated to an underscore.
The translations in turn invoke object-aware macros (SWC_TI_readVI
, SWC_DI_readVI
), which have the responsibility to return TRUE (1) if the object instance whose name is specified as first argument, is in the state that’s specified as second argument.
Definition of Virtual Outputs
The concept here is very similar to the Virtual Input definition: let’s just show the differences
- The names that are defined begin with OA (Output Action) instead of VI (Virtual Input).
- The macros that are invoked end with performOA instead of readVI.
The invoked object-aware macros have the responsibility to perform, on the object instance whose name is specified as first argument, the action that’s specified as second argument.
The Always state
In Button VFSM, there is nothing in the Always state. Since it is declared as static, this should easily optimize away by the compiler.
The state functions prototypes
In the Button VFSM, there are only 3 states: Init
, NotPressed
, Pressed
.
For each, we find here 4 functions, which are composed by State_
prefix, then the state name, then one of the 4 suffixes in the following list:
_e
: contains the entry output actions of the state;_x
: contains the exit output actions of the state;_i
: contains the conditional actions that the state performs while it is active._t
: contains the conditional transitions that the state examines while it is active, to advance the state machine to one of the successor states.
Note that all the state functions are declared as static, in order to have just a module-local scope, which avoids unwanted name collisions, and allows compiler optimizations (mainly automatic inclusion) also.
The state function implementations
We can note the following details:
- It is the entry function of a state that updates the state index variable, setting the new state number (which is always not-zero), in the variable
swc_vfsm_Button_1_stateIdx_new
. - The conditional actions function of every state calls the Always function.
- The conditional transitions function returns 0 if no transition has occurred.
The state function vector and the execution functions
The state function vector directs, with a constant-time cost, the execution calls to the appropriate state function, with a bi-dimensional array: the first index is the state index, the second index is for I or T function address.
The function swc_vfsm_Button_1_Execute_I()
will execute the actual state’s conditional actions piece of logic, and will always return 0.
The function swc_vfsm_Button_1_Execute_T()
will execute the actual state’ conditional transition logic, and – if a transition occurs – exit function of this state and entry function of next state will be executed, and 1 will be returned.
If no transitions occur, the function will return zero.
Generated code for Button State Machine
The Button state machine, which provides the two instances Button_1 and Button_2, also generates some code: it is a header file: swc_vfsm_button.h
.
Let’s look at the generated code:
There are the single-inclusion safeguard macros, then:
- enumeration of all the states, with BUT prefix: this is the unique prefix that is specified in SWStudio, which belongs to the Button state machine, with the first being numbered as 1, and a final constant
BUT_State__NUMBER
, which can be useful if we would like to dimension an array, for example. - externs for all the instances of Button (Button_1 and Button_2): state index variables (actual and new), and execution functions.
Generated code for the whole State Machine system
There is the normal include for the whole system definitions, then the two execution functions that operate on all the state machine instances.
The function swc_vfsm_Execute_All_I()
will always return zero.
The function swc_vfsm_Execute_All_T()
will return one if at least one state machine instance has changed state, and – if this is the case – it will commit all the _new state index variables to the corresponding actual state index variables.
In order to make the whole state machine system work, these are the only functions that we need to call.
Generated code for the whole system configuration
It immediately includes app_swc_config.h
, which must contain the user’s system definitions, then defines the state function type, then includes all the vfsm’s include files, then it declares as extern
’s the global invocation points (implemented in swc_vfsm.c
).
User code
Now that we have seen what the thinStates compiler produces, it’s time to look at the code that we supplied in the example, to complete the system.
We supplied the following:
app_swc_config.h
: the definitions that the generated code needs to get compiledapp_swc_cmd.c/.h
: our implementation of CMD (state machine command) objectsapp_swc_di.c/.h
: our implementation of DI (digital input) objectsapp_swc_do.c/.h
: our implementation of DO (digital output) objectsapp_swc_ofun.c/.h
: our implementation of OFUN (output function) objectsapp_swc_timer.c/.h
: our implementation of TI (timer) objectsmain.c
: application entry point, and execution loop.
Let’s see the most important aspects, starting from the main:
Initialization
Before entering the main loop, it initializes the board, then configures the objects: the timers are the only objects that need to be initialized.
The implementation we provided for timers exports a function: swc_timer_setMatchValue(int idx, unsigned int value)
, which serves this purpose, and it is called in the main’s swc_configuration()
function:
Our timer implementation works in milliseconds.
Main loop
Here the system runs.
In the main module, a rolling milliseconds time-base is kept updated by the function advanceTimer()
.
Then, the StateWORKS timers (implementation provided in app_swc_timer.c/.h
) are updated by the function swc_timer_advanceTimers(milliseconds)
.
Then, a copy of the system’s digital inputs is taken in memory by the function updateDis()
.
Now, the memory contains the fresh information that the state machines need, in order to perform their tasks.
The actual states are now invoked to perform their conditional actions (and Always state also). No transitions for now. We will let them examine the system and perform their actions, calling the function swc_vfsm_Execute_All_I()
.
During the call, all the state machine instances will perform some conditional output actions.
In particular, they could have performed some actions on the COMMAND objects, to give commands to the other state machines.
The COMMAND objects are implemented in a way that separates output variables from input variables, so that actions on command are not immediately visible, but a commit must be called to the command implementation module, which performs the copy.
So, after giving the chance for action to all the state machines, it is time to commit their changes in a single phase with the function commitCmds()
.
Then, the state machines are invoked to perform their transitions, if any, with the function swc_vfsm_Execute_All_T()
.
We already know that this function automatically commit state changes after all the transitions.
But since during transitions also output actions happen (through exit and entry commands), maybe some command has been changed also.
So let’s commit these changes again, calling commitCmds()
, in order to have these changes visible at the next loop interaction.
At the end, calling updateDos()
, transfers the digital outputs memory image to the hardware.
Note on commits
We will further explain why there is the need to commit for commands and state index.
The basic concept is to keep invariant the behavior of the state machines, regardless of the execution order. Let’s see for example what happens when we invoke the global transition function:
The vfsms are called in a particular sequence. So, if any output action operation from a state machine was made immediately visible as input to all the other state machines, changing the execution order would change the behavior of the system.
Instead, keeping the important inputs constant during the round, the execution order will not change the system behavior.
In SWStudio, there is no way to control the execution order: all should behave as if it was running simultaneously.
Separating the outputs and the inputs by a commit phase, we can guarantee that the system will always run consistently, regardless of the execution order.
But which objects should be considered important? They are the objects that are shared across vfsms, so primarily state indexes and commands.
As a general rule, other objects should not be shared.
But if they need to be shared for a particular purpose, care should be taken to analyze if the separation-commit algorithm should be implemented.
Conclusions
The thinStates C Compiler for StateWORKS brings the StateWORKS’ idea of an executable specification, to the microcontroller level.
After properly modeling the “abstraction layer” between the microcontroller board resources and the used StateWORKS objects, the user will be able to execute the specification with a minimal amount of memory.
The generated code is very human-readable, and it is easy to debug, because of the instance-to-c-file translation strategy.
The article was originally written by author V. Marolda in February 2011
Here you can view the unedited file directly from the author: PDF
http://www.thinstates.com