Есть на GitHub репозиторий dbraun1981/hal-cia402: один файл cia402.comp, папка example, 9 коммитов, 48 звёзд, лицензия GPL-2.0. При этом через него проходит заметная часть всех станков, где LinuxCNC разговаривает с сервоприводами по EtherCAT. Похожих компонентов практически нет: ни развитых форков, ни конкурентов, ни «переписал на Rust».

Когда мы начинали стенд, это казалось странным: настолько важный кусок — и один, маленький, почти не развивается. Неужели все пишут своё? Спустя месяцы наладки ответ другой, и он интереснее. Разберём весь стек — от сетевой карты до joint.0.motor-pos-cmd - на рабочих примерах, а по дороге вскроем то, чего нет ни в одном README.

Сразу о том, откуда у меня факты: я работаю в компании, которая делает платформу управления станками «из браузера до серво», и всё, что ниже, — с наладки нашего живого стенда: три сервопривода Inovance IS620N, шпиндель на приводе Wecon, LinuxCNC на промышленном боксе. Каждое важное число в статье помечено: [замерено] — проверили железом, [из паспорта] — из документации, [не знаем] — честно не знаем.

Зачем EtherCAT, если шаговики и так едут

Классическая связка LinuxCNC — step/dir через LPT или Mesa: контроллер шлёт импульсы, привод молчит. Команда идёт в одну сторону; что происходит с осью на самом деле — никто не знает.

EtherCAT — полевой Ethernet: кадр выходит из мастера, пробегает сквозь все слейвы «на лету» (каждый читает и пишет свои байты, не останавливая кадр) и возвращается. Отсюда цикл 1 кГц и жёстче на обычной сетевой карте, а distributed clocks синхронизируют оси с точностью до микросекунд [из паспорта].

Но главное не скорость, а обратная связь. Каждую миллисекунду позиция, скорость и момент каждой оси — просто числа в памяти. Из этого бесплатно получаются:

— осциллограф приводов — без осциллографа; — контроль отставания (following error) в реальном времени; — touch-off по моменту: опускаем ось малыми шагами и смотрим на момент — при касании он растёт. Щуп не нужен, момент и так приходит каждый такт.

Данные ходят двумя дорогами:

— PDO (process data) — циклический обмен, «горячие» данные каждый такт: позиции, statusword, момент; — SDO (service data) — почтовый ящик для параметров: медленно, по запросу, вне цикла.

Это разделение — ось всей дальнейшей истории.

EtherCAT ≠ Beckhoff

EtherCAT придумал Beckhoff — и на этом эксклюзив заканчивается. Протокол передан в ETG (EtherCAT Technology Group), спецификация открыта, мастер IgH — open source и вендор-нейтрален. Для стека из этой статьи не нужно ничего от Beckhoff: ни TwinCAT, ни их промышленных ПК, ни их клеммников. Обычный x86-бокс, обычная сетевая карта, Linux с PREEMPT_RT.

Наш стенд — тому иллюстрация: на нём нет ни одного устройства Beckhoff. Оси — сервоприводы Inovance IS620N (тот самый средний китайский ценовой сегмент), шпиндель — на приводе Wecon VD3E в скоростном режиме, дискретное IO — каплер Omron (E-STOP-петля, концевики). В хозяйстве заведены профили и под Schneider LXM28E с Mitsubishi MR-J4 — все они говорят один и тот же CiA 402, и весь стек статьи работает с каждым без единой строчки нового кода. Разница между вендорами начинается ровно там, где кончается профиль, — об этом вторая половина статьи.

Для тех, кто держит EtherCAT за «дорогую промышленную экзотику», это главная практическая новость: связка «обычный ПК + IgH + lcec + cia402.comp + недорогие приводы» собирается в работающий станок без единого проприетарного компонента.

Стек: четыре слоя

IgH EtherCAT Master   - модуль ядра, гоняет кадры; утилита `ethercat`
        ↓
lcec (linuxcnc-ethercat) - HAL-драйвер: PDO ↔ HAL-пины
        ↓
cia402.comp (hal-cia402) - машина состояний профиля, скейлы, homing
        ↓
motion LinuxCNC       - траектория

