Controllers

This section contains the description of all controllers implemented in VSlib.

General interface

All controllers described in further detail below, the RST and PID, are both derived from and implement the Component interface. They have a number settable Parameters that need to be defined before the component can be used.

The two distinct components serve only as an interface for your convenience, as they both use the same engine, that of an RST controller. Both components include a LimitRange component to provide saturation protection and ensure that the actuation provided by the controller falls within the expected range.

Both components have a single access method to return the next double-type actuation value called control, that takes two arguments of type double: current reference (set-point) and current measurement (process value) values. In case of and RST, the control method will return 0.0 until the buffers with input histories for measurement and references are filled, which is equal to the order of the controller. The algorithm to calculate the next actuation is the following:

\[u_{k} = \frac{1}{S_{0}} \left[ \sum_{i=0}^{n_{T}} T_{i} r_{k-i} - \sum_{i=0}^{n_{R}} R_{i} y_{k-i} - \sum_{i=1}^{n_{S}} S_{i} u_{k-i} \right],\]

where: u are actuations, r are references, and y are measurements.

The input history buffers can also be filled manually by calling updateInputHistories method, taking two double-type arguments with reference and measurement values respectively.

The anti-windup is triggered automatically when the resulting actuation value is outside the limits specified by the internal LimitRange component. It can be also triggered manually, by calling updateReference method. The method takes one argument of type double with a new value of the actuation, which it uses to back-calculate what would have been the reference value, given the known measurement, to result in that exact actuation value. The back-calculation uses the following formula, following libreg’s implementation:

\[r^{*}_{k} = \frac{1}{T_{0}} \left[ \sum_{i=0}^{n_{S}} S_{i} u^{*}_{k-i} + \sum_{i=0}^{n_{R}} R_{i} y_{k-i} - \sum_{i=1}^{n_{T}} T_{i} r^{*}_{k-i} \right].\]

Both components implement a reset method that clear the cached histories of measurements, references, and actuations to bring each component to its initial state.

RST

The RST component has a template parameter Order that allows you to define the order of the controller. It has three settable Parameters: R, S, and T polynomial coefficients, stored as std::array<double, Order+1>, and the aforementioned LimitRange-type component called actuation_limits. All Parameters need to be defined before the controller can be used, even if the actuation limiter is not intended to be used. In that case, it is recommended to set its min and max Parameters outside of the operational range of the controller, e.g. numerical limits of the double type.

The provided input R, S, and T coefficients, as well as min and max Parameters for the actuation_limits internal component are validated after any of the Parameters is modified. The validation includes checks internal to the actuation_limits Component and specific to R, S, and T. Neither of the polynomial coefficient arrays’ first element can be equal to 0.0. In addition, the S and T coefficients are verified for stability using Jury’s stability test:

  1. Sum of odd-index coefficients must be larger than sum of even-index coefficients

  2. Sum of even and odd coefficients divided by the sum of absolute values of coefficients must be less than a negative value for floating point precision (equal to -1e-7)

  3. Roots of the polynomial must lie within the unit circle.

In case any issue is found during validation, the offending Parameter will not be accepted and cannot be used by this controller. If everything is correct, the defined R, S, and T values are used directly by the RST controller engine.

For more details regarding the API, see the API documentation for RST.

Usage examples

Features example in an example function (not real-time task in vloop):

#include "rst.h"
#include "rootComponent.h"

using namespace vslib;

void your_function(RootComponent& root)
    RST<2> rst("rst_1", root); // 2nd order RST

    // set three-element array of R, S, and T to desired value
    // here, assuming:
    // r = {3.0015005, -5.999999, 2.9985005};
    // s = {1.001, -2, 0.999};
    // t = {4.0025005, -7.999999, 3.9975005};
    // actuation limit at numerical limits

    const double set_point_value   = 3.14159;
    const double measurement_value = 1.111;

    const double expected_actuation = ((t[0] + t[1] + t[2]) * set_point_value - (r[0] + r[1] + r[2]) * measurement_value) / s[0];

    auto actuation = rst.control(set_point_value, measurement_value); // 0.0
    auto ready = rst.isReady();                                       // false
    actuation = rst.control(set_point_value, measurement_value);      // 0.0
    ready = rst.isReady();                                            // false
    actuation = rst.control(set_point_value, measurement_value);      // expected_actuation's value
    ready = rst.isReady();                                            // true

    // reset between not-connected uses to clear cached data
    rst.reset();

    // update histories manually:
    ready = rst.isReady(); // false
    rst.updateInputHistories(set_point_value, measurement_value);
    ready = rst.isReady(); // false
    rst.updateInputHistories(set_point_value, measurement_value);
    ready = rst.isReady(); // true

    // trigger anti-windup calculation:
    const double limited_actuation = actuation - 1.0; // force clamping
    rst.updateReference(limited_actuation);
}

