Skip to content
Robin Krämer edited this page Sep 29, 2026 · 2 revisions

Driver: devices/beckhoff_modules/el4732.rs PDO objects: pdo/oversampling.rs (AnalogOutputOversample, CycleCount) io trait: io/analog_output.rs (AnalogOutputDevice) Example: examples/el4732_minimal.rs

The EL4732 is a two-channel analog output terminal (16-bit, −10 V to +10 V) that supports oversampling. With an oversampling factor N, the master sends N samples per channel in every EtherCAT cycle, and the terminal outputs them one after another, one per SYNC0 tick. So a 1 ms cycle with N = 25 gives an output rate of 25 kHz. That makes the terminal suitable for waveform generation, not just for setpoints.

The terminal has no inputs.

Struct EL4732
Identities EL4732_IDENTITY_A…_C (product 0x127C3052; revisions 0x20000, 0x30000, 0x40000). Revisions 0 and 1 are hidden as legacy in the ESI and aren't supported.
Constructors EL4732::new() (N = 1), EL4732::new_with_oversample(n)
Config EL4732Configuration, stored in el4732.configuration. It isn't a ConfigurableDevice; you apply it yourself (see Setup).
Ports two: EL4732Port::AO1 (0) and EL4732Port::AO2 (1)
Needs input_post_process/output_pre_process No, both are no-ops
Needs DC Yes, SYNC0 and SYNC1

Oversampling factor

N must be one of the DC OpModes in the EL4732 ESI:

1, 2, 3, 4, 5, 8, 10, 16, 20, 25, 32, 40, 50, 100

The driver doesn't check this. Any other value builds a driver with a process-image layout the terminal won't accept.

N is fixed when you construct the driver. To change it, create a new EL4732 and go through setup again.

Process data

Each channel has its own sync manager with two PDOs: a 16-bit cycle counter followed by N 16-bit samples.

Sync manager PDO Contents Field in EL4732RxPdo
SM0 0x1680 cycle counter, u16 ch1_cycle_count: Option<CycleCount>
SM0 0x1600 N × i16 samples, channel 1 ch1_samples: Option<AnalogOutputOversample>
SM1 0x1780 cycle counter, u16 ch2_cycle_count: Option<CycleCount>
SM1 0x1700 N × i16 samples, channel 2 ch2_samples: Option<AnalogOutputOversample>

Each channel takes 2 + 2·N bytes, so the terminal takes 4 + 4·N bytes of the output image. That is 104 bytes for N = 25, and 404 bytes for N = 100.

The cycle counter

CycleCount has to change in every cycle, or the terminal faults. The driver increments a channel's counter each time you call set_output or set_output_samples for that channel. So:

  • Set both channels every cycle, even one you don't use. The example writes zeros to AO2 for this reason.
  • Set each channel once per frame you send. Every call increments the counter, and nothing resets it between frames.

Values and scaling

Both setters take values in −1.0..=1.0, which map linearly to −10 V..=+10 V:

raw = round(clamp(value, -1.0, 1.0) * 32767)

So 0.5 is +5 V, and -1.0 gives -32767, not -32768. Values outside the range are clamped silently.

Method What it writes
set_output(port, AnalogOutputOutput(v)) (from AnalogOutputDevice) the same value into all N slots
set_output_samples(port, &[f32]) one value per slot, in output order. Panics if the slice length isn't N.

A port other than 0 or 1 is ignored by both methods.

Setup (in PreOp)

Oversampling needs three things to agree: the driver's N, the PDO lengths the master maps, and the DC timing.

1. Master and DC timing

The master's cycle stays at the full cycle time. SYNC0 fires once per sample, and SYNC1 is set to the rest of the cycle:

Setting Value For 1000 µs, N = 25
MasterConfiguration::target_cycle_time_us cycle time 1000 µs
DcConfiguration::sync0_period cycle time / N 40 µs
DcConfiguration::sync0_shift sync0_period / 2 20 µs
SYNC1 period (passed to enable_dc_sync01) sync0_period · (N − 1) 960 µs

Choose a cycle time that N divides exactly in whole microseconds. Otherwise N × SYNC0 no longer adds up to one cycle.

This is the exception mentioned in the Application guide: normally sync0_period equals the cycle time.

2. PDO lengths and SYNC1

let mut el4732 = EL4732::new_with_oversample(n);

// after reaching PreOp:
if n > 1 {
    channel.configure_oversampling(sd.device_address, el4732.configuration.oversampling_config.clone())?;
}
channel.enable_dc_sync01(sd.device_address, Duration::from_micros(sync1_period_us))?;
  • configure_oversampling tells the master to multiply the bit length of PDOs 0x1600 and 0x1700 by N when it sizes the sync managers. oversampling_config is [(0x1600, N), (0x1700, N)]. It is only needed for N > 1.
  • enable_dc_sync01 switches the terminal to SYNC0 + SYNC1.

