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 работает быстрее и легче.