Привод, которого нет в каталоге lcec, заводится через generic — картой PDO, набранной руками в XML: у нашего стенда это 236 строк, и ошибка в одном индексе означает «слейв не дошёл до OP», больше лог не скажет ничего. Свой драйвер CiA 402 заменяет эту простыню на строку <slave type="VD3E" name="s"/> и занимает двадцать содержательных строк. Ниже — зачем его писать и когда не надо, что нужно узнать про прибор, где это взять и что ломается по дороге. Маршрут: как устроена связка и где в ней lcec → что у пяти приводов одинаково, а что у каждого своё → как снять с незнакомого привода пять нужных фактов четырьмя командами → сам драйвер целиком, с разбором → что ломается и как это выглядит в логе.

Что такое lcec и где он стоит

LinuxCNC про EtherCAT ничего не знает. Он умеет одно: раз в такт, у нас раз в миллисекунду, прочитать и записать значения на пинах HAL — своей внутренней шины сигналов. Интерполятор решил, что ось X должна оказаться на отметке 100,5 мм, и положил это число на пин. Дальше кто-то должен доставить его в привод.

Доставкой занимаются два слоя, и их полезно не путать.

Мастер IgH EtherCAT — драйвер сетевой карты, который гоняет по витой паре кадр с жёстким периодом. Про станки он не знает ничего: его дело — довезти набор байтов до каждого устройства на шине и забрать набор байтов обратно. Ставится отдельно от LinuxCNC, живёт своей службой, проверяется своей утилитой ethercat.

linuxcnc-ethercat, он же lcec — HAL-компонент, склейка между ними. Он заводит пины в HAL, знает, в каком месте кадра у конкретного привода лежит целевая позиция, и раз в такт перекладывает одно в другое. Он же поднимает шину при старте, проводя каждое устройство по лестнице INIT → PREOP → SAFEOP → OP, где OP означает «участвует в циклическом обмене». Половина всех бед выглядит как «слейв не дошёл до OP».

Цепочка целиком:

G-код → интерполятор LinuxCNC → пины HAL → lcec → мастер IgH →
     кадр EtherCAT → привод → мотор

Статья про третье звено. Первые два ставятся штатно и работают.

Как это выглядит в конфиге станка

lcec — обычный HAL-компонент, который LinuxCNC загружает при старте машины; отдельной службы у него нет. В .hal-файле это четыре строки:

loadusr -W lcec_conf ethercat-conf.xml   # прочитать описание шины, дождаться готовности
loadrt lcec                              # загрузить сам компонент в реальном времени
addf lcec.read-all  servo-thread         # забрать входы кадра в начале такта
addf lcec.write-all servo-thread         # отдать выходы в конце такта

Порядок здесь не косметика: read-all стоит первым в такте, write-all последним, между ними работает интерполятор. Если поменять местами — команда уедет в привод на такт позже, чем посчитана, и ось получит лишнюю миллисекунду запаздывания в контуре.

ethercat-conf.xml — описание шины: какой мастер, с каким периодом, какие устройства и в каком порядке стоят в цепи. Именно этот файл раздувается до двухсот строк, когда драйвера нет, и схлопывается в четыре тега, когда он есть.

После загрузки привод виден как набор пинов HAL, и ось привязывается к нему обычными net:

net x-pos-cmd  joint.0.motor-pos-cmd  => lcec.0.x.srv-target-position
net x-pos-fb   lcec.0.x.srv-actual-position => joint.0.motor-pos-fb
net x-enable   joint.0.amp-enable-out => lcec.0.x.srv-enable

Всё, что даёт драйвер, — это правильные имена слева и правильные байты справа. Больше LinuxCNC от EtherCAT ничего и не нужно.

Как устроен сам репозиторий lcec

