EtherCAT 从站开发实战:从 XML 配置文件到伺服驱动器通信

摘要:EtherCAT 作为工业自动化领域的高性能实时以太网协议,其从站开发是国产高端数控系统、机器人控制器等设备的核心技术。本文通过一个完整的伺服驱动器从站开发案例,详细讲解 EtherCAT 从站 XML 配置文件的编写、PDO 映射、同步时钟配置,并提供基于 IgH EtherCAT Master 的 Linux 平台 C 代码实现,以及 Wireshark 抓包分析、同步时钟调试等实战技巧。

关键词:EtherCAT, 工业以太网, 伺服驱动, 实时通信, XML 配置, PDO 映射, 同步时钟, IgH Master


目录


EtherCAT 从站开发流程概览

一个完整的 EtherCAT 从站开发流程包括:

1. 硬件设计(FPGA/ASIC 实现 EtherCAT 从站控制器)
2. XML 配置文件编写(描述从站对象字典、PDO 映射等)
3. EtherCAT Master 集成(Linux 平台常用 IgH Master)
4. 通信测试与性能调优

本文聚焦于 2-4 步,假设硬件已具备 EtherCAT 从站接口(如 ET1100、LAN9252 等芯片)。


第一步:编写从站 XML 配置文件

EtherCAT 从站通过 XML 配置文件向主站描述其能力。以下是一个伺服驱动器的简化配置文件 servo_drive.xml

<?xml version="1.0" encoding="UTF-8"?>
<EtherCATConfig xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="EtherCATConfig.xsd">
  <Config>
    <Groups>
      <Group Name="ServoDrive">
        <Type>Servo</Type>
      </Group>
    </Groups>
    <Devices>
      <Device ProductCode="0x12345678" RevisionNo="0x00010000" 
               Type="ServoDrive" Name="SV660 Servo Drive">
        <!-- 厂商信息 -->
        <Vendor Id="0x00001234">Inovance</Vendor>
        <Description>SV660 Series Servo Drive</Description>
        
        <!-- 邮箱通信(CoE)配置 -->
        <Mailbox>
          <CoE SdoInfo="true" PdoAssign="true" PdoConfig="true" />
          <SupportedProtocols>
            <CoE />
          </SupportedProtocols>
        </Mailbox>
        
        <!-- 同步管理器配置 -->
        <Sm Num="0">
          <Enable>1</Enable>
          <StartAddress>0x1000</StartAddress>
          <DefaultSize>128</DefaultSize>
          <ControlByte>0x26</ControlByte>
          <Enable>1</Enable>
        </Sm>
        <Sm Num="1">
          <Enable>1</Enable>
          <StartAddress>0x1100</StartAddress>
          <DefaultSize>128</DefaultSize>
          <ControlByte>0x22</ControlByte>
          <Enable>1</Enable>
        </Sm>
        <Sm Num="2">
          <Enable>1</Enable>
          <StartAddress>0x1200</StartAddress>
          <DefaultSize>128</DefaultSize>
          <ControlByte>0x24</ControlByte>
          <Enable>1</Enable>
        </Sm>
        <Sm Num="3">
          <Enable>1</Enable>
          <StartAddress>0x1300</StartAddress>
          <DefaultSize>64</DefaultSize>
          <ControlByte>0x20</ControlByte>
          <Enable>1</Enable>
        </Sm>
        
        <!-- 对象字典 -->
        <Dictionary>
          <!-- 设备基本信息 -->
          <Object Index="0x1000" DataType="UINT32" BitSize="32" Access="ro">
            <Name>Device Type</Name>
            <DefaultValue>0x00020192</DefaultValue>
          </Object>
          
          <!-- 伺服控制字 (0x6040) -->
          <Object Index="0x6040" DataType="UINT16" BitSize="16" Access="rw">
            <Name>Control Word</Name>
            <DefaultValue>0x0000</DefaultValue>
            <Info>
              <Bit Offs="0">Switch On</Bit>
              <Bit Offs="1">Enable Voltage</Bit>
              <Bit Offs="2">Quick Stop</Bit>
              <Bit Offs="3">Enable Operation</Bit>
              <Bit Offs="4">Operation Mode Specific</Bit>
              <Bit Offs="5">Operation Mode Specific</Bit>
              <Bit Offs="6">Operation Mode Specific</Bit>
              <Bit Offs="7">Fault Reset</Bit>
              <Bit Offs="8">Halt</Bit>
              <Bit Offs="9">Operation Mode Specific</Bit>
              <Bit Offs="10">Reserved</Bit>
              <Bit Offs="11">Reserved</Bit>
              <Bit Offs="12">Reserved</Bit>
              <Bit Offs="13">Reserved</Bit>
              <Bit Offs="14">Reserved</Bit>
              <Bit Offs="15">Reserved</Bit>
            </Info>
          </Object>
          
          <!-- 伺服状态字 (0x6041) -->
          <Object Index="0x6041" DataType="UINT16" BitSize="16" Access="ro">
            <Name>Status Word</Name>
            <DefaultValue>0x0000</DefaultValue>
          </Object>
          
          <!-- 目标位置 (0x607A) -->
          <Object Index="0x607A" DataType="INT32" BitSize="32" Access="rw">
            <Name>Target Position</Name>
            <DefaultValue>0</DefaultValue>
            <Info>Unit: pulse</Info>
          </Object>
          
          <!-- 实际位置 (0x6064) -->
          <Object Index="0x6064" DataType="INT32" BitSize="32" Access="ro">
            <Name>Actual Position</Name>
            <DefaultValue>0</DefaultValue>
            <Info>Unit: pulse</Info>
          </Object>
          
          <!-- 模式选择 (0x6060) -->
          <Object Index="0x6060" DataType="INT8" BitSize="8" Access="rw">
            <Name>Modes of Operation</Name>
            <DefaultValue>8</DefaultValue> <!-- 8 = Cyclic Synchronous Position Mode -->
            <Info>
              <Enum>1: Profile Position Mode</Enum>
              <Enum>3: Profile Velocity Mode</Enum>
              <Enum>4: Torque Profile Mode</Enum>
              <Enum>6: Homing Mode</Enum>
              <Enum>8: Cyclic Synchronous Position Mode (CSP)</Enum>
              <Enum>9: Cyclic Synchronous Velocity Mode (CSV)</Enum>
              <Enum>10: Cyclic Synchronous Torque Mode (CST)</Enum>
            </Info>
          </Object>
        </Dictionary>
      </Device>
    </Devices>
  </Config>
