ControlWare Tutorial

  1. Introduction
  2. How to implement a component
    1. What's a component

    2. What's a Module
    3. Dynamic module
    4. Static module
  3. How to Use ControlWare
    1. Add into Application Function Callc to ControlWare
    2. Link application with the runtime library
    3. Configure ControlWare
  4. Component Library
    1. squareWaveGen
    2. rampGen
    3. comparer
    4. PIController
    5. tee
    6. acceptRate
    7. acceptRatio
    8. netsensor
  5. A Complete Example
    1. Collect Data for System Identification
    2. Decide the Model and Design the Controller
    3. Put the System Under Control

1. Introduction

ControlWare is designed to help the programmers apply control theory to control software performance. It enables the programmer to test different control schemes without recompiling the program. It also help reuse the same control scheme in different applications.

A basic abstraction provided by ControlWare is component. Sensor, actuator and controller are all examples of component. The component can be connected with each other and a control loop is just a group of properly connected components. Which components are used in a control scheme and how they are connected with each other are described in a configuration file, thus separated from the application. Also, the components don't have to be linked with the application. All these combined make it possible to experiment with different component or control scheme without modifying the application and recompilation.

Also, the components can be stored in a component library and reused by different applications. We have populate the library with some of the most commonly used components

To use ControlWare, programmers need

  1. Implement or choose from the library the components needed
  2. Insert some function call into the application code
  3. Configure ControlWare
  4. Link the program with the library

2. How to Implement a component

2.1. What's a Component

A component can have several input ports, output ports and some parameters. An input port must be connected to one and only one output port of some other component. However, an output port can be connected to several components' input ports. Parameter value is supplied via configuration file. The task of a component is to read data from its input port(s) and parameters, and calculate its output, which will be accessed by other components connected to it.

According to how it's invoked by ControlWare, the component can be classified into reactive and proactive. A reactive component exports a callback function produceData, which will be periodically invoked by ControlWare to produce the component's output. On the contrary, a proactive component does not export such a callback function and it will produce the output periodically. In most cases, it may create a process or thread to help achieve this.

2.2. What's a Module

The module is the physical implementation of a component. Module name is the same as the name of the component it implements. Each module has to export several callback functions and other information. Most of the callback function are required to return -1 in case of error and 0 otherwise.

Suppose a reactive component A is implemented in module A, the typical invocation sequence is:

  1. module A is loaded, A->initModule() is called to initialize the module.
  2. the first instance I1 of the component A is created.
    the second instance
    I2 is created.
  3. for each parameter value of instance I1 specified in configuration file, A->setParam(I1, ...) is called to parse the value.
    for each parameter value of instance
    I2 specified in configuration file, A->setParam(I2, ...) is called to parse the value.
  4. A->initInstance(I1) is called.
    A->initInstance(I2) is called.
  5. ControlWare is started. Periodically, A->produceData(I1) and A->produceData(I2) are called.
  6. ControlWare is stopped.
    A->cleanupInstance(I1)
    is called. Instance I1 is destroyed.
    A->cleanupInstance(I2) is called. Instance I2 is destroyed.
    A->cleanupModule() is called. Module A is unloaded.

According to the implementation, there are two types of module: static module and dynamic module.

2.3. Dynamic Module

A dynamic module is not linked into the application, and can be loaded/unloaded at runtime as needed. It enables the sharing of some components among different applications. We will use the tee component provided in the library as an example. The most important rules are highlighted using red color.

Tee has one input port, one output port and one parameter. Its functionality is similar to the Unix tee command: to forward the input to the output and save the data to a log file at the same time. The name of the log file is specified by its parameter.

Step 1. Declare a structure to representing the component. It can have as many fields as needed to store some data private to the component. But, the first field of the structure must be of type COMPONENT. ControlWare stores some internal information in this field.

typedef struct {
    COMPONENT c;
    FILE *fLog;
} TEE;
 

Step 2. Initialize a global variable of type MODULE to export the required information. The following is the definition of the structure:

typedef struct {
    unsigned short nInstSize;
    unsigned char nInputPortCnt, nOutputPortCnt, nParamPortCnt;
    int (*init) (void);
    int (*cleanup) (void);
    int (*produceData) (struct tagComponent *);
    int (*initInstance) (struct tagComponent *);
    int (*cleanupInstance) (struct tagComponent *);
    int (*setParam) (struct tagComponent *, int, char *);
    char *(*getParamString) (struct tagComponent *, int);
} MODULE_DESC;