Both go through the ChannelRequest queue, so they have to be sent in PreOp. The master applies them when it maps the process image on the way to Op.

Don't call the derived el4732.rxpdo.write_config(...). It writes all four PDOs into 0x1C12, which doesn't match the terminal's SM0/SM1 layout. The terminal's default assignment is already the one the driver expects.

Example: a sine wave on AO1

This is a shortened version of examples/el4732_minimal.rs. Run the full version with:

# network-interface cycle_us N Hz amplitude
cargo run --release --example el4732_minimal -- <iface> 1000 25 50.0 0.5
use bitvec::slice::BitSlice;
use std::{f64::consts::PI, time::Duration};
use ethercat_hal::{
    DcConfiguration, EtherCATState, MasterConfiguration, init_ethercat,
    devices::{EthercatDevice, beckhoff_modules::el4732::{EL4732, EL4732Port, EL4732_PRODUCT_ID}},
};

let (cycle_us, n, freq, amplitude) = (1000u64, 25usize, 50.0f64, 0.5f64);
let sync0_us = cycle_us / n as u64;
let sync1_us = sync0_us * (n as u64 - 1);

let mut dc_config = DcConfiguration::default();
dc_config.sync0_period = Duration::from_micros(sync0_us);
dc_config.sync0_shift = Duration::from_micros(sync0_us / 2);

let control = init_ethercat(&interface, Some(MasterConfiguration {
    target_cycle_time_us: cycle_us as usize,
    dc_config,
    ..MasterConfiguration::default()
}));
let mut handle = control.app_handle;
let mut el4732 = EL4732::new_with_oversample(n);

// PreOp: oversampling + SYNC01
control.channel.request_state_change(EtherCATState::PreOp)?;
while handle.get_state() != EtherCATState::PreOp { std::thread::sleep(Duration::from_millis(10)); }
for sd in handle.try_get_subdevices_vec_sync()? {
    if sd.product_id == EL4732_PRODUCT_ID {
        if n > 1 {
            control.channel.configure_oversampling(sd.device_address, el4732.configuration.oversampling_config.clone())?;
        }
        control.channel.enable_dc_sync01(sd.device_address, Duration::from_micros(sync1_us))?;
    }
}

// Op, then re-read the subdevice list for the offsets
control.channel.request_state_change(EtherCATState::Op)?;
while !handle.check_all_op() { std::thread::sleep(Duration::from_millis(10)); }
let sd = handle.try_get_subdevices_vec_sync()?
    .into_iter().find(|sd| sd.product_id == EL4732_PRODUCT_ID).unwrap();

let step_per_slot = 2.0 * PI * freq * (cycle_us as f64 * 1e-6) / n as f64;
let mut phase = 0.0f64;
let mut ch1 = vec![0.0f32; n];
let ch2 = vec![0.0f32; n];                          // AO2 unused, but must still be set every cycle
let mut last_cycle = handle.get_current_cycle();

loop {
    for (i, slot) in ch1.iter_mut().enumerate() {
        *slot = ((phase + step_per_slot * i as f64).sin() * amplitude) as f32;
    }

    // Wait for the mailbox before touching the driver, so each counter increment
    // lands in exactly one frame.
    let outputs = loop { if let Some(o) = handle.write_outputs() { break o; } };
    el4732.set_output_samples(EL4732Port::AO1 as usize, &ch1);
    el4732.set_output_samples(EL4732Port::AO2 as usize, &ch2);
    el4732.output(BitSlice::from_slice_mut(&mut outputs[sd.start_rx..sd.end_rx]))?;

    // Advance the phase by the cycles that actually passed, so the waveform stays continuous
    // even if the loop misses a cycle.
    let now = handle.get_current_cycle();
    phase = (phase + step_per_slot * n as f64 * now.wrapping_sub(last_cycle) as f64) % (2.0 * PI);
    last_cycle = now;

    handle.send_outputs();
}

Real-time settings

At high N the terminal is sensitive to jitter. The example runs with RtOptimizationConfig and puts both master threads on one isolated core: the tx/rx thread at SCHED_FIFO priority 99, the state-machine thread at 50, and the NIC IRQ pinned to the same core. Its comment says this works well with io_uring. The application loop runs on a separate core. See Architecture.

Troubleshooting

Symptom Likely cause
Terminal doesn't reach Op, or drops out shortly after N isn't an ESI OpMode, SYNC0 × N doesn't equal the cycle time, or enable_dc_sync01 wasn't called. Check get_last_transition_failure().
Terminal faults while running A cycle counter stopped changing because a channel wasn't set in some cycle (see The cycle counter).
set_output_samples panics The slice length doesn't match the N the driver was built with.
output() returns "Range … is out of bounds for buffer with length …" configure_oversampling wasn't sent (N > 1), so the master mapped only one sample per channel and the terminal's slice is smaller than the driver's PDO.

Tests

There are no unit tests for the EL4732 driver or the oversampling PDO objects yet.

Clone this wiki locally