Полезно представлять, куда вы будете класть свой файл.

  • src/lcec_main.c, src/lcec_conf.c — ядро: разбор XML, лестница состояний, такт. Его вы не трогаете.

  • src/devices/lcec_*.cкарточки устройств, по файлу на вендора или семейство. Это и есть «драйверы»; ваш будет соседом.

  • src/lcec_class_cia402.cкласс: готовая реализация профиля CiA 402, общая для всех приводов. Она собирает карту PDO, заводит пины srv-*, разворачивает слово состояния. Ради неё всё и затевается: карточка устройства сводится к декларации «вот мои цифры, вот мои лимиты, включи вот эти режимы». Классы есть и другие — например, для цифрового ввода-вывода.

  • documentation/*.md — страница на устройство: пины, модпараметры, грабли. В апстриме это часть вклада, а не приложение к нему.

Слово «драйвер» здесь стоит понимать буквально как «описание прибора». Кода, который что-то вычисляет, в нём обычно нет вовсе — вычисляет класс.

PDO — договор о том, что лежит в кадре

Кадр EtherCAT — это байты без подписей. Устройство и мастер обязаны заранее условиться: первые два байта — управляющее слово, следующие четыре — целевая позиция, дальше режим работы. Такой договор называется PDO (process data object), а список «какой объект лежит на каком месте» — картой PDO, или маппингом.

Объекты у привода названы номерами из CiA 402 — профиля приводов, который EtherCAT унаследовал от CANopen. 0x6040 — управляющее слово, 0x6041 — слово состояния, 0x607a — целевая позиция, 0x6064 — фактическая. Тот же стандарт описывает режимы: csp — циклическая позиция, csv — скорость, cst — момент. Для фрезера обычно нужен csp на осях и csv на шпинделе.

Договор кто-то должен составить. Либо вы пишете его руками в XML, либо это делает драйвер устройства. Ровно об этом дальше.

Вашего привода в каталоге, скорее всего, нет

Каталог linuxcnc-ethercat на день замера: 272 устройства, девять вендоров, 209 карточек Beckhoff — 77 процентов списка. Полноценных семейств CiA 402-сервоприводов там четыре: Omron G5, Delta ASDA, Stöber и частично RTelligent. Остальное — терминалы ввода-вывода и шаговые драйверы.

Мы взяли пять вендоров, которых там нет: Inovance IS620N и SV660, Wecon VD3E, Mitsubishi MR-J4-20TM, Schneider LXM28E, плюс каплер Omron NX-ECC202. Все живы, все отвечают по мейлбоксу, драйвера нет ни у одного.

Что у них одинаково, а что различается

Это главный практический результат, и он же — ответ на вопрос «а что вообще придётся узнавать про мой привод». Стандарт покрывает меньше, чем ожидаешь.

Одинаково у всех пяти — на это можно опираться, не проверяя:

  • номера объектов CiA 402: 0x6040 управляющее слово, 0x6041 состояние, 0x607a целевая позиция, 0x6064 фактическая, 0x6060 режим;

  • лестница состояний INIT → PREOP → SAFEOP → OP и порядок её прохождения;

  • identity в 0x1018: vendor id, product code, revision читаются одинаково;

  • режим csp есть у всех — циклическая позиция, основной режим для оси станка.

Различается — и это придётся выяснять для каждого прибора:

что

как расходится

сколько записей влезает в карту PDO

VD3E — 10 на объект; MR-J4 приехал с 12 на выход и 14 на вход; у IS620N назначается ровно один TxPDO, а индекса 0x1A01 нет вовсе

отдаёт ли привод свой словарь по SDO-Info

Mitsubishi — 809 объектов, Wecon — 109, Inovance и Schneider — ноль, молчат

набор режимов

у Schneider нет csv и cst, у остальных четырёх есть

ответ на 0x6502 (какие режимы поддерживаются)

у трёх вендоров одинаковый, у Schneider свой

complete access при записи SDO

Wecon отвечает отказом, Mitsubishi на том же стенде принимает спокойно

разрешение энкодера через 0x608f / 0x6092

у VD3E этих объектов нет: «object does not exist»; число достаётся вендорным объектом, о котором знает только завод

доступность ESI

Wecon — открыто на сайте, Inovance — только через регистрацию на портале

параметры Distributed Clocks

assignActivate у каждого свой, из ESI; угадать нельзя

И одинаково плохо у всех пяти: таблицы кодов ошибок в машинном виде не публикует никто. Оператор у станка видит 0x603F = 0x7305 вместо строки «обрыв фазы энкодера». Один вендор из пяти прислал такую таблицу по прямому запросу — об этом ниже, в разделе про ответ завода; но именно по запросу, а не с сайта.

Отсюда и берётся работа: стандарт даёт общий язык, а всё, что нужно драйверу сверх него, добывается прибором наружу.

На чём всё это проверялось

Чтобы числа можно было сравнить со своими:

версия

ОС

Debian 12 (bookworm)

ядро

6.1.0-51-rt-amd64 (PREEMPT_RT)

LinuxCNC

2.9.10, uspace

мастер EtherCAT

IgH 1.6.10 (1.6.10.gc4b4ac4)

linuxcnc-ethercat

1.42.2

параметры RT

isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3 irqaffinity=0,1, 4 ядра гостя

период servo-thread

1 мс (appTimePeriod=1000000)

Прошивки приборов важны: поведение привязано к ревизии, и если у вас те же приводы с другой — числа могут разойтись. Полный список ревизий вынесен в реестр устройств, чтобы не занимать здесь половину экрана.

Как поставить весь стек с нуля

Если EtherCAT вы ещё не заводили, до драйверов нужно пройти три шага. Ниже — то, что делает наш инсталлер на чистой Debian 12; ничего своего в этих командах нет.

1. RT-ядро и LinuxCNC. Репозиторий проекта плюс ядро с PREEMPT_RT:

gpg --no-default-keyring --keyring /usr/share/keyrings/linuxcnc-archive-keyring.gpg \
    --keyserver hkps://keyserver.ubuntu.com --recv-keys 3CB9FD148F374FEF
echo "deb [signed-by=/usr/share/keyrings/linuxcnc-archive-keyring.gpg] \
https://www.linuxcnc.org bookworm base 2.9-uspace" \
    > /etc/apt/sources.list.d/linuxcnc.list
apt-get update
apt-get install -y linux-image-rt-amd64 linux-headers-rt-amd64 linuxcnc-uspace

2. Мастер IgH. Своих пакетов в Debian у него нет, берётся из репозитория science:EtherLab на OpenSUSE Build Service:

curl -fsSL http://download.opensuse.org/repositories/science:/EtherLab/Debian_12/Release.key \
    | gpg --dearmor -o /usr/share/keyrings/etherlab-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/etherlab-archive-keyring.gpg] \
http://download.opensuse.org/repositories/science:/EtherLab/Debian_12 ./" \
    > /etc/apt/sources.list.d/etherlab.list
apt-get update
apt-get install -y ethercat-master ethercat-dkms linuxcnc-ethercat

Грабли на ровном месте. ethercat-dkms собирает модуль под работающее ядро, а не под то, которое вы только что поставили. Без заголовков текущего ядра постинсталл падает, пакеты остаются недонастроенными, и после этого любой следующий apt-get install завершается ошибкой dpkg. Ставьте linux-headers-$(uname -r) до всего остального, а после перезагрузки в RT-ядро — dpkg-reconfigure ethercat-dkms.

3. Указать мастеру сетевую карту — в /etc/ethercat.conf, по MAC-адресу:

MASTER0_DEVICE="00:1b:21:xx:xx:xx"
DEVICE_MODULES="generic"

generic означает «через обычный сетевой драйвер ядра». Есть и оптимизированные драйверы под конкретные чипы, но начинать стоит с generic: он работает всегда, а разницу вы увидите только на цикле сильно ниже миллисекунды.

Карта должна быть отдельная и ни для чего другого: мастер забирает её целиком, IP на ней не живёт.

Проверка, что шина поднялась:

systemctl start ethercat
ethercat slaves

Каждое устройство отвечает строкой вида 0 0:0 PREOP + IS620N_ECAT_v2.6.8. PREOP на этом этапе — правильно: до OP слейвов доводит уже LinuxCNC с lcec.

Вторая ловушка. Мастер читает шину один раз при старте и не пересканирует её при появлении линка. Включили питание приводов после запуска службы — увидите Slaves: 0 навсегда, пока не сделаете systemctl restart ethercat. Мы на это ловились не раз: выглядит как мёртвая шина, лечится перезапуском службы.

Чем платишь, пока драйвера нет

Привод заводится через generic: карту PDO пишешь руками.

<slave idx="0" type="generic" vid="00100000" pid="000C0108" configPdos="true">
  <syncManager idx="2" dir="out">
    <pdo idx="1600">
      <pdoEntry idx="6040" subIdx="00" bitLen="16" halPin="controlword" halType="u32"/>
      <pdoEntry idx="607A" subIdx="00" bitLen="32" halPin="target-position" halType="s32"/>
      … ещё три десятка строк …

Наш рабочий конфиг фрезера: 236 строк XML, 53 записи PDO вручную. Ошибся в одном индексе — слейв не доходит до OP, и лог об этом скажет мало.

Три способа завести привод, и когда свой драйвер не нужен

Способов ровно три, и выбрать стоит осознанно.

способ

что пишете вы

когда подходит

generic

карту PDO руками в XML, 40–60 строк на привод

больше нечем: устройство не CiA 402, экзотика, каплер с произвольным набором модулей

basic_cia402

одну строку <slave> плюс vid, pid и модпараметры

привод честного CiA 402, машина одна, конфиг пишется руками один раз

свой драйвер

20 строк C один раз, потом <slave type="VD3E"/>

приводов и машин много, конфиги генерятся, знание о приборе нужно сохранить

Про средний вариант надо сказать честно и раньше остального: в апстриме живёт basic_cia402 — универсальный драйвер для любого CiA 402-устройства. Он сам собирает маппинг и отдаёт те же единообразные пины srv-*. Три четверти экономии строк даёт именно он, а не наш код.

Один станок, привод честного CiA 402 — начинайте с него и на этом останавливайтесь. Писать C не придётся.

Зачем тогда свой драйвер

Всё дальнейшее — про случай, когда среднего варианта не хватает. Четыре причины, по возрастанию того, насколько больно без них.

1. Конфиг называет прибор именем, а не цифрами. type="VD3E" против type="basic_cia402" с выписанными руками vid и pid. У Inovance product code двух разных приводов различается одним байтом. Ошиблись — драйвер приложился к чужому приводу, и узнаете вы об этом по поведению, а не по сообщению.

2. Универсальный драйвер не знает, чего у прибора нет. У Schneider LXM28E нет csv и cst. Включите их — маппинг не соберётся, слейв встанет на PREOP, и в логе будет код сервисного обмена, а не «этого режима у привода нет». Вендорный драйвер режет такое на входе, потому что кто-то уже выяснил.

3. Знание о приборе перестаёт быть вашим личным. Лимит в десять записей на мапирующий объект, assignActivate = 0x300, отсутствующий 0x608f — всё это добыто экспериментом за вечер. В generic-конфиге оно останется в вашем XML на вашем компьютере. В драйвере — уезжает в апстрим, и следующему человеку с тем же приводом достаётся одна строка вместо вечера.

4. Конфиг становится генерируемым. Пока в XML лежит карта PDO, тот, кто собирает конфиги машин (у нас это делает модель станка), обязан знать вендорские тонкости каждого привода. Как только в XML один тег на прибор, генератор становится тупым, а вендорское знание живёт там, где ему место — в lcec. Для одной машины это неважно. Для десятка — это разница между «поддерживаем» и «не поддерживаем».

То есть вендорный драйвер убирает не столько XML, сколько необходимость выяснять про прибор то, что кто-то уже выяснил.

Мы объясняли себе эту простыню тем, что приводы китайские и драйверов для них никто не напишет. Объяснение неверное. Мешает другое — отсутствие ESI: машинное XML-описание устройства, которое вендор выкладывает (или не выкладывает). У Inovance он на 13.08.2026 доступен только через регистрацию на портале. У Wecon лежит открыто, в разделе загрузок на сайте, — и драйвер пишется за вечер.

Что нужно знать про привод

Возвращаемся к драйверу. Чтобы его написать, нужно знать про прибор пять вещей — и три из них в пользовательской документации отсутствуют: лимиты PDO, параметры DC и список того, чего у привода нет.

1. Identity

ethercat slaves -v
# Vendor Id 0x00000eff, Product code 0x0d3e0001, Revision 0x00000073

Две живые ловушки. У Wecon VD5 и VD5L делят один product code и различаются только ревизией. У Inovance наоборот: IS620N и SV660 делят vendor id и префикс 0x000c01, различаясь одним байтом, — сравнение по префиксу выдаёт один привод за другой. Мы на этом уже поскальзывались в собственной платформе.

2. Сколько PDO и сколько записей влезает

Что это за число и на что оно влияет. В каждом такте привод получает и отдаёт фиксированный набор величин. Управляющее слово и целевая позиция нужны всегда — это две записи из бюджета. Дальше начинается выбор: везти ли обратно фактический момент, ошибку слежения, код аварии, состояние входов. Каждая такая величина — ещё одна запись в карте, и лимит прибора говорит, сколько их влезет.

Мало записей — придётся решать, что важнее: видеть момент на шпинделе или код ошибки. Много — можно отдать в HAL всё сразу и не думать. Практически: пяти записей на выход и семи на вход хватает фрезеру с запасом, так что при лимите в десять выбирать не приходится, а при четырёх приходится.

Ограничений на самом деле два, и их путают:

  1. сколько PDO-объектов можно назначить (0x1c12 для выходов, 0x1c13 для входов) — у большинства приводов ровно один;

  2. сколько записей влезает в один такой объект (SubIndex 000 у 0x1600, 0x1701 и подобных) — у VD3E это десять.

Второе число мы сначала прочитали как «столько назначено сейчас» и допускали, что предел выше. Завод ответил, что десять — жёсткий лимит прошивки, а не значение по умолчанию: расширять маппинг некуда. Разница практическая — в первом случае имеет смысл пробовать одиннадцатую запись, во втором это гарантированный отказ на PREOP.

Как узнать. Из ESI, если он есть:

<Object><Index>#x1C12</Index>          <!-- RxPDO assign -->
  <SubItem><Name>SubIndex 000</Name><DefaultData>01</DefaultData>

01 — назначается ровно один RxPDO. В объекте маппинга SubIndex 000 = 0A, то есть до десяти записей.

Если ESI нет, те же два числа читаются прямо с шины, приводом наружу:

ethercat upload -p3 0x1c12 0x00 --type uint8   # сколько PDO можно назначить
ethercat upload -p3 0x1600 0x00 --type uint8   # сколько записей в карте
ethercat pdos -p3                              # что назначено прямо сейчас

Что будет, если ошибиться. Превысили лимит — привод отвечает ошибкой сервисного обмена на переходе PREOP → SAFEOP, и слейв не доходит до OP. В логе будет код обмена, а не «записей слишком много», поэтому число лучше знать заранее, чем искать его методом деления пополам.

Так мы сняли и Mitsubishi: у MR-J4 в RxPDO 12 записей, в TxPDO 14 — заметно больше остальных, потому что рядом со стандартными объектами он мапит свои: 0x2d01..0x2d03 Control DI, 0x2d11..0x2d13 Status DO, 0x2d20 предел скорости. Оговорка: это то, с чем привод приехал, а истинный максимум без ESI взять неоткуда — в драйвере стоят именно наблюдённые числа.

3. Distributed Clocks

<Dc><OpMode><Name>DC</Name><AssignActivate>#x300</AssignActivate>

Если ESI нет — значение не выдумывать. В драйверах Mitsubishi и Schneider мы сознательно не задали DC: пусть его назначает XML, это честнее угадывания.

Кроме assignActivate у DC есть второй параметр, про который в документации приводов молчат, — сдвиг sync0. Импульс SYNC0 говорит приводу «читай данные из своей памяти сейчас», и вопрос в том, в какой момент цикла он должен прийти относительно того, как мастер эти данные положил. Завод Wecon рекомендовал для VD3E половину периода — то есть при цикле 1 мс сдвиг 500 000 нс. Мы это проверили на своей шине 17 августа:

без сдвига

sync0Shift = 500 мкс

dc-sync-diff на старте

129 023 нс

через 10 секунд

ещё сходится

1 нс

время до единиц нс

~20 с

~10 с

установившееся значение

единицы нс

единицы нс

Что покупает сдвиг — сходимость на старте, а не более тугой захват в установившемся режиме: через полминуты оба варианта стоят в единицах наносекунд, phase-jitter ноль, приводы в OP. Сказать «стало точнее» было бы неправдой; правда — «раньше перестало быть неточным». Оговорка та же, что и везде в этом разделе: шина при замере стояла, под движением не повторяли.

4. Какие режимы привод умеет

Объект 0x6502, и вот тут был сюрприз.

привод

0x6502

что это значит

Inovance IS620N

0x3AD

pp, pv, tq, hm, csp, csv, cst

Wecon VD3E

0x3AD

то же самое

Mitsubishi MR-J4

0x3AD

то же самое

Schneider LXM28E

0xED

pp, pv, tq, hm, ip, csp — нет csv и нет cst

Три вендора из четырёх отвечают байт в байт одинаково — и после третьего совпадения рука тянется считать этот набор универсальным. Четвёртый показывает, что это не так: у Lexium нет ни синхронной скорости, ни синхронного момента, зато есть интерполяция, которой нет у остальных.

Поэтому в драйвере Schneider включён только csp. Попросить у привода режим, которого он не реализует, — это смапить объекты, которых нет.

5. Чего у привода НЕТ

Самый недооценённый пункт. У Wecon VD3E отсутствуют 0x608F (разрешение энкодера) и 0x6092 (feed constant) — на запрос приходит «object does not exist». Стандартными средствами CiA 402 разрешение с шины не читается вовсе; завод это позже подтвердил письмом и назвал свою замену — про неё чуть ниже.

Энкодер 23-битный, то есть 8 388 608 отсчётов по паспорту. Но паспорт лежит отдельно от прибора, а автоматике нужно число здесь и сейчас. Мы измерили его физически: сняли момент, поставили метку, провернули вал рукой на пять оборотов, посчитали приросты 0x6064.

0x6091 (электронный редуктор)

прирост за 5 оборотов

на оборот

отклонение от 2²³

1:1

42 203 264

8 440 653

+0,6%

2:1

20 787 053

4 157 411

−0,9%

Отношение 2,03 — объект делит позицию строго по стандарту, разрешение равно 8 388 608 импульсов на оборот. Шум покоя — ±7 отсчётов из 8,4 миллиона.

Обходной путь всё-таки есть, но он вендорный. Стандартного объекта нет, а свой у завода нашёлся: 0x201E:0x34 (в их системе параметров это U0-52) отдаёт число бит энкодера, из которого разрешение считается сдвигом. Прочитали — 23, то есть 2²³ = 8 388 608, сходится и с паспортом, и с рукой на валу:

ethercat upload -p3 0x201e 0x34 --type uint16   # 23 — бит энкодера
ethercat upload -p3 0x6091 0x01 --type uint32   # 1  — числитель редуктора
ethercat upload -p3 0x6091 0x02 --type uint32   # 1  — знаменатель

Тип здесь важен: 0x201E:0x34uint16, попытка прочитать его как uint32 падает с «type mismatch», и это выглядит как отсутствие объекта. А 0x6091 равный 1:1 означает, что 0x6064 отдаёт сырые отсчёты энкодера — вся миллиметровая шкала остаётся на вашей стороне.

Мораль общая, не про Wecon: если стандартного объекта нет, спросите завод про вендорный. Стандарт описывает, что должно быть у всех; вендорная область 0x2000..0x5FFF описывает, что есть у этого прибора, и туда обычно вынесено ровно то, чего вам не хватает.

Сюда же — мелочь, которая стоит часа отладки: VD3E отказывается принимать запись SDO с флагом complete access. Соседний Mitsubishi на той же шине его принимает спокойно. То есть это свойство прибора, а не шины, и проверяется одной командой:

ethercat download -p3 -c 0x1600 0x00 --type uint8 0   # у VD3E вернётся NAK

Сам драйвер целиком

Вот весь файл lcec_wecon.c, из которого убраны только лицензионная шапка и большой комментарий с фактами про прибор (те, что собирали выше). Осталось семьдесят строк, из них содержательных двадцать.

#include "../lcec.h"
#include "lcec_class_cia402.h"

static int lcec_wecon_init(int comp_id, lcec_slave_t *slave);

/* Модпараметры XML: у этого привода своих нет, но макрос ниже их ждёт. */
static const lcec_modparam_desc_t modparams_perchannel[] = {{NULL}};
static const lcec_modparam_desc_t modparams_base[] = {{NULL}};
static const lcec_modparam_doc_t chan_docs[] = {{NULL}};
static const lcec_modparam_doc_t base_docs[] = {{NULL}};

