Motorcortex Application Template

This section will explain you how to create a new poject and how the Motorcortex project template works.

7 minute read

In Clion there is a template avaiable to create a new C++ Motorcortex project. This template provides you a structure to get started creating a control application and will consist of the following ellements:

  • The config folder includes saved values of the module’s parameters and EtherCAT configuration.
  • The control folder contains a template of the main control loop Module. This where all the control computation happens.
  • The logic folder (optional) contains a template of the Finite State Machine to build advanced logic.
  • A main.cpp file, where all system and task configuration and linking of parameters happens. The generated main.cpp already has all the required code, necessary to run a Hard-Real Time application and communication server.

This section will explain how to create a new C++ Motorcortex project, and explain the elements provided by the template.

Creating a new C++ Motorxortex Project

  1. In the CLion main menu select File → New → C++ Motorcortex. Then enter a new location for you project and projectname. If your project requires a logic Tasks, check the box to generate a Logic template.

The generated template provided the following structure:

  • The config folder
  • The control folder
  • The logic folder (optional)
  • A main.cpp file

When developing remotely you will have to set your toolchain to remote host when creating a new project. When you use crosscomplilation ignore steps 2 and 3.

  1. In the Main Menu open go to File → Settings.

  1. go to CMake toolchain select remote host.

The Config Folder

Configuring your C++ Motorcortex project has to be done in the config folder. Here you can configurate EtherCAT-devices, Controller Configuration in the config.json file and manage your certificate.

control

In the control folder you will find the control.xml in this xml file you can.

io

In the io folder you can configurate your io devices. this has to be done configuring the master.xml and topology.xml

master.xml

topology.xml

config.json

demo-cert.pem

The Control Folder

The Logic Folder

Main.cpp

The main.cpp is

Programming Concepts

Modules and Tasks

A Motorcortex application is built around the concept of Modules and Tasks. A Module is an object that performs calculations based on inputs and internal variables and produces outputs. Inputs are either set directly from a higher level Module (via a setter function) or are set via the Parameter Tree.

Below a template for a Module header file is shown.

//Module header
#ifndef MAINCONTROLLOOP_H
#define MAINCONTROLLOOP_H
#include <mcx/mcx_core.h>

class MainControlLoop : public mcx::container::Module {
public:
  MainControlLoop() = default;
  ~MainControlLoop() override = default;
private:
  void create_(const char* name, mcx::parameter_server::Parameter* parameter_server, uint64_t dt_micro_s) override;
  bool initPhase1_() override;
  bool initPhase2_() override;
  bool startOp_() override;
  bool stopOp_() override;
  bool iterateOp_(const mcx::container::TaskTime& system_time, mcx::container::UserTime* user_time) override;
  double myDouble_{0};
};

#endif /* MAINCONTROLLOOP_H */

The module is a passive object, which means that it does not have an execution thread and must be executed by a Task object. A Task plays the role of an execution thread. Modules can be registered in the Task, which will allocate required resources, like CPU, scheduler policy, priority and start to execute modules sequentially in a loop.

The Module State Machine

All Motorcortex modules have a certain structure, which describes the life-cycle of the module. Modules go through different phases of the life-cycle during startup according to the schematic below.

   Module:                                        Task:
   ┌──────────────────────────────┐                        
   │   Not Initialized            │                 
   └───┬──────────────────────────┘               ┌─────────────────────────────────────┐    
       │ (Event: create)                          │ Creates modules and submodules.     │
   ┌───v───────┐                                  └───┬─────────────────────────────────┘
   │   Phase0  │                                      │ (Event: configure)
   └───┬───────┘                                  ┌───v─────────────────────────────────┐
       │ (Event: initPhase1)                      │ Add parameters to the tree          │  
       │                                          │                                     │           
   ┌───v───────┐                                  └───┬─────────────────────────────────┘
   │   Phase1  │                                      │ (Event: start)  
   └───┬───────┘                                  ┌───v─────────────────────────────────┐  
       │ (Event: initPhase2)                      │ Parameter tree is ready,            │
       │                                          │                                     │
   ┌───v──────────────────────────┐               └───┬─────────────────────────────────┘           
   │   Phase2                     │                   │    
   └───┬──────────────────────^───┘               ┌───v─────────────────────────────────┐
       │ (Event: startOp)     │ (Event: stopOp)   │  Real-time event loop is            │ 
       │                      │                   │  ready to start.                    │
   ┌───v──────────────────────┴───┐               └─────────────────────────────────────┘
   │   Operation                  │                  
   └───┬──────────────────────────┘  
       │ (Event: exit)
   ┌───v───────┐
   │ Destroyed │
   └───────────┘

