ZeroOne AI
← 返回文章列表

SOEM-EtherCAT 源代码解析(一):数据类型定义

👁 3
分类:机器人自动化

逐段拆解开源 EtherCAT 主站库 SOEM 的核心头文件 ec_type.h:从基础数据类型、错误码、帧长与超时宏,到以太网头、数据包头结构体,再到状态/命令/数据类型枚举、SII 从站信息接口、邮箱协议与寄存器定义,一文打透 EtherCAT 主站的类型地基。

SOEM-EtherCAT 源代码解析(一):数据类型定义

一、项目背景

EtherCAT(Ethernet for Control Automation Technology)是德国倍福(Beckhoff)推出的高性能工业以太网现场总线协议,凭借纳秒级同步精度、灵活的拓扑结构和低成本实现,已成为运动控制、机器人与智能装备领域应用最广泛的实时以太网协议之一。

SOEM(Simple Open EtherCAT Master)是一个轻量级的开源 EtherCAT 主站实现,采用 C 语言编写,代码结构清晰、可移植性强,被大量运动控制卡、机器人控制器和 LinuxCNC 等开源项目采用。本系列文章将带领大家从源码层面剖析 SOEM 主站的实现原理。作为开篇,本文聚焦于 SOEM 最核心的基础头文件 ec_type.h——它是整个库的"公共字典",所有类型、宏、枚举、结构体都在这里定义,是理解后续一切源码的钥匙。

二、技术方案:ec_type.h 的职责

ec_type.h 定义了整个库通用的基础内容,按功能可划分为七大类:

三、系统架构:逐段解读核心定义

3.1 编译环境配置

#ifndef _EC_TYPE_H
#define _EC_TYPE_H

#include <stdint.h>

