SoE (Servo over EtherCAT)
SoE 协议将 SERCOS 伺服通信协议封装在 EtherCAT 邮箱中,适用于 SERCOS 兼容的伺服驱动器。
通过 slave.SoE 访问。从站不支持 SoE 时为 null。
大多数 EtherCAT 伺服驱动器使用 CoE + CiA 402 协议栈。 SoE 仅用于 SERCOS 兼容 的驱动器(如 Bosch Rexroth IndraDrive 系列)。 两者不能混用——从站支持哪种取决于硬件。
SERCOS IDN 体系
SoE 通过 IDN(Identification Number) 寻址驱动器参数。每个 IDN 是一个 16 位编号,代表一个参数(位置、速度、控制字等)。
IDN 命名规则:
| 前缀 | 范围 | 说明 |
|---|---|---|
| S-x-xxxx | 0x0000 – 0x7FFF | SERCOS 标准参数(所有厂商通用) |
| P-x-xxxx | 0x8000 – 0xBFFF | 产品特定参数 |
| — | 0xC000 – 0xFFFF | 厂商自定义参数 |
常用标准 IDN(伺服控制相关):
| IDN | SERCOS 名 | 说明 | 数据类型 |
|---|---|---|---|
| S-0-0001 (0x0001) | Cycle Time | 通信周期时间 | uint, μs |
| S-0-0011 (0x000B) | Position Feedback 1 | 实际位置 | int, inc |
| S-0-0012 (0x000C) | Position Command | 目标位置 | int, inc |
| S-0-0013 (0x000D) | Velocity Feedback | 实际速度 | int, rpm |
| S-0-0014 (0x000E) | Velocity Command | 目标速度 | int, rpm |
| S-0-0016 (0x0010) | AT Config | 输入映射配置列表 | list |
| S-0-0019 (0x0013) | Drive Status | 驱动器状态字 | ushort |
| S-0-0024 (0x0018) | MDT Config | 输出映射配置列表 | list |
| S-0-0036 (0x0024) | Max Velocity | 最大速度限制 | uint, rpm |
| S-0-0040 (0x0028) | Homing Velocity | 回零速度 | uint, rpm |
| S-0-0064 (0x0040) | Drive Control Word | 驱动器控制字 | ushort |
| S-0-0071 (0x0047) | Drive Status Word | 驱动器状态字 | ushort |
元素标志(Element Flags)— 读取 IDN 的不同"层面":
| 标志 | 含义 |
|---|---|
| 0x01 | 数据状态 |
| 0x02 | 参数名称(字符串) |
| 0x04 | 属性(数据类型/长度/权限位域) |
| 0x08 | 单位(字符串) |
| 0x10 | 最小值 |
| 0x20 | 最大值 |
| 0x40 | 数据值(默认,最常用) |
| 0x80 | 默认值 |
基本读写
Read(ushort idn, byte elementFlags = 0x40, int timeoutMs = 500)
public byte[]? Read(ushort idn, byte elementFlags = 0x40, int timeoutMs = 500)
读取 IDN 参数原始字节。非可用状态自动切换。
参数:
idn(ushort) — IDN 编号elementFlags(byte) — 元素标志(默认 0x40 = 数据值)timeoutMs(int) — 超时时间(毫秒)
返回值:
byte[]?— 读取的数据,失败返回null
示例:
// 读取通信周期时间 (S-0-0001)
byte[]? data = slave.SoE.Read(0x0001, 0x40);
if (data != null)
{
uint cycleTime = BitConverter.ToUInt32(data, 0);
Console.WriteLine($"周期时间: {cycleTime} μs");
}
Write(ushort idn, byte[] data, byte elementFlags = 0, int timeoutMs = 500)
public bool Write(ushort idn, byte[] data, byte elementFlags = 0, int timeoutMs = 500)
写入 IDN 参数。非可用状态自动切换。
参数:
idn(ushort) — IDN 编号data(byte[]) — 要写入的数据elementFlags(byte) — 元素标志(默认 0 = 数据值)timeoutMs(int) — 超时时间(毫秒)
返回值:
bool— 成功返回true
示例:
// 设置目标位置 (S-0-0012)
byte[] position = BitConverter.GetBytes(100000);
bool success = slave.SoE.Write(0x000C, position);
类型化读取
所有方法签名一致:ReadXxx(ushort idn, byte elementFlags = 0x40, int timeoutMs = 500)
| 方法 | 返回类型 | 说明 |
|---|---|---|
| ReadInt16 | short? | 读取 16 位有符号整数 |
| ReadInt32 | int? | 读取 32 位有符号整数 |
| ReadUInt16 | ushort? | 读取 16 位无符号整数 |
| ReadUInt32 | uint? | 读取 32 位无符号整数 |
| ReadFloat | float? | 读取单精度浮点数 |
| ReadDouble | double? | 读取双精度浮点数 |
| ReadString | string? | 读取字符串 |
示例:
// 读取实际位置 (S-0-0011)
int? actualPos = slave.SoE.ReadInt32(0x000B);
// 读取驱动器状态字 (S-0-0071)
ushort? status = slave.SoE.ReadUInt16(0x0047);
// 读取参数名称
string? name = slave.SoE.ReadString(0x000B, elementFlags: 0x02);
类型化写入
所有方法签名一致:WriteXxx(ushort idn, T value, byte elementFlags = 0, int timeoutMs = 500),返回 bool。
| 方法 | 值类型 | 说明 |
|---|---|---|
| WriteInt16 | short | 写入 16 位有符号整数 |
| WriteInt32 | int | 写入 32 位有符号整数 |
| WriteUInt16 | ushort | 写入 16 位无符号整数 |
| WriteUInt32 | uint | 写入 32 位无符号整数 |
示例:
// 设置目标位置 (S-0-0012)
slave.SoE.WriteInt32(0x000C, 50000);
// 写控制字 (S-0-0064)
slave.SoE.WriteUInt16(0x0040, 0x0006);
元数据读取
ReadName(ushort idn, int timeoutMs = 500)
public string? ReadName(ushort idn, int timeoutMs = 500)
读取 IDN 参数名称(内部使用 ElementFlag 0x02)。
ReadUnit(ushort idn, int timeoutMs = 500)
public string? ReadUnit(ushort idn, int timeoutMs = 500)
读取 IDN 参数单位(内部使用 ElementFlag 0x08)。
ReadMinValue / ReadMaxValue / ReadDefaultValue
public byte[]? ReadMinValue(ushort idn, int timeoutMs = 500)
public byte[]? ReadMaxValue(ushort idn, int timeoutMs = 500)
public byte[]? ReadDefaultValue(ushort idn, int timeoutMs = 500)
读取 IDN 参数的最小值(0x10)、最大值(0x20)、默认值(0x80)。
示例:
string? name = slave.SoE.ReadName(0x000B);
string? unit = slave.SoE.ReadUnit(0x000B);
byte[]? min = slave.SoE.ReadMinValue(0x000B);
byte[]? max = slave.SoE.ReadMaxValue(0x000B);
Console.WriteLine($"参数: {name} ({unit})");
if (min != null && max != null)
Console.WriteLine($"范围: {BitConverter.ToInt32(min, 0)} - {BitConverter.ToInt32(max, 0)}");
参数信息
GetAvailableIDNs(int timeoutMs = 5000)
public List<ushort> GetAvailableIDNs(int timeoutMs = 5000)
获取从站支持的所有 IDN 列表。
返回值:
List<ushort>— IDN 编号列表
GetParameterInfo(ushort idn, int timeoutMs = 5000)
public SoEParameter GetParameterInfo(ushort idn, int timeoutMs = 5000)
获取 IDN 参数的完整信息(名称、单位、属性、当前值、最小/最大值等)。
返回值:
SoEParameter— 包含完整的参数信息
相关结构:
public class SoEParameter
{
public ushort IDN { get; } // IDN 编号
public string Name { get; } // 参数名称
public string Unit { get; } // 单位
public SoEAttributes Attributes { get; } // 属性(数据类型、长度等)
public byte[] Value { get; } // 当前值
public byte[] DefaultValue { get; } // 默认值
public byte[] MinValue { get; } // 最小值
public byte[] MaxValue { get; } // 最大值
public string SercosIDN { get; } // SERCOS IDN 格式(如 "S-0-0001")
public string Category { get; } // 类别("Standard" / "Product" / "Vendor")
public bool IsReadOnly { get; } // 是否只读
public string AccessMode { get; } // 访问权限("RO" / "RW" / "RW*")
public string FormattedValue { get; } // 格式化的当前值
public string FormattedMinValue { get; } // 格式化的最小值
public string FormattedMaxValue { get; } // 格式化的最大值
}
public struct SoEAttributes
{
public ushort EvaluationFactor; // 评价因子
public byte Length; // 数据长度
public bool IsList; // 是否为列表
public bool IsCommand; // 是否为命令
public SoEDataType DataType; // 数据类型
public byte Decimals; // 小数位数
public bool WriteProtectedPreOp; // PreOp 状态写保护
public bool WriteProtectedSafeOp; // SafeOp 状态写保护
public bool WriteProtectedOp; // Op 状态写保护
}
public enum SoEDataType
{
Binary = 0, // 二进制
UInt = 1, // 无符号整数
Int = 2, // 有符号整数
Hexadecimal = 3, // 十六进制
String = 4, // 字符串
IDN = 5, // IDN 引用
Float = 6, // 浮点数
Parameter = 7 // 参数
}
示例:
var param = slave.SoE.GetParameterInfo(0x000B);
Console.WriteLine($"IDN: {param.SercosIDN} - {param.Name}");
Console.WriteLine($"单位: {param.Unit}, 访问: {param.AccessMode}");
Console.WriteLine($"当前值: {param.FormattedValue}");
Console.WriteLine($"范围: {param.FormattedMinValue} - {param.FormattedMaxValue}");
AT/MDT 映射
SERCOS 使用 AT(Acknowledge Telegram)和 MDT(Master Data Telegram)传输周期数据:
- AT = 从站→主站的周期输入(实际位置、实际速度、状态字…)
- MDT = 主站→从站的周期输出(目标位置、目标速度、控制字…)
映射配置决定了哪些 IDN 参数被包含在周期数据帧中。
GetIDNMapping(int timeoutMs = 5000)
public SoEMappingInfo GetIDNMapping(int timeoutMs = 5000)
读取当前驱动器的 AT/MDT 映射配置。
返回值:
SoEMappingInfo— 映射信息对象
相关结构:
public class SoEMappingInfo
{
public List<SoEMappingEntry> ATMapping { get; } // AT(输入)映射列表
public List<SoEMappingEntry> MDTMapping { get; } // MDT(输出)映射列表
public int ATBitSize { get; } // AT 总位数
public int MDTBitSize { get; } // MDT 总位数
public int ATByteSize { get; } // AT 总字节数
public int MDTByteSize { get; } // MDT 总字节数
}
public class SoEMappingEntry
{
public ushort IDN { get; } // IDN 编号
public string Name { get; } // 参数名称
public int BitLength { get; } // 数据长度(位)
public int ByteLength { get; } // 数据长度(字节)
}
GetAllDriveMappings(int timeoutMs = 5000)
public Dictionary<byte, SoEMappingInfo> GetAllDriveMappings(int timeoutMs = 5000)
获取所有驱动器的映射信息(最多扫描 8 个驱动器)。
返回值:
Dictionary<byte, SoEMappingInfo>— 驱动器编号到映射信息的字典
参数变化通知
SoE Notification 功能允许监控 SERCOS 从站 IDN 参数的变化。当参数值发生改变时,通过事件通知应用程序。
实现机制:
- 硬件通知:通过写入 S-0-0127(通知请求 IDN)尝试启用从站的原生 SoE Notification(OpCode=5)
- 轮询检测:如果从站不支持硬件通知,自动回退到轮询模式,周期性读取参数值并与基线比较
NotificationReceived 事件
public event Action<SoENotificationEventArgs>? NotificationReceived;
当监控的 IDN 参数值发生变化时触发。
EnableNotification(ushort idn, int timeoutMs = 500)
public bool EnableNotification(ushort idn, int timeoutMs = 500)
启用指定 IDN 的参数变化通知。首先尝试通过 S-0-0127 启用硬件通知,无论是否成功都会将 IDN 加入轮询监控列表。
参数:
idn(ushort) — 要监控的 IDN 编号timeoutMs(int) — 超时时间(毫秒,默认 500)
返回值:
bool—true表示从站确认支持硬件通知;false表示回退到轮询模式
DisableNotification(ushort idn)
public void DisableNotification(ushort idn)
禁用指定 IDN 的参数变化通知,从监控列表中移除。
DisableAllNotifications()
public void DisableAllNotifications()
禁用所有已启用的通知,清空监控列表。
PollNotifications(int timeoutMs = 200)
public int PollNotifications(int timeoutMs = 200)
轮询检测所有已监控 IDN 的参数变化。逐个读取当前值并与上次记录值比较,发现变化时触发 NotificationReceived 事件。适合在定时器或后台线程中周期性调用。
参数:
timeoutMs(int) — 每个 IDN 读取的超时时间(毫秒,默认 200)
返回值:
int— 本次轮询检测到变化的 IDN 数量
SoENotificationEventArgs
public class SoENotificationEventArgs : EventArgs
{
public ushort SlaveIndex { get; } // 从站索引
public byte DriveNumber { get; } // 驱动器编号
public ushort IDN { get; } // 变化的 IDN 编号
public byte[] NewValue { get; } // 新值
public byte[] OldValue { get; } // 旧值
public string SercosIDN { get; } // 格式化的 SERCOS IDN 名称 (如 "S-0-0127")
}
示例:
if (slave.SoE != null)
{
// 注册通知事件
slave.SoE.NotificationReceived += (args) =>
{
Console.WriteLine($"参数变化: {args.SercosIDN} (IDN 0x{args.IDN:X4})");
Console.WriteLine($" 旧值: {BitConverter.ToString(args.OldValue)}");
Console.WriteLine($" 新值: {BitConverter.ToString(args.NewValue)}");
};
// 启用监控实际位置 (S-0-0011)
bool hwSupported = slave.SoE.EnableNotification(0x000B);
Console.WriteLine($"硬件通知: {(hwSupported ? "支持" : "回退到轮询")}");
// 启用监控驱动器状态字 (S-0-0071)
slave.SoE.EnableNotification(0x0047);
// 在定时器中周期性轮询
while (running)
{
int changed = slave.SoE.PollNotifications();
if (changed > 0)
Console.WriteLine($"检测到 {changed} 个参数变化");
Thread.Sleep(100);
}
// 停止监控
slave.SoE.DisableAllNotifications();
}
标准 IDN 常量
public static class StandardIDN
{
public const ushort IDNList = 0x0000; // IDN 列表 (S-0-0000)
public const ushort CycleTime = 0x0001; // 通信周期时间 (S-0-0001)
public const ushort ATConfig = 0x0010; // AT 配置 - 输入映射 (S-0-0016)
public const ushort MDTConfig = 0x0018; // MDT 配置 - 输出映射 (S-0-0024)
public const ushort NotificationRequest = 0x007F; // 通知请求 (S-0-0127)
}
完整示例
参数扫描
if (slave.SoE != null)
{
var idns = slave.SoE.GetAvailableIDNs();
Console.WriteLine($"从站支持 {idns.Count} 个 IDN 参数\n");
foreach (var idn in idns.Take(10))
{
var info = slave.SoE.GetParameterInfo(idn);
Console.WriteLine($"{info.SercosIDN}: {info.Name} = {info.FormattedValue} {info.Unit} [{info.AccessMode}]");
}
}
伺服驱动器控制
if (slave.SoE != null)
{
// 1. 读取驱动器状态
ushort? statusWord = slave.SoE.ReadUInt16(0x0047);
Console.WriteLine($"状态字: 0x{statusWord:X4}");
// 2. 查看 AT/MDT 映射(了解周期数据布局)
var mapping = slave.SoE.GetIDNMapping();
Console.WriteLine($"\nAT 映射(输入 {mapping.ATByteSize} 字节):");
foreach (var entry in mapping.ATMapping)
Console.WriteLine($" IDN 0x{entry.IDN:X4}: {entry.Name} ({entry.ByteLength}B)");
Console.WriteLine($"\nMDT 映射(输出 {mapping.MDTByteSize} 字节):");
foreach (var entry in mapping.MDTMapping)
Console.WriteLine($" IDN 0x{entry.IDN:X4}: {entry.Name} ({entry.ByteLength}B)");
// 3. 写控制字使能驱动器 (S-0-0064)
slave.SoE.WriteUInt16(0x0040, 0x0006);
// 4. 设置目标位置 (S-0-0012)
slave.SoE.WriteInt32(0x000C, 50000);
// 5. 读取实际位置 (S-0-0011)
int? actualPos = slave.SoE.ReadInt32(0x000B);
Console.WriteLine($"\n实际位置: {actualPos} inc");
}
程序命令
ExecuteCommand(ushort idn, int timeoutMs = 5000, int pollIntervalMs = 50)
public bool ExecuteCommand(ushort idn, int timeoutMs = 5000, int pollIntervalMs = 50)
执行 SoE 程序命令(SERCOS Procedure Command)。通过写入 DataState 元素(0x01)的 bit0 触发命令执行,然后轮询等待命令完成。
参数:
idn(ushort) — 要执行的命令 IDN 编号timeoutMs(int) — 等待命令完成的超时时间(毫秒,默认 5000)pollIntervalMs(int) — 轮询间隔(毫秒,默认 50)
返回值:
bool— 命令执行成功返回true,超时或失败返回false
示例:
// 执行回零命令 (S-0-0148)
bool success = slave.SoE.ExecuteCommand(0x0094);
if (success)
Console.WriteLine("回零完成");
else
Console.WriteLine("回零超时或失败");
// 自定义超时(长时间命令)
bool ok = slave.SoE.ExecuteCommand(0x0094, timeoutMs: 30000, pollIntervalMs: 100);
ReadDataState(ushort idn, int timeoutMs = 500)
public byte[]? ReadDataState(ushort idn, int timeoutMs = 500)
读取 IDN 的 Data State 元素(ElementFlag 0x01)。Data State 包含命令状态、数据有效性等信息。
参数:
idn(ushort) — IDN 编号timeoutMs(int) — 超时时间(毫秒,默认 500)
返回值:
byte[]?— Data State 字节数据,失败返回null
示例:
// 读取 IDN 的 Data State
byte[]? state = slave.SoE.ReadDataState(0x0094);
if (state != null)
{
bool commandActive = (state[0] & 0x01) != 0;
bool commandError = (state[0] & 0x02) != 0;
Console.WriteLine($"命令激活: {commandActive}, 命令错误: {commandError}");
}
运行时异步通知 (Notification / Emergency)
除了同步的 Read/Write API, SoE 还支持两类由从站侧异步推送的消息, SDK 将其表面化为两个事件:
| 事件 | SERCOS OpCode | 用途 |
|---|---|---|
| NotificationReceived | 5 | IDN 参数值变化通知 (热更新监听) |
| EmergencyReceived | 6 | 驱动器 Sercos Emergency (紧急停止 / 故障) |
典型用例:
- 伺服参数热变化监听 — HMI 改写 S-0-0036 Max Velocity 后, 其他监控端点通过 Notification 实时收到新值
- 紧急停止监听 — 驱动器硬件级 Emergency (过流/过温/编码器丢失) 主动推送, 应用层立刻切换到安全分支
- 多端调参一致性 — 多个工程师通过不同上位机调参时, 参数变化可广播到所有监听端
NotificationReceived 事件
public event Action<SoENotificationEventArgs>? NotificationReceived;
当监控的 IDN 参数值发生变化时触发。SDK 首先尝试通过 S-0-0127 启用硬件通知 (OpCode=5), 若从站不支持则回退到周期性读取 + 本地比较的轮询模式。两种模式对应用层透明, 订阅者收到的事件结构一致。
事件参数:
public class SoENotificationEventArgs : EventArgs
{
public ushort SlaveIndex { get; } // 从站索引 (1-based)
public byte DriveNumber { get; } // 驱动器编号 (多轴驱动用)
public ushort IDN { get; } // 发生变化的 IDN 编号
public SoEElementFlags ElementFlags { get; } // 触发通知的 Element 位掩码
public byte[]? NewValue { get; } // 变化后的值 (可能 null 表示读取失败)
public byte[]? OldValue { get; } // 变化前的值 (首次通知时为 null)
public string SercosIDN { get; } // SERCOS 格式, 如 "S-0-0036"
}
EmergencyReceived 事件
public event Action<SoEEmergencyEventArgs>? EmergencyReceived;
驱动器推送 Sercos Emergency (OpCode=6) 时触发。与 CoE 的 EMCY 消息不同, Sercos Emergency 是伺服专属, 错误码遵循 SERCOS 定义 (非 CiA 301)。
事件参数:
public class SoEEmergencyEventArgs : EventArgs
{
public ushort SlaveIndex { get; } // 来源从站索引 (1-based)
public byte DriveNumber { get; } // 驱动器编号 (SoEHeader driveNo 位段, 3 bit)
public ushort ErrorCode { get; } // Sercos Emergency Error Code (2 字节)
public DateTime Timestamp { get; } // 接收时间戳 (UTC)
}
线程模型
这两个事件在 DLL worker 线程 上触发, 不是 UI 线程。若需在 WinForms / WPF UI 上更新控件, 必须 Invoke / Dispatcher.Invoke 切回 UI 线程:
// WinForms
slave.SoE.EmergencyReceived += (args) =>
{
if (mainForm.InvokeRequired)
mainForm.BeginInvoke(new Action(() => HandleEmergency(args)));
else
HandleEmergency(args);
};
回调内禁止做长时间阻塞 (> 几毫秒), 避免拖慢 SoE 邮箱处理; 耗时工作丢到 Task.Run 异步执行。
完整示例 — 订阅 + 取消 + UI 切换
if (slave.SoE == null) return;
// 1. 订阅 Notification (参数热变化)
Action<SoENotificationEventArgs> onNotification = args =>
{
string newStr = args.NewValue != null ? BitConverter.ToString(args.NewValue) : "N/A";
string oldStr = args.OldValue != null ? BitConverter.ToString(args.OldValue) : "(首次)";
Console.WriteLine(
$"[SoE Notify] 从站={args.SlaveIndex} 驱动器={args.DriveNumber} " +
$"{args.SercosIDN} (0x{args.IDN:X4}) Elements=0x{(byte)args.ElementFlags:X2} " +
$"旧=[{oldStr}] → 新=[{newStr}]");
};
// 2. 订阅 Emergency (紧急故障)
Action<SoEEmergencyEventArgs> onEmergency = args =>
{
Console.WriteLine(
$"[SoE EMCY] 从站={args.SlaveIndex} 驱动器={args.DriveNumber} " +
$"ErrorCode=0x{args.ErrorCode:X4} @ {args.Timestamp:HH:mm:ss.fff}");
// 典型响应: 切换 UI 到故障分支, 停止运动轴
if (mainForm.InvokeRequired)
mainForm.BeginInvoke(new Action(() => mainForm.ShowEmergencyBanner(args.ErrorCode)));
};
slave.SoE.NotificationReceived += onNotification;
slave.SoE.EmergencyReceived += onEmergency;
// 3. 启用需要监控的 IDN (支持多个)
slave.SoE.EnableNotification(0x0024); // Max Velocity
slave.SoE.EnableNotification(0x0047); // Drive Status Word
// 4. 应用退出时取消订阅, 避免悬挂引用
try
{
// ... 应用运行 ...
}
finally
{
slave.SoE.DisableAllNotifications();
slave.SoE.NotificationReceived -= onNotification;
slave.SoE.EmergencyReceived -= onEmergency;
}
注意事项
- Notification 的首次触发
OldValue可能为null(基线刚建立, 无历史值) - 从站不支持硬件 Notification 时, SDK 按
PollNotifications()周期 (默认 100ms) 检测, 变化延迟取决于轮询频率 - Emergency 回调内不要再调用阻塞的 SDO/SoE 读写, 容易在故障状态下堆积请求
- 订阅事件不需要从站处于 OP 状态, PreOp/SafeOp 也能收到 (与邮箱可用性一致)