ethercrab

repository·main·Indexed 19 days ago

https://github.com/ethercrab-rs/ethercrab

A high-performance, async-first EtherCAT MainDevice implementation written in pure Rust. It supports both desktop (std) and embedded (no_std) environments. The project includes ethercrab-wire for packet serialization and ethercrab-wire-derive for procedural macros to automate wire-format reading and writing for structs and enums.

Tokens
21.5K
Snippets
50
Records
93
Agent score
65%

What's inside ethercrab

  1. Use ethercrab-wire for EtherCAT packet serialization

    main

    ethercrab-wire provides traits used to pack and unpack Rust structs and enums from EtherCAT packets on the wire. While it is designed to be used alongside the ethercrab crate, it can also be used as a standalone library for EtherCAT wire protocol serialization.

    To implement these traits for your own data types, you can either implement them manually or, for convenience, use the ethercrab-wire-derive crate to automatically derive the necessary logic.

  2. Use `ethercrab-wire-derive` for wire-format serialization

    main

    ethercrab-wire-derive provides procedural macros to automatically implement wire-format reading and writing for structs and enums. This is useful for defining data structures that match specific bit- or byte-aligned layouts used in EtherCAT communication.

    Warning: This crate is experimental and may contain bugs or breaking changes. Use with caution.

    To use these derives, you typically implement EtherCrabWireReadWrite, EtherCrabWireRead, or EtherCrabWireWrite on your types using the #[derive(...)] attribute combined with #[wire(...)] metadata.

    #[derive(ethercrab_wire::EtherCrabWireReadWrite)]
    #[wire(bytes = 4)]
    struct MyStruct { ... }
  3. Conceptual PDI (Process Data Image) Grouping

    main

    The PDI design focuses on thread safety by grouping slaves into non-overlapping segments.

    Key Concepts:

    • Grouping: Slaves are grouped into SlaveGroups. Each group has its own PDI with a specific start address and length.
    • Thread Safety: Groups are Send but not Sync. This means a group can be moved to a different thread but cannot be shared between threads. This prevents data races by design.
    • Execution Loop: PDI updates are typically performed in a loop using an interval.

    Usage Pattern:

    let interval = Interval::new(Duration::from_millis(2));
    
    while let Some(_) = interval.next().await {
        // Update all groups
        group.tx_rx(&client).await;
    
        // Access specific slave data
        group.slave_by_index(0).outputs()[0] = 0xff;
    
        for slave in group.slaves_mut() {
            // ...
        }
    }
    let interval = Interval::new(Duration::from_millis(2));
    
    while let Some(_) = interval.next().await {
        group.tx_rx(&client).await;
    
        group.slave_by_index(0).outputs()[0] = 0xff;
    
        for slave in group.slaves_mut() {
            // ...
        }
    }
  4. Basic EtherCrab usage pattern

    main

    A typical EtherCrab application follows these steps:

    1. Initialize PDU Storage: Create a PduStorage instance to manage EtherCAT frames.
    2. Create MainDevice: Initialize the MainDevice with a pdu_loop and Timeouts.
    3. Spawn TX/RX Task: On desktop systems, use tx_rx_task to handle network I/O.
    4. Initialize SubDevices: Use init_single_group to discover and initialize the EtherCAT topology.
    5. Transition to OP Mode: Use into_op to move the network from PRE-OP to OPERATIONAL mode.
    6. Cyclic Loop: Run a loop that calls group.tx_rx(&maindevice) and performs I/O operations using io_raw_mut().
    // Simplified workflow sketch
    let (tx, rx, pdu_loop) = PDU_STORAGE.try_split().expect("can only split once");
    let maindevice = Arc::new(MainDevice::new(pdu_loop, timeouts, config));
    
    tokio::spawn(tx_rx_task(&interface, tx, rx).expect("spawn TX/RX task"));
    
    let mut group = maindevice.init_single_group::<MAX_SUBDEVICES, PDI_LEN>(ethercat_now).await.expect("Init");
    
    // Transition to OP
    let mut group = group.into_op(&maindevice).await.expect("PRE-OP -> OP");
    
    loop {
        group.tx_rx(&maindevice).await.expect("TX/RX");
        for mut subdevice in group.iter(&maindevice) {
            let mut io = subdevice.io_raw_mut();
            // Perform cyclic I/O here
        }
        tick_interval.tick().await;
    }
  5. Understand CANopen over EtherCAT (CoE)

    main

    CoE uses mailbox PDUs with an Address Offset (ADO) in the range 0x1000-0x1FFF. The frame structure follows:

    1. Ethernet II
    2. EtherCAT frame
    3. Array of ethercat datagrams (containing common headers and datagram data like register reads, mailbox/CoE, or PDU).
  6. Reset Distributed Clock (DC) filters

    main

    Before starting drift compensation in an EtherCAT network, you should reset the internal filters of the Time Control Loop to ensure a proper settling time.

    According to the ESC datasheet, this is achieved by writing the current value of the Speed Counter Start register (0x0930:0x0931) back to itself. Writing the current value again is sufficient to trigger the reset.

  7. Sharing data in Embassy tasks

    main

    When working with Embassy, you may need to share resources (like SPI/I2C buses or state) across tasks. Common patterns include:

    • Forever<Cell<Thing>>: Put in main, pass &'static Cell<Thing> to tasks.
    • Forever<RefCell<Thing>>: Put in main, pass &'static RefCell<Thing> to tasks. Warning: Do not hold a Ref across an .await point.
    • Global ThreadModeMutex: Use if all tasks run in thread mode (default).
    • Global CriticalSectionMutex: Use if using InterruptExecutor or raw IRQ handlers.
    • Channels: Use for passing data between tasks.
    • Async Mutex: Best for resources requiring async method calls (e.g., shared buses).
  8. Understand AssignActivate in EtherCAT ESI

    main

    In EtherCAT ESI (EtherCAT Slave Information) files, AssignActivate refers to the bitmask used to configure Distributed Clock (DC) synchronization.

    Specifically, it corresponds to the RegisterAddress::DcSyncActive bitmask for register 0x0981.

    Common configurations:

    • 0x0700: Activates both SYNC1 and SYNC0.
    • 0x0300: Activates SYNC0 only.

    Note: Some drivers may require writing to specific ESC registers (like 0x0980 or 0x0981) to activate these sync signals, regardless of standard instructions.

  9. Identify EtherCAT Topology types

    main

    The topology is determined by the number of open ports on a node. Understanding the port count is essential for Distributed Clock (DC) calculations.

    Port Count Interpretations:

    1. End of line: Single port active.
    2. Normal chain link: 2 ports open.
    3. Node with children: 3 ports open (e.g., a branch/fork).
    4. Cross structure: 4 ports open (data enters port 0, loops through 3, 2, 1, and exits port 0).

    In a tree topology, if a node has more than one port, it can form branches. If a port is in loopback, data is passed straight through it.

  10. Discover EtherCAT slaves

    main
    Devices are discovered using an Ethernet broadcast (destination MAC allballs). To detect the number of slaves, a Broadcast (BRD) frame is sent and the Working Counter (WKC) is read. Once counted by index, slaves can be assigned a 'configured station address' by the master using an APWR service, iterating through the slaves by index address (typically 1 - slave).
  11. Use Generic types in wire-compatible structs

    main

    Structs can contain generic fields as long as the generic parameters implement the appropriate EtherCrabWire* traits (e.g., EtherCrabWireRead).

    Responsibility: The developer must ensure that the size of the generic type T matches the size specified by the #[wire(bits|bytes = N)] attribute on the containing struct.

    Example

    #[derive(ethercrab_wire::EtherCrabWireRead)]
    #[wire(bytes = 2)]
    struct Value {
        #[wire(bytes = 2)]
        raw: u16
    };
    
    #[derive(ethercrab_wire::EtherCrabWireRead)]
    #[wire(bytes = 4)]
    pub struct AnalogInput<T: ethercrab_wire::EtherCrabWireRead> {
        // ... bit fields ...
        #[wire(bits = 16)]
        value: T, // T must be 16 bits to match the attribute
    }
    
    type IntegerAnalog = AnalogInput<Value>;
    #[derive(ethercrab_wire::EtherCrabWireRead)]
    #[wire(bytes = 2)]
    struct Value {
        #[wire(bytes = 2)]
        raw: u16
    };
    
    #[derive(ethercrab_wire::EtherCrabWireRead)]
    #[wire(bytes = 4)]
    pub struct AnalogInput<T: ethercrab_wire::EtherCrabWireRead> {
        #[wire(bits = 1)]
        underrange: bool,
        #[wire(bits = 1)]
        overrange: bool,
        #[wire(bits = 2)]
        limit1: u8,
        #[wire(bits = 2)]
        limit2: u8,
        #[wire(bits = 1)]
        error: bool,
        #[wire(pre_skip = 6, bits = 1)]
        sync_error: bool,
        #[wire(bits = 1)]
        tx_pdo_bad: bool,
        #[wire(bits = 1)]
        tx_pdo_toggle: bool,
        #[wire(bits = 16)]
        value: T,
    }
    
    type IntegerAnalog = AnalogInput<Value>;
  12. Perform remote packet capture on Linux

    main

    To capture EtherCAT traffic on a remote Linux machine and view it locally in Wireshark, you can pipe tcpdump over SSH.

    1. Ensure root login is allowed in /etc/ssh/sshd_config by setting PermitRootLogin yes.
    2. Restart the sshd service.
    3. Run the following command from your local machine to stream the capture to your local Wireshark instance:
    ssh root@ethercrab tcpdump -U -s0 -i enp2s0 -w - | sudo wireshark -k -i -