четверг, 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 указатели... э-э-э, гибкие? изменчивые? динамические? отзывчивые?


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


Жуткий по сложности и непонятности эксперимент:
  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

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



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


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

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

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

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

вторник, 31 января 2017 г.

Вебинар. Технологии измерений на базе аналоговых модулей

24 января этого года, Мартин Подрушек (Martin Podrouschek) рассказал про аналоговые модули Бекхофф, а затем обещал в следующем вебинаре рассказать про то, как их конфигурировать и наблюдать на цифровом осциллографе в их естественной среде обитания.

Изображение: beckhoff.com


Когда-то давно была подборка физических интерфейсов и модулей расширения, а сейчас мы узнаем некие подробности об аналоговых модулях (как входных, так и выходных). Также можно ознакомиться с флаером Extremely accurate, fast and robust: High-end measurement technology from Beckhoff, где есть более-менее подробное изложение вебинара (правда, на английском языке).


Программная часть


Программная часть, отвечающая за обработку и настройку аналоговых модулей, условно делится на несколько частей:
  • Scope View|Server — цифровой осциллограф клиент/сервер, бесплатный в базовой поставке.
  • Bode Plot — ЛАФЧХ, диаграммы Боде.
  • Analytics — более комплексный анализ данных в совокупности и за большой промежуток времени, со сбором данных в "облаке" и т. п.
  • Configuration — настройка модулей.
  • Filter Designer — настройка цифровых фильтров Баттерворта, Чебышева, НЧ, ВЧ, и т. п. Можно настраивать параметры и рисовать кривые фильтров, затем загружать их в модули.

Ниже и ближе к железу, лежит слой обеспечения реального времени, про который мы должны помнить, что он работает с заданной цикличностью и ни микросекундой больше или меньше. Это важно помнить и знать, особенно, относительно измерительной аналоговой техники, где придется много раз делать преобразования из аналога в цифру и обратно.
Аналоговые величины существуют как бы в двух измерениях — это дискретизация по времени (TwinCAT обеспечивает синхронизацию между устройствами с точностью до ±100нс) и значение измеряемой величины. Важно помнить об этом, так как точность измерения определяется по двум координатам.
Еще ниже (но не всегда) есть дополнительные библиотеки-драйверы, которые обеспечивают работоспособность специализированных модулей. Например, библиотеки Energy Library для модулей измерения параметров электроэнергии (Energy Measurement) — модули серии EL3xxx, KL3xxx или библиотека CM library контроля состояния (Condition Monitoring) — IEPE акселерометры и другие датчики.


Пошаговый выбор модулей


Сейчас в каталоге доступно порядка 100 модулей. Чтобы не потеряться среди них, предлагается пошаговый подход к выбору.

После определения какой функционал требуется от модуля, можно выбрать тип защиты корпуса:
  • IP20 — для установки в шкафах автоматизации. Серии BK, BC, EL, KL, EM, KM, ES, JS, IPC|CX|CP.
  • IP67 — для установки под открытым небом, дождем и снегом. Серии: EP, EPP, EQ, ER, IP.

Дальше интересней, так как Beckhoff по сути не настаивает на обязательном использовании EtherCAT для подключения модулей, а предлагает на выбор различные полевые шины: Profibus, Profinet, Sercos II, много еще чего, ну и EtherCAT конечно же. Для всего этого зоопарка шин существуют соответствующие коплеры (coupler), объединяющие модули в удаленные или локальные группы.

Дальше необходимо определиться с аккуратностью или погрешностью измерения (±%) и пропускной способностью (100Гц..100кГц).

Также важным параметром являются тайминги и частота дискретизации:
  • XFC — модули умеют делать оверсамплинг, что повышает точность, за счет интерполяции дополнительных отсчетов между тиками системы реального времени. Только для шины EtherCAT.
  • DC — поддержка синхронизации распределенных часов.
  • Timestamp — фиксируется ли отпечаток времени события (важно, так как измерение может произойти в произвольный момент длительности цикла).

Также, при необходимости, можно посмотреть на:
  • фильтры входных сигналов;
  • выбор в пользу многофункциональных модулей, умеющих измерять разные величины. В настройках можно выбрать, что именно модуль будет измерять: напряжение, ток, мощность, и т. п.;
  • требуемые сертификаты на калибровку, допуски и другую технику безопасности и бюрократию.


