June 28, 2019

Символьная линковка через атрибуты

Данные железа связываются с переменными программной части через конфигурацию контроллера. Иногда это не удобно и хочется идти не от конфигурации, а от кода (Code First). Как пример, я взял линковку переменных, связанных с управлением движением сервоосей NC PTP. И еще, верните, пожалуйста, карту мапинга — она была бессмысленной, но красивой ↘


Атрибуты


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

{attribute 'TcNcAxis' := 'Axis 3'}
axMaster : AXIS_REF;

Здесь атрибут (ключевое слово attribute) заключен в фигурные скобки. Строка атрибута должна идти перед объявлением переменной или экземпляром функционального блока. Далее в кавычках следует одно или несколько(!) имен переменных или ФБ, символ присваивания  := , и наконец значение атрибута. В данном случае значением служит имя NC оси взятое из конфигурации  'Axis 3' . Записывается прямо с пробелами и другими символами, для этого и нужны кавычки.

Добавив атрибут и пересобрав проект, я автоматически получу слинкованную с сервоосью переменную типа AXIS_REF. Остается только реактивировать конфигурацию в ПЛК и все готово. Еще раз:
  1. Добавить строку атрибута или заменить имя оси в значении атрибута на другое.
  2. Пере/собрать проект (Build → Rebuild Solution).
  3. Активировать конфигурацию. Внимание! Активировать конфигурацию!
После активации, слинкованные через атрибут переменные будут подсвечены синим значком линковки (см. FromPlc и ToPlc):


Когда системе не удается слинковать данные, она сообщает об этом большим и внезапным сообщением поперек экрана. Проект в итоге соберется, но данные бегать не будут. Если же все прошло удачно, то после пересборки проекта система сообщит:
Message 06.01.2019 21:56:05 729 ms | 'TwinCAT XAE': Existing NC axis 'Axis 3' linked to instance 'MAIN.axMaster'
Очень удобно, так как теперь можно еще больше сконцентрироваться на коде. Главное не забывать пересобирать проект, а затем активировать конфигурацию. Аналогично можно работать с функциональными блоками.


Ввод/вывод


Переменные ввода/вывода также удобно линковать со стороны кода. Причем, здесь мы как раз увидим тот самый обещанный символьный маппинг во всей красе, так как будем использовать сложный символьный путь к данным.

На этот раз воспользуемся атрибутом TcLinkTo. Вообще, он простой, поэтому я возьму что-нибудь посложнее TcLinkToOSO. Атрибут c -OSO на конце позволяет не просто связать переменную типа AT %IQ* с данными, но также задать количество разрядов тут и там, а также их битовое смещение.

Немногим ранее, я проверял как работает функция быстрого останова в NC. Пришлось сплясать с бубном, чтобы прикрепить булевый флаг к биту частично занятой переменной. Надеюсь, что с атрибутами получится в разы проще.

Напомню задачу: в настройках NC оси, в ветке конфигурации Axis X → Drive → Inputs →... есть поле nState4. Седьмой бит этого поля отвечает за быстрый останов данной сервооси. Пара битов этого поля уже автоматически слинкована с какими-то данными, поэтому действовать нужно аккуратно.
Быстрый стоп не работает на виртуальных осях! Необходимо серво-железо.
Для начала сходим в конфигурацию и посмотрим, где расположены требуемые данные в конфигурации со стороны железа, то есть смотрим во вкладку Variable, текстовое поле Full NameTINC^NC-Task 1 SAF^Axes^Axis 3^Drive^Inputs^In^nState4. Это полный путь к данным переменной nState4 в конфигурации:


Копируем и вставляем его в код программы как значение атрибута

{attribute 'TcLinkToOSO' := '<0,1,7>TINC^NC-Task 1 SAF^Axes^Axis 3^Drive^Inputs^In^nState4'}
cmd_FastStop AT %Q* : BOOL;

Мне нужен всего-лишь один бит, поэтому я расширил путь специальным суффиксом -OSO: <0,1,7>. Последовательно слева-направо:
  • 0 — битовое смещение в переменной ПЛК, то есть с какого бита ПЛК переменной начинать связывать. Можно связать не всю переменную, а только ее часть и не обязательно с начала и до конца: с помощью -OSO можно выхватить кусочек из середины.
  • 1 — количество связываемых бит.
  • 7 — битовое смещение в переменной на стороне железа (в моем случае в переменной nState4). Мне нужен бит номер 7, поэтому смещение = 7.