Modules are first created by calling the Module’s create function. This in turn calls the create method of all sub-modules to register all the Modules in the Parameter Tree.

Then the Modules are added to a Task and the Task’s configure method is called. This calls initPhase1 of all the (sub-)modules. In initPhase1 all (sub-)modules shall add their Parameters to the Parameter Tree.

Now that the Tree is complete and running (Phase1), Parameter values can be loaded from a file.

Then the Task can be started by calling it’s start method. This calls the initPhase2 methods of all (sub-)modules and then startOp, just before the task switches to realtime mode.

In operation state, the system cyclically calls the Module’s iterateOp method that calculates the new state in each timestep. In the iterateOp method all submodules also need to be iterated.

Creating submodules (create)

Submodules are created in the create method of their parent Module, like shown below. The submodule then registers its own parameters and submodules in the tree.

void MainControlLoop::create_(const char* name,
  mcx::parameter_server::Parameter* parameter_server,
  uint64_t dt_micro_s) {
  createSubmodule(&mySubmodule, "mySubmodule");
}

Registering Parameters (initPhase1)

In initPhase1 the Module’s parameters need to be registered into the Parameter Tree.

bool MainControlLoop::initPhase1_() {
  using ParameterType = mcx::parameter_server::ParameterType;
  addParameter("input", ParameterType::INPUT,&input_);
  addParameter("gain", ParameterType::PARAMETER, &gain_);
  addParameter("output", ParameterType::OUTPUT, &output);
  return true;
}

The ParameterType can be set to one of the types as specified in paragraph Parameter Types in Motorcortex.

Optional callbacks for special cases (initPhase2, startOp, stopOp)

The initPhase2 method is in general not used. In special cases, it can be used for additional initializations, memory allocations, validations of the loaded parameters.

The startOp and stopOp methods can be used to set and reset module state during start/stop routine.

It is not recommended to put any delays or blocking calls in these callbacks, because they can cause a significant delay during module startup.

Iterating (iterateOp)

Calculations shall be done in the Module’s iterateOp method. The iterateOp method of the top-level Module is called cyclically by the corresponding Task. The top-level module then iterates submodules from its own iterateOp method. For instance:

bool MainControlLoop::iterateOp_(const container::TaskTime& system_time, container::UserTime* user_time) {
   submodule.setInput(input);
   submodule.iterate(system_time, user_time);
   double subOutput = submodule.getOutput();
   output_ = gain_ * input_ + subOutput * getDtSec();
   return true;
}

Usually, for a submodule, first the inputs are set, then the module is iterated, then the outputs are read.

For a lot of digital control systems the step size is required in calculations (e.g. for calculating filters or integrators). The application cycle time in seconds can be obtained by calling the getDtSec() function. The getDtSec function returns the configured cycle time of the Task, not the actual cycle time (which may vary a small amount each cycle).

Overriding inputs and outputs

If the Module’s inputs are set via the Parameter Tree, the inputs can be overridden (forced) at runtime. Outputs can also be overridden before they are updated into the Parameter Tree. The output overrides have no effect on the actual variable value of the module. Also a parameter tree input might have no effect if its associated variable is set directly via a setter function.

Application Configuration.

Tasks

Tasks provide the execution environment of Motorcortex Modules. Each Task can manage multiple Modules. A Task also switches the Modules through the Module State Machine.

The iterate methods of all the Modules inside a Task are called cyclically with the update rate that was specified when the Task is created. The Modules are iterated sequentially in the order they were added to the Task.

 // creates a Module
 mcx::log::Module myModule();
 myModule.create("myModule", &param_server, rt_dt_micro_s);
 // create and configure the task
 mcx::container::Task myTask("myTask", &param_server);
 myTask.add(&myModule);
 myTask.configure();
 ...
 myTask.start(rt_dt_micro_s, container::TaskSched::REALTIME, {0}, 80);

The Logger Task

The Logger is a standard Module that provides logging functions for other Modules to write messages to certain log-levels. The standard log levels are:

  • LOG_DEBUG
  • LOG_INFO
  • LOG_WARNING
  • LOG_ERROR

The log is accessible from inside any module by calling the respective log function. E.g. LOG_INFO(“%d red balloons”,99). The LOG_* functions provide similar syntax to the standard C function printf. An example how the logger Task is created is shown below:

 // creates log output to a file
 mcx::log::Module logger(path_log);
 logger.create("logger", &param_server, rt_dt_micro_s);
 // create and configure log output task
 mcx::container::Task logger_task("Logger_task", &param_server);
 logger_task.add(&logger);
 logger_task.configure();
Last modified March 23, 2021: Restructured GRID (44d0658)