跳到主要内容

CoE (CANopen over EtherCAT)

CoE 提供基于 CANopen 的 SDO(服务数据对象)访问,用于读写从站的对象字典参数。

CoE 可用性检查

通过 GetSlaveCoEDetails(master_index, slave) 返回值判断从站是否支持 CoE(非 0 表示支持)。

函数命名

SDO 邮箱读写在 C SDK 中以 dx_sdo_* 前缀提供。动态加载模式经 dll.dx_sdo_read(...) 调用,静态链接模式直接调 dx_sdo_read(...)

C 特有语法糖

读 SDO 前常需获取从站 Vendor / Product 信息, C 可用 ec_vendor(mi, si) / ec_product(mi, si) inline 短名替代 GetSlaveVendorId / GetSlaveProductCode, 编译期完全展开, 零开销. 详见 inline 包装.

SDO 读写

dx_sdo_read()

char* dx_sdo_read(uint16_t master_index, uint16_t slave, uint16_t index,
uint8_t subindex, BOOL CA, int* out_byte_size);

读取 SDO 原始字节数据。

参数:

  • master_index (uint16_t) — 主站索引
  • slave (uint16_t) — 从站索引
  • index (uint16_t) — 对象索引
  • subindex (uint8_t) — 子索引
  • CA (BOOL) — Complete Access 模式
  • out_byte_size (int*) — 输出数据大小

返回值:

  • char* — 数据指针,需调用 FreeMemory() 释放。失败返回 NULL

dx_sdo_write()

uint8_t dx_sdo_write(uint16_t master_index, uint16_t slave, uint16_t index,
uint8_t subindex, BOOL CA, uint8_t* bytes, int length);

写入 SDO 数据。

参数:

  • master_index (uint16_t) — 主站索引
  • slave (uint16_t) — 从站索引
  • index (uint16_t) — 对象索引
  • subindex (uint8_t) — 子索引
  • CA (BOOL) — Complete Access 模式
  • bytes (uint8_t*) — 要写入的数据
  • length (int) — 数据长度

返回值:

  • uint8_t — 0 成功,非 0 失败

示例:

/* 读取 VendorID (0x1018:01) */
int sdo_size = 0;
char* data = dll.dx_sdo_read(master, 1, 0x1018, 0x01, FALSE, &sdo_size);
if (data && sdo_size > 0) {
uint32_t vendor_id = 0;
memcpy(&vendor_id, data, sdo_size > 4 ? 4 : sdo_size);
printf("Vendor ID: 0x%08X\n", vendor_id);
dll.FreeMemory(data);
}

/* 写入 ControlWord (0x6040:00) */
uint16_t cw = 0x0006;
dll.dx_sdo_write(master, 1, 0x6040, 0x00, FALSE, (uint8_t*)&cw, sizeof(cw));

dx_sdo_read_ex()

char* dx_sdo_read_ex(uint16_t master_index, uint16_t slave, uint16_t index,
uint8_t subindex, BOOL CA, int* out_byte_size,
uint32_t* out_abort_code);

扩展版 SDO 读取,相对于 dx_sdo_read() 额外回填 CANopen Abort Code(失败诊断用)。普通版 dx_sdo_read() 仅通过 out_byte_size 负值或 NULL 指针报错;扩展版在 out_abort_code 输出标准的 4 字节 SDO 中止码(参见本页SDO 错误码表),便于上层精准定位失败原因。与 C++/C#/Java/Python/Rust 五个 SDK 接口对齐。

参数:

  • master_index (uint16_t) — 主站索引
  • slave (uint16_t) — 从站索引
  • index (uint16_t) — 对象索引
  • subindex (uint8_t) — 子索引
  • CA (BOOL) — Complete Access 模式
  • out_byte_size (int*) — 输出数据大小
  • out_abort_code (uint32_t*) — [输出] SDO Abort Code,成功时为 0

返回值:

  • char* — 数据指针,需调用 FreeMemory() 释放。失败返回 NULL,并通过 out_abort_code 输出错误码

dx_sdo_write_ex()

uint8_t dx_sdo_write_ex(uint16_t master_index, uint16_t slave, uint16_t index,
uint8_t subindex, BOOL CA, uint8_t* bytes, int length,
uint32_t* out_abort_code);

