This page explains the different steps to create an EtherCAT device driver for a configurable device: the Maxon EPOS4 controller. 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 represents the creation pipeline for a 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 dependencies 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 dependency have been declared, you have to declare your component as a “shared library” and its dependency to ethercatcpp-core in the src/CMakeLists.txt file:

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

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.

And then, don’t forget including needed header:

#pragma once
#include <ethercatcpp/core.h>
namespace ethercatcpp {
class Epos4 : 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 Syncmanager (buffers) details (address, length, flag and type).
  • Type of synchronization (Distributed clock or SM synchro).
  • If device used CoE, we need cyclics buffers (RxPDO and TxPDO) details (mapping address and data type) and all others SDO “init” configurations.
  • If device used VoE, we need its specific protocol communication.

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 (e.g. eth0).

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: EPOS4  
Manufacturer ID Man: 000000fb  
Device ID ID: 63500000  
synchronization supported hasDC: 1 support DC
Communication mode CoE details: 2d support CoE mode

To determine the type of a buffer (cyclic or not, input or output), we report information from slaveinfo into table in this paragraph.

Data type Prefix name Value
Syncmanager details:    
SM number SM 0
Address A: 1000
Length L: 48
Flag F: 00010026
Type Type : 1

To extract other SyncManagers informations (SM1, SM2, SM3), do the same like SM0.

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 EPOS4, we have:

...
class Epos4 : public SlaveDevice {
public:
    Epos4(){
      set_id("EPOS4", 0x000000fb, 0x63500000);
    }
};

Step 1.2 Differences between configurations of communications modes

EtherCAT is compatible with some communication profiles (CANopen, SERCOS, etc…). To identify which type of device you have, you can read device data-sheet. Or, if you haven’t any informations, you can try to extract them from EEPROM like previously by reading “communication mode” field.

Each communication mode has a specific way to be configured, but the buffer configuration is pretty similar with all mode. Next diagram summarize different steps to configure your device.

To learn more about CANopen specifications, you can see this page.

Step 2 : Buffers configuration

Then we need to identify and configure buffers. To make this, we start by a reminder on EtherCAT frame and buffer type. Then we explain all functions used to define buffers.

Step 2.1 Frame composition and buffers

EtherCAT frame is like an Ethernet frame, it is composed by an header and a data area but the data area is decomposed in several datagrams. Next figure shows decomposition of an EtherCAT frame.

These datagrams are linked to different buffers types:

  • Mailboxes: buffer used for all asynchronous communications.

  • Cyclic process datas: buffer used for synchronous communication (for example PDOs in CoE mode)

In your application, each device use reserved datagrams to communicate with Master and other devices. It can use a datagram for asynchronous (mailbox) and/or synchronous (cyclic process datas) communication.

Step 2.2 Define communication Buffers (Mailboxes and cyclic buffer)

Generally, devices are composed by each type of buffer (mailbox in/out and cyclic buffer in/out). Buffers communications need two steps to be configured. First we set the buffer (type, address, flag), then we define “data structure” for the communication.

To set buffers, we used this function:

  define_physical_buffer<struct_data_type_t>(buffer_type, physical_address, flag);
  • physical_address is the syncmanager start physical address .
  • flag represent the syncmanager configurations.
  • buffer_type parameter have 4 possibilities :

    • ASYNCHROS_OUT for output mailbox (SyncManager type 1)
    • ASYNCHROS_IN for input mailbox (SyncManager type 2)
    • 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
1 ASYNCHROS_OUT output mailbox
2 ASYNCHROS_IN input mailbox
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 defines data structure containing data that will be exchanged between the slave and the master. Now we have to define struct_data_type_t to configure buffers and communicate with device.

2.2.1 Mailboxes

For mailboxes, we only have to define a structure with the good buffer size in struct_data_type_t witch just reserve memory. Be careful to match rigorously with buffer size we have to declare our datas structure in packed mode. To make this we used:

  struct [[gnu::packed]]mailbox_t
  {
    int8_t mailbox[mailbox_size];
  };

mailbox_size is obtained in previously by reading the Syncmanager field (type 1 for output and type 2 for input).

2.2.2 Cyclic buffers

Output and input cyclic data buffers (generally syncmanager type 3 and type 4) depend on communication mode (CoE, VoE, …). We will show later (VoE, CoE) how to configure this buffer but code structure looks like:

  struct [[gnu::packed]] cyclic_buffer_t
  {
    uint16_t first_item;      
    int8_t   second_item;     
    int32_t  third_item;     
    int32_t  fourth_item;     
  };

2.2.3 Epos4 example

Here is an example defining data structures for the Epos4:

...
class Epos4 : public SlaveDevice {
private:
  static constexpr size_t mailbox_size=48;
  //same size for IN and OUT mailboxes
  struct [[gnu::packed]] mailbox_t
  {
    int8_t mailbox[mailbox_size];
  };

  //Define output cyclic buffer
  struct [[gnu::packed]] buffer_out_cyclic_command_t
  {
    uint16_t control_word;           //name_0x6040_00
    int8_t   operation_modes;        //name_0x6060_00
    int32_t  target_position;        //name_0x607A_00
    int32_t  target_velocity;        //name_0x60FF_00
    int16_t  target_torque;          //name_0x6071_00
    int32_t  position_offset;        //name_0x60B0_00
    int32_t  velocity_offset;        //name_0x60B1_00
    int16_t  torque_offset;          //name_0x60B2_00
    uint32_t digital_output_state;   //name_0x60FE_01
  };


  //Define input cyclic buffer
  typedef [[gnu::packed]] struct buffer_in_cyclic_status_t
  {
    uint16_t status_word;                //name_0x6041_00
    int8_t   operation_modes_read;       //name_0x6061_00
    int32_t  actual_position;            //name_0x6064_00
    int32_t  actual_velocity;            //name_0x606C_00
    int16_t  actual_torque;              //name_0x6077_00
    int32_t  actual_current;             //name_0x30D1-2_00
    int32_t  actual_average_current;     //name 0x30D1-01
    int16_t  actual_average_torque;      //name 0x30D2-01
    int32_t  actual_average_velocity;    //name 0x30D3-01
    uint32_t digital_input_state;        //name_0x60FD_00
    int16_t  analog_input_1;             //name 0x3160-01
    int16_t  analog_input_2;             //name 0x3160-02
  };

public:
    Epos4(){
      set_id("EPOS4", 0x000000fb, 0x63500000);
    }
};
  • Then we define the 4 buffers in Epos4 constructor:
...
class Epos4 : public SlaveDevice {
private:
  ...
public:
    Epos4(){
      set_id("EPOS4", 0x000000fb, 0x63500000);
      // Mailboxes configuration
      define_physical_buffer<mailbox_t>(ASYNCHROS_OUT, 0x1000, 0x00010026);
      define_physical_buffer<mailbox_t>(ASYNCHROS_IN, 0x1030, 0x00010022);

      // Communication buffer configuration (RxPDO / TxPDO)
      define_physical_buffer<buffer_out_cyclic_command_t>(SYNCHROS_OUT, 0x1060, 0x00010064);
      define_physical_buffer<buffer_in_cyclic_status_t>(SYNCHROS_IN, 0x10f0, 0x00010020);
    }
};

Step 3 : VoE specific configuration

In vendor over EtherCAT mode (VoE), a manufacturer can use its own communication protocol. So, we have to define this protocol and configure corresponding buffer like previously. The struct_data_type_t of cyclic buffer represent process data used to communicate, so this data structure contains specifics datas choose by manufacturer.

For example, command buffer used by shadow hand (that use this communication mode) looks like:

 struct [[gnu::packed]] buffer_shadow_out_command_t
 {
     EDC_COMMAND                 EDC_command;           //!< What type of data should the palm send back in the next packet?
     FROM_MOTOR_DATA_TYPE        from_motor_data_type;  //!< Which data does the host want from the motors?
     int16_t                     which_motors;          //!< Which motors does the host want to read? 0: Even motor numbers.  1: Odd motor numbers
     TO_MOTOR_DATA_TYPE          to_motor_data_type;    //!< Type of datas ask by the host
     int16_t                     motor_data[NUM_MOTORS];//!< Data to send to motors. Typically torque/PWM demands, or configs.
     uint32_t                    tactile_data_type;     //!< Request for specific tactile data FROM_TACTILE_SENSOR_TYPE or FROM_TACTILE_BIOTAC
 } ;

 

Step 4 : CoE specific configuration

To sum up the principle when using CANOpen over Ethercat is:

  • A dictionnary of data is used. The dictionnary contains all available data of the device.
  • In this dictionnary there are memories called PDO that contain data that will be exchanged on the ethercat BUS.
  • We can interact with the dictionnary using so called SDO services. These services are mainly used to configure the data exchanges by targetting adequate PDOs.

The first thing is to understand how SDO/PDO is used within ethercat. For more informations about CANopen specification, you can see this page.

Step 4.1 Read Canopen SDO/PDO informations

If SDO/PDO informations are not described in device datasheet, we can extract them with:

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

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

This program extracts and list all the Canopen dictionary. So, we obtain a lot of informations and have to search PDOs informations since this is what we want to use in the end. Fortunately, datas localization are normalized and we can found them with there “index”.

CoE standard defines two CoE specific objects in dictionnary:

  • 0x1C12 (called Rx PDO Assign): this is the table containing the PDOs for the data input. It contains references to dictionnary specific object called IO RxPDO Map starting from address 0x1600.
  • 0x1C13 (called Tx PDO Assign): this is the table containing the PDOs for the data output. It contains references to dictionnary specific object called IO TxPDO Map starting from address 0x1A00.

Each IO Map (Rx or Tx) is a table of data (possibly without content) that user need to set (or use if predefined) in order to allow reception/transmission of data with the corresponding device. Each entry defines a mapping between an address of an object to be exchanged with an object of the dictionnary (typically a data field).

  • IO RxPDO Map defines mappings in dictionnary between adresses starting from 0x1600 to adresses ranging from 0x7000 to 0x7FFF.
  • IO TxPDO Map defines mappings in dictionnary between adresses starting from 0x1A00 to adresses ranging from 0x6000 to 0x6FFF.

To resume CoE standard:

PDO buffer PDO Assign IO PDO Map Process Datas
Rx 0x1C12 0x1600 0x7000-0x7FFF
Tx 0x1C13 0x1A00 0x6000-0x6FFF

About Mapping configuration:

  • Objects of dictionnary located at adresses between 0x7000 to 0x7FFF and 0x6000 to 0x6FFF are read-only. They contain data that is predefined for the given device so they cannot be configured in any way. These data are called process data.
  • Objects of dictionnary located at adresses 0x1C12, 0x1600, 0x1C13 and 0x1A00 can be read-only or read-write depending the given device. If they are read-write this means that exchanged data can or must be configured.

For one Rx or Tx PDO (data exchanged) we have to configure at least two CoE objects:

  • A PDO Assign object to make the data exchanged during cyclic communication.
  • One or more IO PDO Map that define the structure of the data exchanged. It practically defines links to the different value in device dictionary.

Depending on the device:

  • zero , one or more PDO Assign can be configured (i.e. modified by the user).
  • zero , one or more IO PDO Map cab be configured (i.e. modified by the user).

example Epos4

When looking Rx PDO Assign at index 0x1C12:

  • its first subindex 00 is always present and define the number of IO PDO Map PDO. We can see that there is exactly one IO PDO Map (since size is 0x01).
  • the subsequent subindexes are defined IO PDO Maps. Here there is exactly one IO PDO Map defined at subindex 01. Its address is 0x1600 (start of the memory zone in dictionnary reserved for Rx IO PDO Maps).

So we can deduce that the Rx PDO Assign configuration is already made by default.

When looking assigned Rx IO PDO Map at index 0x1600 (so the first defined mapping):

  • its first subindex 00 is always present and define the number of mappings to process data. We can see that there is no mapping defined (since size is 0x00).
  • all sub-index are empty (since value is 0x00000000).

So, we have to configure the Rx IO PDO Map by adding useful mappings to process datas (in our example up to 8 datas). It is the same for Tx PDO. Refer to this section to configure PDOs buffers.

example Epos3

In this other example we can found a completely defined Rx IO PDO Map.

The value of each mapping to process data for a given IO PDO Map follows the pattern:

  • 8 first bytes define the process data index address: this is the target object in range 0x7000 to 0x7FFF.
  • 4 following bytes are sub-index localization: This is the address of the process data relative to process data index address. Indeed in CAN open process data are themselves structured with one or more subobjects (at least one). For simple process data the value is 00.
  • 4 last bytes is data size.

For example, the value of PDO object 1 (at index 0x01 of the IO PDO Map 0x1600) is 0x60400010:

  • it links process data at index 0x6040 at sub-index 0x00
  • its size is 0x10 bits. All values are in hexadecimal format.

Others devices can have different PDOs configurations. Here is a sum up of possible situations:

Step 4.2 Configure CANOpen device

With CoE there are specific actions to perform in order to configure the CANOpen device’s dictionnary. This is achieved using a specific function to be called in constructor: configure_at_init. This function takes a user defined function as argument, typically a lambda, something like:

  ...
  configure_at_init( [this](){
    // Add all SDO, PDO and device configurations.
  } );
  ...

The purpose of next subsections is to explain which actions you can perform during this configuration : all following functions must be used into the user defined function passed to configure_at_init.

Please note that configure_at_init is particularly useful to allow user to configure a CoE device but it is not restricted to CoE. You can also call Sercos configuration functions (read_sercos and write_sercos), FoE function(read_file, write_file) and distributed clocks functions (see next subsections).

There is also a configure_at_end function that does similar job but that is called just before end of the execution (during call to end()).

Step 4.2.1 Set PDO cyclic output and input buffers

The goal here is to configure the PDO cyclic buffer, that is the data exchanged between device and master.

Like previously, many possibilities exist to configure cyclic buffer. As a reminder, we have to configure two CoE objects :

  • a map (IO PDO Map) which contains links to values in device dictionary.
  • a map address (PDO Assign) to link elements of the map to the PDOs (cyclic buffer).
Step 4.2.1.1 Device with predefined map and just one map to assign to cyclic buffer

This case is a basic configuration. We just have to indicate PDO map address that we want to used. To do that, we use:

// Link maps
// Config Command PDO mapping (Rx PDO assign: 0x1c12)
start_command_pdo_mapping<uint8_t>();     
add_command_pdo_mapping<uint8_t>(0x1600); //Assign IO Map at CoE index 0x1600 to Rx PDO in 0x1c12.
end_command_pdo_mapping<uint8_t>();      

// Config Status PDO mapping (Tx PDO assign: 0x1c13)
start_status_pdo_mapping<uint8_t>();
add_status_pdo_mapping<uint8_t>(0x1A00);  //Assign IO Map at CoE index 0x1A00 to Tx PDO in 0x1c13.
end_status_pdo_mapping<uint8_t>();

Functions start_/end_* are used to delimit commands used to describe the buffer. add_* functions are used to assign PDO in use in the input and output buffers.

Step 4.2.1.2 Device with predefined map and many maps to assign to cyclic buffer

In this case, we need to assign two or more IO PDO Map to the PDO buffer. The final buffer will be composed by the first IO map process data then the others in memory sequence. Next example shows how to assign 5 maps to RxPDO and 2 maps to TxPDO.

// Link maps
// Config Command PDO mapping (Rx PDO assign: 0x1c12)
start_command_pdo_mapping<uint8_t>();     
add_command_pdo_mapping<uint8_t>(0x1601); //Assign IO Map at CoE index 0x1601 to Rx PDO in 0x1c12.
add_command_pdo_mapping<uint8_t>(0x1603); //Assign IO Map at CoE index 0x1603 to Rx PDO in 0x1c12.
add_command_pdo_mapping<uint8_t>(0x1605); //Assign IO Map at CoE index 0x1605 to Rx PDO in 0x1c12.
add_command_pdo_mapping<uint8_t>(0x1608); //Assign IO Map at CoE index 0x1608 to Rx PDO in 0x1c12.
add_command_pdo_mapping<uint8_t>(0x161A); //Assign IO Map at CoE index 0x161A to Rx PDO in 0x1c12.
end_command_pdo_mapping<uint8_t>();      

// Config Status PDO mapping (Tx PDO assign: 0x1c13)
start_status_pdo_mapping<uint8_t>();
add_status_pdo_mapping<uint8_t>(0x1A04);  //Assign IO Map at CoE index 0x1A04 to Tx PDO in 0x1c13.
add_status_pdo_mapping<uint8_t>(0x1A06);  //Assign IO Map at CoE index 0x1A06 to Tx PDO in 0x1c13.
end_status_pdo_mapping<uint8_t>();

Here exactly same functions are used than in previous case, only difference is that we define many assignments.

Step 4.2.1.3 Device without predefined map and just one map to assign to cyclic buffer

Some times manufacturers don’t implement IO map and leave users to create their own map by selecting only useful process datas. So we have to create and assign it to our PDO buffer.

//Create map
uint8_t item_nb = 0;
uint32_t pdo_item = 0;

// Have to deactivate map by indicate 0 item to change it
write_sdo(0x1600, 0x00, item_nb);

// Add 4 new items to the map at CoE index 0x1600
pdo_item = 0x60400010;                        //Item: CoE index= 0x6040, CoE sub-index= 0x01, process data size= 0x10 bits (16 bits)
write_sdo(0x1600, 0x01, pdo_item);    //Add pdo_item in map at CoE index 0x1600 in first (0x01) place

pdo_item = 0x60600008;                        //Item: CoE index= 0x6060, CoE sub-index= 0x02, process data size= 0x08 bits (8 bits)
write_sdo(0x1600, 0x02, pdo_item);    //Add pdo_item in map at CoE index 0x1600 in second (0x02) place

pdo_item = 0x607A0020;                        //Item: CoE index= 0x607A, CoE sub-index= 0x03, process data size= 0x20 bits (32 bits)
write_sdo(0x1600, 0x03, pdo_item);    //Add pdo_item in map at CoE index 0x1600 in third (0x03) place

pdo_item = 0x60FF0020;                        //Item: CoE index= 0x60FF, CoE sub-index= 0x04, process data size= 0x20 bits (32 bits)
write_sdo(0x1600, 0x04, pdo_item);    //Add pdo_item in map at CoE index 0x1600 in fourth (0x04) place

// Reactive map
item_nb = 4; // map item number
write_sdo(0x1600, 0x00, item_nb);

// Link maps
// Config Command PDO mapping (Rx PDO assign: 0x1c12)
start_command_pdo_mapping<uint8_t>();     
add_command_pdo_mapping<uint8_t>(0x1600); //Assign IO Map at CoE index 0x1600 to Rx PDO in 0x1c12.
end_command_pdo_mapping<uint8_t>();      

The function write_sdo() is used to define the IO map prior to add it to the PDO buffer. This allows to select the exact elements from dictionnary we want to exchange. The resulting map is added to the buffer using start/add/end as usual.

For the Epos4 example we define the mapping this way:

...
class Epos4 : public SlaveDevice {
private:
  ...
public:
    Epos4(){
      set_id("Epos4", 0x000000fb, 0x63500000);
      configure_at_init([this]() {
        // PDO map configuration
        this->command_map_configuration();
        this->status_map_configuration();

        // Config Command PDO mapping
        this->start_command_pdo_mapping<uint8_t>();     // 0x1c12
        this->add_command_pdo_mapping<uint8_t>(0x1600); // 0x1c12
        this->end_command_pdo_mapping<uint8_t>();       // 0x1c12

        // Config Status PDO mapping
        this->start_status_pdo_mapping<uint8_t>();     // 0x1c13
        this->add_status_pdo_mapping<uint8_t>(0x1A00); // 0x1c13
        this->end_status_pdo_mapping<uint8_t>();       // 0x1c13
        
        ...
      });
      // Mailboxes configuration
      define_physical_buffer<mailbox_out_t>(ASYNCHROS_OUT, 0x1000, 0x00010026);
      define_physical_buffer<mailbox_in_t>(ASYNCHROS_IN, 0x1030, 0x00010022);

      // Communication buffer config. (RxPDO / TxPDO)
      define_physical_buffer<buffer_out_cyclic_command_t>(SYNCHROS_OUT, 0x1060, 0x00010064);//0x1c12
      define_physical_buffer<buffer_in_cyclic_status_t>(SYNCHROS_IN, 0x10f0, 0x00010020);//0x1c13
      ...
    }
};

With two private functions building the IO maps:

  • command_map_configuration() to build the command map with address 0x1600:
bool Epos4::command_map_configuration() {
  int wkc = 0;
  uint8_t val = 0;
  uint32_t pdo_item = 0;
  // Have to desactivate buffer to change it
  val = 0;
  this->write_sdo(0x1600, 0x00, val);
  // add new item to the map
  pdo_item = 0x60400010;
  this->write_sdo(0x1600, 0x01, pdo_item);
  pdo_item = 0x60600008;
  this->write_sdo(0x1600, 0x02, pdo_item);
  pdo_item = 0x607A0020;
  this->write_sdo(0x1600, 0x03, pdo_item);
  pdo_item = 0x60FF0020;
  this->write_sdo(0x1600, 0x04, pdo_item);
  pdo_item = 0x60710010;
  this->write_sdo(0x1600, 0x05, pdo_item);
  pdo_item = 0x60B00020;
  this->write_sdo(0x1600, 0x06, pdo_item);
  pdo_item = 0x60B10020;
  this->write_sdo(0x1600, 0x07, pdo_item);
  pdo_item = 0x60B20010;
  this->write_sdo(0x1600, 0x08, pdo_item);
  pdo_item = 0x60FE0120;
  this->write_sdo(0x1600, 0x09, pdo_item);
  // Reactive buffer
  val = 9;
  this->write_sdo(0x1600, 0x00, val);
  if (wkc == 9) {
    return (true);
  } else {
    return (false);
  }
}
  • status_map_configuration() to build the status map with address 0x1A00:
bool Epos4::status_map_configuration() {
  int wkc = 0;
  uint8_t val = 0;
  uint32_t pdo_item = 0;
  // Have to desactivate buffer to change it
  val = 0;
  this->write_sdo(0x1A00, 0x00, val);
  // add new item to the map
  pdo_item = 0x60410010;
  this->write_sdo(0x1A00, 0x01, pdo_item);
  pdo_item = 0x60610008;
  this->write_sdo(0x1A00, 0x02, pdo_item);
  pdo_item = 0x60640020;
  this->write_sdo(0x1A00, 0x03, pdo_item);
  pdo_item = 0x606C0020;
  this->write_sdo(0x1A00, 0x04, pdo_item);
  pdo_item = 0x60770010;
  this->write_sdo(0x1A00, 0x05, pdo_item);
  pdo_item = 0x30D10220;
  this->write_sdo(0x1A00, 0x06, pdo_item);
  pdo_item = 0x30D10120;
  this->write_sdo(0x1A00, 0x07, pdo_item);
  pdo_item = 0x30D20110;
  this->write_sdo(0x1A00, 0x08, pdo_item);
  pdo_item = 0x30D30120;
  this->write_sdo(0x1A00, 0x09, pdo_item);
  pdo_item = 0x60FD0020;
  this->write_sdo(0x1A00, 0x0A, pdo_item);
  pdo_item = 0x31600110;
  this->write_sdo(0x1A00, 0x0B, pdo_item);
  pdo_item = 0x31600210;
  this->write_sdo(0x1A00, 0x0C, pdo_item);
  // Reactive buffer
  val = 12;
  this->write_sdo(0x1A00, 0x00, val);
  if (wkc == 12) {
    return (true);
  } else {
    return (false);
  }
}

Addresses of these two IO Maps are then used to set the command and status map assignements.

Step 4.2.1.4 Device without predefined map and many maps to assign to cyclic buffer

This configuration is the more complete, we have to create several maps and link them to PDO buffers. All functions and configurations to make and link all maps are described previously. Take care with maps CoE address, maps item numbers and items descriptions, just with a minor error, all device configuration is wrong and device will not work.

Step 4.2.2 Others configurations and initialization

If device needs others configurations or some initializations, we can add them (in canopen_configure_SDO) by reading/writing SDO.

// To write to SDO
write_sdo(CoE_index, CoE_sub_index, var);

// To read from SDO
read_sdo(CoE_index, CoE_sub_index, var);

In the Epos4 we use SDO read/write to perform initialization of the device, for instance to define the reset_fault function:

void Epos4::reset_fault() {
  uint16_t value = 0;
  read_sdo(0x6040, 0x00, value); // read control_word
  value &= 0xFF7F;                       // mask for unset the "reset fault" bit
  write_sdo(0x6040, 0x00, value);
  read_sdo(0x6040, 0x00, value);
  value |= 0x80; // mask for set the "reset fault" bit
  write_sdo(0x6040, 0x00, value);
  read_sdo(0x6040, 0x00, value);
  value &= 0xFF7F; // mask for unset the "reset fault" bit
  write_sdo(0x6040, 0x00, value);
}

Than is also used in the same user defined function:

...
class Epos4 : public SlaveDevice {
private:
  ...
public:
    Epos4(){
      set_id("Epos4", 0x000000fb, 0x63500000);
      configure_at_init([this]() {
        // PDO map configuration
        this->command_map_configuration();
        this->status_map_configuration();

        // Config Command PDO mapping
        this->start_command_pdo_mapping<uint8_t>();     // 0x1c12
        //same code as previously
        ...
        this->reset_fault();
      });
      ...
    }
};

Available SDO services are device dependant so you should always have a look at device CANOpen datasheet.

Step 5 Configure distributed clock synchronization signal

Some ethercat devices allow to use distributed clock synchronization signals. This mechanism is used to better synchronize all devices of the bus supporting this feature. It is typically used to synchronize motor drives.

First of all you need to declare the unit device as having distributed clock mechanism implemented, this is achieved after set_id() has been called in the constructor:

define_distributed_clock(true);
  • without this feature: devices update their buffers (input and output) immediately when receiving ethercat packets. This is the basic synchronization.
  • with this freature: devices apply operations at the same time (more precisely within a very narrow tolerance range) in order to be more perfectly synchronized. This allows to have a higher level of confidence in synchronization for input and output data, as devices do not apply operation immediately when receving.

Distributed clock mechanism of devices usually features 2 interrupts that can be triggered time-controlled: SYNC0 and SYNC1.