После сборки проекта система сообщает:
Message 06.01.2019 22:11:15 930 ms | 'TwinCAT XAE': Variable 'MAIN.cmd_FastStop' (Offs: 0) linked with 'TINC^NC-Task 1 SAF^Axes^Axis 3^Drive^Inputs^In^nState4' (Offs: 7, Size: 1)

Разбавлю полным листингом программы:

PROGRAM MAIN
VAR
    {attribute 'TcNcAxis' := 'Axis 3'}
    axMaster     : AXIS_REF;
    
    McPower      : MC_Power;
    McVelocity   : MC_MoveVelocity;

    cmd_PowerOn  : BOOL;
    cmd_Velocity : BOOL;
    {attribute 'TcLinkToOSO' := '<0,1,7>TINC^NC-Task 1 SAF^Axes^Axis 3^Drive^Inputs^In^nState4'}
    cmd_FastStop AT %Q* : BOOL;
    
    actVelo      : LREAL;
    state        : INT;
END_VAR

[...]

axMaster.ReadStatus();
actVelo := axMaster.NcToPlc.ActVelo;

CASE state OF
0:
    IF McPower.Status THEN
        cmd_Velocity := TRUE;
        state := 100;
    END_IF

100:
    IF McVelocity.InVelocity THEN
        cmd_Velocity := FALSE;
        state := 200;
    END_IF
    
200:
    IF actVelo < 0.1 OR NOT McPower.Status THEN
        cmd_PowerOn := FALSE;
        state := 300;
    END_IF     
    
300:
    IF NOT McPower.Status THEN
        cmd_FastStop := FALSE;
        state := 0;
    END_IF

END_CASE

McPower(
    Axis            := axMaster, 
    Enable          := cmd_PowerOn, 
    Enable_Positive := TRUE, 
    Enable_Negative := TRUE
);

McVelocity(
    Axis     := axMaster, 
    Execute  := cmd_Velocity, 
    Velocity := 200
);

Теперь если во время работы программы дернуть за булевый флаг cmd_FastStop и установить его в TRUE, мы незамедлительно получим быструю остановку сервооси, а система подскажет нам, что:
Message 06.01.2019 22:23:25 772 ms | 'TCNC' (500): 'Axis 3' (Axis ID: 3) <NOTE>: 'Fast axis stop' triggered by IO interrupt 'Drive->Status4->Bit7' (SignalType=1 (SignalType_RisingEdge), FastDec=0.000000, FastJerk=0.000000, IoState=1, OldIoState=0)!

Мышой проще


А еще есть кнопка Link To PLC... которая никакого отношения к символьной линковке не имеет, зато позволяет присобачить одним кликом мыши переменную AXIS_REF к NC оси, и безо всяких там NcToPlc, PlcToNc, ...


June 27, 2019

Структурированные исключения которые не работают

Версия TwinCAT 3.1.4022.29.


И похоже мы имеем дело с ядрами разных версий на десктопе и в полноценных ПЛК. Под полноценные контроллеры проект собирается без ошибок, но исключения по прежнему не работают.
Исключения доступны начиная с версии 3.1.4024.0, но только на ПЛК и только в 32-х разрядных системах. Читайте комментарии к посту. Обновлено 10 октября 2019.
Исключения эти вроде как расширение стандарта МЭК. Поэтому пока эта штука не работает можно познакомиться с Exceptionhandling in IEC Applikationen mit CODESYS и документацией на CoDeSys.

June 19, 2019

New, Delete и память роутера

Мир готов отказаться от ручного управления динамической памятью, а мы ее только-только получили, в нагрузку к TwinCAT 3. Иначе классы и прочий полиморфизм не работают. Теперь, когда появилась динамическая память, должны появиться и динамические типы данных, а если не появятся — мы их сделаем сами.

Ключевые слова


Во времена TwinCAT 2 динамической памяти не было. Если что-то изменяло свой размер, то под него выделялся буфер максимального размера, рядом ставился счетчик длины или количества элементов, или как-то еще, но в итоге как-то отмечали окончание цепочки символов. Например, нулевой символ \0 в конце строки. Как вариант, можно было создать массив структур, и по мере необходимости добавлять в него указатели, и таким образом нафантазировать себе списки, очереди, стеки, деревья, а также другие типы данных переменной длины.

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

PROGRAM NewDeleteRepeatAgain
VAR
    ptr : POINTER TO ARRAY[0..4095] OF BYTE; 
