Белобородов А.В., Зорин А.Г., Семашко С.В., Харьюзов П.Р.
Введение
Приводятся инструкции, которые помогут сделать первые шаги в экспериментах с контроллером в режиме драйвера шагового двигателя SMSD–4.2CAN, выпускаемого под маркой НПО «Электропривод» [1]. Это настройка контроллера в режиме драйвера и запуск шагового двигателя с помощью программы CANopen Builder Limited v1.0 [2], запуск двигателя из консоли, программ, написанных на языках С++ и Java, в которых потребуется библиотека CHAI для преобразователя интерфейсов USB–CAN ГКМН.468351.017-03 КБ «Марафон» [3].
Первые две части представляют собой расшифровку лекций, прочитанных Белобородовым А.В. и Харьюзовым П.Р. в 2024–2025 годах.
И так перед нами лежат четыре купленных контроллера SMSD–4.2CAN, четыре новых шаговых двигателя ШД8682-4.5А [4] и снятый с банкомата двигатель Minebea-Matsushita 49-207984-000A / 23KM-C051-09V, который будет использован для опытов. Будем использовать источник постоянного тока на 24 вольта с мощностью в 15Вт от RealLab [5]. К контроллеру шагового двигателя приложен добротный pdf–файл с подробным описанием команд [6], но главы «Быстрый старт» в заводском описании нет. Заполнил этот важен пробел.
Содержание:
Раздел I: Подключение через заводскую программу «CANopen Builder»
Раздел II: Команды приложения «CANMon» из строки терминала
Раздел III: Библиотека CHAI в коде С++
Раздел IV: Доступ к библиотеке CHAI из кода Java
Ссылки
Раздел I: Подключение через заводскую программу «CANopen Builder»
Шаговые двигатели — это отдельный огромный раздел с нетривиальными расчетами не смотря на внешнюю компактность и внутреннюю лаконичность устройства. Для первого подключения достаточно знать, что шаговый двигатель может управляться импульсами и каждый импульс — это микрошаг двигателя. Текст ниже повторяет Приложение Б руководства [6] страницы 148–154 с акцентом на «быстрый запуск» шагового двигателя.
На странице контроллера [1] и в техническом описание [6] на страницах 115–116 приведены схемы подключения к контроллеру обмоток шагового двигателя и концевых выключателей. Для первого опыта достаточно подключить электрическую цепь шагового двигателя к контроллеру без какой-либо механики, присоединенной к валу двигателя. По желанию можно на валу сделать отметку изолентой или фломастером, чтобы видеть перемещение вала. Подключаем питание к котроллеру.
Кабелем microUSB надо соединить контроллер и компьютер.
Чтобы скачать заводскую программу «CANopen Builder» с официального сайта [2], надо написать в их службу поддержки, чтобы получить логин и пароль для скачивания. Наверно, с определенной попытки и нескольких писем вы это сможете сделать. Пароль и логин действительны в течении трех дней.
Свободно скачать программу «CANopen Builder» можно с англоязычный страницы сайта НПО «Электропривод» [7] .
После установки программы «CANopen Builder» надо зайти в папку c:\Program Files (x86)\Electroprivod\CANopen Builder Limited 1.0\driver\ и установить соответствующий драйвер контроллера. Время от времени драйвер слетает и его можно таким образом восстанавливать. Если не установить драйвер, программа «CANopen Builder» не будет видеть контроллер.
Переустановка драйвера понадобится, если в программе «CANopen Builder» отображается модель контроллера и не отображается его заводской номер. В целом связка программа–контроллер через usb–соединение работала устойчиво на двух Windows 11 Pro 22H2 и одной портативной системе.
Проверяем питание контроллера и его соединение кабелем с компьютером.
Открываем программу и подключаем контроллер:
Верхняя строка меню «Подключение» → «Список устройств». Выбираем контроллер. Или выбираем крайнюю левую пиктограмму.
После подключения начнет мигать условный светодиод под строкой меню. Если отображается модель контроллера без серийного номера, то подключить не получится. Тогда надо или переустановить драйверы как описано в пункте 5, или переподключить кабель контроллера к usb–порту.
Этот пункт и пункты до 14 включительно можно пропустить, если с шаговым двигателем не соединена никакая механика и будете подавать нагрузку на обмотки двигателя соизмеримую с его характеристиками. В пунктах 9–14 задаем данные для расчета и прописываем характеристики шагового двигателя в контроллер.
Меню «Конфигурация двигателя» → «BEMF compensation»:
В крайней левой колонке выбираем напряжения 24В, которое используем.
В нижней строке средней колонки выбираем модель двигателя точно или близко к тому, что используем.
Жмем «Рассчитать». В случае успеха появятся две кривые и будет заполнена крайняя правая колонка «Значения объектов».
В случае ошибки в левой колонке в характеристике «Ток постоянной скорости» уменьшаем характеристику «шагов за секунду».
Жмем «Записать».
Расчетные параметры из третьей колонки «Значения объектов» будут записаны, но без последующего сохранения при следующем включении будут потеряны. Как сохранить параметры в постоянной памяти устройства написано после пункта (22) части I. Доступ к параметрам можно получить через меню «Конфигурация двигателя» → «Конфигурация двигателя»:
В этом разделе параметры двигателя можно проверить и изменить вручную. Расшифровку параметров можно посмотреть в руководстве [6] страницы 148–154.
Открываем «Конфигурация драйвера» → «Основные операции»:
Щелчок правой кнопкой мыши («ПКЛ») на окне «Основные операции» → «Добавить объект»:
В открывшимся окне «Объектного словаря» выбираем объект c адресом 607Ah и названием «Target position / Заданное положение»:
В окне «Основные операции» появится объект с адресом 607Ah:
Установим значение нового положения «Target position» равное 10. Это число можно связать с шагами, угловым перемещением или единицами длины. Эта условная единица перемещения. Для первого запуска пропустим этот шаг. В конце будут даны ссылки на инструкции для расчетов.
Для установки значения «Target position» равным десяти левой кнопкой мыши («ЛКМ») выберем объект 607Ah и введем значение:
Значения можно вводить в десятичной или шестнадцатеричной системе, а также выделять двоичные биты. Жмем значок «микросхемы» и запишем новое значение. Закроем окно. Обратите внимание на объект 6064h «Текущее значение положения». Сейчас оно равно нулю. А значения объектов 6040h «Управляющее слово» и 6041h «Состояние» соответствуют значениям при включении контроллера. В дальнейшем их значения в состоянии покоя двигателя будут иметь другие значения.
Перейдем к объекту 6040h «Управляющее слово». и запустим перемещение шагового двигателя. Откроем левой кнопкой мыши объект 6040h и выберем биты с нулевого по четвертый. Под значениями битов есть расшифровка. Более детальное описание приводится в руководстве [6] на страницах 91–93.
Жмем на значок «Микросхема» и тем самым передаем управляющее слово в контроллер. Если все правильно подсоединено и нет ошибок, то шаговый двигатель придет в движение.
После достижения шаговым двигателем цели увидим следующие значения:
Объект 607Ah «Заданное значение цели» свое значение не поменял, но изменился объект 6064h «Текущее значение позиции» и его значение стало равным 10. «Управляющее слово» 6040h стало равным 15, а «Состояние» 6041h стало равным 1591. Расшифровку состояния можно найти или нажав левой кнопкой мыши на объект 6041h, или на страницах 94–96 руководства [6]:
Повторим операции (19) и (20). Получим следующие скрины:
Можно задавать частоту обновления параметров. Для этого правой кнопкой жмем на окно «Основные операции» → «Таймаут обновления» → <100ms | 500ms | 1000ms>:
На скрине окно «Основные операции» с объектом 60FDh «Цифровые входы». В этот объект можно вывести концевики или любые четыре сигнала, включая аварийную остановку. Более подробно можно посмотреть в руководстве [6] на страницах 85 и 115.
Наверно, для первого включения приведенных шагов достаточно. Ниже справочно приведены основные объекты и страницы руководства, которые потребуются для настройки контроллера в режиме драйвера для реальной механики и точных перемещений, а также для проверки работы программы управления по шине CAN.
1010h_01h = 0x65766173 → Сохранить все параметры в постоянной памяти устройства.
1011h_01h = 0x64616F6C → Загрузить заводские настройки. Требуется перезагрузка.|
6040h_00h = 0x4000 → Перезагрузка. Это бит №14. Руководство [6] , страницы 91–93.
6040h_00h = 0x80 → Сброс ошибки. Это бит №7. Руководство [6] , страницы 91–93.
607Ah → «Заданное значение цели»
6064h → «Текущее значение позиции»
60FDh → «Концевики»
Руководство [6] , раздел 5.1.3, страницы 91–93:
6040h → «Управляющее слово»
Руководство [6] , раздел 5.1.4, страницы 94–96:
6041h → «Состояние»
Руководство [6] , страницы 12 и 56:
2045h → Скорость CAN–шины.
Параметры двигателя. Руководство [6] , раздел 4.7, страницы 65–83.
Меню «Конфигурация двигателя» → «BEMF compensation»:
6510h_0Ah → VM_KVAL_HOLD
6510h_0Bh → VM_KVAL_RUN
6510h_0Ch → VM_KVAL_ACC
6510h_0Dh → VM_KVAL_DEC
6510h_0Eh → VM_INT_SPEED
6510h_0Fh → VM_ST_SLP
6510h_10h → VM_FN_SLP_ACC
6510h_11h → VM_FN_SLP_DEC
Настройка шагов:
6093h_01h → Position_factor_Numerator. Руководство [6], страница 42.
6093h_02h → Position_factor_Divisor. Руководство [6], страница 42.
6095h_01h → Velocity_factor_Numerator. Руководство [6], страница 45.
6095h_02h → Velocity_factor_Divisor. Руководство [6], страница 45.
6097h_01h → Acceleration_factor_Numerator. Руководство [6], страница 48.
6097h_02h → Acceleration_factor_Divisor. Руководство [6], страница 48.
Начальное положение, крайнее левое:
6098h_00h = 17 → Homing_method. Руководство [6], страница 100.
Смещение от начала координат:
607Ch_00h = 0 → Home_offset. Руководство [6], страница 99.
Перемещение в начальное положение:
6060h_00h = 6 → Modes_of_operation. Руководство [6], страница 97.
Доступ к объектам можно получить через окно «Основные операции» → правая кнопка мыши → «Добавить объект» → в окне «Объективный словарь» выбрать нужный объект, смотрите пункт 17 в этом разделе.
Раздел II: Команды приложения «CANMon» из строки терминала
В этой части рассмотрим управление двигателем из командной строки. Тестовыми командами из терминала будут: запрос состояния, установка цели и запуск перемещения.
Будем использовать конвертер CAN–USB от КБ «Марафон» [3], в режиме драйвера контроллер SMSD–4.2CAN от НПО «Электропривод» [1] , шаговый двигатель и стандартный терминал системы Windows 11 Pro 22H2.
Питание контроллера, подключение шагового двигателя и конвертера к контроллеру выполним согласно инструкциям руководства [6] страницы 115–116, страницы 9–12, руководства [8] страница 7. Чтобы официально скачать инструкцию по подключению конвертера интерфейсов тоже надо регистрироваться.
При помощи переключателей на лицевой панели контроллера SMSD–4.2CAN устанавливаем адрес равным 1: первый переключатель в режиме «On», остальные «Off». Подробности в руководстве [6] страница 11.
Для сборки «на столе» для короткой can–линия меньше ста метров терминальный резистор как правило можно отложить в сторону, но он лишним не будет. Переключатель терминального резистора находится на верхней панели контроллера, руководство [6] страница 7, положение 9.
Проверим скорость шины контроллера и, если надо, установим скорость равной 500 кбит/с или любую другу выбранную. Для этого подключим контроллер к компьютеру, смотрите пункты 1–7 раздела I, и заходим в программу «CANopen Builder» → в нем подключаем контроллер → Верхнее меню «Строка драйвера» → «CANOopen конфигурация» → «Other». Откроется окно «Другие параметры»:
В окне «Другие параметры» левой кнопкой мыши выберем объект 2045h «Скорость шины CAN» и установим значение 50, что соответствует скорости 500 кбит/с:
Доступ к регистру 2045h также можно получить через окно «Основные операции»: «Конфигурация драйвера» → «Основные операции» → правая кнопка мыши → «Добавить объект» → Выбрать в окне «Объектный словарь» объект 2045h.
Значения объекта 2045h и скорость передачи данных, руководство [6], страница 4:
2045h | Скорость передачи данных по CAN–шине, кбит/с |
---|---|
5 | 50 кбит/с |
12 | 125 кбит/с |
25 | 250 кбит/с |
50 | 500 кбит/с |
100 | 1000 кбит/с |
Сохраняем значение и переходим к конвертору интерфейсов can–usb.
Использовать будет конвертер ГКМН.468351.017-03 от КБ «Марафон [3]. Проверяем подключение согласно руководству [8], страница 7. Два выключателя терминальных резисторов расположены внутри корпуса конвертера. Работать «на столе» будем без них. Основной канала будет нулевой.
Кабелем «usb тип A– тип B» соединим конвертер с компьютером.
Заходим на интернет–страницу [9] и выбираем нужный драйвер. Далее все примеры будут для драйвера «CHAI 2.14.0 для ОС Windows (64-bit)».
Скачиваем и устанавливаем нужный драйвер. Происходит обычная установка драйвера для конвертера «usb–com».
Примечания:
а) Виртуальный com–порт специально не создается.
б) В папке «c:\Program Files (x86)\CHAI-2.14.0\doc\» лежат два pdf–файла. Один с описанием консольного приложения «canmon», второй с описанием библиотеки CHAI.
в) Исходник «canmon» лежит в папке «c:\Program Files (x86)\CHAI-2.14.0\src\».Открываем утилиту canmon: меню «Пуск» → «CHAI–2.14.0» → «canmon», «enter». Для удобства можно создать ярлык программы на рабочем столе.
Если конвертер [3] подключен, на экране появится консольное приложение, в котором попросят выбрать канал и скорость:
Здесь выбран канал «0» и скорость 500 кбит/с. Если конвертер [3] не подключен, то появится предложение проверить установку драйверов:
Считать состояние контроллера можно следующей командой: tr 0x601:0x40,0x41,0x60,0x00 sff, в которой всего два пробела. Один пробел после tr, а второй пробел стоит перед sff. Ответ: RX 0000001 SFF 00000581 8 DAT 4B 41 60 00 70 02 00 00 0011334347
Программа перешла в режим ожидания и будет принимать все сообщения от контроллера. Чтобы выйти из режима ожидания сообщений, надо нажать «Enter».
Разберем команду: tr 0x601:0x40,0x41,0x60,0x00 sff. В ней:
Фрагмент: | Пояснение: |
---|---|
tr | Команда отправки кадров в шину. Более подробно в руководстве [10] страница 15. |
0x601 | 0x600+адрес контроллера. В нашем случае адрес равен 1 и устанавливается переключателями на лицевой панели, пункт 1 или более подробно можно посмотреть в руководстве [6] страницы 7, 11. |
0x40 | Идентификатор задания чтения, руководство [6] страница 15. |
0x41,0x60,0x00 | Регистр 6041h_00h «StausWord», руководство [6] страницы 94–96. |
sff | Указание на то, что идентификатор кадра имеет длину 11 бит. |
Примечания:
а) Синтаксис консольной программы допускает только два пробела: после tr и перед sff.
б) Хвост регистра 6041h_00h нулевой, поэтому возможен лаконичный синтаксис:
tr 0x601:0x40,0x41,0x60 sff
Программа «canmom» допишет и отправит остальное байты равными нулю.
Ответ:
RX 0000002 SFF 00000581 8 DAT 4B 41 60 00 70 02 00 00 0074518025

