EtherCAT 基础编程指南 (IgH Master)

基于 Linux IgH EtherCAT Master 协议栈,面向初学者与现场调试人员,兼顾“教学 + 查阅”双需求。阅读顺序建议:先跑通第 0 章自检,再按需跳转到各功能章节。


快速导航


0. 环境准备与验证

0.1 前置条件清单

  • 内核/补丁:优先使用 PREEMPT_RT 内核或 IgH 官方实时补丁;uname -a 确认版本。
  • IgH Master:与驱动对应版本(示例使用 1.6.x);确认已安装 ethercat 工具链。
  • 权限配置groupadd ethercat 并将用户加入;/dev/EtherCAT0 需 0660 权限。
  • 网卡:独立物理口,关闭 NetworkManager,锁定全双工 100Mbps。
  • 设备:准备好 ESI XML 文件并确认 Vendor ID / Product Code。

0.2 最小自检步骤

目标 命令 预期输出 失败排查
驱动加载 sudo /etc/init.d/ethercat start Starting EtherCAT master 1.6... done 查看 `dmesg
主站状态 ethercat master Master0 State: OP 若为 INIT 检查网卡绑定
从站可见性 ethercat slaves 列出所有设备及 ID 若为空,检查布线/拓扑
PDO 结构 ethercat pdosethercat cstruct 对应条目及位宽 若报错,核对 XML 与固件
SDO 通信 ethercat upload --type uint16 0 0x1000 0x00 返回 Vendor ID 若超时,确认从站上电

提示:运行示例程序前,确保上述命令在 root 或加入 ethercat 组的用户下都可执行;否则会在 ecrt_request_master 处失败。


1. 核心概念速览

术语 全称 解释 类比
Master EtherCAT Master 运行在 PC 上的主站程序,负责管理总线。 乐队指挥
Slave EtherCAT Slave 连接在网线上的设备(驱动器、IO 模块)。 乐手
Domain Process Data Domain 主站管理的共享内存,用于存放所有从站的实时数据 (PDO)。 乐谱架
PDO Process Data Object 周期性实时数据,如位置、速度。快但不重传。 演奏中的音符
SDO Service Data Object 非周期配置数据,如参数、报警。慢但可靠。 演出前调音
DC Distributed Clocks 纳秒级同步机制,确保多从站同拍执行。 节拍器

2. 程序整体架构

1
2
3
4
5
6
7
8
9
10
11
12
项目
├─include
│ └─ ecrt.h
│ └─ ectty.h
└─lib
│ └─ libethercat.so

└─402.h
└─ec_slave_config.h
└─main.c

└─CMakeLists.txt或者Makefile

一个标准的 EtherCAT 控制程序由 初始化阶段实时周期循环 两部分组成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
graph TD
A[程序启动] --> B{初始化阶段};
B --> C[1. 请求主站实例];
B --> D[2. 扫描/配置从站];
B --> E[3. 配置PDO映射];
B --> F[4. 注册域内存];
B --> G[5. 激活主站];
G --> H{实时周期循环};
H --> I[Wait & AppTime];
I --> J[Receive];
J --> K[Process Domain];
K --> L[Control Logic];
L --> M[Sync];
M --> N[Queue & Send];
N --> H;

3. 常用 API 速查表

函数 用途 关键参数 常见失误
ecrt_request_master 获取主站实例 master_index 对应 /dev/EtherCATX 无权限、服务未启动
ecrt_master_create_domain 创建 PDO Domain master 多域场景未区分输入输出
ecrt_master_slave_config 绑定从站配置 alias, position, vendor_id, product_code ID 与现场不符,导致 PREOP 卡死
ecrt_slave_config_pdos 设置 PDO 映射 n_syncs, syncs 位宽求和错误、漏写 EC_END
ecrt_domain_reg_pdo_entry_list 将 PDO 条目映射到域内存 domain, reg_list 忘记以 {} 结尾、offset 指针指向局部变量
ecrt_slave_config_dc 配置分布时钟 assign_activate, cycle_time 从站不支持 DC 却强行配置
ecrt_master_activate 应用配置 master 激活后仍调用 _config 接口
ecrt_domain_data 获取域内存入口 domain 忘记检查返回值导致段错误
ecrt_master_receive/send 硬件收发帧 master 调用顺序错误,引入一拍延迟
ecrt_domain_process/queue 解析/排队域数据 domain 未调用 process 就取数,读取到旧帧

4. 初始化流程详解 (API 深度解析)

初始化的目标是回答三件事:谁在线?交换什么数据?如何同步? 以下步骤沿用 IgH 官方推荐顺序。

4.1 请求主站与创建域

核心函数 1.1:ecrt_request_master

  • 功能:请求 EtherCAT 主站实例的控制权。
  • 原型ec_master_t *ecrt_request_master(unsigned int master_index);
  • 参数
    • master_index: 主站索引,通常为 0(对应 /dev/EtherCAT0)。
  • 返回值:成功返回主站指针,失败返回 NULL
  • 说明:这是程序的入口,必须首先调用。

核心函数 1.2:ecrt_master_create_domain

  • 功能:在主站中创建一个新的过程数据域 (Domain)。
  • 原型ec_domain_t *ecrt_master_create_domain(ec_master_t *master);
  • 参数
    • master: 步骤 1.1 获取的主站指针。
  • 返回值:成功返回域指针,失败返回 NULL
  • 说明:Domain 就像一个“购物车”,你需要把所有从站的 PDO 数据(商品)都注册到这个域里,最后一次性结账(交换数据)。
  • ecrt_request_master:申请 /dev/EtherCAT0 控制权。确保以 root/实时用户运行。
  • ecrt_master_create_domain:创建一个 Domain。一个 Domain 等价于一块共享内存,可承载多个从站 PDO。
1
2
3
4
5
6
7
8
9
10
11
ec_master_t *master = ecrt_request_master(0);
if (!master) {
perror("request_master");
return -1;
}

ec_domain_t *domain = ecrt_master_create_domain(master);
if (!domain) {
fprintf(stderr, "create_domain failed\n");
return -1;
}

4.2 获取从站配置 (Slave Config)

核心函数:ecrt_master_slave_config

  • 功能:获取或创建指定从站的配置对象。
  • 原型
    1
    2
    3
    4
    5
    6
    7
    ec_slave_config_t *ecrt_master_slave_config(
    ec_master_t *master,
    uint16_t alias,
    uint16_t position,
    uint32_t vendor_id,
    uint32_t product_code
    );
  • 参数
    • alias: 从站别名。如果未设置别名,填 0
    • position: 拓扑位置。总线上第 1 个设备为 0,第 2 个为 1,依此类推。
    • vendor_id: 厂商 ID (如汇川、松下、倍福的唯一 ID)。
    • product_code: 产品编号。
  • 返回值:成功返回配置对象指针,失败返回 NULL
  • 说明:此函数不仅是获取句柄,更是一个“断言”。如果总线上实际设备的厂商/产品码与此处不符,主站会报错并拒绝工作。

使用 ecrt_master_slave_config 为每个拓扑位置断言正确的 Vendor/Product。若别名未设置,可取 0,仅依赖位置。

1
2
3
4
5
6
#define INOVANCE_VENDOR_ID  0x00100000
#define IS620N_PRODUCT_CODE 0x000C010D

ec_slave_config_t *sc_axis0 = ecrt_master_slave_config(master, 0, 0,
INOVANCE_VENDOR_ID, IS620N_PRODUCT_CODE);
if (!sc_axis0) return -1;

4.3 配置 PDO 映射

核心函数:ecrt_slave_config_pdos

  • 功能:为从站指定完整的 PDO 映射关系(Sync Manager -> PDO -> Entries)。
  • 原型
    1
    2
    3
    4
    5
    int ecrt_slave_config_pdos(
    ec_slave_config_t *sc,
    unsigned int n_syncs,
    const ec_sync_info_t *syncs
    );
  • 参数
    • sc: 步骤 2 获取的从站配置指针。
    • n_syncs: 同步管理器数量,通常使用宏 EC_END 让函数自动计算。
    • syncs: 指向 ec_sync_info_t 数组的指针,描述了所有的 SM 和 PDO 结构。
  • 返回值0 表示成功,非 0 表示失败。
  • 说明:这是最繁琐的一步。你需要查阅驱动器手册,构建 ec_pdo_entry_info_t -> ec_pdo_info_t -> ec_sync_info_t 的嵌套结构体。

ecrt_slave_config_pdos 需要按照 Entry -> PDO -> SyncManager 的嵌套结构依次声明。位宽必须与手册一致,位宽求和需满足 8*n

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static ec_pdo_entry_info_t axis0_pdo_entries[] = {
{0x6040, 0x00, 16}, // Control Word
{0x607A, 0x00, 32}, // Target Position
{0x6041, 0x00, 16}, // Status Word
{0x6064, 0x00, 32}, // Actual Position
};

static ec_pdo_info_t axis0_pdos[] = {
{0x1600, 2, axis0_pdo_entries + 0},
{0x1A00, 2, axis0_pdo_entries + 2},
};

static ec_sync_info_t axis0_syncs[] = {
{0, EC_DIR_OUTPUT, 0, NULL, EC_WD_DISABLE},
{1, EC_DIR_INPUT, 0, NULL, EC_WD_DISABLE},
{2, EC_DIR_OUTPUT, 1, axis0_pdos + 0, EC_WD_ENABLE},
{3, EC_DIR_INPUT, 1, axis0_pdos + 1, EC_WD_ENABLE},
{0xFF}
};

if (ecrt_slave_config_pdos(sc_axis0, EC_END, axis0_syncs)) {
fprintf(stderr, "pdo config failed\n");
return -1;
}

4.4 注册 PDO 条目

核心函数:ecrt_domain_reg_pdo_entry_list

  • 功能:批量注册 PDO 条目,获取其在共享内存中的偏移量。
  • 原型
    1
    2
    3
    4
    int ecrt_domain_reg_pdo_entry_list(
    ec_domain_t *domain,
    const ec_pdo_entry_reg_t *pdo_entry_regs
    );
  • 参数
    • domain: 步骤 1.2 创建的域指针。
    • pdo_entry_regs: 一个以空结构体 {} 结尾的数组,包含所有要读写的变量信息。
  • 返回值0 表示成功,非 0 表示失败。
  • 说明
    • 这是将“逻辑变量”映射到“物理内存”的关键步骤。
    • 数组中的 offset 字段是一个指针,函数执行后,会将计算出的偏移量填入该指针指向的变量中。

ecrt_domain_reg_pdo_entry_list 将逻辑变量与域内存偏移绑定。offset 指针需为全局/静态变量,否则函数结束就失效。

1
2
3
4
5
6
7
8
9
10
11
12
13
static unsigned int off_ctrl_word;
static unsigned int off_target_pos;

const ec_pdo_entry_reg_t regs_axis0[] = {
{0, 0, INOVANCE_VENDOR_ID, IS620N_PRODUCT_CODE, 0x6040, 0x00, &off_ctrl_word},
{0, 0, INOVANCE_VENDOR_ID, IS620N_PRODUCT_CODE, 0x607A, 0x00, &off_target_pos},
{}
};

if (ecrt_domain_reg_pdo_entry_list(domain, regs_axis0)) {
fprintf(stderr, "domain reg failed\n");
return -1;
}

4.5 配置分布时钟 (DC,同步场景)

核心函数:ecrt_slave_config_dc

  • 功能:配置从站的分布时钟参数。
  • 原型
    1
    2
    3
    4
    5
    6
    7
    8
    void ecrt_slave_config_dc(
    ec_slave_config_t *sc,
    uint16_t assign_activate,
    uint32_t sync0_cycle_time,
    int32_t sync0_shift_time,
    uint32_t sync1_cycle_time,
    int32_t sync1_shift_time
    );
  • 参数
    • assign_activate: 激活字。通常 0x0300 (激活 Sync0) 或 0x0700 (激活 Sync0 & Sync1)。需查阅从站手册 (ESI XML)。
    • sync0_cycle_time: Sync0 信号周期(纳秒)。通常等于主站通信周期。
    • sync0_shift_time: Sync0 偏移量。用于微调触发时间,避免与数据传输冲突。
  • 说明:只有需要高精度同步(如多轴插补)时才需要此步骤。

ecrt_slave_config_dc(sc, 0x0300, cycle_ns, shift, 0, 0);

  • 确认从站 AssignActivate 支持 Sync0,否则关闭 DC。
  • cycle_ns 建议等于周期任务周期;shift 用于避开帧发送窗口。

4.6 激活主站并获取域内存

核心函数 6.1:ecrt_master_activate

  • 功能:结束配置阶段,应用所有设置,启动总线通信。
  • 原型int ecrt_master_activate(ec_master_t *master);
  • 返回值0 表示成功,非 0 表示失败。
  • 说明:一旦激活,就不能再调用 _config 类的函数。此时主站会开始在后台发送数据帧。

核心函数 6.2:ecrt_domain_data

  • 功能:获取过程数据域的共享内存首地址。
  • 原型uint8_t *ecrt_domain_data(ec_domain_t *domain);
  • 返回值:指向共享内存的指针。
  • 说明
    • 拿到这个指针后,结合步骤 4 获取的 offset,就可以读写数据了。
    • 例如:target_pos = domain_pd + offset_target_pos;
1
2
3
4
5
6
7
if (ecrt_master_activate(master)) {
fprintf(stderr, "activate failed\n");
return -1;
}

uint8_t *domain_pd = ecrt_domain_data(domain);
if (!domain_pd) return -1;

4.7 多从站提示

  • 多驱动共享一个 Domain:为每个从站生成独立的 offset 变量,建议使用结构体封装(见第 8 章)。
  • 多 Domain:若需要将高速输出与慢速输入隔离,可创建多个 Domain 分别绑定不同任务线程。
  • 检查命令ethercat pdos -p <pos> 验证映射是否与代码一致;ethercat diag 可查看 SM 状态。

5. 规范化初始化函数封装

下面示例展示一个可复用的 init_ethercat,包含日志输出,方便在嵌入式系统上排查问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <ecrt.h>
#include <stdio.h>

static ec_master_t *master = NULL;
static ec_domain_t *domain = NULL;
static uint8_t *domain_pd = NULL;
static ec_slave_config_t *sc_axis = NULL;

static unsigned int off_ctrl_word;
static unsigned int off_target_pos;
static unsigned int off_status_word;
static unsigned int off_actual_pos;

#define VENDOR_ID 0xE0000000
#define PRODUCT_CODE 0x00000001

/* PDO 定义同 4.3 略 */

const ec_pdo_entry_reg_t domain_regs[] = {
{0, 0, VENDOR_ID, PRODUCT_CODE, 0x6040, 0x00, &off_ctrl_word},
{0, 0, VENDOR_ID, PRODUCT_CODE, 0x607A, 0x00, &off_target_pos},
{0, 0, VENDOR_ID, PRODUCT_CODE, 0x6041, 0x00, &off_status_word},
{0, 0, VENDOR_ID, PRODUCT_CODE, 0x6064, 0x00, &off_actual_pos},
{}
};

int init_ethercat(void)
{
puts("[Init] Requesting master...");
master = ecrt_request_master(0);
if (!master) return -1;

puts("[Init] Creating domain...");
domain = ecrt_master_create_domain(master);
if (!domain) return -1;

puts("[Init] Configuring slave (0,0)...");
sc_axis = ecrt_master_slave_config(master, 0, 0, VENDOR_ID, PRODUCT_CODE);
if (!sc_axis) return -1;

puts("[Init] Configuring PDOs...");
if (ecrt_slave_config_pdos(sc_axis, EC_END, slave_syncs)) return -1;

puts("[Init] Registering PDO entries...");
if (ecrt_domain_reg_pdo_entry_list(domain, domain_regs)) return -1;

puts("[Init] Configuring DC (1ms)...");
ecrt_slave_config_dc(sc_axis, 0x0300, 1000000, 0, 0, 0);

puts("[Init] Activating master...");
if (ecrt_master_activate(master)) return -1;

domain_pd = ecrt_domain_data(domain);
if (!domain_pd) return -1;

return 0;
}

6. 周期任务与实时线程

6.1 实时线程骨架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <pthread.h>
#include <sys/mman.h>

void *rt_thread(void *arg)
{
mlockall(MCL_CURRENT | MCL_FUTURE); // 锁页防止缺页

struct sched_param sp = {.sched_priority = 80};
pthread_setschedparam(pthread_self(), SCHED_FIFO, &sp);
cpu_set_t affinity;
CPU_ZERO(&affinity);
CPU_SET(2, &affinity); // 绑核,避免与普通任务抢占
pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &affinity);

cyclic_task();
return NULL;
}

要点

  1. mlockall 避免缺页造成长抖动。
  2. 使用 SCHED_FIFO + 绑核,配合 PREEMPT_RT 内核。
  3. 记录 clock_gettime 结果,结合 TIMESPEC2NS 宏传给 ecrt_master_application_time

6.2 周期循环示例

大致顺序

  1. Wait: 等待周期时间到。
  2. App Time: 告知主站当前时间 (DC同步关键)。
  3. Receive: 接收数据。
  4. Process: 处理数据域。
  5. Logic: 读取输入 -> 运行算法 -> 写入输出。
  6. Sync: 计算并补偿时钟漂移。
  7. Queue & Send: 加入队列并发送。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#define EC_READ_U16(DATA)  ((uint16_t)(*((uint16_t *)(DATA))))
#define EC_READ_S32(DATA) ((int32_t)(*((int32_t *)(DATA))))
#define EC_WRITE_U16(DATA, VAL) (*((uint16_t *)(DATA)) = (uint16_t)(VAL))
#define EC_WRITE_S32(DATA, VAL) (*((int32_t *)(DATA)) = (int32_t)(VAL))

void cyclic_task(void)
{
struct timespec wakeup_time;
clock_gettime(CLOCK_MONOTONIC, &wakeup_time);

while (1) {
wakeup_time.tv_nsec += 1000000; // 1 ms 周期
while (wakeup_time.tv_nsec >= 1000000000) {
wakeup_time.tv_nsec -= 1000000000;
wakeup_time.tv_sec++;
}
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &wakeup_time, NULL);

ecrt_master_application_time(master, TIMESPEC2NS(wakeup_time));
ecrt_master_receive(master);
ecrt_domain_process(domain);

uint16_t status = EC_READ_U16(domain_pd + off_status_word);
int32_t act_pos = EC_READ_S32(domain_pd + off_actual_pos);

uint16_t ctrl = 0x0006; // 默认 Shutdown
int32_t tgt_pos = act_pos;
if ((status & 0x0250) == 0x0250) {
ctrl = 0x000F; // Operation Enabled
tgt_pos += 100; // 示范位置增量
}

EC_WRITE_U16(domain_pd + off_ctrl_word, ctrl);
EC_WRITE_S32(domain_pd + off_target_pos, tgt_pos);

struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
ecrt_master_sync_reference_clock_to(master, TIMESPEC2NS(now));
ecrt_master_sync_slave_clocks(master);

ecrt_domain_queue(domain);
ecrt_master_send(master);
}
}

7. CiA-402 状态机速览

状态 StatusWord 条件 下一步控制字 备注
Switch On Disabled status & 0x004F == 0x0040 0x0006 (Shutdown) 上电初始态
Ready to Switch On status & 0x006F == 0x0021 0x0007 (Switch On) 需满足伺服上电条件
Switched On status & 0x006F == 0x0023 0x000F (Enable Op) 可进使能
Operation Enabled status & 0x006F == 0x0027 0x000F 可发送运动目标
Fault status & 0x0008 0x0080 (Fault Reset) 需先复位再重走流程

最小状态机实现示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static uint16_t cia402_control_word(uint16_t status)
{
if (status & 0x0008) {
return 0x0080; // Fault Reset
} else if ((status & 0x004F) == 0x0040) {
return 0x0006; // Shutdown
} else if ((status & 0x006F) == 0x0021) {
return 0x0007; // Switch On
} else if ((status & 0x006F) == 0x0023) {
return 0x000F; // Enable Operation
} else if ((status & 0x006F) == 0x0027) {
return 0x000F; // Stay Enabled
}
return 0x0000; // Default: disable
}

在周期任务中可将上述函数与状态字结合,确保任何异常都能自动复位并重走 CiA-402 流程。


8. 多从站与 PDO 组织建议

  1. 结构体封装 offsets

    1
    2
    3
    4
    5
    6
    7
    typedef struct {
    unsigned int ctrl;
    unsigned int tgt;
    unsigned int status;
    unsigned int act;
    } axis_offsets_t;
    static axis_offsets_t axis[2];

    结合 ec_pdo_entry_reg_tuser1 字段或手动拆分,避免 offset 命名失控。

  2. 模板化 PDO 表:对相同型号驱动,可将 ec_pdo_entry_info_tec_pdo_info_t 定义在单独 .c 文件,供多实例复用。

  3. 不同厂商混合:每类从站单独一组 regs[],注册到同一 Domain。通过 alias/position 区分,可读性更佳。

  4. 位宽校验脚本:用 Python 读取 ESI XML,计算每个 PDO 位宽总和,与代码常量比对,避免现场才发现长度不符。

  5. 命名建议off_<slave>_<signal>,配合注释标注单位与类型,方便交叉检查。


9. 常见问题排查 (Troubleshooting)

症状 可能原因 诊断命令 处理建议
Failed to request master 未加入 ethercat 组或服务未启动 ethercat master sudo /etc/init.d/ethercat restart,检查权限
PDO registration failed Vendor/Product 不符或 regs[] 未以 {} 结尾 ethercat slaves 更新宏常量;确认 regs 终止项
从站卡在 PREOP/SAFEOP PDO 映射和 XML 不一致 ethercat pdos -p N 根据 ESI 重新生成 syncs,或临时禁用自定义映射
进入 OP 但数据全 0 忘记 ecrt_domain_process 或 offset 无效 ethercat diag -p N 检查调用顺序;确保 offset 变量为静态
DC 同步失败、报 clock error 从站不支持 DC 或周期时间不匹配 ethercat diag --dc 关闭 DC (assign_activate=0x0000) 或校准 cycle_ns
电机抖动/漂移 周期抖动大、线程被抢占 cyclictest, htop 使用 PREEMPT_RT、绑核、提高优先级、减少 printf
Reg request busy/SDO 超时 在周期线程中过度发 SDO ethercat sdos 将 SDO 操作放在初始化线程或降低频率
多从站偶发掉线 布线/电源或环网拓扑问题 ethercat crc, ethercat slaves -v 更换网线、缩短总线、为伺服单独供电

排查顺序建议:物理层 → 主站日志 (dmesg) → ethercat 命令 → 应用日志 → 抓包 (Wireshark ECAT 插件)。


通过上述结构,初学者可以先做环境自检,再按章节查找 API/示例;现场调试时亦可直接定位常见故障与状态机处理逻辑。如需扩展特定驱动或实时算法,可在对应章节追加项目实践笔记。