/* Шаг 1: чем этот драйвер опознаёт прибор на шине.
   "VD3E" — то, что вы напишете в XML как type=. */
static lcec_typelist_t types[] = {
    {"VD3E", LCEC_WECON_VID, 0x0d3e0001, 0, NULL, lcec_wecon_init, NULL, 0},
    {NULL},
};
ADD_TYPES_WITH_CIA402_MODPARAMS(types, 1, modparams_perchannel, modparams_base, chan_docs, base_docs)

static void lcec_wecon_read(lcec_slave_t *slave, long period);
static void lcec_wecon_write(lcec_slave_t *slave, long period);

typedef struct {
  lcec_class_cia402_channels_t *cia402;
} lcec_wecon_data_t;

static int lcec_wecon_init(int comp_id, lcec_slave_t *slave) {
  lcec_master_t *master = slave->master;
  lcec_wecon_data_t *hal_data;

  slave->proc_read = lcec_wecon_read;
  slave->proc_write = lcec_wecon_write;

  hal_data = LCEC_HAL_ALLOCATE(lcec_wecon_data_t);
  slave->hal_data = hal_data;

  /* Шаг 2: часы. assignActivate взят из ESI, режим DC.
     Если в XML задан <dcConf>, он победит — это значение по умолчанию. */
  if (slave->dc_conf == NULL) {
    lcec_slave_dc_t *dc = LCEC_HAL_ALLOCATE(lcec_slave_dc_t);
    dc->assignActivate = 0x300;
    dc->sync0Cycle = master->app_time_period;
    slave->dc_conf = dc;
  }

  lcec_class_cia402_options_t *options = lcec_cia402_options();

  /* Шаг 3: лимиты прибора — те самые два числа, снятые с шины. */
  options->rxpdolimit = 10;
  options->txpdolimit = 10;
  options->channels = 1;

  /* Шаг 4: что включаем в карту. Каждая строка = запись в PDO
     и пин в HAL. Считать против лимита выше. */
  options->channel[0]->enable_opmode = 1;
  options->channel[0]->enable_csp = 1;
  options->channel[0]->enable_csv = 1;
  options->channel[0]->enable_cst = 1;
  options->channel[0]->enable_target_torque = 1;
  options->channel[0]->enable_actual_torque = 1;
  options->channel[0]->enable_actual_following_error = 1;
  options->channel[0]->enable_error_code = 1;

  /* Шаг 5: дальше всё делает класс cia402 — собирает синк-менеджеры,
     раскладывает объекты по карте, заводит пины srv-*. */
  lcec_syncs_t *syncs = lcec_cia402_init_sync(slave, options);
  lcec_cia402_add_output_sync(slave, syncs, options);
  lcec_cia402_add_input_sync(slave, syncs, options);
  slave->sync_info = &syncs->syncs[0];

  hal_data->cia402 = lcec_cia402_allocate_channels(options->channels);
  hal_data->cia402->channels[0] = lcec_cia402_register_channel(slave, 0x6000, options->channel[0]);

  return 0;
}

