Skip to content

EL70x1 Steppers

Robin Krämer edited this page Sep 29, 2026 · 3 revisions

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

What is supported

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

Process data (velocity presets)

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 where txpdo_toggle is 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, plus set_counter / set_counter_value to preset it.

Configuration

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.

operation_mode: set it, even though it isn't written

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_output return an error,
  • set_output panics,
  • set_speed, set_position and get_position unwrap, 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 as 0 (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 with ConfigurableDevice::write_config in PreOp.

Speed scaling

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.

Position counter

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 next output_pre_process() sets set_counter = true with pos mod 65536, and the wrapper takes pos as its new value. In every other cycle, set_counter is sent as false.

The processing hooks are required

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 RxPDO

output_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.

io trait: StepperVelocityEL70x1Device

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

Example: run a motor at 500 full steps/s

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_0052 and EL7041_0052Configuration.
  • EL7031-0030: use EL7031_0030 and EL7031_0030Configuration, and set config.pdo_assignment = EL7031_0030PredefinedPdoAssignment::VelocityControlCompact, because its default preset is PositionControl.

Tests

Run these from ethercat_hal/:

  • cargo test el7031 checks the PDO preset sizes for the EL7031 and EL7031-0030 (el7031/pdo.rs, el7031_0030/pdo.rs).
  • cargo test el70xx_velocity_converter covers the speed scaling.
  • cargo test counter_wrapper covers the position wrap handling.

The shared PDO objects in pdo/el70x1.rs and the EL7041-0052 have no tests of their own.

Clone this wiki locally