CoE Utilities
ethercatcpp provide a set of classes utilities that can be helpful when implementing CoE devices’ drivers. Following sections explain these utilities.
Object dictionary
The core concept of CoE is the object dictionary: it contains definitions of all variables used in a CoE device and exposed to user. Up to now, with the previous tutorials the object dictionary was implicit, meaning that you have to deal with definitions directly using their corresponding index, subindex and size. For simple devices this is obviously enough because there is not too much dictionary entries to deal with. But when you start using complex, highly configurable devices with many entries, having a clean and explicit representation of the dictionary help implementing clean code.
Create the dictionary
ethercatcpp-core provides the ObjectDictionary class to represent a device dictionary. The main utility of such object is to hold all definitions (at least all useful ones for a end user) in an explicit way. It is declared like that:
coe::cia402::CIA402ObjectDictionary dictionary_ =
coe::cia402::dico_t{
{"input_inertia", {0x27F2, 0x03, 0x20}},
{"torque_constant", {0x2003, 0x02, 0x20}},
{"gear_motor_revolutions", {0x6091, 0x01, 0x20}},
{"gear_shaft_revolutions", {0x6091, 0x02, 0x20}},
...
};The dictionary is created using coe::cia402::dico_t constructor, passing an initializer list whose each field is a DictionaryEntry bound to a given name. For instance the forst element is named "input_inertia" and its dictionary entry is:
0x27F2is the object index.0x03is the subindex. The entry is so the subobject at index 3 of object0x27F2.- the last field is the object size in bits, here 32 bits (
0x20).
Most of time is is a good idea to define the dictionary as a static attribute or global variable because it will be shared between all instances of device with same type.
Using entries
Later in your code you can simply request an object of the dictionary using the object() method and do whetever you need with it (commonly read_sdo()/write_sdo()):
auto [addr, sub, bits] = dico.object("input_inertia");
int32_t inertia;
read_sdo(addr, sub, inertia);
...There are other usefull methods but the object() method is definitively the most commonly used and useful one.
PDO Mappings
Existence of the dictionary allows to reify other concepts that will help you automating things and avoid bugs. The other core concept of CoE is the PDOMapping and a class is provided by ethercatcpp-core to represent this concept.
A PDO mapping, in CANOpen is just a kind of table that lists a set of objects. A Mapping is an object and so exists in the dictionary with a given index but this is not really necessary to represent them it in the dictionary because it follows standard indexing rules. PDO mappings are of two possible types : TX (receiving status from device) and RX (sending commands to device).
The PDOMapping object thus simply help describing such a mapping, for instance:
coe::PDOMapping rx_mapping_1_ = coe::mapping_t{
dictionary_,
0,
false,
{"controlword", "set_mode", "target_output_torque",
"target_output_velocity"}};We define the variable rx_mapping_1_ by calling coe::mapping_t constructor with arguments:
- the dictionary used to create the mapping.
- The local index of the mapping. This local index is automatically converted to the good CoE index according to the standard. For RX mappings CoE index
0x1600matches the local index 0,0x1601the local index 1, etc. For TX mappings CoE index0x1A00matches the local index 0,0x1A01the local index 1, etc. - the boolean telling if the mapping is TX (true) or RX (false).
- The list of objects contained in the mapping. Of course these object must be defined in the dictionary.
PDOMapping objects are used to represent any PDO mapping you could possibly use for a device driver implementation. You can have alternative mappings that your code can selectively choose depending on some conditions (e.g. options) or you can build them more or less dynamically if necessary.
Also these objects can represent either predefined non modifiable mappings, predefined modifiable mappings or user defined ones, the choice depends on the possibilities offered by the device and user preferences.
For non predefined mappings, you can opt out for dynamic construction of mappings using the add_object() function, for instance:
rx_mapping_1_.add_object("target_position");The main utility of such mappings is to be bound to buffers, which is explained in next section. But you can use them to automatically configure the real corresponding CoE mapping in device object dictionary:
rx_mapping_1_.configure(*this*);The argument should always be *this as it defines the device whose mapping is configured. This call should always be made in the function passed to configure_at_init() of the device instance.
The principal interest of this call is to avoid “manual” configuration of mappings using write_sdo() calls related with correct index and arguments. This way of doing is more straightforward and less bug prone.
PDO Buffers
PDO buffers are the buffers used by the master to communicate with ethercat slaves. There are possibly two buffers : one input (TX buffer) and one output (RX buffer) or just one of them for simple devices.
Defining the buffers
ethercatcpp-core provides the PDOBuffer class to represent this concept:
coe::PDOBuffer rx_buffer_ =
coe::buffer_t{false, 0x1800, 0x00010064,
rx_mapping_1_, rx_mapping_2_};The call to constructor coe::buffer_t is made with following arguments:
- the boolean telling if the buffer is the TX (
true) or the RX (false) buffer of the device. - the address of the corresponding ethercat buffer in device memory (
0x1800in the example). - the flags of ethercat buffer (
0x00010064in the example). - optionaly a variadic list of mappings (
rx_mapping_1_, rx_mapping_2_in the example)
The buffer should always hold at least one mapping but this can be done dynamically (after initialization) using the add_mapping() method.
...
rx_buffer_.add_mapping(rx_mapping_3_);
...PDOBuffer can be defined as a static attribute or global variable, or as a class attribute depending on if you want to force homoegenity of all instance at a given moment or allow instance specific configuration of buffers.
Whatever the solution you choose, a PDOBuffer should be completely define during construction of the driver object (during call to driver class constructor).
Using the buffers
The PDOBuffer objects have different utilities.
Defining physical buffer
A basic usage is to replace calls to define_physical_buffer() of the SlaveDevice class by a corresponding call to the function of PDOBuffer:
//in device constructor ...
rx_buffer_.define_physical_buffer(*this);The argument should always be *this as it defines the device whose buffer is defined. The function simply delegates the call to define_physical_buffer() of SlaveDevice by passing correct arguments contained in the PDOBuffer definition. This way PDOBuffer can be static variables or global variables that can be shared among device instances.
Automatic configuration
Same as for mappings PDO buffers can be automatically ocnfigured using dedicated configure() function:
//in function passed to configure_at_init() ...
if (not rx_buffer_.configure(*this)) {
//error !!
}The argument should always be *this as it defines the device whose buffer is configured. This call should always be made in the function passed to configure_at_init() of the device instance. Please note that this call substitute to the call to configure() of all mappings used in buffer. Indeed what this function does is:
- configuring all mappings contained in the
PDOBufferobject. - assign these mappings to the PDO buffer.
The principal interest of this call is to avoid “manual” configuration of mappings and assignemnts using (start/add/end)_(comand/status)_pdo_mapping related functions.
Automatic assignment
Sometimes, mappings are not configurable (they are predefined and not modifiable) but they can be selected to be part of a buffer. What you need then is only the assignment of mappings to buffers:
//in function passed to configure_at_init() ...
if (not rx_buffer_.assign(*this)) {
//error !!
}The call is similar to call to configure() but no mapping configuration takes place.
Memory bindings
One great functionalty offered by PDOBuffer object is the possible direct binding of device real buffer with intenal variables used by the device driver class. The main interest is then to avoid unecessary copies of variables.
Binding suppose your device class holds variables that are references to adequates types. Since these variables cannot exist at device construction time you have to define them in a private structure like that :
// in .h
#include <memory>
class MyDeviceDriver: public ethercatcpp::SlaveDevice{
struct InternalData;
std::unique_ptr<InternalData> internal_data_;
};
//in .cpp
struct MyDeviceDriver::InternalData{
uint32_t & target_position_;
InternalData(uint32_t & tgt_pos): target_position_{tgt_pos}{
}
};Then you have to define the binding between these variables and the buffer. This must be achieved in an init step of the driver because the buffer trully exist in memory at that time:
add_init_step(
[this]() {
rx_buffer_.bind_physical_buffer(*this);
auto& target_position =
rx_buffer_.map_memory<int32_t>(rx_mapping_1_, "target_position");
internal_data_ = std::make_unique<InternalData>(target_position);
//set initial commands
internal_data_->target_position_=232;
},
[this]() {
//eventually do some stuff with read variables
});Call to bind_physical_buffer is mandatory before any call to any of the possible map_memory() functions. There are many variants of map_memory() function. The one used in example:
- takes the mapping (
rx_mapping_1_) and the variable nable ("target_position") contained in this mapping as argument. - takes the type of returned variable (
int32_t) as template parameter. - returns the reference with adequate type in the device buffer memory.
Then we create the InternalData instance by passing the obtained references (target_position) at construction time.
Finally after this first init step we can directly read/write device buffer using meaningfull variables (e.g. internal_data_->target_position_=232;). There is no more need to copy variables in the buffer with driver object internal variables in pre/post functions of run steps. So a basic run step is simply:
add_run_step([]() {}, []() {});In otherwer you have no more variables copy to do !! Of course you can still trigger automatic computation on sending/reception.
You can also use dedicated map_memoryto directly map an entire mapping, a set of mappings, or the whole buffer. For instance:
add_init_step(
[this]() {
rx_buffer_.bind_physical_buffer(*this);
auto& [map1,map2,map3] = rx_buffer_.map_memory<mapping1_t,mapping2_t,mapping3_t>(rx_mapping_1_,rx_mapping_2_,rx_mapping_3_);
internal_data_ = std::make_unique<InternalData>(map1,map2,map3);
//set initial commands
},
[this]() {
//eventually do some stuff with read variables
});In this later example the map_memory() function return a tuple of references on structures olding entire mappings. For instance the mapping1_t is a struct that could be defined like:
struct [[gnu::packed]] mapping1_t{
uint16_t controlword;
int8_t set_mode;
int32_t target_output_torque;
int32_t target_output_velocity;
};Fields of the structure exactly match the sequence and type of CoE objects used in the mapping definition. Don’t forget to declare the structure as a packed (using C++ attribute [[gnu::packed]]).
Then these references are passed to the structure holding references, that must be of course modified adequately:
struct MyDeviceDriver::InternalData{
mapping1_t& map1_;
mapping2_t& map2_;
mapping3_t& map3_;
InternalData(mapping1_t& map1, mapping2_t& map2, mapping3_t& map3):
map1_{map1},
map3_{map3}{
map2_{map2},
}
};In the end this way of doing is not mandatory, you could prefer using input_buffer() and output_buffer() directly in init/run/end steps and so do an extra copy of driver internal variables, it is up to you.
Et voilà ! You now know everything about CoE utilities.