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 :

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

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

PID_Dependency(ethercatcpp-core VERSION 3.2)

Now, package dependency have 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>
        DIRECTORY ethercatcpp
        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 driver are composed just by one device. In this case they have to be declared as an subclass of SlaveDevice class. Here is the example code for the EL1018:

#pragma once
#include <ethercatcpp/core.h>
namespace ethercatcpp {
class EL1018 : 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”.

> cd <pid-worskspace>/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: EL1018  
Manufacturer ID Man: 00000002  
Device ID ID: 03fa3052  
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: 1000  
Length L: 8  
Flag F: 00010000  
Type Type: 4 input 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 EL1018, we have:

    set_id("EL1018", 0x00000002, 0x03fa3052);

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

...
class EL1018 : public SlaveDevice {
public:
    EL1018(){
       set_id("EL1018", 0x00000002, 0x03fa3052);
    }
};

Step 2 : Buffers communications configuration

Buffer identification and configuration is the first step to configure a device. The following subsection explain step by step how to do that.

Step 2.1 Determine if device is configurable.

To determine if a device is configurable, it has to had CoE, FoE, EoE or SoE configuration. In our example, we can see in “CoE details, FoE details, EoE details or SoE details” that EL1018 don’t have CoE, FoE, EoE or SoE configuration (“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 define if the buffer is an input or output. If device is composed by inputs and outputs, there is two syncmanager (one for each type). An output buffer is defined by a syncmanager type 3 and an input buffer by a type 4. The buffer size is display by the length field.

In our example, EL1018 is composed by a type 4 syncmanager of 8 bits length, so it only receives as inputs a 1 byte data.

Step 2.3 Create data structures

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

...
class EL1018 : public SlaveDevice {
private:
    struct [[gnu::packed]]buffer_in_cyclic_status_t
    {
      uint8_t data;
    };

public:
    EL1018(){
       set_id("EL1018", 0x00000002, 0x03fa3052);
    }
};

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 resume :

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 last parameters struct_data_type_t define data structure we will use to communicate, there are created previously in step 2.3

  • In EL1018 constructor:
...
class EL1018 : public SlaveDevice {
...
public:
    EL1018(){
      set_id("EL1018", 0x00000002, 0x03fa3052);
      define_physical_buffer<buffer_in_cyclic_status_t>(SYNCHROS_IN, 0x1000, 0x00010000);
    }
};

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 functions :

  • 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 are execute at each cycle. This step define 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. When last step is reached the next step is the first run step and so on. 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 EL1018 example, only one run step is needed. Only input data is provided by the EL1018 so just a post function is needed to get buffer data.

...
class EL1018 : public SlaveDevice {
private:
...
  uint8_t data_;
public:
  EL1018(){
    set_id("EL1018", 0x00000002, 0x03fa3052);
    define_physical_buffer<buffer_in_cyclic_status_t>(SYNCHROS_IN, 0x1000, 0x00010000);
    add_run_step([this](){
      },
      [this](){
        auto buff = this->input_buffer<buffer_in_cyclic_status_t>(0x1000);
        this->data_ = data_ = buff->data;
    });
  }
...

In previous code, to update the data we use the input_buffer function with the correct buffer address where we can find it (0x1000) and with its corresponding type buffer_in_cyclic_status_t. Once data extracted from physical buffer we memorize it using a simple member variable (data_) in order to be capable of using it later.

Accessing IO data

To access I/O buffers datas, you 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 these function 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 : Make device useful functions

The last thing to do is to provide a function used to retrieve the read data from user code.

In our exemple EL1018 is a digital input device, so we create a function to get state of a specific channel:

...
class EL1018 : public SlaveDevice {
private:
...
  uint8_t data_;
public:
  EL1018(){
    ...
  }
  
  //! This enum define open circuit detection pin.
  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
  };

  bool channel_state(channel_id_t channel){
  switch (channel) {
    case channel_1:
      return((data_ >> 0) & 1U);
    break;
    case channel_2:
      return((data_ >> 1) & 1U);
    break;
    case channel_3:
      return((data_ >> 2) & 1U);
    break;
    case channel_4:
      return((data_ >> 3) & 1U);
    break;
    case channel_5:
      return((data_ >> 4) & 1U);
    break;
    case channel_6:
      return((data_ >> 5) & 1U);
    break;
    case channel_7:
      return((data_ >> 6) & 1U);
    break;
    case channel_8:
      return((data_ >> 7) & 1U);
    break;
    default:
    return(false);
  }
}
...
};

Each bit corresponds to a specific digital input of the EL1018 so the function channel_state simply read the bit of the input 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 !!!