-
Notifications
You must be signed in to change notification settings - Fork 2
Application Guide
This guide walks through a complete program that reads an EL1002 (2× digital input) and drives an EL2002 (2× digital output): each output follows its input. Along the way it covers every step a real application goes through. Architecture explains the mechanisms behind each step.
The examples el1002_minimal.rs,
el2004_minimal.rs and
input_output.rs follow the same structure.
If you don't know which NIC the bus is connected to, interface_discovery can find it:
use ethercat_hal::interface_discovery::{list_ethernet_interfaces, test_interface, LinkType};
for iface in list_ethernet_interfaces()? {
if matches!(iface.link_type, LinkType::Link) && test_interface(&iface.name).is_ok() {
println!("{} has an EtherCAT bus", iface.name);
}
}cargo run --example discovery does the same thing.
use ethercat_hal::{init_ethercat, MasterConfiguration, EtherCATState};
let control = init_ethercat(&interface, None); // None = MasterConfiguration::default()
let channel = control.channel.clone(); // command side, Clone
let mut handle = control.app_handle; // data side, single ownerThe defaults are worth knowing:
| Field | Default |
|---|---|
target_cycle_time_us |
1000 |
dc_config.sync0_period |
1 ms |
dc_config.sync0_shift |
500 µs |
dc_config.start_delay |
100 ms |
wkc_mismatch_threshold |
5 |
op_ramp_grace_cycles |
10 000 |
realtime_optimizations |
None |
If you change the cycle time in most cases keep sync0_period equal to it except for more advanced use cases like Oversampling for example.
channel.request_state_change(EtherCATState::PreOp)?; // returns immediately
while handle.get_state() != EtherCATState::PreOp {
std::thread::sleep(Duration::from_millis(10));
}
for sd in handle.try_get_subdevices_vec_sync()? {
println!("{:#06x} {} vendor={:#x} product={:#x} rev={:#x}",
sd.device_address, sd.get_name()?, sd.vendor, sd.product_id, sd.revision);
}At this point each MetaSubdevice has its identity and device_address filled in. Its PDO offsets
are still 0.
All CoE/SDO traffic has to happen now. After the move to Op, the controller no longer services the request queue.
The EL1002 and EL2002 don't need any configuration: their default PDO assignment is already what the
drivers expect. If you want to write the assignment explicitly, the PDO structs implement
coe::Configuration (derived, see device drivers):
use ethercat_hal::coe::Configuration;
el1002.txpdo.write_config(channel.clone(), sd.device_address)?; // writes 0x1C13
el2002.rxpdo.write_config(channel.clone(), sd.device_address)?; // writes 0x1C12Terminals with real settings expose them through ConfigurableDevice<C>, and some also need DC sync.
From el1259_minimal.rs:
el1259.write_config(channel.clone(), sd.device_address, &el1259.get_config())?;
channel.enable_dc_sync0(sd.device_address)?;You can also read and write raw SDOs. The supported types are bool, u8, u16, i16, u32 and
i32:
let v: u16 = channel.sdo_read(sd.device_address, 0x8000, 0x01)?;
channel.sdo_write(sd.device_address, 0x8000, 0x01, 42u16)?;channel.request_state_change(EtherCATState::Op)?;
while handle.get_state() != EtherCATState::Op {
std::thread::sleep(Duration::from_millis(10));
}
while !handle.check_all_op() { // every subdevice actually reached Op
std::thread::sleep(Duration::from_millis(10));
}
let subdevices = handle.try_get_subdevices_vec_sync()?; // re-read: offsets are valid nowAlways read the subdevice list again after Op. start_tx, end_tx, start_rx and end_rx are only
computed during the move to Op.
If Op never arrives, check handle.get_last_transition_failure()
(Diagnostics).
Look up each subdevice's byte ranges once, before the loop:
use ethercat_hal::devices::beckhoff_modules::{
el1002::{EL1002, EL1002_IDENTITY_A},
el2002::{EL2002, EL2002_IDENTITY_A, EL2002_IDENTITY_B},
};
fn ident(sd: &MetaSubdevice) -> (u32, u32, u32) {
(sd.vendor, sd.product_id, sd.revision)
}
let di = subdevices.iter().find(|sd| ident(sd) == EL1002_IDENTITY_A).expect("no EL1002");
let dout = subdevices.iter()
.find(|sd| matches!(ident(sd), EL2002_IDENTITY_A | EL2002_IDENTITY_B))
.expect("no EL2002");
let mut el1002 = EL1002::new();
let mut el2002 = EL2002::new();Matching on the full identity tuple (vendor, product, revision) is stricter than matching on
product_id alone, and it tells you whether a driver supports that particular revision.
If you would rather construct drivers generically, devices::device_from_subdevice_identity(sd)
returns a Box<dyn EthercatDevice> for any supported identity, and you can downcast it with
as_any_mut().
use bitvec::{order::Lsb0, slice::BitSlice};
use ethercat_hal::devices::EthercatDevice;
use ethercat_hal::io::{digital_input::DigitalInputDevice, digital_output::DigitalOutputDevice};
let mut last_cycle = handle.get_current_cycle();
loop {
// Wait for a fresh cycle (optional: without this you may process the same snapshot twice).
while handle.get_current_cycle() == last_cycle { std::hint::spin_loop(); }
last_cycle = handle.get_current_cycle();
// 1. Decode inputs
if let Some(inputs) = handle.get_inputs() {
el1002.input(BitSlice::<u8, Lsb0>::from_slice(&inputs[di.start_tx..di.end_tx]))?;
}
// 2. Application logic, through the io traits
for port in 0..el1002.get_port_count() {
el2002.set_output(port, el1002.get_input(port)?);
}
// 3. Encode outputs, then hand them to the controller
if let Some(outputs) = handle.write_outputs() {
el2002.output(BitSlice::<u8, Lsb0>::from_slice_mut(&mut outputs[dout.start_rx..dout.end_rx]))?;
handle.send_outputs();
}
}Timing rules:
- Inputs are published at the start of each cycle.
- Outputs are picked up about 10 µs before the next cycle starts. Anything you send after that goes out one cycle later.
-
write_outputs()returnsNonewhile the controller still holds the previous buffer. Skip writing in that case. The controller releases the buffer once per cycle. -
Sleep instead of spinning (for example
std::thread::sleep) if you don't need to act on every cycle. Inputs always return the latest snapshot, and outputs persist until you overwrite them.
println!("cycle {} took {} µs", handle.get_current_cycle(), handle.get_cycle_time_us());
// Serviced in Op too:
for status in channel.al_status_snapshot()? {
if status.is_faulty() { eprintln!("{status}"); }
}If the state-machine thread exits because of an error, you get the error from
control.join_handle.take().unwrap().join(), and get_transition_reports() still returns the full
history.
| Symptom | Cause |
|---|---|
sdo_write times out |
It was called in Op, or before PreOp was reached. SDO requests are only serviced in PreOp, and requests sent during Init are thrown away. |
| A driver reads garbage or out-of-bounds errors | Offsets were taken from the PreOp subdevice list. Read the list again after check_all_op(). |
| Outputs never change |
send_outputs() was never called, or write_outputs() returned None and the code went ahead anyway. |
request_state_change returned Ok but nothing happened |
That is expected: it only queues the request. Poll get_state(), and read get_last_transition_failure() if the state doesn't move. |
| RT priority or IRQ pinning warnings | The binary is missing capabilities. Run it through the cargo runner (bin/run-linux), or apply setcap yourself. |
EtherCAT HAL
EtherCAT Devices
XTREM
Other crates