-
Notifications
You must be signed in to change notification settings - Fork 2
EL4732
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 |
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.
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.
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.
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.
Oversampling needs three things to agree: the driver's N, the PDO lengths the master maps, and the 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.
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_oversamplingtells the master to multiply the bit length of PDOs0x1600and0x1700by N when it sizes the sync managers.oversampling_configis[(0x1600, N), (0x1700, N)]. It is only needed for N > 1. -
enable_dc_sync01switches 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.
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.5use 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();
}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.
| 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. |
There are no unit tests for the EL4732 driver or the oversampling PDO objects yet.
EtherCAT HAL
EtherCAT Devices
XTREM
Other crates