У lcec два способа описать слейв в ethercat-conf.xml:

  • type="generic" — маппинг PDO пишете руками, объект за объектом. Больше буков, но полный контроль и полное понимание;

  • type="basic_cia402" — драйвер сам маппит стандартные объекты профиля. Быстрый старт, меньше контроля.

Конвейер каждого такта servo-thread выглядит так:

lcec.read-all → cia402.N.read-all → motion → cia402.N.write-all → lcec.write-all

Порядок не случаен: сначала прочитать состояние железа, привести его к виду, понятному motion, дать motion посчитать, перевести команду обратно на язык привода, отправить. Перепутаете порядок addf — получите задержку в такт и фазовый сдвиг, который потом будете искать неделями в «странном поведении PID».

CiA 402 за пять минут

CiA 402 — профиль «CANopen for drives», который EtherCAT унаследовал через CoE (CANopen over EtherCAT). Его центральная идея: привод нельзя «включить единицей». Он проходит машину состояний:

Switch on disabled → Ready to switch on → Switched on → Operation enabled
                              (и отдельной веткой - Fault)

Управление - словом controlword (объект 0x6040), ответ — словом statusword (0x6041). Команды [из стандарта]:

Команда

controlword

Переход

Shutdown

0x0006

→ Ready to switch on

Switch on

0x0007

→ Switched on

Enable operation

0x000F

→ Operation enabled

Fault reset

0x0080 (фронт бита 7)

Fault → Switch on disabled

Ответы statusword (по маске 0x006F): Ready = 0x0021, Switched on = 0x0023, Operation enabled = 0x0027; по маске 0x004F: Fault = 0x0008, Switch on disabled = 0x0040 [из стандарта].

Режим работы - объект 0x6060 (и его зеркало 0x6061): 8 = CSP (циклическая синхронная позиция), 9 = CSV (циклическая скорость), 6 = внутренний homing привода.

Смысл CSP: LinuxCNC каждый такт шлёт целевую позицию 0x607A, а контуры тока/скорости/позиции замыкает сам привод. Планирование траектории — наверху, сервоконтуры — внизу, каждый занимается своим.

Вот эту машину состояний кто-то должен крутить. Либо вы пишете её руками из HAL-кирпичей (и ошибаетесь), либо берёте cia402.comp.

Классический пример: разбираем example/ построчно

Установка компонента — одна команда:

git clone https://github.com/dbraun1981/hal-cia402
cd hal-cia402
sudo halcompile --install cia402.comp

ethercat-conf.xml

В примере слейв объявлен как generic (vid/pid 0x26C/0x3C — привод автора; у вас будут свои), и все объекты замапплены руками:

Объект

Что это

HAL-пин

Направление

6040

controlword

cia-controlword

→ привод

6060

режим работы

opmode

→ привод

607A

целевая позиция

target-position

→ привод

60FF

целевая скорость

target-velocity

→ привод

6041

statusword

cia-statusword

← привод

6061

режим (факт)

opmode-display

← привод

6064

позиция (факт)

actual-position

← привод

606C

скорость (факт)

actual-velocity

← привод

6077

момент (факт)

actual-torque

← привод

Плюс distributed clocks: sync0Cycle="*1" — синхроимпульс каждый цикл.

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

cia402.hal

loadrt [KINS]KINEMATICS
loadrt [EMCMOT]EMCMOT servo_period_nsec=[EMCMOT]SERVO_PERIOD num_joints=[KINS]JOINTS
loadrt lcec
loadrt cia402 count=3
loadrt pid names=x-pid,y-pid,z-pid

addf lcec.read-all        servo-thread
addf cia402.0.read-all    servo-thread
addf cia402.1.read-all    servo-thread
addf cia402.2.read-all    servo-thread
addf motion-command-handler servo-thread
addf motion-controller    servo-thread
addf x-pid.do-pid-calcs   servo-thread
addf y-pid.do-pid-calcs   servo-thread
addf z-pid.do-pid-calcs   servo-thread
addf cia402.0.write-all   servo-thread
addf cia402.1.write-all   servo-thread
addf cia402.2.write-all   servo-thread
addf lcec.write-all       servo-thread

Связки для оси X (для Y/Z - то же с другими индексами):

