Estructura de los Frames EtherCAT


EtherCAT Frame Format​

Imagen - fuente: www.embien.com

 

 

Los datos en EtherCAT se transmiten en frames Ethernet estándar, pero con un formato especializado para el protocolo EtherCAT. 

Cada frame puede contener uno o más datagramas EtherCAT en el campo de datos (payload).


I. Cabecera Ethernet 14 bytes


Dirección MAC de destino 6 bytes, es FF:FF:FF:FF:FF:FF (broadcast) porque debe atravesar todos los nodos. 

Dirección MAC de origen 6 bytes, es la MAC del maestro. 

EtherType 2 bytes (0x88A4 para EtherCAT): 



II. Cabecera EtherCAT 2 bytes


Longitud 11 bits

Reservado 1 bit

Tipo 4 bits. PDO (Process Data Object) ó SDO (Service Data Object) 



III. Datos EtherCAT (EtherCAT Datagram), 46–1500 bytes (en Ethernet estándar): 


Comando (Cmd) 1 byte: Indica la operación a realizar (lectura, escritura, etc.). 

Index 1 byte: Identifica el tipo de dato o registro que se está accediendo. 

Address 4 bytes: Dirección del dispositivo esclavo o registro interno. 

Cada esclavo en la red tiene siempre una dirección física única, que es asignada automáticamente por el maestro en función del orden de conexión. La asignación ocurre durante la inicialización. Esta dirección es estática mientras no se modifique la topología. Y pueden también tener o no una dirección lógica, si se la asigna manualmente. Se usa para PDOs permitiendo acceder a los datos sin importar la topología de red, o dado el caso para acceder a varios esclavos a la vez (si se les configura la misma). Se configura desde el ESI File o mediante herramientas del maestro.  

En resumen, aquí pueden ir tres valores diferentes: 

a. Dirección física fija asignada en la inicialización. 

b. Dirección lógica. Permitiendo acceder sin importar el orden en la topología de red. O a varios esclavos a la vez. 

c. Dirección “auto-incremento”. Se usa para recorrer la red en orden. 

Longitud de los datos 11 bits: Tamaño de los datos a transmitir. 

Reservado 3 bits 

C bit (bit circulante) 1 bit. El frame EtherCAT siempre viaja por la red hasta regresar al maestro. Cada esclavo analiza los datagramas que le corresponden y decide si los modifica o no. El bit C indica si el esclavo puede modificar el datagrama después de procesarlo. Si C=1, el esclavo puede modificar el datagrama antes de enviarlo al siguiente nodo. Si C=0, el esclavo lo procesa pero no lo cambia.

Indicador de múltiples datagramas (M) 1 bit. Indica si el frame contiene más de un datagrama EtherCAT dentro del payload. 0 si contiene solo un datagrama, 1 si contiene varios. 

Registro de solicitud de interrupción (IRQ) 2 bytes. Permite que el esclavo notifique al maestro que necesita atención mediante una interrupción. Cada esclavo puede escribir un valor específico en este campo para indicar un evento o condición que requiere atención. Puede ser utilizado para notificar fallas, cambios de estado, sincronización, etc. 

Datos (tamaño variable): La información que se envía o recibe. 

Working Counter (WKC) 2 bytes: Un contador que indica cuántos esclavos han procesado el datagrama. Es especifico para cada esclavo en su datagrama, se incrementa cada vez que un esclavo procesa correctamente un datagrama (no es un contador global para todos los esclavos).



IV. FCS (Frame check sequence) 4 bytes

Utiliza un CRC (Cyclic Redundancy Check), un cálculo matemático para verificar la integridad del frame. 

Raw Sockets en Linux ejemplo de servidor (capturador de paquetes)

Explicación paso a paso (con un ejemplo): 


0. Inclusiones necesarias.

#include <stdio.h>

#include <stdlib.h>

#include <sys/socket.h>               // Para socket(), bind(), recv()     

#include <unistd.h>                  // Para close()

#include <netinet/in.h>               // Para estructuras de red 

#include <netinet/if_ether.h>          // Para encabezados Ethernet como ETH_P_ALL

#include <netpacket/packet.h>         // Para la estructura sockaddr_ll

#include <arpa/inet.h>                // Para ntohs(). En la red se usa big endian (byte más significativo primero).


Preparamos una pequeña función para imprimir en pantalla las MAC address que procesaremos luego:

void print_mac_address(const unsigned char *mac) {


// '%X' se usa para imprimir hexadecimales. Y '02' limita el numero de caracteres a mostrar.

    printf("%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);    

}


1. Buffer.

Necesitamos un buffer donde almacenar lo recibido (o capturado).

#define BUFFER_SIZE 65536             // Tamaño máximo de un paquete basado en IP (0x10000)


int socketfd;

unsigned char buffer[BUFFER_SIZE];


2. Crear el socket.

socketfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));

if (socketfd < 0) {

        perror("Error creating socket");

        return 1;

    }

AF_PACKET indica capa 2 (Data Link Layer) permitiendo ver el encabezado Ethernet.

htons porque se crea en el host y se manda esta información a la red.

