Showing posts with label attribute. Show all posts
Showing posts with label attribute. Show all posts

November 13, 2020

Атрибут TcNcAxis

Атрибут TcNcAxis линкует NC ось со стороны кода, автоматически изменяя конфигурацию ПЛК. Про этот атрибут уже было в символьной линковке через атрибуты. Здесь же будет пара-тройка нюансов.


Массивы

Большое количество осей удобно объединять в массивы. Массив осей можно линковать одним атрибутом:

{attribute 'TcNcAxis' := '[1]:=Axis X; [2]:=Axis Y; [3]:=Axis Z' }
axes: ARRAY [1..3] OF AXIS_REF;


В имени оси можно использовать пробел. Имя оси в атрибуте тоже записывается с пробелами. Количество пробелов должно совпадать и между буквами в имени, и в атрибуте. Axis⎵X и Axis⎵⎵X — это разные оси.

Если имя оси указано в атрибуте, но оси с таким именем нет, то в конфигурации будет создана новая ось.

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


Автомапинг функциональных блоков

Если нет доступа к внутренностям функционального блока, можно воспользоваться… атрибутом:

{attribute 'TcNcAxis' := '.axisZ:=Axis Z'}
fbFoobar: FB_Foobar;

[...]

FUNCTION_BLOCK FB_Foobar
VAR
    axisZ: AXIS_REF;
END_VAR

[...]

В одном программном блоке может быть несколько экземпляров функционального блока. Но можно слинковать оси только одного экземпляра ФБ. Нельзя линковать несколько переменных AXIS_REF с одной и той же осью. При компиляции система выдаст ошибку: 
Mapping conflict! Same data of 'TINC^NC-Task 1 SAF^Axes^Axis Z^Inputs^FromPlc' is written from different sources


August 18, 2020

Управление битовыми полями

В TwinCAT 3 появился тип данных BIT. Он предназначен искючительно для создания новых типов данных с точно заданной структурой и удобного обмена данными с периферией (железом). Применять его можно только при объявлении новых структур данных:

TYPE BitBang :
STRUCT
    b0: BIT;
    b1: BIT;
    b2: BIT;
END_STRUCT
END_TYPE

Биты будут упаковываться в байт. По мере заполнения места в байте и превышения его размера система увеличит размер структуры на еще один байт и так далее, экономя место. Аналогичная структура из полей типа BOOL будет занимать в восемь раз больше места, так как в памяти под одно поле типа BOOL отводится сразу целый байт.

Статьи в инфосисе: BIT и Structure → Bit access in structures.

Экономия не дается просто так. Для доступа к битовым полям требуется больше времени из-за операций упаковки-распаковки. Поэтому используйте их только для обмена данными с устройствами, а затем транслируйте в тип BOOL. Ниже результат, а код будет в конце поста. По вертикали время на исполнение цикла (меньше — быстрее):

Где-то в полтора раза больше времени требуется на доступ к отдельным битам. На двух-трех операциях это будет незаметно. Глядя на результат выше, держите в голове, что счет там идет на десятки тысяч (до 100 000) операций за цикл. И это не то место, где уместно экономить на абстракциях.


Биты целочисленных данных

Целочисленные данные INT, BYTE, ... с самого начала позволяли читать и устанавливать свои отдельные биты. Достаточно написать iVar.3 и получить доступ к биту номер 3 (счет идет от нуля). Внезапно, нам подарили интересный механизм именования отдельных битов целочисленных данных. Проще говоря, теперь можно дать имена отдельным (или вообще всем) битам целого числа. Для этого лучше всего подходят константы, ведь в дальнейшем их значение нельзя будет изменить. Тем самым номер бита будет определен один единственный раз и не сможет изменяться в дальнейшем.

Можно создавать как локальные так и глобальные синонимы. С глобальными есть нюанс, поэтому испытания начнем с них. Идем в GVLs, создаем список GVL, название можно выбрать произвольное, пишем следующее:

//{attribute 'qualified_only'}
VAR_GLOBAL CONSTANT
    Enable:  INT   := 3;
    Disable: BYTE  := 5;
    Error:   DWORD := 15;
