跳到主要内容

CoE (CANopen over EtherCAT)

通过 slave.GetCoE() 获取 CoE& 引用。从站不支持 CoE 时相关方法返回空值或 false。

快速开始

auto& coe = master.GetSlave(1).GetCoE();

// 读取 SDO (原始字节)
auto data = coe.SDORead(0x6041, 0);
if (!data.empty()) {
uint16_t status_word;
std::memcpy(&status_word, data.data(), 2);
printf("StatusWord: 0x%04X\n", status_word);
}

// 写入 SDO (原始字节)
uint16_t control_word = 0x0006;
coe.SDOWrite(0x6040, 0,
reinterpret_cast<const uint8_t*>(&control_word),
sizeof(control_word));

SDO 读写

SDORead()

std::vector<uint8_t> SDORead(uint16_t index, uint8_t subindex = 0,
bool completeAccess = false) const;

读取 SDO 原始字节数据。失败返回空 vector。

SDOWrite()

bool SDOWrite(uint16_t index, uint8_t subindex,
const uint8_t* data, int dataLen,
bool completeAccess = false) const;

bool SDOWrite(uint16_t index, uint8_t subindex,
const std::vector<uint8_t>& data,
bool completeAccess = false) const;

写入 SDO 数据。支持原始指针和 std::vector 两种方式。

类型化 SDO 读取

所有类型化读取方法返回 std::optional,失败时为 std::nullopt

auto& coe = master.GetSlave(1).GetCoE();

// 读取各类型
auto sw = coe.SDOReadU16(0x6041, 0); // std::optional<uint16_t>
auto pos = coe.SDOReadI32(0x6064, 0); // std::optional<int32_t>
auto vid = coe.SDOReadU32(0x1018, 1); // std::optional<uint32_t>
auto temp = coe.SDOReadFloat(0x6078, 0); // std::optional<float>
auto name = coe.SDOReadString(0x1008, 0); // std::optional<std::string>

if (sw) printf("StatusWord: 0x%04X\n", *sw);
if (pos) printf("位置: %d\n", *pos);
方法返回类型说明
SDOReadU8(index, sub)std::optional<uint8_t>无符号 8 位
SDOReadU16(index, sub)std::optional<uint16_t>无符号 16 位
SDOReadU32(index, sub)std::optional<uint32_t>无符号 32 位
SDOReadI16(index, sub)std::optional<int16_t>有符号 16 位
SDOReadI32(index, sub)std::optional<int32_t>有符号 32 位
SDOReadFloat(index, sub)std::optional<float>32 位浮点
SDOReadString(index, sub)std::optional<std::string>字符串

类型化 SDO 写入

auto& coe = master.GetSlave(1).GetCoE();

// 类型化写入
coe.SDOWriteU16(0x6040, 0, 0x0006); // ControlWord
coe.SDOWriteU32(0x607A, 0, 100000); // TargetPosition (uint32)
coe.SDOWriteI32(0x607A, 0, 100000); // TargetPosition (int32)
coe.SDOWriteU8(0x6060, 0, 8); // 操作模式 CSP
方法参数类型说明
SDOWriteU8(index, sub, val)uint8_t无符号 8 位
SDOWriteU16(index, sub, val)uint16_t无符号 16 位
SDOWriteU32(index, sub, val)uint32_t无符号 32 位
SDOWriteI16(index, sub, val)int16_t有符号 16 位
SDOWriteI32(index, sub, val)int32_t有符号 32 位

类型化 SDO 补充

以下类型化方法同样可用:

方法返回/参数类型说明
SDOReadU64(index, sub)std::optional<uint64_t>无符号 64 位
SDOReadDouble(index, sub)std::optional<double>64 位浮点
SDOWriteU64(index, sub, val)uint64_t无符号 64 位
SDOWriteFloat(index, sub, val)float32 位浮点
SDOWriteDouble(index, sub, val)double64 位浮点

属性

属性类型访问说明
LastSdoError()SdoError只读最后一次 SDO 操作的错误码

LastSdoError()

SdoError LastSdoError() const;

最后一次 SDO 操作的错误码。当 SDORead() / SDOWrite() 失败时,可通过此方法获取 SDO 中止码 (Abort Code)。成功操作后自动重置为 SdoError::NoError

缓存语义:

CoE 实例内部维护 last_sdo_error_ 成员(声明为 mutable,对调用方仍是 const 接口):

  • SDORead() / SDOWrite() / SDOReadXxx() / SDOWriteXxx() 等所有 SDO 路径在返回前都会更新该缓存
  • 成功 → 缓存被清为 SdoError::NoError
  • 失败 → 缓存被设为底层映射的 SDO Abort Code(没有 Abort Code 时退化为 SdoError::GeneralError
  • 缓存按 每个 CoE 实例 隔离(即每个从站独立),不会被其他从站的 SDO 失败覆盖
  • 线程安全:调用方只读,SDK 内部以原子写入更新;高并发场景建议在每次 SDO 调用后立即读取缓存,避免被同实例上的下一次 SDO 调用覆盖

示例:

auto data = coe.SDORead(0x6041, 0);
if (data.empty()) {
auto error = coe.LastSdoError();
printf("SDO 读取失败: 0x%08X\n", static_cast<uint32_t>(error));
}

SdoError 枚举

SDO 操作错误代码(CANopen 标准 / ETG.1020)。

名称说明
NoError0x00000000无错误
ToggleBitNotChanged0x05030000Toggle 位未改变
SDOProtocolTimeout0x05040000SDO 协议超时
InvalidCommandSpecifier0x05040001无效的命令标识符
OutOfMemory0x05040005内存不足
UnsupportedAccess0x06010000不支持的访问
ReadWriteOnlyError0x06010001尝试读取只写对象
WriteReadOnlyError0x06010002尝试写入只读对象
SubindexWriteError0x06010003子索引不允许写入
CompleteAccessNotSupported0x06010004不支持完全访问
ObjectLengthExceeded0x06010005对象长度超限
ObjectMappedToRxPDO0x06010006对象已映射到 RxPDO
ObjectDoesNotExist0x06020000对象不存在
CannotBeMappedToPDO0x06040041不可映射到 PDO
ExceedsPDOLength0x06040042超出 PDO 长度
ParameterIncompatibility0x06040043参数不兼容
InternalIncompatibility0x06040047内部不兼容
HardwareAccessError0x06060000硬件访问错误
DataTypeMismatch0x06070010数据类型不匹配
DataTypeTooHigh0x06070012数据类型长度过大
DataTypeTooLow0x06070013数据类型长度过小
SubindexDoesNotExist0x06090011子索引不存在
ValueRangeExceeded0x06090030值超出范围
ValueTooHigh0x06090031值过大
ValueTooLow0x06090032值过小
ModuleIdentListMismatch0x06090033模块列表不匹配
MaxLessThanMin0x06090036最大值小于最小值
GeneralError0x08000000一般错误
DataTransferError0x08000020数据传输错误
LocalControlError0x08000021本地控制错误
DeviceStateError0x08000022设备状态错误
DictionaryError0x08000023字典错误
UnknownError0xFFFFFFFF未知错误

对象字典

auto& coe = master.GetSlave(1).GetCoE();

// 加载对象字典列表(返回 EcODList 指针,失败返回 nullptr)
EcODList* odList = coe.LoadODList();
if (odList) {
printf("对象数量: %d\n", odList->Count());
}

// 异步加载
auto future = coe.LoadODListAsync();
// ... 稍后获取结果 ...
EcODList* result = future.get();

EcODList 字段与方法:

方法返回类型说明
Count()int对象数量
Get(uint16_t idx)const ObjectDictionary*按索引获取
Get(const std::string& key)const ObjectDictionary*按名称获取
GetByPosition(int pos)const ObjectDictionary*按位置获取
ContainsKey(uint16_t idx)bool检查索引是否存在
ContainsKey(const std::string& key)bool检查名称是否存在
Copy()EcODList创建副本

支持 range-based for 循环遍历:

auto* odList = coe.LoadODList();
if (odList) {
for (auto& od : *odList) {
printf("0x%04X %s (%d 条目)\n", od.index, od.name.c_str(), od.Count());
}
}

ObjectDictionary 字段与方法:

字段/方法类型说明
indexuint16_t对象索引
namestd::string对象名称
datatypeuint16_t数据类型
object_codeuint8_t对象代码
max_subuint8_t最大子索引
entriesstd::vector<ObjectEntry>子索引条目
Count()int子索引数量
IsVar()bool是否为 VAR 类型
IsArray()bool是否为 ARRAY 类型
IsRecord()bool是否为 RECORD 类型
Get(uint8_t sub)const ObjectEntry*按子索引获取条目
ContainsKey(uint8_t sub)bool检查子索引是否存在

ObjectEntry 字段与方法:

字段/方法类型说明
od_indexuint16_t父对象索引
sub_indexuint8_t子索引
namestd::string条目名称
data_typeuint16_t数据类型
bit_lengthuint16_t位长度
obj_accessuint16_t访问权限
ByteLength()int字节长度
CanRead()bool是否可读
CanWrite()bool是否可写
IsReadOnly()bool是否只读
AccessDescription()std::string访问权限中文描述

批量 SDO 读取

ReadMultiple()

std::vector<SDOReadResult> ReadMultiple(const std::vector<SDOReadEntry>& entries) const;

批量 SDO 读取 — 一次调用读取多个对象。同步版本,依次读取所有条目。

参数:

  • entries — 要读取的 (index, subindex) 列表

返回值:

struct SDOReadEntry {
uint16_t index; // 对象索引
uint8_t subindex; // 子索引
};

struct SDOReadResult {
uint16_t index; // 对象索引
uint8_t subindex; // 子索引
bool success; // 是否成功
std::vector<uint8_t> data; // 读取的数据 (失败时为空)
};

示例:

auto& coe = master.GetSlave(1).GetCoE();

// 批量读取多个对象
std::vector<CoE::SDOReadEntry> entries = {
{0x1018, 1}, // VendorID
{0x1018, 2}, // ProductCode
{0x6041, 0}, // StatusWord
{0x6064, 0}, // ActualPosition
};

auto results = coe.ReadMultiple(entries);
for (auto& r : results) {
if (r.success)
printf("0x%04X:%02X 读取成功 (%zu 字节)\n", r.index, r.subindex, r.data.size());
else
printf("0x%04X:%02X 读取失败\n", r.index, r.subindex);
}

ReadMultipleAsync()

std::future<std::vector<SDOReadResult>> ReadMultipleAsync(
const std::vector<SDOReadEntry>& entries) const;

批量 SDO 读取的异步版本,在后台线程中执行,不阻塞调用线程。

示例:

auto future = coe.ReadMultipleAsync(entries);
// ... 执行其他操作 ...
auto results = future.get(); // 等待完成

诊断消息 (ETG.1510)

auto messages = coe.ReadDiagnosticMessages();
for (auto& msg : messages) {
printf("诊断码: 0x%08X, 标志: 0x%04X\n", msg.diag_code, msg.flags);
}

设备协议检测

uint16_t profile = coe.GetDeviceProfile();  // 读取 0x1000 低 16 位
bool is402 = coe.IsCiA402(); // 是否为 CiA 402 伺服
bool is401 = coe.IsCiA401(); // 是否为 CiA 401 I/O

紧急消息历史

C++ wrapper 提供两套接口读取 EMCY:

  • CoE::GetEmergencyHistory() / ClearEmergencyHistory() — 通过 SDO 读写 0x1003 (CANopen Pre-defined Error Field), 按子索引逐条取得从站本地缓存的历史错误帧.
  • darra::ethercat::slave_emcy 命名空间下的自由函数 — 读取 DLL 侧实时捕获的 EMCY 事件缓存 (dll.EmcyGetCount / EmcyGetHistory / EmcyClearHistory), 不触发 SDO.

GetEmergencyHistory()

std::vector<std::vector<uint8_t>> GetEmergencyHistory() const;

SDO 读取 0x1003. 返回的每一项为一帧原始 EMCY 字节 (按 ETG/CiA DS301 §7.2.8.1 定义, 至少包含 2 字节 Error Code + 1 字节 Error Register + 5 字节 Manufacturer-specific).

ClearEmergencyHistory()

bool ClearEmergencyHistory() const;

写入 0x1003:00 = 0 清空从站历史错误区. 成功返回 true.

slave_emcy 自由函数

namespace darra::ethercat::slave_emcy {
int emcy_count(dll_t& dll, uint16_t mi, uint16_t si);
std::vector<ec_emcy_record_t>
emcy_history(dll_t& dll, uint16_t mi, uint16_t si);
void clear_emcy(dll_t& dll, uint16_t mi, uint16_t si);
}

读取 DLL 侧实时 EMCY 环形缓冲, 返回 ec_emcy_record_t 数组 (时间戳 + 错误码 + 错误寄存器 + 厂商数据). 不会触发额外 SDO 流量.

示例 (SDO 0x1003 方式):

auto& coe = master.GetSlave(1).GetCoE();
auto history = coe.GetEmergencyHistory();
printf("0x1003 历史条目: %zu\n", history.size());
for (auto& frame : history) {
if (frame.size() >= 3) {
uint16_t err_code;
std::memcpy(&err_code, frame.data(), 2);
uint8_t err_reg = frame[2];
printf("EMCY 错误码=0x%04X 寄存器=0x%02X\n", err_code, err_reg);
}
}
coe.ClearEmergencyHistory();

示例 (实时 EMCY 缓存方式):

using namespace darra::ethercat;
int n = slave_emcy::emcy_count(dll, mi, si);
auto records = slave_emcy::emcy_history(dll, mi, si);
for (auto& r : records) {
printf("EMCY t=%u ms slave=%u code=0x%04X reg=0x%02X\n",
r.timestamp_ms, r.slave_index, r.error_code, r.error_register);
}
slave_emcy::clear_emcy(dll, mi, si);

EcDataType 枚举

CoE 对象字典中使用的 EtherCAT 数据类型标识符。

名称C++ 类型说明
BOOLEAN0x0001bool布尔
INTEGER80x0002int8_t有符号 8 位
INTEGER160x0003int16_t有符号 16 位
INTEGER320x0004int32_t有符号 32 位
UNSIGNED80x0005uint8_t无符号 8 位
UNSIGNED160x0006uint16_t无符号 16 位
UNSIGNED320x0007uint32_t无符号 32 位
REAL320x0008float单精度浮点
VISIBLE_STRING0x0009std::string可见字符串
OCTET_STRING0x000Astd::vector<uint8_t>字节数组
INTEGER640x0015int64_t有符号 64 位
UNSIGNED640x001Buint64_t无符号 64 位
REAL640x0011double双精度浮点

对象字典遍历

auto* odList = coe.LoadODList();
if (odList) {
for (auto& od : *odList) {
printf("0x%04X %s (子索引数=%d)\n", od.index, od.name.c_str(), od.Count());
for (auto& entry : od.entries) {
printf(" [%d] %s (类型=0x%04X, %d位, %s)\n",
entry.sub_index, entry.name.c_str(),
entry.data_type, entry.bit_length,
entry.AccessDescription().c_str());
}
}
}

完整示例

#include "ethercat.hpp"
using namespace darra;

int main() {
EtherCATMaster master(dll);
master.SetNetwork("\\Device\\NPF_{...}");
master.Build();
master.SetState(EcState::OP);
master.Start();

auto& coe = master.GetSlave(1).GetCoE();

// 读取 Identity Object
auto vid = coe.SDOReadU32(0x1018, 1);
auto pid = coe.SDOReadU32(0x1018, 2);
if (vid && pid)
printf("VID=0x%08X PID=0x%08X\n", *vid, *pid);

// 读取 StatusWord
auto sw = coe.SDOReadU16(0x6041, 0);
if (sw) printf("StatusWord: 0x%04X\n", *sw);

// 写入 ControlWord
coe.SDOWriteU16(0x6040, 0, 0x0006);

// 设置操作模式为 CSP
coe.SDOWriteU8(0x6060, 0, 8);

// 完全访问模式读取 PDO 映射
auto pdo_map = coe.SDORead(0x1C12, 0, true);
if (!pdo_map.empty())
printf("PDO 映射数据: %zu 字节\n", pdo_map.size());

// 读取设备名称
auto name = coe.SDOReadString(0x1008, 0);
if (name) printf("设备名称: %s\n", name->c_str());

return 0;
}

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

从站在 0x10F3 对象下维护一个诊断消息环形缓冲区, 记录硬件故障、通信警告、参数超限等事件. C++ wrapper 通过一次调用 ReadDiagnosticMessages() 拉取当前缓冲区内最多 20 条消息 (SDO 逐子索引读取). 如需细粒度的"是否有新消息"轻量轮询或逐条确认接口, 请直接使用 SDORead(0x10F3, ...) 按 ETG.1020 自行封装, 或改用 C# / Rust / Python 绑定的 PollHasNewDiagnostic / ReadDiagnosticMeta / AcknowledgeDiagnostic 高层 API.

ReadDiagnosticMessages()

std::vector<DiagnosticMessage> ReadDiagnosticMessages() const;

读取 0x10F3 诊断历史, 返回 DiagnosticMessage 列表 (最多 20 条). 从站不支持或尚无消息时返回空 vector.

相关结构:

struct DiagnosticMessage {
uint8_t sub_index; // 子索引
uint32_t diag_code; // 诊断码 (ETG.1020 Table 65)
uint16_t flags; // 标志位
uint16_t text_index; // 文本索引
std::vector<uint8_t> raw_data; // 原始字节 (含上述头 + 参数)
};

示例:

auto& coe = master.GetSlave(1).GetCoE();
auto messages = coe.ReadDiagnosticMessages();
for (auto& msg : messages) {
printf("[Sub=%u] 诊断码: 0x%08X, Flags: 0x%04X, TextIdx: 0x%04X\n",
msg.sub_index, msg.diag_code, msg.flags, msg.text_index);
}
标准依据

ETG.1020 §16 诊断历史对象; CANopen CiA 301 DS301 §7.5.3.

0x10F3 诊断历史 (轻量轮询 + 逐条确认)

ReadDiagnosticMessages() 一次性把 0x10F3 子索引拉一遍, 适合"导出全部诊断"的离线场景. 实时监控如果想避免反复读取, 应使用 ETG.1020 §16 的轻量轮询接口: 先 PollHasNewDiagnostic() 看有没有新消息, 有再读 ReadDiagnosticMeta() 拿元数据 (newest 编号 / overrun 标志), 按编号 ReadDiagnosticMessage() 取一条, 处理完写 AcknowledgeDiagnostic() 通知从站删除.

四个 API 通过 LOAD_FUNC 动态绑定 DLL 导出 (coe_diag_poll_new_available / coe_diag_read_meta / coe_diag_read_message / coe_diag_acknowledge), 首次调用缓存. DLL 没导出时所有调用安静返回 false / nullopt / -1, 不抛异常. 失败原因可读 LastDiagAbortCode() 拿 SDO Abort Code.

DiagMeta 结构

struct DiagMeta {
uint8_t maxMessages; // 0x10F3:01 缓冲区最大消息数
uint8_t newestMessage; // 0x10F3:02 最新消息编号 (环形写入下一个位置)
uint8_t newestAcknowledged; // 0x10F3:03 最后一个已确认编号
uint16_t flags; // 0x10F3:05 Bit4=RingBuffer, Bit5=Overrun

bool isRingBuffer() const; // Bit4
bool hasOverrun() const; // Bit5 (从消息超过缓冲容量, 旧消息已被覆盖)
};

PollHasNewDiagnostic()

int PollHasNewDiagnostic();

轻量轮询 — 不消耗邮箱, 只查询 newestMessage > newestAcknowledged. 适合周期性检查.

返回值:

  • >0 — 有新诊断消息
  • 0 — 无新消息
  • <0 — DLL 未导出或通信失败

ReadDiagnosticMeta()

std::optional<DiagMeta> ReadDiagnosticMeta();

读取 0x10F3:01..05 元数据. 失败返回 std::nullopt.

ReadDiagnosticMessage()

std::vector<uint8_t> ReadDiagnosticMessage(uint8_t msgIdx, size_t bufSize = 1024);

读取指定编号的诊断消息字节 (msgIdx 通常 6..255). bufSize 默认 1024, ETG.1020 推荐. 失败返回空 vector, 具体错误见 LastDiagAbortCode().

AcknowledgeDiagnostic()

bool AcknowledgeDiagnostic(uint8_t msgIdx);

msgIdx 写到 0x10F3:03, 通知从站可以从环形缓冲清除该条及之前的消息.

LastDiagAbortCode()

uint32_t LastDiagAbortCode() const;

最近一次 4 个诊断 API 的 SDO Abort Code, 0 表示无错误.

端到端示例

auto& coe = master.GetSlave(1).GetCoE();

while (running) {
int n = coe.PollHasNewDiagnostic();
if (n <= 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
continue;
}

auto meta = coe.ReadDiagnosticMeta();
if (!meta) {
printf("读元数据失败, AbortCode=0x%08X\n", coe.LastDiagAbortCode());
continue;
}

if (meta->hasOverrun()) {
printf("[警告] 诊断历史溢出, 部分旧消息丢失\n");
}

// 从最后已确认的下一条开始, 读到最新
for (uint8_t i = static_cast<uint8_t>(meta->newestAcknowledged + 1);
i != static_cast<uint8_t>(meta->newestMessage + 1); ++i) {
auto bytes = coe.ReadDiagnosticMessage(i);
if (bytes.size() < 8) continue;

uint32_t diagCode;
uint16_t flags, textIdx;
std::memcpy(&diagCode, bytes.data(), 4);
std::memcpy(&flags, bytes.data() + 4, 2);
std::memcpy(&textIdx, bytes.data() + 6, 2);

printf("Diag[%u] code=0x%08X flags=0x%04X textIdx=0x%04X\n",
i, diagCode, flags, textIdx);

coe.AcknowledgeDiagnostic(i);
}
}
与 ReadDiagnosticMessages() 的取舍
  • ReadDiagnosticMessages() — 一次拉满最多 20 条, 不区分新旧, 适合离线导出
  • 4 个轻量 API — 增量轮询 + 逐条确认, 适合 7×24 实时监控, 避免重复读取与缓冲溢出
  • 不要混用. 一旦走轻量 API, 由应用维护 newestAcknowledged 进度, 不要再调 ReadDiagnosticMessages() 干扰从站环形缓冲
参考

ETG.1020 §16 Table 65; ETG.1510 §6.2 Diagnosis history.