typedef struct tagModule {
    int nMagicNum;
    int nRefCount;
    void *pvHandle;
    char *szModName;
    struct tagModule *pNext;
    /* the above fields must be initialized using DEFAULT_DM_HEADER */
    MODULE_DESC desc;
} MODULE;

 The name of this global variable must be the name of the component (or module, remember that they are the same), i.e. tee in this example. And always use the macro DEFAULT_DM_HEADER as the first value. Following that is the size of the structure representing the component, i.e. sizeof(TEE). The other fields are self-explaining.

MODULE tee = {
    DEFAULT_DM_HEADER,
    {
     sizeof(TEE),
     1/* 1 input port */, 1/* 1 output port */, 1/* 1 parameter */,
     initModule,
     cleanupModule,
     produceData,
     initInst,
     destroyInst,
     setParam,
     getParamString}
};

Step 3. Implement the callback functions.

In this example, initModule() and cleanupModule() simply return 0 to indicate success.

setParam accepts 3 parameters: pointer to the component, which parameter's value is to be parse and the parameter value. It returns 0 to indicates success and -1 otherwise. All the parameter values of a component are stored in a array: aParams[]. The element of the array is a union:

typedef union {
    char c;
    unsigned char uc;
    short s;
    unsigned short us;
    int i;
    unsigned int ui;
    long l;
    unsigned long ul;
    long long ll;
    unsigned long long ull;
    float f;
    double d;
    void *p;
} PARAMETER;

In this example, the parameter value, a file name,  is duplicated and stored in its first element of the parameter array.


int setParam(COMPONENT * pc, int nWhich, char *str)

{
    if (nWhich == 0) {
        (char*)(pc->aParams[0].p) = strdup(str);
        return 0;
    } else {
        printf("Error at line %d, extra parameters, only need 2\n", getLineNo());
        return -1;
    }
}


initInst and destroyInst take one parameter: pointer to the component. Since it's required that the first field of the component structure be of COMPONENT type, it's always safe to cast the pointer to your own component structure, i.e. TEE * in this example.  Here, initInst simply open the file and store the handle for further reference. And destroyInst simply close the log file.

Note: since more than one instance of tee component can be created, the file handle can not be stored in a static variable.


#define FILE_NAME(pc) ((char*)(pc->aParams[0].p))

int initInst(COMPONENT * pc)
{
    TEE *pl = (TEE *) pc;

    TRACE2("Module %s init instance %s\n", MOD_NAME, pc->szName);
    if (NULL == (pl->fLog = fopen(FILE_NAME(pc), "wt"))) {
        fprintf(stderr, "Can't create data file:%s\n", FILE_NAME(pc));
        return -1;
    }

    return 0;
}

int destroyInst(COMPONENT * pc)

{
    TEE *pl = (TEE *) pc;

    TRACE2("Module %s destroy instance %s\n", MOD_NAME, pc->szName);
    fclose(pl->fLog);
    free(FILE_NAME(pc));
    return 0;
}

Finally comes the produceData callback function. It also takes one parameter: pointer to the component. Inside this callback function, component must use readInput to read its input port and writeOutput to publicize its output.

int produceData(COMPONENT * pc)
{
    TEE *pl = (TEE *) pc;
    double d;

    if (pl->fLog == NULL)
        return -1;

    if (readInput(pc, 0, &d) < 0)
        printf("\t%s Cannt read 0th input\n", pc->szName);
    else
        TRACE2("\t%s 0th input = %.2f\n", pc->szName, d);

    fprintf(pl->fLog, "%.5f\n", d);
    fflush(pl->fLog);

    writeOutput(pc, 0, d);
    TRACE2("\t%s produce output: %.5f\n", pc->szName, d);
    return 0;
}

Step 4. compile the code. The following command line is used to compile it (Only applies to Linux + gcc. Other combinations may requires different options). -shared option must be specified and the output file name must be the component name suffixed by '.com', i.e. tee.com in this example.

gcc -shared -o tee.com tee.c -g -Wall -I $(CONTROLWARE_INCLUDE_DIR)

2.4. Static Module

