跳到主要内容

CoE (CANopen over EtherCAT)

通过 slave.CoE 访问。从站不支持 CoE 时为 null

快速开始

CoE 对象字典是三层嵌套结构,通过索引器层层访问:

slave.CoE                        → CoEInstance(对象字典集合)
└─ [0x6040] → ObjectDictionary(一个对象)
└─ [0] → ObjectEntry(一个子对象)
├─ .Value → 类型化读写(自动转换,推荐)
└─ .Bytes → 原始字节读写

示例:

// 读取 — 自动类型转换
ushort status = slave.CoE[0x6041][0].Value;
int position = slave.CoE[0x6064][0].Value;
string name = slave.CoE[0x1008][0].Value;

// 写入 — 需显式指定类型
slave.CoE[0x6040][0].Value = (ushort)0x0006;
slave.CoE[0x607A][0].Value = 100000;

// 按字节写入
slave.CoE[0x6040][0].Bytes = BitConverter.GetBytes((ushort)0x0006);

CoEInstance(第一层)

对象字典集合,首次访问时自动加载结构并缓存。

this[]

public ObjectDictionary this[ushort index] { get; }
public ObjectDictionary this[int index] { get; }
public ObjectDictionary this[string key] { get; }

按索引或名称获取 ObjectDictionary。

参数:

  • index (ushort/int) — 对象索引(int >= 0x1000 按对象索引,否则按位置)
  • key (string) — 十六进制字符串 "0x1000"、十进制字符串 "4096" 或对象名称 "Device Type"

示例:

var od = slave.CoE[0x6040];
var od = slave.CoE["Device Type"];

foreach (var item in slave.CoE)
Console.WriteLine($"0x{item.Key:X4}: {item.Value.Name}");

Count

public int Count { get; }

对象字典中的对象数量。

ContainsKey()

public bool ContainsKey(ushort index)
public bool ContainsKey(string key)

检查对象索引或名称是否存在。

IsODListLoading

public bool IsODListLoading { get; }

对象字典是否正在异步加载中。

LoadODListAsync()

public Task<bool> LoadODListAsync(CancellationToken ct = default)

异步加载对象字典结构,不阻塞调用线程。

行为:

  • 已加载成功 → 直接返回 true
  • 正在加载中 → 不重复启动,返回 false
  • 加载过程中 ODList 逐步填充,可边加载边遍历已加载部分
  • 支持通过 CancellationToken 取消,已加载的部分仍保留

示例:

// 异步加载,不阻塞主线程
bool loaded = await slave.CoE.LoadODListAsync();
if (loaded)
{
foreach (var item in slave.CoE)
Console.WriteLine($"0x{item.Key:X4}: {item.Value.Name}");
}

// 支持取消
var cts = new CancellationTokenSource();
var task = slave.CoE.LoadODListAsync(cts.Token);
cts.Cancel(); // 中途取消

Copy()

public EcODList Copy()

创建对象字典的缓存副本(结构 + 值的快照)。副本中的值为快照,不再实时读取。

ObjectDictionary(第二层)

代表一个对象字典对象(如 0x6040 控制字),包含若干子对象。

属性类型访问说明
Indexushort只读对象索引号
Namestring只读对象名称
Datatypeushort只读数据类型标识
Objectcodeushort只读对象代码(变量/数组/记录)
Countint只读子对象数量

this[]

public ObjectEntry this[byte subindex] { get; }
public ObjectEntry this[int subindex] { get; }
public ObjectEntry this[string key] { get; }

按子索引或名称获取 ObjectEntry。

OEList

public OEDictionary OEList { get; }

子对象集合,支持 foreach 遍历和索引器访问。

示例:

var od = slave.CoE[0x1018];
var vendorId = od[1].Value;
var productCode = od["Product Code"].Value;

foreach (var entry in od.OEList)
Console.WriteLine($" [{entry.Index}] {entry.Name}: {entry.Value}");

SDOWrite()