END_VAR

Важно (!) убрать атрибут 'qualified_only' и указать, что это константы. С включенным атрибутом работать не будет. Конкретный тип данных не важен, главное чтобы он был целочисленным. Значение константы (число) обозначает номер бита, нумерация по прежнему начинается с нуля.

Теперь ближе к коду, что именно можно с этим сделать?

PROGRAM MAIN
VAR
    iVar: LINT;
    bVar: BYTE;
END_VAR
VAR CONSTANT
    Enable:  WORD := 3;
    Ready:   WORD := 3;
    Disable: WORD := 5;
    Error:   WORD := 15;
END_VAR

iVar.Enable := TRUE;
IF iVar.Ready THEN
    iVar.Disable := FALSE;
END_IF

// bVar.Error := FALSE; >>> `Error` is no valid bit number for `bVar`

iVar.Enable установит бит разрешения #3, а iVar.Disable сбросит бит запрета #5. Выйти за разрядную сетку не получится, компилятор бдит еще на этапе сборки (см. комментарий про bVar).

Можно дать одному и тому же биту разные названия. В примере выше поля Enable и Ready — это имена-синонимы четвертого бита (бит №3). Иногда это удобно и наглядно:

iVar.Enable := TRUE;
// iVar = 2#0000_1000
// iVar.Ready == TRUE

iVar.Disable := TRUE;
// iVar = 2#0010_1000

iVar.Ready := FALSE;
// iVar = 2#0010_0000
// iVar.Enable = FALSE
  


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

Любые операции с отдельными битами, независимо от типа операции, чтение ли это или установка бита, приведут к дополнительным операциям упаковки/распаковки битов в байты. Это конечно же отразиться на производительности. Да, это будут копейки относительно других операций (см. картинку выше), но давайте посмотрим как обстоят дела с операциями чтения и записи по отдельности, а в конце еще раз всё вместе:

Чтение требует чуть меньше времени, чем запись. Операции с отдельными битами требуют больше времени, чем работа с булевым типом. Комплексные операции чтение-запись привносят еще большую нагрузку. Но(!) еще раз повторюсь, здесь счет идет на десятки тысяч операций за один цикл. Не стоит выигрывать наносекунды за счет читаемости кода. Кстати, код:

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 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: Копаем глубже


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

February 7, 2018

Деление на ноль

Давно ли вы делили на ноль?



Случайные ошибки


PROGRAM MAIN
VAR
    a, b, c : INT;
END_VAR

a := 12 / 0;

Такую случайную ошибку среда разработки отловит еще на этапе компиляции:



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

PROGRAM MAIN
VAR
    a, b, c : INT;
END_VAR

a := 12 / b; // <<<< !!!!

...TwinCAT переходит с состояние "стоп" (Stop), выбрасывая в консоль сообщение об ошибке:
Error 01.02.2018 15:02:24 456 ms | 'Port_851' (851): Exception (Exception Code: 0xc0000094, Integer divide by zero) in PLC Application Untitled1 Instance, Task PlcTask (EBP: 0xa9b5eeb8, EIP: 0xa7927056, ESP: 0xa9b5ee90)

Что произойдет если у вас несколько задач (тасков)? Остановится ли только "эта" задача или все сразу? Как поведет себя система в многоядерной архитектуре? И что с изолированными (isolated) выделенными ядрами?


Изолируем и форкаем


Добавляю и запускаю вторую, параллельную задачу. И сразу же роняю первую, установив переменную b := 0. Первая задача падает в точку останова, но вторая задача продолжает работать — счетчик тикает:



Пробую восстановить задачу после падения, устранив ошибку: значение переменной b := 1, затем пытаюсь продолжить выполнение первой задачи. Не получается. Пробую подтвердить на экране ПЛК сообщение об ошибке деления на ноль (кнопка ОК). TwinCAT целиком переключается в режим конфигурирования (синий значок), но теперь уже с остановкой всех задач: и ошибочных, и корректных. При этом:
  • Reset Cold — не помогает.
  • Rest Origin — помогает, так как он начисто сносит весь рантайм задачи (это заметно по предложению пересоздать порт при последующем запуске). Затем можно перезапустить задачу заново, но непонятно, что в это время творится с другими задачами.

