307 lines
9.9 KiB
Plaintext
307 lines
9.9 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 synchronised 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))
|
||
|
|
||
|
\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];
|
||
|
...
|
||
|
ec_config_map(&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 application Application
|
||
|
|
||
|
IO data is accessed through the IOmap, the ec_slave struct keep 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 > 0)
|
||
|
{
|
||
|
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_slave[slave].islost = TRUE;
|
||
|
printf("ERROR : slave %d lost\n",slave);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if (ec_slave[slave].islost)
|
||
|
{
|
||
|
if(!ec_slave[slave].state)
|
||
|
{
|
||
|
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
|
||
|
|
||
|
This tutorial is just one way of doing it.
|
||
|
Enjoy and happy coding!
|
||
|
|
||
|
Andreas Karlsson, rt-labs AB, www.rt-labs.com
|
||
|
*/
|