Быстродействие и точность


Не стоит считать высокоточными — модули с высокой разрядностью оцифровки величины (hi resolution). 16 или 24 бит вовсе не означает, что эти модули точнее. Просто не имеет смысл проводить точные измерения, а затем терять эту точность из-за малой разрядности оцифровки. Выбирая модули, учитывайте обе величины — и разрядность, и погрешность измерения.

Во втором квартале 2017 года должна выйти новая серия модулей ELM в железном корпусе и для сверхточных измерений:
  • 24 разряда;
  • до 50 000 отсчетов в секунду;
  • синхронизация с точностью до ±100нс;
  • интегрированная диагностика подключений и работы функционала.

Серия модулей для шины K-bus пока еще существует — это модули KL3xxx. Следует учитывать, что из-за ограничений шины K-bus, эти модули не могут обеспечить высокое быстродействие.


Полный вебинар на английском языке EtherCAT Terminals | Measurement technology with analog input terminals. Продолжение следует...

четверг, 26 января 2017 г.

Задание положения на лету

Можно ли сбросить значение текущего положения оси "на лету", во время ее движения? Не вбросить новое задание на выполнение (это совершенно другая задача "контурного режима"), а просто сказать, что в данный момент, вот это место — это позиция 3 200 851 или 0, или -600, или любое другое число.

Можно, хотя всё говорит об обратном. System Manager и XAE отказываются, сообщая: "Axis function is not allowed in motion!", — это означает, что функция недоступна во время движения.



Справочная система говорит, что для задания позиции "на лету" предназначен относительный режим (Relative), но ничего не говорит об абсолютном (Absolute) режиме. А ведь, на самом деле, всё работает, правда при условии, что вы делаете это из ПЛК-задачи и с помощью функционального блока MC_SetPosition.
Если есть вероятность что ваша программа будет работать на более ранних версиях TwinCAT, то лучше довериться System Manager и считать, что нельзя задавать позицию "на лету". Для большей уверенности стоит проверить, так как я проверял только на версиях 2.11.2254 и 3.1.4020.

Итак, сбросить позицию "на лету" в произвольное значение во время движения — можно. Рассогласование при этом не возникнет (хотя справочная неуверенно заявляет о возможности обратного). Что еще нужно знать о функции MC_SetPosition?

Существует два режима, передаваемые через параметр Mode. Абсолютный режим (Mode = FALSE) просто задает новое текущее значение позиции. Относительный же (Mode = TRUE) — задает смещение от текущего значения позиции. Например, если в относительном режиме задать отрицательное число — позиция уменьшится на заданное число, если положительное — увеличится.

В будущих версиях библиотеки (относительно версии 3.02.52 от 19.10.15) существует вероятность замены имени параметра Mode на Relative. Пока же это название остается прежним из соображения совместимости.

Когда-то давно, теперь уже в далеком 2005 году, функция MC_SetPosition называлась MC_SetActualPositionOnTheFly и ответ был сразу же понятен из ее названия.

вторник, 24 января 2017 г.

Питание энкодерной оси

Нужно ли активировать питание энкодерной оси (Encoder Axis) с помощью функционального блока MC_Power?



Нет, не нужно. Достаточно просто регулярно читать статус переменной, связанной с осью движения. Следующего ― достаточно:

PROGRAM MAIN
VAR
    EncoderAxis : AXIS_REF;
END_VAR

(* Обновляем информацию об оси движения *)
EncoderAxis.ReadStatus;

(* Работаем с текущей позицией *)
IF EncoderAxis.NcToPlc.ActPos > 0 THEN
[...]
Стоит учитывать, что без вызова метода ReadStatus, автоматически обновляются только статусные поля структуры NcToPlc (поля, отвечающие за состояние оси). Поля, содержащие позицию, скорость и т. п., обновляться без вызова ReadStatus не будут. Это также относится и к обычным (Continuous Axis) приводным осям.
Делаем предварительный вывод ― энкодерная ось отличается от обычной приводной (Continuous Axis) только тем, что преобразует параметры движения в цифру, но не осуществляет управление. Следовательно, силовое питание энкодерной оси не требуется.

