Control and Regulation
Control and regulation with the math module
Given the extensive function library of the math module, it makes sense to also implement control and regulation functions in this scripting language.
This is actually possible with a few restrictions and taking the following points into account:
Signal processing, time behavior
The process of control and regulation requires hard real-time calculations, i.e., a fixed, as short as possible signal propagation time from the acquisition of the measured variable to the output of the control variable to the process. In control systems, this is typically around 1 to 10 ms. This hard constraint cannot be met with the smartCORE architecture!
Measurement data is acquired by specific plug-ins and written to the channels of the smartCORE. For this purpose, the formula set of the math module is calculated asynchronously in approximately constant, large time intervals on the available data, provided that all required channels also provide data at a given point in time. The results are written to smartCORE channels at the rate of this calculation. For this purpose, data from the smartCORE channels is output to the process asynchronously, e.g., via CAN.
There are therefore enough places where the real-time requirement is violated by the asynchrony in smartCORE, the channels as buffers or ring memory, and the batch processing of the math module.
If the processes to be influenced are slow1 enough, it is at least possible to adjust the calculation process and the output so that the discrete sampling time discreteSampleTimeMs matches the execution time of the batch process evaluationTimeMs and the output to the process. Possible values here are in the range of 50 to 1000 ms. Then the asynchrony and buffering of the data streams become tolerable.
Controller
PID controller "pidCtrl"
This function is currently being implemented and is not yet intended for productive operation.
The function pidCtrl() implements a controller with configurable proportional (P), integral (I), and derivative (D) behavior.
The input parameters of the function are at least the reference variable (setpoint) sp and the process variable to be controlled (actual value, current value) cur. The output is the control variable.
Optionally, the operating mode can be selected via the input mode:
| mode | Description |
|---|---|
| 0, false | Normal control mode (Enable) |
| 1, true | Hold control variable (Hold) |
| 2 | Control variable follows the manual control input u_man (Bypass) |
Optionally, the control parameters [K, Ti, Td] can also be assigned dynamically using the input vector parVec. The vector can contain 1 to 3 parameter values. These overwrite the specifications in the configuration object.
The calculation is always performed with the discrete sampling time discreteSampleTimeMs. To ensure that the control variable can be transferred to the process in a timely manner, the execution time of the batch process evaluationTimeMs must be set to the same value.
u1 = pidCtrl(sp, cur, {...});
u2 = pidCtrl(sp, cur, mode, {...});
u3 = pidCtrl(sp, cur, mode, u_man, {...});
u4 = pidCtrl(sp, cur, mode, u_man, parVec, {...});
// Mandatory configuration for all variants
ux = pidCtrl(..., { K: <dbl>
, Ti: off|<dbl>
, Td: off|<dbl>
, reverse: <bool>
, commonK: <bool>
, dWidth: <uint>
, lower: off|<dbl>
, upper: off|<dbl>
});
The properties are described using examples from the time domain, although the function calculates internally in discrete sampling steps.
| Property | Value | Description |
|---|---|---|
| K | <dbl> | Amplification of the controller |
| Ti | off|<dbl> | Activation and integration time for I component |
| Td | off|<dbl> | Activation and differentiation time for D component |
| reverse | <bool> | Reversal of control direction false: , (def.) true: |
| commonK | <bool> | Interpretation of gain: false: true: , (def.) |
| dWidth | <uint> | Number of sampling intervals for weighted averaging of the differential value (def.: 3) |
| lower | off|<dbl> | Lower limit of the control variable, the integral component is tracked to enable shock-free resumption. |
| upper | off|<dbl> | Upper limit of the control variable, |
Controller structure: The commonK property switches the interpretation of the gain factor K. In control engineering, K usually acts equally on all three paths P, I, and D (commonK: true, def.!) and also converts the physical dimensions between control deviation (e.g., in hPa) and control variable (e.g., in %). Thus, K does not influence the dynamic behavior of the controller, which is determined by its poles and zeros, but only the agility and stability of the closed control loop. Many tuning rules for PID controllers are based on this structure.
It is rarely interpreted that K actually only influences the proportional path of the controller (commonK: false). This shifts the task of unit conversion to the time parameters Ti and Td, and changing K directly changes the poles and zeros of the controller and thus its dynamic behavior. This makes it considerably more difficult to calibrate the controller.
Using the D component: Since the controller function is tied to the very slow discrete clocking of the math module, the evaluation of the gradient of the control deviation in the D component should also be used with caution. Due to the "on-change" philosophy used to provide measurement data in a smartCORE channel, there may be long periods of time during which the signal does not change. In order to be able to detect a slow drift in temperature, for example, the values are calculated over several sampling steps using a weighting function to obtain the value of the derivative. The width of this window is specified by the dWidth property. However, this also creates additional dead time (dWidth/2 * discreteSampleTimeMs) in the controller's D path, which can have a negative impact on the control process. dWidth should therefore be as small as possible but as large as necessary. This depends on the process to be controlled.
Time reference: If the reference variable sp comes from a (shared) attribute and is only updated rarely, this channel should definitely be detached from the time reference using the macro #timeless. The same may also apply to the process variable cur.
Finite State Machine
Implementation of a Finite State Machine (FSM)
A general description can be found in the article Finite-state machine - Wikipedia or here (DE).
In this implementation, evaluation is clocked with the global discrete sampling time discreteSampleTimeMs. In each clock cycle, exactly one transition can cause a state change.
This finite state machine has a finite number of states, identified by a unique label sx. Each state is stable until a state transition triggers a change. This means that a state can contain information about the past, since the system has reached it on its previous path — i.e., it reflects to a certain extent the changes in the input since system startup until the current point in time.
A state transition is a transition from the current state to a new (different) state. This transition occurs when the specified logical conditions / "inputs" are present that must be fulfilled to enable the transition. In this implementation, a Boolean condition, a definable time window, a repeat counter, and a priority value are available for this purpose.
The following image shows a fictional state machine with 5 states "A" through "D" and "end".
For the design it is recommended to draw exactly such a graph for the state machine to be implemented, showing the required states and transitions.
With the start of the formula interpreter "init", a basic state "A" is entered. Various state transitions cause the automaton to switch to new states or, via defined transition priorities, to stay in a state under certain conditions. A state from which no transitions lead is called a final state.
"From-anywhere" transitions can trigger a state change independently of the active state (including a final state).
The structure shown above would be translated into formula text according to the following scheme — arguments for switching conditions or time parameters, as well as JSON parameters for transitions, must be added accordingly:
act = states( // from anywhere to ...
transit('A', ...) // reset/restart
, transit('C', ...) // resume
, 'A' // State A: (re)start and prepare the tasks
, transit('B', ...) // preparation completed
, 'B' // State B: doing crazy stuff
, transit(..., {prio: 100}) // explicit locker-transition
, transit('D', ...) // condition 1 to move on
, transit('D', ...) // condition 2 to move on
, 'C' // State C: await feedback and decide where to continue
, transit('A', ...)
, transit('B', ...)
, transit('D', ...)
, 'D' // State D: doing some nice stuff to complete
, transit('C', ...) // there's more to do
, transit('end', ...)
, 'end' // final, stable state
// properties of state machine:
, { init: 'A'
, outIdx: true });
Multi-line notation with appropriate indentation helps to maintain an overview of states and transitions. So does the extensive use of comments.
The variable act contains the index of the current state (1, 2, 3, …) due to outIdx: true and can be used in subsequent expressions to control the actions of the state machine. Suitable constructs include simple comparisons such as someResult = (act == 3) ? ... : ...;, the function select(act, ...), or edge detection. Different parameter values can also be read directly from a vector with parVec[act-1] or looked up from a table using selectRow() and getField(). See also the reference description for states(...) below.
Finite state machine "states"2
This function states() can be used to model behavior consisting of states, state transitions, and actions.
s1, … sN: define the identifiers of the states sx of the state machine as const <uint/int/str> (!) The first state is the start state, unless another is specified via the property init: sx. The output of states() is the respective value of sx of the active state, unless the output of the state index as is forced with the property outIdx: true. All sx must be unique, as they are used in the transit() functions to specify the next state.
trXY: define transit() objects that are evaluated for the aforementioned state. The first transit() function for which the configured conditions apply triggers the state transition.
State transitions defined before the first state play a special role. They are evaluated in any state before the transitions defined in the state itself are evaluated. They thus take on the role of "from any" transitions, e.g. to catch error conditions, implement timeouts, or force resets.
The parameters of the states() function thus describe the automaton in which a const <uint/int/str> parameter either defines a new state or defines state transitions from this state using transit().
act = states(tr00, tr01, …
, s1, tr10, tr11, …
, s2, tr20, tr21, …);
// Optional configuration
actX = states(..., { init: <sx>
, outIdx: <bool>
, ageVar: <str>
, dbgVar: <str>
});
| Property | Value | Description |
|---|---|---|
| init | <var> | Start state for the automaton, unless defined as the first |
| outIdx | <bool> | true: Output of the index of the active state <uint> false: Output of the identifier defined for the state <uint/int/str> |
| ageVar | <str> | The "age" of the active state in seconds can be published in the variable designated here. |
| dbgVar | <str> | The node number of the triggering transition is written to the variable specified here. "0" if no transition is pending. |
| storage | <str> | (in preparation) |
An action is the "output" of the FSM that occurs in a specific situation. There are four types of actions:
-
Input action: The action is executed/output when entering a state (regardless of which state transition was used to reach the state, if there are several).
-
Exit action: The action is generated when leaving a state (regardless of which state transition is used to leave the state).
-
Input action: The action is generated depending on the current state and the input. This means that several actions can be assigned to a state, which are executed depending on the state transition via which it is reached/left.
-
Transition action: The action is executed during a state transition.
If act = state(…) is a variable that maps the active state, the state-dependent actions can be implemented as follows:
-
Input action:
posedge(act == K) -
Output action:
negedge(act == K) -
Input action:
(act == K)
With the actVar: "TR" that can be defined in the transition, the following can also be implemented
- Transition actions: TR
can also be implemented.
Transitions "transit"2
The function transit() generates the transition objects that are passed as parameters to the state machine states(). The result of this function can only be processed in the function states().
-
sx: is the next state, provided that the transition condition is met. If sx is missing, the transition returns to the currently active initial state without restarting it. sx can also come from a calculation or a variable. If sx does not refer to a known state, the replacement value sFail is used.
-
cond: Condition for the state transition, def.: true. If a condition should only take effect after a minimum response time (switch-on delay / debouncing), it is recommended to pre-filter the condition with
delay(),trigger()orthreshold(). -
tBegin, tEnd: Time interval relative to the active time of the state in which the condition is evaluated. Def.:
states(..., transit({...}) // Transition to the initial state
, transit(sx, {...}) // Transition to sx
, transit(sx, cond) // ...with dynamic condition
, transit(sx, cond, tBegin) // ...valid from time
, transit(sx, cond, tBegin, tEnd) // ...valid in time interval
, ...);
// Optional configuration for all variants, unless
// explicitly required
transit(..., { tBegin: <dbl>
, tEnd: <dbl>
, sFail: <var>
, nMax: <int>
, nVar: <str>
, actVar: <str>
, edge: <int>
, prio: <int>
})
| Property | Value | Description |
|---|---|---|
| tBegin | <dbl> | Time in seconds relative to the active time of the state from which the condition is evaluated, def.: 0.0 |
| tEnd | <dbl> | Time in seconds relative to the active time of the state until which the condition is evaluated (exclusive), def.: |
| sFail | <var> | Replacement identifier / target state if sx does not designate a valid state |
| nMax | <int> | Maximum number of consecutive activations (loop counter). The counter is reset as soon as the target state of this transition is re-entered via an external or unrelated transition. For cleanly nested loops (first definition = inner loop) the counters do not interfere with each other. The jump target must be a static state label — a dynamic expression is treated as an error. |
| nVar | <str> | The loop counter can be published in the designated variable and is updated when the transition is triggered. |
| actVar | <str> | In the step in which the transition fires, true is output on the designated variable; otherwise false. |
| edge | <int> | Determines whether the transition is triggered statically (0), with posedge (1) or negedge (-1) of the condition. |
| prio | <int> | Optional priority if several transitions trigger simultaneously. In principle, the first with the highest prio level wins in the order of definition. |
The reset behavior of nMax is only defined for cleanly nested loops — i.e. when an inner loop always jumps back to its target state before its outer loop does. If the jump targets of two nMax transitions overlap (e.g. C→A and D→B in a chain A→B→C→D), the counter reset behavior is unpredictable.