跳到主要内容

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)。

名称说明
NoError0x00000000无错误
ToggleBitNotChanged0x05030000Toggle 位未改变
SdoProtocolTimeout0x05040000SDO 协议超时
InvalidCommandSpecifier0x05040001无效的命令标识符
OutOfMemory0x05040005内存不足
UnsupportedAccess0x06010000不支持的访问
ReadWriteOnlyError0x06010001尝试读取只写对象
WriteReadOnlyError0x06010002尝试写入只读对象
SubindexWriteError0x06010003子索引不允许写入
CompleteAccessNotSupported0x06010004不支持完全访问
ObjectLengthExceeded0x06010005对象长度超限
ObjectMappedToRxPdo0x06010006对象已映射到 RxPDO
ObjectDoesNotExist0x06020000对象不存在
CannotBeMappedToPdo0x06040041不可映射到 PDO
ExceedsPdoLength0x06040042超出 PDO 长度
ParameterIncompatibility0x06040043参数不兼容
InternalIncompatibility0x06040047内部不兼容
HardwareAccessError0x06060000硬件访问错误
DataTypeMismatch0x06070010数据类型不匹配
DataTypeTooHigh0x06070012数据类型长度过大
DataTypeTooLow0x06070013数据类型长度过小
SubindexDoesNotExist0x06090011子索引不存在
ValueRangeExceeded0x06090030值超出范围
ValueTooHigh0x06090031值过大
ValueTooLow0x06090032值过小
ModuleIdentListMismatch0x06090033模块列表不匹配
MaxLessThanMin0x06090036最大值小于最小值
GeneralError0x08000000一般错误
DataTransferError0x08000020数据传输错误
LocalControlError0x08000021本地控制错误
DeviceStateError0x08000022设备状态错误
DictionaryError0x08000023字典错误
UnknownError0xFFFFFFFF未知错误

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..=255
  • buf_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);
}
配合 0x10F3:04 NewAvailable

高频轮询场景仍优先使用 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),
}