Current section
Files
Jump to
Current section
Files
lib/ethercat/master/startup.ex
defmodule EtherCAT.Master.Startup do
@moduledoc false
require Logger
alias EtherCAT.{Bus, Domain, Slave}
alias EtherCAT.Bus.Transaction
alias EtherCAT.DC.API, as: DCAPI
alias EtherCAT.Master.Config
alias EtherCAT.Master.Config.DomainPlan
alias EtherCAT.Master.Startup.InitRecovery
alias EtherCAT.Master.Startup.Reset, as: InitReset
alias EtherCAT.Master.Startup.Verification, as: InitVerification
alias EtherCAT.Slave.ESC.Registers
@frame_timeout_base_us 200
@frame_timeout_per_slave_us 40
@frame_timeout_host_floor_ms 2
@frame_timeout_max_ms 10
@init_poll_limit 100
@init_poll_interval_ms 10
@max_auto_increment_slaves 32_769
@max_station_address 0xFFFF
@spec tune_bus_frame_timeout(%EtherCAT.Master{}, non_neg_integer()) :: :ok
def tune_bus_frame_timeout(data, slave_count) do
if bus_running?() do
target_ms = recommended_frame_timeout_ms(data, slave_count)
case Bus.set_frame_timeout(Bus, target_ms) do
:ok ->
Logger.info(
"[Master] bus frame timeout set to #{target_ms}ms (slaves=#{slave_count}, dc_cycle_ns=#{inspect(dc_cycle_ns(data))})"
)
:ok
{:error, reason} ->
Logger.warning(
"[Master] failed to tune bus frame timeout to #{target_ms}ms: #{inspect(reason)}"
)
:ok
end
else
:ok
end
end
@spec configure_network(%EtherCAT.Master{}) ::
{:ok, %EtherCAT.Master{}} | {:error, term(), %EtherCAT.Master{}}
def configure_network(data) do
count = data.slave_count
Logger.info("[Master] configuring #{count} slave(s)")
with :ok <- validate_topology_addressing(data, count),
{:ok, stations} <- assign_station_addresses(data, count),
{:ok, slave_topology} <- read_topology_statuses(stations),
:ok <- reset_slaves_to_init(stations),
{:ok, dc_ref_station, dc_stations} <- initialize_distributed_clocks(data, slave_topology),
{:ok, domain_refs} <- start_domains(data),
{:ok, effective_slave_configs, slaves, pending_preop, activatable_slaves, slave_refs} <-
start_slaves(data, count, if(dc_ref_station, do: dc_cycle_ns(data), else: nil)) do
{:ok,
%{
data
| dc_ref_station: dc_ref_station,
dc_stations: dc_stations,
slave_configs: effective_slave_configs,
slaves: slaves,
pending_preop: MapSet.new(pending_preop),
activatable_slaves: activatable_slaves,
activation_failures: %{},
domain_refs: domain_refs,
slave_refs: slave_refs
}}
else
{:error, reason} ->
{:error, reason, data}
{:error, reason, started_slaves} ->
{:error, reason, %{data | slaves: started_slaves}}
end
end
@doc false
@spec recommended_frame_timeout_ms(%EtherCAT.Master{}, non_neg_integer()) :: pos_integer()
def recommended_frame_timeout_ms(%{frame_timeout_override_ms: timeout_ms}, _slave_count)
when is_integer(timeout_ms) and timeout_ms > 0 do
timeout_ms
end
def recommended_frame_timeout_ms(data, slave_count)
when is_integer(slave_count) and slave_count > 0 do
topology_timeout_ms =
@frame_timeout_base_us
|> Kernel.+(slave_count * @frame_timeout_per_slave_us)
|> ceil_div(1_000)
cycle_cap_ms = cycle_relative_timeout_cap_ms(data)
floor_ms = min(@frame_timeout_host_floor_ms, cycle_cap_ms)
topology_timeout_ms
|> max(floor_ms)
|> min(cycle_cap_ms)
|> min(@frame_timeout_max_ms)
end
def recommended_frame_timeout_ms(data, _slave_count) do
min(@frame_timeout_host_floor_ms, cycle_relative_timeout_cap_ms(data))
end
@doc false
@spec validate_topology_addressing(%EtherCAT.Master{}, non_neg_integer()) ::
:ok | {:error, term()}
def validate_topology_addressing(%{base_station: base_station}, slave_count)
when is_integer(base_station) and is_integer(slave_count) and slave_count >= 0 do
cond do
slave_count > @max_auto_increment_slaves ->
{:error,
{:unsupported_topology,
{:too_many_slaves_for_auto_increment, slave_count, @max_auto_increment_slaves}}}
slave_count > 0 and base_station + slave_count - 1 > @max_station_address ->
{:error,
{:unsupported_topology,
{:station_address_overflow, base_station, slave_count, @max_station_address}}}
true ->
:ok
end
end
defp ceil_div(value, divisor) when is_integer(value) and is_integer(divisor) and divisor > 0 do
div(value + divisor - 1, divisor)
end
defp cycle_relative_timeout_cap_ms(%{domain_configs: domain_configs})
when is_list(domain_configs) and domain_configs != [] do
domain_configs
|> Enum.map(&domain_cycle_time_us/1)
|> Enum.min()
|> half_cycle_timeout_ms()
|> min(@frame_timeout_max_ms)
|> max(1)
end
defp cycle_relative_timeout_cap_ms(data) do
case dc_cycle_ns(data) do
cycle_ns when is_integer(cycle_ns) and cycle_ns > 0 ->
cycle_ns
|> div(1_000)
|> half_cycle_timeout_ms()
|> min(@frame_timeout_max_ms)
|> max(1)
_ ->
@frame_timeout_max_ms
end
end
defp domain_cycle_time_us(%DomainPlan{cycle_time_us: cycle_time_us}), do: cycle_time_us
defp domain_cycle_time_us(%{cycle_time_us: cycle_time_us}), do: cycle_time_us
defp half_cycle_timeout_ms(cycle_time_us)
when is_integer(cycle_time_us) and cycle_time_us > 0 do
cycle_time_us
|> ceil_div(2)
|> ceil_div(1_000)
end
defp station_for_position(data, pos), do: data.base_station + pos
defp assign_station_addresses(data, count) do
stations = Enum.map(0..(count - 1), &station_for_position(data, &1))
result =
Enum.reduce_while(0..(count - 1), :ok, fn pos, :ok ->
station = station_for_position(data, pos)
case Bus.transaction(Bus, Transaction.apwr(pos, Registers.station_address(station))) do
{:ok, [%{wkc: 1}]} ->
{:cont, :ok}
{:ok, [%{wkc: wkc}]} ->
{:halt, {:error, {:station_assign_failed, pos, station, {:unexpected_wkc, wkc}}}}
{:error, reason} ->
{:halt, {:error, {:station_assign_failed, pos, station, reason}}}
end
end)
case result do
:ok -> {:ok, stations}
{:error, _} = err -> err
end
end
defp read_topology_statuses(stations) do
Enum.reduce_while(stations, {:ok, []}, fn station, {:ok, acc} ->
case Bus.transaction(Bus, Transaction.fprd(station, Registers.dl_status())) do
{:ok, [%{data: status, wkc: 1}]} ->
{:cont, {:ok, [{station, status} | acc]}}
{:ok, [%{wkc: wkc}]} ->
{:halt, {:error, {:topology_read_failed, station, {:unexpected_wkc, wkc}}}}
{:error, reason} ->
{:halt, {:error, {:topology_read_failed, station, reason}}}
end
end)
|> case do
{:ok, topology_rev} -> {:ok, Enum.reverse(topology_rev)}
{:error, _} = err -> err
end
end
defp reset_slaves_to_init(stations) do
count = length(stations)
with :ok <- reset_slaves_to_default(count),
:ok <- broadcast_init_ack(count),
:ok <- verify_init_states(stations, @init_poll_limit) do
:ok
else
{:error, _} = err ->
err
end
end
defp reset_slaves_to_default(count) do
case Bus.transaction(Bus, InitReset.transaction()) do
{:ok, replies} ->
case InitReset.validate_results(replies, count) do
:ok ->
:ok
{:error, wkcs, ^count} ->
{:error, {:init_default_reset_failed, wkcs, count}}
end
{:error, _} = err ->
err
end
end
defp broadcast_init_ack(count) do
case Bus.transaction(Bus, Transaction.bwr(Registers.al_control(0x11))) do
{:ok, replies} ->
case InitReset.validate_init_ack_reply(replies, count) do
:ok ->
:ok
{:partial, wkc, ^count} ->
Logger.warning(
"[Master] partial broadcast init-ack response during reset: wkc=#{wkc} expected<=#{count}; continuing with per-station init verification"
)
:ok
{:error, {:unexpected_wkc, _, _} = reason} ->
{:error, {:init_reset_failed, reason}}
end
{:error, _} = err ->
err
end
end
defp verify_init_states(_stations, 0), do: {:error, :init_verification_exhausted}
defp verify_init_states(stations, attempts_left) do
statuses = Enum.map(stations, &read_init_status/1)
blocking = InitVerification.blocking_statuses(statuses)
if blocking == [] do
log_lingering_init_errors(InitVerification.lingering_error_statuses(statuses))
:ok
else
if attempts_left == 1 do
{:error, {:init_verification_failed, blocking}}
else
with :ok <- recover_init_states(blocking) do
Process.sleep(@init_poll_interval_ms)
verify_init_states(stations, attempts_left - 1)
end
end
end
end
defp recover_init_states(statuses) do
statuses
|> InitRecovery.actions()
|> Enum.reduce_while(:ok, fn
{:ack_error, station, control}, :ok ->
case write_al_control(station, control) do
:ok -> {:cont, :ok}
{:error, reason} -> {:halt, {:error, {:init_recovery_failed, station, reason}}}
end
{:request_init, station, control}, :ok ->
case write_al_control(station, control) do
:ok -> {:cont, :ok}
{:error, reason} -> {:halt, {:error, {:init_recovery_failed, station, reason}}}
end
end)
end
defp write_al_control(station, control) do
case Bus.transaction(Bus, Transaction.fpwr(station, Registers.al_control(control))) do
{:ok, [%{wkc: 1}]} -> :ok
{:ok, [%{wkc: wkc}]} -> {:error, {:unexpected_wkc, wkc}}
{:error, reason} -> {:error, reason}
end
end
defp read_init_status(station) do
case Bus.transaction(Bus, Transaction.fprd(station, Registers.al_status())) do
{:ok, [%{data: <<_::3, error::1, state::4, _::8>>, wkc: 1}]} ->
%{
station: station,
state: state,
error: error,
error_code: if(error == 1, do: read_al_status_code(station), else: nil)
}
{:ok, [%{wkc: wkc}]} ->
%{station: station, state: nil, error: nil, error_code: nil, wkc: wkc}
{:error, reason} ->
%{station: station, state: nil, error: nil, error_code: nil, error_reason: reason}
end
end
defp read_al_status_code(station) do
case Bus.transaction(Bus, Transaction.fprd(station, Registers.al_status_code())) do
{:ok, [%{data: <<code::16-little>>, wkc: 1}]} -> code
_ -> nil
end
end
defp log_lingering_init_errors([]), do: :ok
defp log_lingering_init_errors(statuses) do
Logger.debug(
"[Master] continuing with slaves in INIT but with AL error latched: #{inspect(statuses)}"
)
end
defp initialize_distributed_clocks(%{dc_config: nil}, _slave_topology) do
{:ok, nil, []}
end
defp initialize_distributed_clocks(_data, slave_topology) do
case DCAPI.initialize_clocks(Bus, slave_topology) do
{:ok, ref_station, dc_stations} ->
Logger.info("[Master] DC initialized, ref=0x#{Integer.to_string(ref_station, 16)}")
{:ok, ref_station, dc_stations}
{:error, :no_dc_capable_slave} ->
Logger.debug("[Master] no DC-capable slaves found — running without DC")
{:ok, nil, []}
{:error, reason} ->
Logger.warning("[Master] DC init failed (#{inspect(reason)}) — running without DC")
{:ok, nil, []}
end
end
defp start_domains(data) do
Enum.reduce_while(data.domain_configs || [], {:ok, %{}}, fn entry, {:ok, refs} ->
domain_opts = Config.domain_start_opts(entry)
id = entry.id
case DynamicSupervisor.start_child(
EtherCAT.SessionSupervisor,
{Domain, [{:bus, Bus} | domain_opts]}
) do
{:ok, pid} ->
{:cont, {:ok, Map.put(refs, Process.monitor(pid), id)}}
{:error, {:already_started, pid}} ->
{:cont, {:ok, Map.put(refs, Process.monitor(pid), id)}}
{:error, reason} ->
{:halt, {:error, {:domain_start_failed, id, reason}}}
end
end)
end
defp start_slaves(data, bus_count, dc_cycle_ns) do
with {:ok, effective_config} <-
Config.effective_slave_config(data.slave_configs || [], bus_count) do
Enum.with_index(effective_config)
|> Enum.reduce_while(
{:ok, [], [], [], %{}},
fn {entry, pos}, {:ok, slave_acc, pending_acc, activatable_acc, slave_refs} ->
station = station_for_position(data, pos)
name = entry.name
opts = [
bus: Bus,
station: station,
name: name,
driver: entry.driver,
config: entry.config,
process_data: entry.process_data,
dc_cycle_ns: dc_cycle_ns,
sync: entry.sync,
health_poll_ms: entry.health_poll_ms
]
case DynamicSupervisor.start_child(EtherCAT.SlaveSupervisor, {Slave, opts}) do
{:ok, pid} ->
next_activatable =
if entry.target_state == :op do
[name | activatable_acc]
else
activatable_acc
end
{:cont,
{:ok, [{name, station} | slave_acc], [name | pending_acc], next_activatable,
Map.put(slave_refs, Process.monitor(pid), name)}}
{:error, reason} ->
{:halt,
{:error, {:slave_start_failed, name, station, reason}, Enum.reverse(slave_acc)}}
end
end
)
|> case do
{:ok, slaves, pending, activatable, slave_refs} ->
{:ok, effective_config, Enum.reverse(slaves), Enum.reverse(pending),
Enum.reverse(activatable), slave_refs}
{:error, reason, started_slaves} ->
{:error, reason, started_slaves}
{:error, _} = err ->
err
end
end
end
defp dc_cycle_ns(%{dc_config: %{cycle_ns: cycle_ns}})
when is_integer(cycle_ns) and cycle_ns > 0,
do: cycle_ns
defp dc_cycle_ns(_data), do: nil
defp bus_running? do
is_pid(Process.whereis(Bus))
end
end