CoE (CANopen over EtherCAT)
CoE 提供基于 CANopen 的 SDO(服务数据对象)访问,用于读写从站的对象字典参数。
通过 GetSlaveCoEDetails(master_index, slave) 返回值判断从站是否支持 CoE(非 0 表示支持)。
SDO 邮箱读写在 C SDK 中以 dx_sdo_* 前缀提供。动态加载模式经 dll.dx_sdo_read(...) 调用,静态链接模式直接调 dx_sdo_read(...)。
读 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);
}
- 需要区分"通信失败"与"从站拒绝"(如值超范围 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 读取
SDOReadMultiple 批量读取接口当前版本未实现,ethercat.h 中的 dll.SDOReadMultiple 字段保持为 NULL,调用无效。批量读取多个 SDO 条目请逐次调用 dx_sdo_read() / dx_sdo_read_ex()。结构体 ec_sdo_entry_t / ec_sdo_read_result_t 在 slave/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: 驱动器异步推送的 1 帧 8 字节错误码, 由 SDK 侧历史缓冲捕获
- Diagnosis History (0x10F3): 从站内部环形缓冲的结构化消息, 主动调用 SDO 读取
0x10F3 子索引布局
诊断历史对象按 ETG.1020 §16 定义子索引, 全部经标准 SDO 通道读写:
| 子索引 | 名称 | 含义 |
|---|---|---|
0x10F3:01 | MaxMessages | 缓冲区容量 (最多保留条数) |
0x10F3:02 | NewestMessage | 最新消息编号 |
0x10F3:03 | NewestAcknowledged | 已确认的最新编号 (写入即标记已读) |
0x10F3:04 | NewMessageAvailable | 是否有新消息 (布尔标志) |
0x10F3:05 | Flags | Bit4 表示覆盖模式 (0=Overwrite 1=AckMode) |
0x10F3:06..FF | Diagnosis 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 | 无错误 |
| 0x05030000 | Toggle 位未改变 |
| 0x05040000 | SDO 协议超时 |
| 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;
}