  • SYNC0 is the interrupt specifying when operations are applied.
  • SYNC1 is the interrupt triggerred a given delay after SYNC0 triggerring. Typically used to wait that all devices are ready to produce data.

If your device defines distributed clock you can activate and configure it as explained in next subsections.

Step 5.1 Use and configure a DC Sync0 synchro

To activate and define SYNC0 signal, we use:

configure_dc_sync0 (cycle_time_0, cycle_shift);

where:

  • cycle_time_0 define interruption cyclic time (in ns).
  • cycle_shift is used to shift the first interrupt (in ns). This is used to add a delay after communication cycle interrupt before applying the operation (this is typically used to wait that the frame has been received and managed by all devices). This global shift allow to wait that all devices are ready to operate.

NOTE: this function should be called in function passed to configure_at_init.

Step 5.2 Use and configure a DC SYNC0 and SYNC1 synchro

To activate SYNC0 and SYNC1 interruption signal,

configure_dc_sync0_1 (cycle_time_0, cycle_time_1, cycle_shift)

where:

  • cycle_time_0 define interruption cyclic time (in ns)
  • cycle_time_1 define delta time in relation to the SYNC0 fire (in ns) that is used to trigger SYNC1.
  • cycle_shift is used to shift the first interrupt (in ns).

NOTE: this function should be called in function passed to configure_at_init.

Step 6 : Create operating steps

Now device is completely configured (from ethercat network perspective), operating steps (init, run and end steps) must be defined to update cyclic buffer data (configurations, commands, device state, etc…). To do this, each step has two lambda functions:

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

To access to I/O PDO buffers datas, you have to use these functions :