― Хорошо, а что будет если все-таки задействовать MC_Power? Сможет ли он запретить вращение в обратную сторону или что-нибудь подобное?
― Нет, не сможет. Энкодерная ось всегда находится в неподвижном состоянии. Поля State.HasBeenStopped и .StandStill всегда равны TRUEState.MotionState также всегда равно MC_AXISSTATE_STANDSTILL. Теперь мы знаем, что MC_Power активирует и контролирует только управление движением (то есть выход), но никак не влияет на входные данные.

Что еще полезного в структуре State?

Поля .Moving / .NotMoving ― показывают, движется ли ось. Причем, ожидаемо, каламбур: .NotMoving = NOT .Moving. Направление движения помогают определить поля .NegativeDirection и .PositiveDirection.

Не ждите полезной информации от полей Error. Лучше следите за Status.IoDataInvalid. Это поле показывает валидность данных. Поле становится равным TRUE, когда данные неверны, что можно интерпретировать как ошибку энкодерной оси.

пятница, 13 января 2017 г.

System Manager и четыре рантайма

При попытке создать несколько рантаймов и добавить несколько ПЛК-проектов, System Manager начинает капризничать, брыкаться, а в конце выдает ошибку: Runtime system is used by another project!



... и это не баг и не фича — это нюанс, который придется обойти.

Проект строится с помощью двух системных программ: System Manager — конфигуратор ПЛК и PLC Control в котором непосредственно разрабатывается программа контроллера. В какой-то момент разработки необходимо пробросить между ними мостик и познакомить их, передавать настройки из одной в другую и обратно. Для этого служат файлы с расширением .tpy, которые делает PLC Control при сборке (Project → Build) или пересборке (Project → Rebuild all) проекта.
Кроме файлов TPY, также существуют файлы TPA. Их создает System Manager, когда вы добавляете в конфигурацию новый проект или пересканируете уже существующие приекты (Rescan Project...). В эти TPA-файлы System Manager сохраняет конфигурацию переменных ввода/вывода (%I*, %Q*), заменяя звездочки (*) на реальные адреса.

При последующей пересборке проекта, PLC Control увидит файл TPA, созданный ранее System Manager'ом, и добавит в ПЛК-проект конфигурацию переменных, которую впоследствии можно увидеть в Resources → Global Variables → TwinCAT_Configuration.
В файле TPY находится полная информация о проекте, в том числе информация о рантайме, в котором планируется исполнять ПЛК-программу. К сожалению, невозможно выбрать нужный рантайм вручную, если вы разрабатываете проект без контроллера на столе и рядом с вами. По умолчанию система автоматически подставляет в проект первый рантайм, а затем сама же на это ругается, когда вы пытаетесь добавить второй, третий или четвертый проект в System Manager: ведь первый рантайм уже занят и нужно выбрать другой?

Чтобы покинуть тупиковую ситуацию, придется вручную отредактировать файл TPY, а конкретнее поле <Port> настроек роутинга <RoutingInfo> проекта <PlcProjectInfo>. Откройте в любом текстовом редакторе файл с расширением .tpy:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--This file is created by the TCatPlcCtrl automatically. Manually changes will be overwritten!-->
<PlcProjectInfo xmlns:p="http://www.beckhoff.com/2002/01/TcPlcProjectDesc">
    <ProjectInfo>
[...]
    </ProjectInfo>
    <RoutingInfo>
        <AdsInfo>
            <NetId>0.0.0.0.0.0</NetId>
            <Port>801</Port>
            <TargetName>
                <![CDATA[Target: Local (192.168.56.1.1.1), Run Time: 1]]>
            </TargetName>
        </AdsInfo>
    </RoutingInfo>

Номера портов жестко фиксированы: 1 — 801; 2 — 811; 3 — 821; 4 — 831. Достаточно заменить номер порта на соответствующий необходимому рантайму, а все остальные упоминания о рантайме можно оставить без изменений: на них система не обращает никакого внимание. Сохраните файл и без проблем добавьте его в конфигурацию System Manager'а.

среда, 30 ноября 2016 г.

TcHVAC: Структура библиотеки и проекта

Функционал управления отоплением, вентиляцией и кондиционированием (ОВК, ОВиК, HVAC: Heating, Ventilation, & Air Conditioning) в TwinCAT оформлены в виде библиотеки TcHVAC. Библиотека стоит денег и обозначена артикулом TS8000. В справочной системе можно найти в разделе: TS8000 | TwinCAT PLC Lib: HVAC.


