EtherCAT
Pure-Elixir EtherCAT master for Linux and Nerves, built on OTP without NIFs or kernel modules. Intended for discrete I/O, diagnostics, and soft-real-time 1–10 ms cyclic loops—not sub-millisecond hard-real-time control.
Experimental; not production-ready. This project explores soft-real-time automation. I would like to develop it into a bachelor's thesis; if you know a professor who could help, please reach out.
Want an interactive UI? Start with
kino_ethercat.
Without hardware, try the UDP simulator walkthrough
or run the simulator tests below.
1. Install and supervise the runtime
This guide targets EtherCAT 0.5.0. Add the Hex dependency to your
application's mix.exs:
def deps do
[{:ethercat, "~> 0.5.0"}]
end
Use the 0.5.0 API reference.
Upgrading from 0.4.x? Read the migration notes below;
0.5.0 changes the runtime and driver APIs without compatibility shims.
Run mix deps.get, then add {EtherCAT.Runtime, []} to your application's
supervision tree. EtherCAT does not start the runtime automatically.
For a standalone iex -S mix session, start it once with:
{:ok, _supervisor} =
Supervisor.start_link([{EtherCAT.Runtime, []}], strategy: :one_for_one)
2. Discover your hardware
Use a dedicated Ethernet interface connected to your EtherCAT ring. Raw socket
access needs root or CAP_NET_RAW. To grant the capability to your current BEAM
executable (this affects every application using that executable):
BEAM=$(readlink -f "$(dirname "$(dirname "$(command -v erl)")")"/erts-*/bin/beam.smp)
sudo setcap cap_net_raw+ep "$BEAM"
Restart IEx/the application after granting the capability. Replace eth0 with
your interface. Scan before opening a master session; scanning assigns
station addresses and must not run against an active ring.
backend = {:raw, %{interface: "eth0"}}
{:ok, scan} = EtherCAT.Scan.scan(backend)
scan.discovered_slaves
To inspect slaves without cyclic I/O, start in PREOP:
{:ok, session} = EtherCAT.start(backend: backend)
:ok = EtherCAT.await_ready(session)
EtherCAT.Diagnostics.slaves(session)
:ok = EtherCAT.stop(session)
3. Read a cyclic input
Drivers are application-owned. This minimal driver reads channel 1 of an EL1809; static endpoint metadata is optional. For shared mappings and metadata, see the driver authoring guide.
defmodule MyApp.EL1809 do
@behaviour EtherCAT.Driver
@impl true
def signal_model(_config, _pdos), do: [ch1: EtherCAT.Driver.Signal.whole_pdo(0x1A00)]
@impl true
def decode_signal(_signal, _config, <<0::7, bit::1>>), do: {:ok, bit == 1}
def decode_signal(_signal, _config, _raw), do: {:error, :invalid_data}
end
List slaves in physical ring order. This example assumes an EK1100 coupler followed by an EL1809; adapt it to the hardware you discovered before starting:
{:ok, session} = EtherCAT.start(
backend: backend,
domains: [%EtherCAT.Domain.Config{id: :io, cycle_time_us: 1_000}],
slaves: [
%EtherCAT.Slave.Config{name: :coupler},
%EtherCAT.Slave.Config{
name: :inputs,
driver: MyApp.EL1809,
process_data: {:all, :io},
target_state: :op
}
]
)
:ok = EtherCAT.await_operational(session)
EtherCAT.sample(session, :inputs, :io)
#=> {:ok, %EtherCAT.Sample{inputs: %{ch1: false}, errors: %{}, ...}}
# May return {:error, :not_ready} before the first sample arrives.
:ok = EtherCAT.stop(session)
Keep the returned session for every runtime call. Stopping it leaves the
host-supervised runtime alive; reusing a stopped session returns
{:error, :stale_session}. A sample covers one domain cycle, not a cross-domain
snapshot. Check its errors as well as its inputs.
4. Diagnose problems
Inspect a live session before stopping it:
EtherCAT.state(session)
EtherCAT.Diagnostics.master_status(session)
EtherCAT.status(session, :inputs)
| Symptom | Action |
|---|---|
| Raw socket permission failure | Check the interface and capability on the BEAM executable actually running your app. |
| Session stays in PREOP | Use target_state: :op and attach signals to a domain; await_ready/1 does not require cyclic I/O. |
Activation fails or state is :recovering |
Inspect master and slave status for transport, WKC, AL-state, or configuration faults. |
| Sample has decoding errors | Check the driver's mapping, bit widths, and codec; failed values are not replaced with zero or earlier values. |
| Subscriber mailbox grows | Poll with EtherCAT.samples/2 instead; subscriptions have no backpressure or dropping. |
Upgrade guide
For applications using 0.4.x:
- Supervise the runtime yourself. Add
{EtherCAT.Runtime, []}to the host supervision tree; starting the:ethercatapplication no longer starts a master. - Keep the session.
EtherCAT.start/1returns{:ok, session}, not:ok. Pass it to runtime, provisioning, diagnostics, signal, and capture operations. A stopped session returns{:error, :stale_session}; it never follows a restart. - Use an explicit backend. Replace
interface: "eth0"withbackend: {:raw, %{interface: "eth0"}}. UDP and redundant raw configurations also useEtherCAT.Backend.
| 0.4.x call | 0.5.0 replacement |
|---|---|
EtherCAT.await_running() |
EtherCAT.await_ready(session); use await_operational(session) for cyclic I/O |
EtherCAT.read_input(slave, signal) |
EtherCAT.read(session, slave, signal) |
EtherCAT.write_output(slave, signal, value) |
EtherCAT.write(session, slave, signal, value) |
EtherCAT.subscribe(slave, signal) |
EtherCAT.Signals.subscribe(session, slave, signal) |
EtherCAT.configure_slave(slave, opts) / activate() / deactivate() |
EtherCAT.Provisioning equivalents, with the session first |
SDO transfers and DC lock waits on EtherCAT |
EtherCAT.Provisioning, with the session first |
Slave/domain/DC inspection on EtherCAT |
EtherCAT.Diagnostics, with the session first |
EtherCAT.stop() |
EtherCAT.stop(session) |
EtherCAT.slaves(session) returns configured names; use
EtherCAT.Diagnostics.slaves(session) for detailed summaries. For coherent
per-domain observations, use sample/3 or subscribe/3 on EtherCAT.
Protocol subscriptions return {:ok, ref, status, samples} and deliver
{:ethercat, ref, payload} messages; cancel them with unsubscribe/3.
Migrate custom drivers from EtherCAT.Slave.Driver to EtherCAT.Driver.
Implement signal_model/2 with named EtherCAT.Driver.Signal mappings, and
return {:ok, value} or {:error, reason} from codecs. Mailbox setup moves to
c:EtherCAT.Driver.Provisioning.mailbox_steps/2; consume lifecycle notifications
and latch subscriptions instead of inline driver hooks. Simulator companions
implement EtherCAT.Simulator.Adapter and supply an explicit profile.
See the driver contract
and full changelog.
Test without hardware
From a repository checkout:
mix deps.get
ETHERCAT_INTEGRATION_TRANSPORT=udp mix test test/integration/simulator --exclude raw_socket --exclude raw_socket_redundant --exclude raw_socket_redundant_toggle
mix test test/ethercat/driver/catalogue_example_test.exs
The simulator suite exercises the real master over virtual slave segments. The driver comparison demonstrates a shared signal catalogue without changing the public callbacks. The command above explicitly excludes raw transports. Hardware and raw-socket tests need separate setup.
Pick your next task
- Write a driver: callback contract and catalogue pattern.
- Configure a PREOP session or use SDOs:
EtherCAT.Provisioning. - Write outputs or subscribe to observations:
EtherCAT; signal/latch subscriptions:EtherCAT.Signals. - Build virtual devices or inject faults:
EtherCAT.Simulator. - Capture hardware:
iex -S mix ethercat.capture --interface eth0; seeEtherCAT.Capture. - Run hardware checks:
MIX_ENV=test mix run test/integration/hardware/scripts/scan.exs --interface eth0; see the hardware guide before running bench scripts. - Understand internals: architecture. Build the API reference for your checkout with
mix docs.