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

Driver: devices/beckhoff_modules/el6021.rs io trait: io/serial_interface.rs (SerialInterfaceDevice)

The EL6021 is a one-channel serial interface terminal. Bytes you want to transmit go out in the RxPDO, and received bytes come back in the TxPDO. Each handshake moves at most 22 bytes, and the terminal and the master acknowledge each other with toggle bits. Because each step of the handshake needs a cycle to reach the terminal and another for the answer to come back, the driver's API is a set of non-blocking calls that you make once per cycle.

Struct EL6021
Identities EL6021_IDENTITY_A…_D (product 0x17853052; revisions 0x150000, 0x140000, 0x160000, 0x100000)
Config EL6021Configuration, applied with ConfigurableDevice::write_config
Ports one (EL6021Port::SI1). The port argument of every trait method is ignored.
Needs input_post_process/output_pre_process No, both are no-ops

Process data

Only the Standard 22 Byte MDP 600 layout is implemented. It is a 24-byte PDO in each direction:

Byte TxPDO 0x1A02 (terminal → master): Standard22ByteMdp600Input RxPDO 0x1602 (master → terminal): Standard22ByteMdp600Output
0 status: bit 0 transmit_accepted, 1 receive_request, 2 init_accepted, 3 buffer_full, 4 parity_error, 5 framing_error, 6 overrun_error control: bit 0 transmit_request, 1 received_acepted (spelled this way in the code), 2 init_request
1 length: number of valid bytes in data length: number of bytes to send
2–23 data: [u8; 22] data: [u8; 22]

EL6021PdoPreset also lists the Legacy and other Standard layouts, but their txpdo_assignment() and rxpdo_assignment() hit todo!(). Choosing any preset other than Standard22ByteMdp600 panics, both in write_config and when you construct the driver from that config.

Configuration

EL6021Configuration::default() returns:

Field CoE Default Written by write_config?
rts_enabled 0x8000:01 true no, the field is never written
xon_off_supported_rx 0x8000:03 false yes
xon_on_supported_tx 0x8000:02 false yes
fifo_continuous_send_enabled 0x8000:04 false yes
enable_transfer_rate_optimization 0x8000:05 true yes
half_duplex_enabled 0x8000:06 true yes
point_to_point_connection_enabled 0x8000:07 false yes
baud_rate 0x8000:11 EL6021Baudrate::B19200 yes
data_frame 0x8000:15 SerialEncoding::Coding8E1 yes
rx_buffer_full_notification 0x8000:1A 0x0360 yes
pdo_assignment 0x1C12 / 0x1C13 Standard22ByteMdp600 yes

Some field doc comments in the source give different defaults (9600 baud, 8N1, half duplex off). The table above is what Default actually returns.

write_config stores the config in the driver and rebuilds txpdo and rxpdo from pdo_assignment. get_baudrate() and get_serial_encoding() return the stored values. You can use them to work out timing, for example SerialEncoding::total_bits() gives the bits on the wire per byte.

Like every CoE write, this has to happen in PreOp.

Protocol

All three operations are handshakes between the control bits you set and the status bits the terminal answers with. Each call to the driver moves the handshake at most one step, and nothing reaches the terminal until you write the outputs. So call the driver once per cycle, between input() and output().

1. Initialization: serial_interface_initialize(0) -> bool

Call it every cycle until it returns true. Don't send or receive before then.

cycle  init_request (ours)  init_accepted (terminal)  driver action                       returns
  1          0 → 1                  0                 request init                         false
  …          1                      0                 wait for terminal                    false
  n          1 → 0                  1                 terminal accepted, drop request      false
  …          0                      1                 wait for terminal to drop accepted   false
  m          0                      0                 remember receive toggle state        true

The driver sets an internal initialized flag at the first step. That flag is never cleared, so to run the initialization again you need a new EL6021 instance.

