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.extest/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
@type config() :: map()
@type identity() :: %{ :vendor_id => non_neg_integer(), :product_code => non_neg_integer(), optional(:revision) => non_neg_integer() | :any }
@type latch_edge() :: :pos | :neg
@type mailbox_step() :: {:sdo_download, index :: non_neg_integer(), subindex :: non_neg_integer(), data :: binary()}
@type signal_name() :: atom()
Callbacks
@callback decode_signal(signal_name(), config(), binary()) :: term()
Decode raw input bytes for one logical signal from the process image.
@callback encode_signal(signal_name(), config(), term()) :: binary()
Encode one logical output signal into raw bytes for the process image.
@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.
@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.
@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.
Called on entry to Op state. Optional.
Called on entry to PreOp state. Optional.
Called on entry to SafeOp state. Optional.
@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.
@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—:inputor: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.
@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
@spec signal_model(module(), config()) :: [ {signal_name(), non_neg_integer() | EtherCAT.Slave.ProcessData.Signal.t()} ]
@spec signal_model(module(), config(), [map()]) :: [ {signal_name(), non_neg_integer() | EtherCAT.Slave.ProcessData.Signal.t()} ]