/* Шаг 6: такт. Своей логики нет — перекладываем через класс. */
static void lcec_wecon_read(lcec_slave_t *slave, long period) {
  lcec_wecon_data_t *hal_data = (lcec_wecon_data_t *)slave->hal_data;
  if (!slave->state.operational) return;
  lcec_cia402_read_all(slave, hal_data->cia402);
}

static void lcec_wecon_write(lcec_slave_t *slave, long period) {
  lcec_wecon_data_t *hal_data = (lcec_wecon_data_t *)slave->hal_data;
  if (!slave->state.operational) return;
  lcec_cia402_write_all(slave, hal_data->cia402);
}

Файл целиком, с комментариями и лицензией: src/devices/lcec_wecon.c.

Читается он так. Первая половина — декларация: вот по каким цифрам узнать прибор, вот его часы, вот его лимиты, вот какие режимы включить. Вторая половина — две функции такта, и в них нет ничего своего: чтение и запись целиком отданы классу cia402 из апстрима. Он собирает маппинг, заводит пины, разворачивает 0x6502 в булевы srv-supports-mode-*. Ваша работа — только верхняя половина, и она целиком состоит из фактов, собранных на предыдущем шаге.

Чего в драйверах нет — touch probe. Объекты 0x60b8..0x60bd есть и у Wecon, и у Mitsubishi, у обоих 0x60b8 стоит в заводском RxPDO. Не включили сознательно: проверить их на железе было не на чем, а неверный маппинг стоит перехода в OP. Пустое место честнее непроверенной строки.

