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) | float | 32 位浮点 |
| SDOWriteDouble(index, sub, val) | double | 64 位浮点 |
属性
| 属性 | 类型 | 访问 | 说明 |
|---|---|---|---|
| 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)。
| 名称 | 值 | 说明 |
|---|---|---|
| NoError | 0x00000000 | 无错误 |
| ToggleBitNotChanged | 0x05030000 | Toggle 位未改变 |
| SDOProtocolTimeout | 0x05040000 | SDO 协议超时 |
| InvalidCommandSpecifier | 0x05040001 | 无效的命令标识符 |
| OutOfMemory | 0x05040005 | 内存不足 |
| UnsupportedAccess | 0x06010000 | 不支持的访问 |
| ReadWriteOnlyError | 0x06010001 | 尝试读取只写对象 |
| WriteReadOnlyError | 0x06010002 | 尝试写入只读对象 |
| SubindexWriteError | 0x06010003 | 子索引不允许写入 |
| CompleteAccessNotSupported | 0x06010004 | 不支持完全访问 |
| ObjectLengthExceeded | 0x06010005 | 对象长度超限 |
| ObjectMappedToRxPDO | 0x06010006 | 对象已映射到 RxPDO |
| ObjectDoesNotExist | 0x06020000 | 对象不存在 |
| CannotBeMappedToPDO | 0x06040041 | 不可映射到 PDO |
| ExceedsPDOLength | 0x06040042 | 超出 PDO 长度 |
| ParameterIncompatibility | 0x06040043 | 参数不兼容 |
| InternalIncompatibility | 0x06040047 | 内部不兼容 |
| HardwareAccessError | 0x06060000 | 硬件访问错误 |
| DataTypeMismatch | 0x06070010 | 数据类型不匹配 |
| DataTypeTooHigh | 0x06070012 | 数据类型长度过大 |
| DataTypeTooLow | 0x06070013 | 数据类型长度过小 |
| SubindexDoesNotExist | 0x06090011 | 子索引不存在 |
| ValueRangeExceeded | 0x06090030 | 值超出范围 |
| ValueTooHigh | 0x06090031 | 值过大 |
| ValueTooLow | 0x06090032 | 值过小 |
| ModuleIdentListMismatch | 0x06090033 | 模块列表不匹配 |
| MaxLessThanMin | 0x06090036 | 最大值小于最小值 |
| GeneralError | 0x08000000 | 一般错误 |
| DataTransferError | 0x08000020 | 数据传输错误 |
| LocalControlError | 0x08000021 | 本地控制错误 |
| DeviceStateError | 0x08000022 | 设备状态错误 |
| DictionaryError | 0x08000023 | 字典错误 |
| UnknownError | 0xFFFFFFFF | 未知错误 |
对象字典
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 字段与方法:
| 字段/方法 | 类型 | 说明 |
|---|---|---|
| index | uint16_t | 对象索引 |
| name | std::string | 对象名称 |
| datatype | uint16_t | 数据类型 |
| object_code | uint8_t | 对象代码 |
| max_sub | uint8_t | 最大子索引 |
| entries | std::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_index | uint16_t | 父对象索引 |
| sub_index | uint8_t | 子索引 |
| name | std::string | 条目名称 |
| data_type | uint16_t | 数据类型 |
| bit_length | uint16_t | 位长度 |
| obj_access | uint16_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++ 类型 | 说明 |
|---|---|---|---|
| BOOLEAN | 0x0001 | bool | 布尔 |
| INTEGER8 | 0x0002 | int8_t | 有符号 8 位 |
| INTEGER16 | 0x0003 | int16_t | 有符号 16 位 |
| INTEGER32 | 0x0004 | int32_t | 有符号 32 位 |
| UNSIGNED8 | 0x0005 | uint8_t | 无符号 8 位 |
| UNSIGNED16 | 0x0006 | uint16_t | 无符号 16 位 |
| UNSIGNED32 | 0x0007 | uint32_t | 无符号 32 位 |
| REAL32 | 0x0008 | float | 单精度浮点 |
| VISIBLE_STRING | 0x0009 | std::string | 可见字符串 |
| OCTET_STRING | 0x000A | std::vector<uint8_t> | 字节数组 |
| INTEGER64 | 0x0015 | int64_t | 有符号 64 位 |
| UNSIGNED64 | 0x001B | uint64_t | 无符号 64 位 |
| REAL64 | 0x0011 | double | 双精度浮点 |
对象字典遍历
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()— 一次拉满最多 20 条, 不区分新旧, 适合离线导出- 4 个轻量 API — 增量轮询 + 逐条确认, 适合 7×24 实时监控, 避免重复读取与缓冲溢出
- 不要混用. 一旦走轻量 API, 由应用维护
newestAcknowledged进度, 不要再调ReadDiagnosticMessages()干扰从站环形缓冲
ETG.1020 §16 Table 65; ETG.1510 §6.2 Diagnosis history.