Repository navigation
EL70x1 Steppers
The three Beckhoff stepper drivers are nearly identical. They share their PDO objects, CoE config blocks, velocity scaling and io trait, and differ only in a few extras:
EL7031 |
EL7031_0030 |
EL7041_0052 |
|
|---|---|---|---|
| Identity |
EL7031_IDENTITY_A/_B (product 0x1b773052, revision 0x1A0000/0x190000) |
EL7031_0030_IDENTITY_A (same product 0x1b773052, revision 0x10001E) |
EL7041_0052_IDENTITY_A (product 461451346, revision 1048628) |
| Stepper ports | 1 | 1 | 1 |
| Digital inputs | 2 (in StmStatus) |
2 | 2 |
| Analog inputs | – | 2 × 0–10 V (AiStandard/AiCompact, 0x1A0A–0x1A0D) |
– |
| Default PDO preset | VelocityControlCompact |
PositionControl |
VelocityControlCompact |
| Extra config | – |
analog_input_channel_{1,2} (0x8030 / 0x8040) and its own StmFeatures with digital-input emulation |
– |
The EL7031 and EL7031-0030 have the same product ID, so only the revision tells them apart. Match on
the full identity tuple, or use device_from_subdevice_identity, which already does.
Shared code:
| What | Where |
|---|---|
PDO objects (StmStatus, StmControl, StmVelocity, EncStatusCompact, …) |
pdo/el70x1.rs |
CoE config blocks (EncConfiguration, StmMotorConfiguration, StmControllerConfiguration, StmFeatures, PosConfiguration, PosFeatures) and enums |
shared_config/el70x1.rs |
io trait StepperVelocityEL70x1Device
|
io/stepper_velocity_el70x1.rs |
| steps/s ⇄ PDO velocity | helpers/el70xx_velocity_converter.rs |
16-bit encoder counter → i128 position |
helpers/counter_wrapper_u16_i128.rs |
Only direct velocity control is exposed through the io trait: you set a speed in full steps per
second and the terminal runs at it. The PDO structs and presets for the position controller and
positioning interface (PosControl, PosStatus, …) exist, but the drivers don't use them, and
get_input, get_output and set_output require DirectVelocity (see below).
| Preset | TxPDO (terminal → master) | RxPDO (master → terminal) |
|---|---|---|
VelocityControlCompact |
EncStatusCompact (0x1A00, 6 B), StmStatus (0x1A03, 2 B) |
EncControlCompact (0x1600, 4 B), StmControl (0x1602, 2 B), StmVelocity (0x1604, 2 B) |
VelocityControlCompactWithInfoData |
as above + StmSynchronInfoData (0x1A04) |
as above |
VelocityControl |
EncStatus (32-bit counter) instead of compact |
EncControl (32-bit) instead of compact |
On the EL7031-0030, every preset also maps its two analog inputs.
The drivers read the encoder position only from EncStatusCompact. input_post_process returns
an error when that PDO isn't mapped, so in practice use VelocityControlCompact or
VelocityControlCompactWithInfoData.
The fields you'll work with most:
-
StmStatus:ready_to_enable,ready,warning,error,moving_positive,moving_negative,torque_reduced,digital_input_1,digital_input_2,sync_error. Everything except the toggle bit is only updated in cycles wheretxpdo_toggleis set. In other cycles the previous values are kept. -
StmControl:enable,reset,reduce_torque. -
StmVelocity:velocity: i16. The sign gives the direction, and ±32767 means ±100 % of the configured speed range. -
EncStatusCompact/EncControlCompact: a 16-bit step counter with underflow and overflow flags, plusset_counter/set_counter_valueto preset it.
Each driver has its own config struct (EL7031Configuration, EL7031_0030Configuration,
EL7041_0052Configuration), built from the shared blocks. write_config writes them in this order:
| Block | CoE | What's written |
|---|---|---|
encoder: EncConfiguration |
0x8000:0E | reversion_of_rotation |
stm_motor: StmMotorConfiguration |
0x8010 |
max_current (mA, default 1500), reduced_current (mA, 750), nominal_voltage (mV, 50000), motor_coil_resistance (0.01 Ω, 100), motor_emf, motor_full_steps (200), start_velocity, drive_on_delay_time / drive_off_delay_time (ms, 100) |
stm_controller_1 / _2: StmControllerConfiguration
|
0x8011 / 0x8013 | only kp_factor, ki_factor and inner_window. outer_window, filter_cutoff_frequency, ka_factor and kd_factor are in the struct but not written. |
stm_features: StmFeatures |
0x8012 |
speed_range, invert_motor_polarity, select_info_data_1/2, invert_digital_input_1/2, function_for_input_1/2 (default PlcCam). The EL7031-0030 also writes digital_input_emulation_channel_1/2. |
pos_configuration: PosConfiguration |
0x8020 | positioning parameters (not used in velocity mode) |
pos_features: PosFeatures |
0x8021 | positioning features |
analog_input_channel_1/2 (EL7031-0030 only) |
0x8030 / 0x8040 | user scale, filter, limits |
pdo_assignment |
0x1C12 / 0x1C13 | the preset |
The motor defaults (1.5 A and so on) come from the terminal datasheet, not from your motor. Set
max_current, reduced_current, nominal_voltage and motor_full_steps to match your motor.
stm_features.operation_mode defaults to Automatic, but the driver methods get_input,
get_output and set_output check operation_mode == DirectVelocity against the config stored in
the driver:
-
get_input/get_outputreturn an error, -
set_outputpanics, -
set_speed,set_positionandget_positionunwrap, so they panic too.
At the same time, the value isn't sent to the terminal the way you'd expect:
-
EL7031 / EL7041-0052 (shared
StmFeatures): 0x8012:01 is not written at all. The terminal keeps whatever mode it was set to before. -
EL7031-0030 (its own
StmFeatures): 0x8012:01 is always written as0(Automatic), whatever you configured.
In Automatic mode, the terminal chooses its mode from the mapped PDOs, so a velocity preset gives you direct velocity. The practical rule:
Set
config.stm_features.operation_mode = EL70x1OperationMode::DirectVelocity, pick a velocity PDO preset (the EL7031-0030 needs this explicitly), and apply the config withConfigurableDevice::write_configin PreOp.
speed_range (0x8012:05) sets what 100 % velocity means. The unit is full steps per second:
EL70x1SpeedRange |
100 % = | Resolution of one velocity unit |
|---|---|---|
Steps1000 |
1000 steps/s | ≈ 0.03 steps/s |
Steps2000 (default) |
2000 steps/s | ≈ 0.06 steps/s |
Steps4000 |
4000 steps/s | ≈ 0.12 steps/s |
Steps8000 |
8000 steps/s | ≈ 0.24 steps/s |
Steps16000 |
16000 steps/s | ≈ 0.49 steps/s |
Steps32000 |
32000 steps/s | ≈ 0.98 steps/s |
velocity = steps_per_second / range × 32767. Choose the smallest range that covers your top speed.
Requests above the range don't clamp safely: the f64 → i16 conversion saturates.
set_speed(port, steps_per_second) uses EL70x1VelocityConverter with probabilistic rounding: it
randomly rounds up or down, weighted by the fractional part. Averaged over many cycles, the commanded
speed is then more precise than one velocity unit. The side effect is that the PDO value can move by ±1
from one cycle to the next even when you don't change the speed. get_speed() converts back and
rounds the same way.
The terminal reports a 16-bit counter. CounterWrapperU16U128 extends it to an i128 by watching the
rising edges of the underflow and overflow flags. It is updated in input_post_process().
-
get_position(0)returns the extended counter, in the terminal's raw counter units. -
set_position(0, pos)schedules a preset. The nextoutput_pre_process()setsset_counter = truewithpos mod 65536, and the wrapper takesposas its new value. In every other cycle,set_counteris sent asfalse.
These drivers do real work in EthercatDeviceProcessing. The derived input() and output() don't
call these hooks, so the application has to call them itself:
stepper.input(bits)?; // decode TxPDO
stepper.input_post_process()?; // update the i128 position
// … application logic …
stepper.output_pre_process()?; // auto-reset on error, apply pending set_position
stepper.output(bits)?; // encode RxPDOoutput_pre_process() also sets StmControl::reset = true whenever StmStatus::error is set, to
clear errors automatically. Nothing sets reset back to false, and get_output reports the current
value, so set_enabled and set_speed keep sending reset = true. If the terminal only resets on a
rising edge, later errors won't clear until you send an output with reset: false yourself.
There is also a block in output_pre_process() that tries to clear the counter overflow and underflow
flags by setting the counter to its current value. It has no effect, because the pop_override()
match straight after it always overwrites set_counter.
| Method | What it does |
|---|---|
set_enabled(0, bool) / is_enabled(0)
|
StmControl::enable. set_enabled quietly does nothing if get_output fails. |
set_speed(0, steps_per_s) / get_speed(0)
|
velocity in full steps per second (see scaling) |
get_position(0) / set_position(0, i128)
|
extended encoder counter |
get_input(0) -> StepperVelocityEL70x1Input |
counter_value plus the StmStatus flags |
get_output(0) / set_output(0, StepperVelocityEL70x1Output)
|
raw access to velocity, enable, reduce_torque, reset, set_counter
|
get_speed_range(0) |
the stored speed_range
|
get_digital_input(0 or 1) |
digital_input_1/2 from StmStatus (with function_for_input_x = NormalInput if you want plain inputs) |
get_analog_input(0 or 1) |
EL7031-0030 only: normalized value (raw / 32767) and a wiring-error flag, range 0–10 V |
use bitvec::{order::Lsb0, slice::BitSlice};
use ethercat_hal::coe::ConfigurableDevice;
use ethercat_hal::devices::{EthercatDevice, EthercatDeviceProcessing, NewEthercatDevice};
use ethercat_hal::devices::beckhoff_modules::el7031::{EL7031, EL7031_IDENTITY_A, EL7031_IDENTITY_B};
use ethercat_hal::devices::beckhoff_modules::el7031::coe::EL7031Configuration;
use ethercat_hal::devices::beckhoff_modules::el7031::pdo::EL7031PredefinedPdoAssignment;
use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1Device;
use ethercat_hal::shared_config::el70x1::{EL70x1OperationMode, EL70x1SpeedRange};
// PreOp: configure
let sd = subdevices.iter()
.find(|sd| matches!((sd.vendor, sd.product_id, sd.revision), EL7031_IDENTITY_A | EL7031_IDENTITY_B))
.unwrap();
let mut config = EL7031Configuration::default();
config.stm_features.operation_mode = EL70x1OperationMode::DirectVelocity; // required, see above
config.stm_features.speed_range = EL70x1SpeedRange::Steps1000;
config.stm_motor.max_current = 1000; // mA — match your motor
config.stm_motor.nominal_voltage = 24000; // mV
config.stm_motor.motor_full_steps = 200;
config.pdo_assignment = EL7031PredefinedPdoAssignment::VelocityControlCompact;
let mut stepper = EL7031::new();
stepper.write_config(channel.clone(), sd.device_address, &config)?;
// ... request Op, wait for check_all_op(), re-read the subdevice list (offsets) ...
loop {
if let Some(inputs) = handle.get_inputs() {
stepper.input(BitSlice::<u8, Lsb0>::from_slice(&inputs[sd.start_tx..sd.end_tx]))?;
stepper.input_post_process()?;
}
let status = stepper.get_input(0)?;
if status.ready_to_enable || status.ready {
stepper.set_enabled(0, true);
stepper.set_speed(0, 500.0)?;
}
println!("pos={} err={} warn={}", stepper.get_position(0), status.error, status.warning);
if let Some(outputs) = handle.write_outputs() {
stepper.output_pre_process()?;
stepper.output(BitSlice::<u8, Lsb0>::from_slice_mut(&mut outputs[sd.start_rx..sd.end_rx]))?;
handle.send_outputs();
}
}The same code works for the other two terminals if you change the types:
-
EL7041-0052: use
EL7041_0052andEL7041_0052Configuration. -
EL7031-0030: use
EL7031_0030andEL7031_0030Configuration, and setconfig.pdo_assignment = EL7031_0030PredefinedPdoAssignment::VelocityControlCompact, because its default preset isPositionControl.
Run these from ethercat_hal/:
-
cargo test el7031checks the PDO preset sizes for the EL7031 and EL7031-0030 (el7031/pdo.rs,el7031_0030/pdo.rs). -
cargo test el70xx_velocity_convertercovers the speed scaling. -
cargo test counter_wrappercovers the position wrap handling.
The shared PDO objects in pdo/el70x1.rs and the EL7041-0052 have no tests of their own.
EtherCAT HAL
EtherCAT Devices
XTREM
Other crates