Re-implementation of rustbootd's ecat_el6695_* examples as a single binary with fixes found in review and on hardware: - DC-follow PLL hardened against period-2 hunting: slew-limited anchor (+/-50us/cycle), bistable-trap snap re-anchor, re-prime on stale deadline - drift-free absolute-grid ticker mode; probe mode for timestamp forensics - bounded-memory online stats (histograms), graceful SIGINT/SIGTERM shutdown with full report, error-streak abort - timestamp plausibility filter comparing against the previous raw sample (avoids the deadlock after a startup outlier) - XFC scope waveform options: --el2202 (with --el2202-dual), --el2262, --el1252 latch timestamp readback with per-channel edge statistics - register access unified in regs.rs: named bit constants everywhere, read-modify-write for enable/activation bytes - vendored patched ethercrab 0.7.1 (sdo_write_complete, send_raw_coe) Verified on J1900 (PREEMPT_RT 6.6.135): 600k cycles/600s exact 1 kHz, tx/rx zero errors, phase_err p50=164us std=5us; EL2202<->EL1252 loopback edge interval mean 2000.24us std=24.65us.
367 lines
12 KiB
Plaintext
367 lines
12 KiB
Plaintext
//! Configure a Kollmorgen AKD servo drive and put it in enabled state.
|
||
|
||
use env_logger::Env;
|
||
use ethercrab::{
|
||
error::{Error, MailboxError},
|
||
std::{ethercat_now, tx_rx_task},
|
||
MainDevice, MainDeviceConfig, PduStorage, Timeouts,
|
||
};
|
||
use std::{sync::Arc, time::Duration};
|
||
use tokio::time::MissedTickBehavior;
|
||
|
||
/// Maximum number of SubDevices that can be stored. This must be a power of 2 greater than 1.
|
||
const MAX_SUBDEVICES: usize = 16;
|
||
const MAX_PDU_DATA: usize = PduStorage::element_size(1100);
|
||
const MAX_FRAMES: usize = 16;
|
||
const PDI_LEN: usize = 64;
|
||
|
||
static PDU_STORAGE: PduStorage<MAX_FRAMES, MAX_PDU_DATA> = PduStorage::new();
|
||
|
||
#[tokio::main]
|
||
async fn main() -> Result<(), Error> {
|
||
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
|
||
|
||
let interface = std::env::args()
|
||
.nth(1)
|
||
.expect("Provide network interface as first argument.");
|
||
|
||
log::info!("Starting AKD demo...");
|
||
log::info!("Ensure a Kollmorgen AKD drive is the first SubDevice");
|
||
log::info!("Run with RUST_LOG=ethercrab=debug or =trace for debug information");
|
||
|
||
let (tx, rx, pdu_loop) = PDU_STORAGE.try_split().expect("can only split once");
|
||
|
||
let maindevice = Arc::new(MainDevice::new(
|
||
pdu_loop,
|
||
Timeouts {
|
||
wait_loop_delay: Duration::from_millis(2),
|
||
mailbox_response: Duration::from_millis(1000),
|
||
..Default::default()
|
||
},
|
||
MainDeviceConfig::default(),
|
||
));
|
||
|
||
tokio::spawn(tx_rx_task(&interface, tx, rx).expect("spawn TX/RX task"));
|
||
|
||
let mut group = maindevice
|
||
.init_single_group::<MAX_SUBDEVICES, PDI_LEN>(ethercat_now)
|
||
.await
|
||
.expect("Init");
|
||
|
||
for subdevice in group.iter(&maindevice) {
|
||
// --- Reads ---
|
||
|
||
// // Name
|
||
// dbg!(subdevice
|
||
// .read_sdo::<heapless::String<64>>(0x1008, SdoAccess::Index(0))
|
||
// .await
|
||
// .unwrap());
|
||
|
||
// // Software version. For AKD, this should equal "M_01-20-00-003"
|
||
// dbg!(subdevice
|
||
// .read_sdo::<heapless::String<64>>(0x100a, SdoAccess::Index(0))
|
||
// .await
|
||
// .unwrap());
|
||
|
||
// --- Writes ---
|
||
|
||
let profile = match subdevice.sdo_read::<u32>(0x1000, 0).await {
|
||
Err(Error::Mailbox(MailboxError::NoMailbox)) => Ok(None),
|
||
Ok(device_type) => Ok(Some(device_type & 0xffff)),
|
||
Err(e) => Err(e),
|
||
}?;
|
||
|
||
// CiA 402/DS402 device
|
||
if profile == Some(402) {
|
||
log::info!("SubDevice {} supports DS402", subdevice.name());
|
||
}
|
||
|
||
// AKD config
|
||
if subdevice.name() == "AKD" {
|
||
subdevice.sdo_write(0x1c12, 0, 0u8).await?;
|
||
// 0x1702 = fixed velocity mapping
|
||
subdevice.sdo_write(0x1c12, 1, 0x1702u16).await?;
|
||
subdevice.sdo_write(0x1c12, 0, 0x01u8).await?;
|
||
|
||
// Must set both read AND write SDOs for AKD otherwise it times out going into OP
|
||
subdevice.sdo_write(0x1c13, 0, 0u8).await?;
|
||
subdevice.sdo_write(0x1c13, 1, 0x1B01u16).await?;
|
||
subdevice.sdo_write(0x1c13, 0, 0x01u8).await?;
|
||
|
||
// Opmode - Cyclic Synchronous Position
|
||
// subdevice.write_sdo(0x6060, 0, 0x08).await?;
|
||
// Opmode - Cyclic Synchronous Velocity
|
||
subdevice.sdo_write(0x6060, 0, 0x09u8).await?;
|
||
|
||
{
|
||
// Shows up as default value of 2^20, but I'm using a 2^32 counts/rev encoder.
|
||
let encoder_increments = subdevice.sdo_read::<u32>(0x608f, 1).await?;
|
||
let num_revs = subdevice.sdo_read::<u32>(0x608f, 2).await?;
|
||
|
||
let gear_ratio_motor = subdevice.sdo_read::<u32>(0x6091, 1).await?;
|
||
let gear_ratio_final = subdevice.sdo_read::<u32>(0x6091, 2).await?;
|
||
|
||
let feed = subdevice.sdo_read::<u32>(0x6092, 1).await?;
|
||
let shaft_revolutions = subdevice.sdo_read::<u32>(0x6092, 2).await?;
|
||
|
||
let counts_per_rev = encoder_increments / num_revs;
|
||
|
||
log::info!("Drive info");
|
||
log::info!("--> Encoder increments {}", encoder_increments);
|
||
log::info!("--> Number of revolutions {}", num_revs);
|
||
log::info!("--> Gear ratio (motor) {}", gear_ratio_motor);
|
||
log::info!("--> Gear ratio (final) {}", gear_ratio_final);
|
||
log::info!("--> Feed {}", feed);
|
||
log::info!("--> Shaft revolutions {}", shaft_revolutions);
|
||
log::info!("--> Counts per rev {}", counts_per_rev);
|
||
}
|
||
}
|
||
}
|
||
|
||
let group = group.into_op(&maindevice).await.expect("PRE-OP -> OP");
|
||
|
||
log::info!("SubDevices moved to OP state");
|
||
|
||
log::info!("Discovered {} SubDevices", group.len());
|
||
|
||
let mut subdevice = group
|
||
.subdevice(&maindevice, 0)
|
||
.expect("first SubDevice not found");
|
||
|
||
// Run twice to prime PDI
|
||
group.tx_rx(&maindevice).await.expect("TX/RX");
|
||
|
||
let cycle_time = {
|
||
let base = subdevice.sdo_read::<u8>(0x60c2, 1).await?;
|
||
let x10 = subdevice.sdo_read::<i8>(0x60c2, 2).await?;
|
||
|
||
let base = f32::from(base);
|
||
let x10 = 10.0f32.powi(i32::from(x10));
|
||
|
||
let cycle_time_ms = (base * x10) * 1000.0;
|
||
|
||
Duration::from_millis(unsafe { cycle_time_ms.round().to_int_unchecked() })
|
||
};
|
||
|
||
log::info!("Cycle time: {} ms", cycle_time.as_millis());
|
||
|
||
// AKD will error with F706 if cycle time is not 2ms or less, but we're reading it from the
|
||
// drive so we should be fine.
|
||
let mut cyclic_interval = tokio::time::interval(cycle_time);
|
||
cyclic_interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||
|
||
// Check for and clear faults
|
||
{
|
||
log::info!("Checking faults");
|
||
|
||
group.tx_rx(&maindevice).await.expect("TX/RX");
|
||
|
||
let (i, o) = subdevice.io_raw_mut();
|
||
|
||
let status = {
|
||
let status = u16::from_le_bytes(i[4..=5].try_into().unwrap());
|
||
|
||
AkdStatusWord::from_bits_truncate(status)
|
||
};
|
||
|
||
if status.contains(AkdStatusWord::FAULT) {
|
||
log::warn!("Fault! Clearing...");
|
||
|
||
let (_pos_cmd, control) = o.split_at_mut(4);
|
||
let reset = AkdControlWord::RESET_FAULT;
|
||
let reset = reset.bits().to_le_bytes();
|
||
control.copy_from_slice(&reset);
|
||
|
||
loop {
|
||
group.tx_rx(&maindevice).await.expect("TX/RX");
|
||
|
||
let (i, _o) = subdevice.io_raw_mut();
|
||
|
||
let status = {
|
||
let status = u16::from_le_bytes(i[4..=5].try_into().unwrap());
|
||
|
||
AkdStatusWord::from_bits_truncate(status)
|
||
};
|
||
|
||
if !status.contains(AkdStatusWord::FAULT) {
|
||
log::info!("Fault cleared, status is now {status:?}");
|
||
|
||
break;
|
||
}
|
||
|
||
cyclic_interval.tick().await;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Shutdown state
|
||
{
|
||
log::info!("Putting drive in shutdown state");
|
||
|
||
let (_i, o) = subdevice.io_raw_mut();
|
||
|
||
let (_pos_cmd, control) = o.split_at_mut(4);
|
||
let value = AkdControlWord::SHUTDOWN;
|
||
let value = value.bits().to_le_bytes();
|
||
control.copy_from_slice(&value);
|
||
|
||
loop {
|
||
group.tx_rx(&maindevice).await.expect("TX/RX");
|
||
|
||
let (i, _o) = subdevice.io_raw_mut();
|
||
|
||
let status = {
|
||
let status = u16::from_le_bytes(i[4..=5].try_into().unwrap());
|
||
|
||
AkdStatusWord::from_bits_truncate(status)
|
||
};
|
||
|
||
if status.contains(AkdStatusWord::READY_TO_SWITCH_ON) {
|
||
log::info!("Drive is shut down");
|
||
|
||
break;
|
||
}
|
||
|
||
cyclic_interval.tick().await;
|
||
}
|
||
}
|
||
|
||
// Switch drive on
|
||
{
|
||
log::info!("Switching drive on");
|
||
|
||
let (_i, o) = subdevice.io_raw_mut();
|
||
|
||
let (_pos_cmd, control) = o.split_at_mut(4);
|
||
let reset = AkdControlWord::SWITCH_ON
|
||
| AkdControlWord::DISABLE_VOLTAGE
|
||
| AkdControlWord::QUICK_STOP;
|
||
let reset = reset.bits().to_le_bytes();
|
||
control.copy_from_slice(&reset);
|
||
|
||
loop {
|
||
group.tx_rx(&maindevice).await.expect("TX/RX");
|
||
|
||
let (i, o) = subdevice.io_raw_mut();
|
||
|
||
let status = {
|
||
let status = u16::from_le_bytes(i[4..=5].try_into().unwrap());
|
||
|
||
AkdStatusWord::from_bits_truncate(status)
|
||
};
|
||
|
||
if status.contains(AkdStatusWord::SWITCHED_ON) {
|
||
log::info!("Drive switched on, begin cyclic operation");
|
||
|
||
let (_pos_cmd, control) = o.split_at_mut(4);
|
||
|
||
// Enable operation so we can send cyclic data
|
||
let state = AkdControlWord::SWITCH_ON
|
||
| AkdControlWord::DISABLE_VOLTAGE
|
||
| AkdControlWord::QUICK_STOP
|
||
| AkdControlWord::ENABLE_OP;
|
||
let state = state.bits().to_le_bytes();
|
||
control.copy_from_slice(&state);
|
||
|
||
break;
|
||
}
|
||
|
||
cyclic_interval.tick().await;
|
||
}
|
||
}
|
||
|
||
let mut velocity: i32 = 0;
|
||
|
||
loop {
|
||
group.tx_rx(&maindevice).await.expect("TX/RX");
|
||
|
||
let (i, o) = subdevice.io_raw_mut();
|
||
|
||
let (pos, status) = {
|
||
let pos = u32::from_le_bytes(i[0..=3].try_into().unwrap());
|
||
let status = u16::from_le_bytes(i[4..=5].try_into().unwrap());
|
||
|
||
let status = AkdStatusWord::from_bits_truncate(status);
|
||
|
||
(pos, status)
|
||
};
|
||
|
||
println!("Position: {pos}, Status: {status:?} | {:?}", o);
|
||
|
||
let (pos_cmd, _control) = o.split_at_mut(4);
|
||
|
||
pos_cmd.copy_from_slice(&velocity.to_le_bytes());
|
||
|
||
if velocity < 1_000_000 {
|
||
velocity += 200;
|
||
}
|
||
|
||
cyclic_interval.tick().await;
|
||
}
|
||
}
|
||
|
||
bitflags::bitflags! {
|
||
/// AKD EtherCAT Communications Manual section 5.3.55
|
||
struct AkdControlWord: u16 {
|
||
/// Switch on
|
||
const SWITCH_ON = 1 << 0;
|
||
/// Disable Voltage
|
||
const DISABLE_VOLTAGE = 1 << 1;
|
||
/// Quick Stop
|
||
const QUICK_STOP = 1 << 2;
|
||
/// Enable Operation
|
||
const ENABLE_OP = 1 << 3;
|
||
/// Operation mode specific
|
||
const OP_SPECIFIC_1 = 1 << 4;
|
||
/// Operation mode specific
|
||
const OP_SPECIFIC_2 = 1 << 5;
|
||
/// Operation mode specific
|
||
const OP_SPECIFIC_3 = 1 << 6;
|
||
/// Reset Fault (only effective for faults)
|
||
const RESET_FAULT = 1 << 7;
|
||
/// Pause/halt
|
||
const PAUSE = 1 << 8;
|
||
|
||
const SHUTDOWN = Self::DISABLE_VOLTAGE.bits() | Self::QUICK_STOP.bits();
|
||
}
|
||
}
|
||
|
||
bitflags::bitflags! {
|
||
/// AKD EtherCAT Communications Manual section 5.3.56
|
||
#[derive(Debug)]
|
||
struct AkdStatusWord: u16 {
|
||
/// Ready to switch on
|
||
const READY_TO_SWITCH_ON = 1 << 0;
|
||
/// Switched on
|
||
const SWITCHED_ON = 1 << 1;
|
||
/// Operation enabled
|
||
const OP_ENABLED = 1 << 2;
|
||
/// Fault
|
||
const FAULT = 1 << 3;
|
||
/// Voltage enabled
|
||
const VOLTAGE_ENABLED = 1 << 4;
|
||
/// Quick stop
|
||
const QUICK_STOP = 1 << 5;
|
||
/// Switch on disabled
|
||
const SWITCH_ON_DISABLED = 1 << 6;
|
||
/// Warning
|
||
const WARNING = 1 << 7;
|
||
/// STO – Safe Torque Off
|
||
const STO = 1 << 8;
|
||
/// Remote
|
||
const REMOTE = 1 << 9;
|
||
/// Target reached
|
||
const TARGET_REACHED = 1 << 10;
|
||
/// Internal limit active
|
||
const INTERNAL_LIMIT = 1 << 11;
|
||
/// Operation mode specific (reserved)
|
||
const OP_SPECIFIC_1 = 1 << 12;
|
||
/// Operation mode specific (reserved)
|
||
const OP_SPECIFIC_2 = 1 << 13;
|
||
/// Manufacturer-specific (reserved)
|
||
const MAN_SPECIFIC_1 = 1 << 14;
|
||
/// Manufacturer-specific (reserved)
|
||
const MAN_SPECIFIC_2 = 1 << 15;
|
||
}
|
||
}
|