#ifdef __cplusplus
extern "C"
{
#endif

/** Define Little or Big endian target */
/*定义通讯为小端模式*/
#define EC_LITTLE_ENDIAN

/** define EC_VER1 if version 1 default context and functions are needed
 * comment if application uses only ecx_ functions and own context */
/*使用V1版本上下文函数*/
#define EC_VER1

3.2 返回值、错误码与超时参数

/** return value general error */
/*通用错误返回值*/
#define EC_ERROR           -3

/** return value no frame returned */
/*无数据帧返回*/
#define EC_NOFRAME         -1

/** return value unknown frame received */
/*无法识别的数据包接收*/
#define EC_OTHERFRAME      -2

SOEM 的接口返回值遵循"非负为成功、负值为异常"的约定:EC_NOFRAME(-1)表示超时未收到数据帧;EC_OTHERFRAME(-2)表示收到帧但无法识别;EC_ERROR(-3)为通用错误。

库级错误码则用枚举 ec_err 表达,供上层应用区分失败原因:

typedef enum
{
   EC_ERR_OK         = 0,   // 正常
   EC_ERR_ALREADY_INITIALIZED   = 1,  // 库已初始化
   EC_ERR_NOT_INITIALIZED   = 2,  // 库未初始化
   EC_ERR_TIMEOUT   = 3,    // 执行超时
   EC_ERR_NO_SLAVES = 4,    // 未发现从站
   EC_ERR_NOK = 5           // 函数执行失败
} ec_err;

超时参数全部以微秒(µs)为单位,集中定义了主站各环节的轮询与等待节奏:

值(µs)用途
EC_TIMEOUTRET2000发送帧返回接收的轮询超时
EC_TIMEOUTRET36000安全传输超时,最多重试 3 次
EC_TIMEOUTSAFE20000"safe"变体返回超时(如无线场景)
EC_TIMEOUTEEP20000从站 EEPROM 访问超时
EC_TIMEOUTTXM20000发送邮箱轮询超时
EC_TIMEOUTRXM700000接收邮箱轮询超时(邮箱数据量较大)
EC_TIMEOUTSTATE2000000状态机切换检测超时

3.3 帧长、缓冲区与报文结构体

/** maximum EtherCAT frame length in bytes */
/*最大ethercat数据包长度*/
#define EC_MAXECATFRAME    1518

/** maximum EtherCAT LRW frame length in bytes */
/*最大EtherCAT LRW数据包长度*/
/* MTU - Ethernet header - length - datagram header - WCK - FCS */
#define EC_MAXLRWDATA      (EC_MAXECATFRAME - 14 - 2 - 10 - 2 - 4)

/** size of DC datagram used in first LRW frame */
/*在首帧LRW数据包中DC同步数据帧大小*/
#define EC_FIRSTDCDATAGRAM 20

/** standard frame buffer size in bytes */
/*标准数据帧缓冲大小*/
#define EC_BUFSIZE         EC_MAXECATFRAME

/** datagram type EtherCAT */
/*EtherCAT 数据包类型*/
#define EC_ECATTYPE        0x1000

/** number of frame buffers per channel (tx, rx1 rx2) */
/*每通道(tx, rx1 rx2)数据包缓存数量*/
#define EC_MAXBUF          16

报文结构体是理解 EtherCAT 数据链路的关键,先看以太网头:

/** ethernet header definition */
/*以太网头数据包*/
PACKED_BEGIN
typedef struct PACKED
{
   uint16  da0,da1,da2;   // 目标MAC
   uint16  sa0,sa1,sa2;   // 源MAC
   uint16  etype;         // 帧类型0x88A4
} ec_etherheadert;
PACKED_END

#define ETH_HEADERSIZE      sizeof(ec_etherheadert)

再看紧随其后的 EtherCAT 数据包头(子报文头):

PACKED_BEGIN
typedef struct PACKED
{
   uint16  elength;   // 数据包长度
   uint8   command;   // EtherCAT命令
   uint8   index;     // 索引,SOEM用于Tx/Rx重组
   uint16  ADP;       // 从站地址区
   uint16  ADO;       // 从站偏移地址区
   uint16  dlength;   // 数据段长度
   uint16  irpt;      // 中断(未使用)
} ec_comt;
PACKED_END

#define EC_HEADERSIZE       sizeof(ec_comt)
#define EC_ELENGTHSIZE      sizeof(uint16)
#define EC_CMDOFFSET        EC_ELENGTHSIZE
#define EC_WKCSIZE          sizeof(uint16)
#define EC_DATAGRAMFOLLOWS  (1 << 15)   // 子报文中后续报文标志位

一个 EtherCAT 帧可以串联多个子报文(datagram),每个子报文由 ec_comt 头 + 数据段 + WKC(工作计数器)组成。EC_DATAGRAMFOLLOWSdlength 的最高位标志,置位表示后面还有子报文。WKC 是 EtherCAT 协议的核心机制:每个从站处理报文后递增自己的工作计数,主站通过 WKC 判断数据是否被正确处理。

3.4 EEPROM 缓存与重试

/** size of EEPROM bitmap cache */
#define EC_MAXEEPBITMAP    128

/** size of EEPROM cache buffer */
#define EC_MAXEEPBUF       EC_MAXEEPBITMAP << 5   // 4096

/** default number of retries if wkc <= 0 */
#define EC_DEFAULTRETRIES  3

EC_MAXEEPBITMAP 为 128,EC_MAXEEPBUF 为 128 << 5 = 4096 字节,对应 EtherCAT 从站 EEPROM(SII)的标准容量(4KB),用于缓存从站 EEPROM 信息;EC_DEFAULTRETRIES 定义了当 WKC ≤ 0 时的默认重试次数,保证数据在偶发丢帧场景下仍能可靠送达。

3.5 基础数据类型与时间结构体

/* General types */
typedef uint8_t             boolean;    //无符号8位作为bool类型
#define TRUE                1
#define FALSE               0
typedef int8_t              int8;       //有符号8位
typedef int16_t             int16;      //有符号16位
typedef int32_t             int32;      //有符号32位
typedef uint8_t             uint8;      //无符号8位
typedef uint16_t            uint16;     //无符号16位
typedef uint32_t            uint32;     //无符号32位
typedef int64_t             int64;      //有符号64位
typedef uint64_t            uint64;     //无符号64位
typedef float               float32;    //单浮点
typedef double              float64;    //双浮点

typedef struct
{
    uint32 sec;     /*< Seconds elapsed since the Epoch (Jan 1, 1970) */
    int32 usec;      /*< Microseconds elapsed since last second boundary */
} ec_timet;         /*时间单位结构体*/

typedef struct osal_timer
{
    ec_timet stop_time;
} osal_timert;      /*定时器记录时间结构体*/

typedef uint8 ec_bufT[EC_BUFSIZE];   // 帧缓冲区

SOEM 基于 <stdint.h> 为所有整型定义统一别名,保证跨平台编译时数据类型宽度完全确定,避免不同编译器下 int/long 宽度不一致导致的报文解析错位。ec_timet 采用"秒 + 微秒"结构表示绝对时间,与 POSIX timeval 对齐,是所有超时判断的基础;osal_timert 配合 OSAL(操作系统抽象层)提供跨平台定时能力;ec_bufT 是每个通道的收发帧缓冲类型。

3.6 PACKED 对齐宏与跨平台适配

#if defined(WIN32)
    #ifndef PACKED
        #define PACKED_BEGIN __pragma(pack(push, 1))
        #define PACKED
        #define PACKED_END __pragma(pack(pop))
    #endif
    #define OSAL_THREAD_HANDLE HANDLE
    #define OSAL_THREAD_FUNC void
    #define OSAL_THREAD_FUNC_RT void
#elif defined(__linux__)
    #ifndef PACKED
        #define PACKED_BEGIN
        #define PACKED  __attribute__((__packed__))
        #define PACKED_END
    #endif
    #define OSAL_THREAD_HANDLE task_t *
    #define OSAL_THREAD_FUNC void
    #define OSAL_THREAD_FUNC_RT void
#else
    ...
#endif

PACKED 系列宏解决结构体字节对齐问题:Windows 下用 #pragma pack(push, 1) 强制 1 字节对齐,GCC/Linux 下用 __attribute__((__packed__)) 实现同样效果。EtherCAT 报文字段均为紧排,任何隐式填充都会导致解析错位,因此所有协议结构体都声明为 PACKEDOSAL_THREAD_* 系列宏则把线程句柄、线程函数签名统一抽象,保证主站可以无缝运行在 Windows 与 Linux 上。

3.7 状态、缓冲区与命令枚举

从站状态机枚举 ec_state 是 EtherCAT 状态机(Init → Pre-Op → Safe-Op → Op)的基础:

typedef enum
{
   EC_STATE_INIT           = 0x01,   // 初始化
   EC_STATE_PRE_OP         = 0x02,   // 预操作
   EC_STATE_BOOT           = 0x03,   // 升级模式
   EC_STATE_SAFE_OP        = 0x04,   // 安全操作
   EC_STATE_OPERATIONAL    = 0x08,   // 运行模式
   EC_STATE_ACK            = 0x10,   // 错误或ACK错误
   EC_STATE_ERROR          = 0x10
} ec_state;

缓冲区状态 ec_bufstate 描述一个帧缓冲的生命周期:EMPTY(空)→ ALLOC(已分配)→ TX(已发送)→ RCVD(已接收)→ COMPLETE(周期完成),主站据此管理收发缓冲的轮转。

EtherCAT 命令类型枚举 ec_cmdtype 对应协议定义的 15 种命令字:

typedef enum
{
   EC_CMD_NOP          = 0x00,   // 空命令
   EC_CMD_APRD,                  // 自增读
   EC_CMD_APWR,                  // 自增写
   EC_CMD_APRW,                  // 自增读写
   EC_CMD_FPRD,                  // 配置地址读
   EC_CMD_FPWR,                  // 配置地址写
   EC_CMD_FPRW,                  // 配置地址读写
   EC_CMD_BRD,                   // 广播读
   EC_CMD_BWR,                   // 广播写
   EC_CMD_BRW,                   // 广播读写
   EC_CMD_LRD,                   // 逻辑内存读
   EC_CMD_LWR,                   // 逻辑内存写
   EC_CMD_LRW,                   // 逻辑内存读写
   EC_CMD_ARMW,                  // 自增多写
   EC_CMD_FRMW                   // 配置多写
} ec_cmdtype;

APxx 系列按从站在总线上的物理位置寻址(自动递增),FPxx 系列按配置的站地址寻址,Bxx 广播寻址,LRW 则是过程数据交换最常用的逻辑寻址方式(配合 FMMU 映射)。

EtherCAT 数据类型枚举 ec_datatype 与 CANopen 对象字典数据类型一致(BOOLEAN=0x0001、INTEGER8=0x0002……BIT1BIT8=0x00300x0037),用于描述对象字典中每个条目的数据类型。

3.8 SII 从站信息接口与邮箱协议

SII(Slave Information Interface)从站信息接口用于描述从站能力,其内容存放在从站 EEPROM 中:

#define ECT_SII_START   0x0040   // EEPROM中SII起始地址

enum
{
   ECT_SII_STRING      = 10,     // 字符串段
   ECT_SII_GENERAL     = 30,     // 通用段(厂商、产品、版本等)
   ECT_SII_FMMU        = 40,     // FMMU段
   ECT_SII_SM          = 41,     // 同步管理器段
   ECT_SII_PDO         = 50      // PDO段
};

通用段内的关键偏移包括厂商 ID(0x0008)、产品 ID(0x000a)、版本号(0x000c)、邮箱配置等。主站上电后就是通过读取这些 SII 段来识别从站的厂商、型号与通信能力,从而完成自动配置。

邮箱协议类型枚举定义了从站支持的应用层协议:

enum
{
   ECT_MBXT_ERR        = 0x00,   // 错误邮箱
   ECT_MBXT_AOE,                 // ADS over EtherCAT
   ECT_MBXT_EOE,                 // Ethernet over EtherCAT
   ECT_MBXT_COE,                 // CANopen over EtherCAT
   ECT_MBXT_FOE,                 // File over EtherCAT(固件升级)
   ECT_MBXT_SOE,                 // Servo over EtherCAT
   ECT_MBXT_VOE        = 0x0f    // Vendor over EtherCAT
};

其中 CoE(CANopen over EtherCAT)是伺服/驱动器最常用的协议,ec_type.h 中进一步定义了 CoE 邮箱服务类型(SDO 请求/响应、TxPDO/RxPDO 等)、SDO 命令字(下载/上传/中止)、对象字典描述命令,以及 FoE(固件升级)、SoE(伺服)的操作码。

3.9 EtherCAT 寄存器定义

从站 ESC(EtherCAT Slave Controller)内部寄存器地址也集中定义在此,例如:

enum
{
   ECT_REG_TYPE        = 0x0000,   // 从站类型
   ECT_REG_ALIAS       = 0x0012,   // 别名地址
   ECT_REG_DLCTL       = 0x0100,   // 数据链路控制
   ECT_REG_ALCTL       = 0x0120,   // AL控制
   ECT_REG_ALSTAT      = 0x0130,   // AL状态
   ECT_REG_ALSTATCODE  = 0x0134,   // AL状态码
   ECT_REG_EEPCFG      = 0x0500,   // EEPROM配置
   ECT_REG_EEPCTL      = 0x0502,   // EEPROM控制
   ECT_REG_EEPADR      = 0x0504,   // EEPROM地址
   ECT_REG_EEPDAT      = 0x0508,   // EEPROM数据
   ECT_REG_FMMU0       = 0x0600,   // FMMU0(+0x10递增到FMMU3)
   ECT_REG_SM0         = 0x0800,   // 同步管理器SM0(+0x08递增到SM3)
   ECT_REG_DCSYSTIME   = 0x0910,   // DC系统时间
   ECT_REG_DCSYSOFFSET = 0x0920,   // DC系统时间偏移
   ECT_REG_DCCYCLE0    = 0x09A0    // DC周期
};

主站读写从站就是通过 FPWR/FPRD 等命令访问这些寄存器,例如状态机切换写 ALCTL、读取 ALSTAT 确认状态迁移,DC 同步则读写 0x0900~0x09A0 段的分布式时钟寄存器。

3.10 错误信息与常用操作宏

错误信息结构体 ec_errort 统一封装了运行期错误(SDO 错误、紧急报文、邮箱错误等),包含时间、从站号、对象索引、错误类型与错误码(AbortCode 或 Emergency 错误码)。

常用操作宏:

#define MK_WORD(msb, lsb)   ((((uint16)(msb))<<8) | (lsb))  // 两字节组装word
#define HI_BYTE(w)          ((w) >> 8)    // 取高字节
#define LO_BYTE(w)          ((w) & 0x00ff)  // 取低字节
#define SWAP(w)             ((((w)& 0xff00) >> 8) | (((w) & 0x00ff) << 8))  // 高低字节交换
#define LO_WORD(l)          ((l) & 0xffff)  // 取低字
#define HI_WORD(l)          ((l) >> 16)     // 取高字

内存对齐访问宏(解决非对齐地址读写问题):

#define get_unaligned(ptr)   ({ __typeof__(*(ptr)) __tmp; memcpy(&__tmp, (ptr), sizeof(*(ptr))); __tmp; })
#define put_unaligned32(val, ptr)  (memcpy((ptr), &(val), 4))
#define put_unaligned64(val, ptr)  (memcpy((ptr), &(val), 8))

大小端转换宏根据平台字节序自动选择直通或交换实现:

#if !defined(EC_BIG_ENDIAN) && defined(EC_LITTLE_ENDIAN)
  #define htoes(A) (A)   // 小端平台:主机序 == 网络序,直通
  #define htoel(A) (A)
  #define htoell(A) (A)
  #define etohs(A) (A)
  #define etohl(A) (A)
  #define etohll(A) (A)
#elif !defined(EC_LITTLE_ENDIAN) && defined(EC_BIG_ENDIAN)
  #define htoes(A) ((((uint16)(A) & 0xff00) >> 8) | (((uint16)(A) & 0x00ff) << 8))
  #define htoel(A) ((((uint32)(A) & 0xff000000) >> 24) | ... )  // 32位逐字节反转
  #define htoell(A) ( ... )  // 64位逐字节反转
  #define etohs  htoes
  #define etohl  htoel
  #define etohll htoell
#else
  #error "Must define one of EC_BIG_ENDIAN or EC_LITTLE_ENDIAN"
#endif

hto*(host to EtherCAT)与 eto*(EtherCAT to host)成对出现,分别用于发送前的字节序转换与接收后的还原,保证 EtherCAT 报文在网络侧始终是小端字节序。

四、实施过程:如何基于 ec_type.h 构建主站

在实际项目中,ec_type.h 通常作为第一个被包含的头文件:

  1. 工程中引入 SOEM 源码,确认 EC_LITTLE_ENDIANEC_VER1 等宏按需开启;
  2. 应用代码 #include "soem/ec_type.h" 后即可使用 uint16、ec_timet、ec_err 等统一类型;
  3. 网卡驱动层基于 ec_bufT 申请收发缓冲,按 EC_MAXECATFRAME 对齐帧长;
  4. 协议层按 ec_etherheadert + ec_comt 拼装报文,用 ec_cmdtype 选择寻址方式;
  5. 业务层通过 EC_TIMEOUT* 系列宏控制轮询节奏,保证实时性;
  6. 解析从站时,读取 EEPROM 的 SII 段(ECT_SII_)识别从站,读写 ECT_REG_ 寄存器完成状态机切换与 DC 同步。

五、应用价值

理解 ec_type.h 是掌握 SOEM 主站的第一步,也是排查 EtherCAT 通讯问题的钥匙:

六、SEO关键词

SOEM、EtherCAT主站、ec_type.h、EtherCAT源代码解析、EtherCAT数据类型、工业以太网、运动控制、开源EtherCAT主站、EtherCAT寄存器、SII从站信息接口、EtherCAT报文、LinuxCNC EtherCAT

评论(0