Example usage in a vloop:

#include "vslib.hpp"

namespace fgc::user
{
    class Converter : public vslib::RootComponent
    {
    public:
        Converter() noexcept
        : vslib::RootComponent("example"),
          interrupt_1("stg", *this, 128, vslib::InterruptPriority::high, RTTask),
          rst_1("rst_1", *this)
        {
        }

        // Define your interrupts here
        vslib::PeripheralInterrupt<Converter> interrupt_1;

        // Define your public Components here
        vslib::RST<2> rst_1;

        void init() override
        {
            interrupt_1.start();
        }

        void backgroundTask() override
        {
        }

        static void RTTask(Converter& converter)
        {
            // Read the reference and measurement values:
            const double reference   = converter.m_data[0];
            const double measurement = converter.m_data[1];

            const auto act = converter.rst_1.control(reference, measurement);
            // use the act
        }

        private:
            // actual source of data omitted for simplicity
            std::array<double, 2> m_data{0.0};
    };
}   // namespace fgc::user

PID

PID controller component implements the two-degrees of freedom controller functionality. This component provides a PID interface, while internally, the control is calculated based on the 2nd order RST algorithm described in the general interface section.

This controller contains a previously mentioned LimitRange component to clamp the actuation, as well as a number of Parameters, all of type double:

  1. kp - Proportional gain coefficient

  2. ki - Integral gain coefficient

  3. kd - Derivative gain coefficient

  4. kff - Feed-forward scaling coefficient

  5. b - Reference signal proportional gain scaling

  6. c - Reference signal derivative gain scaling

  7. N - Filter order for derivative input

  8. T - Control period

  9. f0 - pre-warping frequency

When any of the Parameters changes value, it triggers validation workflow. In that workflow, the Parameters 1-9 are recalculated to R, S, and T polynomial coefficients according to the following formula, in a general case when \(k_{p} \ne 0 \ne k_{d}\):

\[\begin{split}\begin{align} R_{0} &= \frac{1}{a^{2}}\left( k_{i} \cdot k_{p} \cdot N + k_{d} \cdot k_{i} \cdot a + k_{d} \cdot k_{p} \cdot a^{2} + k_{p}^{2} \cdot N \cdot a + k_{d} \cdot k_{p} \cdot N \cdot a^{2} \right) \\ R_{1} &= \frac{2}{a^{2}}\left( k_{i} \cdot k_{p} \cdot N - k_{d} \cdot k_{p} \cdot a^{2} - k_{d} \cdot k_{p} \cdot N \cdot a^{2} \right) \\ R_{2} &= \frac{1}{a^{2}}\left( k_{i} \cdot k_{p} \cdot N - k_{d} \cdot k_{i} \cdot a + k_{d} \cdot k_{p} \cdot a^{2} - k_{p}^{2} \cdot N \cdot a + k_{d} \cdot k_{p} \cdot N \cdot a^{2} \right) \\ \\ S_{0} &= \frac{1}{a^{2}} \left( k_{d} \cdot a^{2} + k_{p} \cdot N \cdot a \right) \\ S_{1} &= -2.0 \cdot k_{d} \\ S_{2} &= \frac{1}{a^{2}} \left( k_{d} \cdot a^{2} - k_{p} \cdot N \cdot a \right) \\ \\ T_{0} &= \frac{1}{a^{2}} \left( k_{i} \cdot k_{p} \cdot N + k_{d} \cdot k_{i} \cdot a + k_{d} \cdot k_{ff} \cdot a^{2} + k_{d} \cdot k_{p} \cdot a^{2} \cdot b \ + \right. \\ &\quad \quad \quad \left. k_{p}^{2} \cdot N \cdot a \cdot b + k_{ff} \cdot k_{p} \cdot N \cdot a + k_{d} \cdot k_{p} \cdot N \cdot a^{2} \cdot c \right) \\ T_{1} &= \frac{2}{a^{2}} \left( k_{i} \cdot k_{p} \cdot N - k_{d} \cdot k_{ff} \cdot a^{2} - k_{d} \cdot k_{p} \cdot a^{2} \cdot b - k_{d} \cdot k_{p} \cdot N \cdot a^{2} \cdot c \right) \\ T_{2} &= \frac{1}{a^{2}} \left( k_{i} \cdot k_{p} \cdot N - k_{d} \cdot k_{i} \cdot a + k_{d} \cdot k_{ff} \cdot a^{2} + k_{d} \cdot k_{p} \cdot a^{2} \cdot b - k_{p}^{2} \cdot N \cdot a \cdot b \ \right. \\ &\quad \quad \quad \left. - k_{ff} \cdot k_{p} \cdot N \cdot a + k_{d} \cdot k_{p} \cdot N \cdot a^{2} \cdot c \right), \end{align}\end{split}\]