# шина → компонент
net x-statusword      lcec.0.0.cia-statusword   => cia402.0.statusword
net x-opmode-display  lcec.0.0.opmode-display   => cia402.0.opmode-display
net x-drv-act-pos     lcec.0.0.actual-position  => cia402.0.drv-actual-position
net x-drv-act-velo    lcec.0.0.actual-velocity  => cia402.0.drv-actual-velocity

# компонент → шина
net x-controlword        cia402.0.controlword         => lcec.0.0.cia-controlword
net x-modes-of-operation cia402.0.opmode              => lcec.0.0.opmode
net x-drv-target-pos     cia402.0.drv-target-position => lcec.0.0.target-position
net x-drv-target-velo    cia402.0.drv-target-velocity => lcec.0.0.target-velocity

# motion ↔ компонент
net x-enable     <= joint.0.amp-enable-out => cia402.0.enable
net x-amp-fault  => joint.0.amp-fault-in   <= cia402.0.drv-fault
net x-pos-cmd    <= joint.0.motor-pos-cmd  => cia402.0.pos-cmd
net x-pos-fb     => joint.0.motor-pos-fb   <= cia402.0.pos-fb
net x-home-index <= joint.0.index-enable   => cia402.0.home

setp cia402.0.csp-mode 1
setp cia402.0.pos-scale 3600

Что тут происходит на самом деле:

  • joint.0.amp-enable-out => cia402.0.enable — это вся магия. LinuxCNC говорит «хочу мочь двигаться», а компонент сам проводит привод через Shutdown → Switch on → Enable operation, следя за statusword на каждом шаге. Вместо ручной логики битов — один пин. Ровно ради этой строки компонент и существует.

  • pos-scale — счётчиков привода на единицу станка. У автора 3600; ваша формула: counts_per_unit = (счётчик энкодера на оборот × передача) / шаг винта. Для 23-битного энкодера (8 388 608 counts/об [замерено] — про это ниже) и винта 5 мм без редуктора: 1 677 721.6 counts/мм.

  • cia402.0.homejoint.0.index-enable — homing по индексной метке делает сам привод, LinuxCNC только жмёт на курок через стандартный handshake index-enable.

INI для homing

HOME_SEARCH_VEL = 0.0
HOME_LATCH_VEL  = 0.2
HOME_USE_INDEX  = TRUE

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

halrun: пощупать привод без станка

Секрет, который экономит дни: не поднимайте весь LinuxCNC, пока не увидели привод живым в halrun.

Предохранитель: серво под питанием может поехать. Ось — без нагрузки и без инструмента, E-STOP — под рукой, руки — не в зоне хода.

Сначала - что вообще на шине:

$ ethercat slaves
0  0:0  PREOP  +  IS620N (COE)

$ ethercat pdos -p0            # фактический маппинг PDO
$ ethercat upload -p0 -t uint16 0x6041 0   # statusword руками

Теперь мини-стенд в halrun - только шина, без motion:

$ halrun
halcmd: loadusr -W lcec_conf ethercat-conf.xml
halcmd: loadrt lcec
halcmd: loadrt threads name1=servo-thread period1=1000000
halcmd: addf lcec.read-all servo-thread
halcmd: addf lcec.write-all servo-thread
halcmd: start
halcmd: show pin lcec.0.0.cia-statusword

И главный трюк — ручной проход машины состояний, голыми setp, без всякого компонента:

halcmd: setp lcec.0.0.opmode 8            # CSP
halcmd: setp lcec.0.0.cia-controlword 6   # Shutdown
halcmd: show pin lcec.0.0.cia-statusword  # ждём 0x0021 (по маске 0x006F)
halcmd: setp lcec.0.0.cia-controlword 7   # Switch on   → 0x0023
halcmd: setp lcec.0.0.cia-controlword 15  # Enable op   → 0x0027

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

Одна практическая тонкость [замерено на своей шкуре]: в CSP перед Enable operation целевая позиция должна равняться фактической. Иначе в момент включения привод прыгнет в накопленную цель — рывком, на полной динамике. cia402.comp делает это выравнивание сам; если ходите по машине состояний руками — сначала прочитайте actual-position и запишите её в target-position.

Как адаптировать пример под свой привод

