EtherCAT.Slave.Driver behaviour (ethercat v0.4.2)

Copy Markdown View Source

Behaviour for slave-specific drivers.

A driver is a pure module — no process state, no Application.get_env. All configuration is passed as a config map from EtherCAT.start/1.

If a slave config omits :driver, EtherCAT.Slave.Driver.Default is used. That default driver exposes no PDO profile and is intended for couplers or dynamically configured devices.

Generic SYNC0/SYNC1/latch intent does not live in the driver. It belongs on %EtherCAT.Slave.Config{sync: %EtherCAT.Slave.Sync.Config{...}}. Drivers only own device-specific translation through the optional sync_mode/2 callback when a slave application needs additional mailbox objects beyond the generic ESC DC registers.

Optional identity and simulator support

Drivers may optionally declare static device identity through identity/0. That metadata is useful for future driver discovery against scanned bus identity and for tooling. Keep it high-level: vendor, product, and revision.

Exact simulator authoring does not live in the real driver behaviour. Use an optional MyDriver.Simulator companion module that implements EtherCAT.Simulator.DriverAdapter when EtherCAT.Simulator.Slave.from_driver/2 needs profile-specific simulator configuration.

Signal model

signal_model/1 returns a keyword list of {signal_name, declaration} pairs. Each value declares where that signal lives in the slave's PDO layout:

  • an integer means "this signal spans the whole PDO at that index"
  • %EtherCAT.Slave.ProcessData.Signal{} may select a bit-range inside a PDO

The master reads SII EEPROM categories 0x0032 (TxPDO) and 0x0033 (RxPDO) to derive SyncManager assignment, direction, total SM size, and each PDO's bit offset within its SyncManager. The driver's signal model sits on top of that hardware description and names the application-facing signals.

Use a keyword list (not a map) so signal order is explicit and deterministic.

[channels: 0x1A00]

Each signal is encoded and decoded independently. Sub-byte signals (e.g. 1-bit digital channels) receive/return 1 padded byte with the value in bit 0 (LSB). Larger signals receive exactly enough bytes to carry the declared bit_size.

Example — EL1809 / EL2809 digital cards

Full compiled example drivers live in the test integration support tree:

  • test/integration/support/drivers/el1809.ex
  • test/integration/support/drivers/el2809.ex

They show the minimal signal_model/1 and bit-oriented encode_signal/3 / decode_signal/3 callbacks for Beckhoff-style 16-channel digital I/O terminals.

Example — EL3202 (2-ch PT100 input, 2 × 32-bit TxPDOs)

defmodule MyApp.EL3202 do
  @behaviour EtherCAT.Slave.Driver

  @impl true
  def signal_model(_config) do
    # 0x1A00 = channel 1 (SM3, bytes 0–3), 0x1A01 = channel 2 (SM3, bytes 4–7)
    [channel1: 0x1A00, channel2: 0x1A01]
  end

  @impl true
  def mailbox_config(_config) do
    [
      {:sdo_download, 0x8000, 0x19, <<8::16-little>>},
      {:sdo_download, 0x8010, 0x19, <<8::16-little>>}
    ]
  end

  @impl true
  def encode_signal(_signal, _config, _value), do: <<>>

  @impl true
  def decode_signal(:channel1, _config, <<
        _::1, error::1, _::2, _::2, overrange::1, underrange::1,
        toggle::1, state::1, _::6, value::16-little>>) do
    %{ohms: value / 16.0, overrange: overrange == 1, underrange: underrange == 1,
      error: error == 1, invalid: state == 1, toggle: toggle}
  end
  def decode_signal(:channel2, _config, <<
        _::1, error::1, _::2, _::2, overrange::1, underrange::1,
        toggle::1, state::1, _::6, value::16-little>>) do
    %{ohms: value / 16.0, overrange: overrange == 1, underrange: underrange == 1,
      error: error == 1, invalid: state == 1, toggle: toggle}
  end
  def decode_signal(_signal, _config, _), do: nil
end

Summary

Callbacks

Decode raw input bytes for one logical signal from the process image.

Encode one logical output signal into raw bytes for the process image.

Static device identity for this driver, or nil if the driver does not declare one.

Return PREOP mailbox configuration steps.

Called when an ESC hardware LATCH event is captured during Op.

Called on entry to Op state. Optional.

Called on entry to PreOp state. Optional.

Called on entry to SafeOp state. Optional.

Return the driver's logical signal model.

Optional 2-arity variant of signal_model/1 that receives SII PDO configs.

Translate public sync intent into device-specific PREOP mailbox steps.

Types

config()

@type config() :: map()