where: \(a = \frac{2\pi f_{0}}{tan(\pi f_{0} T)}\). When the general case does not apply, and you have an integrator-only controller, the following set of equations is used instead:

\[\begin{split}R_{0} &= \frac{k_{i}}{a} \\ R_{1} &= \frac{k_{i}}{a} \\ R_{2} &= 0 \\ \\ S_{0} &= 1 \\ S_{1} &= -2 \\ S_{2} &= 0 \\ \\ T_{0} &= \frac{k_{i}}{a} + k_{ff} \\ T_{1} &= \frac{k_{i}}{a} - k_{ff} \\ T_{2} &= 0\end{split}\]

Then, the R, S, and T arrays of polynomial coefficients are set to the underlying RST controller, and a validation described in the RST section is performed.

For more details regarding the API, see the API documentation for PID.

Usage examples

Features example in an example function (not real-time task in vloop):

#include "pid.h"
#include "rootComponent.h"

using namespace vslib;

void your_function(RootComponent& root)
    PID pid("pid", root);

    // set three-element array of R, S, and T to desired value
    // here, assuming:
    // kp  = 0;
    // ki  = 0.0472;
    // kd  = 0;
    // kff = 6.1190;
    // b   = 0.03057;
    // c   = 0.8983;
    // N   = 17.79;
    // T   = 1.0e-3;
    // f0  = 1e-15;
    // actuation limit at numerical limits

    const double set_point_value   = 3.14159;
    const double measurement_value = 1.111;

    auto actuation = pid.control(set_point_value, measurement_value); // 0.0
    auto ready = pid.isReady();                                       // false
    actuation = pid.control(set_point_value, measurement_value);      // 0.0
    ready = pid.isReady();                                            // false
    actuation = pid.control(set_point_value, measurement_value);
    ready = pid.isReady();                                            // true

    // reset between not-connected uses to clear cached data
    pid.reset();

    // update histories manually:
    ready = pid.isReady(); // false
    pid.updateInputHistories(set_point_value, measurement_value);
    ready = pid.isReady(); // false
    pid.updateInputHistories(set_point_value, measurement_value);
    ready = pid.isReady(); // true

    // trigger anti-windup calculation:
    const double limited_actuation = actuation - 1.0; // force clamping
    pid.updateReference(limited_actuation);
}

Example usage in a vloop:

#include "vslib.hpp"

namespace fgc::user
{
    class Converter : public vslib::RootComponent
    {
    public:
        Converter() noexcept
        : vslib::RootComponent("example"),
          interrupt_1("stg", *this, 128, vslib::InterruptPriority::high, RTTask),
          pid_1("pid_1", *this)
        {
        }

        // Define your interrupts here
        vslib::PeripheralInterrupt<Converter> interrupt_1;

        // Define your public Components here
        vslib::PID pid_1;

        void init() override
        {
            interrupt_1.start();
        }

        void backgroundTask() override
        {
        }

        static void RTTask(Converter& converter)
        {
            // Read the reference and measurement values:
            const double reference   = converter.m_data[0];
            const double measurement = converter.m_data[1];

            const auto act = converter.pid.control(reference, measurement);
            // use the act
        }

        private:
            // actual source of data omitted for simplicity
            std::array<double, 2> m_data{0.0};
    };
}   // namespace fgc::user

Performance

Performance of the controllers depend on their order and the frequency at which the internal automatic anti-windup mechanism is engaged. The general numbers that can be expected for the two controllers are presented in the table below and also as a figure.

Class

Order

Anti-windup

Access time [ns]

PID

2

None

64

PID

2

Frequent

102

RST

1

None

34

RST

2

None

59

RST

3

None

64

RST

4

None

68

RST

5

None

74

RST

6

None

80

RST

7

None

91

RST

8

None

90

RST

9

None

113

RST

10

None

123

RST

11

None

132

RST

12

None

135

RST

13

None

150

RST

14

None

177

RST

15

None

180

RST

16

None

211

Controllers performance depending on the order