Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Hardware Integration: EtherCAT

EtherCAT Overview

EtherCAT is a high-performance fieldbus commonly used for servo drives, digital I/O modules, and analog I/O. If you have used EtherCAT with TwinCAT or acontis, the concepts are the same — slaves are scanned on an Ethernet interface, PDOs (Process Data Objects) are exchanged cyclically, and SDOs (Service Data Objects) are used for acyclic configuration.

In AutoCore, the autocore-ethercat module handles the EtherCAT master. It:

  1. Scans and configures slaves on startup
  2. Exchanges PDO data cyclically (synchronized with the server tick)
  3. Maps all PDO entries into shared memory variables

Configuring EtherCAT Slaves

EtherCAT slaves are configured in the modules.ethercat.config.slaves array in project.json. Each slave needs:

  • A name (used to build variable FQDNs)
  • A position on the bus (0 = first slave)
  • A device_id (vendor ID, product code, revision)
  • Sync managers with PDO mappings

Here is a simple example with a Beckhoff EK1100 coupler and an EL1008 8-channel digital input module:

{
  "modules": {
    "ethercat": {
      "enabled": true,
      "args": ["service"],
      "config": {
        "interface_name": "eth0",
        "auto_activate": true,
        "runtime_settings": {
          "cycle_time_us": 5000,
          "priority": 99
        },
        "slaves": [
          {
            "name": "EK1100",
            "position": 0,
            "device_id": {
              "vendor_id": 2,
              "product_code": 72100946,
              "revision_number": 1114112
            }
          },
          {
            "name": "DI_8CH",
            "position": 1,
            "device_id": {
              "vendor_id": 2,
              "product_code": 66084946,
              "revision_number": 1048576
            },
            "sync_managers": [
              {
                "direction": "Inputs",
                "index": 0,
                "pdos": [
                  {
                    "name": "Channel 1-8",
                    "entries": [
                      { "index": "0x6000", "sub": 1, "name": "Input 1", "type": "BIT", "bits": 1 },
                      { "index": "0x6010", "sub": 1, "name": "Input 2", "type": "BIT", "bits": 1 },
                      { "index": "0x6020", "sub": 1, "name": "Input 3", "type": "BIT", "bits": 1 },
                      { "index": "0x6030", "sub": 1, "name": "Input 4", "type": "BIT", "bits": 1 },
                      { "index": "0x6040", "sub": 1, "name": "Input 5", "type": "BIT", "bits": 1 },
                      { "index": "0x6050", "sub": 1, "name": "Input 6", "type": "BIT", "bits": 1 },
                      { "index": "0x6060", "sub": 1, "name": "Input 7", "type": "BIT", "bits": 1 },
                      { "index": "0x6070", "sub": 1, "name": "Input 8", "type": "BIT", "bits": 1 }
                    ]
                  }
                ]
              }
            ]
          }
        ]
      }
    }
  },
  "variables": {
    "di_input_1": { "type": "bool", "direction": "input", "link": "ethercat.di_8ch.channel_1_8.input_1" },
    "di_input_2": { "type": "bool", "direction": "input", "link": "ethercat.di_8ch.channel_1_8.input_2" },
    "di_input_3": { "type": "bool", "direction": "input", "link": "ethercat.di_8ch.channel_1_8.input_3" },
    "di_input_4": { "type": "bool", "direction": "input", "link": "ethercat.di_8ch.channel_1_8.input_4" },
    "di_input_5": { "type": "bool", "direction": "input", "link": "ethercat.di_8ch.channel_1_8.input_5" },
    "di_input_6": { "type": "bool", "direction": "input", "link": "ethercat.di_8ch.channel_1_8.input_6" },
    "di_input_7": { "type": "bool", "direction": "input", "link": "ethercat.di_8ch.channel_1_8.input_7" },
    "di_input_8": { "type": "bool", "direction": "input", "link": "ethercat.di_8ch.channel_1_8.input_8" }
  }
}

Finding device IDs: The vendor ID, product code, and revision number come from the EtherCAT slave’s ESI (EtherCAT Slave Information) file. You can find these in the device manufacturer’s documentation, or by running ethercat slaves on a system with the IgH EtherCAT master installed.

Digital I/O Example

Using the EtherCAT digital input module above, here is a control program that reads the inputs and uses them:

#![allow(unused)]
fn main() {
use autocore_std::{ControlProgram, TickContext};
use autocore_std::fb::RTrig;
use crate::gm::GlobalMemory;

pub struct MyControlProgram {
    start_trig: RTrig,
    stop_trig: RTrig,
    running: bool,
}

impl MyControlProgram {
    pub fn new() -> Self {
        Self {
            start_trig: RTrig::new(),
            stop_trig: RTrig::new(),
            running: false,
        }
    }
}

impl ControlProgram for MyControlProgram {
    type Memory = GlobalMemory;

    fn process_tick(&mut self, ctx: &mut TickContext<Self::Memory>) {
        // Input 1 = Start button, Input 2 = Stop button
        if self.start_trig.call(ctx.gm.di_input_1) {
            log::info!("Start button pressed");
            self.running = true;
        }
        if self.stop_trig.call(ctx.gm.di_input_2) {
            log::info!("Stop button pressed");
            self.running = false;
        }

        // Input 3 = Emergency stop (normally closed, active low)
        if !ctx.gm.di_input_3 {
            self.running = false;
        }

        // Input 4 = Part sensor
        // Input 5 = Home position sensor
        // etc.
    }
}
}

Motion Control with CiA 402 Drives

Many EtherCAT servo drives follow the CiA 402 device profile, which defines standard PDO objects for:

  • Control word (0x6040): Commands to the drive (enable, start, stop, fault reset)
  • Status word (0x6041): Drive state feedback
  • Target position (0x607A): Position command
  • Position actual (0x6064): Encoder feedback
  • Profile velocity (0x6081): Speed limit for profile moves
  • Profile acceleration (0x6083): Acceleration ramp
  • Profile deceleration (0x6084): Deceleration ramp

AutoCore provides the Axis helper from autocore-std that wraps the CiA 402 state machine, giving you high-level methods like home(), enable(), move_absolute(), and reset_faults().

When you configure an EtherCAT slave with "axis": "pp" (Profile Position mode), the code generator in gm.rs automatically creates a TeknicPpView — a borrowed-reference struct that maps the CiA 402 objects to your global memory fields.

Full Example: Teknic ClearPath Servo

This complete example drives a Teknic ClearPath servo motor in a continuous back-and-forth motion using Profile Position mode.

project.json (EtherCAT slave configuration):

{
  "modules": {
    "ethercat": {
      "enabled": true,
      "args": ["service"],
      "config": {
        "interface_name": "eth0",
        "auto_activate": true,
        "runtime_settings": {
          "cycle_time_us": 5000,
          "priority": 99
        },
        "slaves": [
          {
            "name": "ClearPath_0",
            "axis": "pp",
            "position": 0,
            "device_id": {
              "vendor_id": 4919,
              "product_code": 1,
              "revision_number": 1
            },
            "sync_managers": [
              {
                "direction": "Outputs",
                "index": 2,
                "pdos": [{
                  "name": "RxPDO 5",
                  "entries": [
                    { "index": "0x6040", "name": "Controlword",          "type": "UINT",  "bits": 16 },
                    { "index": "0x607A", "name": "Target Position",      "type": "DINT",  "bits": 32 },
                    { "index": "0x6081", "name": "Profile Velocity",     "type": "UDINT", "bits": 32 },
                    { "index": "0x6083", "name": "Profile Acceleration", "type": "UDINT", "bits": 32 },
                    { "index": "0x6084", "name": "Profile Deceleration", "type": "UDINT", "bits": 32 }
                  ]
                }]
              },
              {
                "direction": "Inputs",
                "index": 3,
                "pdos": [{
                  "name": "TxPDO 5",
                  "entries": [
                    { "index": "0x6041", "name": "Statusword",            "type": "UINT", "bits": 16 },
                    { "index": "0x6064", "name": "Position Actual Value", "type": "DINT", "bits": 32 }
                  ]
                }]
              }
            ]
          }
        ]
      }
    }
  },
  "variables": {
    "clearpath_0_rxpdo_5_controlword":          { "type": "u16", "direction": "command", "link": "ethercat.clearpath_0.rxpdo_5.controlword" },
    "clearpath_0_rxpdo_5_target_position":      { "type": "i32", "direction": "command", "link": "ethercat.clearpath_0.rxpdo_5.target_position" },
    "clearpath_0_rxpdo_5_profile_velocity":     { "type": "u32", "direction": "command", "link": "ethercat.clearpath_0.rxpdo_5.profile_velocity" },
    "clearpath_0_rxpdo_5_profile_acceleration": { "type": "u32", "direction": "command", "link": "ethercat.clearpath_0.rxpdo_5.profile_acceleration" },
    "clearpath_0_rxpdo_5_profile_deceleration": { "type": "u32", "direction": "command", "link": "ethercat.clearpath_0.rxpdo_5.profile_deceleration" },
    "clearpath_0_txpdo_5_statusword":           { "type": "u16", "direction": "status",  "link": "ethercat.clearpath_0.txpdo_5.statusword" },
    "clearpath_0_txpdo_5_position_actual_value":{ "type": "i32", "direction": "status",  "link": "ethercat.clearpath_0.txpdo_5.position_actual_value" }
  }
}