Порядок работы: от незнакомого прибора до OP

Если свести всё вышесказанное в последовательность, получается семь шагов. Они делаются ровно в этом порядке, потому что каждый следующий опирается на факт, добытый предыдущим.

  1. Поднять прибор на шине через generic или basic_cia402 и довести до OP. Пока прибор не дошёл до OP хоть как-нибудь, писать драйвер рано: вы не будете знать, чей отказ ловите — свой или прибора.

  2. Снять пять фактов — identity, лимиты PDO, параметры DC, 0x6502, список отсутствующих объектов. Четыре команды из конца статьи закрывают почти всё.

  3. Завести файл src/devices/lcec_<вендор>.c и объявить в нём типы: строка на устройство, с vendor id, product code и, если нужно, ревизией.

  4. Заполнить декларацию — DC по умолчанию, лимиты, включённые режимы и величины. Здесь же выключается то, чего у прибора нет: лишний enable_csv у привода без csv стоит перехода в OP.

  5. Собрать, проверить, что тип попал в модуль, и запустить на одном слейве. Один — принципиально: на пяти вы не поймёте, чей отказ.

  6. Написать страницу в documentation/ — пины, модпараметры, чего у прибора нет, на какой прошивке проверяли. Это не бюрократия: через полгода вы сами будете читать её как чужую.

  7. Заполнить TestingStatus честно. «Опознан на шине» и «крутили мотор» — разные вещи, и следующий человек принимает решение по этой строке.

