Tutorial¶
This chapter gives a gentle introduction to State Notation Language (SNL).
A First Example¶
We start with a simple state machine volt_check
that controls a
light switch depending on the value of a voltage measurement and
the internal state of the program. The following code fragment
defines the state machine:
ss volt_check {
state light_off {
when (voltage > 5.0) {
light = TRUE;
pvPut(light);
} state light_on
}
state light_on {
when (voltage < 3.0) {
light = FALSE;
pvPut(light);
} state light_off
}
}
At the top level we use the keyword ss
to declare a state set (which
is SNL speak for state machine) named volt_check
. Inside the code
block that follows, we define the states of this state set, using the
state
keyword. There are two states here: light_off
and light_on
.
Inside each state, we define conditions under which the program will
enter another state, indicated by the keyword when
. The block
following the condition contains action statements that are executed
when the condition fires.
In our example, in state light_off
, whenever the voltage exceeds a
value of 5.0, the light switch is turned on, and the internal state
changes to light_on
. In state light_on
, whenever the voltage is
or drops below 3.0, the light switch is turned off, and the internal
state changes to light_off
.
The following is a graphical representation of the above state machine:
Note that the output or action depends not only on the input or
condition, but also on the current state. For instance, an input
voltage
of 4.2 volts does not alone determine the output (light
),
the current state matters, too.
As you can see, the SNL code is syntactically very similar to the C language. Particularly, the syntax for variable declarations, expressions, and statements are exactly as in C, albeit with a few restrictions.
You might wonder about the function calls in the above code. The
pvPut
function is a special built-in function that writes (or puts)
the value in the variable light
to the appropriate process variable.
But before I can explain how this works, we must talk about how program
variables are “connected” to process variables.
Variables¶
SNL programs interact with the outside world via variables that are bound
to (or connected to) process variable
s (PVs) in EPICS. In our example,
there are two such variables: voltage
, which represents a measured
voltage, and light
which controls a light switch. In an actual SNL
program, these variables must be declared before they can be used:
float voltage;
int light;
We also want to associated them with PVs i.e. EPICS record fields:
assign voltage to "Input_voltage";
assign light to "Indicator_light";
The above assign
clauses associate the variables voltage
and
light
with the process variables “Input_voltage” and
“Indicator_light” respectively.
We also want the value of voltage
to be updated automatically whenever
it changes. This is accomplished with the following code:
monitor voltage;
Whenever the value in the control system (the EPICS database) changes,
the value of voltage
will likewise change. Note however that this
depends on the underlying system sending update messages for the value
in question. When and how often such updates are communicated by the
underlying system may depend on the configuration of the PV. For
instance if the PV “Input_voltage” is the VAL field of an ai (analog
input) record, then the value of the MDEL field of the same record
specifies the amount of change that the designer considers a
“relevant” change; smaller changes will not cause an event to be sent,
and accordingly will not cause a state change in the above program.
Built-in PV Functions¶
I said above that the program interacts with the outside world via variables assigned to PVs. However, mutating such a variable e.g. via the C assignment operator (see Assignment Operators), as in:
light = TRUE;
only changes the value of light
as seen from inside the program.
In order for the new value to take effect, it must be written to the
PV connected with the variable. This is done by calling the special
built-in function pvPut
, that gets the variable as argument.
Note that calling such a special built-in function does not follow
the standard C semantics for function calls! Particularly, what
actually gets passed to the function is not the value of the variable
light
(as it would in C), instead an internal representation of the
variable gets passed (by reference). You can think of what actually
gets passed as an “object” (as in “object-oriented”) or a “handle”
that contains all the necessary run-time information, one of which is
the name of PV the variable is connected with.
There are many more of these built-in functions, the SNL Reference for Version 2.2
contains detailed description of each one. For now, let’s keep to the
basics; I’ll mention just one more built-in function: With
pvGet
, you can poll PVs explicitly, instead of using
monitor
. That is, a statement such as
pvGet(voltage);
has the effect of sending a get request to the PV “Input_voltage”, waiting for the response, and then updating the variable with the new value.
A Complete Program¶
Here is what the complete program for our example looks like:
program level_check
float voltage;
assign voltage to "Input_voltage";
monitor voltage;
short light;
assign light to "Indicator_light";
ss volt_check {
state light_off {
when (voltage > 5.0) {
/* turn light on */
light = TRUE;
pvPut(light);
} state light_on
}
state light_on {
when (voltage < 5.0) {
/* turn light off */
light = FALSE;
pvPut(light);
} state light_off
}
}
Each program must start with the word program
, followed by the name
of the program (an identifier):
program level_check
After that come global declarations and then one or more state sets.
Adding a Second State Set¶
We will now add a second state set to the previous example. This new state set generates a changing value as its output (a triangle function with amplitude 11).
First, we add the following lines to the declaration:
float vout;
float delta;
assign vout to "Output_voltage";
Next we add the following lines after the first state set:
ss generate_voltage {
state init {
when () {
vout = 0.0;
pvPut(vout);
delta = 0.2;
} state ramp
}
state ramp {
when (delay(0.1)) {
if ((delta > 0.0 && vout >= 11.0) ||
(delta < 0.0 && vout <= -11.0)) {
delta = -delta; /* change direction */
}
vout += delta;
} state ramp
}
}
The above example exhibits several concepts. First, note that the
transition
clause in state init
contains an empty event
expression. This means unconditional execution of the transition. The
first state in each state set is always the initial state, so we give
it the name init
. From this first state there is an immediate
unconditional transition to the state ramp
, initializing some
variables during the transition. Note that the ramp
state always
returns to itself. The structure of this state set is shown in the
following STD:
The final concept introduced in the last example is the delay
function.
This function returns a boolean that tells us whether the given time
interval has elapsed. The interval is given in seconds (as a floating point
value) and counts from the time the state was entered.
At this point, you may wish to try an example with the two state sets. You can jump ahead and read parts of the Chapters Compiling SNL Programs and Running SNL Programs to find out how. You probably want to pick unique names for your process variables, rather than the ones used above.
Variable Initialization and Entry Blocks¶
Since version 2.1 it has become simpler to initialize variables: you can use the same syntax as in C, i.e. initialize together with the declaration:
float vout = 0.0;
float delta = 0.2;
which, by the way, can also be written as
float vout = 0.0, delta = 0.2;
More complicated initialization (e.g. involving non-constant expressions or
side-effects) can be done using an entry
block instead of using a separate
state:
ss generate_voltage {
state ramp {
entry {
pvPut(vout);
}
when (delay(0.1)) {
...
} state ramp
}
}
The actions in an entry block in a state declaration are executed whenever the state is entered from a different state. In this case this means the
pvPut(vout);
that appears inside the entry block will be executed only once when the state is entered for the first time.
PV Names Using Program Parameters¶
You can use program parameter substitution to parameterize the PV names
in your program. In our example we could replace the assign
statements with the following:
assign voltage to "{unit}:ai1";
assign vout to "{unit}:ao1";
The string within the curly braces is the name of a program parameter and the whole thing (the name and the braces) are replaced with the value of the parameter. For example, if the parameter “unit” has value “DTL_6:CM_2”, then the expanded PV name is “DTL_6:CM_2:ai1”. See Program Parameters for more on program parameters (and particularly how to give them values).
Data Types¶
In earlier versions, variables were restricted to a hand full of predefined types, plus one or two-dimensional arrays of these.
This is no longer true: you can declare variables of any type you like. The only restrictions are:
you cannot define new types, only use them in declarations
when using type aliases (“typedef”) you must prefix them with the keyword “typename”
only variables of the above mentioned restricted list can be
assign
'ed to PVs.
The built-in types are: char
, unsigned char
, short
,
unsigned short
, int
, unsigned int
, long
, unsigned
long
, float
, and double
. These correspond exactly to their C
equivalents. In addition there is the type string
, which is an array
of 40 char
.
Sequencer variables having any of these types may be assigned to a process variable. The type declared does not have to be the same as the native control system value type. The conversion between types is performed at run-time. For more details see the corresponding section in the reference.
You may specify array variables as follows:
long arc_wf[1000];
When assigned to a process variable, operations such as
pvPut
are performed for the entire array.
Arrays of Variables¶
Often it is necessary to have several associated process variables. The ability to assign each element of an SNL array to a separate process variable can significantly reduce the code complexity. The following illustrates this point:
float Vin[4];
assign Vin[0] to "{unit}1";
assign Vin[1] to "{unit}2";
assign Vin[2] to "{unit}3";
assign Vin[3] to "{unit}4";
We can then take advantage of the Vin
array to reduce code size
as in the following example:
for (i = 0; i < 4; i++) {
Vin[i] = 0.0;
pvPut (Vin[i]);
}
We also have a shorthand method for assigning channels to array elements:
assign Vin to { "{unit}1", "{unit}2", "{unit}3", "{unit}4" };
Similarly, the monitor declaration may be either by individual element:
monitor Vin[0];
monitor Vin[1];
monitor Vin[2];
monitor Vin[3];
Alternatively, we can do this for the entire array:
monitor Vin;
And the same goes when Synchronizing State Sets with Event Flags and Queuing Monitors.
Double subscripts offer additional options:
double X[2][100];
assign X to {"apple", "orange"};
The declaration creates an array with 200 elements. The first 100
elements of X
are assigned to (array) “apple”, and the second
100 elements are assigned to (array) “orange” .
It is important to understand the distinction between the first and
second array indices here. The first index defines a 2-element array
of which each element is associated with a process variable. The
second index defines a 100-element double array to hold the value of
each of the two process variables. When used in a context where a
number is expected, both indices must be specified, e.g. X[1][49]
is the 50th element of the value of “orange” . When used in a context
where a process variable is expected, e.g. with pvPut
, then
only the first index should be specified, e.g. X[1]
for “orange” .
Dynamic Assignment¶
You may dynamically assign or re-assign variable to process variables during the program execution as follows:
float Xmotor;
assign Xmotor to "Motor_A_2";
...
sprintf (pvName, "Motor_%s_%d", snum, mnum)
pvAssign (Xmotor[i], pvName);
Note that dynamic (re-)assignment fails (with a compiler error) if the variable has not been assigned statically.
An empty string in the assign declaration implies no initial assignment and can be used to mark variables or array elements for later dynamic assignment:
assign Xmotor to "";
Likewise, an empty string can de-assign a variable:
pvAssign(Xmotor, "");
The current assignment status of a variable is returned by the
pvAssigned
function as follows:
isAssigned = pvAssigned(Xmotor);
The number of assigned variables is returned by the
pvAssignCount
function as follows:
numAssigned = pvAssignCount();
The following inequality will always hold:
pvConnectCount() <= pvAssignCount() <= pvChannelCount()
Having assigned a variable, you should wait for it to connect before using it (although it is OK to monitor it). See Connection Management.
Status of Process Variables¶
Process variables have an associated status, severity and time stamp.
You can obtain these with the pvStatus
, pvSeverity
and pvTimeStamp
functions. For example:
when (pvStatus(x_motor) != pvStatOK) {
printf("X motor status=%d, severity=%d, timestamp=%d\\n",
pvStatus(x_motor), pvSeverity(x_motor),
pvTimeStamp(x_motor).secPastEpoch);
...
These routines are described in Built-in Functions. The values
for status and severity are defined in the include file pvAlarm.h,
and the time stamp is returned as a standard EPICS TS_STAMP
structure, which is defined in tsStamp.h . Both these files are
automatically included when compiling sequences (but the SNL compiler
doesn’t know about them, so you will get warnings when using constants
like pvStatOK
or tags like secPastEpoch
).
Synchronizing State Sets with Event Flags¶
State sets within a program may be synchronized through the use
of event flags. Typically, one state set will set an event flag, and
another state set will test that event flag within a transition
clause. The sync
statement may also be used to associate an
event flag with a process variable that is being monitored. In that
case, whenever a monitor is delivered, the corresponding event flag is
set. Note that this provides an alternative to testing the value of
the monitored channel and is particularly valuable when the channel
being tested is an array or when it can have multiple values and an
action must occur for any change.
This example shows a state set that forces a low limit always to be
less than or equal to a high limit. The first transition
clause
fires when the low limit changes and someone has attempted to set it
above the high limit. The second transition
clause fires when the
opposite situation occurs.
double loLimit;
assign loLimit to "demo:loLimit";
monitor loLimit;
evflag loFlag;
sync loLimit loFlag;
double hiLimit;
assign hiLimit to "demo:hiLimit";
monitor hiLimit;
evflag hiFlag;
sync hiLimit hiFlag;
ss limit {
state START {
when ( efTestAndClear( loFlag ) && loLimit > hiLimit ) {
hiLimit = loLimit;
pvPut( hiLimit );
} state START
when ( efTestAndClear( hiFlag ) && hiLimit < loLimit ) {
loLimit = hiLimit;
pvPut( loLimit );
} state START
}
}
The event flag is actually associated with the SNL variable, not the underlying process variable. If the SNL variable is an array then the event flag is set whenever a monitor is posted on any of the process variables that are associated with an element of that array.
Queuing Monitors¶
Neither testing the value of a monitored channel in a transition
clause nor associating the channel with an event flag and then testing
the event flag can guarantee that the sequence is aware of all
monitors posted on the channel. Often this doesn’t matter, but
sometimes it does. For example, a variable may transition to 1 and
then back to 0 to indicate that a command is active and has completed.
These transitions may occur in rapid succession. This problem can be
avoided by using the syncq
statement to associate a variable
with a queue. The pvGetQ
function retrieves and removes the
head of queue.
This example illustrates a typical use of pvGetQ
: setting a
command variable to 1 and then changing state as an active flag
transitions to 1 and then back to 0. Note the use of pvFlushQ
to clear the queue before sending the command. Note also that, if
pvGetQ
hadn’t been used then the active flag’s transitions
from 0 to 1 and back to 0 might both have occurred before the
transition
clause in the sent
state fired:
long command; assign command to "commandVar";
long active; assign active to "activeVar"; monitor active;
syncq active 2;
ss queue {
state start {
entry {
pvFlushQ( active );
command = 1;
pvPut( command );
}
when ( pvGetQ( active ) && active ) {
} state high
}
state high {
when ( pvGetQ( active ) && !active ) {
} state done
}
state done {
/* ... */
}
}
The active
SNL variable could have been an array in the above
example. It could therefore have been associated with a set of
related control system active
flags. In this case, the queue
would have had an entry added to it whenever a monitor was posted
on any of the underlying control system active
flags.
Asynchronous Use of pvGet¶
Normally the pvGet
operation completes before the function
returns, thus ensuring data integrity. However, it is possible to use
these functions asynchronously by specifying the +a
compiler
flag (see Compiler Options). The operation might not be
initiated until the action statements in the current transition have
been completed and it could complete at any later time. To test for
completion use the function pvGetComplete
, which is
described in Built-in Functions.
pvGet
also accepts an optional SYNC
or ASYNC
argument, which overrides the +a
compiler flag. For
example:
pvGet( initActive[i], ASYNC );
Asynchronous Use of pvPut¶
Normally pvPut
is a “fire and forget” operation without any provisions for
testing if and when it completed successfully. However, this
behaviour can be modified by passing an optional SYNC
or ASYNC
argument. With SYNC
, the call blocks until the operation is complete,
while with ASYNC
the call returns immediately. In the latter case,
pvPutComplete
tells you whether the operation completed.
For example,
pvPut(init[i], SYNC);
will block until the put operation to the PV behind init[i]
(and all the
processing resulting from it) is complete, while
pvPut(init[i], ASYNC);
does not block and instead lets you test completion explicitly, e.g.
when(pvPutComplete(init[i])) {
...
}
Note that pvPutComplete
can only be used with single PVs. Testing
completion for multiple PVs in a multi-PV array can be done with
pvArrayPutComplete
as in the following example
#define N 3
long init[N];
seqBool done[N]; /* used in the modified example below */
assign init to {"ss1:init", "ss2:init", "ss3:init"};
state inactive {
when () {
for ( i = 0; i < N; i++ ) {
init[i] = 1;
pvPut( init[i], ASYNC );
}
} state active
}
state active {
when ( pvArrayPutComplete( init ) ) {
} state done
when ( delay( 10.0 ) ) {
} state timeout
}
pvArrayPutComplete
accepts optional arguments to tweak its behaviour.
For instance, the following could be inserted
before the first transition
clause in the active
state above.
The TRUE
argument causes pvPutComplete
to return
TRUE
when any command completed (rather than only when all
commands complete). The done
argument is the address of a seqBool
array of the same size as init
; its elements are set to FALSE
for
puts that are not yet complete and to TRUE
for puts that are complete.
when ( pvPutComplete( init, TRUE, done ) ) {
for ( i = 0; i < N; i++ )
printf( " %ld", done[i] );
printf( "\n" );
} state active
Connection Management¶
All process variable connections are handled by the sequencer via the
PV API. Normally the programs are not run until all process
variables are connected. However, with the -c
compiler flag,
execution begins while the connections are being established. The
program can test for each variable’s connection status with the
pvConnected
routine, or it can test for all variables
connected with the following comparison (if not using dynamic
assignment, see Dynamic Assignment, pvAssignCount
will be
the same as pvChannelCount
):
pvConnectCount() == pvAssignCount()
These routines are described in Built-in Functions. If a variable
disconnects or re-connects during execution of a program, the
sequencer updates the connection status appropriately; this can be
tested in a transition
clause, as in:
when (pvConnectCount() < pvAssignCount()) {
} state disconnected
When using dynamic assignment, you should wait for the newly assigned variables to connect, as in:
when (pvConnectCount() == pvAssignCount()) {
} state connected
when (delay(10)) {
} state connect_timeout
Note that the connection callback may be delivered before or after the initial monitor callback (the PV API does not specify the behavior, although the underlying message system may do so). If this matters to you, you should synchronize the value with an event flag and wait for the event flag to be set before proceeding. See Synchronizing State Sets with Event Flags for an example.
Multiple Instances and Reentrant Object Code¶
Occasionally you will create a program that can be used in
multiple instances. If these instances run in separate address spaces,
there is no problem. However, if more than one instance must be
executed simultaneously in a single address space, then the objects
must be made reentrant using the +r
compiler flag. With this
flag all variables are allocated dynamically at run time; otherwise
they are declared static. With the +r
flag all variables
become elements of a common data structure, and therefore access to
variables is slightly less efficient.
Process Variable Element Count¶
All requests for process variables that are arrays assume the array
size for the element count. However, if the process variable has a
smaller count than the array size, the smaller number is used for all
requests. This count is available with the pvCount
function.
The following example illustrates this:
float wf[2000];
assign wf to "{unit}:CavField.FVAL";
int LthWF;
...
LthWF = pvCount(wf);
for (i = 0; i < LthWF; i++) {
...
}
pvPut(wf);
...
What’s Happening at Run Time¶
At run time the sequencer blocks until something “interesting” occurs, where
“interesting” means things like receiving a monitor from a PV used in a
transition
clause, an event flag changing state, or a delay timer
expiring. See section transitions
in the SNL Reference for Version 2.2 for a
detailed list.
The sequencer then scans the list of transition
statements for the
current state and evaluates each expression in turn. If a transition
expression evaluates to non-zero the actions within that transition
block
are executed and the sequencer enters the state specified by that
transition
statement. The sequencer then blocks again waiting for
something “interesting” to happen.
Note, however, that whenever a new state is entered, the corresponding
transition
conditions for that state are evaluated once without first
waiting for events.
Safe Mode¶
New in version 2.1.
SNL code can be interpreted in safe mode. This must be enabled with the
+s
option, because it changes the way variables are handled and is thus
not fully backwards compatible. It should, however, be easy to adapt
existing programs to safe mode by making communication between state sets
explicit. New programs should no longer use the traditional unsafe mode.
Rationale¶
In the traditional (unsafe) mode, variables are not protected against
access from concurrently running threads. Concurrent access to SNL
variables was introduced in version 2.0, when implementation of the PV
layer switched from the old single threaded CA mode (“preemptive
callbacks disabled”) to the multi-threaded mode (“preemptive callbacks
enabled”) in order to support more than one state set per program. This
could result in data corruption for variables that are not read and written
atomically, the details of which are architecture and compiler dependent
(i.e. plain int
is typically atomic, whereas double is problematic on
some, string and arrays on almost all architectures/compilers). Even for
plain int
variables, read-modify-write cycles (like v++
) cannot be
guaranteed to have any consistent result. Furthermore, condition
s that have
been met inside a transition
clause cannot be relied upon to still hold
inside the associated action block.
Concurrent access to SNL variables happens when
multiple state sets access the same variable, or
variables are updated from the PV layer due to monitors and asynchronous get operations.
While it is possible to avoid the first case by careful coding (using e.g. event flags for synchronization) it is not possible to guard against the second case as these events can interrupt action statements at any time.
One of the reasons SNL programs have mostly worked in spite of this is that due to the standard CA thread priorities the callback thread does not interrupt the state set threads. Furthermore (and contrary to what many people believe) the VxWorks scheduler does not normally serve threads with equal priority in a round-robin (time-sliced) fashion; instead each thread keeps running until it gets interrupted by a higher priority thread or until it blocks on a semaphore.
However, RTEMS does time-share threads at the same priority, while Linux and Windows may or may not honor thread priorities, depending on the system configuration. Most importantly, priorities should only be used to improve latency for certain operations (at the cost of others) and never should be relied upon for program correctness.
Safe mode solves all these problems by changing the way variables, particularly global variables, are interpreted.
How it Works¶
In safe mode, all variables
–except event flags– are interpreted as if they were local to the state
set. This means that setting a variable (even a global variable) in one
state set does not automatically change its value as seen by other state
sets. State sets are effectively isolated against each other, and all
communication between them must be explicit. They are also isolated against
updates by callbacks from the PV layer except at those points where they
don’t do anything i.e. when they wait for events in a transition
clause.
In safe mode, variable values get updated right before the condition
s are
evaluated, or when explicitly calling synchronization functions like
pvGetComplete
or pvGet
(the latter only if called in
synchronous mode), as well as efTest
and efTestAndClear
.
The documentation for the built-in functions explains the details.
For instance, with the declaration
int var;
assign var;
the action statement
pvPut(var)
makes the value of var
available to other state sets. They will,
however, not see the new value until they issue either a (synchronous)
pvGet
, or the variable is declared as monitored and state
change condition
s are evaluated.
The action
pvGet(var, SYNC)
updates var
immediately with whatever has been written to it
previously via pvPut
by some other state set. Whereas
pvGet(var, ASYNC)
has no immediate effect on the variable var
. Instead, var
will be updated only if the code calls pvGetComplete
(and it returns true
).
Note
This behaviour is exactly the same as with external PVs.
Common Pitfalls and Misconceptions¶
The delay function does not block¶
A common misconception among new SNL programmers is that the sequencer
somehow blocks inside the delay
function within transition
statements. This interpretation of the delay
function is
incorrect but understandable given the name. The delay
function
does not block at all, it merely compares its argument with a timer that
is reset whenever the state is entered (from the same or another state),
and then returns the result (a boolean value). Any blocking (in case the
returned value is FALSE and no other condition fires) is done outside of
the delay
function by the run time system. You might want to
think of the operation as elapsed(s)
rather than delay(s)
.
If your action statements have any sort of polling loops or calls to
epicsThreadSleep
you should reconsider your design. The presence
of such operations is a strong indication that you’re not using the
sequencer as intended.
Using pvPut and monitor in the same state set¶
Let’s say you have a channel variable x that is monitored, and this code fragment:
state one {
when () {
x = 1;
pvPut(x);
x++;
} state two
}
state two {
when (x > 1) {
do_something();
}
when (x <= 1) {
do_something_else();
}
}
This pattern is hazardous in a number of ways. What exactly happens here depends on whether you are using Safe Mode or not.
Assuming traditional (unsafe) mode, it is unpredictable which branch in
state two
will be taken. The pvPut(x)
might cause a monitor event to
be posted by the PV that was assigned to x
. This event will change x
back to 1
whenever it arrives. This might happen at any time in between
the pvPut(x)
and the testing of the conditions. It could even interrupt
in the middle of the x++
operation. As a result, this code behaves in
conpletely unpredictable ways, depending on the timing of the pvPut-monitor
round-trip.
In Safe Mode things are slightly better: the only point where the event can lead to an update of the variable is right before evaluation of the conditions. However, it is still undetermined which branch will be taken.
You might be tempted to test your code and find that “it works”, in the
sense that the behavior you see appears to consistently chose one of the two
branches, perhaps after adding some delay
s to the conditions. But this
impression is misleading, since what actually happens depends on details
of thread scheduling and priorities and a host of other timing factors, some
of which are very hard to control such as network or system load.
If you cannot avoid using pvPut for a monitored variable, then you should at least