Разберем ответ контроллера:
RX 0000002 SFF 00000581 8 DAT 4B 41 60 00 70 02 00 00 0074518025
Фрагмент: | Пояснение: |
---|---|
RX | Показывает, что это ответ. |
0000002 | Номер принятого кадра. |
SFF | Идентификатор кадра равен 11. |
00000581 | 0x580+адрес контроллера. |
8 | Получено 8 байт. |
DAT | Метка данных. |
4B | Значимый ответ содержится в двух байтах, руководство [6] страница 15. |
41 60 00 | Регистр 6041h_00h «StatusWord». |
70 02 00 00 | Ответ надо расшифровывать в обратном порядке: 0x(00 00 02 70) = 624d. |
0074518025 | Метка времени. |
Выдержка идентификаторов чтения, записи и ошибок из руководства [6] страница 15:
Идентификатор | 8 битов | 16 битов | 32 бита |
---|---|---|---|
Идентификатор задания для чтения | 40h | 40h | 40h |
Идентификатор ответа на чтение | 4Fh | 4Bh | 43h |
Идентификатор задания для записи | 2Fh | 2Bh | 23h |
Идентификатор ответа успешной записи | 60h | 60h | 60h |
Идентификатор ответа ошибки | 80h | 80h | 80h |
Установим новую цель равной 10 в условных единицах. Для этого в регистр 607Ah_00h запишем значение 10. Идентификатор команды записи в этом случае равняется 23h, так как тип значения Integer32, то есть надо записать 32 бита или четыре байта.
Узнать тип данных регистра можно в руководстве [6] страницы 103, 104, 137, или в «Приложение А» руководства [6] страницы 118–140, или в программе «CANOpen Builder» → «Конфигурация драйвера» → «Объектный словарь».
В команде младшие байты значения регистра расположены слева. То есть значение цели равное 260d в команде будет выглядеть так: 0x04,0x01,0x00, а 10d так 0x0A,0x00,0x00.
Запишем значение 10d:
tr 0x601:0x23,0x7A,0x60,0x00,0x0A,0x00,0x00 sff
и получим ответ:
RX 0000003 SFF 00000581 8 DAT 60 7A 60 00 00 00 00 00 1569349521
Значение записалось! Об этом свидетельствует значение 60h после идентификатора ответа «RX 0000003 SFF 00000581 8 DAT», смотрите таблицу выше в пункте (12) или руководство [6] страница 15. Команда и ответ на нее представлен на скрине:
Запустим двигатель командой:
tr 0x601:0x2B,0x40,0x60,0x00,0x1F,0x00 sff
Идентификатор задания равен 2Bh и показывает, что надо записать два байта, где младший байт 1Fh, а справа от него расположен старший байт равный нулю. Значение 1Fh в двоичной системе — это первые пять бит:11111b, если будете использовать графическую программу «CANopen Builder», раздел I пункт (19).
Ответ:
RX 0000004 SFF 00000581 8 DAT 60 40 60 00 00 00 00 00 1642301077
Проверим, что цель достигнута. Запрашиваем состояние контроллера командой:
tr 0x601:0x40,0x41,0x60,0x00 sff
на который приходит следующий ответ:
RX 0000005 SFF 00000581 8 DAT 4B 41 60 00 37 06 00 00 2098098168
Значение 4Bh указывает на два байта полезной информации: 0x37 0x06, где слева расположен младший байт, а старший — справа. В двоичной системе 0637h равно 0110 0011 0111b. Здесь десятый бит равен 1, что означает, что «цель достигнута». Значения битов «StatusWord» приведены в руководстве [6] таблице 5.8 на странице 95 и в программе «CANopen Builder» → «Конфигурация драйвера» → «Основные операции» → «6041» или «Statusword».
Раздел III: Библиотека CHAI в коде С++
В этом разделе будут разобраны первые шаги в работе с библиотекой «C++» от производителя КБ «Марафон» [3, 8–11]. К самому драйверу CHAI [9] для переходника «usb–can» [3] приложено описание библиотеки и три исходника для консольных программ: canmon, chaitest и эхо–программа can-echowt.
Собираем контроллер SMSD–4.2CAN [2], шаговый двигатель, блок питания на 24В [5] и конвертер «CAN в USB» [3] придерживаясь рекомендаций в руководстве [6] и [8].
Регистрируемся на сайте КБ «Марафон» [9], скачиваем и устанавливаем пакет CHAI для Windows / 64 бита.
Структура папок установленного пакета CHAI:
c:\Program Files (x86)\CHAI-2.14.0\
Подпапка: | Содержание: |
---|---|
doc | папка с руководством по установке и с описанием библиотеки; |
ex | папка с кодом эхо–программы; |
include | папка с заголовочным файлом chai.h; |
inf | папка с драйверами устройства «can в usb»; |
inf64 | папка с драйверами устройства «can в usb»; |
src | папка с кодами для программ canmon и chaitest; |
x32 | папка с программами canmon и chaitest и библиотеками chai; |
x64 | папка с программами canmon и chaitest и библиотеками chai. |
Реализуем типичную последовательность вызовов, руководство [11] страница 13, на примере цепочки команд: инициализация — установка цели — команда движения — деактивация. Для этого возьмем проект программы canmon из пакета CHAI разработанный в среде MS Visual C++ 2017, руководство [11] страница 14, и уберем все лишнее.
Структура проекта будет следующей:
Пути к файлам chai.h, chai.dll и chai.lib в пакете CHAI:
c:\Program Files (x86)\CHAI-2.14.0\include\chai.h
c:\Program Files (x86)\CHAI-2.14.0\x64\chai.dll
c:\Program Files (x86)\CHAI-2.14.0\x64\chai.libПереработаем файл canmon.c из пакета драйверов [9], который находится в папке c:\Program Files (x86)\CHAI-2.14.0\src.
Переработанный код из файла canmon.c запишем в файл hello.cpp:
содержание файла hello.cpp
// gcc hello.cpp -o hello -I. -L. -lchai
// gcc src\hello.cpp -o target\hello -Iinclude -Lx64 -lchai
// target\hello.exe
#include <stdio.h>
#include <stdarg.h>
#include <time.h>
#include <chai.h>
#include <windows.h>
#include <process.h>
#include <time.h>
// Вспомогательная функция вывода сформированного кадра:
void print_tx(canmsg_t tx) {
fprintf(stdout,
"TR:\t 0x%02X, 0x%02X, 0x%02X, 0x%02X, 0x%02X, 0x%02X, 0x%02X, 0x%02X\n",
tx.data[0], tx.data[1], tx.data[2], tx.data[3],
tx.data[4], tx.data[5], tx.data[6], tx.data[7]
);
}
// Вспомогательная функция вывода ответного кадра:
void print_rx(canmsg_t* x) {
fprintf(stdout,
"Answer:\t 0x%02X, 0x%02X, 0x%02X, 0x%02X, 0x%02X, 0x%02X, 0x%02X, 0x%02X\n",
x->data[0], x->data[1], x->data[2], x->data[3],
x->data[4], x->data[5], x->data[6], x->data[7]
);
}
// Вспомогательная функция:
void a_to_b (_u8 a[8], _u8 b[8]) {
int i;
for (i = 0; i < 8; i++) {
b[i] = a[i];
}
}
// Функция передачи команды и приема ответа:
int tr(_u8 a[8]) {
// Переменная с результатом,
// страница 11 руководства или строка 83 файла chai.h:
_s16 result;
// Номер канала CAN,
// страница 74 руководства или строка 83 файла chai.h:
_u8 channel = 0;
// Формирование кадра,
// страницы 11 и 12 руководства:
canmsg_t tx, rxs[10];
// Адрес, длина кадра и флаг для кадра:
tx.id = 0x601;
tx.len = 0x08;
tx.flags = 0x00;
int i = 0, j, repeat=5, tr=0;
a_to_b(a, tx.data);
print_tx(tx);
do {
// Отправка кадров,
// страница 22 руководства:
result = CiTransmit(channel, &tx);
// // С паузой работает более устойчиво:
Sleep(100);
// Чтение ответа,
// страница 26 руководства:
result = CiRead(channel, rxs, 10);
// Поиск ответа без ошибки в считанных кадрах:
for (j = 0; j < result; j++) {
// Диагностика, если надо:
// print_rx(&rxs[j]);
if ( (rxs[j].data[0] == 0x60
|| rxs[j].data[0] == 0x4F
|| rxs[j].data[0] == 0x4B
|| rxs[j].data[0] == 0x43)
&& rxs[j].data[1] == tx.data[1]
&& rxs[j].data[2] == tx.data[2]
&& rxs[j].data[3] == tx.data[3])
{
i = 5;
tr = 1;
print_rx(&rxs[j]);
// fprintf(stdout, "Successfully!\n", channel, result);
}
else
{
// print_rx(&rxs[j]);
}
}
i++;
} while (i < 5);
return tr;
}
int main(int argc, char **argv)
{
// Переменная с результатом,
// страница 11 руководства или строка 83 файла chai.h:
_s16 result;
// Номер канала CAN,
// страница 74 руководства или строка 83 файла chai.h:
_u8 channel = 0;
// Настройки двигателя:
int i = 0;
_u8 statusword[8] = { 0x40, 0x41, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00 };
_u8 data_0x0A[8] = { 0x2F, 0x10, 0x65, 0x0A, 0x18, 0x00, 0x00, 0x00 };
_u8 data_0x0B[8] = { 0x2F, 0x10, 0x65, 0x0B, 0x44, 0x00, 0x00, 0x00 };
_u8 data_0x0C[8] = { 0x2F, 0x10, 0x65, 0x0C, 0x22, 0x00, 0x00, 0x00 };
_u8 data_0x0D[8] = { 0x2F, 0x10, 0x65, 0x0D, 0x22, 0x00, 0x00, 0x00 };
_u8 data_0x0E[8] = { 0x2B, 0x10, 0x65, 0x0E, 0x38, 0x04, 0x00, 0x00 };
_u8 data_0x0F[8] = { 0x2F, 0x10, 0x65, 0x0F, 0x44, 0x00, 0x00, 0x00 };
_u8 data_0x10[8] = { 0x2F, 0x10, 0x65, 0x10, 0xCB, 0x00, 0x00, 0x00 };
_u8 data_0x11[8] = { 0x2F, 0x10, 0x65, 0x11, 0xCB, 0x00, 0x00, 0x00 };
// Состояние контроллера:
_u8 data_status[8] = { 0x40, 0x41, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00 };
// Позиция:
//_u8 data_target[8] = { 0x23, 0x7A, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00 };
_u8 data_target[8] = { 0x23, 0x7A, 0x60, 0x00, 0x0A, 0x00, 0x00, 0x00 };
// Команда "Пуск":
_u8 data_start[8] = { 0x2B, 0x40, 0x60, 0x00, 0x1F, 0x00, 0x00, 0x00 };
// Инициализация библиотеки,
// страницы 9, 13 и 15 руководства:
result = 0;
result = CiInit();
fprintf(stdout, "CiInit()\t=> %i\n", result);
// Открываем канал,
// страница 16 руководства или строка 249 файла chai.h:
result = CiOpen(channel, CIO_CAN11);
fprintf(stdout, "CiOpen(...)\t=> %i\n", result);
// Устанавливаем скорость канала,
// страница 21 руководства или строка 219 файла chai.h:
result = CiSetBaud(channel, BCI_500K);
fprintf(stdout, "CiSetBaud(...)\t=> %i\n", result);
// Переводим канал в рабочее положение,
// страница 18 руководства:
result = CiStart(channel);
fprintf(stdout, "CiStart(%i)\t=> %i\n", channel, result);
fprintf(stdout, "\n");
// Передаем в контроллер настройки двигателя:
i = 0;
i += tr(data_0x0A);
i += tr(data_0x0B);
i += tr(data_0x0C);
i += tr(data_0x0D);
i += tr(data_0x0E);
i += tr(data_0x0F);
i += tr(data_0x10);
i += tr(data_0x11);
if (i == 8)
fprintf(stdout, "\nThe constants for the engine are written to the controller.\n\n");
else
fprintf(stdout, "\nThe constants for the engine are not written to the controller.\n\n");
// Состояние контроллера:
tr(data_status);
// Записываем в контроллер новую позицию шагового двигателя:
tr(data_target);
// Запускаем шаговый двигатель:
tr(data_start);
// Состояние контроллера:
tr(data_status);
// Состояние контроллера после остановки двигателя.
// При текущих настройках движение вала занимало меньше пяти секунд:
Sleep(5000);
tr(data_status);
/*
// Чтение серии кадров,
// востребованная функция в практической работе,
// страница 26 руководства:
canmsg_t tx, rx, rbuf[10];
result = CiRead(channel, rbuf, 10);
fprintf(stdout, "\nCiRead(channel, rbuf, 10) => %i\n", result);
for (i=0; i<result; i++)
print_rx(&rbuf[i]);
*/
// Останавливаем канал,
// страница 26 руководства:
fprintf(stdout, "\n");
result = CiStop(channel);
fprintf(stdout, "CiStop(%i)\t=> %i\n", channel, result);
// Закрываем канал,
// страница 17 руководства:
result = CiClose(channel);
fprintf(stdout, "CiClose(%i)\t=> %i\n", channel, result);
//system("pause");
return 0;
}
Команда компиляции [12]:
gcc src\hello.cpp -o target\hello -Iinclude -Lx64 -lchai
в случае успеха запишет рабочий файл target\hello.exe:
Если все файлы лежат в одной папке, то команда компиляции будет выглядит совсем лаконично: gcc hello.cpp -o hello -I. -L. -lchai
Командой target\hello.exe запустим двигатель:
Обратите внимание, что пока происходит движение вала двигателя нельзя будет задать новую цель и запустить новое перемещение.
Во время движения статус контроллера равен 1337h, а после остановки он становится равны 637h — это последние четыре строчки «TR–Answer»:
1337h = 0001 0011 0011 0111 / движение;
0637h = 0000 0110 0011 0111 / цель достигнута.
Структуру команда можно посмотреть, например, в пункте 12 раздела III.
Здесь восьмой бит в состоянии контроллера указывает на движение двигателя, а десятый бит показывает достигнута ли поставленная цель. Более подробно прочитать о значения битов можно в руководстве [6] на страницах 94–96 или в программе CANbuilder [2, 7] → Меню «Конфигурация драйвера» → «Основные операции» → Регистр 6041h «Statusword».
Раздел IV: Доступ к библиотеке CHAI из кода Java
В этом разделе рассмотрит классы Java [13] для работы с драйвером [1] шагового двигателя через переходник «can—usb» [3]. Выбор инструментов Java был обусловлен требованием управления шаговыми двигателями мишенной станции с сервера посредством удаленного рабочего стола.
Для подключения библиотеки CHAI [9, 10 и 11] нам надо будет классы Си и одну структуру сопоставить с типами и классами Java. С этой целью будем использовать библиотеку JNA [14–25].
Библиотека CHAI для работы с устройством «can–usb» использует следующие типы, руководство [11] страница 11:
Обозначение: | |
---|---|
_u8 | беззнаковое целое длины 8 бит (1 байт); |
_s8 | знаковое целое длины 8 бит (1 байт); |
_u16 | беззнаковое целое длины 16 бит (2 байта); |
_s16 | знаковое целое длины 16 бит (2 байта); |
_u32 | беззнаковое целое длины 32 бит (4 байта); |
_s32 | знаковое целое длины 32 бит (4 байта). |
Прототип структуры Си для представления CAN-кадра, руководство [11] страница 12:
typedef struct {
_u32 id; /* идентификатор кадра */
_u8 data[8]; /* данные */
_u8 len; /* фактическая длина поля данных, от 0 до 8 байт */
_u16 flags; /* bit 0 - RTR, bit 2 – EFF */
_u32 ts; /* отметка времени получения (timestamp) в микросекундах */
} canmsg_t;
В строках 53–70 файла chai.h находятся определения для архитектуры x64:
Файл chai.h:
#ifndef _u8
#define _u8 unsigned char
#endif
#ifndef _s8
#define _s8 char
#endif
#ifndef _u16
#define _u16 unsigned short
#endif
#ifndef _s16
#define _s16 short
#endif
#ifndef _u32
#define _u32 unsigned int
#endif
#ifndef _s32
#define _s32 int
#endif
Из библиотеки CHAI нам понадобятся четыре типа переменных:
— Тип _u8 занимает 8 бит и не имеет знака. Принимает значения от 0 до 28-1=255.
— Тип _u16 занимает 16 бит и не имеет знака. Принимает значения от 0 до 216-1=65535.
— Тип _u32 занимает 32 бит и не имеет знака. Принимает значения от 0 до 232-1.
— Тип _s16 занимает 16 бит со знаком. Принимает значения от -215 до 215-1.
В Java восемь базовых типов переменных и есть библиотека JNA сопоставления переменных Си и Ява [14, 15, 16, 23, 24, 25]:
_u8 → com.sun.jna.IntegerType;
_u16 → char;
_u32 → int;
_s16 → short.
Те функции из библиотеки CHAI, что будут разобраны используют тип _u16 только в структуре canmsg_t, где переменная данного типа принимает значения от 0 до 2. Поэтому типу _u16 здесь можно сопоставить шестнадцатеричный битовый базовый тип short или char. Также, где значения восьмеричных битовых переменных не превышают допустимых числовых значений, можно сопоставить типу _u8 или знаковый тип byte, или обертку com.sun.jna.IntegerType. В случае неправильного сопоставления через библиотеку JNA возвращаемые значения переменных могут содержать часть указателя адреса на значение используемой переменной.
Перейдем к созданию класса для взаимодействия с контроллером шагового двигателя:
Командой:
mvn archetype:generate -D groupId=CANBus -D artifactId=CANBus -D interactiveMode=false
генерируется пустой проект maven.
Переписываем файл CANBus/pom.xml следующим образом:
Файл pom.xml:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>CANBus</groupId>
<artifactId>CANBus</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>22</maven.compiler.source>
<maven.compiler.target>22</maven.compiler.target>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/net.java.dev.jna/jna -->
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>5.15.0</version>
</dependency>
</dependencies>
</project>
В файле pom.xml указали, что текст кода в кодировке UTF–8, версия JDK равна двадцати двум и подключили пакет net.java.dev.jna.
Идем в папку проекта src\main\java\CANBus\ и запишем в нее пять файлов, первый из которых будет класс U8.java, который является оберткой беззнакового восьмибитового числа от 0 до 28-1=255, руководство [11] страница 11:
Файл U8.java:
package CANBus;
import com.sun.jna.IntegerType;
/**
Обертка для беззнакового целого длинной восемь бит из библиотеки CHAI для класса {@link CANBus.CANmsg_t} и использования в коде Java.
@see
<a href =
"https://stackoverflow.com/questions/33544028/how-should-i-pass-and-return-unsigned-int-by-value-from-java-to-c-c-in-jna">
StackOverflow Forum
</a>,
<a href = "http://java-native-access.github.io/jna/4.2.1/">
JNA API Documentation
</a>,
<a href = "https://www.javatips.net/api/jna-master/src/com/sun/jna/IntegerType.java">
com.sun.jna.IntegerType
</a>
*/
public class U8 extends IntegerType {
public U8() {
super(1, true);
}
}
Следующим файлом будет обертка структура Си canmg_t, которая используется в библиотеке CHAI, руководство [11], страница 12:
Файл CABmsg_t.java:
package CANBus;
import com.sun.jna.Structure;
import com.sun.jna.Structure.FieldOrder;
/**
Обертка структуры Cи canmsg_t из библиотеки CHAI.
<p>Структура данных Си:
<pre>
typedef struct {
_u32 id;
_u8 data[8];
_u8 len;
_u16 flags;
_u32 ts;
} canmsg_t;
</pre>
@see
<a href = "https://habr.com/ru/companies/jugru/articles/524342/">
В нативный код из уютного мира Java: путешествие туда и обратно (часть 2)
</a>
@see
<a href="http://java-native-access.github.io/jna/4.2.1/">
JNA API Documentation
</a>
*/
@FieldOrder({ "id", "data", "len", "flags", "ts"})
public class CANmsg_t extends Structure {
/** Адрес устройства CAN. */
public int id;
/** Восемь байт передаваемой или получаемой полезной информации. */
public U8[] data={
new U8(), new U8(), new U8(), new U8(),
new U8(), new U8(), new U8(), new U8()
};
/** Число бит информации. */
public U8 len=new U8();
/** Индентификатор CAN–кадра в 11 или 29 бит.*/
public char flags;
/** Метка времени. */
public int ts;
}
В папку src\main\resources\ записываем файлы: chai.h, chai.dll и chai.lib.
В классе CANchai.java подключаем непосредственно саму библиотеку CHAI, [15–18] с сигнатурами функций Java:
Файл CANchai.java:
package CANBus;
import com.sun.jna.*;
/**
Заключительный шаг для подключения библиотеки CHAI инструментом JNA.
После чего для использования внешней библиотеки в основном коде достаточно будет прописать, например, такую строку:<br>
private static CANchai chaiLibrary = CANchai.INSTANCE;
*/
public interface CANchai extends Library {
// Папка c chai.h, chai.dll и chai.lib: src/main/resources/
CANchai INSTANCE = (CANchai) Native.loadLibrary("chai", CANchai.class);
public short CiInit();
public short CiOpen(U8 channel, U8 flags);
public short CiSetBaud(U8 channel, U8 bt0, U8 bt1);
public short CiStart(U8 channel);
public short CiTransmit(U8 channel, CANmsg_t[] tx1);
public short CiRead(U8 channel, CANmsg_t[] rxs, short n);
public short CiStop(U8 channel);
public short CiClose(U8 channel);
}
Класс CANminmum.java выдержка из класса CANbus.java для наглядной демонстрации запуска шагового двигателя из программы, написанной на Ява с использованием библиотек Си CHAI [9] и пакета JNA [15, 24]:
Файл CANminmum.java:
package CANBus;
import com.sun.jna.IntegerType;
import java.util.TreeMap;
/**
Минимальный набор команд из класса {@link CANBus.CANbus}. <br>
mvn compile <br>
mvn exec:java -D exec.mainClass=CANBus.CANminimum <br>
mvn exec:java -D exec.mainClass=CANBus.CANminimum -D exec.args="10 5000" <br>
mvn exec:java -D exec.mainClass=CANBus.CANminimum -D exec.args="положение пауза"
<p>Пауза — время между началом движения и командой на возврат в начальное положение.
Если пауза меньше времени движения, то возврата вала в первоначальное положение не поизойдет.
*/
public class CANminimum {
private static CANchai chaiLibrary = CANchai.INSTANCE;
private static U8 channel=new U8();
// Ошибки инициализации и открытия:
private static short busError=0;
private static short canError=0;
// Формирование указателя на переменную с данными:
private static short messageSize=10;
private static CANmsg_t receivedMessage_0=new CANmsg_t();
private static CANmsg_t[] receivedMessage=(CANmsg_t[]) receivedMessage_0.toArray(messageSize);
private static short[] driverTarget = { 0x23, 0x7A, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00 };
private static short[] driverStart = { 0x2B, 0x40, 0x60, 0x00, 0x1F, 0x00, 0x00, 0x00 };
private static short[] driverReset = { 0x2B, 0x40, 0x60, 0x00, 0x00, 0x40, 0x00, 0x00 };
private static short[] errorReset = { 0x2B, 0x40, 0x60, 0x00, 0x80, 0x00, 0x00, 0x00 };
private static short[] statusWord = { 0x40, 0x41, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00 };
private static short[] currentPosition = { 0x40, 0x64, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00 };
private static short[] limitSwitch = { 0x40, 0xFD, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00 };
private static short[] quickStop = { 0x2B, 0x40, 0x60, 0x00, 0x06, 0x00, 0x00, 0x00 };
private static short[] setPositionMode = { 0x2F, 0x60, 0x60, 0x00, 0x01, 0x00, 0x00, 0x00 };
private static short[] setHomingMode = { 0x2F, 0x60, 0x60, 0x00, 0x06, 0x00, 0x00, 0x00 };
private static int dt_quick =50;
private static int dt_middle =200;
/**
Инициализация и открытия канала с номером ноль и скоростью 500 Мбит/с.
*/
public CANminimum() {
open(0);
}
/**
Инициализация и открытия канала со скоростью 500 Мбит/с.
@param openChannel Номер канала, ноль или один.
*/
public CANminimum(int openChannel) {
open(openChannel);
}
// Инициализация и открытия канала со скоростью 500 Мбит/с.
private static void open(int openChannel) {
channel.setValue(openChannel);
// Инициализация:
busError = chaiLibrary.CiInit() !=0 ? (short)(busError|0b1):busError;
// Открыть канал, 11 бит:
U8 flags=new U8();
flags.setValue(0x2);
chaiLibrary.CiOpen(channel, flags);
// Скорость канала обмена:
// Файл chai.h:
// #define BCI_500K_bt0 0x00
// #define BCI_500K_bt1 0x1c
// #define BCI_500K BCI_500K_bt0, BCI_500K_bt1
U8 bt0=new U8();
U8 bt1=new U8();
bt0.setValue(0x0);
bt1.setValue(0x1c);
chaiLibrary.CiSetBaud(channel, bt0, bt1);
// Запуск канала:
chaiLibrary.CiStart(channel);
}
// Закрытие канала:
public static void close () {
chaiLibrary.CiStop(channel);
chaiLibrary.CiClose(channel);
}
public static int getStatusWord(int address){
return command_dt(address, statusWord, dt_quick);
}
public static int resetError (int address){
return command_dt(address, errorReset, dt_quick);
}
// Передать команду:
private static void transmit(int address, short[] data) {
CANmsg_t transmittedСommand_0=new CANmsg_t();
CANmsg_t[] transmittedСommand=(CANmsg_t[]) transmittedСommand_0.toArray(messageSize);
// Служебная часть сообщения:
transmittedСommand[0].id = (int)(0x600+address);
transmittedСommand[0].len.setValue(0x08);
transmittedСommand[0].flags = 0x00;
// Формирование непосредственно команды:
for (int i=0; i<8; i++) {
transmittedСommand[0].data[i].setValue(data[i]);
}
// Отправка команды:
chaiLibrary.CiTransmit(channel, transmittedСommand);
// Диагностика:
//System.out.format("void transmit(...), data:");
//for(short v: data)
//System.out.format("%02X ",v);
//System.out.format("%n");
}
// Забрать все входящие сообщения из буфера:
private static short read () {
// Чтение:
return chaiLibrary.CiRead(channel, receivedMessage, messageSize);
}
/**
Отправка команды, чтение всех входящих сообщений и отбор ответа на отправленную комнаду.
То же что int command_dt(int address, short[] data, int timeout) с timeout = dt_middle = 200 ms.
@param address Адрес CAN–устройста.
@param data Восемь байт передаваемой информации.
@return Возвращает ответ устройства, в случае успеха, или -1, если ответ не пойман.
@see
<a href = "https://electroprivod.ru/pdf/description/SMSD-4.2CAN.pdf">
Контроллер шагового двигателя SMSD–4.2CAN. Техническое описание. / ООО «Электропривод». — 2020.
</a>,
Библиотека CHAI 2.14.0 : Руководство по разработке программного обеспечения : Версия документа 0.28 / ООО «Марафон». — Файл: c:\Program Files (x86)\CHAI-2.14.0\doc\CHAI-Software-Design-Guide.pdf.
*/
public static int command(int address, short[] data) {
return command_dt(address, data, dt_middle);
}
/**
Отправка команды, чтение всех входящих сообщений и отбор ответа на отправленную комнаду.
@param address Адрес CAN–устройста.
@param data Восемь байт передаваемой информации.
@param timeout Пауза между отпрвкой команды и чтением ответа.
@return Возвращает ответ устройства, в случае успеха, или -1, если ответ не пойман.
@see
<a href = "https://electroprivod.ru/pdf/description/SMSD-4.2CAN.pdf">
Контроллер шагового двигателя SMSD–4.2CAN. Техническое описание. / ООО «Электропривод». — 2020.
</a>,
Библиотека CHAI 2.14.0 : Руководство по разработке программного обеспечения : Версия документа 0.28 / ООО «Марафон». — Файл: c:\Program Files (x86)\CHAI-2.14.0\doc\CHAI-Software-Design-Guide.pdf.
*/
public static int command_dt(int address, short[] data, int timeout) {
// Диагностика:
System.out.format("%nКоманда:\t%02X ", 0x600+address);
for (short value:data)
System.out.format("%02X ", value);
System.out.format("%n");
// Передача команды:
transmit(address, data);
// Пауза:
pause (timeout);
// Прочитать ответ:
short result=read();
// Проверить адрес, метку ответа и адрес регистра:
// Вспомогательные перменные для диагностики:
int answer=-1;
int i, j, flag, temp;
String answerString = new String();
for (i=0; i<result; i++) {
// Диагностика:
// вывод всех полученных сообщений:
/*
answerString="0x"+Integer.toHexString(receivedMessage[i].id).toUpperCase();
for (j=0; j<8; j++) {
temp=receivedMessage[i].data[j].intValue();
answerString+=" 0x"+Integer.toHexString(temp).toUpperCase();
}
System.out.format("answerString (%d)=\t%s%n", i, answerString);
*/
System.out.format("Ответ: \t\t%02X ", receivedMessage[i].id);
for (U8 value:receivedMessage[i].data)
System.out.format("%02X ", value.intValue());
System.out.format("%n");
// Проверка ответа:
// Проверка совпадения адресов:
flag=receivedMessage[i].id==(0x580+address)?1:0;
// Проверка признака того, что команда выполнилась:
flag=(receivedMessage[i].data[0].intValue()&0x40) == 0x40?++flag:flag;
// Проверка совпадения адресов регистров команды и ответа:
for (j=1; j<4; j++)
flag=receivedMessage[i].data[j].intValue() == (int)data[j]?++flag:flag;
// Все условия выполнены:
if (flag==5) {
answer=0;
for (j=4; j<8; j++) {
temp=receivedMessage[i].data[j].intValue();
answer+=temp<<(8*(j-4));
}
}
}
// Диагностика:
System.out.format("Значение:\t%d%n", answer);
return answer;
}
/**
Команда для перемещения в новую позицию.
@param address Адрес CAN–устройства.
@param target Новая позиция в условных единицах.
*/
public static void toNewPosition (int address, int target) {
// Установка режима позиционироваия:
command(address, setPositionMode);
// Передача значения новой позиции в шагах:
driverTarget[4]=(short)(target&0xFF);
driverTarget[5]=(short)((target&0xFF00)>>8);
driverTarget[6]=(short)((target&0xFF0000)>>16);
driverTarget[7]=(short)((target&0xFF000000)>>24);
command(address, driverTarget);
// Старт:
command(address, driverStart);
}
// Пауза:
private static void pause (int milliseconds) {
try {
Thread.sleep(milliseconds);
}
catch (InterruptedException e) {
}
}
/**
Консольное приложение и тест класса.
*/
public static void main(String[] args) {
// Адрес контроллера:
int address = 1;
// Новая позиция:
int position=10;
// Пауза перед возвращением вала обратно:
int delta=5000;
// Обработка аргументов программы:
if (args.length > 0) {
try {
position = Short.parseShort(args[0]);
if (position<0)
position=0;
if (position>255)
position=255;
//data_target[5]=position;
}
catch (NumberFormatException e) {
System.err.println("Argument" + args[0] + " must be an integer.");
}
try {
delta = Integer.parseInt(args[1]);
if (delta<0)
delta=0;
}
catch (NumberFormatException e) {
System.err.println("delta = \t" + args[1]);
}
}
// Инициализация:
new CANminimum();
// Запуск движения:
System.out.format("%nНачнем перемещение в позицию %d: %n", position);
toNewPosition(address, position);
// Пауза:
try {
Thread.sleep(delta);
}
catch (InterruptedException e) {
}
// Обраный ход вала:
System.out.format("%nВернем вал обратно: %n");
toNewPosition(address, 0);
// Закрытие канала:
close();
System.out.println();
System.out.println("Канал закрыт");
System.out.println("Конец программы");
}
}
Рабочий класс CANbus учетом аварийной остановки и режимами позиционирования:
Файл CANbus.java:
package CANBus;
import com.sun.jna.IntegerType;
import java.util.TreeMap;
/**
Библиотека команд для перемещения двигателя с опросом текущего положения
и с учетом аварийной остановки по концевикам.
<p>Тестирование:<br>
mvn compile <br>
mvn exec:java -D exec.mainClass=CANBus.CANbus <br>
mvn exec:java -D exec.mainClass=CANBus.CANbus -D exec.args="100 0" <br>
mvn exec:java -D exec.mainClass=CANBus.CANbus -D exec.args="положение пауза"
<p>Пауза — время между началом движения и командой на возврат в начальное положение.
Если пауза меньше времени движения, то возврата вала в первоначальное положение не произойдет.
<p>В рабочем применении класс проверяет состояние двигателя и концевиков и только потом делает попытки повернуть вал.
Всего попыток десять / maximumAttemptNumber=10.
*/
public class CANbus {
private static CANchai chaiLibrary = CANchai.INSTANCE;
private static U8 channel=new U8();
// Ошибки инициализации и открытия:
private static short busError=0;
private static short canError=0;
// Формирование указателя на переменную с данными:
private static short messageSize=10;
private static CANmsg_t receivedMessage_0=new CANmsg_t();
private static CANmsg_t[] receivedMessage=(CANmsg_t[]) receivedMessage_0.toArray(messageSize);
private static short[] driverTarget = { 0x23, 0x7A, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00 };
private static short[] driverStart = { 0x2B, 0x40, 0x60, 0x00, 0x1F, 0x00, 0x00, 0x00 };
private static short[] driverReset = { 0x2B, 0x40, 0x60, 0x00, 0x00, 0x40, 0x00, 0x00 };
private static short[] errorReset = { 0x2B, 0x40, 0x60, 0x00, 0x80, 0x00, 0x00, 0x00 };
private static short[] statusWord = { 0x40, 0x41, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00 };
private static short[] currentPosition = { 0x40, 0x64, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00 };
private static short[] limitSwitch = { 0x40, 0xFD, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00 };
private static short[] quickStop = { 0x2B, 0x40, 0x60, 0x00, 0x06, 0x00, 0x00, 0x00 };
private static short[] setPositionMode = { 0x2F, 0x60, 0x60, 0x00, 0x01, 0x00, 0x00, 0x00 };
private static short[] setHomingMode = { 0x2F, 0x60, 0x60, 0x00, 0x06, 0x00, 0x00, 0x00 };
private static int dt_quick =50;
private static int dt_middle =200;
/**
Инициализация и открытия канала с номером ноль и скоростью 500 Мбит/с.
*/
public CANbus() {
open(0);
}
/**
Инициализация и открытия канала со скоростью 500 Мбит/с.
@param openChannel Номер канала, ноль или один.
*/
public CANbus(int openChannel) {
open(openChannel);
}
// Инициализация и открытия канала со скоростью 500 Мбит/с.
private static void open(int openChannel) {
channel.setValue(openChannel);
// Инициализация:
busError = 0;
busError = chaiLibrary.CiInit() !=0 ? (short)(busError|0b1):busError;
// Открыть канал, 11 бит:
U8 flags=new U8();
flags.setValue(0x2);
busError = chaiLibrary.CiOpen(channel, flags)!=0 ? (short)(busError|0b10):busError;
// Скорость канала обмена:
// Файл chai.h:
// #define BCI_500K_bt0 0x00
// #define BCI_500K_bt1 0x1c
// #define BCI_500K BCI_500K_bt0, BCI_500K_bt1
U8 bt0=new U8();
U8 bt1=new U8();
bt0.setValue(0x0);
bt1.setValue(0x1c);
busError = chaiLibrary.CiSetBaud(channel, bt0, bt1)!=0 ? (short)(busError|0b100):busError;
// Запуск канала:
busError = chaiLibrary.CiStart(channel)!=0 ? (short)(busError|0b1000):busError;
// Диагностика:
//System.out.format("%n%nОшибки при инициализации канала обмена: %d%n%n", busError);
}
// Переоткрытие канала шины:
public static void reOpen(int openChannel) {
close();
open(openChannel);
}
// Закрытие канала:
public static void close () {
chaiLibrary.CiStop(channel);
chaiLibrary.CiClose(channel);
}
public static int getBusError(){
return (int)busError;
}
public static int getCanError(){
return (int)canError;
}
public static int getStatusWord(int address){
return command_dt(address, statusWord, dt_quick);
}
public static int resetError (int address){
return command_dt(address, errorReset, dt_quick);
}
// Передать команду:
private static void transmit(int address, short[] data) {
CANmsg_t transmittedСommand_0=new CANmsg_t();
CANmsg_t[] transmittedСommand=(CANmsg_t[]) transmittedСommand_0.toArray(messageSize);
// Служебная часть сообщения:
transmittedСommand[0].id = (int)(0x600+address);
transmittedСommand[0].len.setValue(0x08);
transmittedСommand[0].flags = 0x00;
// Формирование непосредственно команды:
for (int i=0; i<8; i++) {
transmittedСommand[0].data[i].setValue(data[i]);
}
// Отправка команды:
chaiLibrary.CiTransmit(channel, transmittedСommand);
// Диагностика:
//System.out.format("void transmit(...), data:");
//for(short v: data)
//System.out.format("%02X ",v);
//System.out.format("%n");
}
// Забрать все входящие сообщения из буфера:
private static short read () {
// Чтение:
return chaiLibrary.CiRead(channel, receivedMessage, messageSize);
}
/**
Отправка команды, чтение всех входящих сообщений и отбор ответа на отправленную комнаду.
То же что int command_dt(int address, short[] data, int timeout) с timeout = dt_middle = 200 ms.
@param address Адрес CAN–устройста.
@param data Восемь байт передаваемой информации.
@return Возвращает ответ устройства, в случае успеха, или -1, если ответ не пойман.
@see
<a href = "https://electroprivod.ru/pdf/description/SMSD-4.2CAN.pdf">
Контроллер шагового двигателя SMSD–4.2CAN. Техническое описание. / ООО «Электропривод». — 2020.
</a>,
Библиотека CHAI 2.14.0 : Руководство по разработке программного обеспечения : Версия документа 0.28 / ООО «Марафон». — Файл: c:\Program Files (x86)\CHAI-2.14.0\doc\CHAI-Software-Design-Guide.pdf.
*/
public static int command(int address, short[] data) {
return command_dt(address, data, dt_middle);
}
/**
Отправка команды, чтение всех входящих сообщений и отбор ответа на отправленную комнаду.
@param address Адрес CAN–устройста.
@param data Восемь байт передаваемой информации.
@param timeout Пауза между отпрвкой команды и чтением ответа.
@return Возвращает ответ устройства, в случае успеха, или -1, если ответ не пойман.
@see
<a href = "https://electroprivod.ru/pdf/description/SMSD-4.2CAN.pdf">
Контроллер шагового двигателя SMSD–4.2CAN. Техническое описание. / ООО «Электропривод». — 2020.
</a>,
Библиотека CHAI 2.14.0 : Руководство по разработке программного обеспечения : Версия документа 0.28 / ООО «Марафон». — Файл: c:\Program Files (x86)\CHAI-2.14.0\doc\CHAI-Software-Design-Guide.pdf.
*/
public static int command_dt(int address, short[] data, int timeout) {
// Диагностика:
System.out.format("%nКоманда:\t%02X ", 0x600+address);
for (short value:data)
System.out.format("%02X ", value);
System.out.format("%n");
// Передача команды:
transmit(address, data);
// Пауза:
pause (timeout);
// Прочитать ответ:
short result=read();
// Проверить адрес, метку ответа и адрес регистра:
// Вспомогательные перменные для диагностики:
int answer=-1;
int i, j, flag, temp;
String answerString = new String();
for (i=0; i<result; i++) {
// Диагностика:
/*
answerString="0x"+Integer.toHexString(receivedMessage[i].id).toUpperCase();
for (j=0; j<8; j++) {
temp=receivedMessage[i].data[j].intValue();
answerString+=" 0x"+Integer.toHexString(temp).toUpperCase();
}
System.out.format("answerString (%d)=\t%s%n", i, answerString);
*/
System.out.format("Ответ: \t\t%02X ", receivedMessage[i].id);
for (U8 value:receivedMessage[i].data)
System.out.format("%02X ", value.intValue());
System.out.format("%n");
// Проверка ответа:
// Проверка совпадения адресов:
flag=receivedMessage[i].id==(0x580+address)?1:0;
// Проверка признака того, что команда выполнилась:
flag=(receivedMessage[i].data[0].intValue()&0x40) == 0x40?++flag:flag;
// Проверка совпадения адресов регистров команды и ответа:
for (j=1; j<4; j++)
flag=receivedMessage[i].data[j].intValue() == (int)data[j]?++flag:flag;
// Все условия выполнены:
if (flag==5) {
answer=0;
for (j=4; j<8; j++) {
temp=receivedMessage[i].data[j].intValue();
answer+=temp<<(8*(j-4));
}
}
}
// Диагностика:
System.out.format("Значение:\t%d%n", answer);
return answer;
}
// Обрабатывает statusWord и определяет условие остановки:
public static boolean stopCondition(int address){
// Проверка состояния устройства:
int answer = command(address, statusWord);
// Обработка ответа:
// 6й бит, быстрая остановка:
int bit6=(answer>>6)&1;
// 10й бит, цель достигнута:
int bit10=(answer>>10)&1;
// 12й бит, начальная точка достигнута:
//int bit12=(answer>>12)&1;
// Результат:
return (bit6 | bit10 )==1 || (answer==560) ? true : false;
}
// Пауза для новой команды:
private static void toWait (int address) {
// Номер попытки:
int attemptNumber=0;
int maximumAttemptNumber=10;
// Начальное состояние:
while (!stopCondition(address) && attemptNumber<maximumAttemptNumber) {
// Пауза:
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
}
++attemptNumber;
System.out.format("Попытка: \t%d%n", attemptNumber);
}
}
/**
Команда для перемещения в новую позицию.
@param address Адрес CAN–устройства.
@param target Новая позиция в условных единицах.
*/
public static void toNewPosition (int address, int target) {
// Ждем завершения предыдущего задания:
toWait (address);
if (stopCondition(address)) {
// Установка режима позиционироваия:
command(address, setPositionMode);
// Передача значения новой позиции в шагах:
driverTarget[4]=(short)(target&0xFF);
driverTarget[5]=(short)((target&0xFF00)>>8);
driverTarget[6]=(short)((target&0xFF0000)>>16);
driverTarget[7]=(short)((target&0xFF000000)>>24);
command(address, driverTarget);
// Старт:
command(address, driverStart);
/*
new Thread() {
@Override
public void run() {
checkLimitSwitches (address);
}
}.start();
*/
}
else {
canError=1;
}
}
private static void checkLimitSwitches (int address) {
do {
if (command_dt(address, limitSwitch, dt_quick)!=0)
command_dt(address, quickStop, dt_quick);
} while(!stopCondition(address));
}
// Пауза:
private static void pause (int milliseconds) {
try {
Thread.sleep(milliseconds);
}
catch (InterruptedException e) {
}
}
// Перевод двигателя в начальную точку:
public static void toHomePoint (int address) {
// Ждем завершения предыдущего задания:
toWait (address);
if (stopCondition(address)) {
// Установка режима:
command(address, setHomingMode);
// Команда старт:
command(address, driverStart);
} else {
canError=1;
}
}
// Текущая позиция:
public static int getPosition (int address) {
// Обработка концевиков и экстренное торможение:
if (command_dt(address, limitSwitch, dt_quick)!=0) {
command_dt(address, quickStop, dt_quick);
afterQuikStop(address);
}
// Возврат позиции:
return command_dt(address, currentPosition, dt_quick);
}
// Концевики:
public static int getLimitSwitchsState(int address){
int switchesState = command_dt(address, limitSwitch, dt_quick);
if (switchesState==-1)
switchesState=0;
int leftSwitches =(switchesState&0b0011)!=0 ? 1 : 0;
int rightSwitches =(switchesState&0b1100)!=0 ? 1 : 0;
return leftSwitches + (rightSwitches<<1);
}
// Быстрый стоп:
public static void stopEngine(int address) {
command_dt(address, quickStop, dt_quick);
afterQuikStop(address);
}
// После экстренной остановки присвоить значени цели текущие значение:
private static void afterQuikStop(int address){
int target = command_dt(address, currentPosition, dt_quick);
short[] point = new short[8];
for (int i=0; i<4; i++)
point[i]=driverTarget[i];
point[4]=(short)(target&0xFF);
point[5]=(short)((target&0xFF00)>>8);
point[6]=(short)((target&0xFF0000)>>16);
point[7]=(short)((target&0xFF000000)>>24);
command_dt(address, point, dt_quick);
}
// Общее состояние:
public static int getState (int address) {
return command(address, statusWord);
}
// Перезагрузка:
public static void reset(int address) {
command(address, driverReset);
}
// System.out.format("%n%n stopCondition = %d%n%n", (bit6 | bit10 | 0 ));
/**
Консольное приложение и тест класса.
*/
public static void main(String[] args) {
// Адрес контроллера:
int address = 1;
// Новая позиция:
int position=10;
// Пауза перед возвращением вала обратно:
int delta=5000;
// Обработка аргументов программы:
if (args.length > 0) {
try {
position = Short.parseShort(args[0]);
if (position<0)
position=0;
if (position>255)
position=255;
//data_target[5]=position;
}
catch (NumberFormatException e) {
System.err.println("Argument" + args[0] + " must be an integer.");
}
try {
delta = Integer.parseInt(args[1]);
if (delta<0)
delta=0;
}
catch (NumberFormatException e) {
System.err.println("delta = \t" + args[1]);
}
}
// Инициализация:
new CANbus();
// Запуск движения:
System.out.format("%nНачнем перемещение в позицию %d: %n", position);
toNewPosition(address, position);
// Пауза:
try {
Thread.sleep(delta);
}
catch (InterruptedException e) {
}
// Обраный ход вала:
System.out.format("%nВернем вал обратно: %n");
toNewPosition(address, 0);
// Закрытие канала:
close();
System.out.println();
System.out.println("Канал закрыт");
System.out.println("Конец программы");
}
}
В результате исходная структура папок и файлов проекта Ява с артифактом CANBus будет иметь следующий вид:
Команда компиляции:
mvn compile
Запустить двигатель можно будет несколькими командами:
mvn exec:java -D exec.mainClass=CANBus.CANminimum
mvn exec:java -D exec.mainClass=CANBus.CANminimum -D exec.args="10 5000"
mvn exec:java -D exec.mainClass=CANBus.CANbus
mvn exec:java -D exec.mainClass=CANBus.CANbus -D exec.args="10 0"
mvn exec:java -D exec.mainClass=CANBus.CANbus -D exec.args="положение пауза"
Выполним команду:
mvn exec:java -D exec.mainClass=CANBus.CANminimum -D exec.args="10 5000"