Static module is part of the application, thus can't be loaded and unloaded and can't be shared by other applications. Except this, it's similar to dynamic module.

Step 1. same as that of dynamic module

Step 2. initialize a variable md of type MODULE_DESC to export the required information. Why MODULE_DESC instead of MODULE? Well, good question! I don't remember what drove me to make such stupid (at least to me, now) design decision :-(. Maybe it will be fixed in next version.

Step 3. same as that of dynamic module

Step 4. In the application code, call registerModule(moduleName, &md) to register this module after calling initControlWare() and before calling startControlWare()

3. How to Use ControlWare

The first step has already been covered before.

3.1. Add into Source Code Function Calls to ControlWare

Basically, the change to the application code is not very intrusive. The following is the typical code added into the application's initialization code (registerModule call is not needed if no static module is implemented by the application):

#include "controlWare.h"
#include "moduleMan.h"

if
(initControlWare() < 0){
    fprintf(stderr, "ControlWare initialization error\n");
    exit(-1);
}

if (registerModule("my_sensor", &s_mdMySensorModule) < 0 ||
    registerModule("my_actuator", &s_mdMyActuatorModule) < 0){
    fprintf(stderr, "Can't start ControlWare\n");
    exit(-1);
}

if (startControlWare("./controlware.conf") < 0){
    fprintf(stderr, "Can't start ControlWare\n");
    exit(-1);
}

The parameter of startControlWare is the file name of the configuration file. And the following is the code added into the application's cleanup code:

#include "controlWare.h"
cleanupControlWare();

3.2. Link application with the runtime library

Suppose ControlWare runtime library is located in directory /home/rz5b/controlWare/kernel, and its include files are in directory /home/rz5b/controlWare/include. The following gcc command options need to be specified:

        -I/home/rz5b/controlWare/include -L/home/rz5b/controlWare/kernel -lcw -lpthread -ldl -rdynamic

Again, this only applies to Linux + gcc. Other combination may need different options.

3.3. Configure ControlWare

A lot of information important to ControlWare is supplied by a configuration file. Its file name is given as a parameter of startControlWare function call. Actually, without this configuration file, ControlWare can _not_ do anything. The configuration  file is divided into three sections: global option, control loop description and component parameter values. The order is:

    global option
    control loop descriptions
    component parameter values

Global Option

DirServer:  ControlWare supports distributed control loop, i.e. the components can be on different machines. To run in a distributed way, a directory server is needed, whose address is specified using this option. If no directory server is needed, set its value to be None or NoServer . (NOTE: there are some quirks about running distributed control, which are not covered here. Contact me if you are really interested in it) Example:

    DirServer tarek5.cs.virginia.edu
    DirServer None

ModulePath: this option specifies the paths for dynamic module . It's a colon-separated list of directories in which ControlWare looks for dynamic module. If not specified, current directory is used. Example:

    ModulePath /home/rz5b/controlWare/lib:./:

Period: this option controls the period (in second) ControlWare invokes the reactive comonents' callback function to produce output. The default value is 30 seconds. Example:

    Period 10

Delay: how many seconds should pass from the moment startControlWare() is called to the moment ControlWare actually starts. The default value is 30 seconds, and the minimum is 0. Example:

    Delay 10

Control Loop Description

More than one control loop can be specified in this section. The syntax of the description is the following (words in <> is what users should supplied) :

    QoS <control loop name>
        CREATE <component instance name> AS <component/module name>;
           ......
       CONNECT <instance 1>.[<output port number>] TO <instance 2>.input[<input port number>];
           ......
    QoS END

The first part is to specify the component instances to be created. To create a component, the module implementing it should be first loaded into memory. ControlWare first searches currently already loaded modules, including the static modules registered using registerModule(), and then it searches the directories specified by ModulePath option. The name of the instances must be unique, even if they may belong to different control loop. Example:

        CREATE sensor1 AS cpu_sensor;

requires ControlWare to create an instance of the component cpu_sensor, and the name of the instance is sensor1.

The second part is to specify the connectivity of the components. The components mentioned in the second part must have been created. Example:

       CONNECT sensor1.[0] TO tee.input[0];

connects the first output port of sensor1 to the first input of tee.

Component Parameter Values

The parameter values for the components created are supplied here. Each component occupies one line and the parameter values are separated by semicolon. The format is:

        COMPONENT <component instance name> <param0>, <param1>,...

