448 lines
15 KiB
Plaintext
448 lines
15 KiB
Plaintext
/** \file
|
|
|
|
\section general General
|
|
|
|
The SOEM is a library that provides the user application with the means to send
|
|
and receive EtherCAT frames. It is up to the application to provide means for:
|
|
- Reading and writing process data to be sent/received by SOEM
|
|
- Keeping local IO data synchronized with the global IOmap
|
|
- Detecting errors reported by SOEM
|
|
- Managing errors reported by SOEM
|
|
|
|
The following sections show some basic examples on how to get the SOEM up
|
|
and running, as well as making use of the process data and checking
|
|
for errors. Since all code is local to the application or global
|
|
variables, it is possible to tweak and optimize when possible.
|
|
|
|
The following example shows how to add a main function that will be
|
|
called by startup code. In this example main's only purpose is to
|
|
spawn a new task that executes SOEM.
|
|
|
|
\code
|
|
|
|
int main (void)
|
|
{
|
|
rprintp("SOEM (Simple Open EtherCAT Master)\nSimple test\n");
|
|
|
|
task_spawn ("simpletest", simpletest, 9, 8192, NULL);
|
|
|
|
\endcode
|
|
|
|
\section configuration Configuration
|
|
Followed by start of the application we need to set up the NIC to be used as
|
|
EtherCAT Ethernet interface. In a simple setup we call ec_init(ifname) and
|
|
if SOEM comes with support for cable redundancy we call ec_init_redundant
|
|
that will open a second port as backup. You can send NULL as ifname if you
|
|
have a dedicated NIC selected in the nicdrv.c. It returns >0 if succeeded.
|
|
|
|
\code
|
|
|
|
/* initialise SOEM, bind socket to ifname */
|
|
if (ec_init(ifname) > 0)
|
|
|
|
\endcode
|
|
|
|
SOEM is a light weight ethercat master library used in embedded systems, It
|
|
supports only runtime configuration. It requests a BRD (Broad Cast Read) of
|
|
address 0, all fully functional slaves in the network will respond to this
|
|
request, and therefore we will get a working counter equal to the number of
|
|
slaves in the network. ec_config_init also sets up the mailboxes for slaves
|
|
that support it. When ec_config_init finishes it will have requested all slaves
|
|
to state PRE_OP. All data read and configured are stored in a global array
|
|
which acts as a placeholder for key values, consult ec_slave for detailed
|
|
information.
|
|
\code
|
|
/* find and auto-config slaves */
|
|
if ( ec_config_init(FALSE) > 0 )
|
|
{
|
|
rprintp("%d slaves found and configured.\n",ec_slavecount);
|
|
\endcode
|
|
|
|
SOEM has now discovered and configured the network it is connected to.
|
|
Now we can verify that all slaves are present as expected. These
|
|
definitions could be generated by an external tool in an offline .h file.
|
|
The definitions could be replaced by a struct keeping slave number.
|
|
|
|
\code
|
|
|
|
#define EK1100_1 1
|
|
#define EL4001_1 2
|
|
...
|
|
#define EL2622_3 8
|
|
#define EL2622_4 9
|
|
#define NUMBER_OF_SLAVES 9
|
|
|
|
snippet
|
|
...
|
|
|
|
uint32 network_configuration(void)
|
|
{
|
|
/* Do we got expected number of slaves from config */
|
|
if (ec_slavecount < NUMBER_OF_SLAVES)
|
|
return 0;
|
|
|
|
/* Verify slave by slave that it is correct*/
|
|
if (strcmp(ec_slave[EK1100_1].name,"EK1100"))
|
|
return 0;
|
|
else if (strcmp(ec_slave[EL4001_1].name,"EL4001"))
|
|
return 0;
|
|
...
|
|
else if (strcmp(ec_slave[EL2622_4].name,"EL2622"))
|
|
return 0;
|
|
|
|
return 1;
|
|
}
|
|
|
|
simpletest
|
|
...
|
|
if (network_configuration())
|
|
...
|
|
else
|
|
rprintp("Mismatch of network units!\n");
|
|
|
|
|
|
|
|
\endcode
|
|
|
|
We now have the network up and configured. Mailboxes are up for slaves that support
|
|
it. Next we will create an IOmap and configure the SyncManager's and
|
|
FMMU's to link the EtherCAT master and the slaves. The IO mapping is done
|
|
automatically, SOEM strives to keep the logical process image as compact as
|
|
possible. It is done by trying to fit Bit oriented slaves together in single
|
|
bytes. Below is an example of 8 slaves and how they are ordered. During
|
|
mapping SOEM also calculates an expected WKC for the IO mapped together.
|
|
That is the primary key to detect errors.
|
|
- Outputs are placed together in the beginning of the IOmap
|
|
- Inputs follow
|
|
|
|
When the mapping is done SOEM requests slaves to enter SAFE_OP.
|
|
|
|
\code
|
|
|
|
char IOmap[128];
|
|
int usedmem;
|
|
...
|
|
usedmem = ec_config_map(&IOmap);
|
|
if (usedmem <= sizeof(IOmap))
|
|
...
|
|
\endcode
|
|
|
|
\image html memory_layout.png "memory layout, mapping between physical and logical"
|
|
\image latex memory_layout.png "memory layout, mapping between physical and logical" width=15cm
|
|
|
|
To enter state OP we need to send valid data to outputs. The EtherCAT frame
|
|
handling is split into ec_send_processdata and ec_receive_processdata.
|
|
- ec_send_processdata sends the frame on the NIC and saves the frame on
|
|
the stack for receive to fetch.
|
|
- ec_receive_processdata(EC_TIMEOUTRET) tries to fetch the frames on the
|
|
stack. We send an argument for how long we will try to fetch the frame.
|
|
ec_receive_processdata returns the working counter.
|
|
|
|
\code
|
|
/* send one valid process data to make outputs in slaves happy*/
|
|
ec_send_processdata();
|
|
wkc = ec_receive_processdata(EC_TIMEOUTRET);
|
|
...
|
|
ec_writestate(0);
|
|
/* wait for all slaves to reach OP state */
|
|
ec_statecheck(0, EC_STATE_OPERATIONAL, EC_TIMEOUTSTATE);
|
|
\endcode
|
|
|
|
- Now we have a system up and running, all slaves are in state operational.
|
|
|
|
\section configuration_custom Custom Configuration
|
|
|
|
\subsection iomap_config PDO Assign and PDO Config
|
|
|
|
Do custom configuration with PDO Assign or PDO Config. SOEM support custom configuration during start via a
|
|
PreOP to SafeOP configuration hook. It can be done per slave and should be set before calling
|
|
the configuration and mapping of process data, e.g. the call to ec_config_map. Setting the configuration
|
|
hook ensure that the custom configuration will be applied when calling recover and re-configuration
|
|
of a slave, as described below.
|
|
|
|
\code
|
|
|
|
int EL7031setup(uint16 slave)
|
|
{
|
|
int retval;
|
|
uint16 u16val;
|
|
|
|
retval = 0;
|
|
|
|
/* Map velocity PDO assignment via Complete Access*/
|
|
uint16 map_1c12[4] = {0x0003, 0x1601, 0x1602, 0x1604};
|
|
uint16 map_1c13[3] = {0x0002, 0x1a01, 0x1a03};
|
|
|
|
retval += ec_SDOwrite(slave, 0x1c12, 0x00, TRUE, sizeof(map_1c12), &map_1c12, EC_TIMEOUTSAFE);
|
|
retval += ec_SDOwrite(slave, 0x1c13, 0x00, TRUE, sizeof(map_1c13), &map_1c13, EC_TIMEOUTSAFE);
|
|
|
|
/* set some motor parameters, just as example */
|
|
u16val = 1200; // max motor current in mA
|
|
retval += ec_SDOwrite(slave, 0x8010, 0x01, FALSE, sizeof(u16val), &u16val, EC_TIMEOUTSAFE);
|
|
u16val = 150; // motor coil resistance in 0.01ohm
|
|
retval += ec_SDOwrite(slave, 0x8010, 0x04, FALSE, sizeof(u16val), &u16val, EC_TIMEOUTSAFE);
|
|
|
|
/* set other necessary parameters as needed */
|
|
...
|
|
printf("EL7031 slave %d set, retval = %d\n", slave, retval);
|
|
return 1;
|
|
}
|
|
|
|
void simpletest(char *ifname)
|
|
{
|
|
...
|
|
/* Detect slave beckhoff EL7031 from vendor ID and product code */
|
|
if((ec_slave[slc].eep_man == 0x00000002) && (ec_slave[slc].eep_id == 0x1b773052))
|
|
{
|
|
printf("Found %s at position %d\n", ec_slave[slc].name, slc);
|
|
/* link slave specific setup to preop->safeop hook */
|
|
ec_slave[slc].PO2SOconfig = EL7031setup;
|
|
}
|
|
...
|
|
\endcode
|
|
|
|
\subsection iomap_layout Legacy versus overlapping IOmap
|
|
|
|
IOmap options legacy versus overlapping. Overlapping IOmap was introduced to handle
|
|
the TI ESC that doesn't support RW access to non-interleaved input and output process
|
|
data of multiple slaves. The difference is that legacy IOmapping will send IOmap as is
|
|
on the EtherCAT network while the overlapping will re-use logic addressing per slave to
|
|
replace RxPDO process data coming from the Master with TxPDO process data generated by the slave
|
|
sent back to the master.
|
|
|
|
Overview of legacy pdo map
|
|
\image html legacy_iomap.png "Legacy IOmapping"
|
|
\image latex legacy_iomap.png "Legacy IOmapping" width=15cm
|
|
|
|
Overview of overlapping pdo map
|
|
\image html overlapping_iomap.png "Overlapping IOmapping"
|
|
\image latex overlapping_iomap.png "Overlapping IOmapping" width=15cm
|
|
|
|
\subsection iomap_groups EtherCAT slave groups
|
|
|
|
Slave groups can be used to group slaves into separate logic groups within an EtherCAT network.
|
|
Each group will have its own logic address space mapped to an IOmap address and make it possible to
|
|
send and receive process data at different update rate.
|
|
|
|
Below is an example on how to assign a slave to a group. <b>OBS!</b> A slave can only be member in one group.
|
|
|
|
\code
|
|
for (cnt = 1; cnt <= ec_slavecount; cnt++)
|
|
{
|
|
if ( <some condition> )
|
|
{
|
|
ec_slave[cnt].group = X;
|
|
}
|
|
else
|
|
{
|
|
ec_slave[cnt].group = Y;
|
|
}
|
|
}
|
|
\endcode
|
|
|
|
Alternative 1, configure all slave groups at once, call ec_config_map or ec_config_map_group with arg 0.
|
|
This option will share IOmap and store the group IOmap data at offset EC_LOGGROUPOFFSET.
|
|
|
|
\code
|
|
ec_config_map_group(&IOmap, 0);
|
|
\endcode
|
|
|
|
Alternative 2, configure the slave groups one by one, call ec_config_map or ec_config_map_group with arg X, Y.
|
|
This option will use different, supplied by the user, IOmaps.
|
|
|
|
\code
|
|
ec_config_map_group(&IOmap1, X);
|
|
ec_config_map_group(&IOmap2, Y);
|
|
\endcode
|
|
|
|
To exchange process data for given group(s) the user must call send/recv process data per group.
|
|
The send and receive stack of process data don't consider groups, so the application has to send
|
|
and receive the process data for one group before sending/receiving process data for another group.
|
|
|
|
\code
|
|
ec_send_processdata_group(X);
|
|
ec_receive_processdata_group(X, EC_TIMEOUTRET);
|
|
ec_send_processdata_group(Y);
|
|
ec_receive_processdata_group(Y, EC_TIMEOUTRET);
|
|
\endcode
|
|
|
|
\section application Application
|
|
|
|
\subsection iomap Accessing data through IOmap
|
|
|
|
IOmap is the fastest mechanism for accessing slaves' IO data.
|
|
Using this mechanism, the ec_slave struct keeps pointers
|
|
to the start byte in the IO map on slave level together with start bit within
|
|
the start byte. This way we can bit mask IO on bit level even though SOEM
|
|
has combined slave data to minimize the frame size to be sent. We'll use
|
|
slave 8 in the picture above as an example. From a printout from ec_slave we
|
|
have the following:
|
|
- Slave:8
|
|
- Name:EL2622
|
|
- Output size: 2bits
|
|
- Input size: 0bits
|
|
- Configured address: 1008
|
|
- Outputs address: 18cf6
|
|
- Inputs address: 0
|
|
- FMMU0 Ls:2 Ll: 1 Lsb:4 Leb:5 Ps:f00 Psb:0 Ty:2 Act:1
|
|
|
|
The Outputs address: 18cf6 is the pointer to slave 8's start byte. The FMMU's
|
|
Lsb:4 (LogicalStartBit) = ec_slave.Ostartbit telling us how to mask for the
|
|
individual bits in the combined byte. The same goes for byte addressed slaves,
|
|
but byte slaves only need the byte start address since they are byte aligned,
|
|
the start bit will be 0.
|
|
|
|
Some example on how to access different types of data
|
|
|
|
Set an output int 16 value when memory alignment needs to be considered,
|
|
arguments is:
|
|
- slave number in ethercat network
|
|
- module index as index internal to the slave in case more than one
|
|
channel
|
|
- value to write
|
|
|
|
\code
|
|
|
|
#define EL4001_1 2
|
|
...
|
|
void set_output_int16 (uint16 slave_no, uint8 module_index, int16 value)
|
|
{
|
|
uint8 *data_ptr;
|
|
|
|
data_ptr = ec_slave[slave_no].outputs;
|
|
/* Move pointer to correct module index*/
|
|
data_ptr += module_index * 2;
|
|
/* Read value byte by byte since all targets can't handle misaligned
|
|
* addresses
|
|
*/
|
|
*data_ptr++ = (value >> 0) & 0xFF;
|
|
*data_ptr++ = (value >> 8) & 0xFF;
|
|
}
|
|
...
|
|
set_output_int16(EL4001_1,0,slave_EL4001_1.out1);
|
|
|
|
\endcode
|
|
|
|
Target can handle non aligned pointers to the IOmap
|
|
\code
|
|
|
|
typedef struct PACKED
|
|
{
|
|
int16 outvalue1;
|
|
int16 outvalue2;
|
|
} out_EL4132t;
|
|
|
|
out_EL4132t *out_EL4132;
|
|
...
|
|
/* connect struct pointers to slave I/O pointers */
|
|
out_EL4132 = (out_EL4132t*) ec_slave[3].outputs;
|
|
out_EL4132->outvalue2 = 0x3FFF;
|
|
|
|
...
|
|
\endcode
|
|
|
|
Identify and manage errors. The key is the Working Counter, CRC errors
|
|
and errors local to the slave causing a state change can be detected by loss
|
|
of Working Counter since the syncmanagers won't get updated. When returning
|
|
Working Counter don't match Expected Working Counter something is wrong, then it
|
|
is up to an error handler to act, locate the erroneous slave and decide what action
|
|
to perform. The error may not be fatal. Some basic code from simple_test.
|
|
\code
|
|
|
|
wkc = ec_receive_processdata(EC_TIMEOUTRET);
|
|
expectedWKC = (ec_group[0].outputsWKC * 2) + ec_group[0].inputsWKC;
|
|
|
|
if( inOP && ((wkc < expectedWKC) || ec_group[currentgroup].docheckstate))
|
|
{
|
|
if (needlf)
|
|
{
|
|
needlf = FALSE;
|
|
printf("\n");
|
|
}
|
|
/* one ore more slaves are not responding */
|
|
ec_group[currentgroup].docheckstate = FALSE;
|
|
ec_readstate();
|
|
for (slave = 1; slave <= ec_slavecount; slave++)
|
|
{
|
|
if ((ec_slave[slave].group == currentgroup) && (ec_slave[slave].state != EC_STATE_OPERATIONAL))
|
|
{
|
|
ec_group[currentgroup].docheckstate = TRUE;
|
|
if (ec_slave[slave].state == (EC_STATE_SAFE_OP + EC_STATE_ERROR))
|
|
{
|
|
printf("ERROR : slave %d is in SAFE_OP + ERROR, attempting ack.\n", slave);
|
|
ec_slave[slave].state = (EC_STATE_SAFE_OP + EC_STATE_ACK);
|
|
ec_writestate(slave);
|
|
}
|
|
else if(ec_slave[slave].state == EC_STATE_SAFE_OP)
|
|
{
|
|
printf("WARNING : slave %d is in SAFE_OP, change to OPERATIONAL.\n", slave);
|
|
ec_slave[slave].state = EC_STATE_OPERATIONAL;
|
|
ec_writestate(slave);
|
|
}
|
|
else if(ec_slave[slave].state > EC_STATE_NONE)
|
|
{
|
|
if (ec_reconfig_slave(slave, EC_TIMEOUTMON))
|
|
{
|
|
ec_slave[slave].islost = FALSE;
|
|
printf("MESSAGE : slave %d reconfigured\n",slave);
|
|
}
|
|
}
|
|
else if(!ec_slave[slave].islost)
|
|
{
|
|
/* re-check state */
|
|
ec_statecheck(slave, EC_STATE_OPERATIONAL, EC_TIMEOUTRET);
|
|
if (ec_slave[slave].state == EC_STATE_NONE)
|
|
{
|
|
ec_slave[slave].islost = TRUE;
|
|
printf("ERROR : slave %d lost\n",slave);
|
|
}
|
|
}
|
|
}
|
|
if (ec_slave[slave].islost)
|
|
{
|
|
if(ec_slave[slave].state == EC_STATE_NONE)
|
|
{
|
|
if (ec_recover_slave(slave, EC_TIMEOUTMON))
|
|
{
|
|
ec_slave[slave].islost = FALSE;
|
|
printf("MESSAGE : slave %d recovered\n",slave);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ec_slave[slave].islost = FALSE;
|
|
printf("MESSAGE : slave %d found\n",slave);
|
|
}
|
|
}
|
|
}
|
|
if(!ec_group[currentgroup].docheckstate)
|
|
printf("OK : all slaves resumed OPERATIONAL.\n");
|
|
}
|
|
\endcode
|
|
|
|
|
|
\subsection sdo Accessing SDOs and PDOs
|
|
|
|
There are multiple ways a slave can communicate with the master. CANopen over
|
|
EtherCAT (CoE) is a (slow but flexible) asynchronous mechanism for transferring
|
|
data via mailboxes.
|
|
|
|
SOEM provides the ecx_SDOread() and ecx_SDOwrite() functions for reading and
|
|
writing a CoE SDO (Service Data Object) given the corresponding index and
|
|
subindex.
|
|
|
|
SOEM does not provide specific functions for accessing CoE PDOs (Process Data
|
|
Objects). On most slaves, however, it is possible to use the same functions
|
|
available for SDOs. In the seldom case in which the PDO object has been marked
|
|
in the CoE dictionary as "PDO only", only IOmap access is allowed.
|
|
Note that a list of the PDO mappings can be retrieved through the "slaveinfo
|
|
<interface> -map" command.
|
|
|
|
---------------------
|
|
|
|
This tutorial is just one way of doing it.
|
|
Enjoy and happy coding!
|
|
|
|
Andreas Karlsson, rt-labs AB, www.rt-labs.com
|
|
*/
|