This page explains the different steps to create an EtherCAT device driver. For more informations on EtherCAT specifications please read this page.

Driver creation overview

To create an ethercatcpp driver, many informations and steps are needed. The next schema represent the creation pipeline for a non-configurable device.

The first step is to get device informations (see step 1).

Step 0 : Install and configure PID

Before starting to create device driver, you have to install PID and ethercatcpp-core. Then you can use an exiting package or create a package that will contain all the driver code with :

pid cd
pid create package=ethercatcpp-<device_name> url=git@gite.lirmm.fr:ethercatcpp/ethercatcpp-<device_name>.git

You have to declare a dependency to ethercatcpp-core in the root CMakelists.txt file of your package, after the package declaration:

PID_Dependency(ethercatcpp-core VERSION 3.0)

Now, package dependencies are declared, you have to declare your component as a “shared library” and its dependency to ethercatcpp-core in the CMakeLists.txt files:

PID_Component(ethercatcpp-<device_name>
  CXX_STANDARD 11   
  EXPORT  ethercatcpp/core 
          posix
  DESCRIPTION Ethercatcpp-<device_name> is a component providing the EtherCAT driver for <device_name> devices.
)

An other important point before starting is that all unique device drivers are composed just by one device. So, they have to be declared as an inherit class from SlaveDevice class.

And then, don’t forget including needed header:

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

namespace ethercatcpp {
class EL2008 : public SlaveDevice {
...
};
}

Step 1 : Get device informations

To create and configure our driver, we need many informations :

  • Device name.
  • EtherCAT Manufacturer and Device Id.
  • Device configuration communication mode : CoE (CanOpen over EtherCAT), VoE (Vendor over EtherCAT) or SoE (SERCOS over EtherCAT).
  • All buffers (syncmanager) details (address, length, flag and type).

We have different way to obtain these informations, firstly with the Data sheet, secondly directly by reading informations in the EEPROM device.

Step 1.1 Read device EEPROM informations

To extract informations from EEPROM device, we used an ethercatcpp-core application named “slaveinfo”.

pid cd
cd install/<platform>/soem/1.3.2/bin
sudo ./slaveinfo <network-interface-name>

where <network-interface-name> is the interface network name where the device is plugged on your workstation.

After executing program, you will have:

Now, we can extract all datas we need in a table:

Data type Prefix name Data  
Device name Name: EL2008  
Manufacturer ID Man: 00000002  
Device ID ID: 07d83052  
Configuration CoE details: 0 not configurable

To determine the type of a buffer (synchro or not, input or output), shows type of syncmanager and report to table in this paragraph.

Data type Prefix name Value  
Syncmanager details:      
SM number SM 0  
Address A: 0f00  
Length L: 8  
Flag F: 00090044  
Type Type: 3 output type

Step 1.1.1 Set name and device Id

The first element to configure is the device name and IDs with set_id function.

    set_id(devicename, man_id, device_id);

where : man_id is manufacturer ID.

For an EL2008, we have:

    set_id("EL2008", 0x00000002, 0x07d83052);

This function must be used in the device class constructor. So the pattern for the EL2008 class is:

...
class EL2008 : public SlaveDevice {
public:
    EL2008(){
       set_id("EL2008", 0x00000002, 0x07d83052);
    }
};

Step 2 : Buffers communications configuration

Then you need to identify and configure buffersof the device. To do this, we explain the use of each function in following sub sections.

Step 2.1 Determine if device is configurable.

To determine if a device is configurable, it has to have CoE, FoE, EoE or SoE configuration. In our example, we can see in “CoE details, FoE details, EoE details or SoE details” that EL2008 doesn’t have CoE, FoE, EoE or SoE configuration (i.e. 0 value).

Step 2.2 Check input / output data size

This allows to check if device is composed by input or output or both. The syncmanager (device buffer) informations are display previously (when reading EEPROM device). Syncmanager type defines if the buffer is an input or output buffer. If device is composed by input and output, there is two syncmanager (one for each type). An output buffer is defined by a syncmanager type 3 and an input by a type 4.

The buffer size is display by the length field.

In our example, EL2008 is composed by a type 3 syncmanager of 8 bits length, so it only writes 1 byte data as output.

Step 2.3 Create data structures

To use data is our program, we have to define the type of data used by a syncmanager which corresponds exactly to the buffer size. To match rigorously with buffer size we have to declare our data structure in packed mode. In our example (EL2008), a data structure of 8 bits length has to be created. The code above show the EL2008 data structure.

  • EL2008 constructor is then:
...
class EL2008 : public SlaveDevice {
private:
    struct [[gnu::packed]] buffer_out_cyclic_command_t
    {
      uint8_t data = 0;
    };

public:
    EL2008(){
       set_id("EL2008", 0x00000002, 0x07d83052);
    }
};

Step 2.4 Buffers configuration