2. Sending: serial_interface_write_message(0, bytes) -> Result<bool>

  • With a non-empty bytes (at most 22): the driver copies the bytes and length into the RxPDO and toggles transmit_request. It returns Ok(true), which means the bytes were queued, not that they were sent. If bytes is longer than 22, it returns an error.
  • With an empty bytes: this polls for completion. It returns Ok(true) once the terminal's transmit_accepted equals our transmit_request, which means the last frame was taken.

The driver does not stop you from queuing a new frame before the previous one has been accepted. If you do, the pending data is overwritten and the toggle flips back, so the terminal may never see the first frame. Always wait for the empty-message poll to return true before sending again.

Messages longer than 22 bytes have to be split up by the caller.

3. Receiving: serial_interface_has_messages(0) / serial_interface_read_message(0)

The terminal toggles receive_request whenever it puts new bytes in the TxPDO. has_messages() returns true while that toggle is different from the last value the driver acknowledged.

read_message() does three things:

  1. It returns the first length bytes.
  2. It records the new toggle state.
  3. It toggles received_acepted, which tells the terminal it can deliver the next chunk.

A message longer than 22 bytes arrives as several chunks over several cycles, so it's up to you to reassemble it (for example until a terminator byte or an expected length). With transfer-rate optimisation on (the default), the terminal sends a chunk after a pause of about 2 byte times, or when the PDO is full.

If the terminal ever toggles receive_request with length == 0, read_message() returns None without acknowledging. has_messages() then stays true from that point on.

Example

use bitvec::{order::Lsb0, slice::BitSlice};
use ethercat_hal::coe::ConfigurableDevice;
use ethercat_hal::devices::{EthercatDevice, NewEthercatDevice};
use ethercat_hal::devices::beckhoff_modules::el6021::{
    EL6021, EL6021Baudrate, EL6021Configuration, EL6021_PRODUCT_ID,
};
use ethercat_hal::io::serial_interface::{SerialEncoding, SerialInterfaceDevice};

// PreOp: configure
let mut el6021 = EL6021::new();
let sd = subdevices.iter().find(|sd| sd.product_id == EL6021_PRODUCT_ID).unwrap();
let config = EL6021Configuration {
    baud_rate: EL6021Baudrate::B9600,
    data_frame: SerialEncoding::Coding8N1,     // one of the accepted pairs, see above
    ..EL6021Configuration::default()
};
el6021.write_config(channel.clone(), sd.device_address, &config)?;

// ... request Op, wait for check_all_op(), re-read the subdevice list (offsets) ...

let mut ready = false;
let mut send_pending = false;
let mut rx_buffer: Vec<u8> = Vec::new();

loop {
    if let Some(inputs) = handle.get_inputs() {
        el6021.input(BitSlice::<u8, Lsb0>::from_slice(&inputs[sd.start_tx..sd.end_tx]))?;
    }

    if !ready {
        ready = el6021.serial_interface_initialize(0);
    } else {
        // receive
        if let Some(chunk) = el6021.serial_interface_read_message(0) {
            rx_buffer.extend_from_slice(&chunk);
        }
        // send one frame at a time
        if send_pending {
            if el6021.serial_interface_write_message(0, vec![])? {
                send_pending = false;           // previous frame accepted
            }
        } else {
            el6021.serial_interface_write_message(0, b"PING\r\n".to_vec())?;
            send_pending = true;
        }
    }

    if let Some(outputs) = handle.write_outputs() {
        el6021.output(BitSlice::<u8, Lsb0>::from_slice_mut(&mut outputs[sd.start_rx..sd.end_rx]))?;
        handle.send_outputs();
    }
}

Status bits the driver does not expose

buffer_full, parity_error, framing_error and overrun_error are decoded, but no trait method returns them. Read them directly:

if let Some(pdo) = &el6021.txpdo.com_tx_pdo_map_22_byte {
    if pdo.status.parity_error || pdo.status.framing_error { /* line settings wrong? */ }
}

Tests

cd ethercat_hal && cargo test el6021 runs the encoding and decoding tests for the 22-byte input and output PDO objects.

Clone this wiki locally