END_VAR

[...]

ptr := __NEW(BYTE, 4096);
__DELETE(ptr);

Оба ключевых слова встроены в язык программирования и являются расширением стандарта IEC 61131-3. Еще раз, это не функциональные блоки, это встроенные средства языка программирования. Обе команды работают с указателями типа POINTER TO ТипДанных. С "голыми" адресами типа DINT и PVOID команды работать не умеют и тем самым достигается какая-никакая безопасность при работе с арифметикой указателей.

Кроме динамических структур, можно на лету создавать экземпляры функциональных блоков. В результате получится указатель на функциональный блок, который затем используется как обычная функция. Чтобы функциональный блок или структуру стали динамически создаваемыми, их необходимо пометить специальным атрибутом {attribute 'enable_dynamic_creation'}.


Память роутера


Память занимается у роутера. Ее объем — это обычная настройка проекта:



Роутеру же память и возвращается — если, конечно, разработчик не забудет вернуть. Если забудет, то память закончится и __NEW начнет возвращать нули вместо указателей. Я пробовал выделять блоки длиной в 4 мегабайта и тут же удалять их, и так каждый ПЛК цикл. Ни каких проблем с этим не возникло. Время работы практически не отличается от работы с фиксированными данными. Выделение и освобождение памяти происходит максимально быстро и практически не заметно. Разве что, чуть-чуть.

В эксперименте принимал участие контроллер С6015 с атомом E3845 на борту (Windows 10 IoT) и CX9020 c ARMv7 и WinCE Emb. Результаты по таймингам идентичные. Более слабый процессор CX9020 позволяет обрабатывать меньшее количество данных за цикл, поэтому размер блоков пришлось сократить до 100 килобайт, но задержки по прежнему аналогичны старшему брату. От разрядности и операционной системы тоже ничего не зависит. Вот за эту универсальность все и любят третью версию TwinCAT.

__DELETE возвращает теперь уже свободное место в кучу. С нулевыми указателями команда не работает: она просто игнорируя их. Указатели в TC3 типизированные, поэтому кроме как трюками с ADS их не испортить. Если не забывать освобождать занятую память, то все должно быть нормально. На картинке ниже, я ежесекундно занимаю по 4 мегабайта из памяти роутера, а затем... не освобождаю ее: когда-нибудь память у роутера закончится.



Самоконтроль


Библиотека Tc2_Utilities предлагает функцию FB_GetRouterStatusInfo, отвечающую за контроль состояния роутера. Поле maxMemAvail выходной структуры ST_TcRouterStatusInfo хранит текущий объем свободной памяти роутера в байтах. Эта же память является "кучей" динамической памяти.

Аналогичную информацию разработчик может получить и без программирования, просто ткнув правой кнопкой мыши в иконку TwinCAT, а затем в Router → Info.


К сожалению, в данный момент (для TwinCAT 3.1.4022.29) единственный способ очистить кучу не отключая контроллер — это перезапустить системный сервис TwinCAT. Перезапуск программы ПЛК, перезаливка конфигурации, холодный или полный сброс переменных, переключение в режим конфигурации, любые другие телодвижения — не скроют следы утечек памяти у неаккуратного разработчика. Бдите!


Производительность


Память занимается и освобождается быстро. Что еще? Копирование динамически созданных блоков памяти происходит с той же скоростью, что и у статических (статически выделенных на этапе сборки проекта переменных). Проверка на массивах большого размера различия не показала.

Как там дела с обработкой элементов массива или динамического списка (vector, List, итп типов данных)? Здесь есть небольшое различие, которые не так чтобы сильно заметны и возможно это просто накладные расходы на работу с указателями, но все-таки чуть медленнее. По вертикали отложены миллисекунды от 4 до 10 мс:


Каждый цикл программа пробегает по всем элементам массива: 8 миллисекунд длится забег по статическому массиву и 9 миллисекунд по динамическому. Резкие провалы вниз — это выделение динамической памяти под массив (которые, как нам теперь известно, почти не занимает времени цикла).

Итого, не сильно большая разница чтобы осторожничать, но проверить стоило.


PS: Копаем глубже


При создании нового экземпляра происходит инициализация объекта и его полей. Если в объекте есть вложенные объекты (структура в структуре), они также будут инициализированы. С классами немного сложнее, так как в них есть конструкторы и деструкторы, но это уже отдельная тема.