Now we have all informations to define all buffers configurations.

To set buffers, we used this function in the class constructor:

  define_physical_buffer<struct_data_type_t>(buffer_type, physical_address, flag);
  • physical_address is the syncmanager physical start address .
  • flag represent the syncmanager configurations.
  • buffer_type parameter represents the type of buffer and have 2 possibilities :

    • SYNCHROS_OUT for output cyclic process datas (generally device commands) (SyncManager type 3)
    • SYNCHROS_IN for input cyclic process datas (generally device status) (SyncManager type 4)

To sum up:

Syncmanager type Buffer type Comments
3 SYNCHROS_OUT output cyclic process datas
4 SYNCHROS_IN input cyclic process datas

Theses parameters (buffer_type, physical_address and flag) are obtained previously.

The template parameter struct_data_type_t defines data structure we will use to communicate, there are created previously in step 2.3

  • In EL2008 constructor
...
class EL2008 : public SlaveDevice {
...
public:
    EL2008(){
      set_id("EL2008", 0x00000002, 0x07d83052);
      define_physical_buffer<buffer_out_cyclic_command_t>(SYNCHROS_OUT, 0x0f00, 0x00090044);
    }
};

Step 3 : Create operating steps

Now device is completely configured, operating steps (run steps) must be defined to update cyclic buffer datas (configurations, commands, device state, etc…). To do this, all steps have two lambda function :

  • pre function: run before start of cycle, generally used to set all commands and configurations datas.
  • post function: run after end of cycle, generally used to get all status and states datas.

A run step is a step that is executed at each cycle. This step defines normal use of device like update command, update device status, etc… If device needs many run steps, only one is executed per cycle and at each new cycle, the next step is run. To add a run step you have to use this function in constructor class:

  add_run_step([this](){
      //pre function
    },
    [this](){
      //post function
    });

In our EL2008 example, only one run step is needed. Only output datas are used so just a pre function is needed to set buffer data.

...
class EL2008 : public SlaveDevice {
private:
...
  uint8_t data_;
public:
    EL2008(){
      set_id("EL2008", 0x00000002, 0x07d83052);
      define_physical_buffer<buffer_out_cyclic_command_t>(SYNCHROS_OUT, 0x0f00, 0x00090044);
      add_run_step(
        [this](){
          auto buff = this->output_buffer<buffer_out_cyclic_command_t>(0x0f00);
          buff->data = this->data_;
          },
        [this](){
      });// add_run_step end
    }
  }
...

In previous code, to access the data we use the output_buffer function with the correct buffer address where we can find it (0x0f00) and with its corresponding type buffer_out_cyclic_command_t. The buffer data is set from the value of a simple member variable (data_) that contains the user defined value to set.

Accessing IO data

To access I/O buffers datas, 2 functions are available :

  output_buffer<struct_data_type_t>(physical_address);

to access output buffer (where the user code can write)

and:

  input_buffer<struct_data_type_t>(physical_address);

to access input buffer (where the user code can read)

Both functions have same logic:

  • physical_address is the PDO buffer physical address that you have used to define PDO buffer.
  • struct_data_type_t data structure we will use to communicate (created previously in step 2.3).

Step 4 : Write useful functions

The last thing to do is to provide a function used to set data from user code.

In our example, EL2008 is a digital output device, so we create a function to set state of a specific channel:

...
class EL2008 : public SlaveDevice {
private:
...
  uint8_t data_;
public:
  EL2008(){
    ...
  }
  enum channel_id_t {
    channel_1, //!< Channel 1
    channel_2, //!< Channel 2
    channel_3, //!< Channel 3
    channel_4, //!< Channel 4
    channel_5, //!< Channel 5
    channel_6, //!< Channel 6
    channel_7, //!< Channel 7
    channel_8  //!< Channel 8
  };

  void set_channel_state(channel_id_t channel, bool state){
    switch (channel) {
    case channel_1:
    data_ ^= (-state ^ data_) & (1UL << 0);
    break;
    case channel_2:
    data_ ^= (-state ^ data_) & (1UL << 1);
    break;
    case channel_3:
    data_ ^= (-state ^ data_) & (1UL << 2);
    break;
    case channel_4:
    data_ ^= (-state ^ data_) & (1UL << 3);
    break;
    case channel_5:
    data_ ^= (-state ^ data_) & (1UL << 4);
    break;
    case channel_6:
    data_ ^= (-state ^ data_) & (1UL << 5);
    break;
    case channel_7:
    data_ ^= (-state ^ data_) & (1UL << 6);
    break;
    case channel_8:
    data_ ^= (-state ^ data_) & (1UL << 7);
    break;
    }
  }
}
...

Each bit corresponds to a specific digital output of the EL2008 so the function set_channel_state simply writes the bit of the output corresponding to the one required by the user.

Note: Contrarily to this example code, you should put implementation code in a source file instead of a header !!!