Showing posts with label TwinCAT 3. Show all posts
Showing posts with label TwinCAT 3. Show all posts

August 23, 2018

Очень длинный кейс

В продолжение темы экономии на абстракциях, я заметил необычное поведение отладчика TwinCAT 2 при пошаговом прохождении оператора CASE.

Программист, используя оператор CASE, заменяет спагетти из "ифов" (IF's) на красивую таблицу из структурированных блоков кода. Выполнение этих блоков зависит от значения одной единственной переменной. Именно так мы строим машину состояний, конечный автомат или просто разбиваем большой код на небольшие куски кода.

Мы ожидаем, что при входе в кейс, будет прочитано значение переменной, затем будет выполнен шаг с номером, хранящимся в этой переменной. Под дебагером же происходит нечто странное: происходит вход в кейс, а затем отладчик проходит по каждому шагу кейса, пока не дойдет до заданного шага и здесь, ожидаемо, начнет выполнять блок кода. Остальные участки кода он слава богу игнорирует, в том числе и те, что лежат условно "ниже" текущего шага (по тексту программы), но что если у нас кейс состоит из 1000 шагов? А то, что он проскачет по всем шагам, пока не достигнет нужного, и мы уже видели, что даже пустые вызовы подпрограмм, налагают дополнительную нагрузку на процессор ПЛК. Это поведение стоит проверить.

(* Я вынес анимированные иллюстрации в конец поста, чтобы не мелькали перед глазами. *)

Так как замерять производительность нужно на большом количестве вызовов и написать пару тысяч строк мне сложно и просто лень, да и времени на это жалко — я накидал программу на C#, которая генерирует другую программу, но уже на ST. Остается только скопировать из сгенерированного текстового файла output.txt код ST-программы, а затем вставить его в проект.

using System.IO;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var writer = new StreamWriter("output.txt")) {
                writer.WriteLine("CASE i OF");

                for(int i = 0; i < 65000; i++)
                    writer.WriteLine($"{i}: c := c + 1;");

                writer.WriteLine("END_CASE");
            }
        }
    }
}


Выводы

  1. Количество шагов оператора CASE никак не влияют на производительность. Во время выполнения программы, при входе в оператор CASE, управление сразу же передают в текущий шаг кейса. Никаких посторонних прыжков не происходит. Это исключительно заморочки режима отладки, причем, только в TwinCAT 2.
  2. Не надо писать гигантские кейсы из 100500 шагов — отладчик падает, а проект собирается два часа.
  3. В TwinCAT 3 все нормально и с отладкой, и с производительность. Отладчик непосредственно переходит к исполняемому в данный момент шагу, игнорируя остальные блоки. Но отладчик все равно не переваривает 100500 шагов кейса и падает.
  4. Удобство языка программирования ST в его текстовости и в том, что вы можете генерировать его программы с помощью других языков программирования: C#, VB, Python, JS, ... автоматизируя рутинные действия. Похожий прием я использовал в проекте Tc3_PcSpeaker для работы со звуковой-пищалкой x86 контроллеров. Только программа была написана на питоне. 

TwinCAT 2




TwinCAT 3



August 22, 2018

Экономия на абстракциях

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

Что если разбивка на множество подпрограмм привносит дополнительную нагрузку на процессор ПЛК и снижает быстродействие?

Быстродействие важно и любые телодвижения в сторону абстракций, приводят к увеличению количества вызовов подпрограмм (функций, методов, других кусков кода). А ведь нам нужно успеть уложиться в миллисекунды базового времени (Base Time, в первую очередь), и только затем улучшить сопровождаемость (когда-нибудь потом, но лучше прямо сейчас).

Спагетти код ругают: все-таки трудно ориентироваться в бесконечной портянке неструктурированного кода непонятного назначения. Поэтому я хочу знать — имеет смысл экономить на абстракциях или я могу разбивать код на сколько угодно удобных мне частей?

Итак, исследуем влияние количества вызовов подпрограмм на загруженность процессора. Проверять TwinCAT 2 мне не очень-то и хотелось, но я проверил и не пожалел.

PROGRAM MAIN
VAR
    i: UINT;
END_VAR

FOR i := 0 TO 65000 DO
    PRG2();
END_FOR

(* ********************** *)

PROGRAM PRG2
VAR
    c : UINT;
END_VAR

;
// c := c + 1;
// c := c + 1;

Основная программа MAIN каждый цикл совершает ряд вызовов подпрограммы PRG2. Количество вызовов определяется количеством итераций цикла. Их максимальное число равно 65000 — как в примере выше. Так как нас в основном интересует нагрузка от ветвления программы, то подпрограмма PRG2 пустая. Точнее, она состоит из единственного оператора точка-с-запятой (;). Чтобы не было совсем банально, я разбавил подпрограмму парой строк со счетчиком (и это было правильным решением).

По вертикали (Y) — нагрузка процессора в процентах, по горизонтали (X) — количество вызовов подпрограммы за время одного цикла:
Ничего необычного: чем больше вызовов, тем больше нагрузка на процессор. При этом нужно учесть два факта: первый — подпрограмма PRG2 ничего не делает и нагрузка растет только за счет количества передачи управления из одной программы в другую; и второй — тест синтетический и в реальных ситуациях такого количеств вызовов за один цикл не бывает.

Теперь проверим TwinCAT 3:
...и он показывает более линейную зависимость.

Теперь об аномалиях второй версии TwinCAT. В подпрограмме PRG2 я убрал пустой оператор ";" и раскомментировал одну строку со счетчиком. Зафиксировал количество вызовов на числе 65000. Замерил нагрузку. Раскомментировал вторую строчку со счетчиком (теперь их две). Замерил нагрузку и впал в задумчивость.