  output_buffer<buffer_out_cyclic_command_t>(physical_address);
  input_buffer<buffer_in_cyclic_status_t>(physical_address);

where physical_address is the PDO buffer physical address that you used to define PDO buffer.

EPOS4 uses an init step (executed only once at device initialization time) and a run step (executed at each cycle):

...
class Epos4 : public SlaveDevice {
private:
  ...
public:
    Epos4(){
      set_id("Epos4", 0x000000fb, 0x63500000);
      configure_at_init([this]() {
        // same code as previously
        ...
        this->reset_fault();
        this->configure_dc_sync0(1000000, 100000);
        ...
      });
       // Mailboxes configuration
      define_physical_buffer<mailbox_out_t>(ASYNCHROS_OUT, 0x1000, 0x00010026);
      define_physical_buffer<mailbox_in_t>(ASYNCHROS_IN, 0x1030, 0x00010022);

      // Communication buffer config. (RxPDO / TxPDO)
      define_physical_buffer<buffer_out_cyclic_command_t>(SYNCHROS_OUT, 0x1060, 0x00010064);//0x1c12
      define_physical_buffer<buffer_in_cyclic_status_t>(SYNCHROS_IN, 0x10f0, 0x00010020);//0x1c13
      
      //distributed clock
      define_distributed_clock(true);

      ...
      add_init_step(
        [this]() { update_command_buffer(); },
        [this]() { unpack_status_buffer(); }
      );
      add_run_step(
          [this]() { update_command_buffer(); },
          [this]() { unpack_status_buffer(); }
      ); // add_run_step end
      add_end_step(...);
    }

};

With two private member functions defining the code executed at each cycle:

  • update_command_buffer() that sets commands values in command buffer, using output_buffer function.
void Epos3::update_command_buffer() {
    auto buff = this->output_buffer<buffer_out_cyclic_command_t>(0x1800);
    buff->control_word = control_word_;
    buff->target_position = target_position_;
    buff->target_velocity = target_velocity_;
    buff->target_torque = target_torque_;
    buff->position_offset = position_offset_;
    buff->velocity_offset = velocity_offset_;
    buff->torque_offset = torque_offset_;
    buff->operation_modes = control_mode_;
    buff->digital_output_state = digital_output_state_;
    buff->touch_probe_funct = touch_probe_funct_;
}
  • unpack_status_buffer() reads and memorizes values in status buffer, using input_buffer function.
void Epos3::unpack_status_buffer() {
    auto buff = this->input_buffer<buffer_in_cyclic_status_t>(0x1c00);
    status_word_ = buff->status_word;
    position_ = buff->current_position;
    velocity_ = buff->current_velocity;
    average_velocity_ = calculate_Average_value(velocity_, average_velocity_);
    torque_ = buff->current_torque;
    average_torque_ = calculate_Average_value(torque_, average_torque_);
    operation_mode_ = buff->operation_modes_read;
    digital_input_state_ = buff->digital_input_state;
    touch_probe_status_ = buff->touch_probe_status;
    touch_probe_position_pos_ = buff->touch_probe_position_pos;
    touch_probe_position_neg_ = buff->touch_probe_position_neg;
 }

Those two functions respectively read from and write to member variables to set or get values to/from the physical buffer.

Step 6.1 About the init step

An init step is executed at start of program to initialize the device, it is executed when init() function of the ethercat master is called. This step defines all commands that are needed to initialize the device like send initial command, make first update of device status, etc… If device needs many init step, all steps are execute at start. These steps are not in the main cyclic task so, to configure the sending period a timer is used. By default this timer is configured at 1 ms. If you want to change it, you have to use:

define_period_for_non_cyclic_steps(time); // In us

Step 6.2 About run step

A run step is a step that executes 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 execute per cycle and at each new cycle, the next step is run. To add a “run step” you have to use this function:

The period of run steps is completely dependant from the global loop frequency.

Step 6.3 Add an end step

An End step is a step that is executed at the end of communication (when end() function of the ethercat master is called). This step defines all commands that are needed to close correctly the device like disable command, disable power stage, etc… If device needs many end steps, all steps are execute just before close program. These steps are not in the main cyclic task so, to configure the sending period same timer that init step is used. By default this timer is configured at 1 ms. If you want to change it, you have to use same define_period_for_non_cyclic_steps() function than in init step.

In our EPOS4 example, we want to put device in a safe closed state (disable power, disable command mode, and send null commands) so we can define an end step like:

add_end_step([this](){
    set_device_state_control_word(disable_voltage);
    set_target_torque(0);
    set_target_position(0);
    set_target_velocity(0);
    set_control_mode(no_mode);
    update_command_buffer();
  },
  [this](){
    unpack_status_buffer();
  });

Step 7 : Create Epos4 API

Once everything related to ethercat, canopen configuration and device use initialization/termination has been defined, the last step simply consist in defining the member functions a user can call to control the Epos4 device.

Main role of these functions is to get or set the value of member variables used in update_command_buffer() and unpack_status_buffer.

For instance for the command variable target_torque_ used in update_command_buffer() we define a set_target_torque function like:

void Epos4::set_target_torque(double target_torque) {
  target_torque_ = static_cast<int16_t>(
      round(target_torque / static_cast<double>(rated_torque_) * 1000000000));
  // target_torque_ in rated torque/1000 and rated_torque is in uNm
}

Then these functions can be called in communication loop, as explained in the first tutorial