1. Личность слейва. ethercat cstruct -p0 выдаёт фактические vid/pid и структуру объектов; ESI-файл вендора - то же в XML. vid/pid в вашем конфиге должны совпасть с живой шиной. И определяйте тип привода по vid/pid, а не по имени прошивки [замерено]: имена совпадают у разных устройств, и однажды у нас пинаут одного привода уверенно нарисовался в интерфейсе на совсем другом.

2. PDO под себя. Захотели добавить момент 0x6077, ошибку слежения 0x60F4 — святое дело. Но сначала выясните, сколько TxPDO разрешает ваш привод. Мы добавили второй PDO 0x1A01 «как у всех» — а у IS620N его не существует: привод принимает ровно один TxPDO из фиксированного набора (0x1A00 либо 0x1B01..0x1B04), это записано в объекте 0x1C13:01 [из паспорта, проверено замером]. Второй PDO — это SDO-ошибка на этапе конфигурации: слейв не доходит до OP, станок не едет. Наши тесты были зелёные — они проверяли наличие строк в XML, а не форму допустимого.

3. Масштабы. pos-scale — выше. Для скорости — отдельный масштаб, и он не обязан совпадать по структуре: у нас velo-scale = коэффициент скоростного контура × передача.

4. CSV вместо CSP. setp cia402.0.csp-mode 0 — и компонент работает по скорости. Нужно для шпинделя и для схем, где позиционный контур замыкает LinuxCNC через PID.

5. generic против basic_cia402. Начинайте с generic и ручного маппинга, как в примере: час рутины покупает понимание, которое окупится на первой же нестыковке. basic_cia402 хорош, когда приводов много и они одинаковые.

Чего не расскажет ни один README

Всё ниже — [замерено] на живых приводах, с датами в наших рабочих журналах.

1. «Стандартный» объект, который у каждого свой

Объект 0x60FD (digital inputs) — стандартный: биты концевиков, home-датчика, свободные DI. С середины июля у нас в нём горело 0x18000000 - биты 27 и 28, которые обе таблицы мануала помечают как NA. Три недели это было фоновой загадкой.

Разгадка: раскладка битов 60FD у IS620N зависит от параметра H0C-41 (SDO 0x200C:2A) — и фактическая карта смещена на +4 против таблицы мануала. Биты 27/28 — это концевики P-OT/N-OT, приехавшие не туда, где им положено быть по документации. Проверено переключением параметра: поставили 0 — регистр обнулился, 1 — тоже ноль, вернули 2 — биты 27/28 снова зажглись.

Мораль для generic-компонента: он не может знать даже, что означает бит в стандартном объекте. Это знание живёт только в паре «ваш привод + ваш параметр».

2. Прочитать параметр ≠ он действует

Электронный редуктор в CiA 402 — объект 0x6091. Он у IS620N есть, читается, пишется. А действующий редуктор живёт в вендорных параметрах H05-07/09, и 0x6091 может быть заполнен и мёртв — зависит от режима. Наша формула масштаба однажды почти затянула в конфиг редуктор, который не действует.

Разрешилось только физикой: приводы в ESTOP, крутим вал руками, смотрим 0x6063 (сырые counts) и 0x6064 (позиция). Оборот вала дал Δ6063 = 8 368 033 → энкодер 2²³ = 8 388 608 counts/об. Записали редуктор 2:1 — 6063 не дрогнул, 6064 поделился вдвое: значит 6064 отдаётся после редуктора. Побочный вывод: стена int32 наступает через 2³¹ / 2²³ = 256 оборотов — планируйте счёт заранее.

3. Записи по шине — навсегда

Параметр H0C-13 (SDO 0x200C:0E) управляет тем, сохраняются ли SDO-записи в EEPROM. На нашем стенде он оказался = 3: каждая запись по шине уходит в энергонезависимую память. «Примерить параметр и откатить перезагрузкой» — невозможно, пока не переключишь режим. У EEPROM ещё и ресурс записи конечен.

Бонус-ловушка — терминология: 200C-0E — это адрес, а не код параметра. Правило соответствия у Inovance: Hgg-pp ↔ 0x2gg:(pp+1). Пока мы называли адреса кодами, чуть не завели себе ложное «исключение из правила адресации».

