CoE (CANopen over EtherCAT)
快速开始
CoE 对象字典通过 OdList::load() 加载,然后使用 sdo_read_value::<T>() / sdo_write_value::<T>() 进行类型化读写。
use ethercat::{OdList, Slave};
let slave = master.slave(1);
// 类型化 SDO 读写 (最常用)
let status: u16 = slave.sdo_read_value(0x6041, 0x00)?;
let position: i32 = slave.sdo_read_value(0x6064, 0x00)?;
slave.sdo_write_value(0x6040, 0x00, &0x0006u16)?;
// 加载完整 OD 树 (用于遍历)
let od = OdList::load(master.index(), 1)?;
for obj in &od.objects {
println!("0x{:04X}: {} ({} 条目)", obj.index, obj.name, obj.entries.len());
}
SDO 直接读写
sdo_read_value::<T>()
通过 Slave 句柄直接读取 SDO 值,支持泛型类型。
// 从 Slave 直接读取 (推荐)
let value: u16 = slave.sdo_read_value(0x6041, 0x00)?;
let name: String = slave.sdo_read_value(0x1008, 0x00)?;
sdo_write_value::<T>()
// 从 Slave 直接写入
slave.sdo_write_value(0x6040, 0x00, &0x0006u16)?;
slave.sdo_write_value(0x607A, 0x00, &100000i32)?;
sdo_read() / sdo_write()
// 原始字节读写 (第三个参数: complete_access)
let data: Vec<u8> = slave.sdo_read(0x1000, 0x00, false)?;
slave.sdo_write(0x6040, 0x00, false, &0x0006u16.to_le_bytes())?;
批量读取
read_multiple()
pub fn read_multiple(&self, entries: &[(u16, u8)]) -> HashMap<(u16, u8), Option<Vec<u8>>>
批量 SDO 读取。一次性读取多个 (index, subindex) 对应的 SDO 数据。成功的条目返回 Some(Vec<u8>),失败的条目返回 None。
参数:
entries(&[(u16, u8)]) -- 要读取的 (index, subindex) 列表
返回值:
HashMap<(u16, u8), Option<Vec<u8>>>-- 每个条目的读取结果
示例:
let coe = CoEInstance::new(master.index(), 1);
let entries = [(0x1000, 0x00), (0x1008, 0x00), (0x1018, 0x01)];
let results = coe.read_multiple(&entries);
for ((idx, sub), data) in &results {
match data {
Some(bytes) => println!("0x{:04X}:{:02X} = {} 字节", idx, sub, bytes.len()),
None => println!("0x{:04X}:{:02X} 读取失败", idx, sub),
}
}
属性
| 属性 | 类型 | 访问 | 说明 |
|---|---|---|---|
| last_sdo_error() | SdoError | 只读 | 最后一次 SDO 操作的错误码 |
last_sdo_error()
pub fn last_sdo_error(&self) -> SdoError
最后一次 SDO 操作的错误码。当 sdo_read() / sdo_write() 失败时,可通过此方法获取 SDO 中止码 (Abort Code)。成功操作后自动重置为 SdoError::NoError。
示例:
let data = slave.sdo_read(0x6041, 0x00, false);
if data.is_err() {
let error = slave.last_sdo_error();
println!("SDO 读取失败: {:?} (0x{:08X})", error, error as u32);
}
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 | 未知错误 |
OD 树遍历 (OdList)
OdList 用于从从站加载完整的对象字典结构,支持按索引查找和遍历。
OdList::load()
pub fn load(master_index: u16, slave_index: u16) -> Result<Self>
加载指定从站的完整 OD(先获取索引列表,再逐一获取条目详情)。
示例:
use ethercat::OdList;
let od = OdList::load(master.index(), 1)?;
println!("从站 1 对象字典: {} 个对象", od.len());
// 按索引查找
if let Some(obj) = od.find(0x6040) {
println!("0x{:04X}: {} ({} 条目)", obj.index, obj.name, obj.entries.len());
for entry in &obj.entries {
println!(" [{:02X}] {} {} {}bit",
entry.sub_index, entry.name, entry.data_type, entry.bit_length);
}
}
// 按名称模糊查找
let results = od.find_by_name("control");
for obj in results {
println!("找到: 0x{:04X} {}", obj.index, obj.name);
}
OdObject 结构(第二层)
代表一个对象字典对象(如 0x6040 控制字),包含若干子对象。
pub struct OdObject {
pub index: u16, // 对象索引号
pub name: String, // 对象名称
pub object_code: u8, // 对象代码: 0x07=VAR, 0x08=ARRAY, 0x09=RECORD
pub data_type: EcDataType, // 数据类型标识
pub entries: Vec<ObjectEntry>, // 子条目列表
}
ObjectEntry 结构(第三层)
代表一个子对象(如 0x6040:00),是最终的数据读写节点。
pub struct ObjectEntry {
pub od_index: u16, // 父对象索引
pub sub_index: u8, // 子索引
pub name: String, // 名称
pub data_type: EcDataType, // 数据类型 (决定读取时的自动转换目标)
pub bit_length: u16, // 位长度
pub access: ObjAccess, // 访问权限标志
pub value_info: u8, // ValueInfo 标志
}
ObjectEntry 支持直接读写:
// 通过 ObjectEntry 读写
let entry = &od.find(0x6040).unwrap().entries[0];
let raw = entry.read_raw(master.index(), 1)?;
let val: u16 = entry.read_u16(master.index(), 1)?;
entry.write_u16(master.index(), 1, 0x0006)?;
// 检查访问权限
if entry.access.writable() {
entry.write_u16(master.index(), 1, 0x0006)?;
}
EcDataType 枚举
#[repr(u16)]
pub enum EcDataType {
Boolean = 0x0001, // bool — 布尔
Integer8 = 0x0002, // i8 — int8
Integer16 = 0x0003, // i16 — int16
Integer32 = 0x0004, // i32 — int32
Unsigned8 = 0x0005, // u8 — uint8
Unsigned16 = 0x0006, // u16 — uint16
Unsigned32 = 0x0007, // u32 — uint32
Real32 = 0x0008, // f32 — 单精度浮点
VisibleString = 0x0009, // String — ASCII 字符串
OctetString = 0x000A, // Vec<u8> — 字节串
Real64 = 0x0011, // f64 — 双精度浮点
Integer64 = 0x0015, // i64 — int64
Unsigned64 = 0x001B, // u64 — uint64
}
ObjAccess 访问权限
pub struct ObjAccess(pub u16);
impl ObjAccess {
pub const READ_PREOP: u16 = 0x0001; // PreOp 可读
pub const READ_SAFEOP: u16 = 0x0002; // SafeOp 可读
pub const READ_OP: u16 = 0x0004; // OP 可读
pub const WRITE_PREOP: u16 = 0x0008; // PreOp 可写
pub const WRITE_SAFEOP: u16 = 0x0010; // SafeOp 可写
pub const WRITE_OP: u16 = 0x0020; // OP 可写
pub const MAP_RXPDO: u16 = 0x0040; // 可映射到 RxPDO
pub const MAP_TXPDO: u16 = 0x0080; // 可映射到 TxPDO
pub fn readable(self) -> bool; // 任意状态可读
pub fn writable(self) -> bool; // 任意状态可写
}
EMCY 紧急消息历史
CoE 支持 CiA 301 紧急消息 (Emergency) 记录。每个从站的 CoE 实例自动收集该从站的 EMCY 消息。
max_emcy_history_size() / set_max_emcy_history_size()
pub fn max_emcy_history_size(master_index: u16, slave_index: u16) -> usize
pub fn set_max_emcy_history_size(master_index: u16, slave_index: u16, size: usize)
EMCY 历史记录最大容量(默认 256)。超出容量时自动丢弃最旧的消息。最小值为 1。
emcy_get_history()
pub fn emcy_get_history(master_index: u16, slave_index: u16) -> Vec<EmergencyMessage>
获取该从站的 EMCY 紧急消息历史记录(返回副本)。最多保留最近 max_emcy_history_size 条。
emcy_clear_history()
pub fn emcy_clear_history(master_index: u16, slave_index: u16)
清除该从站的 EMCY 历史记录。
EmergencyMessage 结构
pub struct EmergencyMessage {
pub error_code: u16, // 紧急错误代码 (CiA 301 Table 24)
pub error_register: u8, // 错误寄存器 (对象 0x1001)
pub data: Vec<u8>, // 厂商特定数据 (5 字节)
pub slave_index: u16, // 来源从站编号
pub timestamp: u64, // 接收时间 (毫秒时间戳)
}
impl EmergencyMessage {
/// 获取错误代码的文本描述 (CiA 301 标准错误分类)
pub fn error_description(&self) -> &str;
}
error_description() 根据 CiA 301 标准错误代码高字节返回分类描述,如 "电流错误"、"温度错误"、"通信错误" 等。
示例:
use ethercat::{emcy_get_history, emcy_get_count, emcy_clear_history};
// 获取从站 EMCY 消息数量
let count = emcy_get_count(master.index(), 1);
// 获取历史记录
let history = emcy_get_history(master.index(), 1);
for msg in &history {
println!("EMCY 0x{:04X}: {} 寄存器=0x{:02X}",
msg.error_code, msg.error_description(), msg.error_register);
}
// 调整历史容量
set_max_emcy_history_size(master.index(), 1, 100);
// 清除历史
emcy_clear_history(master.index(), 1);
完整示例
遍历对象字典
use ethercat::OdList;
let od = OdList::load(master.index(), 1)?;
println!("从站 1 对象字典: {} 个对象", od.len());
for obj in &od.objects {
println!("0x{:04X}: {} (代码=0x{:02X}, {} 条目)",
obj.index, obj.name, obj.object_code, obj.entries.len());
for entry in &obj.entries {
let rw = if entry.access.writable() { "RW" } else { "RO" };
println!(" [{:02X}] {}: {} {}bit [{}]",
entry.sub_index, entry.name, entry.data_type,
entry.bit_length, rw);
}
}
// 按索引查找
if let Some(obj) = od.find(0x6040) {
println!("0x{:04X}: {} ({} 条目)", obj.index, obj.name, obj.entries.len());
}
// 按名称模糊查找
let results = od.find_by_name("control");
for obj in results {
println!("找到: 0x{:04X} {}", obj.index, obj.name);
}
SDO 批量读取
let slave = master.slave(1);
// 读取设备信息
let device_type: u32 = slave.sdo_read_value(0x1000, 0x00)?;
let device_name: String = slave.sdo_read_value(0x1008, 0x00)?;
let vendor_id: u32 = slave.sdo_read_value(0x1018, 0x01)?;
let product_code: u32 = slave.sdo_read_value(0x1018, 0x02)?;
println!("设备: {} (VID=0x{:08X}, PID=0x{:08X})", device_name, vendor_id, product_code);
诊断历史 (Diagnosis History, 0x10F3)
ETG.1020 §16 定义从站通过 0x10F3 对象字典上报诊断消息的标准接口, 主站可查询
"是否有新消息 / 元数据 / 单条消息 / 确认已处理" 四类操作. Darra SDK 将上述流程封装在 CoEInstance
下, 全部返回 Result<T, CoeError>, 方便与 ? 运算符串联.
- 高频轮询用
poll_has_new_diagnostic()避免每次读完整字典 - 检测到新消息后
read_diagnostic_meta()获取最新 subindex 范围 - 按 subindex 顺序
read_diagnostic_message(msg_idx, 512)取原始字节 - 处理完用
acknowledge_diagnostic(msg_idx)让从站清理 Ring Buffer
CoeError 枚举
诊断历史专用错误类型, 通用 SDO 读写仍用 DarraError::SdoReadFailed / SdoWriteFailed.
#[derive(Debug, Clone)]
pub enum CoeError {
/// DLL 调用失败 (带 SDO Abort Code, 0 = 无中止码)
DllFailed { abort_code: u32 },
/// 参数越界
InvalidParameter(String),
/// 底层返回数据长度异常
InvalidResponse(String),
}
impl From<CoeError> for DarraError { /* 转为 DarraError::Other */ }
错误类别说明:
DllFailed— DLL 返回失败, 带 SDO Abort Code(从站不支持 0x10F3 / Abort 0x06020000)InvalidParameter— Rust 层参数校验失败(msg_idx 不在 6..=255 / buf_size 超界)InvalidResponse— DLL 返回长度异常(out_len > buf_size 等)
DiagMeta 结构
对齐 0x10F3 子索引 01/02/03/05, 对应 ETG.1020 Table 48/49.
#[derive(Debug, Clone, Copy, Default)]
pub struct DiagMeta {
/// 0x10F3:01 MaxMessages — 支持的最大消息数 (通常 <= 250)
pub max_messages: u8,
/// 0x10F3:02 NewestMessage — 最新消息所在 subindex
pub newest_message: u8,
/// 0x10F3:03 NewestAcknowledged — 已确认的最新 subindex
pub newest_acknowledged: u8,
/// 0x10F3:05 Flags — bit4=Ring/Linear, bit5=Overrun
pub flags: u16,
}
impl DiagMeta {
/// 是否为 Ring Buffer 模式 (bit4=1); false = Linear
pub fn is_ring_buffer(&self) -> bool { self.flags & 0x0010 != 0 }
/// 是否发生过 Overrun 消息丢失 (bit5=1)
pub fn has_overrun(&self) -> bool { self.flags & 0x0020 != 0 }
}
poll_has_new_diagnostic()
pub fn poll_has_new_diagnostic(&self) -> i32
快速轮询 0x10F3:04 "NewAvailable" 标志 (ETG.1020 §16.2). 无新消息时不读完整历史, 适合高频轮询.
返回值:
1— 有新消息0— 无新消息-1— 通信失败
read_diagnostic_meta()
pub fn read_diagnostic_meta(&self) -> Result<DiagMeta, CoeError>
读取 0x10F3 元数据 (MaxMessages / NewestMessage / NewestAcknowledged / Flags).
read_diagnostic_message()
pub fn read_diagnostic_message(
&self,
msg_idx: u16,
buf_size: usize,
) -> Result<Vec<u8>, CoeError>
读取指定 subindex (6..=255) 的诊断消息原始字节 (Octet String). 返回 Vec<u8> 长度即实际消息长度.
参数:
msg_idx— 消息子索引, 必须在6..=255buf_size— 接收缓冲容量, 建议 512 字节 (ETG Table 49 上限), 必须在1..=65536
acknowledge_diagnostic()
pub fn acknowledge_diagnostic(&self, msg_idx: u16) -> Result<(), CoeError>
确认指定 subindex (6..=255) 已处理, 写入 0x10F3:03 NewestAcknowledged. Ring 模式下从站会清理 <= msg_idx 的消息.
示例: 诊断历史轮询
use ethercat::slave::{CoEInstance, CoeError};
let coe = CoEInstance::new(master.index(), slave.index());
// 1. 高频轮询 "NewAvailable" 标志
if coe.poll_has_new_diagnostic() != 1 {
return Ok(());
}
// 2. 读取元数据
let meta = coe.read_diagnostic_meta()?;
println!(
"最大={} 最新={} 已确认={} Ring={} Overrun={}",
meta.max_messages,
meta.newest_message,
meta.newest_acknowledged,
meta.is_ring_buffer(),
meta.has_overrun(),
);
// 3. 按 subindex 顺序拉取新消息
let mut idx = meta.newest_acknowledged.saturating_add(1).max(6) as u16;
while idx <= meta.newest_message as u16 {
match coe.read_diagnostic_message(idx, 512) {
Ok(bytes) => {
println!("消息 [{:02X}] {} 字节: {:02X?}", idx, bytes.len(), &bytes[..bytes.len().min(16)]);
coe.acknowledge_diagnostic(idx)?;
}
Err(CoeError::DllFailed { abort_code }) => {
eprintln!("读 [{:02X}] 失败, Abort=0x{:08X}", idx, abort_code);
break;
}
Err(e) => {
eprintln!("读 [{:02X}] 失败: {}", idx, e);
break;
}
}
idx += 1;
}
- ETG.1020 §16 Diagnosis History Object
- ETG.1020 Table 48/49 — 0x10F3 子索引定义与 Flags 位域
诊断消息批量读取 (模块级函数)
read_diagnostic_messages
pub fn read_diagnostic_messages(master_index: u16, slave_index: u16) -> Vec<DiagnosticMessage>
一次性读取从站 0x10F3 全部诊断消息 (最多 20 条), 返回 DiagnosticMessage 列表。内部按 ETG.1510 协议: 先读 0x10F3:00 拿条数, 再循环读 0x10F3:N 解析 8 字节固定头 (DiagCode + Flags + TextIndex) 与剩余原始负载。读取失败的子索引会自动跳过。
对应 C# DarraEtherCAT.ReadDiagnosticMessages, 区别于 CoEInstance::read_diagnostic_message 的"按需单条 + 元数据"模式; 本函数适合一次性导出全部历史。
返回结构:
#[derive(Debug, Clone)]
pub struct DiagnosticMessage {
/// 子索引 (1-based)
pub sub_index: u8,
/// ETG 诊断代码 (32-bit)
pub diag_code: u32,
/// 标志位 (Type/Severity)
pub flags: u16,
/// 文本索引 (引用 ESI 字符串表)
pub text_index: u16,
/// 整段原始字节 (8 字节头 + 可选附加数据)
pub raw_data: Vec<u8>,
}
示例:
use ethercat::slave::coe::read_diagnostic_messages;
let msgs = read_diagnostic_messages(master.index(), slave.index());
println!("总共 {} 条诊断消息", msgs.len());
for m in msgs {
println!("{} → Code 0x{:08X}, Flags 0x{:04X}, TextIdx 0x{:04X}",
m.sub_index, m.diag_code, m.flags, m.text_index);
}
高频轮询场景仍优先使用 CoEInstance::poll_has_new_diagnostic, 仅在标志为真时调本函数, 避免每个周期遍历所有子索引带来的邮箱压力。
get_device_profile
pub fn get_device_profile(master_index: u16, slave_index: u16) -> u16
读取 0x1000 Device Type 低 16 位, 即 CiA 设备协议号 (例如 402=驱动, 401=通用 IO)。失败返回 0。便于在不加载完整 OD 的前提下快速分类设备。
let profile = get_device_profile(master.index(), slave.index());
match profile {
402 => println!("CiA 402 驱动"),
401 => println!("CiA 401 通用 IO"),
_ => println!("其它/未知 profile = {}", profile),
}