7.5.18 EtherCAT 用户手册
1. 概述
本模块基于开源 SOEM(Simple Open EtherCAT Master)库,针对搭载 FreeRTOS 的嵌入式 MCU 平台(S100)进行了移植与适配。主要特性如下:
本模块基于开源 SOEM(Simple Open EtherCAT Master)库,针对搭载 FreeRTOS 的嵌入式 MCU 平台(S600)进行了移植与适配。主要特性如下:
| 特性 | 说明 |
|---|---|
| EtherCAT 主站 | 实现完整的 EtherCAT MainDevice(主站)功能 |
| 实时调度 | 通过 FreeRTOS 任务实现周期性过程数据收发 |
| 硬件适配 | 通过 McalCdd/Ethernet(Eth.h)驱动底层以太网收发 |
| 邮箱协议 | 支持 CoE、FoE、EoE、SoE、AoE 邮箱协议 |
| 分布式时钟 | 支持 DC 同步,提供 PI 控制器进行时钟对齐 |
| 从站诊断 | 支持从站状态检测、自动恢复与重配 |
| Shell 接口 | 提供交互式命令行测试入口 |
2. 软件架构
| 层级 | 模块 / 文件 | 说明 | 对外接口 |
|---|---|---|---|
| 用户应用层 | ec_sample/src/ec_sample.c | EtherCAT 完整运行示例,包含实时任务、检测任务与启动流程 | EtherCAT_Sample_Start() / Shell Ec_Sample_Test |
| 用户应用层 | slaveinfo/src/slaveinfo.c | 从站信息扫描、SDO 读写工具 | EtherCAT_SlaveInfo_Start() / Shell Ec_SlaveInfo、Ec_SlaveOP |
| SOEM 核心层 | core/src/ec_main.c | 主站初始化、状态机、邮箱收发、EEPROM 读写 | ecx_init / ecx_readstate / ecx_writestate 等 |
| SOEM 核心层 | core/src/ec_config.c | 从站自动枚举与 PDO 映射配置 | ecx_config_init / ecx_config_map_group |
| SOEM 核心层 | core/src/ec_coe.c | CoE(CANopen over EtherCAT)SDO 读写、对象字典查询 | ecx_SDOread / ecx_SDOwrite / ecx_readODlist |
| SOEM 核心层 | core/src/ec_dc.c | 分布式时钟(DC)配置与同步 | ecx_configdc |
| SOEM 核心层 | core/src/ec_foe.c | FoE(File over EtherCAT)文件传输 | ecx_FOEread / ecx_FOEwrite |
| SOEM 核心层 | core/src/ec_eoe.c | EoE(Ethernet over EtherCAT)以太网隧道 | ecx_EOEsend / ecx_EOErecv |
| SOEM 核心层 | core/src/ec_soe.c | SoE(Servo over EtherCAT)伺服参数访问 | ecx_SoEread / ecx_SoEwrite |
| SOEM 核心层 | core/src/ec_base.c | 底层 EtherCAT 帧构造与收发 | ecx_send_processdata / ecx_receive_processdata |
| SOEM 核心层 | core/src/ec_print.c | 错误码与状态码转可读字符串 | ec_ALstatuscode2string / ecx_elist2string |
| OSAL 抽象层 | osal/src/osal.c | OS 时间、任务、互斥锁的 FreeRTOS 适配实现 | osal_usleep / osal_mutex_lock 等 |
| OSHW/NIC 层 | oshw/src/nicdrv.c | 网络收发适配,将 SOEM 帧操作映射到 McalCdd/Ethernet API | ecx_setupnic / ecx_outframe / ecx_inframe |
| OSHW/NIC 层 | oshw/src/oshw.c | 字节序转换工具(大小端互转) | oshw_htons / oshw_ntohs |
| 硬件层 | 以太网控制器 Controller 0 | 物理网口,收发 EtherCAT 帧(ETH 类型 0x88A4) | McalCdd Eth_Transmit / Eth_Receive_ethercat |
3. 目录结构
McalCdd/EtherCAT/
├── SConscript # 顶层 构建脚本
├── README.md # 原始说明
├── core/ # SOEM 核心源码
│ ├── SConscript
│ ├── src/
│ │ ├── ec_main.c # 主功能:初始化、状态机、邮箱
│ │ ├── ec_config.c # 从站自动配置(PDO 映射)
│ │ ├── ec_coe.c # CoE(CANopen over EtherCAT)SDO 操作
│ │ ├── ec_dc.c # 分布式时钟(DC)配置
│ │ ├── ec_eoe.c # EoE(Ethernet over EtherCAT)
│ │ ├── ec_foe.c # FoE(File over EtherCAT)
│ │ ├── ec_soe.c # SoE(Servo over EtherCAT)
│ │ ├── ec_base.c # 底层帧操作
│ │ └── ec_print.c # 调试打印工具
│ └── inc/
│ ├── nicdrv.h # NIC 驱动接口
│ ├── osal.h # OSAL 接口
│ ├── osal_defs.h # OSAL 类型定义
│ ├── oshw.h # OS/HW 工具接口
│ └── soem/
│ ├── soem.h # 总头文件(一键包含)
│ ├── ec_main.h # 数据结构与主 API 声明
│ ├── ec_type.h # 基本类型、枚举、宏
│ ├── ec_options.h # 编译期配置参数
│ ├── ec_coe.h # CoE API 声明
│ ├── ec_config.h # 配置 API 声明
│ ├── ec_dc.h # DC API 声明
│ ├── ec_eoe.h # EoE API 声明
│ ├── ec_foe.h # FoE API 声明
│ ├── ec_soe.h # SoE API 声明
│ └── ec_print.h # 打印工具声明
├── osal/ # FreeRTOS OSAL 实现
│ ├── SConscript
│ └── src/osal.c
└── oshw/ # 硬件驱动适配
├── SConscript
└── src/
├── nicdrv.c # McalCdd/Ethernet NIC 驱动
└── oshw.c # 字节序工具
samples/EtherCAT/
├── SConscript
├── ec_sample/
│ ├── SConscript
│ └── src/ec_sample.c # 基本运行示例
└── slaveinfo/
├── SConscript
└── src/slaveinfo.c # 从站信息扫描示例
4. 关键数据结构
4.1 ecx_contextt — 主站上下文
所有 SOEM API 均以此结构体的指针作为第一个参数。应用层需声明一个该类型的静态变量:
static ecx_contextt ctx;
主要成员:
| 成员 | 类型 | 说明 |
|---|---|---|
port | ecx_portt | 网络端口(含帧缓冲、互斥锁) |
slavelist[] | ec_slavet[EC_MAXSLAVE] | 从站列表,下标 0 为广播虚拟从站 |
slavecount | int | 实际发现的从站数量 |
grouplist[] | ec_groupt[EC_MAXGROUP] | 从站分组信息 |
ecaterror | boolean | 是否存在 EtherCAT 错误 |
DCtime | int64 | 从站 DC 时间(ns) |
ENI | ec_enit * | EtherCAT 网络信息(可选) |
FOEhook | 函数指针 | FoE 数据回调钩子 |
EOEhook | 函数指针 | EoE 数据回调钩子 |
manualstatechange | int | 0=自动状态转换,1=手动控制 |
overlappedMode | boolean | 重叠模式(适用于 TI ESC) |
packedMode | boolean | 紧凑 PDO 映射模式 |
4.2 ec_slavet — 从站信息
| 成员 | 说明 |
|---|---|
state | 当前从站状态(ec_state 枚举值) |
ALstatuscode | AL 状态码,错误时提供具体原因 |
configadr | 配置地址(站地址) |
eep_man / eep_id / eep_rev | EEPROM 中的厂商/产品/版本信息 |
Obytes / Ibytes | 输出/输入字节数 |
outputs / inputs | 指向 IOmap 中对应区域的指针 |
hasdc | 是否支持分布式时钟 |
CoEdetails | CoE 功能标志位 |
mbx_proto | 支持的邮箱协议位掩码 |
islost | 从站是否已丢失连接 |
name | 从站可读名称字符串 |
4.3 ec_groupt — 从站分组
| 成员 | 说明 |
|---|---|
Obytes / Ibytes | 整组输出/输入字节数 |
outputsWKC / inputsWKC | 期望的工作计数器值 |
docheckstate | 需要执行状态检查标志 |
hasdc | 组内是否有 DC 从站 |
5. 从站状态机
EtherCAT 从站遵循以下状态机(ESM):
INIT (0x01)
│
▼ I→P
PRE_OP (0x02)
│
▼ P→S
SAFE_OP (0x04)
│
▼ S→O
OPERATIONAL (0x08)
各状态对应枚举值:
| 枚举值 | 数值 | 说明 |
|---|---|---|
EC_STATE_NONE | 0x00 | 无效状态 |
EC_STATE_INIT | 0x01 | 初始化 |
EC_STATE_PRE_OP | 0x02 | 预运行 |
EC_STATE_BOOT | 0x03 | 引导状态(固件更新) |
EC_STATE_SAFE_OP | 0x04 | 安全运行(只读 PDO) |
EC_STATE_OPERATIONAL | 0x08 | 运行(全双工 PDO) |
EC_STATE_ERROR | 0x10 | 错误/ACK |
写状态示例:
/* 请求所有从站进入 OPERATIONAL */
ctx.slavelist[0].state = EC_STATE_OPERATIONAL;
ecx_writestate(&ctx, 0);
/* 等待确认(超时 2s) */
ecx_statecheck(&ctx, 0, EC_STATE_OPERATIONAL, EC_TIMEOUTSTATE);
6. API 参考
6.1 初始化与关闭
ecx_init
int ecx_init(ecx_contextt *context, const char *ifname);
初始化 EtherCAT 主站,打开底层网络接口。
| 参数 | 说明 |
|---|---|
context | 主站上下文指针 |
ifname | 网络接口名称(嵌入式平台填 NULL,默认使用控制器 0) |
返回值:成功返回非零值,失败返回 0。
int rv = ecx_init(&ctx, NULL);
if (rv == 0) {
/* 初始化失败 */
}
ecx_close
void ecx_close(ecx_contextt *context);
关闭主站,释放底层网络资源。
6.2 从站配置
ecx_config_init
int ecx_config_init(ecx_contextt *context);
扫描总线上的所有从站,将其配置至 PRE_OP 状态,填充 ctx.slavelist[] 和 ctx.slavecount。
返回值:发现的从站数量。
ecx_config_map_group
int ecx_config_map_group(ecx_contextt *context, void *pIOmap, uint8 group);
为指定分组的所有从站配置 PDO 映射,将输入/输出区域映射到 pIOmap 缓冲区。
| 参数 | 说明 |
|---|---|
context | 主站上下文 |
pIOmap | IO 映射缓冲区(建议 4096 字节) |
group | 分组编号(0 表示默认分组) |
返回值:映射成功的 IO 字节总数。
static uint8 IOmap[4096];
int map_result = ecx_config_map_group(&ctx, IOmap, 0);
配置完成后可获取期望工作计数器:
ec_groupt *group = &ctx.grouplist[0];
int expectedWKC = (group->outputsWKC * 2) + group->inputsWKC;
ecx_reconfig_slave
int ecx_reconfig_slave(ecx_contextt *context, uint16 slave, int timeout);
重新配置指定从站(用于故障恢复)。成功返回值 >= EC_STATE_PRE_OP。
ecx_recover_slave
int ecx_recover_slave(ecx_contextt *context, uint16 slave, int timeout);
恢复已丢失连接的从站。
6.3 状态管理
ecx_readstate
int ecx_readstate(ecx_contextt *context);
读取所有从站当前状态,更新 ctx.slavelist[i].state。
返回值:最低优先级从站的状态值。
ecx_writestate
int ecx_writestate(ecx_contextt *context, uint16 slave);
向指定从站写入 ctx.slavelist[slave].state 中设置的目标状态。slave=0 时广播到所有从站。
ecx_statecheck
uint16 ecx_statecheck(ecx_contextt *context, uint16 slave, uint16 reqstate, int timeout);
轮询等待从站达到目标状态,直到超时。
| 参数 | 说明 |
|---|---|
slave | 从站编号,0=所有 |
reqstate | 期望的目标状态 |
timeout | 超时时间(微秒),建议使用 EC_TIMEOUTSTATE(2s) |
返回值:当前达到的状态值。
6.4 过程数据(PDO)
PDO 通信是 EtherCAT 实时数据交换的核心,须在专用实时任务中周期性调用。
ecx_send_processdata
int ecx_send_processdata(ecx_contextt *context);
发送所有分组的过程数据帧(将 outputs 区域内容发送到从站)。
ecx_receive_processdata
int ecx_receive_processdata(ecx_contextt *context, int timeout);
接收过程数据响应帧(将从站输入更新至 inputs 区域)。
返回值:工作计数器(WKC)。应与 expectedWKC 比较以检测通信异常。
ecx_send_processdata_group / ecx_receive_processdata_group
分组版本,只操作指定分组:
int ecx_send_processdata_group(ecx_contextt *context, uint8 group);
int ecx_receive_processdata_group(ecx_contextt *context, uint8 group, int timeout);
PDO 数据访问
配置映射完成后,可通过 ec_slavet 中的指针直接读写 PDO 数据:
ec_slavet *slave = &ctx.slavelist[1];
/* 写输出数据(主站 → 从站) */
memcpy(slave->outputs, &my_output, slave->Obytes);
/* 读输入数据(从站 → 主站) */
memcpy(&my_input, slave->inputs, slave->Ibytes);
6.5 邮箱协议(CoE / SDO)
SDO 读取
int ecx_SDOread(ecx_contextt *context,
uint16 slave, uint16 index, uint8 subindex,
boolean CA, int *psize, void *p, int timeout);
从指定从站读取一个 SDO 对象。
| 参数 | 说明 |
|---|---|
slave | 从站编号(1 起) |
index | 对象字典索引 |
subindex | 子索引 |
CA | 是否使用完整访问(Complete Access) |
psize | 传入缓冲区大小,返回实际读取字节数 |
p | 数据缓冲区指针 |
timeout | 超时(µs),建议 EC_TIMEOUTRXM(700ms) |
返回值:工作计数器(> 0 表示成功)。
uint32_t vendor_id = 0;
int size = sizeof(vendor_id);
int wkc = ecx_SDOread(&ctx, 1, 0x1018, 0x01, FALSE, &size, &vendor_id, EC_TIMEOUTRXM);
SDO 写入
int ecx_SDOwrite(ecx_contextt *context,
uint16 Slave, uint16 Index, uint8 SubIndex,
boolean CA, int psize, const void *p, int Timeout);
向指定从站写入一个 SDO 对象。
uint16_t mode = 0x0008; /* 例如:设置运行模式 */
ecx_SDOwrite(&ctx, 1, 0x6060, 0x00, FALSE, sizeof(mode), &mode, EC_TIMEOUTRXM);
邮箱处理任务
对于支持 CoE 的从站,须在循环任务中定期调用邮箱处理函数:
ecx_mbxhandler(&ctx, 0, 4); /* 处理分组 0,最多 4 条消息 */
将从站加入循环邮箱处理:
ecx_slavembxcyclic(&ctx, slave_index);
CoE 对象字典查询
/* 读取对象字典列表 */
ec_ODlistt ODlist;
ecx_readODlist(&ctx, slave, &ODlist);
/* 读取对象描述 */
ecx_readODdescription(&ctx, item, &ODlist);
/* 读取对象条目信息 */
ec_OElistt OElist;
ecx_readOE(&ctx, item, &ODlist, &OElist);
6.6 分布式时钟(DC)
ecx_configdc
int ecx_configdc(ecx_contextt *context);
配置所有支持 DC 的从站的分布式时钟,使其与第一个 DC 从站同步。应在 ecx_config_map_group 之后调用。
DC 时间同步(PI 控制器)
应用层通常使用 PI 控制器将本地系统时钟与 DC 时间对齐,示例见 ec_sample.c:
static float pgain = 0.01f;
static float igain = 0.00002f;
static int64 syncoffset = 500000; /* 500µs 偏移 */
void ec_sync(int64 reftime, int64 cycletime, int64 *offsettime)
{
static int64 integral = 0;
int64 delta = (reftime - syncoffset) % cycletime;
if (delta > (cycletime / 2)) delta -= cycletime;
integral += -delta;
*offsettime = (int64)((-delta * pgain) + (integral * igain));
}
在实时任务中使用:
if (ctx.slavelist[0].hasdc && (wkc > 0))
{
ec_sync(ctx.DCtime, cycletime, &toff);
}
6.7 错误处理
错误栈操作
/* 检查是否有未读错误 */
boolean ecx_iserror(ecx_contextt *context);
/* 从错误栈弹出一个错误 */
boolean ecx_poperror(ecx_contextt *context, ec_errort *Ec);
ec_errort 结构体:
| 成员 | 说明 |
|---|---|
Time | 错误发生时间 |
Slave | 产生错误的从站编号 |
Index / SubIdx | CoE 对象索引/子索引 |
Etype | 错误类型(ec_err_type 枚举) |
AbortCode | SDO 中止代码(CoE 错误时有效) |
错误类型枚举:
| 枚举值 | 说明 |
|---|---|
EC_ERR_TYPE_SDO_ERROR | SDO 错误 |
EC_ERR_TYPE_EMERGENCY | 紧急报文 |
EC_ERR_TYPE_PACKET_ERROR | 数据包错误 |
EC_ERR_TYPE_FOE_ERROR | FoE 错误 |
EC_ERR_TYPE_SOE_ERROR | SoE 错误 |
EC_ERR_TYPE_MBX_ERROR | 邮箱错误 |
7. 快速入门:ec_sample
samples/EtherCAT/ec_sample/src/ec_sample.c 提供了一个完整的 EtherCAT 主站运行示例,展示了从初始化到运行再到关闭的全流程。
7.1 总体流程
EtherCAT_Sample_Start(cycle_time_us)
│
├─ xTaskCreate("ECAT_RT", ecatthread, ...) ← 最高优先级
├─ xTaskCreate("ECAT_CHK", ecatcheck, ...) ← 低优先级
└─ xTaskCreate("ECAT_BRINGUP", EtherCAT_Bringup_Task, ...)
│
└─ ecatbringup()
├─ ecx_init(&ctx, NULL)
├─ ecx_config_init(&ctx)
├─ ecx_config_map_group(&ctx, IOmap, 0)
├─ ecx_configdc(&ctx)
├─ ecx_slavembxcyclic() ← 注册 CoE 从站
├─ ecx_writestate() → OPERATIONAL
├─ [运行循环 500×20ms]
└─ ecx_writestate() → SAFE_OP → INIT
7.2 启动函数
/**
* 启动 EtherCAT 示例
* @param cycle_time_us 周期时间(微秒),传 0 则使用默认 1000µs
* @return 0 = 成功
*/
int EtherCAT_Sample_Start(int cycle_time_us);
7.3 实时任务(ecatthread)
运行在最高 FreeRTOS 优先级,周期性执行:
void ecatthread(void *pvParameters)
{
/* 等待映射完成 */
while (!mappingdone) osal_usleep(100);
/* 获取初始时间基准 */
osal_get_monotonic_time(&ts);
while (1)
{
/* 计算并等待下一周期开始时刻 */
add_time_ns(&ts, cycletime + toff);
osal_monotonic_sleep(&ts);
if (dorun > 0)
{
wkc = ecx_receive_processdata(&ctx, EC_TIMEOUTRET);
/* DC 时间同步 */
if (ctx.slavelist[0].hasdc && (wkc > 0))
ec_sync(ctx.DCtime, cycletime, &toff);
/* 邮箱处理 */
ecx_mbxhandler(&ctx, 0, 4);
ecx_send_processdata(&ctx);
}
vTaskDelay(1);
}
}
7.4 故障检测任务(ecatcheck)
运行在低优先级(每 10ms 执行一次),负责监控和恢复异常从站:
void ecatcheck(void *pvParameters)
{
while (1)
{
if (inOP && ((dowkccheck > 2) || ctx.grouplist[0].docheckstate))
{
ecx_readstate(&ctx);
for (int i = 1; i <= ctx.slavecount; i++)
{
if (slave->state == (EC_STATE_SAFE_OP + EC_STATE_ERROR))
/* 清除错误 ACK */
else if (slave->state == EC_STATE_SAFE_OP)
/* 推回 OPERATIONAL */
else if (slave->state > EC_STATE_NONE)
ecx_reconfig_slave(...); /* 重配 */
else if (slave->islost)
ecx_recover_slave(...); /* 恢复 */
}
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
7.5 FreeRTOS 任务参数汇总
| 任务名 | 函数 | 栈大小 | 优先级 |
|---|---|---|---|
ECAT_RT | ecatthread | configMINIMAL_STACK_SIZE × 8 | configMAX_PRIORITIES - 1(最高) |
ECAT_CHK | ecatcheck | configMINIMAL_STACK_SIZE × 8 | tskIDLE_PRIORITY + 1 |
ECAT_BRINGUP | EtherCAT_Bringup_Task | configMINIMAL_STACK_SIZE × 10 | configMAX_PRIORITIES - 2 |
注意:
ecx_config_map_group会消耗较大栈空间,因此 Bringup 任务使用了更大的栈。
8. 从站信息查询:slaveinfo
samples/EtherCAT/slaveinfo/src/slaveinfo.c 提供了扫描并打印总线上所有从站详细信息的工具。
8.1 启动函数
/**
* 启动从站信息扫描
* @param sdo_flag 是否打印 SDO 对象字典信息
* @param map_flag 是否打印 PDO 映射信息
*/
void EtherCAT_SlaveInfo_Start(boolean sdo_flag, boolean map_flag);
8.2 输出内容
扫描完成后将逐一打印每个从站的以下信息:
- 从站编号、名称
- 厂商 ID、产品码、修订号、序列号
- 输出/输入字节数
- 支持的邮箱协议(CoE / FoE / EoE / SoE / AoE)
- SM(同步管理器)配置
- FMMU 配置
- DC 支持情况与参数
- PDO 映射详情(
-map标志) - 对象字典条目(
-sdo标志)
8.3 SDO 操作命令
通过 Shell 命令可直接执行 SDO 读写:
/**
* @param slave 从站编号
* @param index 对象索引(十六进制)
* @param subindex 子索引
* @param value 写入值(读操作时忽略)
* @param action 1=读, 2=写
*/
int Ec_SlaveOP(uint16 slave, uint16 index, uint8 subindex, uint8 value, uint16 action);
9. Shell 命令接口
两个示例程序均通过 SHELL_EXPORT_CMD 宏向 Shell 系统注册了命令,可在运行时直接调用:
Ec_Sample_Test
Ec_Sample_Test [ifname] [cyc_time_us]
启动 EtherCAT 示例(ifname 参数已废弃,cyc_time_us=0 使用默认 1ms 周期)。
示例:
Ec_Sample_Test "" 2000 # 2ms 周期运行
Ec_Sample_Test "" 0 # 默认 1ms 周期
Ec_SlaveInfo
Ec_SlaveInfo [-sdo] [-map]
扫描并打印从站信息。 可选参数:
-sdo:同时打印 SDO 对象字典-map:同时打印 PDO 映射
Ec_SlaveOP
Ec_SlaveOP <slave> <index> <subindex> <value> <action>
直接对从站执行 SDO 读(action=1)或写(action=2)操作。
10. 平台适配层
10.1 OSAL(操作系统抽象层)
文件:osal/src/osal.c
将 SOEM 对操作系统的依赖抽象为统一接口,当前实现基于 FreeRTOS。
| OSAL 函数 | FreeRTOS 实现 | 说明 |
|---|---|---|
osal_usleep(usec) | vTaskDelay(pdMS_TO_TICKS(...)) | 微秒级睡眠 |
osal_get_monotonic_time(ts) | xTaskGetTickCount() |