Пункт про «не CiA 402» отдельной строкой: если прибор не сервопривод — каплер ввода-вывода, частотник со своим профилем, — класса под него может не быть вовсе, и тогда карточка устройства пишется руками: свои структуры пинов, свои функции такта, ручное описание синк-менеджеров. Это уже не двадцать строк, а несколько сотен, и решение писать такое надо принимать осознанно. Для CiA 402-привода — двадцать.

Как это собрать у себя

Файл кладётся в src/devices/, вендор дописывается строкой в src/lcec.h, регистрация — тем самым макросом ADD_TYPES_WITH_CIA402_MODPARAMS в конце файла типов. Отдельно в Makefile ничего вносить не надо: сборка забирает devices/*.c по маске.

sudo apt-get install -y build-essential pkg-config git libexpat1-dev \
    libethercat-dev linuxcnc-uspace-dev

git clone -b synctwin/bench https://github.com/SyncTwin/linuxcnc-ethercat.git
cd linuxcnc-ethercat
make configure && make build && make test
sudo make install          # LinuxCNC при этом должен быть остановлен

Проверка, что модуль действительно содержит ваш тип:

strings /usr/lib/linuxcnc/modules/lcec.so | grep -x VD3E

Класс cia402 есть только в свежем апстриме. Если у вас lcec из дистрибутива или старый форк sittner, пинов srv-* не появится вовсе, и вы потратите вечер, выясняя почему. Ориентир — ветка linuxcnc-ethercat версии 1.42 и новее.

Прогон

Сначала один слейв. Результат: OP, все пины живые. И сразу — ошибка в логе:

Failed to get reference clock time: Input/output error

Опорные часы живут на первом DC-способном слейве цепи. Если описать в конфиге только третий слейв, часам не на что опереться. Слейв в OP при этом выходит — DC просто не работает.

Дальше — весь конфиг фрезера, переведённый с generic на драйверы.

было

стало

ethercat-conf.xml

236 строк

83

записей PDO руками

53

10

Оставшиеся десять — каплер Omron: для него драйвера нет, и его карта зависит от набора модулей в стеке, так что тут ручное описание оправдано.

<slave idx="0" type="IS620N" name="x"/>
<slave idx="1" type="IS620N" name="y"/>
<slave idx="2" type="IS620N" name="z"/>
<slave idx="3" type="VD3E" name="s"/>

Результат:

0  0:0  OP  +  IS620N_ECAT_v2.6.8
1  0:1  OP  +  IS620N_ECAT_v2.6.8
2  0:2  OP  +  IS620N_ECAT_v2.6.8
3  0:3  OP  +  Wecon VD3E EtherCAT Servo v1.15
4  0:4  OP  +  NX-ECC202 EtherCAT coupler V1.2

lcec.0.all-op                 TRUE
lcec.0.x.srv-opmode-display   8      (CSP)
lcec.0.s.srv-opmode-display   9      (CSV, шпиндель)
lcec.0.dc-sync-diff           0x64 = 100 нс

Что сломалось по дороге

Тип пина меняется вместе с драйвером

Это стоило запуска и было самым неочевидным:

HAL: ERROR: type mismatch 'sampler.0.pin.3' <- 'x-drv-ferr'
./machine.hal:148: link failed

В generic мы сами объявляли following-error как s32. Класс cia402 экспортирует srv-actual-following-error как u32 — и HAL узнаёт об этом только в момент связывания, то есть при запуске. Лечится на приёмной стороне: каналы sampler переведены в u32.

Мораль: при переходе на драйвер сверяйте не только имена пинов, но и типы. halcmd show pin lcec показывает и то, и другое.

Имена пинов теперь называет драйвер

было

стало

lcec.0.0.cia-statusword

lcec.0.x.srv-cia-statusword

lcec.0.0.actual-position

lcec.0.x.srv-actual-position

lcec.0.0.following-error

lcec.0.x.srv-actual-following-error

Заодно меняется адресация: вместо номера слейва подставляется name из XML. Это лучше — переставили привод в цепи, имена пинов не поехали.

refClockSyncCycles решает, будут ли часы вообще

Замер на одной и той же шине — три IS620N, VD3E и каплер:

refClockSyncCycles="1"

refClockSyncCycles="-1"

часы сошлись

нет

да

dc-sync-diff через 40 с

1 175 551 нс

103 нс

phase-jitter

22 634

0

-1 означает, что мастер подстраивается под опорные часы шины, а не тянет их за собой. С положительным значением часы расходятся линейно и не сходятся никогда. Атрибут пишется в строке <master> того же XML, рядом с appTimePeriod.

Это, кстати, первый случай, когда сотню наносекунд внутри одной шины мы показываем собственным замером, а не ссылкой на литературу. Оговорка: шина при этом стояла — под движением осей замер не повторяли. И phase-jitter здесь в наносекундах; ровный ноль означает, что за окно наблюдения мастер не сдвинул фазу ни разу. Что пин при этом жив, видно по соседним счётчикам: они менялись.

apt поставил чужой пакет и отчитался успехом

Это ждёт любого, кто собирает свой пакет linuxcnc-ethercat и при этом держит подключённым репозиторий EtherLab из openSUSE — а его подключают почти все, там живёт сам IgH.

Мы собрали пакет версии 1.42.2-1+synctwin1, поставили, всё отчиталось успехом. Драйверов в модуле не оказалось: приехал снапшот 1.42.2.g7fffa62-0 из того самого стороннего репозитория, потому что dpkg ранжирует snapshot-суффикс выше обычной версии. Лечится Debian-эпохой (1: перед версией) — она старше любого снапшота.

Отсюда правило: «пакет установился» и «установился НАШ пакет» — разные факты. Проверять надо по содержимому:

strings /usr/lib/linuxcnc/modules/lcec.so | grep -x VD3E

Подменить lcec.so под работающей машиной нельзя

Очевидно, когда написано, и совершенно неочевидно в момент, когда вы просто пересобрали драйвер и хотите проверить правку. Модуль загружен в работающий процесс, установка кладёт файл поверх — а дальше поведение зависит от того, что именно успело прочитаться.

Две команды перед установкой:

pgrep -a linuxcncsvr                       # LinuxCNC работает?
ethercat master | grep -E "Phase|Active"   # мастер в Operation?

Если ответили — сначала остановить, потом ставить. У нас это знание год жило фразой в переписке, пока не превратилось в проверку перед установкой.

Чего мы так и не поняли

На малых оборотах VD3E мгновенная скорость гуляет: ±12% на 0,01 об/с и около ±5% на 0,2 об/с, с периодом порядка двух секунд. Считали по приростам счётчика положения, первую секунду разгона отбросив. Средняя скорость при этом верна с точностью 0,01%, так что это не масштаб и не шкала.

Что это — нормальное зубцовое усилие на малой скорости или следствие настроек контура? Написали производителю. Ответ пришёл: завод называет это нормальным явлением для VD3E и параметров тюнинга не предложил. То есть вопрос «почему» остаётся открытым — механическую природу нам не объяснили, — но практический вывод теперь есть: на ползучих подачах судить о таком приводе надо по средней скорости, мгновенная у него шумит штатно, и гоняться за этим шумом настройками контура бесполезно.

Заодно они закрыли ещё несколько мест этой статьи, и на это стоит посмотреть отдельно.

Что ответил завод Wecon

Это, пожалуй, самый неожиданный для нас результат всей работы, и он не про код.

Мы отправили производителю одного из приводов девять вопросов — те самые, что не закрывались ни документацией, ни шиной: лимиты PDO, минимальный цикл, сдвиг sync0, отсутствующие объекты, расшифровка 0x603F, температура, поведение на малых оборотах, привязка ESI к прошивке, машинный ESI на каплер. Через четыре дня ответил их технический инженер, поимённо, разобрав вопросы по одному.

Что из догадок стало фактом:

было в наших заметках

стало словом завода

«десять записей — похоже, столько назначено»

десять — жёсткий лимит прошивки, расширять некуда

«крутим шину на 1 мс, вероятно, можно быстрее»

1 мс — минимум, ниже стабильность не гарантируется

«sync0Shift не задаём, вроде и так работает»

сдвиг 50 % периода — рекомендация завода (замер выше)

«0x608F нет, берём из паспорта»

есть вендорный 0x201E:0x34 — число бит энкодера

«feed constant отсутствует»

замены нет вовсе, есть только редуктор 0x6091, по умолчанию 1:1

«температуру бы вывести оператору»

температуры нет ни в одном объекте — обещать мониторинг нельзя

«ripple на 0,01 об/с — гейны?»

нормальное явление, параметров тюнинга не предложили

«как декодировать 0x603F

значение в десятичном виде и есть номер кода Er.xx

И отдельно — таблица кодов ошибок: 66 строк, где у каждого кода есть класс, причина, признак «сбрасывается ли», требуемое действие и порядок устранения. Тот самый третий пункт из списка «чем вендоры отличаются», который, как мы написали выше, не делает почти никто. Прислали по прямому запросу, файлом, в первом же ответе. Публиковать её в открытом драйвере мы пока не можем — спросили разрешение и ждём; но в нашей панели оператор с этого дня видит Er.34 · перегрузка двигателя с причиной и действием вместо голого 0x22.

Слово завода мы всё равно проверили по шине, и это стоит делать всегда: тип 0x201E:0x34 оказался uint16, а не uint32, шкала напряжения звена постоянного тока в объекте не заявлена вовсе — 3157 при поданном силовом читается как 315,7 В только потому, что это похоже на выпрямленные 220 В, и в самом объекте эта шкала нигде не заявлена. Прежде чем показывать такие вольты оператору, их надо сверить мультиметром.

Вывод из всего этого простой и немного стыдный: девять вопросов, четыре дня и одно письмо закрыли больше, чем неделя экспериментов на стенде. Мы полгода исходили из того, что до инженеров китайского завода не достучаться, и не проверяли этого допущения. Оно оказалось ложным. Единственное, что нам понадобилось, — задать вопросы в виде, в котором на них можно ответить: номера объектов, а не «плохо крутится».

Что в итоге лежит открыто

Четыре драйвера, GPL v2, в нашей ветке апстрима — ссылки ведут прямо на файлы, каждый читается за пять минут:

драйвер

устройства

статус

lcec_inovance.c

IS620N, SV660

оба доведены до OP на железе; SV660 прокручен в csp на свободном валу

lcec_wecon.c

VD3E

доведён до OP на железе

lcec_mitsubishi.c

MR-J4 ™

опознан на шине, до OP не доводили

lcec_schneider.c

LXM28E

опознан на шине, до OP не доводили

Последние два написаны по данным с живой шины, но проверить их до конца нечем — на этих приводах нет моторов.

Что было бы, если бы драйверы были у всех приводов

Вопрос не риторический, потому что ровно так устроен мир Beckhoff внутри TwinCAT: там каталог полон, и никто никогда не видел карты PDO.

Сегодня запуск незнакомого привода в открытом стеке стоит вечера: снять пять фактов, написать XML, поймать отказ на PREOP, разобраться, повторить. Умножьте на число приводов в цеху и на число людей, которые делают это независимо друг от друга, каждый в своём файле.

Если бы карточки были на всё, изменилось бы три вещи.

Конфиг машины стал бы списком приборов. Не карта PDO, а перечисление: вот три оси, вот шпиндель, вот модуль ввода-вывода. Такой конфиг можно сгенерировать из модели станка, показать человеку на экране, проверить автоматически — с простынёй XML ничего этого не сделать.

Смена вендора перестала бы быть проектом. Пины srv-* одинаковы у всех драйверов класса cia402. Приехал другой привод — меняется одна строка type=, а .hal-файл станка остаётся прежним. Сейчас смена вендора означает переписать маппинг и заново выяснить, чего у нового прибора нет.

Знание перестало бы теряться. Всё, что мы выясняли экспериментом, у кого-то уже написано в блокноте — просто в чужом и недоступном. Карточка устройства в общем каталоге и есть форма, в которой это знание переживает конкретного интегратора.

Именно поэтому мы отправляем драйверы в апстрим, а не держим в своём форке. Ценность не в четырёх файлах — их можно написать за неделю. Ценность в том, чтобы следующий человек с тем же приводом их не писал.

Чем вендоры отличаются, если драйвер писать тебе

Пять приводов на одном столе дали побочный результат, которого мы не искали: стало видно, по каким признакам выбирать вендора, если поддержку в открытом стеке делать придётся самому. Факты — в таблице различий выше, здесь порядок по тому, во сколько работы обходится каждый пункт:

  1. ESI лежит на сайте открыто, без регистрации. Ноль затрат для завода, дни работы для интегратора, если файла нет.

  2. Словарь читается из самого прибора по SDO-Info. Прибор, который молчит, приходится описывать вручную и хранить это описание вечно.

  3. Таблица кодов ошибок в машинном виде. Самый дешёвый пункт списка и самый заметный для конечного оператора.

  4. Живой инженер, до которого можно дойти с техническим вопросом. Всё остальное про файлы, этот — про людей, и он же обычно решает, чей привод встанет в следующую машину.

Первые два — про то, что завод уже сделал или не сделал. Третий и четвёртый в 2026 году не делает почти никто, и именно в них разница между «привод, который заводится» и «привод, который выбирают».

Проверить это удалось буквально: третий и четвёртый пункты у одного из пяти вендоров оказались выполнимы — правда, только по запросу. Значит, дело не в том, что таблицу кодов невозможно отдать наружу или что инженера нельзя выделить. Дело в том, что об этом никто не просит.

Выводы

Работы в вендорном драйвере оказалось меньше, чем мы боялись: двадцать содержательных строк поверх готового класса. Барьер тут информационный, а не технический — надо знать пять вещей о приборе, и три из них приходится добывать экспериментом, потому что в документации их нет.

Но добывать экспериментом — не единственный путь, и это главная поправка, которую мы внесли в собственные представления по ходу. Из пяти нужных фактов два пришли письмом от завода за один заход, причём именно те, которые с шины не читаются в принципе: жёсткий лимит записей в карте и рекомендованный сдвиг sync0. Стенд отвечает на вопрос «что прибор делает». На вопрос «что у прибора можно и чего нельзя» отвечает тот, кто его спроектировал, — и оказалось, что он отвечает, если спросить номерами объектов, а не жалобой. Спрашивать всё равно надо с проверкой: половину присланного мы сверили по SDO в тот же день, и одна шкала при сверке оказалась выведенной нами по здравому смыслу: в объекте она не заявлена.

Куда важнее другое наблюдение. Мастер-стек в открытом мире есть, интерполятор мирового уровня есть, а между ними — человек с XML-файлом и невнятной SDO- ошибкой. Дефицит не в исполнителях, а в конфигураторе.

Пять приводов на одном столе показали, чего стоит слово «стандарт». Общий язык он задаёт: номера объектов, лестницу состояний, режимы. Всё, что нужно для запуска сверх этого, у каждого своё, и добывается прибором наружу. Общий провод и общее время шина даёт, а общее поведение — нет.

А что вы об этом думаете

Первое. У вас есть привод, которого нет в каталоге? Четыре команды снимают почти всё, что нужно драйверу:

ethercat slaves -v
ethercat pdos -p<N>
ethercat sdos -p<N> | wc -l
ethercat upload -p<N> 0x6502 0x00 --type uint32

Пришлите это и ESI, если он существует в машинном виде, — соберём драйвер и отправим наверх с указанием приславшего. Канал любой: issue в linuxcnc-ethercat-configs, issue прямо в апстриме или просто комментарий здесь.

Второе. Кто пишет карту PDO руками до сих пор, зная про basic_cia402, — и почему? Нам интересны случаи, где универсальный драйвер не подошёл: это ровно те места, где вендорная карточка обязана появиться.

Третье, спорное. Правильно ли, что конфигуратора шины в открытом мире нет как класса, — или это как раз та часть, которую каждый пишет под себя?

Четвёртое. Писали ли вы производителю привода технические вопросы — и отвечали ли вам? Это перевернуло представление о том, где брать недостающие факты. Если у вас так же или наоборот — расскажите, чей это был привод.

Ссылки

Комментарии (3)


  1. NutsUnderline
    21.08.2026 13:36

    я эпизодически пытаюсь понять "что ты такое" про ethercat, так что статья как бы не моего уровня, поэтому вопрос соответвует: и вот это все для того чтобы движками управлять, задумывалось гибко и универсально, а по факту - как обычно, закрытый клуб и напильник?


    1. synctwin Автор
      21.08.2026 13:36

      Для простого - из коробки: базовый профиль CiA 402 одинаков у всех, generic в конфиге и поехали. Напильник за пределами профиля: сколько данных привод пускает в обмен, что он про себя рассказывает, как синхронизируется - у каждой железки своё, и это разово прячется в драйвер устройства. Мы такие драйверы выкладываем открытыми, чтобы следующий с тем же приводом их не писал.


      1. NutsUnderline
        21.08.2026 13:36

        у каждой железки своё

        про то и речь