扩展版 SDO 写入,相对于 dx_sdo_write() 额外回填 CANopen Abort Code。失败时既能拿到非 0 返回值,也能从 out_abort_code 读到具体的中止码(如 0x06090030 值超出范围、0x06010002 写只读对象等),无需再追加一次诊断调用。

参数:

  • master_index (uint16_t) — 主站索引
  • slave (uint16_t) — 从站索引
  • index (uint16_t) — 对象索引
  • subindex (uint8_t) — 子索引
  • CA (BOOL) — Complete Access 模式
  • bytes (uint8_t*) — 要写入的数据
  • length (int) — 数据长度
  • out_abort_code (uint32_t*) — [输出] SDO Abort Code,成功时为 0

返回值:

  • uint8_t — 0 成功,非 0 失败

示例:

/* 扩展版 SDO 读取,失败时能拿到 abort code */
int sdo_size = 0;
uint32_t abort = 0;
char* data = dll.dx_sdo_read_ex(master, 1, 0x6041, 0x00, FALSE, &sdo_size, &abort);
if (data) {
uint16_t status_word = 0;
memcpy(&status_word, data, sizeof(status_word));
printf("StatusWord: 0x%04X\n", status_word);
dll.FreeMemory(data);
} else {
printf("SDO 读取失败, AbortCode=0x%08X\n", abort);
}

/* 扩展版 SDO 写入 */
uint16_t cw = 0x000F;
abort = 0;
uint8_t err = dll.dx_sdo_write_ex(master, 1, 0x6040, 0x00, FALSE,
(uint8_t*)&cw, sizeof(cw), &abort);
if (err != 0) {
printf("SDO 写入失败, AbortCode=0x%08X\n", abort);
}
何时使用 _ex 版本
  • 需要区分"通信失败"与"从站拒绝"(如值超范围 vs 总线断链)
  • 调试新驱动器,把 abort code 写入日志便于回查
  • 应用层无需 abort code 时仍可继续用普通版,更简洁

类型化读写 (ethercat_advanced.h)

ethercat_advanced.h 提供便捷的类型化 SDO 读写函数,自动处理类型转换。

类型化读取

所有读取函数签名一致: sdo_read_xxx(dll, master, slave, index, sub, out_ptr),返回 0 成功。

  • sdo_read_u8 (uint8_t*) — 读取 8 位无符号整数
  • sdo_read_u16 (uint16_t*) — 读取 16 位无符号整数
  • sdo_read_u32 (uint32_t*) — 读取 32 位无符号整数
  • sdo_read_i8 (int8_t*) — 读取 8 位有符号整数
  • sdo_read_i16 (int16_t*) — 读取 16 位有符号整数
  • sdo_read_i32 (int32_t*) — 读取 32 位有符号整数
  • sdo_read_f32 (float*) — 读取单精度浮点数
  • sdo_read_f64 (double*) — 读取双精度浮点数
  • sdo_read_string (char* buf, int buf_size) — 读取字符串

类型化写入

所有写入函数签名一致: sdo_write_xxx(dll, master, slave, index, sub, value),返回 0 成功。

  • sdo_write_u8 (uint8_t) — 写入 8 位无符号整数
  • sdo_write_u16 (uint16_t) — 写入 16 位无符号整数
  • sdo_write_u32 (uint32_t) — 写入 32 位无符号整数
  • sdo_write_i8 (int8_t) — 写入 8 位有符号整数
  • sdo_write_i16 (int16_t) — 写入 16 位有符号整数
  • sdo_write_i32 (int32_t) — 写入 32 位有符号整数
  • sdo_write_f32 (float) — 写入单精度浮点数
  • sdo_write_f64 (double) — 写入双精度浮点数

示例:

#include "ethercat_advanced.h"

/* 读取 StatusWord */
uint16_t sw;
sdo_read_u16(&dll, master, 1, 0x6041, 0, &sw);
printf("StatusWord: 0x%04X\n", sw);

/* 写入 ControlWord */
sdo_write_u16(&dll, master, 1, 0x6040, 0, 0x0006);

/* 读取设备名称 */
char name[64];
sdo_read_string(&dll, master, 1, 0x1008, 0, name, sizeof(name));
printf("设备名称: %s\n", name);