Example:

        COMPONENT tee ./sensor1.log

4. Component Library

4.1. squareWaveGen

Functionality:    generate square wave signal
Input Port:          N/A
Output Port:       1
        port 0:          the signal generated
Parameter:         2
        param 0:     lower bound of the signal value
        param 1:     upper bound of the signal value

4.2. rampGen

Functionality:    generate ramp signal, the signal increases from its minimum to its maximum in specified sampling periods
Input Port:          N/A
Output Port:       1
        port 0:          the signal generated
Parameter:         2
        param 0:     the signal value
        param 1:     upper bound of the signal value
        param 2:     period

4.3. comparer

Functionality:    calculate the difference between the input and the reference value
Input Port:          1
Output Port:       1
        port 0:          the difference between the input and the reference value
Parameter:         1
        param 0:     the reference value

4.4. PIController

Functionality:    a standard PI controller. The output is calculated using the following formula:
                GAIN * (error - ZERO * previous error) + previous output
Input Port:          1
        port 0:          error, usually connected to the output of a comparer
Output Port:       1
        port 0:          controller's output, usually connected to the input of an actuator.  
Parameter:         5
        param 0:     ZERO
        param 1:     GAIN
        param 2:     initial output
        param 3:     initial error
        param 4:     lower bound of the output. Output can never be smaller than it
        param 5:     upper bound of the output. Output can never be larger than it.

4.5. tee

Functionality:    forward the input to the output and record the data to a file at the same time
Input Port:          1
Output Port:       1
        port 0:          the input
Parameter:         1
        param 0:     the file name of the log

4.6. acceptRate

Functionality:    an actuator capable of changing the accept rate of a listening port according to its input
Input Port:          1
        port 0:         new accept rate
Output Port:       N/A
Parameter:         1
        param 0:     the port number
        param 1:     initial accept rate
        param 2:     is it blocking

4.7. acceptRatio

Functionality:    an actuator capable of changing the accept ratio of two listening ports according to its input
Input Port:          1
        port 0:          new accept ratio
Output Port:       N/A
Parameter:         1
        param 0:     the first port number
        param 1:     the second port number
        param 2:     initial accept ratio
        param 3:     is it blocking

4.8. netsensor

Functionality:    a sensor reporting various network statistics related to a listening port
Input Port:          N/A
Output Port:       5
        port 0:         bytes sent out from all the connections accepted from this port
        port 1:         bytes received from all the connections accepted from this port
        port 2:         number of the connections accepted from this port
        port 3:         average connection delay
        port 4:         number of connection waiting to be accepted
Parameter:         1
        param 0:     the port number
        param 1:     when 1, output 0 ~ 3 are the total since the system is running. Otherwise, they are the value since the last time the output is produced.

5. A Complete Example

Here we give a complete example to demonstrate the whole procedure. The source code and configuration file can be found here. Plant.c simulates a second-order system. It also includes two static modules: dummySensor, dummyActuator. To compile it, first modify the variables in makefile to be the correct path, and then simply type make. You also need to modify ModulePath option in the configuration files to be the correct directory where the components reside.

5.1. Collect Data for System Identification

To control this system, we first need to identify the system model. Config.sysid is used to configure ControlWare to collect data for system identification. A white noise generator is attached to the system, and the output of the sensor and noise generator are stored into files: sensor_log and signal_log, respectively.

    ./plant config.sysid

will start collect data. Keep the program running for around 5 minutes, and then stop it. Now we have two data files: sensor_log and signal_log.

5.2. Decide the Model and Design the controller

Run Matlab to do system identification based on the data files generated in previous step, and you will get the following model:

         0.5976z - 0.2092
    -------------------------
    z^2 - 0.01951z - 0.002892

Suppose we are going to use PI controller. Then, we can determine PI controller's parameters using root locus technique. The parameter values which I choose are: 0.3 (ZERO) and 1.1 (GAIN).

5.3.  Put the System under Control

Now, we can configure ControlWare to control the system. Config.ctrl is the configuration file used to run the system with controller. It creates a PI controller and configures it using the values derived in previous step. Use

    ./plant config.ctrl

to verify that the output of the system is actually kept around the set point.

Note This is an over simplified example specially designed for demonstration. Real application may need more sophisticated model and controller.

back to home