public bool SDOWrite(byte[] data, bool useCompleteAccess = false)
public bool SDOWrite(byte subIndex, byte[] data, bool useCompleteAccess = false)

对整个对象或指定子索引进行 SDO 写入。

ReadAll()

public Dictionary<byte, byte[]> ReadAll()

批量读取所有子索引的原始字节数据。读取失败的条目自动跳过。

ObjectEntry(第三层)

代表一个子对象(如 0x6040:00),是最终的数据读写节点。

属性类型访问说明
Indexbyte只读子索引
ODIndexushort只读父对象索引
Namestring只读名称
DataTypeEcDataType只读数据类型(决定 Value 的自动转换目标,见类型映射
BitLengthuint只读位长度
ObjAccessCoEObjectAccess只读访问权限标志
EcDataType 枚举

EtherCAT 数据类型枚举(基于 ETG.1000.6),决定 Value 属性的自动转换目标类型。

枚举值C# 类型说明
Boolean0x0001bool布尔
Integer80x0002sbyteint8
Integer160x0003shortint16
Integer320x0004intint32
Unsigned80x0005byteuint8
Unsigned160x0006ushortuint16
Unsigned320x0007uintuint32
Real320x0008float单精度浮点
VisibleString0x0009stringASCII 字符串
OctetString0x000Abyte[]字节串
UnicodeString0x000BstringUnicode 字符串
Real640x0011double双精度浮点
Integer640x0015longint64
Unsigned640x001Bulonguint64
TimeOfDay0x000CDateTime时间
TimeDifference0x000DTimeSpan时间差
Domain0x000Fbyte[]原始数据
CoEObjectAccess 枚举
[Flags]
public enum CoEObjectAccess : ushort
{
None = 0x00,
ReadPreOp = 0x01, // PreOp 可读
ReadSafeOp = 0x02, // SafeOp 可读
ReadOp = 0x04, // OP 可读
WritePreOp = 0x08, // PreOp 可写
WriteSafeOp = 0x10, // SafeOp 可写
WriteOp = 0x20, // OP 可写
ReadAny = ReadPreOp | ReadSafeOp | ReadOp,
WriteAny = WritePreOp | WriteSafeOp | WriteOp,
ReadWriteAll = ReadAny | WriteAny,
}

Value

public ValueWrapper Value { get; set; }

类型化读写。读取时根据 DataType 自动将字节转换为 C# 类型(见 EcDataType);写入时需显式指定目标类型,自动转换为字节写入。

支持隐式转换的 C# 类型:boolbytesbyteshortushortintuintlongulongfloatdoublestring

示例:

// 读取 — 根据 DataType 自动转换
ushort status = slave.CoE[0x6041][0].Value; // Unsigned16 → ushort
int position = slave.CoE[0x6064][0].Value; // Integer32 → int
string name = slave.CoE[0x1008][0].Value; // VisibleString → string

// 写入 — 需显式指定类型,自动转换为对应字节
slave.CoE[0x6040][0].Value = (ushort)0x0006; // ushort → 2字节
slave.CoE[0x607A][0].Value = 100000; // int → 4字节

Bytes

public byte[] Bytes { get; set; }

原始字节读写。

GetValue<T>()

public T GetValue<T>()

泛型读取,将值转换为指定类型。

SDOWrite()

public bool SDOWrite(byte[] data, bool useCompleteAccess = false)

对子对象进行 SDO 写入。

相关属性:

  • CanRead (bool) — 是否可读
  • CanWrite (bool) — 是否可写
  • IsReadOnly (bool) — 是否只读
  • CanWritePreOp (bool) — PreOp 状态下可写
  • CanWriteSafeOp (bool) — SafeOp 状态下可写
  • CanWriteOp (bool) — OP 状态下可写
  • AccessDescription (string) — 权限描述文字

示例:

var entry = slave.CoE[0x6040][0];
if (entry.CanWrite)
entry.Value = (ushort)0x0006;

LastSdoError

public SDOError LastSdoError { get; }

最近一次 SDO 操作的错误代码。每次 SDORead / SDOWrite 调用后自动更新。成功时为 SDOError.NoError,失败时为对应的 Abort Code。

示例:

var data = slave.CoE.SDORead(0x6040, 0);
if (data == null)
{
Console.WriteLine($"SDO 读取失败: {slave.CoE.LastSdoError}");
}

SDO 直接读写

SDORead()

public byte[] SDORead(ushort index, byte subindex, bool completeAccess = false)

读取 SDO 原始字节数据。

示例:

byte[] data = slave.CoE.SDORead(0x1000, 0);
uint typeValue = BitConverter.ToUInt32(data, 0);
便捷访问

通过对象字典可直接获取类型化的值,无需手动转换:

ushort statusWord = slave.CoE[0x6041][0].Value;

ReadMultipleAsync()

public async Task<Dictionary<(ushort index, byte subindex), byte[]>> ReadMultipleAsync(
IEnumerable<(ushort index, byte subindex)> entries,
CancellationToken cancellationToken = default)

批量 SDO 读取 — 一次调用读取多个对象。异步执行,支持取消。读取失败的条目值为 null

参数:

  • entries (IEnumerable<(ushort, byte)>) — 要读取的 (index, subindex) 列表
  • cancellationToken (CancellationToken) — 取消令牌

返回值:

  • Dictionary<(ushort, byte), byte[]> — 结果字典,key=(index, subindex),value=数据(失败为 null)

示例:

var entries = new[] {
((ushort)0x1018, (byte)1), // VendorID
((ushort)0x1018, (byte)2), // ProductCode
((ushort)0x6041, (byte)0), // StatusWord
};
var results = await slave.CoE.ReadMultipleAsync(entries, cancellationToken);
foreach (var (key, data) in results)
{
if (data != null)
Console.WriteLine($"0x{key.index:X4}:{key.subindex:X2} = {BitConverter.ToString(data)}");
}

相关结构:

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

public enum SDOError : uint
{
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 // 未知错误
}

EMCY 紧急消息

CoE 支持 CiA 301 紧急消息 (Emergency) 记录。每个从站的 CoE 实例自动收集该从站的 EMCY 消息。

MaxEmcyHistorySize

public int MaxEmcyHistorySize { get; set; }

EMCY 历史记录最大容量(默认 256)。超出容量时自动丢弃最旧的消息。最小值为 1。

GetEmergencyHistory()

public List<EmergencyMessage> GetEmergencyHistory()

获取该从站的 EMCY 紧急消息历史记录(返回副本)。最多保留最近 MaxEmcyHistorySize 条。

相关结构:

public struct EmergencyMessage
{
public ushort ErrorCode; // 紧急错误代码 (CiA 301 Table 24)
public byte ErrorRegister; // 错误寄存器 (对象 0x1001)
public byte[] Data; // 厂商特定数据 (5 字节)
public int SlaveIndex; // 来源从站编号
public DateTime Timestamp; // 接收时间

// 获取错误代码的文本描述 (CiA 301 标准错误分类)
public string GetErrorDescription();
}

GetErrorDescription() 根据 CiA 301 标准错误代码高字节返回分类描述,如 "电流错误"、"温度错误"、"通信错误" 等。

ClearEmergencyHistory()

public void ClearEmergencyHistory()

清除该从站的 EMCY 历史记录。

示例:

// 读取 EMCY 历史
var history = slave.CoE.GetEmergencyHistory();
foreach (var msg in history)
{
Console.WriteLine($"EMCY 0x{msg.ErrorCode:X4}: {msg.GetErrorDescription()} " +
$"寄存器=0x{msg.ErrorRegister:X2} 时间={msg.Timestamp:HH:mm:ss.fff}");
}

// 调整历史容量
slave.CoE.MaxEmcyHistorySize = 100;

// 清除历史
slave.CoE.ClearEmergencyHistory();

完整示例

遍历对象字典

foreach (var item in slave.CoE)
{
Console.WriteLine($"0x{item.Key:X4}: {item.Value.Name}");
foreach (var entry in item.Value.OEList)
Console.WriteLine($" [{entry.Index}] {entry.Name}: {entry.Value}");
}

诊断历史 0x10F3 (Diagnosis History)

CiA 301 / ETG.1020 §16 规定了对象 0x10F3 (Diagnosis History) 作为从站向主站报告运行期故障、事件、警告的标准通道。与 EMCY 的"瞬时紧急消息"不同, 0x10F3 是一段持久化的环形缓冲区, 存在从站里最近 N 条诊断记录, 主站可随时翻看 / 确认 / 清理。

典型应用:

  • 设备故障排查 — 从站温度过高、力矩过载、编码器断线等故障码完整保留
  • 从站自报告故障 — 硬件层自动生成诊断条目, 上位机无需主动轮询每个对象
  • 按 FIFO / Ring 语义消费 — 主站消费后 Acknowledge 让从站清理, 避免缓冲区溢出
与 EMCY 的区别
  • EMCY (0x80 CAN ID) — 事件到达瞬间触发, 不保证补发, 适合 RT 通知
  • 0x10F3 — 从站持久化, 宕机重连后仍能读到, 适合事后分析

PollHasNewDiagnostic()

public bool PollHasNewDiagnostic()

快速轮询 0x10F3:04 NewMessagesAvailable 标志, 不触发完整的 SDO 历史拉取, 适合高频应用层轮询。

返回值:

  • booltrue 表示有新消息, false 表示无消息或通信失败

ReadDiagnosticMeta()

public DiagMeta? ReadDiagnosticMeta()

读取诊断历史元数据, 包括最大容量、最新消息编号、已确认编号、标志位。

返回值:

  • DiagMeta? — 元数据对象, 通信失败返回 null

相关结构:

public class DiagMeta
{
public byte MaxMessages { get; } // 0x10F3:01 从站支持的最大消息数 (通常 <= 250)
public byte NewestMessage { get; } // 0x10F3:02 最新消息所在 subindex
public byte NewestAcknowledged { get; } // 0x10F3:03 用户已确认的最新 subindex
public ushort Flags { get; } // 0x10F3:05 标志位
}

Flags 按位解读:

  • bit 4 — Ring/Linear 模式 (1=Ring, 0=Linear)
  • bit 5 — Overrun 标志 (1=缓冲区曾溢出丢帧)

AcknowledgeDiagnostic(byte subIndex)

public bool AcknowledgeDiagnostic(byte subIndex)

写入 0x10F3:03 NewestAcknowledged, 告诉从站"到 subIndex 为止的消息都已处理"。Ring 模式下从站按此清理可回收空间。

参数:

  • subIndex (byte) — 已处理的最后一个 subindex (有效范围 6..255)

返回值:

  • bool — 成功返回 true

DiagnosticMessage

public struct DiagnosticMessage
{
public byte SubIndex; // 0x10F3 子索引 (6..n)
public uint DiagCode; // 4 字节诊断编码 (厂商定义)
public ushort Flags; // 标志位 (严重度 / 类型)
public ushort TextIndex; // 文本索引 (对应 0x1000 字符串表)
public byte[] RawData; // 原始消息字节 (含 8 字节头 + 可变参数)
public DateTime Timestamp; // SDK 接收 / 轮询时刻 (UTC)
public int SlaveIndex; // 来源从站编号

public override string ToString();
}

ETG.1510 诊断消息结构。Flags 高字节 = 严重度 (0=Info, 1=Warning, 2=Error),低字节 = 类型;TextIndex 可在 0x1000 区域字符串表中查到中文化描述。RawData 保留原始字节供需要厂商扩展解析的场景。

示例:

foreach (var msg in slave.CoE.ReadDiagnosticMessages())
{
string severity = (msg.Flags & 0x00FF) switch
{
0 => "Info",
1 => "Warning",
2 => "Error",
_ => "Unknown"
};
Console.WriteLine(
$"[Slave {msg.SlaveIndex}] SI={msg.SubIndex} {severity} " +
$"Code=0x{msg.DiagCode:X8} TextIndex=0x{msg.TextIndex:X4} " +
$"@ {msg.Timestamp:HH:mm:ss.fff}");
}

完整示例 — 轮询并消费诊断历史

if (slave.CoE == null) return;

// 1. 订阅订阅事件 — EMCY 到达时 SDK 会自动拉取增量消息
slave.CoE.DiagnosticMessageReceived += (sender, e) =>
{
foreach (var msg in e.NewMessages)
{
Console.WriteLine(
$"[从站 {msg.SlaveIndex}] 诊断 SubIndex={msg.SubIndex} " +
$"Code=0x{msg.DiagCode:X8} Flags=0x{msg.Flags:X4} " +
$"Text=0x{msg.TextIndex:X4} @ {msg.Timestamp:HH:mm:ss.fff}");
}
};

// 2. 应用层定时轮询 (低频, 1s 一次即可)
while (running)
{
if (slave.CoE.PollHasNewDiagnostic())
{
var meta = slave.CoE.ReadDiagnosticMeta();
if (meta != null)
{
Console.WriteLine(
$"诊断元数据: 最大 {meta.MaxMessages} 条, " +
$"最新={meta.NewestMessage}, 已确认={meta.NewestAcknowledged}, " +
$"Ring={((meta.Flags & 0x10) != 0)}, Overrun={((meta.Flags & 0x20) != 0)}");

// 3. 增量拉取
var messages = slave.CoE.ReadDiagnosticMessages();

// 4. 处理并确认
if (meta.NewestMessage > meta.NewestAcknowledged)
{
slave.CoE.AcknowledgeDiagnostic(meta.NewestMessage);
}
}
}
Thread.Sleep(1000);
}

注意事项

  • 0x10F3可选对象, 从站若不实现则 ReadDiagnosticMeta() 返回 null, 应用层需做 fallback 到 EMCY 监听
  • Ring 模式下超出 MaxMessages 时最旧消息被覆盖, Overrun 位会被置 1
  • 首次读取建议一次性读完所有未确认消息再 Acknowledge, 避免遗漏
  • SDK 内部的 ReadDiagnosticMessages() 已做去重与本地缓存 (默认保留最近 256 条), 多次调用不会重复入库

SDO 扩展接口

SDOReadEx(ushort index, byte subindex, bool completeAccess = false)

public SdoReadResult SDOReadEx(ushort index, byte subindex, bool completeAccess = false)

SDO 读取扩展版 (CoE-H1): 显式返回 abort code + WKC. 直通 DLL SDOread_ex, 调用方可区分 "对象不存在 (0x06020000)" / "数据类型不匹配 (0x06070010)" / "值超范围 (0x06090030)" 等具体中止码.

参数:

  • index (ushort) — 对象索引
  • subindex (byte) — 子索引
  • completeAccess (bool) — Complete Access (ETG.1000.6 §5.6.4), 启用时 subindex 必须为 01

返回值:

  • SdoReadResult — 含 Ok / Data / AbortCode / Wkc 字段

相关结构:

public readonly struct SdoReadResult
{
public bool Ok { get; } // 操作是否成功
public byte[]? Data { get; } // 读到的数据 (失败时为 null)
public uint AbortCode { get; }// CoE Abort 码 (成功时为 0)
public int Wkc { get; } // 返回 WKC
}

示例:

var r = slave.CoE.SDOReadEx(0x6041, 0);
if (r.Ok)
ushort statusword = BitConverter.ToUInt16(r.Data!, 0);
else
Console.WriteLine($"读失败 abort=0x{r.AbortCode:X8} wkc={r.Wkc}");

SDOWriteEx(ushort index, byte subindex, byte[] data, bool completeAccess = false)

public SdoWriteResult SDOWriteEx(ushort index, byte subindex, byte[] data, bool completeAccess = false)

SDO 写入扩展版 (CoE-H6): 显式返回 abort code + WKC.

参数:

  • index (ushort) — 对象索引
  • subindex (byte) — 子索引
  • data (byte[]) — 写入数据 (不可为空)
  • completeAccess (bool) — Complete Access, 启用时 subindex 必须为 01

返回值:

  • SdoWriteResult — 含 Ok / AbortCode / Wkc 字段

示例:

var r = slave.CoE.SDOWriteEx(0x6040, 0, BitConverter.GetBytes((ushort)0x0F));
if (!r.Ok)
Console.WriteLine($"写失败 abort=0x{r.AbortCode:X8}");

OD (对象字典) 描述符查询

按需查询单个对象/子索引的元信息, 比 LoadODListAsync 强制加载整表轻量, 适合 ESI vs 实测对比 / 诊断 UI 单条查询.

GetObjectDictionaryIndices()

public ushort[] GetObjectDictionaryIndices()

读取对象索引清单 (ETG.1000.6 Table 34, SDO Info Service 0x01). 不读描述, 仅返回索引数组, 速度远快于 LoadODListAsync.

返回值:

  • ushort[] — 对象索引数组 (按从站返回顺序), 失败返回空数组

示例:

ushort[] indices = slave.CoE.GetObjectDictionaryIndices();
Console.WriteLine($"OD 共 {indices.Length} 项");

GetObjectDescription(ushort index)

public CoeObjectDescription? GetObjectDescription(ushort index)

读取单个对象的描述 (Table 35, SDO Info Service 0x03). 返回 DataType / MaxSubIndex / Name. 对象不存在或从站不支持 SDO Info 时返回 null.

参数:

  • index (ushort) — 对象索引

返回值:

  • CoeObjectDescription? — 对象描述

相关结构:

public class CoeObjectDescription
{
public ushort Index { get; set; }
public ushort DataType { get; set; }
public byte ObjectCode { get; set; }
public byte MaxSubIndex { get; set; }
public string Name { get; set; }
}

示例:

var desc = slave.CoE.GetObjectDescription(0x6041);
if (desc != null)
Console.WriteLine($"{desc.Name} type=0x{desc.DataType:X4} subs={desc.MaxSubIndex}");

GetEntryDescription(ushort index, byte subindex)

public CoeEntryDescription? GetEntryDescription(ushort index, byte subindex)

读取单个子索引的描述 (Table 36, SDO Info Service 0x05). 返回 DataType / BitLength / ObjectAccess / Name.

返回值:

  • CoeEntryDescription? — 子索引描述

相关结构:

public class CoeEntryDescription
{
public ushort Index { get; set; }
public byte SubIndex { get; set; }
public byte ValueInfo { get; set; }
public ushort DataType { get; set; }
public ushort BitLength { get; set; }
public CoEObjectAccess ObjectAccess { get; set; }
public string Name { get; set; }
}

示例:

var entry = slave.CoE.GetEntryDescription(0x1A00, 1);
if (entry != null)
Console.WriteLine($"sub1: {entry.Name}, bits={entry.BitLength}");

异常类型

CoE_AccessDeniedException

public class CoE_AccessDeniedException : Exception
{
public ushort Index { get; }
public byte SubIndex { get; }
public string EntryName { get; }
public bool IsReadOperation { get; }
public ushort ObjectAccess { get; }
}

CoE 对象访问被拒时由 SDO 写/读路径主动抛出. 通常意味着 PDO 映射对象在 OP 状态下被尝试改写, 或 ObjectAccess 标记禁止当前状态访问. 调用方可选择捕获后回退到 PreOp 重试.

示例:

try
{
slave.CoE.SDOwrite(0x1C12, 0, BitConverter.GetBytes((byte)0));
}
catch (CoE_AccessDeniedException ex)
{
Console.WriteLine($"对象 0x{ex.Index:X4}:{ex.SubIndex:X2} 当前不可写");
master.SetState(EcState.PreOp);
}