SoE (Servo over EtherCAT)
SoE 协议将 SERCOS 伺服通信协议封装在 EtherCAT 邮箱中,适用于 SERCOS 兼容的伺服驱动器。
通过 slave.soe() 获取 SoEInstance 实例。
大多数 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 | 通信周期时间 | u32, us |
| S-0-0011 (0x000B) | Position Feedback 1 | 实际位置 | i32, inc |
| S-0-0012 (0x000C) | Position Command | 目标位置 | i32, inc |
| S-0-0013 (0x000D) | Velocity Feedback | 实际速度 | i32, rpm |
| S-0-0014 (0x000E) | Velocity Command | 目标速度 | i32, rpm |
| S-0-0016 (0x0010) | AT Config | 输入映射配置列表 | list |
| S-0-0019 (0x0013) | Drive Status | 驱动器状态字 | u16 |
| S-0-0024 (0x0018) | MDT Config | 输出映射配置列表 | list |
| S-0-0036 (0x0024) | Max Velocity | 最大速度限制 | u32, rpm |
| S-0-0040 (0x0028) | Homing Velocity | 回零速度 | u32, rpm |
| S-0-0064 (0x0040) | Drive Control Word | 驱动器控制字 | u16 |
| S-0-0071 (0x0047) | Drive Status Word | 驱动器状态字 | u16 |
SoEElementFlags 元素标志: 读取 IDN 的不同"层面" (Element), 提供 bitflags 风格的
u8 包装。
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SoEElementFlags(pub u8);
impl SoEElementFlags {
pub const DATA_STATE: Self = Self(0x01); // 数据状态
pub const NAME: Self = Self(0x02); // 参数名称(字符串)
pub const ATTRIBUTE: Self = Self(0x04); // 属性(数据类型/长度/权限位域)
pub const UNIT: Self = Self(0x08); // 单位(字符串)
pub const MIN_VALUE: Self = Self(0x10); // 最小值
pub const MAX_VALUE: Self = Self(0x20); // 最大值
pub const VALUE: Self = Self(0x40); // 数据值(默认,最常用)
pub const DEFAULT_VALUE: Self = Self(0x80); // 默认值
pub fn contains(self, other: Self) -> bool;
pub fn bits(self) -> u8;
}
SERCOS IDN 编解码 (模块级关联函数): 将 SERCOS 字符串格式 (如 "S-0-0001") 与 16 位
IDN 编号互转, 无需实例。
// (is_standard, parameter_set 0..7, data_block 0..0x0FFF) -> 16 位 IDN
pub fn encode_idn(is_standard: bool, parameter_set: u8, data_block: u16) -> u16
// 16 位 IDN -> (is_standard, parameter_set, data_block)
pub fn decode_idn(idn: u16) -> (bool, u8, u16)
// 解析 "S-0-0011" / "P-1-0123" -> 16 位 IDN, 失败返回 None
pub fn try_parse_sercos_idn(text: &str) -> Option<u16>
// 16 位 IDN -> "S-x-xxxx" / "P-x-xxxx" 字符串
pub fn format_sercos_idn(idn: u16) -> String
约定: bit15 = 0 表示标准 (S-), bit15 = 1 表示厂商 (P-); bit14..12 为 set, bit11..0 为 data block。
use ethercat::slave::soe::{
encode_idn, decode_idn, try_parse_sercos_idn, format_sercos_idn, SoEElementFlags,
};
// 字符串 -> IDN
let idn = try_parse_sercos_idn("S-0-0011").unwrap(); // 0x000B
let val = soe.read_i32(idn, 500)?;
// 直接拼装 / 反向解码
let idn = encode_idn(true, 0, 11); // 0x000B (标准, set 0, block 11)
let (is_std, set, block) = decode_idn(0x000B); // (true, 0, 11)
// 反向格式化 (调试 / 日志)
println!("{}", format_sercos_idn(idn)); // "S-0-0011"
// 厂商 IDN 例子
let p_idn = encode_idn(false, 1, 123); // 0x9123
assert_eq!(format_sercos_idn(p_idn), "P-1-0123");
// 元素标志读不同层面
let data = soe.read(0x000B, SoEElementFlags::VALUE.bits(), 500)?;
let name = soe.read(0x000B, SoEElementFlags::NAME.bits(), 500)?;
基本读写
read()
pub fn read(&self, idn: u16, element_flags: u8, timeout_ms: i32)
-> Result<Vec<u8>, DarraError>
读取 IDN 参数原始字节。非可用状态自动切换。
参数:
idn(u16) — IDN 编号element_flags(u8) — 元素标志(默认 0x40 = 数据值)timeout_ms(i32) — 超时时间(毫秒)
返回值:
Result<Vec<u8>, DarraError>— 读取的数据
示例:
let soe = slave.soe().ok_or("不支持 SoE")?;
// 读取通信周期时间 (S-0-0001)
let data = soe.read(0x0001, 0x40, 500)?;
let cycle_time = u32::from_le_bytes(data[0..4].try_into()?);
println!("周期时间: {} us", cycle_time);
write()
pub fn write(&self, idn: u16, data: &[u8], element_flags: u8, timeout_ms: i32)
-> Result<(), DarraError>
写入 IDN 参数。非可用状态自动切换。
参数:
idn(u16) — IDN 编号data(&[u8]) — 要写入的数据element_flags(u8) — 元素标志(默认 0 = 数据值)timeout_ms(i32) — 超时时间(毫秒)
返回值:
Result<(), DarraError>— 成功或错误
示例:
// 设置目标位置 (S-0-0012)
soe.write(0x000C, &100000i32.to_le_bytes(), 0, 500)?;
类型化读取
pub fn read_i16(&self, idn: u16, timeout_ms: i32) -> Result<i16>
pub fn read_i32(&self, idn: u16, timeout_ms: i32) -> Result<i32>
pub fn read_u16(&self, idn: u16, timeout_ms: i32) -> Result<u16>
pub fn read_u32(&self, idn: u16, timeout_ms: i32) -> Result<u32>
pub fn read_f32(&self, idn: u16, timeout_ms: i32) -> Result<f32>
pub fn read_f64(&self, idn: u16, timeout_ms: i32) -> Result<f64>
pub fn read_string(&self, idn: u16, timeout_ms: i32) -> Result<String>
示例:
// 读取实际位置 (S-0-0011)
let pos = soe.read_i32(0x000B, 500)?;
println!("实际位置: {} inc", pos);
// 读取驱动器状态字 (S-0-0071)
let status = soe.read_u16(0x0047, 500)?;
println!("状态字: 0x{:04X}", status);
// 读取参数名称
let name = soe.read_string(0x000B, 500)?;
println!("参数名: {}", name);
类型化写入
pub fn write_i16(&self, idn: u16, value: i16, timeout_ms: i32) -> Result<()>
pub fn write_i32(&self, idn: u16, value: i32, timeout_ms: i32) -> Result<()>
pub fn write_u16(&self, idn: u16, value: u16, timeout_ms: i32) -> Result<()>
pub fn write_u32(&self, idn: u16, value: u32, timeout_ms: i32) -> Result<()>
示例:
// 设置目标位置 (S-0-0012)
soe.write_i32(0x000C, 50000)?;
// 写控制字 (S-0-0064)
soe.write_u16(0x0040, 0x0006)?;
元数据读取
read_name()
pub fn read_name(&self, idn: u16, timeout_ms: i32) -> Result<String>
读取 IDN 参数名称(内部使用 ElementFlag 0x02)。
read_unit()
pub fn read_unit(&self, idn: u16, timeout_ms: i32) -> Result<String>
读取 IDN 参数单位(内部使用 ElementFlag 0x08)。
示例:
let name = soe.read_name(0x000B, 500)?;
println!("参数名: {}", name);
let unit = soe.read_unit(0x000B, 500)?;
println!("单位: {}", unit);
read_min_value() / read_max_value() / read_default_value()
pub fn read_min_value(&self, idn: u16, timeout_ms: i32) -> Result<Vec<u8>>
pub fn read_max_value(&self, idn: u16, timeout_ms: i32) -> Result<Vec<u8>>
pub fn read_default_value(&self, idn: u16, timeout_ms: i32) -> Result<Vec<u8>>
读取 IDN 参数的最小值(0x10)、最大值(0x20)、默认值(0x80)。
示例:
let name = soe.read_name(0x000B, 500)?;
let unit = soe.read_unit(0x000B, 500)?;
let min = soe.read_min_value(0x000B, 500)?;
let max = soe.read_max_value(0x000B, 500)?;
println!("参数: {} ({})", name, unit);
if min.len() >= 4 && max.len() >= 4 {
let min_val = i32::from_le_bytes(min[0..4].try_into().unwrap());
let max_val = i32::from_le_bytes(max[0..4].try_into().unwrap());
println!("范围: {} - {}", min_val, max_val);
}
参数信息
get_available_idns()
pub fn get_available_idns(&self, timeout_ms: i32) -> Result<Vec<u16>>
获取从站支持的所有 IDN 列表。
get_parameter_info()
pub fn get_parameter_info(&self, idn: u16, timeout_ms: i32) -> SoEParameter
获取 IDN 参数的完整信息(名称、单位、属性、当前值、最小/最大值等)。该方法不返回 Result,读取失败的字段使用默认空值。
pub struct SoEParameter {
pub idn: u16,
pub name: String,
pub unit: String,
pub attributes: SoEAttributes,
pub value: Vec<u8>,
pub default_value: Vec<u8>,
pub min_value: Vec<u8>,
pub max_value: Vec<u8>,
}
impl SoEParameter {
pub fn sercos_idn(&self) -> String // SERCOS IDN 格式(如 "S-0-0001")
pub fn category(&self) -> &'static str // 类别("Standard" / "Product" / "Vendor")
pub fn is_read_only(&self) -> bool // 是否只读
pub fn access_mode(&self) -> &'static str // 访问权限("RO" / "RW" / "RW*")
}
pub struct SoEAttributes {
pub evaluation_factor: u16, // 评价因子
pub length: u8, // 数据长度
pub is_list: bool, // 是否为列表
pub is_command: bool, // 是否为命令
pub data_type: SoEDataType, // 数据类型
pub decimals: u8, // 小数位数
pub write_protected_preop: bool, // PreOp 状态写保护
pub write_protected_safeop: bool, // SafeOp 状态写保护
pub write_protected_op: bool, // Op 状态写保护
}
#[repr(u8)]
pub enum SoEDataType {
Binary = 0, // 二进制
UInt = 1, // 无符号整数
Int = 2, // 有符号整数
Hexadecimal = 3, // 十六进制
String = 4, // 字符串
IDN = 5, // IDN 引用
Float = 6, // 浮点数
Parameter = 7, // 参数
}
示例:
let param = soe.get_parameter_info(0x000B, 500);
println!("IDN: {} - {}", param.sercos_idn(), param.name);
println!("单位: {}, 访问: {}", param.unit, param.access_mode());
AT/MDT 映射
SERCOS 使用 AT(Acknowledge Telegram)和 MDT(Master Data Telegram)传输周期数据:
- AT = 从站→主站的周期输入(实际位置、实际速度、状态字...)
- MDT = 主站→从站的周期输出(目标位置、目标速度、控制字...)
映射配置决定了哪些 IDN 参数被包含在周期数据帧中。
get_idn_mapping()
pub fn get_idn_mapping(&self, timeout_ms: i32) -> SoEMappingInfo
读取当前驱动器的 AT/MDT 映射配置。该方法不返回 Result,读取失败的映射使用空列表。
pub struct SoEMappingInfo {
pub at_mapping: Vec<SoEMappingEntry>, // AT(输入)映射列表
pub mdt_mapping: Vec<SoEMappingEntry>, // MDT(输出)映射列表
pub at_bit_size: i32, // AT 总位数
pub mdt_bit_size: i32, // MDT 总位数
}
impl SoEMappingInfo {
pub fn at_byte_size(&self) -> i32 // AT 总字节数
pub fn mdt_byte_size(&self) -> i32 // MDT 总字节数
}
pub struct SoEMappingEntry {
pub idn: u16, // IDN 编号
pub name: String, // 参数名称
pub bit_length: i32, // 数据长度(位)
}
impl SoEMappingEntry {
pub fn byte_length(&self) -> i32 // 数据长度(字节)
}
get_all_drive_mappings()
pub fn get_all_drive_mappings(&self, timeout_ms: i32) -> HashMap<u8, SoEMappingInfo>
获取所有驱动器的映射信息(最多扫描 8 个驱动器)。
参数变化通知
SoE Notification 功能允许监控 SERCOS 从站 IDN 参数的变化。通过轮询机制检测参数值改变。
实现机制:
- 硬件通知:通过写入 S-0-0127(通知请求 IDN)尝试启用从站的原生通知
- 轮询检测:如果从站不支持硬件通知,自动回退到轮询模式
enable_notification()
pub fn enable_notification(&self, idn: u16, timeout_ms: i32) -> bool
启用指定 IDN 的参数变化通知。首先尝试通过 S-0-0127 启用硬件通知,无论是否成功都会将 IDN 加入轮询监控列表。
返回值:
bool—true表示从站确认支持硬件通知;false表示回退到轮询模式
disable_notification()
pub fn disable_notification(&self, idn: u16)
禁用指定 IDN 的参数变化通知。
disable_all_notifications()
pub fn disable_all_notifications(&self)
禁用所有已启用的通知,清空监控列表。
poll_notifications()
pub fn poll_notifications(&self, timeout_ms: i32) -> Vec<SoENotificationEventArgs>
pub fn monitored_idns(&self) -> Vec<u16>
pub fn get_monitored_idns(&self) -> Vec<u16> // monitored_idns 的别名
poll_notifications() 轮询检测所有已监控 IDN 的参数变化, 返回所有检测到变化的事件列表;
monitored_idns() / get_monitored_idns() 获取当前正在监控的 IDN 列表。
SoENotificationEventArgs
#[derive(Debug, Clone)]
pub struct SoENotificationEventArgs {
pub slave_index: u16, // 从站索引
pub drive_number: u8, // 驱动器编号
pub idn: u16, // 变化的 IDN 编号
pub new_value: Option<Vec<u8>>, // 新值
pub old_value: Option<Vec<u8>>, // 旧值
}
impl SoENotificationEventArgs {
pub fn sercos_idn(&self) -> String // 格式化的 SERCOS IDN 名称 (如 "S-0-0127")
}
示例:
let soe = slave.soe().ok_or("不支持 SoE")?;
// 启用监控实际位置 (S-0-0011)
let hw_supported = soe.enable_notification(0x000B, 500);
println!("硬件通知: {}", if hw_supported { "支持" } else { "回退到轮询" });
// 启用监控驱动器状态字 (S-0-0071)
soe.enable_notification(0x0047, 500);
// 轮询检测变化
let changes = soe.poll_notifications(200);
for args in &changes {
println!("参数变化: {} (IDN 0x{:04X})", args.sercos_idn(), args.idn);
if let Some(ref new_val) = args.new_value {
println!(" 新值: {:?}", new_val);
}
}
// 查看已监控的 IDN
let monitored = soe.monitored_idns();
println!("监控列表: {:?}", monitored);
// 停止监控
soe.disable_all_notifications();
标准 IDN 常量
pub mod standard_idn {
pub const IDN_LIST: u16 = 0x0000; // IDN 列表 (S-0-0000)
pub const CYCLE_TIME: u16 = 0x0001; // 通信周期时间 (S-0-0001)
pub const AT_CONFIG: u16 = 0x0010; // AT 配置 - 输入映射 (S-0-0016)
pub const MDT_CONFIG: u16 = 0x0018; // MDT 配置 - 输出映射 (S-0-0024)
pub const NOTIFICATION_REQUEST: u16 = 0x007F; // 通知请求 (S-0-0127)
}
程序命令
execute_command()
pub fn execute_command(&self, idn: u16, timeout_ms: i32, poll_interval_ms: i32) -> Result<()>
执行 SoE 程序命令(SERCOS Procedure Command)。通过写入 DataState 元素的 bit0 触发命令执行,然后轮询等待命令完成。
参数:
idn(u16) — 要执行的命令 IDN 编号timeout_ms(i32) — 等待命令完成的超时时间(毫秒)poll_interval_ms(i32) — 轮询间隔(毫秒)
返回值:
Result<()>— 成功返回Ok(()),超时返回Err(DarraError::Timeout)
示例:
// 执行回零命令 (S-0-0148)
let success = soe.execute_command(0x0094, 5000, 50)?;
if success {
println!("回零完成");
} else {
println!("回零超时或失败");
}
// 自定义超时(长时间命令)
let ok = soe.execute_command(0x0094, 30000, 100)?;
read_data_state()
pub fn read_data_state(&self, idn: u16, timeout_ms: i32) -> Result<u16>
读取 IDN 的 Data State 元素 (ElementFlag 0x01), 包含命令状态、数据有效性等信息, 返回 u16
值。常与 execute_command() 配合, 在长耗时程序命令期间轮询当前状态。
let state = soe.read_data_state(0x0094, 500)?;
let command_active = (state & 0x0001) != 0;
let command_error = (state & 0x0002) != 0;
println!("命令激活: {}, 命令错误: {}", command_active, command_error);
完整示例
参数扫描
let soe = slave.soe().ok_or("不支持 SoE")?;
let idns = soe.get_available_idns(5000)?;
println!("从站支持 {} 个 IDN 参数\n", idns.len());
for idn in idns.iter().take(10) {
let info = soe.get_parameter_info(*idn)?;
println!("{}: {} = {} {} [{}]",
info.sercos_idn, info.name, info.formatted_value,
info.unit, info.access_mode);
}
伺服驱动器控制
let soe = slave.soe().ok_or("不支持 SoE")?;
// 1. 读取驱动器状态
if let Some(status) = soe.read_u16(0x0047)? {
println!("状态字: 0x{:04X}", status);
}
// 2. 查看 AT/MDT 映射(了解周期数据布局)
let mapping = soe.get_idn_mapping(5000)?;
println!("\nAT 映射(输入 {} 字节):", mapping.at_byte_size);
for entry in &mapping.at_mapping {
println!(" IDN 0x{:04X}: {} ({}B)", entry.idn, entry.name, entry.byte_length);
}
println!("\nMDT 映射(输出 {} 字节):", mapping.mdt_byte_size);
for entry in &mapping.mdt_mapping {
println!(" IDN 0x{:04X}: {} ({}B)", entry.idn, entry.name, entry.byte_length);
}
// 3. 写控制字使能驱动器 (S-0-0064)
soe.write_u16(0x0040, 0x0006)?;
// 4. 设置目标位置 (S-0-0012)
soe.write_i32(0x000C, 50000)?;
// 5. 读取实际位置 (S-0-0011)
if let Some(pos) = soe.read_i32(0x000B)? {
println!("\n实际位置: {} inc", pos);
}
元数据查询
let soe = slave.soe().ok_or("不支持 SoE")?;
// 读取参数名称和单位
let name = soe.read_name(0x000B, 500)?.unwrap_or_default();
let unit = soe.read_unit(0x000B, 500)?.unwrap_or_default();
println!("参数: {} ({})", name, unit);
// 读取最小值和最大值
let min = soe.read_min_value(0x000B, 500)?;
let max = soe.read_max_value(0x000B, 500)?;
if let (Some(min_data), Some(max_data)) = (&min, &max) {
if min_data.len() >= 4 && max_data.len() >= 4 {
let min_val = i32::from_le_bytes(min_data[0..4].try_into().unwrap());
let max_val = i32::from_le_bytes(max_data[0..4].try_into().unwrap());
println!("范围: {} - {} {}", min_val, max_val, unit);
}
}
运行时异步通知 (Notification / Emergency)
轮询 poll_notifications() 适合低频监控, 高实时场景 (毫秒级响应) 建议使用回调分发模式:
一次注册, 由 DLL 线程直接回调用户闭包. SoE 支持两类事件 — Notification (IDN 值变化)
和 Emergency (SoE 错误帧, ETG.5003).
SDK 使用 Arc<dyn Fn(...) + Send + Sync> 类型暴露回调, 支持多订阅者共享且线程安全.
add_*_callback 需要保留传入的 Arc 克隆, 后续 remove_*_callback 按 Arc::ptr_eq 匹配.
未保留克隆则无法精确移除, 可用 disable_all_notifications() 整体清空监控 IDN (但不清回调列表).
SoENotificationEvent
use std::time::SystemTime;
#[derive(Debug, Clone)]
pub struct SoENotificationEvent {
pub master_index: u16, // 主站索引
pub slave_index: u16, // 从站索引
pub drive_number: u8, // 驱动器编号
pub idn: u16, // 发生变化的 IDN
pub new_value: Option<Vec<u8>>, // 新值 (Element Value)
pub old_value: Option<Vec<u8>>, // 旧值 (上次 poll 读到的)
pub timestamp: SystemTime, // 触发时刻
}
impl SoENotificationEvent {
/// SERCOS IDN 格式 "S-0-0016" / "P-1-0100"
pub fn sercos_idn(&self) -> String;
}
SoEEmergencyEvent
#[derive(Debug, Clone)]
pub struct SoEEmergencyEvent {
pub master_index: u16, // 主站索引
pub slave_index: u16, // 从站索引
pub drive_number: u8, // 驱动器编号
pub idn: u16, // 关联 IDN
pub error_code: SoEErrorCode, // SoE 错误码 (ETG.5003)
pub data: Vec<u8>, // Emergency 附加数据 (原始字节)
pub timestamp: SystemTime, // 触发时刻
}
add_notification_callback() / remove_notification_callback()
use std::sync::Arc;
pub type SoENotificationCallback = Arc<dyn Fn(&SoENotificationEvent) + Send + Sync>;
pub type SoEEmergencyCallback = Arc<dyn Fn(&SoEEmergencyEvent) + Send + Sync>;
pub fn add_notification_callback(&self, cb: SoENotificationCallback)
pub fn remove_notification_callback(&self, cb: &SoENotificationCallback)
注册 / 移除 SoE Notification 回调.
同一 Arc 多次注册会被多次调用. 回调在分发线程执行, 不要做阻塞 / 长时间计算.
add_emergency_callback() / remove_emergency_callback()
pub fn add_emergency_callback(&self, cb: SoEEmergencyCallback)
pub fn remove_emergency_callback(&self, cb: &SoEEmergencyCallback)
注册 / 移除 SoE Emergency 回调.
示例: 注册并保留 Arc 用于移除
use std::sync::Arc;
use ethercat::slave::soe::{SoENotificationEvent, SoENotificationCallback};
let soe = slave.soe().ok_or("不支持 SoE")?;
// 1. Notification 回调
let cb: SoENotificationCallback = Arc::new(|e: &SoENotificationEvent| {
println!(
"[{}] Slave {} IDN {} (0x{:04X}) 变化",
format!("{:?}", e.timestamp),
e.slave_index,
e.sercos_idn(),
e.idn,
);
if let Some(v) = &e.new_value {
println!(" 新值 ({} 字节): {:02X?}", v.len(), v);
}
});
soe.add_notification_callback(Arc::clone(&cb));
// 启用轮询监控 (DLL 硬件通知不支持时自动回退)
soe.enable_notification(0x000B, 500); // S-0-0011 实际位置
// 2. Emergency 回调 — 收到 SoE Error Frame 立即报警
soe.add_emergency_callback(Arc::new(|e| {
eprintln!(
"SoE Emergency: Slave {}, IDN 0x{:04X}, 错误码 {:?}, data={:02X?}",
e.slave_index, e.idn, e.error_code, e.data,
);
}));
// 3. 退出前移除
soe.remove_notification_callback(&cb);
- IEC 61800-7-204 — SERCOS over EtherCAT (SoE)
- ETG.5003 — Emergency 帧格式