批量 SDO 读取

C SDK 暂不支持批量 SDO 读取

SDOReadMultiple 批量读取接口当前版本未实现ethercat.h 中的 dll.SDOReadMultiple 字段保持为 NULL,调用无效。批量读取多个 SDO 条目请逐次调用 dx_sdo_read() / dx_sdo_read_ex()。结构体 ec_sdo_entry_t / ec_sdo_read_result_tslave/coe.h 中已预留供后续版本使用。

逐次读取多个参数:

/* 逐次读取多个参数 (替代不可用的批量接口) */
struct { uint16_t index; uint8_t subidx; } items[] = {
{ 0x6041, 0x00 }, /* StatusWord */
{ 0x6064, 0x00 }, /* 实际位置 */
{ 0x6077, 0x00 }, /* 实际转矩 */
};
for (int i = 0; i < 3; i++) {
int sz = 0;
char* data = dll.dx_sdo_read(master, 1, items[i].index, items[i].subidx,
FALSE, &sz);
if (data && sz > 0) {
printf("0x%04X:%02X = %d 字节\n", items[i].index, items[i].subidx, sz);
dll.FreeMemory(data);
}
}

对象字典访问

GetSlaveSDOList()

void* GetSlaveSDOList(uint16_t master_index, uint16_t slave_index);

获取从站的完整对象字典列表(含子条目详情)。返回内部指针,无需释放。

参数:

  • master_index (uint16_t) — 主站索引
  • slave_index (uint16_t) — 从站索引

返回值:

  • void* — 对象字典列表内部指针

GetSlaveSDOListBasic()

void* GetSlaveSDOListBasic(uint16_t master_index, uint16_t slave_index);

获取从站的基础对象字典列表(不含子条目,更快)。返回内部指针,无需释放。

GetSlavePointer_SDO_Value()

char* GetSlavePointer_SDO_Value(uint16_t master_index, uint16_t slave_index,
uint16_t OD_index, uint8_t OE_index, int* out_byte_size);

读取指定对象的子索引值。

参数:

  • OD_index (uint16_t) — 对象索引
  • OE_index (uint8_t) — 子索引
  • out_byte_size (int*) — 输出数据大小

返回值:

  • char* — 数据指针,需调用 FreeMemory() 释放

GetMultiSlaveSDOList()

int  GetMultiSlaveSDOList(uint16_t master_index, uint16_t* slave_indices, int count, void** results);
void FreeMultiSlaveSDOList(void** results, int count);

批量读取多个从站的 SDO 对象字典(流水线并行,比逐个读取更快)。

参数:

  • slave_indices (uint16_t*) — 从站索引数组
  • count (int) — 从站数量
  • results (void**) — 输出结果数组

返回值:

  • int — 成功读取的从站数量

按需查询单个对象 (CoE-H2 SDO Information Services)

GetSlaveSDOList*整表加载接口 (一次性把上千个对象描述拉回本地缓存); 适合 OD 浏览器场景。生产代码里通常只需要某一个对象 (如 0x6041 StatusWord) 的描述, 这时用 H2 单条查询接口避免拉整表。底层走标准 ETG.1000.6 §5.6.3.6 SDO Information Services。

C SDK 提供两层接口:

底层 — dll_t 字段函数 (ethercat.h)

CoE-H2 原语经 dll_t 结构体字段导出 (动态加载模式由 LOAD_DLL 绑定), 必须经 dll. 调用; 返回的是不透明指针 (void*, 内部布局不公开):

void* dll.coe_get_od_list   (uint16_t mi, uint16_t si);
void* dll.coe_get_object_desc(uint16_t mi, uint16_t si, uint16_t index);
void* dll.coe_get_entry_desc (uint16_t mi, uint16_t si, uint16_t index, uint8_t subindex);
void dll.coe_free_odlist (void* p);
void dll.coe_free_oelist (void* p);

由于内部结构体不公开, 应用层不应直接解引用这些 void*。生产代码请用下面的高级封装。

高级 — 类型化封装 (ethercat_advanced.h)

ethercat_advanced.h 把上述原语包成填充调用者结构体的便捷函数, 返回 1 成功 / 0 失败:

int coe_get_object_description(dll_t* dll, uint16_t master, uint16_t slave,
uint16_t index, od_object_t* out_obj);
int coe_get_entry_description (dll_t* dll, uint16_t master, uint16_t slave,
uint16_t index, uint8_t subindex,
od_entry_t* out_entry);
  • coe_get_object_description — 仅查询单个主索引的对象描述 (Object Code / DataType / MaxSubIndex / Name), 不展开子条目 (out_obj->entries 始终为 NULL)。适合判断"对象存在吗"或"是 RECORD 还是 VARIABLE"。
  • coe_get_entry_description — 查询单个 (index, subindex) 的子条目描述 (BitLength / ObjAccess / Name)。配合 dx_sdo_read_ex 在不知道字段长度时按描述自适应解析。

这两个函数把结果写进调用者栈上od_object_t / od_entry_t (结构定义见本页 OD 树遍历 节), 不分配堆内存, 无需手动释放。若要整表加载, 用同文件的 od_load_via_coe_h2(dll, master, slave) (返回 od_list_t*, 用 od_free 释放)。

示例:

#include "ethercat_advanced.h"

/* 查询 0x6040 是否存在 */
od_object_t obj;
if (coe_get_object_description(&dll, master, 1, 0x6040, &obj)) {
printf("0x6040 = %s, ObjectCode=0x%02X, MaxSub=%u\n",
obj.name, obj.object_code, obj.max_sub);
} else {
printf("0x6040 不存在或查询失败\n");
}

/* 查询 0x6041:00 子条目 */
od_entry_t entry;
if (coe_get_entry_description(&dll, master, 1, 0x6041, 0x00, &entry)) {
printf("0x6041:00 = %s, BitLen=%u, Access=0x%04X\n",
entry.name, entry.bit_length, entry.obj_access);
}
性能建议
  • 整表 GetSlaveSDOList / od_load_via_coe_h2 适合 OD 浏览器初始化, 一次性后驻留内存
  • 控制循环 / 在线参数调整应只查需要的索引, 走 coe_get_object_description / coe_get_entry_description
  • 这两个接口与 SDO 读写共用邮箱, 不要在 PDO 高频路径调用

OD 树遍历 (ethercat_advanced.h)

高级功能,加载从站的完整对象字典树,支持索引查找和子索引遍历。

od_load()

od_list_t* od_load(dll_t* dll, uint16_t master, uint16_t slave, BOOL load_entries);

加载从站的完整 OD 树。

参数:

  • dll (dll_t*) — DLL 实例
  • master (uint16_t) — 主站索引
  • slave (uint16_t) — 从站索引
  • load_entries (BOOL) — 是否加载子条目(FALSE 只加载主索引)

返回值:

  • od_list_t* — OD 列表,需调用 od_free() 释放

od_find()

od_object_t* od_find(od_list_t* list, uint16_t index);

在 OD 树中查找指定索引的对象。

参数:

  • list (od_list_t*) — OD 列表
  • index (uint16_t) — 对象索引

返回值:

  • od_object_t* — 对象指针,未找到返回 NULL

od_free()

void od_free(od_list_t* list);

释放 OD 树。

相关结构:

typedef struct {
uint16_t index; /* OD 索引 */
char name[42]; /* 名称 */
uint16_t datatype; /* 数据类型 */
uint8_t object_code; /* 对象类型 (0x07=VARIABLE, 0x09=RECORD) */
uint8_t max_sub; /* 最大子索引数 */
int entry_count; /* 实际条目数 */
od_entry_t* entries; /* 条目数组 */
} od_object_t;

typedef struct {
uint8_t subindex; /* 子索引 */
char name[42]; /* 名称 */
uint16_t datatype; /* 数据类型 (ec_datatype_t) */
uint16_t bit_length; /* 位长度 */
uint16_t obj_access; /* 访问权限 (ec_obj_access_t 位掩码) */
} od_entry_t;

示例:

#include "ethercat_advanced.h"