control/src/program.rs:

#![allow(unused)]
fn main() {
use autocore_std::motion::{Axis, AxisConfig, HomingMethod};
use autocore_std::{ControlProgram, TickContext};
use crate::gm::{GlobalMemory, ClearPath0PpStubs};

#[derive(Debug, Clone, Copy, PartialEq)]
enum Step {
    Home,
    WaitHomed,
    Enable,
    WaitEnabled,
    MoveCW,
    WaitCW,
    MoveCCW,
    WaitCCW,
    Reset,
    WaitReset,
}

pub struct MyControlProgram {
    axis: Axis,
    step: Step,
    stubs: ClearPath0PpStubs,
}

impl MyControlProgram {
    pub fn new() -> Self {
        // Configure the axis: 12,800 encoder counts per revolution, display in degrees
        let config = AxisConfig::new(12_800)
            .with_user_scale(360.0);

        Self {
            axis: Axis::new(config, "ClearPath_0"),
            step: Step::Home,
            stubs: ClearPath0PpStubs::default(),
        }
    }
}

impl ControlProgram for MyControlProgram {
    type Memory = GlobalMemory;

    fn initialize(&mut self, _mem: &mut Self::Memory) {
        log::info!("ClearPath reversing program started");
    }

    fn process_tick(&mut self, ctx: &mut TickContext<Self::Memory>) {
        // Destructure self so the borrow checker allows separate field access
        let Self { axis, step, stubs } = self;

        // Build the CiA 402 view (maps global memory fields to the Axis helper)
        let mut view = stubs.view(&mut ctx.gm);

        // Let the Axis state machine process one tick
        axis.tick(&mut view, ctx.client);

        // Application state machine
        match *step {
            Step::Home => {
                axis.home(&mut view, HomingMethod::CurrentPosition);
                log::info!("Homing: setting current position as 0 degrees");
                *step = Step::WaitHomed;
            }
            Step::WaitHomed => {
                if !axis.is_busy {
                    if !axis.is_error {
                        log::info!("Homed at {:.1} degrees", axis.position);
                        *step = Step::Enable;
                    } else {
                        log::error!("Homing failed: {}", axis.error_message);
                        *step = Step::Reset;
                    }
                }
            }
            Step::Enable => {
                axis.enable(&mut view);
                *step = Step::WaitEnabled;
            }
            Step::WaitEnabled => {
                if !axis.is_busy {
                    if axis.motor_on {
                        *step = Step::MoveCW;
                    } else {
                        log::error!("Enable failed: {}", axis.error_message);
                        *step = Step::Reset;
                    }
                }
            }
            Step::MoveCW => {
                // Move to 45 degrees at 90 deg/s, 180 deg/s² accel and decel
                axis.move_absolute(&mut view, 45.0, 90.0, 180.0, 180.0);
                log::info!("Moving CW to 45 degrees");
                *step = Step::WaitCW;
            }
            Step::WaitCW => {
                if !axis.is_busy {
                    if !axis.is_error {
                        log::info!("CW move complete at {:.1} degrees", axis.position);
                        *step = Step::MoveCCW;
                    } else {
                        log::error!("CW move failed: {}", axis.error_message);
                        *step = Step::Reset;
                    }
                }
            }
            Step::MoveCCW => {
                axis.move_absolute(&mut view, 0.0, 90.0, 180.0, 180.0);
                log::info!("Moving CCW to 0 degrees");
                *step = Step::WaitCCW;
            }
            Step::WaitCCW => {
                if !axis.is_busy {
                    if !axis.is_error {
                        log::info!("CCW move complete at {:.1} degrees", axis.position);
                        *step = Step::MoveCW; // Repeat
                    } else {
                        log::error!("CCW move failed: {}", axis.error_message);
                        *step = Step::Reset;
                    }
                }
            }
            Step::Reset => {
                axis.reset_faults(&mut view);
                *step = Step::WaitReset;
            }
            Step::WaitReset => {
                if !axis.is_busy {
                    *step = Step::Enable;
                }
            }
        }
    }
}
}

Key points:

  • ClearPath0PpStubs is auto-generated in gm.rs. It holds CiA 402 fields that are not in the PDO mapping (like modes_of_operation) so you don’t have to manage them manually.
  • stubs.view(&mut ctx.gm) creates a TeknicPpView that the Axis helper can use.
  • The let Self { axis, step, stubs } = self; destructuring is necessary so Rust’s borrow checker can see that stubs, axis, and step are separate borrows.
  • axis.tick() must be called every cycle — it processes the CiA 402 state machine internally.
  • Axis provides high-level methods: home(), enable(), move_absolute(), reset_faults(), and status fields: is_busy, is_error, motor_on, position, error_message.