This page explains the different steps to create a reusable robot driver. For more informations on EtherCAT specifications please read this page.

Step 0 : Install and configure PID

Before starting to create a robot driver, you have to install PID, ethercatcpp-core and all others ethercatcpp packages you need to use the device. In this example, we need ethercatcpp-core, ethercatcpp-epos and ethercatcpp-clipx. Then you can use an exiting package or create a package with :

cd <pid-worskspace>/pid
make create package=ethercatcpp-<robot_name> url=git@gite.lirmm.fr:own/ethercatcpp-<robot_name>.git

You have to declare dependency of all devices drivers packages in the root CMakelists.txt file of your package, after the package declaration, You have to write something like:

PID_Dependency(ethercatcpp-<name> VERSION ...)

with tag name replaced by the name of the package and version with the released version of that package your are using. In our example, we use ethercatcpp-core, ethercatcpp-epos and ethercatcpp-clipx. So, we have to declare:

PID_Dependency(ethercatcpp-core VERSION 3.0)
PID_Dependency(ethercatcpp-epos VERSION 3.0)
PID_Dependency(ethercatcpp-clipx VERSION 3.0)

Now you have to declare your component as a “shared library” and its dependency to all packages used in the CMakeLists.txt files:

PID_Component(
  ethercatcpp-<robot-name>
  CXX_STANDARD 11
  EXPORT  posix 
          ethercatcpp/core
          ethercatcpp-<name>/ethercatcpp-<name>
)

In our example, we have ethercatcpp-core, ethercatcpp-epos and ethercatcpp-clipx:

PID_Component(
  ethercatcpp-myrobot
  CXX_STANDARD 11
  EXPORT  posix
          ethercatcpp/core
          ethercatcpp/epos
          ethercatcpp/clipx
)

An other important point before starting is that all robot drivers are composed by many devices. So, they must inherit from BusDevice class. Also don’t forget including all needed headers. Here is the class declaration of example robot:

#pragma once
#include <ethercatcpp/core.h>
#include <ethercatcpp/epos.h>
#include <ethercatcpp/clipx.h>

namespace ethercatcpp {
  class Robot : public BusDevice
  {
  public:
    static constexpr size_t DOFS=6;
    static constexpr size_t CONTROLLED_DOFS=4;
    
    using state_data_t = std::array<double, DOFS>;
    using ctrl_data_t = std::array<double, CONTROLLED_DOFS>;

    Robot(double control_period);
    ~Robot()=default;

    void set_target_torque(const ctrl_data_t&);
    const state_data_t& get_actual_position() const;
    const state_data_t& get_actual_velocity() const;
    const state_data_t& get_actual_torque() const;

  private:
    double passive_encoder_resolution_;
    // Define the robot hardware components
    EK1100 ek1100_ ;
    EL5101 el5101_0_;
    EL5101 el5101_5_;
    Epos3 m1_;
    Epos3 m2_;
    Epos4 m3_;
    Epos4 m4_;
    ClipX clipx_;
    //define state variables
    state_data_t position_, velocity_,torque_;
      
  };
}

Step 1 : Declare all devices

First of all, all devices have to be define and declared.

  • in robot.h private area: declare all devices present in your robot. For example:
  ...
  // Define the robot hardware components
  EK1100 ek1100_ ;
  EL5101 el5101_0_;
  EL5101 el5101_5_;
  Epos3 m1_;
  Epos3 m2_;
  Epos4 m3_;
  Epos4 m4_;
  ClipX clipx_;

A probably better solution would be to hide those declarations into source code by using pimpl idiom.

  • in robot.cpp constructor: initialize each devices declared. For example:
Robot::Robot(double control_period) :
  BusDevice{},
  ek1100_{},
  el5101_0_{},
  el5101_5_{}
  m1_{Epos3::gen_options(control_period, -M_PI, M_PI)},
  m2_{Epos3::gen_options(control_period, -M_PI, M_PI)},
  m3_{Epos4::gen_options(control_period, -M_PI, M_PI)},
  m4_{Epos4::gen_options(control_period, -M_PI, M_PI)},
  clipx_{}
  {...}

In the example the robot is made of one ethercat BUS with Epos3 and Epos4 devices used to control motors, 2 EL5101 terminal mounted on the EK1100 ethercat head used for managing additional encoders and a ClipX force sensor device.

Epos devices are quite complex and must be initialized with options. Base options are the control period (passed as argument), and software limits for joints.

Step 2 : Declare robot bus

Now we need to set hardware device order on the ethercat BUS. To do this, you have to write this order in robot.cpp constructor. Be careful in order declaration. For example:

Robot::Robot() :
  ...
  {  
    add(ek1100_);//the EK1100 is the first device of the hardware bus
    add(el5101_0_);
    add(el5101_1_);
    add(m1_);
    add(m2_);
    add(m3_);
    add(m4_);
    add(clipx_);// ClipX is the last device of the hardware bus.    
  }

This is exactly the same logic as adding devices to the ethercat master.

Step 3: implement functionalities

Now you have to define the API of your robot by implementing its public functions.

This is really simple to achieve, it simply consists in calling the user functions of the member variables objects. As a simple example the set_target_torque function can be used to set the torque of all individual motors:

void Robot::set_target_torque(const ctrl_data_t& target){
  m1_.set_target_torque(target[0]);
  m2_.set_target_torque(target[1]);
  m3_.set_target_torque(target[2]);
  m4_.set_target_torque(target[3]);
}

Another example function, get_actual_position() provides the position of any joint, including passive joints:

double Tensegrity::get_actual_position() const{
  position[0] = (double)el5101_0_.get_counter_value() * 2*M_PI/passive_encoder_resolution_;
  position[1] = m1_.position();
  position[2] = m2_.position();
  position[3] = m3_.position();
  position[4] = m4_.position();
  position[5] = (double)el5101_5_.get_counter_value() * 2*M_PI/passive_encoder_resolution_
  return position_;
}

So a bus device is no more than a reusable BUS with a simple API allowing to hide hardware details to end user.

Step 4: using the robot driver

This last step is quite simple as it is exactly the same as for unit devices, something like:

#include <ethercatcpp/robot.h>
#include <pid/signal_manager.h>
#include <pid/log.h>
#include <pid/real_time.h>
#include <pid/synchro.h>
#include <chrono>
using std::chrono_literals;

int main(int argc, char* argv[]) {
    auto memory_locker = pid::make_current_thread_real_time();
    ethercatcpp::Master master;
    master.set_primary_interface("eth0");
    ethercatcpp::Robot robot;
    master.add(robot);
    master.init();
    bool stop = false;
    pid::SignalManager::add(pid::SignalManager::Interrupt, "SigInt stop",
                            [&stop]() { stop = true; });
    pid::Period period(1ms);
    double ttorque[4]={0,0,0,0}, positions[6]={0,0,0,0,0,0};
    while (not stop) {
        compute(positions, ttorque);
        robot.set_target_torque(ttorque);

        if (master.next_Cycle()) {
          positions=robot.get_actual_position();
        }
        period.sleep();
    }
    pid::SignalManager::remove(pid::SignalManager::Interrupt, "SigInt stop");
}

From user perspective there is no difference between a slave device and a bus device. Agregate devices can also be composed the same way as unit devices in the same BUS. Ethercatcpp automatically manages the bus configuration, the only thing to respect is the order defined by hardware topology. For instance we can imagine that 2 robots are connected on the same ethercat bus:

...
int main(int argc, char* argv[]) {
    auto memory_locker = pid::make_current_thread_real_time();
    ethercatcpp::Master master;
    master.set_primary_interface("eth0");
    ethercatcpp::Robot robot1, robot2;
    master.add(robot1);
    master.add(robot2);
    master.init();
    bool stop = false;
    pid::SignalManager::add(pid::SignalManager::Interrupt, "SigInt stop",
                            [&stop]() { stop = true; });
    const auto period = std::chrono::duration<double>(control_period);
    pid::Period period(1ms);
    double ttorque1[4]={0,0,0,0}, positions1[6]={0,0,0,0,0,0};
    double ttorque2[4]={0,0,0,0}, positions2[6]={0,0,0,0,0,0};
    while (not stop) {
        compute(positions1, ttorque1);
        compute(positions2, ttorque2);
        robot1.set_target_torque(ttorque1);
        robot2.set_target_torque(ttorque2);
        ...
        if (master.next_Cycle()) {
          positions1=robot1.get_actual_position();
          positions2=robot2.get_actual_position();
        }
        period.sleep();
    }
    pid::SignalManager::remove(pid::SignalManager::Interrupt, "SigInt stop");
}

The only constraint is that robot2 bus is connected on the last device of robot1 bus. A more natural (and pratical) way of doing would be to connect the robots on the same BUS using a ethercat switch like the EK1110 device, implementing a kind of tree like topology.

In the end bus devices open the perspective to build sub systems from slave devices and then compose them to get higher level systems.

Important Note: please consider the previous example code just as they are: simplified codes for demonstrating how to create bus devices. This example is far from a real functionnal code of a robot. Motor drives, like the Maxon Epos, are very complex ethercat devices that requires carefull configuration and state management. Robot drivers using these motor drives are consequently much more complex to implement. To get detailed examples you should have a look at ethercatcpp-epos for Epos motor drives usage and to ethercatcpp-assist to see a complex robot driver based on Epos motor drives.