3. Bucle de escucha. 

Dentro de un loop.

1. Crear las variables necesarias para guardar la dirección del cliente. El tipo de dato de estas variables es el necesario para que las funciones de socket como recvfrom() y sendto() funcionen.

while (1) {

struct sockaddr_ll sender_addr;

socklen_t sender_len = sizeof(sender_addr);


2. Recibir los paquetes Ethernet. Por defecto requiere que el tipo de dato sea ssize_t que es un long int (%ld).

ssize_t packet_size = recvfrom(socketfd, buffer, BUFFER_SIZE, 0, (struct sockaddr *)&sender_addr, &sender_len);

En Raw Sockets puede utilizarse tanto la función recv() como recvfrom(), pero se recomienda más esta última ya que proporciona información sobre la dirección de origen. 

La función devuelve un la cantidad de bytes y es necesario guardarla con el tipo ssize_t.

Los parámetros son:

a. El file descriptor del socket (del servidor obviamente). 

b. El buffer donde guardaremos lo que viene del cliente.

c. El tamaño del buffer

d. Un flag que por lo gral se deja en 0, (tiene opciones avanzadas).

c. La dirección del cliente con cast a un puntero para que sea interpretada como sockaddr 

(en un principio la tuvimos que declarar como sockaddr_ll (capa 2)) 

Nota: en la capa 3 con servidores de socket TCP/UDP el tipo es sockaddr_in


3. Chequeamos que se haya recibido algo.

if (packet_size < 0) {

perror("Error receiving");

break;

}


4. Extraemos la información del paquete Ethernet recibido.

Hasta ahora tenemos lo recibido en el buffer que le indicamos anteriormente a la función recvfrom(). “Todo junto”.

Para interpretar fácilmente esta información nos ayudamos de ethhdr que es una estructura definida en la biblioteca <linux/if_ether.h> en sistemas Linux. Contiene los campos básicos: [MAC_destino] [MAC_origen] [Protocolo].

En C si hacemos un cast a un puntero con un tipo de dato, no estamos cambiando el tipo de dato en si mismo, sino que nos permite decirle al compilador que para este caso interprete este valor como si fuera del tipo de dato al que estamos apuntando.

Un puntero al tipo de datos ethhdr (ethernet header), le dice al compilador que interprete los datos almacenados en buffer con este formato para acceder a ellos.

FF FF FF FF FF FF  00 11 22 33 44 55  08 00

└─ MAC Dest ──┘ └ MAC Origen ┘  └Tipo┘

struct ethhdr *eth = (struct ethhdr *)buffer;

Esto hace que: 

a. los datos en buffer sean tratados como si se fueran del tipo de dato ethhdr 

b. se guarda en un puntero llamado eth la dirección de memoria donde comienzan los datos de buffer 

c. ahora se puede acceder a estos datos como si de un dato tipo ethhdr se tratase, con sus respectivos campos…

     Por ejemplo: 

          ethhdr -> h_dest 

          ethhdr -> h_source 

          ethhdr -> h_proto 

*) ethhdr tiene solo estos tres campos, entonces ¿qué pasa con el resto de lo que hay en ‘buffer’? No son interpretados. El puntero solo interpretara los primeros 14 bytes. Así que no hay problema. 

5. Imprimimos en pantalla cada campo deseado

printf("\n------------------------------------------\n");

printf("Packet received: %ld bytes\n", packet_size);

printf("MAC Source: ");

print_mac_address(eth->h_source);

printf("\nMAC Destination: ");

print_mac_address(eth->h_dest);


// Identificar el protocolo Ethernet

printf("\nProtocol Type: ");

switch (ntohs(eth->h_proto)) {

case ETH_P_IP:

printf("IPv4\n");

break;

case ETH_P_ARP:

printf("ARP\n");

break;

case ETH_P_IPV6:

printf("IPv6\n");

break;

default:

// Revisa el campo del protocolo e imprime en hexadecimal los 4 digitos correspondientes.

        printf("Unknown (0x%04X)\n", ntohs(eth->h_proto));                    

        break;

    }


// Mostrar los primeros bytes del contenido (máximo 10 bytes)

// Los primeros 14 bytes son la cabecera del paquete Ethernet. 

// Asi que el recorremos desde 14 + i ... hasta packet_size - 14 para no salirnos...

// Imprimimos en hexadecimal. 


printf("Payload (first 10 bytes): ");

for (int i = 0; i < 10 && i < packet_size - 14; i++) {                       

    printf("%02X ", buffer[14 + i]);

}

printf("\n------------------------------------------\n");


6. Así los paquetes son capturados y mostrados todos, lo que llenará la pantalla de la terminal muy rápidamente. Como es un simple programa de ejemplo, agregar una pausa al final antes de cerrar el bucle permitirá visualizar mucho mejor (aunque se pierdan muchos paquetes en el medio, no importa en este caso).

    // Pausa para evitar que la salida sea muy rápida

    usleep(500000); // 500ms de espera entre paquetes

} // Cierre del loop “while (1)”


4. Cierre del socket.

close(socketfd);

return 0;