SOEM-EtherCAT 源代码解析(一):数据类型定义
逐段拆解开源 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 定义了整个库通用的基础内容,按功能可划分为七大类:
- 编译环境配置(字节序、版本开关、跨平台宏)
- 返回值、错误码与超时参数
- 帧长、缓冲区与报文结构体(以太网头、数据包头)
- 基础数据类型别名与时间结构体
- 各类枚举(状态、命令、数据类型、错误类型)
- SII 从站信息接口、邮箱协议与寄存器定义
- 常用操作宏(取字节、对齐、大小端转换)
三、系统架构:逐段解读核心定义
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
- 头文件保护:
#ifndef _EC_TYPE_H防止重复包含;#include <stdint.h>引入定长整型。 - extern "C":保证 C++ 工程链接时不会发生名字改编(name mangling)。
- EC_LITTLE_ENDIAN:声明目标平台为小端模式。EtherCAT 报文在网络上的字节序遵循小端规则,该宏为后续大小端转换宏提供编译期依据。
- EC_VER1:启用 V1 版本默认上下文与函数接口。SOEM 同时提供
ec_*(V1 风格,使用全局默认上下文)与ecx_*(可传入自定义上下文)两套接口,V1 接口在底层就是对默认上下文的封装,方便入门用户使用。
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_TIMEOUTRET | 2000 | 发送帧返回接收的轮询超时 |
| EC_TIMEOUTRET3 | 6000 | 安全传输超时,最多重试 3 次 |
| EC_TIMEOUTSAFE | 20000 | "safe"变体返回超时(如无线场景) |
| EC_TIMEOUTEEP | 20000 | 从站 EEPROM 访问超时 |
| EC_TIMEOUTTXM | 20000 | 发送邮箱轮询超时 |
| EC_TIMEOUTRXM | 700000 | 接收邮箱轮询超时(邮箱数据量较大) |
| EC_TIMEOUTSTATE | 2000000 | 状态机切换检测超时 |
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
- EC_MAXECATFRAME:标准以太网 MTU 1500 字节加上以太网头等开销,即 1518 字节,是单个 EtherCAT 帧的最大长度。
- EC_MAXLRWDATA:一次 LRW(逻辑读写信道)数据段的最大长度。计算式清晰地标明了开销构成:14 字节以太网头、2 字节长度域、10 字节报文头、2 字节 WKC、4 字节 FCS。
- EC_FIRSTDCDATAGRAM:首帧 LRW 中用于分布式时钟(DC)同步的数据报大小(20 字节),保证首帧可以携带 DC 同步报文。
- EC_ECATTYPE:SOEM 内部标识 EtherCAT 数据报的类型值(0x1000);真正出现在以太网帧中的 EtherType 是
ETH_P_ECAT(0x88A4),见后文。 - EC_MAXBUF:每个通道(tx、rx1、rx2)的帧缓冲数量,与底层网卡驱动配合实现乒乓收发。
报文结构体是理解 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_DATAGRAMFOLLOWS 是 dlength 的最高位标志,置位表示后面还有子报文。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 报文字段均为紧排,任何隐式填充都会导致解析错位,因此所有协议结构体都声明为 PACKED。OSAL_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 通常作为第一个被包含的头文件:
- 工程中引入 SOEM 源码,确认
EC_LITTLE_ENDIAN、EC_VER1等宏按需开启; - 应用代码
#include "soem/ec_type.h"后即可使用 uint16、ec_timet、ec_err 等统一类型; - 网卡驱动层基于 ec_bufT 申请收发缓冲,按 EC_MAXECATFRAME 对齐帧长;
- 协议层按 ec_etherheadert + ec_comt 拼装报文,用 ec_cmdtype 选择寻址方式;
- 业务层通过 EC_TIMEOUT* 系列宏控制轮询节奏,保证实时性;
- 解析从站时,读取 EEPROM 的 SII 段(ECT_SII_)识别从站,读写 ECT_REG_ 寄存器完成状态机切换与 DC 同步。
五、应用价值
理解 ec_type.h 是掌握 SOEM 主站的第一步,也是排查 EtherCAT 通讯问题的钥匙:
- 协议视角:帧长、WKC、子报文头、命令类型等定义直接对应 EtherCAT 数据链路层行为,读懂它们就能解释"为什么我的报文发不出去""为什么偶发超时";
- 工程视角:统一类型、PACKED 对齐与 OSAL 抽象保证了代码在 Windows/Linux/RTOS 间的可移植性,大幅降低跨平台运动控制项目移植成本;
- 调试视角:寄存器地址与错误码枚举是定位从站问题的速查表——状态机卡在 Safe-Op 看 ALSTAT/ALSTATCODE,EEPROM 读不到看 EEPCTL/EEPSTAT;
- 学习视角:SOEM 代码量小、层次清晰,是学习 EtherCAT 协议与实时以太网实现的最佳范本,本系列后续将依次解析配置信息列表、通讯运行环境等核心模块。
六、SEO关键词
SOEM、EtherCAT主站、ec_type.h、EtherCAT源代码解析、EtherCAT数据类型、工业以太网、运动控制、开源EtherCAT主站、EtherCAT寄存器、SII从站信息接口、EtherCAT报文、LinuxCNC EtherCAT