При всех этих телодвижениях среда разработки VS 2015 ведет себя очень нестабильно, а может быть это контроллер ведет себя некорректно. Не уверен — кто из них. Возможно оба. В надежде повысить стабильность, я выделил одно из ядер ПЛК целиком под TwinCAT: 100% производительности в печенку процессора. Ничего не меняется. Пытаюсь перезапустить ошибочную задачу, но TwinCAT переходит в режим конфигурации, с полной остановкой всех ядер и задач. Версия TwinCAT контроллера: 3.1.4020.32.

Все тщетно, но с какой-то вероятностью можно сказать, что отдельные ядра или задачи будут продолжать работать. Эта неуверенность еще больше запутывает и вероятно такого просто не должно быть. Вообще.


Отлаживаем


Для отладки и только отладки (почему, я расскажу позже) TwinCAT предлагает нам стандартное средство из проверочных функциональных блоков — Object POUs for implicit checks. Теперь их можно создавать тыкая мышкой в: POUs → Add → POU for implicit checks... → +Division Checks. После этого в ПЛК-проект добавятся несколько автоматически сгенерированных функций. Необходимо перекомпилировать проект и перезагрузить его в контроллер целиком (download). Загрузка проекта на лету (online change) с сохранением данных (значений переменных) на этот раз недоступна, так как в проекте появились новые программные объекты (POUs).

Я экспериментирую с делением целого типа, вот пример такой функции:

// Implicitly generated code : DO NOT EDIT
FUNCTION CheckDivDInt : DINT
VAR_INPUT
    divisor:DINT;
END_VAR

// Implicitly generated code : Only an Implementation suggestion
{noflow}
IF divisor = 0 THEN
    CheckDivDInt:=1;
ELSE
    CheckDivDInt:=divisor;
END_IF;
{flow}

Несмотря на предостережение: "НЕ РЕДАКТИРОВАТЬ", — редактировать можно и нужно. Например, добавить сообщение об ошибке в лог и вытащить номер текущего шага из других программных объектов.

Интересно, что эта функция будет вызываться для каждой операции деления! Значением входного параметра divisor будет значение делителя/знаменателя из операции деления, а результат функции (возвращаемое значение) будет подставляться вместо делителя в операции деления. Попробуйте в теле функции заменить CheckDivDInt:=divisor; на CheckDivDInt:=2; и все ваши операции деления, независимо от значения делителя, превратятся в банальным делитель на двойку. Но стоит только поделить на ноль, как вместо делителя (равного нулю) будет подставлена единица.

Не всегда и всюду нужна проверка, поэтому разработчики предоставили нам средство быстрого отключения функции проверки для заданных программных блоков — атрибут {attribute 'no_check'}. Его необходимо добавить в первую строку области объявления переменных, до строк PROGRAM, FUNCTION_BLOCK или FUNCTION. Тем более, что использование автопроверки вызывает дополнительную нагрузку на процессор.


Оценка производительности


Справочная система предупреждает о дополнительной нагрузке на ПЛК при использовании функций проверки деления (а также функций проверки на выход за пределы диапазона или проверки адреса указателя). Давайте измерим эту нагрузку. Для этого я написал специальный тест производительности:

FOR c := 1 TO 1000000 DO
    a := 12 / b;
END_FOR

Управлять нагрузкой будем с помощью количества циклов FOR: от 100 000 до 1 000 000 (столбец A). Непосредственно нагрузку подсмотрим в закладке Online раздела SYSTEM. Всего необходимо рассмотреть три различных случая:
  1. Без контроля деления на ноль, выставлен атрибут "no_check" — зеленый столбец B.
  2. С включенным контролем деления на ноль — желтый столбец C.
  3. С включенным контролем деления на ноль и, через установку переменной b := 0, имитируем деление на ноль в каждой итерации цикла — красный столбец D. Предполагается максимальная нагрузка.
И сразу результат. По вертикальной оси гистограммы отражена нагрузка на процессор ПЛК в процентах (80% — заданный мною потолок для TwinCAT):