Структура справки и структура каталогов в библиотеке различается, поэтому будем отталкиваться от структуры библиотеки, так как она ближе к практике. Следует сразу учесть, что библиотека строилась с учетом специфики разработки в виде функциональных блоков (FBD, CFC) и на ST временами будет выглядеть уродливо: за универсальность приходится платить.

Как правило, простые системы HVAC строятся исходя из релейной логики — "тумблеры и лампы": нажали кнопку, собралась цепочка, загорелась лампа (включился мотор, открылся вентиль и т. п.). Для полного описания такой системы составляют таблицу входов/выходов, оптимизируют ее (например, вручную с помощью карт Карно) и только затем переносят в виде LD-диаграмм или блоков FBD|CFC. На практике так никто не делает. Поэтому библиотека построена так, что следуя ее логике, цепочка функциональных блоков сложится сама: библиотека просто настроена на это — ставите один блок, за ним неминуемо тянутся другие.

Управление объектами строится на базе одного или каскада ПИД-регуляторов. Причем есть как классические (универсальные) регуляторы, так и специализированные, заточенные под управление конкретными объектами ОВиК, что позволяет объединять управляемые объекты в цепочки типа заслонка-рекуператор/подмешивание-подогреватель-увлажнитель-вентилятор-итп (смотри картинку выше).


Каталоги библиотеки


  • Actuators — насосы, моторы, одно-, двух-, трех-скоростные, резервирование и авторотация основной-резервный по времени наработки. Под мотором можно понимать целый исполнительный механизм, например, вентилятор.
  • Analog modules — преобразование аналоговых данных в обе стороны. Исполнительные механизмы с аналоговым управлением и датчики контролируемых параметров: температура PT100/1000, давление, управляющие сигналы 0-10В, 4-20мА или сигналы обратной связи и т. п. Параметры можно масштабировать в обе стороны.
  • BackupVar NOVRAM — автосохранение и автовосстановление переменных и настраиваемых параметров функциональных блоков библиотеки в NOVRAM памяти контроллера.
  • BackupVat Persistent — аналогично предыдущему пункту, но сохранение производится на флешку ПЛК.
  • Controller — различные регуляторы: ПИД, двухточечные, цепочки регуляторов, регуляторы заточенные под конкретный исполнительный механизм.
  • Room functions — специализированные функции для управления параметрами в жилых помещениях: климат-контроль, освещение, засветка Солнцем.
  • Setpoint modules — формируют кривые и рампы задания, основываясь на времени суток, летнем-зимнем сезоне и т. п.
  • Special functions — вспомогательные функции, помогающие преобразовывать типы данных, контролировать фронты входов/выходов или просто универсальные функции-обертки, позволяющие строить универсальные алгоритмы, подходящие для ПЛК различных типов. Самый простой пример — функция Blinker для мигания лампочкой с интервалом в одну секунду.
  • System — обще-системные функции: чтение/установка часов операционной системы, создание копий параметров функциональных блоков, получать сведения о циклах ПЛК-задачи и немного других системных функций.
  • Time schedule — планировщик действий по расписанию на один день, неделю, месяц, по праздникам и будням и т. д.


Структура проекта


Входная точка управления вентиляцией (и всем остальным) расположена в недрах функционального блока FB_HVACStartAirConditioning. Основная роль этого ФБ — быть диспетчером или дирижером всего оркестра устройств. Именно к нему прикрепляются все остальные функциональные блоки, именно он дает разрешение в виде сигнала Enable на блоки управления заслонками, вентиляторами, насосами и т. п.

Параллельно с блоком StartAirConditioning, каждый цикл вызывается ряд блоков, отвечающих за системные функции. Типичный набор этих функций выглядит так:
  • Считывание уличной температуры с компенсацией, FB_HVACOutsideTempDamped.
  • Мигалка, формирователь однополярного меандра, FB_HVACBlink.
  • Сохранение энергонезависимых данных, FB_HVACPersistentDataHandling.
  • Чтение системного времени, FB_HVACGetSystemTime.
  • Чтение параметров ПЛК-задачи, FB_HVACSystemTaskInfo.
  • Дополнительно: трансляция данных наружу через один или несколько протоколов передачи данных (например, Modbus).