/* 加载完整 OD 树 */
od_list_t* od = od_load(&dll, master, 1, TRUE);
if (od) {
printf("对象数量: %d\n", od->count);

/* 遍历所有对象 */
for (int i = 0; i < od->count; i++) {
od_object_t* obj = &od->objects[i];
printf("0x%04X: %s (%d 子条目)\n", obj->index, obj->name, obj->entry_count);
for (int j = 0; j < obj->entry_count; j++) {
od_entry_t* entry = &obj->entries[j];
printf(" [%d] %s (类型=0x%04X, %d 位)\n",
entry->subindex, entry->name, entry->datatype, entry->bit_length);
}
}

/* 查找特定对象 */
od_object_t* cw = od_find(od, 0x6040);
if (cw) printf("找到 ControlWord: %s\n", cw->name);

od_free(od);
}

EMCY 紧急消息历史

EmcyGetCount()

int EmcyGetCount(uint16_t master, uint16_t slave);

获取从站的 EMCY 紧急消息数量。

参数:

  • master (uint16_t) — 主站索引
  • slave (uint16_t) — 从站索引

返回值:

  • int — 消息数量

EmcyGetHistory()

int EmcyGetHistory(uint16_t master, uint16_t slave, ec_emcy_record_t* out, int max);

读取从站的 EMCY 紧急消息历史。

参数:

  • master (uint16_t) — 主站索引
  • slave (uint16_t) — 从站索引
  • out (ec_emcy_record_t*) — 输出缓冲区
  • max (int) — 最大读取条数

返回值:

  • int — 实际读取的条数

EmcyClearHistory()

void EmcyClearHistory(uint16_t master, uint16_t slave);

清除从站的 EMCY 紧急消息历史。

相关结构:

typedef struct {
uint16_t error_code; /* 错误码 */
uint8_t error_register; /* 错误寄存器 */
uint8_t data[5]; /* 厂商自定义数据 */
uint16_t slave_index; /* 从站索引 */
uint32_t timestamp_ms; /* 时间戳 (毫秒) */
} ec_emcy_record_t;

示例:

int count = dll.EmcyGetCount(master, 1);
if (count > 0) {
ec_emcy_record_t records[16];
int n = dll.EmcyGetHistory(master, 1, records, 16);
for (int i = 0; i < n; i++) {
printf("EMCY[%d]: 错误码=0x%04X, 寄存器=0x%02X, 时间=%ums\n",
i, records[i].error_code, records[i].error_register, records[i].timestamp_ms);
}
/* 处理完毕后清除历史 */
dll.EmcyClearHistory(master, 1);
}

CoE 诊断历史 (ETG.1020 §16)

从站对象字典 0x10F3 定义了标准的诊断消息历史 (Diagnosis History)。与 EMCY 不同, 诊断历史是从站主动压栈的结构化消息集合, 包含时间戳、严重级别、文本描述等, 最多保留 250 条。

诊断历史是从站对象字典中的标准对象, 应用层用通用 SDO 读写原语 (dx_sdo_read / dx_sdo_write) 按需轮询 / 读取 / 确认其各个子索引即可, 不需要专门的诊断历史 API。

与 EMCY 的区别
  • EMCY: 驱动器异步推送的 1 帧 8 字节错误码, 由 SDK 侧历史缓冲捕获
  • Diagnosis History (0x10F3): 从站内部环形缓冲的结构化消息, 主动调用 SDO 读取

0x10F3 子索引布局

诊断历史对象按 ETG.1020 §16 定义子索引, 全部经标准 SDO 通道读写:

子索引名称含义
0x10F3:01MaxMessages缓冲区容量 (最多保留条数)
0x10F3:02NewestMessage最新消息编号
0x10F3:03NewestAcknowledged已确认的最新编号 (写入即标记已读)
0x10F3:04NewMessageAvailable是否有新消息 (布尔标志)
0x10F3:05FlagsBit4 表示覆盖模式 (0=Overwrite 1=AckMode)
0x10F3:06..FFDiagnosis Message各条诊断消息 (DiagCode + Flags + TextID + Timestamp + 参数, 见 ETG.1020 Table 49)

读取流程一般是: 读 0x10F3:04 判断有无新消息 → 读 0x10F3:02 拿最新编号 → 读 0x10F3:05 判断是否 AckMode → 读对应 0x10F3:06..FF 消息子索引 → (AckMode 从站) 向 0x10F3:03 写编号标记已读, 否则缓冲区不释放。

示例:

int n = 0;
uint32_t abort = 0;