4. Знаковые SDO

ethercat upload печатает значение сначала в hex. Наш парсер брал первый токен и делал int(tok, 0) — беззнаково. Момент -0.4% превращался в 6553%, маленькая отрицательная позиция — в 511 оборотов. На стоящем станке. Лечится two’s complement по битности типа:

_SIGNED_BITS = {"int8": 8, "int16": 16, "int32": 32}
if bits and v >= 1 << (bits - 1):
    v -= 1 << bits

Стыдно? Да. Но если у вас на неподвижной оси осциллограф показывает момент в тысячи процентов - вы теперь знаете, куда смотреть.

5. Отставание — это число, а не «дёргается»

Пока не начнёшь мерить, любое «станок дёргается» нерасчленимо: в нём слиты отставание, шум квантования и реальные рывки. Мы написали скрипт, который слушает телеметрию и ничего не двигает. Результат: following error пропорционален скорости и одинаков у всех осей — F3000 (50 мм/с) → 1.34 мм (p95), F750 → 0.34 мм. Это Kv ≈ 37 с⁻¹ — свойство П-регулятора позиционного контура, а не дефект. Знать своё Kv надо до того, как судить привод.

6. HAL, ссылающийся на мёртвый слейв, не падает

Самое коварное: если HAL адресует слейв, которого нет в XML (или нет на шине), LinuxCNC стартует «успешно». Просто слейв навсегда остаётся в PREOP, ось мертва, ошибки нет. Мы теперь сверяем тройку «HAL ↔ XML ↔ живая шина» автоматически до старта — после того как одна ось «не поехала» именно так.

Так почему же никто не пишет свой hal-cia402?

Теперь ответ очевиден, и он в трёх пунктах:

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

2. Всё остальное обобщить нельзя. Карта битов 60FD, число TxPDO, две шкалы редуктора, EEPROM-политика — это не «нюансы», это половина работы наладки, и она вендор-специфична. В компонент её не положишь: она живёт в вашем XML, ваших setp и — если вы себе не враг — в ваших автотестах.

3. Некому. Пересечение множеств «пользуется LinuxCNC», «дошёл до EtherCAT» и «пишет тексты» — исчезающе мало. 9 коммитов и 48 звёзд — это, по сути, весь публичный корпус знания по теме.

Компонент не «заброшен». Он ровно того размера, какого должен быть.

Когда и этого мало: путь мимо всех слоёв

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

Мы для этого держим отдельный, подвальный путь: джог прямо по пинам lcec из скрипта — без cia402.comp и без LinuxCNC вообще. Машину состояний при этом проходим сами (ровно та последовательность 6 → 7 → 15, что в разделе про halrun, плюс выравнивание цели). После наладки те же оси живут через нормальный стек - подвал остаётся для подвала.

Чего мы до сих пор не знаем

  • Куда легли физические DI при H0C-41 = 2 — ожидаем биты 24-26 (мануал говорит 20-22), но подтверждения перемычкой ещё нет [не знаем].

  • Потолок оборотов шпинделя по шильдику — в конфиге стоит замеренное значение, документального подтверждения нет [не знаем].

Это нормальное состояние: половина наладки — планомерное превращение «не знаем» в «замерено». Опасно не «не знаю», опасно «наверное так» — за эти месяцы каждое «наверное» обошлось дороже честного пробела.

Вместо заключения

Мы пришли к этому стенду за продуктом, а нашли ничью землю: между красивым CAM сверху и профилем CiA 402 снизу лежит слой, который никем толком не описан — его каждый проходит заново, теряя одни и те же недели на одних и тех же граблях. Именно на этих неделях наладки мы и решили, что компанию стоит строить здесь: наша платформа генерирует из цифрового двойника станка и G-code, и вот эти самые .hal/ethercat-conf.xml из статьи — чтобы слой, о котором вы только что прочитали, собирался автоматически, а не руками в три ночи.

Если тема зайдёт — следующей напишу разбор «принял ≠ сделал»: почему подтверждение команды в станочных (и не только) системах ничего не значит, и как мы научились не верить ack’ам. Вопросы про EtherCAT/CiA 402 — в комментарии, отвечу с замерами.