</EtherCATConfig>

关键配置项说明

配置项 说明 典型值
ProductCode 产品代码,主站用于识别从站 0x12345678
RevisionNo 硬件版本号 0x00010000
Sm 同步管理器,SM0/1 用于邮箱,SM2/3 用于过程数据 SM0: 0x1000, SM2: 0x1200
ControlByte 同步管理器控制字节 0x26: 邮箱写,0x22: 邮箱读,0x24: PDO 输出,0x20: PDO 输入
Index 对象字典索引 0x6040: 控制字,0x6041: 状态字,0x607A: 目标位置

第二步:配置 PDO 映射与同步管理器

PDO(过程数据对象)用于周期性实时数据交换。在 XML 中配置 PDO 映射:

<!-- 在 Device 标签内添加 -->
<TxPdo Fixed="true" Sm="2" Index="0x1A00">
  <Name>Servo Inputs</Name>
  <Entry>
    <Index>0x6041</Index> <!-- 状态字 -->
    <SubIndex>0</SubIndex>
    <BitLen>16</BitLen>
  </Entry>
  <Entry>
    <Index>0x6064</Index> <!-- 实际位置 -->
    <SubIndex>0</SubIndex>
    <BitLen>32</BitLen>
  </Entry>
</TxPdo>

<RxPdo Fixed="true" Sm="3" Index="0x1600">
  <Name>Servo Outputs</Name>
  <Entry>
    <Index>0x6040</Index> <!-- 控制字 -->
    <SubIndex>0</SubIndex>
    <BitLen>16</BitLen>
  </Entry>
  <Entry>
    <Index>0x607A</Index> <!-- 目标位置 -->
    <SubIndex>0</SubIndex>
    <BitLen>32</BitLen>
  </Entry>
</RxPdo>