Не все из них требуют вызова каждый цикл, но некоторые особо чувствительны к этому. Например, FB_HVACSystemTaskInfo. Казалось бы, система TwinCAT — это система жесткого реального времени; достаточно прочитать параметры задачи (Task) один единственный раз и больше они не изменятся. Проблема в том, что время цикла фиксировано, но ПЛК-задача может выполняться быстрее отведенного для нее времени цикла. В то же время функции для работы с ПИД-регуляторами требуют точного значения времени прошедшего с момента последнего вызова ФБ. Специально для этого в ФБ FB_HVACSystemTaskInfo есть поле tCycleTime, содержащее значение количества времени затраченного на выполнение ПЛК-задачи в предыдущем цикле — как долго выполнялся предыдущий цикл.

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

В ответ ФБ StartAirConditioning выдает сигналы на специализированные ФБ, которые управляют исполнительными механизмами: воздушные заслонки, вентили подогревателя, увлажнители, вентиляторы и другие устройства.

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

четверг, 24 ноября 2016 г.

Взгляд со стороны клиента ModbusTCP

Копнем чуть глубже — как выглядит сервер ModbusTCP с точки зрения клиента — по каким адресам модбаса нужно стучаться и что мы там найдем. Для свежести в памяти — обзор катушек и холдинг регистров и настройка сервера Modbus TCP.

После установки сервера ModbusTCP у нас уже есть настроенный конфигурационный файл, которым заранее удобно пользоваться. Все адреса десятеричные и все это можно позднее переделать под себя в том же конфигурационном файле. Пока же воспользуемся параметрами заданными по умолчанию в конфигурационном файле. Сетевой порт TCP = 502, полезная справочная документация = Mapping between Modbus and ADS.


Работа со стандартными массивами


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

После установки на ПЛК сервера ModbusTCP, последний автоматически ищет четыре глобальные переменные с заранее заданными именами.
Эти имена (или пара индекс-смещения) задаются в конфигурационном файле в разделах <VarName> или парой <IndexGroup> // <IndexOffset>.
Еще раз: в самой ПЛК-программе, вам ничего добавлять не нужно: сервер сам попытается найти эти имена среди глобальных переменных, и если найдет — автоматически начнет трансляцию данных по шине Modbus. Включать в проект дополнительные библиотеки не требуется, просто добавляем в глобальные переменные четыре массива:

.mb_Input_Coils [0..255] OF BOOL
.mb_Output_Coils  [0..255] OF BOOL
.mb_Input_Registers [0..255] OF WORD
.mb_Output_Registers  [0..255] OF WORD

С точки зрения ModbusTCP массивы получают адреса начиная с 3276810 (или 0x800016). Расчет полного адреса ведется с учетом индекса в массиве:

.mb_Input_Coils [0..255] OF BOOL
.mb_Output_Coils  [0..255] OF BOOL

Адрес Modbus = 32768 + индекс в массиве (0..255)

Одна катушка или дискретный порт ввода-вывода соответствует одной ячейке массива типа BOOL. Это очень удобно, так как не требуется упаковка битов в "слово". Аналогично с регистрами:

.mb_Input_Registers [0..255] OF WORD
.mb_Output_Registers  [0..255] OF WORD

Адрес Modbus = 32768 + индекс в массиве (0..255)

В каждом массиве всего 256 элементов, поэтому адресация этих массивов заканчивается для Modbus на числе 33023. Полный диапазон 32768..33023.


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


Дополнительно к массивам, автоматически транслируется область переменных ввода-вывода, то есть также одновременно транслируются те переменные, которые связаны с физическими входами/выходами или, иначе говоря, переменные, которые обозначены как AT %I* и AT %Q*. Простые правила помогут запомнить что-с-чем ассоциируется:
  • Катушки (coils) можно намагничивать и размагничивать — поэтому они могут менять свое значение и поэтому они связаны с AT %Q*. Аналогичное правило для регистров хранения (holding registers): в них тоже можно сохранять и читать.
  • Inputs — это входа и они только для ввода данных, поэтому записывать нельзя — AT %I*.