Итак, внимание! С одной строкой счетчика, нагрузка составила 72%. С двумя строками счетчика — 43%. Больше кода — меньше нагрузка! Объяснить такое поведение можно только работой некоего оптимизирующего звена в компиляторе кода, но такая неявность, явно вводит в заблуждение. Я немедленно проверил то же самое в TwinCAT 3...

...и он по прежнему оказался линеен и предсказуем: 1 строка — 56%, две строки — 58%.


Выводы

  1. Вызовы подпрограмм добавляют нагрузку на процессор.
  2. Я решил, что не буду обращать на эту нагрузку внимание, так как ощутимой она становится только при каких-то очень больших числах вызовов за время одного цикла. Даже для CX8xxx серии.
  3. TwinCAT 3 стабильнее и предсказуемей, чем его вторая реализация.
  4. Upd. 29 мая 2019 года, TwinCAT 2 v2.11.2302 странности с "меньше кода - больше нагрузка" не наблюдаются. Возможно был баг с выравниванием переменных в памяти. 


P.S.: посчитаем вызовы


А как вообще прикинуть — сколько вызовов происходит в проекте?

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

Для начала, открываем проект и экспортируем его в единый файл экспорта *.exp. Затем, открываем EXP-файл в текстовом редакторе (Notepad++) и автозаменой удаляем все вхождения следующих строк: (*STRING(, IF (. Файл в итоге будет испорчен, но для нас важнее исключить посторонние элементы. Остается сделать глобальный поиск символа открывающей скобки (, который традиционно используется при вызове подпрограмм. Как вариант, поискать закрывающие скобки, а точнее конец вызова функции, что-то похожее на );

Количество вхождений искомых символов, будет приблизительно равно количеству вызовов функций и подпрограмм. Часть совпадений придется на математические формулы, но их не так много. В то же время часть вызовов мы все равно не обнаружим, так как они скрыты внутри библиотек и в других трудно доступных местах. Одно компенсирует другое, и в среднем получится более-менее правдоподобно.


На картинке выше подсвечены не все скобки, но они учтены и подсчитаны правильно.

В итоге, я нашел 560 вызовов с открывающими скобками и 433 с закрывающими. Это в два раза меньше 1000 (1-2% CPU, как в пустой программе) и совсем далеко от 65000 (56-58% CPU). Еще раз, это максимум, который можно вызвать, но который никогда не будет достигнут: программа разбита на шаги (машина состояний), в каждом из которых выполняется лишь небольшая часть всей технологической программы.

February 15, 2018

Categories of TwinCAT Libraries

When you are ready to publish your library, it is a good idea to place it under the one or several categories in the library repository. Later, it helps you and other developers to find your library among the others at the local repository.

But there are no any official category lists – at least, I have not found any others one.

We can extract one or two (maybe more) categories from the one precisely selected library from the local storage at
x:\TwinCAT\3.1\Components\Plc\Managed Libraries. But we should choose carefuly because that source library should contain required categories. Or we can load category list from the "Description File" with *.libcat.xml extension. But (once more) I can’t find this description file – anywhere on local system or in the internet.

Then, I have made this category list myself.



I uploaded this unofficial list on GitHub. You can download latest version from beckhoff-tc31.libcat.xml or you can clone the whole project Tc_LibcatXml to your laptop or to your github repository or to any other place and finally make your library more shiny.

Категории библиотек TwinCAT 3

Предположим, вы разработали библиотеку или портировали чужую, типа библиотеки OSCAT под TwinCAT 3. Наступает ответственный момент оформления библиотеки, вы заполняете графы описания и решаете выбрать категорию библиотеки (Library Categories), чтобы в дальнейшем быстро находить библиотеку в локальном репозитории,

А списка категорий нет.

Можно экстрагировать категории из других библиотек: они лежат в x:\TwinCAT\3.1\Components\Plc\Managed Libraries, а можно загрузить из некоего файла (From Description File...) с расширением *.libcat.xml, но файла также нет. По крайней мере, для оборудования и библиотек Бекхофф, я их так и не нашел.

Поэтому выдавил из библиотек свой собственный и неофициальный список.



Список можно найти на гитхабе, он лежит в открытом виде и готов к использованию — Tc_LibcatXml. По возможности я буду поддерживать его в актуальном состоянии, либо должен найтись способ получать эти категории из официального источника. Сейчас же доступны три не официальные версии — какое-то наследие TwinCAT 2, затем устаревший TwinCAT 3 и актуальная на данный момент версия beckhoff-tc31.libcat.xml.

February 3, 2018

Spectre и Meltdown

После очередного обновления Windows была пропатчена на предмет уязвимостей Spectre/Meltdown. Им, если кратко, подвержены практически все современные процессоры(!) Именно процессоры, а точнее, их супер-современная архитектура. И пока все еще мутно относительно продукции AMD, с Intel и частично ARM-ом уже многое понятно.

На самом деле, все не так страшно, как трубили СМИ, да и производители операционных систем уже подсуетились и наставили заплат, зато просела производительность ЦПУ, но там интереснее другое. Нам интереснее другое, а дома решайте самостоятельно — отключать или нет.
Детектив о трех частях про уязвимости можно прочитать там: Meltdown // Spectre // часть третью ищите самостоятельно, там кажется про андроид-телефоны, что совершенно неинтересно для мира Total WINdows Control etc, но интересно для расширения кругозора.

Кстати, разобраться как вообще работают эти уязвимости — полезный перк в понимании работы компьютерного "железа".

Основная проблема в заплатках которые выкатили разработчики Windows. Так как уязвимость заключается в самой архитектуре процессора, то и патч задевает что-то важное на очень низком уровне операционной системы, а TwinCAT работает как раз там: на уровне драйверов и системных вызовов.

Кратко: проблема касается только рабочего режима 64-разрядного рантайма TwinCAT 3 на полных версиях Windows. Как только вы попытаетесь запустить локальный 64-разрядный рантайм TwinCAT 3 — тут же отхватите упаковку глюков, багов и синих экранов.

Проблема не касается:
  • Windows CE (Compact).
  • 32-x разрядных версий полноценной Windows.
  • В режиме конфигурирования (синий значок).
  • В инженерных версиях независимо от разрядности.

В общем, как обычно — отключаем этот патч и ждем очередную версию TwinCAT, где обещают решить проблему совместимости. Не нужно отключать обновления операционной системы, оставьте все как есть, просто отключите патч — об этом читайте дальше.

ЗЫ: Проблемы с запуском действительно проявляются. Проверено на живых человеках (мы бережем природу и животных).


Отключаем эти обновления совсем


Beckhoff Security Incident Team предлагает нам скачать официальный патч из спасательного набора WinPE_DeploySkript (257Мб). Распакуйте его куда-нибудь и запустите \WinPE_DeployScript\WinPeX64_Basic\media\patch.cmd от имени Администратора (правой кнопкой → запустить во имя Администратора).

Перезагрузитесь.

Аналогичное можете сделать вручную через редактор реестра и погуглив пару-тройку минут. Если же синий экран смерти все-таки настиг вас, создайте загрузочную флэшку с помощью \WinPE_DeployScript\CreatePE.bat и загружайтесь с нее. Далее скрипт patch.cmd, про который было выше.

September 27, 2017

Расширение UNION

Давно хотел попинать полумертвое существо UNION, которому вообще сложно найти красивое и практичное применение, но тут в тему появилась статья Стефана Хеннекена о возможности расширять UNION через наследование в TwinCAT 3.

Итак, тип UNION — это как бы структура, но не она. Все ее элементы занимают одно адресное пространство, поэтому записывая данные в один элемент, мы перезаписываем целиком или частично данные остальных элементов. В структурах эти элементы называются полями, здесь же сложно использовать термин "поле" потому что адресное поле по сути — одно единственное, а переменных несколько и они могут быть разного типа и разного размера — что, впрочем, тоже является одной из ключевых фишек этого типа данных.

Можно пытаться убедить не применять UNION, что он устарел и больше не нужен, но мы по прежнему одной ногой в эмбедед, где все-таки приходится иногда экономить память (первое применение), использовать трюки с преобразованием типов данных (второе применение) и где только-только появилось ООП (привет CoDeSys 3 и TwinCAT 3).


Расчленение данных


TYPE ST_Word:
STRUCT
    Lo : BYTE;    (* младший байт *)
    Hi : BYTE;    (* старший байт *)
END_STRUCT
END_TYPE

TYPE U_Word:
UNION
    Bytes : ST_Word;
    Value : WORD;
END_UNION
END_TYPE

Объявив переменную как UNION, мы получили структуру, внутри которой два элемента (Bytes и Value) хранятся в одном адресном пространстве. Записывая, что-либо в переменную Value мы также записываем эти данные и в Bytes. Верно и обратное — записывая данные в поля структуры Bytes : ST_WORD мы одновременно заполняем данными переменную Value.

Это очень похоже на синонимы (ALIAS), но с разными типами данных, и на самом деле никакой одновременности записи в разные переменные здесь нет — просто данные лежат в одном месте и разные пересекающиеся части этого места называются по разному. К сожалению, построчная запись в объявлении UNION часто сбивает с толку разработчиков, и они забывают, что данные переменных хранятся в ячейках памяти, и контроллер работает с ячейками памяти, а не с какими-то там именами человеческих переменных.

PROGRAM MAIN
VAR
    yourWord : U_Word;
    hi       : BYTE;
    lo       : BYTE;
END_VAR

yourWord.Value := 16#AABB;
hi := yourWord.Bytes.Hi;    // == 16#AA
lo := yourWord.Bytes.Lo;    // == 16#BB

Записываем в поле Value 16-разрядное число, которое накладыватся на 16-разрядную структуру Bytes : ST_WORD, содержащую два байтовых поля. Таким образом записанное значение разбивается на две однобайтных части: старший байт и младший байт. И никаких битовых операций для доступа к байтам WORD.

В TwinCAT 3 появился механизм наследования, который мы можем применить к UNION:

TYPE U_Word EXTENDS ST_Word:
UNION
    Value : WORD;
END_UNION
END_TYPE

Код становится лаконичнее, так как отпала необходимость в указании промежуточной структуры Bytes:

PROGRAM MAIN

yourWord.Value := 16#AABB;
hi := yourWord.Hi;    // == 16#AA
lo := yourWord.Lo;    // == 16#BB


Одно поле

Картинка лучше тысячи слов. Возьмем скарпель указателей и пройдемся по плитке памяти:



Адреса элементов Value, Lo и адрес в pYourWord (это указатель на UNION) совпадают, так как они расположены в начале области памяти выделенной под UNION. Указатель конечно же не расположен, а ссылается, но нас интересуют адреса, а не механизм работы. Теперь сравните адреса ячеек Lo и Hi (pYourWord_Lo и pYourWord_Hi, соответственно). Они различаются ровно на один байт, так как каждая из них занимает ровно один байт, а расположены они последовательно друг за другом.

Если этого недостаточно, то вот вам код:

PROGRAM MAIN
VAR
    oneWord         : WORD;
    yourWord        : U_WORD;
    
    pYourWord       : ULINT;
    pYourWord_Value : PVOID;
    pYourWord_Lo    : LWORD;
    pYourWord_Hi    : LWORD;

    wordLo          : BYTE;
    wordHi          : BYTE;
END_VAR

pYourWord       := ADR(yourWord);
pYourWord_Value := ADR(yourWord.Value);
pYourWord_Lo    := ADR(yourWord.Lo);
pYourWord_Hi    := ADR(yourWord.Hi);

oneWord         := yourWord.Value;

MEMCPY(
    ADR(wordLo), (* <-- *) pYourWord_Lo,
    SIZEOF(wordLo));

MEMCPY(
    ADR(wordHi), (* <-- *) pYourWord_Hi,
    SIZEOF(wordHi));

Вместо типов LWORD и ULINT здесь лучше использовать POINTER TO U_WORD и POINTER TO BYTE, но мне хотелось убедиться, что указатели теперь 64-х разрядные и что для хранения адресов подходят 64-х разрядные типы: LWORD, ULINT, PVOID. Последний, кстати, как и обещали в TwinCAT 3 стал 64-х разрядным.
PVOID — это синоним для UDINT в TwinCAT 2 (32-х разрядные адреса) или для ULINT в TwinCAT 3 (64-х разрядные адреса). Всегда используйте его как универсальное хранилище адреса, так как независимо от версии TwinCAT, вы всегда получите переменную с правильным типом данных. Вот еще один пример правильного использования ALIAS.
Заодно посмотрите на смертельно опасную функцию MEMCPY, позволяющую писать куда угодно в память.


Другие применения UNION


Экономим память — храним данные разных типов в одном месте. Главное, точно знать какие данные сохранены в текущий момент времени и вовремя их перезагружать.


Реинтерпретация типа данных


Мы хотим считать, что вот тот тип данных сейчас не REAL, а DWORD. Например, чтобы без лишних преобразований пересылать его через Modbus:

TYPE U_Real :
UNION
    Re : REAL;
    Dw : DWORD;
END_UNION
END_TYPE

Для работы используем поле Re, а для отправки данных Dw. И никакого преобразования данных, все происходит автоматически.


Variant


Или когда мы не знаем какой тип данных будет использован во время работы программы. Этот же прием можно использовать для создания псевдо-полиморфизма в TwinCAT 2.

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

TYPE U_VarObject :
UNION
    AsInteger  : INT;
    AsFloat    : REAL;
    AsDouble   : LREAL;
    LikeString : STRING;
END_UNION
END_TYPE

Затем описываем (перечисляем) доступные типы данных:

TYPE E_VarType :
(
    Integer,
    Float,
    Double,
    String255
);
END_TYPE

Упаковываем все это в структуру:

TYPE ST_Variant :
STRUCT
    TypeIs : E_VarType;
    Value  : U_VarObject;
END_STRUCT
END_TYPE


Работает как-то так:

PROGRAM MAIN
VAR
    xVar    : ST_Variant;

    Counter : INT;
    Text    : STRING;
END_VAR

CASE xVar.TypeIs OF
    E_VarType.Integer:
        Counter := xVar.Value.AsInteger;

    E_VarType.Float:
        ;

    E_VarType.Double:
        ;

    E_VarType.String255:
        Text := xVar.Value.LikeString;
END_CASE


Синонимы


Очень похоже на вычленение данных, но будем использовать по принципу ALIAS:

TYPE ST_Vector3R:
STRUCT
    X : REAL; 
    Y : REAL; 
    Z : REAL; 
END_STRUCT
END_TYPE

TYPE U_Vector3R EXTENDS ST_Vector3R:
UNION
    E : ARRAY [1..3] OF REAL;
END_UNION
END_TYPE


Теперь координаты вектора доступны для нас как по именам, так и по индексам:

PROGRAM MAIN
VAR
    Vec : U_Vector3R;
    i   : UINT;
END_VAR

FOR i := 1 TO 3 DO
    Vec[i] := i * 100;
END_FOR

Vec.Y := 0.0;

July 4, 2017

Вебинар. Что нового в сборке №4022

Некоторые новинки сборки TwinCAT №4022 я перечислял в стратегиях развития, но это было не точно и на уровне слухов. 27 июня прошел официальный вебинар TwinCAT 3.1 | New features in Build 4022, на котором Йозеф Папенфот (Dr. Josef Papenfort) подробно рассказал о новых и значимых функциях введенных в систему.

Все новые фишки более-менее перечислены на официальном сайте, но это скорее официальный бюллетень, нам же будут интересны некоторые подробности. Для начала, скачиваем с официального сайта Бекхофф новую сборку 3.1.4022.0: TwinCAT 3.1 – eXtended Automation Engineering (XAE). Один год — один новый релиз, но в этом билде только небольшие исправления и обновления.

Доктор официально подтвердил, что начиная со сборки 4020.x начата поддержка Windows 10 Redstone 4020.x. Поддержка Redstone 2 ожидается в середине 2017 года. Печальная же новость, что начиная с билда 4022 заканчивается поддержка всеми любимой Windows XP и WES 2009 (Windows Embedded Standart), то есть 4022 на них работать уже не будет.


Архив проектов


Что было удобно в TwinCAT 2, так это два файла на целый проект: программа в .pro-файле и конфигурация в .tsm. В то же время это было не удобно для систем контроля версий, так как файлы были бинарные — зато почтой отправлять удобно. Чтобы как-то скомпенсировать эти проблемы, был введен новый формат решения (solution, солюшен) — архив .tnzip. Внутри — это обычный архив ZIP, только расширение подлиннее.

Новый формат позволяет сжимать один или несколько проектов TwinCAT 3 в единственный файл архива. И сразу же отправить по почте: для этого есть специальный пункт меню. Впоследствии такой архив можно открыть как обычное решение (solution), то есть вот прямо сразу из архива, ничего не распаковывая.


Я поархивировал немного и выяснил, что в архиве хранятся только проекты TwinCAT. Все посторонние проекты, относительно TwinCAT (например, проект на C#), система игнорирует и в архив не включает. Зато архивы реально сжатые, сравните на картинке ниже размер "до" (Size) и размер "в" (Packe...)


Полезные мелочи


Постепенно улучшается редактор кода: добавлено раздельное масштабирование шрифтов в редакторе кода ST и окне объявления переменных. Для улучшения восприятия, появились специальные атрибуты создающие схлопывающиеся регионы — это кусок кода между специальными метками {region}

{region 'название для региона'}

... кусок кода

{endregion}

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

В CFC-рисовалке блоков появилось автодополнение кода (IntelliSense), а в менеджере библиотек — специальная иконка, подсказывающая когда плейсхолдер куда-то там переместился и теперь указывает на другую версию библиотеки.


Переменные ввода-вывода


Переменные ввода типа AT %I* (location variables) теперь недоступны для изменения из кода: bInputVar := NOT bInputVar — запрещено на уровне компилятора и при сборке проекта приведет к ошибке компиляции. Зато теперь, начиная с билда 4022, эти переменные могут получать начальное значение: bInputVar AT %I* : INT := 123;

Из-за таких вот нововведений, функция __ISVALIDREF теперь работает только со ссылками. Про ссылки и указатели уже было, но там не было про эту функцию, которая подсказывает существует ссылка или нет:

<результат типа BOOL> := __ISVALIDREF(переменная типа REFERENCE TO <тип данных>);

TRUE — ссылка рабочая, FALSE — ссылка нулевая, подробнее читайте в справочной системе.

Соответственно REFERENCE TO на переменные AT %I* теперь тоже запрещены, так как изменение этих переменных запрещено и такая попытка будет пресекаться ошибкой компиляции.


ADS через MQTT


Поддержка реализована как .dll модуль для Mosquitto Broker. Уже есть как 32-х разрядная так и 64-разрядная версии. Модуль поставляется бесплатно, вместе со стандартным установщиком и лежит в каталоге AdsApi:


Проект-одиночка


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

Не обязательно так делать всегда, просто учтите, что они теперь так могут, и в сложных проектах это будет просто спасение утопающему в строках кода, но вы все еще можете засунуть в конфигурацию ПЛК-программу. Просто теперь есть выбор:


Проекты связываются разработчиком друг с другом через .tmc-файл. Причем в одну конфигурацию можно засунуть несколько программных проектов, а один программный проект можно использовать в нескольких разных конфигураций: не скопировать одну программу в несколько конфигураций, а просто прилинковать проект посредством .tmc-файла.

При таком подходе, изменяя ПЛК-проект в одном месте, мы будем изменять его во всех конфигурациях одновременно. Главной при этом не забыть пересобрать ПЛК-проект: Build → Build Solution (F7), и активировать конфигурацию в TwinCAT-XAE-проекте: Activate Configuration.

Такой принцип чем-то схож с использованием библиотек, но только пишем мы не библиотеку (Empty PLC Project), а обычную программу (Standart PLC Project).



Символьный маппинг


Теперь можно обновлять ПЛК-проект независимо от конфигурации, а новый маппинг переменных несовместим со старым. Переменные привязываются друг к другу с помощью символьной информации (грубо говоря — по именам). Прошивка контроллера должна быть новой и поддерживать.

Список системных типов данных переехал из закладок SYSTEM напрямую в дерево проекта и получил отдельную ветку с неожиданным названием Type System. Теперь туда с помощью заклинания Add New Item... и такого-то .tmc файла можно добавить распределенные типы данных (Shared Types), которые затем можно оттранслировать через EAP по TCP/IP сетям. Или через системы контроля версий, или вообще по любым каналам. Электронная почта все еще работает.


TwinSAFE


На диаграммах отображается состояние TwinSAFE-группы: в случае ошибки вы увидите толстую красную рамку, и вообще появились другие цвета, отражающие состояние входов/выходов и другие состояния. Плюс мелочи в редактировании схем безопасности: копи-паста функциональных блоков или псевдонимов (Alias Devices), разбивка на подкаталоги для псевдонимов устройств.


Книга жалоб и предложений


В XAE встроили специальный пункт меню для отправки письма сразу в тех. поддержку Бекхофф. При нажатии, появляется готовая форма, которую нужно заполнить. Причем ведет форма себя очень странно, так как доступна только из TwinCAT Measurement Project (это который про рисование графиков) и кликнуть нужно обязательно по ветке проекта Scope XY Project.



Монитор ADS


Бесплатный, но качается и устанавливается отдельно с FTP-сайта Бекхофф: TF6010-ADS-Monitor. После установки будет лежать здесь: C:\TwinCAT\Functions\TF6010-ADS-Monitor\Viewer\TcAmsAdsViewer.exe

С помощью монитора можно смотреть на пакеты ADS, иногда отправляя свои собственные. Аналогичное можно получить из WireShark, только здесь фильтры настраивать не нужно.



Установка


В остальном билд №4022 установился без проблем на Windows 10 Home. Если собираетесь запускать контроллер на локальной машине разработчика (ноутбук на котором программируете), то после установки запустите от имени "Администратора" батник: C:\TwinCAT\3.1\System\win8settick.bat и перезагрузите компьютер.

February 9, 2017

Указатель на ФБ

Указатели — это прямой путь в память контроллера. Путь — ничем не ограниченный и крайне опасный: одно не верное движение и вот вы уже в неразмеченной области памяти, отстреливаете себе ногу, вызывая исключение Page Fault! PLC is stopped. Что делать и как быть?

Начнем с простого:

PROGRAM MAIN
VAR
    A  : INT;
    B  : WORD;
    
    pA : POINTER TO INT;
    pB : POINTER TO WORD;
    dw : DWORD;
END_VAR

[...]

pA  := ADR(A);
pA^ := -1;


Переменная А в результате будет равна -1. При попытке сделать аналогичное с переменной B — pB^ := -1; получим ошибку компилятора еще на этапе сборки проекта. Объяснение простое — мы объявили pB как указатель на целый и всегда положительный тип WORD, а пытаемся пропихнуть число со знаком, типа INT. Компилятор бдит.


Выход за границу


Вообще, указателям можно присваивать все, что угодно, лишь бы справа был POINTER TO или DWORD (UDINT):

dw  := ADR(B);
pB  := dw;
pB^ := 123;

ADR возвращает число типа DWORD — это адрес переменной. Поэтому можно присвоить этот адрес указателю pB, а затем разыменовать указатель с помощью оператора ^ и присвоить новое значение для указанной переменной B. После всех действий B будет равен 123.

Оператор ^ применим только к указателям. Нельзя разыменовать переменную другого типа: dw^ := 123; — получим ошибку: '^' needs a pointer type. Поэтому хранить адрес можно в обычных переменных, но работать с адресами получится только через указатели.

Добавим перца — указателю можно присвоить любой адрес или любой другой указатель, указывающий на произвольный тип:

pA  := pB; (* pA теперь указывает на переменную 'B' типа WORD *)
pA^ := -2;

Результат: B = 65534 и ошибки здесь нет. Мы объявляли pA, как указатель на целое со знаком, то есть переменную типа INT, а переменная B — это целое беззнаковое, поэтому бит знака превратился в значимый разряд и дальше бла-бла-бла...

До сих пор у нас совпадал размер переменных — обе занимали ровно два байта. Что будет, если мы сделаем так:

A   : INT;
B   : BYTE;

[...]

pA  := pB; (* pA теперь указывает на переменную 'B' типа BYTE *)
pA^ := 1234;

B = 210;

...и опять без ошибки, но она может легко возникнуть, так как мы уже вышли за пределы переменной: 1234 занимает в памяти два байта, а мы записываем это число в переменную B типа BYTE, длиной один байт, как нам сообщает Капитан Очевидность. Таким образом легко совершить целый ряд безобразий: вылезти за пределы переменной или массива, вызвать сбой при обращении к странице памяти, остановить контроллер и технологический процесс. Завод встал, рабочие с факелами идут карать Франкенштейна.


Указатель на код


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

Для этого эксперимента нам понадобится:
  • Глобальная переменная, значение которой будет изменяться функциональными блоками.
  • Два ФБ, по разному изменяющие содержимое глобальной переменной.
  • Два указателя, которые мы будем испытывать, сталкивая лбами.
  • Немного терпения разработчика.

А давайте как физики в эксперименте про квантовую телепортацию — назовем наши функциональные блоки "Алиса" и "Боб" (в алфавитном порядке):

VAR_GLOBAL
    g_Result : STRING := 'Хзкт';
END_VAR

[...]

FUNCTION_BLOCK Alice
VAR_INPUT
    Name : STRING; (* пригодится позднее, когда появится злой двойник *)
END_VAR

g_Result := 'Алиса';

[...]

FUNCTION_BLOCK Bob
g_Result := 'Боб';

[...]

PROGRAM MAIN
VAR
    Al  : Alice;
    Bo  : Bob;
    pAl : POINTER TO Alice;
    pBo : POINTER TO Bob;
END_VAR

[...]

pAl := ADR(Al);    (* Указатель на ФБ Алиса *)
pBo := ADR(Bo);    (* Указатель на ФБ Боб *)

pAl^(); (* ФБ можно вызывать через разыменованный указатель *)


Каждый из ФБ записывает в глобальную переменную g_Result соответствующее имя: в зависимости от того, чей экземпляр мы вызовем через указатель, мы получим разные имена в глобальной переменной. На этот раз, получим результат 'Алиса'.

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

pAl := ADR(Bo);  (* Указатель на ФБ Боб *)
pAl^();


И если вы подумали, что получился 'Боб', то вы не правы — g_Result по прежнему равно 'Алиса'. Система следит за типом указателя, не давая сменить его на неправильный, но делает это молча — никому, ни о чем не сообщая.

Давайте создадим второй экземпляр ФБ Alice и переведем указатель на него, то есть присвоим указателю указатель, но теперь правильного типа POINTER TO Alice:

VAR
    Al     : Alice := (Name := 'Alice Original');
    AlTwin : Alice := (Name := 'Alice Evil Twin');

[...]

pAl := ADR(Al);       (* Указатель на ФБ Алиса *)
pAl := ADR(AlTwin);   (* Указатель на ФБ близнеца Алисы *)
pAl^();


На этот раз, указатель изменился:



TwinCAT 3


Скопируем проект, с небольшими отличиями в объявлении глобальной переменной:

VAR_GLOBAL
    Result : STRING := 'Хзкт';
END_VAR

[...]

FUNCTION_BLOCK Alice
G.Result := 'Алиса';  (* аналогично для Боба *)

[...]
    
pAl := ADR(Al);       (* Указатель на ФБ Алиса *)
pBo := ADR(Bo);       (* Указатель на ФБ Боб *)

pAl := pBo;

pAl^();


В результате получим — 'Боб', то есть G.Result = 'Боб'. В TwinCAT 3 указатели... э-э-э, гибкие? изменчивые? динамические? отзывчивые?


Нечеловеческий эксперимент над роботами


Жуткий по сложности и непонятности эксперимент над указателями в TwinCAT 2:
  1. Ссылаемся указателем pA на функциональный блок Alice.
  2. Получаем адрес функционального блока Bo.
  3. Получаем группу:смещение указателя pA.
  4. Через функции ADS записываем (подменяем) адрес указателя pA.
  5. Разыменовываем указатель pA и вызываем функциональный блок.

Зачем это нужно? Если раньше мы могли предположить, что компилятор как-то там отслеживает все наши махинации при сборке проекта: контролирует и корректирует указатели, то теперь мы те же действия будем делать уже во время работы контроллера. Компилятор о них ничего не узнает:

PROGRAM MAIN
VAR
    ReadSymInfo : PLC_ReadSymInfoByName;
    SymInfo     : SYMINFOSTRUCT;
    WriteAds    : ADSWRITE;

    Al          : Alice;
    pAl         : POINTER TO Alice;
    Bo          : Bob;
    AddrBob     : UDINT;

    state       : UINT;
END_VAR

[...]

CASE state OF
0:
    pAl     := ADR(Al); (* 1 *)
    AddrBob := ADR(Bo); (* 2 *)
    state   := 100;

100: (* 3 *)
    ReadSymInfo(
        NETID   := '',
        PORT    := 801,
        SYMNAME := 'MAIN.pAl',
        START   := TRUE,
        SYMINFO => SymInfo);

    IF NOT ReadSymInfoByName.BUSY THEN
        ReadSymInfoByName(START := FALSE);
        state := 200;
    END_IF

200: (* 4 *)
    WriteAds(
        NETID   := '',
        PORT    := 801,
        IDXGRP  := SymInfo.idxGroup,
        IDXOFFS := SymInfo.idxOffset,
        LEN     := 4,
        SRCADDR := ADR(AddrBob),
        WRITE   := TRUE);

    IF NOT WriteAds.BUSY THEN
        WriteAds(WRITE := FALSE);
        state := 300;
    END_IF

300:
    pAl^(); (* 5 *)

END_CASE

В результате все равно получится 'Алиса' и это несмотря на то, что указатель, судя по адресу, указывает на 'Боба':



Предположения


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

В TwinCAT 3 завезли объектно-ориентированное программирование с классами, методами и, самое главное, интерфейсами. Последние как раз нуждаются в полиморфизме и гибких указателях (vtable). Поэтому с указателями в TwinCAT 3 все ожидаемо-хорошо.

И там, и там можно устроить крах системы, выйдя за пределы выделенной памяти. Осторожнее там с указателями!

February 2, 2017

События под максимальной нагрузкой

Насколько сильно нагружают процессор ПЛК асинхронные функции TwinCAT.ADS API? Стоит ли пользоваться подпиской на переменные ПЛК-задачи и сидеть в ожидании уведомлений об изменившихся данных или сразу написать polling-велосипед? Я уже измерял скорость передачи данных через ADS.API под .NET, теперь посмотрим как нагружают систему самые тяжелый асинхронные функции событийной модели.

TcAdsClient client = new TcAdsClient();
client.Connect("5.24.134.118.1.1", 801);


Имя для области памяти


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

PROGRAM MAIN
VAR
    a : WORD;
END_VAR

[...]

a := a + 1;
a := a AND 16#00FF; (* А *)

Наращиваем переменную MAIN.а типа WORD каждый цикл на единицу, а старшую часть обнуляем (* А *) чтобы она оставалась неизменной. Для начала подпишемся на всю переменную целиком и убедимся, что система работает.


Адрес переменной


На переменную можно подписаться через ее имя, а можно через адрес в памяти (индекс группы + смещение). И прежде, чем начинать подписываться, я хочу выяснить как мне узнать адрес переменной или хотя бы, где вообще лежат переменные ПЛК-задачи. Для этого воспользуемся функцией ReadSymbolInfo:

ITcAdsSymbol info = client.ReadSymbolInfo("MAIN.a");
Console.WriteLine($"Group:Offset = {info.IndexGroup}:{info.IndexOffset}");

Как результат получим адрес Group : Offset = 16448 : 0. В моем тестовом примере переменная в программе одна единственная, поэтому она расположена в начале области памяти, и поэтому ее смещение равно нулю.
В статье справочной системы "Index-Group/Offset" Specification of the PLC services можно найти упоминание, что память ПЛК-задачи начинается с адреса 0x00004040 = 16448.
Теперь, обращаясь через байтовые смещения, мы можем адресовать только часть переменной. Заменим имя этой переменной на адрес области памяти — Group: 0x4040, Offset: 0, длина 16 разрядов (тип ushort или WORD):

client.AdsNotificationEx += Client_AdsNotificationEx;
client.AddDeviceNotificationEx(0x4040, 0, AdsTransMode.OnChange, 0, 0, null, typeof(ushort));

[...]

private void Client_AdsNotificationEx(object sender, AdsNotificationExEventArgs e)
{
    Log(e.Value);
}


Система работает — как и было задумано: уведомления приходят в метод Client_AdsNotificationEx, значение переменной доступно через поле e.Value.


Часть вторая


Подпишемся теперь на младший байт переменной MAIN.a, то есть на ту половину переменной, которая увеличивается каждый цикл. Для этого мы изменим тип данных с ushort (16-разрядов — эквивалент WORD) на byte (8 разрядов):

client.AddDeviceNotificationEx(0x4040, 0, AdsTransMode.OnChange, 0, 0, null, typeof(byte));

Уведомления по прежнему приходят.

Теперь подпишемся на старшую половину переменной. Для этого достаточно увеличить смещение (Offset) на единицу, то есть перейти на один байт дальше:

client.AddDeviceNotificationEx(0x4040, 1, AdsTransMode.OnChange, 0, 0, null, typeof(byte));

Старший байт переменной искусственно обнуляется каждый цикл (* А *), поэтому старшая часть переменной остается неизменной. И всё — сообщения больше не приходят, кроме самого первого после подписки.
Так было задумано разработчиками — всегда приходит первое сообщение. До подписки мы не знали значение переменной, поэтому для нас оно было неопределенным. После подписки неизвестное значение сразу же может стать известным, поэтому система отправляет нам первое (и возможно единственное) сообщение о текущем значении переменной.

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


Подписываемся и замеряем


В качестве испытуемого возьмем CX9020. Начнем проверку с TwinCAT 2, а чуть позже сравним с третьей версией. Сейчас же идея такая: подписываемся на пару сотен переменных, а затем смотрим через System Manager нагрузку на процессор. Пишем код:

PROGRAM MAIN
VAR
    Arrgh : ARRAY [1..10000] OF WORD;
    i     : INT;
END_VAR

[...]

FOR i := 1 TO 1000 DO
    Arrgh[i] := Arrgh[i] + 1;
END_FOR


Подписываемся:

for (int i = 0; i < 200; i++)
    client.AddDeviceNotificationEx(0x4040, i * 2, AdsTransMode.OnChange, 0, 0, null, typeof(ushort));


Замеряем:


Очень много для программы, которая практически ничего не делает. Давайте попробуем приблизиться к заявленному пределу в 550 переменных (Асинхронные функции TwinCAT.Ads API):


Здесь 500 переменных. Результат ужасный.


Полл Шред


Не будем подписываться, будем опрашивать вручную.

[...]

Прошел час. Пропустим несколько итераций эксперимента и сразу рассмотрим опрос 10 000 переменных. Для этого нам просто необходим отдельный поток:

CancellationTokenSource cancelTokenSource;

private void StartButton_Click(object sender, EventArgs e)
{
    cancelTokenSource = new CancellationTokenSource();
    SynchronizationContext uiContext = SynchronizationContext.Current;
    Thread pollThread = new Thread(() => PollThread(cancelTokenSource.Token, uiContext));
    pollThread.Start();

[...]

private void PollThread(CancellationToken token, object uiContext)
{
    using (var client = new TcAdsClient())
    {
        client.Connect("5.24.134.118.1.1", 801);

        while (!token.IsCancellationRequested)
        {
            for (int i = 0; i < 10000; i++)
            {
                if (token.IsCancellationRequested)
                    goto EndOfThread;

                ushort a = (ushort)client.ReadAny(0x4040, i * 2, typeof(ushort));
            }

            Thread.Sleep(0);
        }

        EndOfThread:;
    }
}


Старт. Результат:



Это был регулярный опрос 10 000 переменных за раз. Если понравилось — давайте попробуем регулярно считывать целиком весь массив. Для этого заменим цикл for на чтение массива целиком:

ushort[] buf = new ushort[10000];
buf = (ushort[])client.ReadAny(0x4040, 0, buf.GetType(), new int[] { buf.Length });


Результат аналогичный — нагрузка не превышает 20%.


Грузите апельсины бочками


Асинхронные функции сильно нагружают систему контроллера. Для старших моделей ПЛК слово "сильно" можно заменить на "значительно", но легче от этого не станет. Тем не менее, если нужно следить за десятком переменных, то можно обойтись асинхронными функциями. Если же переменных много, то есть два выхода: сделать опрос вручную или объединить переменные в структуру/массив.

Рассмотрим второй вариант. Я изменил код и подписался сразу на целый массив из 10 000 ячеек типа WORD. Теперь, чтобы пришло сообщение, достаточно изменить значение только одной ячейки массива (или одного поля структуры, если мы объединили переменные в структуру):

ushort[] buf = new ushort[10000];
client.AddDeviceNotificationEx("Main.Arrgh", AdsTransMode.OnChange, 0, 0, null, buf.GetType(), new int[] { buf.Length});


Результат:



Вывод: перед отправкой упаковывайте данные в структуры и отслеживайте их как единое целое.


TwinCAT 3.1


В третьем твинкате все замечательно — можно смело выкинуть огорчения из предыдущих пунктов и смело использовать подписку на переменные. И хотя нагрузка на ПЛК снизилась в разы, асинхронные функции по прежнему дают ощутимую нагрузку на процессор ПЛК.

Подписываемся на 1000 переменных (контроллер тот же — CX9020):



Но(!) по прежнему нельзя подписаться на 10 000 переменных — большое количество подписок приведет к ошибке: "Ads-Error 0x751 : A hashtable overflow has occured". Это внутренняя ошибка переполнения словаря переменных в библиотеке TwinCAT.Ads, то есть по прежнему имеем ограничение на количество переменных. Мелочь, а неприятно.


Замыкаем


Пора найти плюсы и минусы и замкнуться в сферах применения:

Подписка на события (событийная модель):
+ Удобство использования: методы обработки данных вызываются только тогда, когда переменная изменилась.
+ Регулярность сообщений не зависит от программы на ПК.
+ Снижение нагрузки на сеть: обменом данными занимаются AMS-роутеры, которые отправляют только измененные данные.
– Ограничение на частоту уведомлений (не быстрее 1 миллисекунды).
– Ограниченное количество переменных: 550 штук — TwinCAT 2, несколько тысяч в TwinCAT 3.
– Чем больше переменных, тем выше нагрузка на процессор контроллера.

Регулярный опрос (поллинг):
+ Неограниченное количество переменных.
+ Низкая нагрузка на процессор ПЛК.
+ Максимальная скорость обмена данными (до 50 мкс, быстрее TwinCAT просто не умеет).
– Требуется вручную обмениваться и обрабатывать значения переменных.
– Дополнительная нагрузка на сеть. Запросы и ответы ходят по сети регулярно, независимо от того — изменились данные или нет.
– Дополнительная нагрузка на программу ПК и сложность в синхронизации данных в многопоточном приложении.

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

И в любом случае TwinCAT 3 работает быстрее и легче.