PDO 映射计算

  • TxPdo(输入,从站→主站):状态字(16bit) + 实际位置(32bit) = 48bit = 6 字节
  • RxPdo(输出,主站→从站):控制字(16bit) + 目标位置(32bit) = 48bit = 6 字节

总过程数据长度:12 字节,满足 EtherCAT 帧内最大 1486 字节的限制。


第三步:基于 IgH EtherCAT Master 的代码实现

IgH EtherCAT Master 是 Linux 平台最常用的开源主站实现。以下是一个简化的从站控制程序:

/**
 * servo_ecat.c
 * EtherCAT 伺服驱动器控制示例
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <ecrt.h>

// 从站配置
#define SLAVE_POS 0          // 从站在总线上的位置
#define VENDOR_ID 0x00001234
#define PRODUCT_CODE 0x12345678

// PDO 映射偏移量(需与 XML 配置一致)
#define OFFSET_CONTROL_WORD 0
#define OFFSET_TARGET_POS   2
#define OFFSET_STATUS_WORD  0
#define OFFSET_ACTUAL_POS   2

// 伺服状态机定义
typedef enum {
    STATE_INIT = 0,
    STATE_PRE_OP,
    STATE_SAFE_OP,
    STATE_OP
} servo_state_t;

// 全局变量
static ec_master_t *master = NULL;
static ec_master_state_t master_state;
static ec_domain_t *domain = NULL;
static ec_domain_state_t domain_state;
static ec_slave_config_t *sc = NULL;
static ec_slave_config_state_t sc_state;

// 过程数据缓冲区
static uint8_t *domain_pd = NULL;
static uint16_t *control_word = NULL;
static int32_t *target_position = NULL;
static uint16_t *status_word = NULL;
static int32_t *actual_position = NULL;

// EtherCAT 主站初始化
static int init_ethercat(void) {
    printf("Initializing EtherCAT master...\n");
    
    // 申请主站
    master = ecrt_request_master(0);
    if (!master) {
        fprintf(stderr, "Failed to request master\n");
        return -1;
    }
    
    // 创建域
    domain = ecrt_master_create_domain(master);
    if (!domain) {
        fprintf(stderr, "Failed to create domain\n");
        return -1;
    }
    
    // 配置从站
    sc = ecrt_master_slave_config(master, SLAVE_POS, 
                                   VENDOR_ID, PRODUCT_CODE);
    if (!sc) {
        fprintf(stderr, "Failed to configure slave\n");
        return -1;
    }
    
    // 配置 PDO 映射
    ec_pdo_entry_reg_t regs[] = {
        // RxPdo (输出)
        {SLAVE_POS, 0x6040, 0, &control_word},    // 控制字
        {SLAVE_POS, 0x607A, 0, &target_position}, // 目标位置
        // TxPdo (输入)
        {SLAVE_POS, 0x6041, 0, &status_word},     // 状态字
        {SLAVE_POS, 0x6064, 0, &actual_position}, // 实际位置
        {}
    };
    
    if (ecrt_domain_reg_pdo_entry_list(domain, regs) < 0) {
        fprintf(stderr, "PDO entry registration failed\n");
        return -1;
    }
    
    // 激活主站
    if (ecrt_master_activate(master)) {
        fprintf(stderr, "Failed to activate master\n");
        return -1;
    }
    
    // 获取域数据指针
    domain_pd = ecrt_domain_data(domain);
    
    // 设置指针偏移
    control_word = (uint16_t*)(domain_pd + OFFSET_CONTROL_WORD);
    target_position = (int32_t*)(domain_pd + OFFSET_TARGET_POS);
    status_word = (uint16_t*)(domain_pd + OFFSET_STATUS_WORD);
    actual_position = (int32_t*)(domain_pd + OFFSET_ACTUAL_POS);
    
    printf("EtherCAT master initialized successfully\n");
    return 0;
}

// 伺服状态机
static servo_state_t servo_state_machine(uint16_t status) {
    uint8_t state = (status >> 4) & 0x0F;
    
    switch (state) {
        case 0:  return STATE_INIT;
        case 1:  return STATE_PRE_OP;
        case 2:  return STATE_SAFE_OP;
        case 3:  return STATE_OP;
        default: return STATE_INIT;
    }
}

// 伺服使能序列
static int enable_servo(void) {
    printf("Starting servo enable sequence...\n");
    
    // 1. 等待状态字显示"Ready to switch on"
    int timeout = 100;
    while ((*status_word & 0x004F) != 0x0040 && timeout-- > 0) {
        ecrt_master_application_time(master, 1000000); // 1ms
        ecrt_domain_queue(domain);
        ecrt_master_send(master);
        ecrt_domain_receive(domain);
        usleep(1000);
    }
    if (timeout <= 0) {
        fprintf(stderr, "Timeout waiting for 'Ready to switch on'\n");
        return -1;
    }
    
    // 2. 发送"Switch on"命令
    *control_word = 0x0006; // Switch on + Enable voltage
    ecrt_domain_queue(domain);
    ecrt_master_send(master);
    usleep(1000);
    
    // 3. 等待"Switched on"
    timeout = 100;
    while ((*status_word & 0x004F) != 0x0021 && timeout-- > 0) {
        ecrt_domain_receive(domain);
        usleep(1000);
    }
    
    // 4. 发送"Enable operation"
    *control_word = 0x000F;
    ecrt_domain_queue(domain);
    ecrt_master_send(master);
    usleep(1000);
    
    // 5. 等待"Operation enabled"
    timeout = 100;
    while ((*status_word & 0x004F) != 0x0027 && timeout-- > 0) {
        ecrt_domain_receive(domain);
        usleep(1000);
    }
    
    printf("Servo enabled successfully\n");
    return 0;
}

// 位置模式运动
static int move_to_position(int32_t target) {
    printf("Moving to position: %d pulses\n", target);
    
    // 检查是否在 OP 状态
    if (servo_state_machine(*status_word) != STATE_OP) {
        fprintf(stderr, "Servo not in OP state\n");
        return -1;
    }
    
    // 设置目标位置
    *target_position = target;
    
    // 发送数据
    ecrt_domain_queue(domain);
    ecrt_master_send(master);
    
    // 等待到达目标位置
    int timeout = 5000; // 5秒超时
    while (abs(*actual_position - target) > 10 && timeout-- > 0) {
        ecrt_domain_receive(domain);
        usleep(1000);
    }
    
    if (timeout <= 0) {
        fprintf(stderr, "Position move timeout\n");
        return -1;
    }
    
    printf("Position reached: %d pulses\n", *actual_position);
    return 0;
}

// 主循环
static void run_control_loop(void) {
    printf("Starting control loop...\n");
    
    // 使能伺服
    if (enable_servo() < 0) {
        return;
    }
    
    // 简单的位置往返运动
    for (int i = 0; i < 5; i++) {
        // 正向运动
        if (move_to_position(10000) < 0) break;
        usleep(500000); // 暂停500ms
        
        // 反向运动
        if (move_to_position(0) < 0) break;
        usleep(500000);
    }
    
    // 停止运动
    *control_word = 0x0000;
    ecrt_domain_queue(domain);
    ecrt_master_send(master);
}

// 信号处理
static volatile int running = 1;
static void signal_handler(int sig) {
    running = 0;
}

// 主函数
int main(int argc, char **argv) {
    // 注册信号处理
    signal(SIGINT, signal_handler);
    signal(SIGTERM, signal_handler);
    
    // 初始化 EtherCAT
    if (init_ethercat() < 0) {
        return 1;
    }
    
    // 运行控制循环
    run_control_loop();
    
    // 清理
    printf("Shutting down...\n");
    if (master) {
        ecrt_release_master(master);
    }
    
    return 0;
}

编译与运行

# 安装 IgH EtherCAT Master
sudo apt-get install ethercat

# 编译
gcc -o servo_ecat servo_ecat.c -lethercat -lpthread

# 运行(需要 root 权限)
sudo ./servo_ecat

第四步:Wireshark 抓包分析与协议验证

4.1 配置 Wireshark 抓取 EtherCAT 帧

# 安装 Wireshark
sudo apt-get install wireshark

# 设置网卡为混杂模式
sudo ip link set eth0 promisc on

# 使用 tcpdump 抓包
sudo tcpdump -i eth0 -w ethercat.pcap

4.2 关键帧分析

帧类型 特征 用途
APRD EtherType 0x88A4,OpCode 0x01 读取从站寄存器
APWR EtherType 0x88A4,OpCode 0x02 写入从站寄存器
FPWR EtherType 0x88A4,OpCode 0x03 快速写入(广播)
LRD EtherType 0x88A4,OpCode 0x04 逻辑读取
LWR EtherType 0x88A4,OpCode 0x05 逻辑写入
LRW EtherType 0x88A4,OpCode 0x06 逻辑读写

4.3 常见问题排查

在 Wireshark 过滤器中输入 ecat 查看 EtherCAT 帧,重点关注:

  1. 初始化阶段:检查 INIT → PRE-OP → SAFE-OP → OP 状态转换
  2. PDO 映射:查看 SDO Upload/Download 是否正确配置 PDO
  3. 同步时钟:检查 SYNC0/SYNC1 周期是否一致
  4. 数据一致性:检查输入/输出 PDO 数据是否按预期更新

第五步:同步时钟调试与性能优化

5.1 分布式时钟(DC)配置

// 在 init_ethercat 函数中添加
ec_slave_config_dc(sc, 0x0300, 1000000, 500000, 0, 0);

参数说明:

  • 0x0300:同步模式(0x0300 = SYNC0 激活)
  • 1000000:周期时间(1ms,单位 ns)
  • 500000:同步0偏移(500μs)
  • 0:同步1周期(0 = 禁用)
  • 0:同步1偏移

5.2 抖动测量与优化

# 使用 IgH 工具测量抖动
sudo ethercat -d eth0 dc

# 输出示例:
# Slave    Position  DC SysTime   Diff   MaxDiff   Jitter
#     0          0   yes  1000000     0        5       2

抖动优化建议

抖动源 优化措施
网络中断 设置 CPU 亲和性,绑定到独立核心
系统负载 使用 isolcpus 内核参数隔离核心
内存访问 使用 mlockall() 锁定内存,避免换页
电源管理 禁用 CPU 频率调节(performance 模式)

5.3 实时性配置

# 安装实时内核
sudo apt-get install linux-rt

# 配置实时优先级
sudo sysctl -w kernel.sched_rt_runtime_us=950000
sudo sysctl -w kernel.sched_rt_period_us=1000000

# 设置进程优先级
sudo chrt -f -p 99 $(pidof servo_ecat)

常见故障排查表

故障现象 可能原因 排查步骤
从站无法识别 1. 物理连接问题
2. XML 配置错误
3. 产品代码不匹配
1. 检查网线、指示灯
2. 验证 XML 语法
3. 使用 ethercat slaves 查看识别结果
PDO 映射失败 1. 同步管理器配置错误
2. 对象字典索引错误
3. 位长度不匹配
1. 检查 SM 控制字节
2. 验证对象字典索引
3. 使用 Wireshark 抓包分析 SDO 通信
同步时钟不同步 1. DC 配置错误
2. 网络拓扑过长
3. 从站不支持 DC
1. 检查 ec_slave_config_dc 参数
2. 测量网络延迟
3. 检查从站手册是否支持 DC
通信周期抖动大 1. 系统负载过高
2. 中断冲突
3. 内存访问延迟
1. 使用 top 查看系统负载
2. 检查 /proc/interrupts
3. 使用 cyclictest 测量延迟
伺服无法使能 1. 状态机序列错误
2. 控制字位设置错误
3. 驱动器故障
1. 逐步打印状态字
2. 对照 DS402 状态机图
3. 检查驱动器报警代码

完整代码仓库

本文所有代码、配置文件、测试脚本已整理至 GitHub:

https://github.com/xunfeng-lab/ethercat-servo-driver

仓库包含:

  • config/:XML 配置文件、设备描述文件
  • src/:IgH Master 示例代码、测试程序
  • tools/:Wireshark 过滤脚本、性能测试工具
  • docs/:DS402 状态机图、接线图、调试指南

本文由「洵锋」原创,10 年工业自动化全栈工程师。EtherCAT 开发涉及硬件、固件、驱动、应用多个层面,建议从简单从站开始逐步深入。评论区欢迎交流实际开发中的问题。

Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