Здесь положение 10d = 0Ah в условных единицах, а пауза между командой на перемещение и командой возврата в первоначальное положение выбирается равной 5000 мс.
Сначала в регистр 6060h записывается значение один — другими словами устанавливается режим позиционирования с регулятором положения. Это один из трех режимов позиционирования. Два других — это регулятор вращения и перемещение к началу отсчета, руководство [6] страницы 98–103 для перемещения к началу отсчета, 103–106 для регулятор положения, 106–108 для регулятора вращения. Далее записываем в регистр 607Ah новую цель перемещения. Потом записываем в регистр 6040h значение 1Fh, чем даем старт перемещению.
По истечению пяти секунд после отправки первой команды вал возвращается обратно. В той же последовательность ведутся записи в регистры 6060h, 607Ah и 6040h. Только в регистр цели 607Ah уже записывается значение позиции ноль.
Класс CANbus.java перемещает вал и останавливает его при срабатывании концевиков. Также при паузе 0 мс, программа ждет пока не закончится движение двигателя, чтобы запустить его назад.
На практике используется следующая схема подключения концевиков:
Но здесь будем использовать упрощенную схему:
Учтено то, что левый концевик — это отрицательный вход, соединенный с отрицательным датчиком начала отчета. Та же связка с правым концевиком и положительным входом и датчиком начало отсчета, руководство [6] страница 115. В этом случае регистр 60FDh при срабатывании левого концевика принимает значение &0011, а правого — &1100, руководство [6] страница 85.
Выполним команду:
mvn exec:java -D exec.mainClass=CANBus.CANbus -D exec.args="10 0"