/* 读 0x10F3:04 判断是否有新消息 */
char* avail = dll.dx_sdo_read(master, 1, 0x10F3, 0x04, FALSE, &n);
if (avail && n >= 1 && avail[0]) {
/* 读最新消息编号 0x10F3:02 */
char* newest_p = dll.dx_sdo_read(master, 1, 0x10F3, 0x02, FALSE, &n);
uint8_t newest = (newest_p && n >= 1) ? (uint8_t)newest_p[0] : 0;
if (newest_p) FreeMemory(newest_p);

/* 读对应编号的诊断消息子索引 (从 0x06 起按编号定位) */
char* msg = dll.dx_sdo_read_ex(master, 1, 0x10F3, newest, FALSE, &n, &abort);
if (msg && n >= 4) {
uint32_t diag_code = (uint32_t)(uint8_t)msg[0] | ((uint32_t)(uint8_t)msg[1] << 8)
| ((uint32_t)(uint8_t)msg[2] << 16) | ((uint32_t)(uint8_t)msg[3] << 24);
printf("诊断消息 #%u: DiagCode=0x%08X, %d 字节\n", newest, diag_code, n);
}
if (msg) FreeMemory(msg);

/* AckMode 从站: 向 0x10F3:03 写编号标记已读, 否则缓冲区不释放 */
dll.dx_sdo_write(master, 1, 0x10F3, 0x03, FALSE, &newest, sizeof(newest));
}
if (avail) FreeMemory(avail);
线程安全
  • 诊断历史通过标准 SDO 通道, 与 dx_sdo_read / dx_sdo_write 共用邮箱, 不要在高频 PDO 线程调用
  • 建议由后台线程每 1 秒左右轮询一次 0x10F3:04 标志
  • dx_sdo_read_ex 回填的 out_abort_code 非零时参考 SDO 错误码

SDO 错误码

CANopen 标准 SDO 中止码(ETG.1020)。SDO 读写失败时,dx_sdo_read() 返回的 out_byte_size 负值编码了错误码。

错误码含义
0x00000000无错误
0x05030000Toggle 位未改变
0x05040000SDO 协议超时
0x05040001无效的命令标识符
0x05040005内存不足
0x06010000不支持的访问
0x06010001尝试读取只写对象
0x06010002尝试写入只读对象
0x06010003子索引不允许写入
0x06010004不支持完全访问
0x06010005对象长度超限
0x06010006对象已映射到 RxPDO
0x06020000对象不存在
0x06040041不可映射到 PDO
0x06040042超出 PDO 长度
0x06040043参数不兼容
0x06040047内部不兼容
0x06060000硬件访问错误
0x06070010数据类型不匹配
0x06070012数据类型长度过大
0x06070013数据类型长度过小
0x06090011子索引不存在
0x06090030值超出范围
0x06090031值过大
0x06090032值过小
0x06090033模块列表不匹配
0x06090036最大值小于最小值
0x08000000一般错误
0x08000020数据传输错误
0x08000021本地控制错误
0x08000022设备状态错误
0x08000023字典错误
0xFFFFFFFF未知错误

完整示例

#define DYNAMIC_LOAD
#include "ethercat.h"
#include "ethercat_advanced.h"
#include <stdio.h>

int main(void)
{
dll_t dll;
LOAD_DLL(&dll, "DarraEtherCAT.dll");
uint16_t master = dll.Initialize();
dll.SetNetwork(master, "\\Device\\NPF_{...}", "");
dll.SetStateSequence(master, EC_STATE_OPERATIONAL, 10000);
dll.Start(master);

/* 类型化读取 StatusWord */
uint16_t sw;
if (sdo_read_u16(&dll, master, 1, 0x6041, 0, &sw) == 0) {
printf("StatusWord: 0x%04X\n", sw);
}

/* 遍历对象字典 */
od_list_t* od = od_load(&dll, master, 1, TRUE);
if (od) {
for (int i = 0; i < od->count; i++) {
printf("0x%04X: %s\n", od->objects[i].index, od->objects[i].name);
}
od_free(od);
}

/* 检查 EMCY */
int emcy_count = dll.EmcyGetCount(master, 1);
printf("EMCY 消息数: %d\n", emcy_count);

dll.Stop(master);
dll.Dispose(master);
UNLOAD_DLL(&dll);
return 0;
}