Чтение с помощью функций модбаса:
  • Катушки %QX — функция #1, Read Coils.
  • Регистры хранения %QB, %QW,... — #3, Read Holding Registers.
  • Дискретные входа %IX — функция #2, Read Discrete Inputs.
  • Дискретные регистры ввода %IB%IW,... — #4, Read Input Registers.

Запись доступна только для катушек и регистров хранения, по одному за раз, но вообще можно и несколько за раз (функции 15 и 16):
  • Катушки %QX — функция #5, Write/Force Single Coil.
  • Регистры хранения %QB, %QW,... — #6, Write/Preset Single Register.

В итоге выстраивается стройная система — с точки зрения Modbus, мы номером функции выбираем тип переменных (чтение/запись, катушки/регистры), а затем адресом уточняем какую именно переменную мы хотим.

Адрес Modbus для переменных ввода данных (AT %I) лежит в диапазоне 0..32767. Для переменных ввода/вывода (AT %Q, катушки и регистры хранения) верхняя граница занижена 0..12287, иначе они будут пересекаться с меркерной памятью (читай ниже).

Расчет адресов идет по единому принципу и для тех, и для других. Так как дискретные или битовые входа/выхода упаковываются в байты — значения адресов Modbus придется немного повычислять, :

%IX#.@ (где # — номер байта, @ — номер бита в байте)
%QX#.@

Адрес Modbus = # / 2 * 16 + @ = # * 8 + @

Пример:

bSomeVar AT %IX6.2 : BOOL;

Адрес Modbus = 6 * 8 + 2 = 50


Для байтовых входов/выходов важно учитывать, что чтение с шины Modbus идет 16-разрядными словами. Чтобы прочитать байт, нужно прочитать слово целиком, а затем выделить из слова байт. Важно знать порядок следования байтов в слове — он не определен!

%IB# (где # — номер байта)
%QB#

Адрес Modbus = # / 2

Modbus 0 = %IB1 : %IB0
Modbus 1 = %IB3 : %IB2
Modbus 2 = %IB5 : %IB4
%IB123 → 123 / 2 = 61.5 ≈ 61 → Modbus 61 = %IB123 : %IB122
...

"Слова" состоят из двух байт, поэтому идут с шагом два. Например, для %IW и %QW:

iSomeVar0 AT %QW0 : WORD;
iSomeVar1 AT %QW2 : WORD;
iSomeVar2 AT %QW4 : WORD;
[...]
iSomeVarN AT %QW# : WORD;

Адрес Modbus = # / 2


Меркерная память


Для шины Modbus эта область памяти (AT %M*) лежит в диапазоне адресов 12288..24575. Адреса строятся аналогично переменным ввода/вывода из предыдущего пункта.
Обратите внимание, что адреса Modbus для меркерной памяти пересекаются с адресами переменных ввода/вывода %Q и в то же время не пересекаются с адресами переменных ввода %I. Это происходит по причине доступности переменных %M как на чтение, так и на запись, аналогично переменным ввода/вывода %Q. Именно поэтому M- и Q-области памяти  выглядят одинаково с точки зрения Modbus, а значит и доступны с помощью одних и тех же функций Modbus.


Порядок слов


Modbus как таковой ничего о типах данных не знает и перегоняет битовые единицы упакованными в "слово", а регистры просто как "слова" (WORD). При этом протокол ничего не знает ни о знаке числа (для него все числа целые), ни о плавающей запятой, ни о порядке байт в слове.

Например, я сейчас включил ПЛК и при использовании типа WORD получил следующую картину:

%MB0 — младший байт
%MB1 — старший байт

%MW0 — 12288 (%MB1 : %MB0)
%MW2 — 12289 (%MB3 : %MB2)
%MW4 — 12290 (%MB5 : %MB4)

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


Загадочные области памяти


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


Обновлено: 23 января 2017 г.

четверг, 3 ноября 2016 г.

Системный диалог даты-времени в Windows CE

Как быстро сделать ввод системной даты/времени при создании визуализации TwinCAT 2 под Windows CE? Нужно воспользоваться системным диалогом из панели управления — открыть  проводник explorer.exe, а дальше оператор сам найдет, исправит, настроит, испортит. Чтобы избежать такого поведения можно открывать только панель даты/времени.

1. Из меню Start запускаем notepadce.
2. Аккуратно вводим одну строчку, внимательно следя за пробелами и другими символами:

35#"ctlpnl.exe" \Windows\cplmain.cpl,13

35# — это количество символов в строке. Между ..ctlpnl.exe" и \Windows\... стоит пробел.

3. Файл сохраняем как datetime.lnk во флэш памяти контроллера (имя файла произвольное, если-что). Сохранить можно где-нибудь в \Hard Disk\datetime.lnk

4. Для элемента, при нажатии на который будет открываться панель настройки даты/времени, выставляем параметр визуализации (кавычки там обязательны):

Input → Execute program: "\Hard Disk\datetime.lnk"

Теперь при нажатии на элемент можно отображать только системную панель настройки даты/времени:



Можно открывать и другие апплеты панели управления:


PC Connection – 35#"ctlpnl.exe" \Windows\cplmain.cpl,0
Dialing – 35#"ctlpnl.exe" \Windows\cplmain.cpl,1
Keyboard – 35#"ctlpnl.exe" \Windows\cplmain.cpl,2
Password – 35#"ctlpnl.exe" \Windows\cplmain.cpl,3
Owner – 35#"ctlpnl.exe" \Windows\cplmain.cpl,4
Power – 35#"ctlpnl.exe" \Windows\cplmain.cpl,5
System – 35#"ctlpnl.exe" \Windows\cplmain.cpl,6
Display – 35#"ctlpnl.exe" \Windows\cplmain.cpl,7
Mouse – 35#"ctlpnl.exe" \Windows\cplmain.cpl,8
Stylus – 35#"ctlpnl.exe" \Windows\cplmain.cpl,9
Volume & Sounds – 35#"ctlpnl.exe" \Windows\cplmain.cpl,10
Input Panel – 35#"ctlpnl.exe" \Windows\cplmain.cpl,11
Remove Programs –  35#"ctlpnl.exe" \Windows\cplmain.cpl,12
Date/Time – 35#"ctlpnl.exe" \Windows\cplmain.cpl,13
Certificates – 35#"ctlpnl.exe" \Windows\cplmain.cpl,14
Accessibility – 35#"ctlpnl.exe" \Windows\cplmain.cpl,15


Для открытия CX Configuration или полностью Beckhoff CX Configuration Tool, нужно запустить другой апплет:

35#"ctlpnl.exe" \Windows\CxConfigCpl.cpl


Для юных исследователей доступен каталог \Windows, который может содержать (и содержит) другие апплеты панели управления в виде файлов с расширением .cpl

вторник, 18 октября 2016 г.

Я сделаю свой NC с рампами и траекториями

В частотных приводах AX5000 и сервомодулях EL72xx изначально заложена возможность прямого управления сервоусилителями: кидаете задание Velocity command value, формируете слово управления Master control word и постоянно следите за словом состояния Drive status word. По сути вам необходимо самостоятельно обрабатываете все, что есть в AT|MDT ветках сервоусилителя. Просто и дешево, но:
  • Не умеет масштабировать величины. Все значения задаются в инкрементах. Хотя можно реализовать пересчетные функции (как уже сделано в NC).
  • Только базовый набор функций: вкл/откл силового питания, вращение с заданной скоростью, абсолютное позиционирование, контроль тока, ограничение момента.
  • Все управление идет из ПЛК-задачи: если в программе тех. процесса допустить ошибку, то она отразится в том числе и на работе сервоусилителя, так как они все вместе работают внутри одной и той же задачи (в отличии от NC, которая работает как независимая подсистема TwinCAT).
  • Зависимость от времени цикла ПЛК-задачи. NC не зависит от времени цикла задачи, что положительно сказывается на скорости реакции, плавности/точности траекторий и пр. 
  • Нет разгонно-тормозных рамп, но можно реализовать их самостоятельно.
  • Зависимость от режима работы сервоусилителя: нельзя задать скорость вращения в режиме позиционирования и обратная ситуация — нельзя выйти в абсолютную позицию в режиме управления по скорости.
  • Нет ни гарантии, ни тех. поддержки: умер разработчик — умерла система.

Должны быть плюсы:
  • Дешево.
  • Будет работает на всем, что может обеспечить EtherCAT-мастер, в том числе и на контроллерах CX8010 + EK1110.


Что-то подобное уже всплывало в зачем нужен NC PTP и что он себе позволяет.