На втором скрине первая команда после строки «Вернем вал обратно» и перед строкой «Попытка: 2» команда запроса состояния двигателя получает ответ 1337h = 4919d = 0001 0011 0011 0111. Десятый бит равен нулю, что означает, что вал шагового двигателя в движении и надо ждать его остановки, руководство [6] страницы 94 и 95, биты 6 и 10.
Следующий запрос состояния двигателя, другими словами чтение регистра состояния 6041h дает результат 1591d = 0637h = 0110 0011 0111b. Десятый бит равен единице, что означает, что цель достигнута и можно начать новое движение и возвратиться в начальное положение, руководство [6] страница 95.
Условия остановки проверяют функции с сигнатурой:
public static boolean stopCondition(int address)
private static void toWait (int address)
Функции проверяют значения регистра 6041h: если шестой или десятый бит отмечены, то вал остановлен. Шестой бит равен единице, когда поступает команда о немедленной остановке. Это тоже, что аварийная остановка.
При равенстве единице тринадцатого бита, надо сбрасывать ошибку и только потом начинать движение.
Документацию можно получить командой:
mvn javadoc:javadoc или с учетом изменений mvn clean javadoc:Javadoc
Файл target/reports/apidocs/CANBus/package-summary.html:

Класс CANbus использовался в графической оболочке JavaFX [27–33] с минимальным набором функций.
Графическая программа работала в два потока по следующей схеме: во время остановки двигателя работал поток графики JavaFX и поток с опросом концевиков. После команды «старт перемещения», поток с опросом концевиков останавливался и запускался поток с циклическим опросом текущего положения и значениями концевиков. Поток JavaFX продолжал параллельно обрабатывать данные с текущим положеним. При срабатывание любого концевика — двигатель останавливался. Запас шагов для аварийной остановки по концевикам был в сто микрошагов. Можно увеличить запас прочности по аварийной остановке, например, уменьшив скорость вала.
После остановки двигателя поток с опросом текущего положения и концевиков закрывался, открывался поток с опросом только концевиков:

При скорости обмена по CAN–шине в 500 кбит/с длина команды составляет 108 бита [34, 35] и чистое время на передачу команды при выбранной скорости обмена занимает 216 µс, столько же займет ответное сообщение без учета времени задержки. Задержки по времени наблюдались в самом приложении, при обработке кадров шиной и при ответе контроллера на команды. В результате основные команды стабильно выполнялись в цикле с частотой 55–100 мс. Команды записи констант для двигателя, установки заводских значений по умолчанию и сохранения требовали больше времени на обработку. Пауза между такими командами была выбрана длительностью в 200 мс.
Наверно, для «Первых шагов» здесь изложено достаточно материала. По работе шаговых двигателей можно задавать вопросы Харьюзову Павлу kharyuzov@jinr.ru и Белобородову Алексею aleksstt@jinr.ru, по библиотекам Pythona, например, Семашко Сергею semashko@jinr.ru.
Ссылки:
Контроллер шаговых двигателей c интерфейсом CAN SMSD–4.2CAN / НПО «Электропривод». — URL: https://electroprivod.ru/smsd-42can.htm (дата обращения: 11.03.2025).
Программы для управления шаговыми двигателями / НПО «Электропривод». — URL: https://electroprivod.ru/program.htm htm (дата обращения: 11.03.2025).
Конвертор интерфейсов CAN в USB: ГКМН.468351.017-03 / ООО «Марафон». — URL: http://products.marathon.ru/page/konvertory/CAN-USB (дата обращения: 14.03.2025).
Шаговые двигатели серии ШД86 / НПО «Электропривод». — URL: https://electroprivod.ru/dsh86.htm (дата обращения: 19.03.2025).
NLS–1524 = Интеллектуальный источник питания с интерфейсом RS-485 / ООО «НИЛ АП». — URL: https://www.reallab.ru/catalog/power/nls-1524/ (дата обращения: 19.03.2025).
Контроллер шагового двигателя SMSD–4.2CAN. Техническое описание. / ООО «Электропривод». — 2020. — URL: https://electroprivod.ru/pdf/drivers/smsd-4.2can_users-manual.pdf (дата обращения: 19.03.2025).
Software / Smart motor Devices. — URL: https://smd.ee/software.htm (дата обращения: 31.03.2025).
CAN-bus-USBnp интерфейс : 2 канала CAN : Версия 4.0 : Руководство пользователя Версия 1.1 / ООО «Марафон». — URL: http://products.marathon.ru/page/konvertory/CAN-USB (дата обращения: 11.04.2025).
Библиотека CHAI (драйверы устройств) / ООО «Марафон». — URL: http://products.marathon.ru/page/prog/chai (дата обращения: 11.04.2025).
Библиотека CHAI 2.14..0 : Руководство пользователя : Версия документа 0.24 / ООО «Марафон». — Файл: c:\Program Files (x86)\CHAI-2.14.0\doc\ CHAI-User-Guide.pdf.
Библиотека CHAI 2.14..0 : Руководство по разработке программного обеспечения : Версия документа 0.28 / ООО «Марафон». — Файл: c:\Program Files (x86)\CHAI-2.14.0\doc\CHAI-Software-Design-Guide.pdf.
MSYS2 : Software Distribution and Building Platform for Windows. — URL: https://www.msys2.org/ (дата обращения: 18.04.2025).
Java® Platform, Standard Edition & Java Development Kit : Version 23 API Specification / Oracle. — URL: https://docs.oracle.com/en/java/javase/23/docs/api/index.html (дата обращения: 18.04.2025).
Java Native Access / MvnRepository. — URL: https://mvnrepository.com/artifact/net.java.dev.jna/jna (дата обращения: 18.04.2025).
Java Native Access (JNA) . — URL: https://github.com/java-native-access/jna (дата обращения: 18.04.2025).
В нативный код из уютного мира Java: путешествие туда и обратно (часть 2) / @ValeriaKhokha . — 2020 . — URL: https://habr.com/ru/companies/jugru/articles/524342/ (дата обращения: 18.04.2025).
Севестре, Ф. Использование JNA для доступа к собственным динамическим библиотекам / Ф. Севестре, редактор М. Аибин ; Using JNA to Access Native Dynamic Libraries / P. Sevestre, editor M. Aibin. — 2024. — URL: https://www.baeldung.com/java-jna-dynamic-libraries (дата обращения: 18.04.2025).
How to call/invoke external DLL library method/function from Java code? / Michał Wróbel's blog. — 2011. — URL: http://blog.mwrobel.eu/how-to-call-dll-methods-from-java/ (дата обращения:18.04.2025).
JNA, Structures and Arrays / StackOverflow. — 2012. — URL: https://stackoverflow.com/questions/9691446/jna-structures-and-arrays (дата обращения: 18.04.2025).
Structure array elements must use contiguous memory (bad backing address at Structure) / ProgrammerSought. — URL: https://programmersought.com/article/4982324645/ (дата обращения: 18.04.2025).
Bhagwat, S. toArray() in Java / S. Bhagwat. — 2023 URL: https://www.scaler.com/topics/toarray-in-java/ (дата обращения: 18.04.2025).
com.sun.jna.Structure Maven / Gradle / Ivy / JARdownload. — URL: https://jar-download.com/artifacts/net.java.dev.jna/jna/5.1.0/source-code/com/sun/jna/Structure.java (дата обращения: 18.04.2025).
How should I pass and return unsigned int by value from Java to C/C++ in jna / StackOverflow. — 2015. — URL: https://stackoverflow.com/questions/33544028/how-should-i-pass-and-return-unsigned-int-by-value-from-java-to-c-c-in-jna (дата обращения: 18.04.2025).
JNA API Documentation / Oracle. — URL: http://java-native-access.github.io/jna/4.2.1/ (дата обращения: 18.04.2025).
IntegerType / Javatips. — URL: https://www.javatips.net/api/jna-master/src/com/sun/jna/IntegerType.java (дата обращения: 18.04.2025).
Apache Maven Project / Oracle. — URL: https://maven.apache.org/ (дата обращения: 18.04.2025).
JavaFX / Oracle. — URL: https://openjfx.io/ (дата обращения: 22.04.2025).
Java Platform, Standard Edition (Java SE) 8 : JavaFX : Swing and 2D : JavaFX Scene Builder 2 / Oracle. — URL: https://docs.oracle.com/javase/8/javase-clienttechnologies.htm (дата обращения: 22.04.2025).
JavaFX Documentation Project / Oracle. — 2022. — URL: https://fxdocs.github.io/docs/html5/ (дата обращения: 22.04.2025).
Introduction to JavaFX / Baeldung : Editor : L. Crusoveanu. — 2024. — URL: https://www.baeldung.com/javafx (дата обращения: 22.04.2025).
Ruzicka, V. JavaFX Tutorial: Hello world / V. Ruzicka // Vojtech Ruzicka's Programming Blog. — 2019. — URL: https://www.vojtechruzicka.com/javafx-hello-world/ (дата обращения: 22.04.2025).
Учебник по JavaFX: начало работы / @Val6852. — 2019. — URL: https://habr.com/ru/articles/474292/ (дата обращения: 22.04.2025).
Первая программа на JavaFX. — 2021. — URL: https://metanit.com/java/javafx/1.2.php (дата обращения: 22.04.2025).
Введение в протокол CAN / КБ "Марафон". — URl: http://can.marathon.ru/page/can-protocols/canbus/canintro (дата обращения: 22.04.2025).
ЛикБез по CAN-FD / @Aabzel. — 2025. — URL: https://habr.com/ru/articles/793966/ (дата обращения: 22.04.2025).