-
-
Notifications
You must be signed in to change notification settings - Fork 53
Description
Hello! I'm attempting to use a Kistler 5074B with ethercrab but configure_pdos_coe fails because the PDO mapping SDOs seem to not be readable. E.g. manually running in pre-op:
slave.sdo_read::<u8>(0x1600, 0).await.unwrap();
slave.sdo_read::<u32>(0x1600, 1).await.unwrap();
slave.sdo_read::<u8>(0x1A00, 0).await.unwrap();
slave.sdo_read::<u32>(0x1A00, 1).await.unwrap();
All fail:
Mailbox error for slave 0x1000 (supports complete access: false): 0x06040043: General parameter incompatibility reason
Even though those definitely do correspond to (fixed) PDOs in the ESI:
<RxPdo Sm="2" Mandatory="false" Fixed="true">
<Index>#x1600</Index>
<Name>Control Ch1</Name>
<Entry>
<Index>#x2000</Index>
<SubIndex>1</SubIndex>
<BitLen>8</BitLen>
<Name>Control Ch1 (0)</Name>
<DataType>BYTE</DataType>
</Entry>
</RxPdo>
<TxPdo Sm="3" Mandatory="false" Fixed="true">
<Index>#x1A00</Index>
<Name>Status Ch1</Name>
<Entry>
<Index>#x3000</Index>
<SubIndex>1</SubIndex>
<BitLen>8</BitLen>
<Name>Status Ch1</Name>
<DataType>BYTE</DataType>
</Entry>
</TxPdo>
Attempting to set up from eeprom also fails, though I want to be able to configure the PDO assignments and so didn't look into why all that closely.
Hopefully I'm not just doing something dumb; EtherCAT's new to me 😄
I have had success manually configuring the PDO assignments, SMs, and FMMUs, however there's no way for me to "inform" the group that I have done so, or, for example, use the PDI offset to play nicely with automatic setup of the other slaves.
If this is something you want to support, and you have a suggestion for what would be a sensible, acceptable API design, I might have time to implement it myself. Or, if I'm missing a better solution, I'd love to know.
Thanks for any input, and this lovely crate!
For reference, here's what I've gotten working so far:
//! Kistler 5074b Charge Amplifier
//!
//! cargo build --example kistler_5074b && sudo setcap cap_net_raw=eip target/debug/examples/kistler_5074b && RUST_LOG=debug RUST_BACKTRACE=1 target/debug/examples/kistler_5074b enx40ae306d11a8
use env_logger::Env;
use ethercrab::{
slave_group::{NoDc, PreOpPdi}, std::{ethercat_now, tx_rx_task}, Client, ClientConfig, Command, PduStorage, RegisterAddress, SlaveGroup, Timeouts
};
use futures_lite::StreamExt;
use std::{ops::Deref, sync::{atomic::{AtomicBool, Ordering}, Arc}, time::Duration};
use ethercrab_wire::EtherCrabWireRead;
/// Maximum number of slaves that can be stored. This must be a power of 2 greater than 1.
const MAX_SLAVES: usize = 128;
/// Maximum PDU data payload size - set this to the max PDI size or higher.
const MAX_PDU_DATA: usize = PduStorage::element_size(1100);
/// Maximum number of EtherCAT frames that can be in flight at any one time.
const MAX_FRAMES: usize = 16;
/// Maximum total PDI length.
const PDI_LEN: usize = 500;
static PDU_STORAGE: PduStorage<MAX_FRAMES, MAX_PDU_DATA> = PduStorage::new();
fn main() {
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.");
let (tx, rx, pdu_loop) = PDU_STORAGE.try_split().expect("can only split once");
let client = Arc::new(Client::new(
pdu_loop,
Timeouts::default(),
ClientConfig {
dc_static_sync_iterations: 0,
..ClientConfig::default()
},
));
smol::block_on(async {
smol::spawn(tx_rx_task(&interface, tx, rx).expect("spawn TX/RX task")).detach();
let mut group = client
.init_single_group::<MAX_SLAVES, PDI_LEN>(ethercat_now)
.await
.unwrap();
// Configure PDI, disable watchdogs
let pdi_length_bytes = {
let slave = group.slave(&client, 0).unwrap();
// slave.sdo_read::<u8>(0x1600, 0).await.unwrap();
// slave.sdo_read::<u32>(0x1600, 1).await.unwrap();
// slave.sdo_read::<u8>(0x1A00, 0).await.unwrap();
slave.sdo_read::<u32>(0x1A00, 1).await.unwrap();
let mut pdi_length_bytes = 0;
// XXX: EtherCrab does master read/inputs first. This order seems more sensible to me.
// Master write (outputs)
pdi_length_bytes += {
const SM_IDX: u8 = 2;
const FMMU_IDX: u8 = 0;
const PHYSICAL_START_ADDRESS: u16 = 0x1100;
// PDO mapping
// All PDOs are fixed, nothing to do.
// PDO assignments
// XXX: You have to play this little song and dance with the CoE array's length.
slave.sdo_write::<u8>(0x1C12, 0, 0).await.unwrap();
// Control Ch - BYTE
// XXX: Even though these are the default PDO assignments, you do have to reassign them for whatever reason :(
slave.sdo_write::<u16>(0x1C12, 1, 0x1600).await.unwrap();
slave.sdo_write::<u16>(0x1C12, 2, 0x1610).await.unwrap();
slave.sdo_write::<u16>(0x1C12, 3, 0x1620).await.unwrap();
slave.sdo_write::<u16>(0x1C12, 4, 0x1630).await.unwrap();
// Donzo
slave.sdo_write::<u8>(0x1C12, 0, 4).await.unwrap();
let additional_pdi_bytes: u16 = 4*1;
// Configure SM
Command::fpwr(slave.configured_address(), RegisterAddress::sync_manager(SM_IDX).into()).send(&client, SyncManagerChannel {
physical_start_address: PHYSICAL_START_ADDRESS,
length_bytes: additional_pdi_bytes,
control: Control {
operation_mode: OperationMode::Normal,
direction: Direction::MasterWrite,
ecat_event_enable: true,
dls_user_event_enable: true,
// XXX: Doesn't matter since all will be disabled but might as well match the ESI XML regardless *shrug*
watchdog_enable: true,
},
status: Status::default(),
enable: Enable {
enable: true,
..Enable::default()
},
})
.await.unwrap();
// Configure FMMU
Command::fpwr(slave.configured_address(), RegisterAddress::fmmu(FMMU_IDX).into()).send(&client, Fmmu {
logical_start_address: pdi_length_bytes.into(),
length_bytes: additional_pdi_bytes,
// Mapping into PDI is byte-aligned until/if we support bit-oriented slaves
logical_start_bit: 0,
// Always byte-aligned
logical_end_bit: 7,
physical_start_address: PHYSICAL_START_ADDRESS,
physical_start_bit: 0,
read_enable: false,
write_enable: true,
enable: true,
}).await.unwrap();
additional_pdi_bytes
};
// Master read (inputs)
pdi_length_bytes += {
const SM_IDX: u8 = 3;
const FMMU_IDX: u8 = 1;
const PHYSICAL_START_ADDRESS: u16 = 0x1400;
// PDO mapping
// All PDOs are fixed, nothing to do.
// PDO assignments
// XXX: You have to play this little song and dance with the CoE array's length.
slave.sdo_write::<u8>(SM_BASE_ADDRESS + u16::from(SM_IDX), 0, 0).await.unwrap();
// Status ChX - BYTE
slave.sdo_write::<u16>(SM_BASE_ADDRESS + u16::from(SM_IDX), 1, 0x1A00).await.unwrap();
slave.sdo_write::<u16>(SM_BASE_ADDRESS + u16::from(SM_IDX), 2, 0x1A10).await.unwrap();
slave.sdo_write::<u16>(SM_BASE_ADDRESS + u16::from(SM_IDX), 3, 0x1A20).await.unwrap();
slave.sdo_write::<u16>(SM_BASE_ADDRESS + u16::from(SM_IDX), 4, 0x1A30).await.unwrap();
// Scaled Value ChX - REAL
slave.sdo_write::<u16>(SM_BASE_ADDRESS + u16::from(SM_IDX), 5, 0x1A01).await.unwrap();
slave.sdo_write::<u16>(SM_BASE_ADDRESS + u16::from(SM_IDX), 6, 0x1A11).await.unwrap();
slave.sdo_write::<u16>(SM_BASE_ADDRESS + u16::from(SM_IDX), 7, 0x1A21).await.unwrap();
slave.sdo_write::<u16>(SM_BASE_ADDRESS + u16::from(SM_IDX), 8, 0x1A31).await.unwrap();
// Donzo
slave.sdo_write::<u8>(SM_BASE_ADDRESS + u16::from(SM_IDX), 0, 8).await.unwrap();
let additional_pdi_bytes: u16 = (4*1) + (4*4);
// Configure SM
Command::fpwr(slave.configured_address(), RegisterAddress::sync_manager(SM_IDX).into()).send(&client, SyncManagerChannel {
physical_start_address: PHYSICAL_START_ADDRESS,
length_bytes: additional_pdi_bytes,
control: Control {
operation_mode: OperationMode::Normal,
direction: Direction::MasterRead,
ecat_event_enable: true,
dls_user_event_enable: true,
watchdog_enable: false,
},
status: Status::default(),
enable: Enable {
enable: true,
..Enable::default()
},
})
.await.unwrap();
// Configure FMMU
Command::fpwr(slave.configured_address(), RegisterAddress::fmmu(FMMU_IDX).into()).send(&client, Fmmu {
logical_start_address: pdi_length_bytes.into(),
length_bytes: additional_pdi_bytes,
// Mapping into PDI is byte-aligned until/if we support bit-oriented slaves
logical_start_bit: 0,
// Always byte-aligned
logical_end_bit: 7,
physical_start_address: PHYSICAL_START_ADDRESS,
physical_start_bit: 0,
read_enable: true,
write_enable: false,
enable: true,
}).await.unwrap();
additional_pdi_bytes
};
// Disable PSI and all sync manager watchdogs via timer scaler
// XXX: Saw a note that this might not be supported for all slaves
Command::fpwr(slave.configured_address(), 0x0410u16).send(&client, 0u16).await.unwrap();
Command::fpwr(slave.configured_address(), 0x0420u16).send(&client, 0u16).await.unwrap();
pdi_length_bytes
};
// XXX: PDI has now been configured manually, though ethercrab's metadata about the PDI has not so we'll have to interact with it ourselves.
let group = unsafe { std::mem::transmute::<_, SlaveGroup<MAX_SLAVES, PDI_LEN, PreOpPdi, NoDc>>(group)};
group.into_op(&client).await.unwrap();
fn print_pdi(pdi: &[u8]) {
println!();
for i in 0..4 {
println!("Channel {i} control: {:08b}", pdi[i]);
}
for i in 0..4 {
println!("Channel {i} status: {:08b}", pdi[4+i]);
}
for i in 0..4 {
let start = 8 + (4*i);
let end = start + 4;
println!("Channel {i} value (pC): {:12.3}", f32::unpack_from_slice(pdi[start..end].try_into().unwrap()).unwrap());
}
}
async fn tx_rx<'client>(client: &'client Client<'client>, pdi: &mut [u8]) {
// XXX: Function signature lifetimes should be changed such that allocating a new vec isn't necessary.
let received = Command::lrw(0).with_wkc(3).send_receive_slice(&client, &*pdi).await.unwrap().deref().to_owned();
pdi.copy_from_slice(&received);
print_pdi(&pdi);
}
let mut pdi = vec![0u8; pdi_length_bytes.into()];
// Zero the charge amp.
println!("\nSTARTING");
pdi[..4].fill(0);
tx_rx(&client, &mut pdi).await;
assert!(pdi[4..8].iter().all(|status| status & 1 == 0));
println!("\nWAITING FOR ZERO TO COMPLETE");
pdi[..4].fill(1);
while pdi[4..8].iter().any(|status| status & 1 == 0) {
tx_rx(&client, &mut pdi).await;
}
println!("\nBEGINNING OPERATION");
let mut cycle_time = smol::Timer::interval(Duration::from_millis(1000));
let shutdown = Arc::new(AtomicBool::new(false));
signal_hook::flag::register(signal_hook::consts::SIGINT, Arc::clone(&shutdown))
.expect("Register hook");
loop {
// Graceful shutdown on Ctrl + C
if shutdown.load(Ordering::Relaxed) {
log::info!("Shutting down...");
break;
}
tx_rx(&client, &mut pdi).await;
cycle_time.next().await;
}
});
log::info!("Done.");
}
// =============== COPIED FROM ETHERCRAB =================
/// // ETG1000.6 Table 67 – CoE Communication Area, the address of the first sync manager.
pub const SM_BASE_ADDRESS: u16 = 0x1C10;
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, ethercrab_wire::EtherCrabWireReadWrite)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[wire(bits = 2)]
#[repr(u8)]
pub enum OperationMode {
#[default]
Normal = 0x00,
Mailbox = 0x02,
}
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, ethercrab_wire::EtherCrabWireReadWrite)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[wire(bits = 2)]
#[repr(u8)]
pub enum Direction {
#[default]
MasterRead = 0x00,
MasterWrite = 0x01,
}
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, ethercrab_wire::EtherCrabWireReadWrite)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[wire(bytes = 1)]
pub struct Control {
#[wire(bits = 2)]
pub operation_mode: OperationMode,
#[wire(bits = 2)]
pub direction: Direction,
#[wire(bits = 1)]
pub ecat_event_enable: bool,
#[wire(bits = 1)]
pub dls_user_event_enable: bool,
#[wire(bits = 1, post_skip = 1)]
pub watchdog_enable: bool,
// reserved1: bool
}
#[derive(Default, Copy, Clone, PartialEq, Eq, ethercrab_wire::EtherCrabWireReadWrite)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[wire(bytes = 8)]
pub struct SyncManagerChannel {
#[wire(bytes = 2)]
pub physical_start_address: u16,
#[wire(bytes = 2)]
pub length_bytes: u16,
#[wire(bytes = 1)]
pub control: Control,
#[wire(bytes = 1)]
pub status: Status,
#[wire(bytes = 2)]
pub enable: Enable,
}
/// Buffer state.
///
/// Somewhat described in ETG1000.4 Figure 32 – SyncM mailbox interaction.
///
/// In cyclic mode the buffers need to be tripled. It's unclear why from the spec but that's what it
/// says.
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, ethercrab_wire::EtherCrabWireReadWrite)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[wire(bits = 2)]
#[repr(u8)]
pub enum BufferState {
/// First buffer.
#[default]
First = 0x00,
/// Second buffer.
Second = 0x01,
/// Third buffer.
Third = 0x02,
/// Next buffer.
Next = 0x03,
}
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, ethercrab_wire::EtherCrabWireReadWrite)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[wire(bytes = 1)]
pub struct Status {
#[wire(bits = 1)]
pub has_write_event: bool,
#[wire(bits = 1, post_skip = 1)]
pub has_read_event: bool,
// reserved1: bool
#[wire(bits = 1)]
pub mailbox_full: bool,
#[wire(bits = 2)]
pub buffer_state: BufferState,
#[wire(bits = 1)]
pub read_buffer_open: bool,
#[wire(bits = 1)]
pub write_buffer_open: bool,
}
/// ETG1000.4 Table 56 – Fieldbus memory management unit (FMMU) entity.
#[derive(Default, Copy, Clone, PartialEq, Eq, ethercrab_wire::EtherCrabWireReadWrite)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[wire(bytes = 16)]
pub struct Fmmu {
/// This parameter shall contain the start address in octets in the logical memory area of the
/// memory translation.
#[wire(bytes = 4)]
pub logical_start_address: u32,
#[wire(bytes = 2)]
pub length_bytes: u16,
#[wire(bits = 3, post_skip = 5)]
pub logical_start_bit: u8,
#[wire(bits = 3, post_skip = 5)]
pub logical_end_bit: u8,
#[wire(bytes = 2)]
pub physical_start_address: u16,
#[wire(bits = 3, post_skip = 5)]
pub physical_start_bit: u8,
#[wire(bits = 1)]
pub read_enable: bool,
#[wire(bits = 1, post_skip = 6)]
pub write_enable: bool,
// Lots of spare bytes after this one!
#[wire(bits = 1, post_skip = 31)]
pub enable: bool,
}
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, ethercrab_wire::EtherCrabWireReadWrite)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[wire(bytes = 2)]
pub struct Enable {
#[wire(bits = 1)]
pub enable: bool,
#[wire(bits = 1, post_skip = 4)]
pub repeat: bool,
// reserved4: u8
/// DC Event 0 with EtherCAT write.
///
/// Set to `true` to enable DC 0 events on EtherCAT writes.
#[wire(bits = 1)]
pub enable_dc_event_bus_write: bool,
/// DC Event 0 with local write.
///
/// Set to `true` to enable DC 0 events from local writes.
#[wire(bits = 1)]
pub enable_dc_event_local_write: bool,
#[wire(bits = 1)]
pub channel_pdi_disabled: bool,
#[wire(bits = 1, post_skip = 6)]
pub repeat_ack: bool,
// reserved6: u8,
}