identity()

@type identity() :: %{
  :vendor_id => non_neg_integer(),
  :product_code => non_neg_integer(),
  optional(:revision) => non_neg_integer() | :any
}

latch_edge()

@type latch_edge() :: :pos | :neg

mailbox_step()

@type mailbox_step() ::
  {:sdo_download, index :: non_neg_integer(), subindex :: non_neg_integer(),
   data :: binary()}

signal_name()

@type signal_name() :: atom()

Callbacks

decode_signal(signal_name, config, binary)

@callback decode_signal(signal_name(), config(), binary()) :: term()

Decode raw input bytes for one logical signal from the process image.

encode_signal(signal_name, config, term)

@callback encode_signal(signal_name(), config(), term()) :: binary()

Encode one logical output signal into raw bytes for the process image.

identity()

@callback identity() :: identity() | nil

Static device identity for this driver, or nil if the driver does not declare one.

This is useful for future driver discovery against scanned bus identity and for simulator hydration. Keep it high-level: vendor/product/revision only. Do not put raw ESC register images or SII binaries into the driver API.

mailbox_config(config)

(optional)
@callback mailbox_config(config()) :: [mailbox_step()]

Return PREOP mailbox configuration steps.

Currently the runtime supports {:sdo_download, index, subindex, data} steps. They execute in order before SyncManager/FMMU configuration, so this callback can perform dynamic PDO remapping (0x1600+, 0x1A00+, 0x1C12, 0x1C13) or any other CoE parameterization required before SAFEOP.

data may be any non-empty binary. The runtime selects expedited or segmented CoE transfer mode automatically.

on_latch(atom, config, arg3, latch_edge, non_neg_integer)

(optional)
@callback on_latch(atom(), config(), 0 | 1, latch_edge(), non_neg_integer()) :: :ok

Called when an ESC hardware LATCH event is captured during Op.

timestamp_ns is DC system time in ns since 2000-01-01.

on_op(slave_name, config)

(optional)
@callback on_op(slave_name :: atom(), config()) :: :ok

Called on entry to Op state. Optional.

on_preop(slave_name, config)

(optional)
@callback on_preop(slave_name :: atom(), config()) :: :ok

Called on entry to PreOp state. Optional.

on_safeop(slave_name, config)

(optional)
@callback on_safeop(slave_name :: atom(), config()) :: :ok

Called on entry to SafeOp state. Optional.

signal_model(config)

@callback signal_model(config()) :: [
  {signal_name(), non_neg_integer() | EtherCAT.Slave.ProcessData.Signal.t()}
]

Return the driver's logical signal model.

Each signal maps to either a whole PDO index or a %Signal{} slice.

An optional 2-arity version signal_model/2 receives the SII PDO configs as its second argument. When exported, the runtime calls it instead of /1, giving the driver access to hardware layout for dynamic model generation. The Default driver uses this to auto-discover all PDOs without any hand-written mapping.

signal_model(config, sii_pdo_configs)

(optional)
@callback signal_model(config(), sii_pdo_configs :: [map()]) :: [
  {signal_name(), non_neg_integer() | EtherCAT.Slave.ProcessData.Signal.t()}
]

Optional 2-arity variant of signal_model/1 that receives SII PDO configs.

Each entry in sii_pdo_configs is a map with keys:

  • :index — PDO object index (e.g. 0x1A00)
  • :direction:input or :output
  • :sm_index — SyncManager index
  • :bit_size — total PDO size in bits
  • :bit_offset — PDO offset within its SyncManager image in bits

When this callback is exported, it takes precedence over signal_model/1.

sync_mode(config, t)

(optional)
@callback sync_mode(config(), EtherCAT.Slave.Sync.Config.t()) :: [mailbox_step()]

Translate public sync intent into device-specific PREOP mailbox steps.

Use this for slaves that need object-dictionary sync-mode configuration (for example 0x1C32 / 0x1C33) in addition to the generic ESC SYNC setup handled by the runtime.

EtherCAT.Slave.Sync.CoE provides helpers for the common synchronization mode and cycle-time objects when a driver wants to avoid hand-writing raw SDO tuples.

Functions

identity(driver)

@spec identity(module()) :: identity() | nil

signal_model(driver, config)

@spec signal_model(module(), config()) :: [
  {signal_name(), non_neg_integer() | EtherCAT.Slave.ProcessData.Signal.t()}
]

signal_model(driver, config, sii_pdo_configs)

@spec signal_model(module(), config(), [map()]) :: [
  {signal_name(), non_neg_integer() | EtherCAT.Slave.ProcessData.Signal.t()}
]