There is a long history of data-flow programming languages for reactive systems. In the eighties, several languages were developed under the Synchronous Programming approach.
I think that Translating Discrete Time SIMULINK
to SIGNAL would be a good starting point to get exposed to Simulink, Signal. Also, there is a DC motor use-case.
Synchronous approach
Instead of manipulating values, synchronous languages define equations over synchronous flows. At each logical clock step, inputs are read and instantaneously propagate to local variables and outputs. The zero-delay assumption means that it takes a constant time to update your state (up to you to prove that the actual execution time always fits in the required physical time period). Here is an example with an internal state, in Lustre:
node sum (in : int) returns (sum : int);
let
sum = in + (0 -> pre(sum));
tel;
The arrow separate code that is executed the first time (initialization, on left) and code that is updated at all other instants (general case, on right). On the right of the arrow, it is thus allowed to refer to past values of sum
: this is the purpose of pre
.
With the above definition, we can analyse dependencies ahead of time, make a static scheduling of code and produce C code based on an init
function and a step
function, which basically are:
/* local globals */
inline int sum, in, tmp;
/* Interface with other code */
extern int read_in();
extern void write_sum(int);
void sum_init()
{
tmp = 0;
}
void sum_step ()
{
in = read_in(); /* external */
sum = tmp + in;
write_sum(sum); /* external */
/* Update memory */
tmp = sum;
}
Today there seem to be a renewed interest for Functional Reactive Programming. Even though there are some research around Real-Time FRP
, the FRP generally provided does not offer the same kind of static guarantees that are required in embedded/critical systems:
constant memory usage: memory is statically allocated and completely determined ahead of time (no GC, thanks). Past values are only accessed through delay operators (e.g. pre
), so the number of nested delay operators indicates how many previous versions of a variable to keep, which is statically known. You could imagine that you build a sliding window of memory to observe the infinite flow described by your equations.
constant execution time: worst-case execution time of each time-step is much more easier to compute.
causality analysis: the order of operations is known in advance. When designing distributed reactive systems, you can guarantee that you won't deadlock.
Synchronous languages also use clocks, which determine when a computation must occur: you can write x when b
to have a flow where the only values computed exist when the boolean data-flow b
evaluates to true (and notice that b
is a data-flow itself, which can also have a clock).
In the Signal language, which allows to describe distributed units of computations, clocks allow for example to emit only the necessary synchronization information between separated, asynchronous, units: if you transmit a tick counter between two systems and the clock relationships tell you that flow A is present every 5 ticks and flow B every 7 ticks, you do not need to also transmits the actual boolean clock flow of A and B: the receiver knows exactly when to read a value for A and one for B, based only on the tick. Clocks form boolean relationships and can sometime be inferred implicitly from the known status of other clocks.
Those languages are developed in industrial contexts, generally as part of a larger tool-chain: look for SCADE, etc. Depending on your usage, you might prefer to use academic compilers.
What about F#?
F# is garbage-collected, as far as I know. I tend to think that you can execute controllers in a garbage-collected environment, if you are careful enough and your device does not require precise timings. I would personally mutate existing variable to avoid allocating memory during execution, so that the actual memory consumption in your control loop would be zero.
If memory is continuously allocated (and then collected, hopefully), you might have, on average, a constant memory usage with possible GC pauses. The garbage-collector is not real-time (even though there exist real-time GCs), but if the physical period is large enough, you will have a low probability of missing a deadline.
If missing a deadline is out of question, use some generated code from the above languages, or write one by hand if you only need a simple control loop. Use a low-level language like C or Ada (maybe within a Real-Time OS or directly on the micro-controller).