Showing posts with label программирование. Show all posts
Showing posts with label программирование. Show all posts

November 17, 2020

Переменная через указатель, через ADS

Значение переменной можно прочитать через указатель по ADS.

PROGRAM MAIN
VAR
    state:  UINT;
    pState: POINTER TO UINT;
END_VAR

pState := ADR(state);


Прочитаем значение переменной `state` через ее указатель `pState`. Нужно разыменовать указатель, добавив спец. символ `^` к имени указателя. Получим `pState^`. Программа на C# для текущей версии 4.4.10 библиотеки Beckhoff.TwinCAT.Ads:

ushort state = (ushort) tcAdsClient.ReadSymbol( "MAIN.pState^", typeof( ushort ), false );


Значение переменной читается. Теперь возьмем пробную версию новой библиотеки 5.0.1-preview.12 под .NET Core 3.1.0. Код поподробнее:


Программа читает переменную и в зависимости от ее значения рисует замысловатые узоры. Имея указатель можно вытягивать значение переменной. Это полезное свойство и дополнительные телодвижения здесь не нужны.


Для новой библиотеки под .NET Core требуется создать файл роутинга `StaticRoutes.xml`. В этом файле настраиваются соединения между целевым контроллером и ПК. Если контроллер  и ПК находятся на одной и той же машине, то настраивать этот файл не нужно. Можно просто удалить его.

November 13, 2020

Атрибут TcNcAxis

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


Массивы

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

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


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

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

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


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

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

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

[...]

FUNCTION_BLOCK FB_Foobar
VAR
    axisZ: AXIS_REF;
END_VAR

[...]

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


November 10, 2020

Секретный сервис 700

Не все сервисы TwinCAT описаны в документации. В старых исходниках библиотеки AdsClient, в файле AdsSpecial.cs, есть любопытная функция public string GetTargetDesc(). Описание функции гласит: "Get an xml description of the plc...". Посмотрим, какое именно описание отдаст нам контроллер.

Начнем с системного сервиса, порт номер R3_CTRLPROG = 10000. Официальное описание списка функций заканчивается номером 600 (см. таблицу System Service Index Groups). Мы же прочитаем индекс 700 (0x02BChex). Настраиваем роутинг и начинаем читать. Код C# программы:

 

Первый запрос отправляем с параметром typeof(int). Где-то внутри библиотеки это транслируется в параметр команды равный 4 — это длина типа данных int в байтах. В ответ, запрос вернет длину XML текста с описанием устройства. Зная размер, мы отправляем второй запрос в тот же порт-индекс-смещение, но в качестве параметра передаем длину описания, полученного на предыдущем шаге. Результат парсим. С помощью LINQ ищем и вытаскиваем интересующие нас поля описания.

Контроллер CX9020 вернул такой XML:

 

Сравним описания от CX9020 (Win CE) и ноутбука (Windows 10). Чтобы отследить различия, сохраним XML в отдельный файл: xdoc.Save( "cx9020.xml" ); и воспользуемся программой WinMerge:


TС/BSD

Новая, перспективная и все еще недоступная операционная система выдает следующее описание:

 

Пропали элементы `ImageDevice`, `ImageLevel`. Значение `CPUArchitecture` стало более осмысленным, но что такое 9 все еще непонятно.


Тоже сервисы

Можно найти еще несколько интересных сервисов, если копнуть глубже. Для раскопок пригодится какой-нибудь HEX-вьюер (VSCode hexdump) и следующая строка: File.WriteAllBytes( $"idx_{READDEVDESCRIPTION_IDX}.bin", adsStream.ToArray() ); // Начинайте копать.

Индекс 701 выдаст список всех сетевых интерфейсов доступных на устройстве. IP-адреса, маски подсети и что-то еще. Формат неизвестен, но можно разобраться самостоятельно. Копайте.

Из любопытного, для TC/BSD сервис возвращает название сетевого интерфейса в юникс стиле — `em0`.
А для Windows возвращает GUID: "{7D8FDCBA-6250-8DFF-4089-AB0845B12EDC} Qualcomm Atheros AR5BWB222 Wireless Network Adapter 192.168.2.177 255.255.255.0".

Индекс 702 отдает имя целевой машины: PC-8E5B1A, CX-3F5BC9... Строка заканчивается '\0', не забывайте про .TrimEnd('\0'); Продолжайте копать.


November 6, 2020

Агент данных OPC UA

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

ПКМ по пустому месту → Add Gate (OpcUaDevice). В окне `Properties` ввести в поле`Url` адрес своего OPC UA сервера: opc.tcp://192.168.1.100:4840. Добавьте канал подписчика: Add Channel (Subscriber), стрелка вниз 🡇.

Теперь открываем окно `Target Browser`, закладка `OpcUa`, добавляем OPC UA сервер контроллера, выбираем необходимые объекты-переменные и тащим их на "подписчика".

Возможно понадобится донастроить переменные подписчика. В моем случае Агент добавил мусор в имя переменной ns=4;s=MAIN.nCounter. Исправляется в окне `Properties`, поле `URN` и превращается в MAIN.nCounter. Ниже в примере исправлена только одна переменная-символ:

>>> {"Timestamp":"2020-11-06T11:48:08.151","GroupName":"_MQTT Broker_28","MAIN.nCounter":-1946,"ns=4;s=MAIN.rCounter":784486.0}
>>> {"Timestamp":"2020-11-06T11:48:09.150","GroupName":"_MQTT Broker_28","MAIN.nCounter":-1846,"ns=4;s=MAIN.rCounter":784586.0}
>>> {"Timestamp":"2020-11-06T11:48:10.150","GroupName":"_MQTT Broker_28","MAIN.nCounter":-1746,"ns=4;s=MAIN.rCounter":784686.0}

November 3, 2020

Начало работы с агентом данных

Агент позволяет передавать переменные программы и другие данные из одного места в другое. Например, есть группа контроллеров CX8090. На отдельном ПК устанавливается TC3 IoT Data Agent. Он настраивается на проброс данных через интернет на сервер-брокер MQTT. Из брокера данные забираются в базу данных. Позже аналитики анализируют, а сервисный отдел мониторит и бдит. Версия TwinCAT, разрядность и тип процессора, древность контроллеров — все это не важно. Переменные из контроллера можно передавать куда угодно, в обе стороны.

Изображение: Beckhoff Automation

Современные протоколы типа MQTT–AMQTT–RabbitMQ не требуют входящего подключения. Агент и контроллеры могут находится за NAT, файерволом или другой сетевой инфраструктурой. IP-адрес может быть серым и динамическим, но подключение к брокеру всегда исходящее. Поэтому переменные контроллера легко отдавать и легко забирать. В обе стороны.


Лицензии

Для ПК, на котором установливается Агент, необходимы минимум две лицензии: TC1000 | TC3 ADS и TF6720 | TC3 IoT Data Agent. Доступна пробная лицензия на 7 дней.

Лицензирование основано на группах порталов. Порталы объединяются в пакеты (Gate packs). Порталом называют одно подключение. Например, подключение к устройству через ADS или OPC UA. Лицензия TF6720 обеспечивает работу с четырьмя порталами. Большее количество порталов можно получить после покупки дополнительных лицензий (TF6721-TF6724). Количество порталов складываются: TF6720 + TF6721 = 8 порталов.


Принципы работы

Open local, Save local работают со схемой в локальной конфигурации Агента C:\TwinCAT\3.1\Boot\TcIotDataAgentConfig.xml. Эта схема будет использована при старте Агента на этом локальном ПК. Во время работы рядом будет лежать лог TcIotDataAgent.log. По нему можно проводить диагностику работы Агента.

Open file, Save file импорт/экспорт схемы из отдельного файла.

С помощью кнопки "активировать конфигурацию", можно активировать схему на удаленном контроллере. На кнопке изображена традиционная горка кубиков (Save to selected target and activate).

В окне `Topology` создаем схему передачи данных. Нужно запомнить два простых принципа: создаем правой кнопкой мыши (ПКМ), а затем соединяем элементы с помощью Ctrl + тащим и бросаем. Например:

  • ПКМ по пустому месту → Add Gate (ADS) → получился круг — это ADS-портал, ведущий к переменным контроллера.
  • Затем, ПКМ → Add Gate (MQTT) → появилось облако — это брокер MQTT, источник данных.
  • ПКМ ADS портал → Add Channel (Subscriber) → создается подписчик (subscriber) в виде прямоугольника. Стрелка вниз 🡇 указывает направление подключения.
  • ПКМ Подписчик → Add Symbol → добавляется новая переменная (symbol) для чтения из контроллера. Можно сделать проще: открыть окно `Target Browser`, перетащить и бросить переменную на "подписчика".
Схему можно создавать и редактировать через другие окна программы. Исследуйте их. Выберите удобный способ работы с программой.

Аналогично поступаем с порталом MQTT Broker, где вместо `Add Gate` доступен `Add Channel`. Брокер работает не с переменными, а с каналами. Через них идут потоки переменных.

Дальше тащим прямоугольник подписчика ADS: Ctrl + левая клавиша мыши (ЛКМ). Бросаем его на прямоугольник канала MQTT. Между элементами появляются связи.

Настройки всех элементов собраны в окне `Properties`.


Пример программы

Необходимо проработать четыре момента:

  • ПЛК программу как источник данных. Подойдет любая версия TwinCAT. Я брал как вторую, так и третью версию TwinCAT. Меняется номер порта ADS 801 → 851, но принципы создания схемы остается прежним.
  • Создать схему передачи данных для Агента.
  • Выбрать MQTT брокер данных.
  • Создать клиента для брокера MQTT. Я напишу простую программу на C#. Она будет читать данные из брокера. Здесь можно воспользоваться готовыми клиентами MQTT и запустить их на смартфоне.


ПЛК программа примитивная:

PROGRAM MAIN
VAR
    iCount: INT;
    rCount: REAL;
END_VAR

iCount := iCount + 1;
rCount := rCount + 0.1;


Схема Агента

Пора выбрать бесплатного брокера на тестирование. Мне понравился HiveMQ. Кроме него проверил Mosquitto. Он работал, но значительно медленнее.

Пришло время создать схему:


Разбивка по каналам позволяет устанавливать единые сетевые настройки для нескольких переменных сразу. Можно создать один единственный канал и транслировать в нем сразу несколько переменных. Чтобы было интереснее, я разбил трансляцию от брокера на два канала (см. картинку выше, правая часть):

  • Ads_Mqtt_11_19 — транслирует переменную MAIN.rCount;
  • Ads_Mqtt_11_17 — передает целое число из переменной MAIN.iCount.

Внутри канала данные брокера можно раскидать по темам (Topic). Это настраивается в окне `Properties`. Например, пусть `rCount` как бы передается из жилой комнаты GOT/TWINCAT/ROOM, а переменная `iCount` приходит из офиса GOT/TWINCAT/OFFICE. В клиенте брокера я смогу выбрать или одну конкретную, интересующую меня тему, или сразу несколько тем. Темы фильтруются с помощью спец. символов `*`, `?` или `#`. Например, я хочу в одном канале получать данные ROOM+OFFICE: GOT/TWINCAT/#.


Клиент брокера

Для C# я использовал библиотеку MQTTnet. Она легко устанавливается из NuGet. Раскомментируйте строку и подставьте название своего топика-комнаты в константу `MQTT_TOPIC`.

 

Результат работы клиента:


Одновременно я установил на телефон бесплатный клиент `MQTT Dash` и он также смог отображать данные с ПЛК. Трансляция идет через интернет, можно сходить на обед и одним глазом посматривать как контроллер продолжает работать:

October 31, 2020

Функции измерения нагрузки ПЛК

Измерение загрузки контроллера можно условно разделить на:

  • измерение нагрузки процессора. Показывает справляется ли вся система в целом: Windows + TwinCAT.
  • Измерение времени исполнения программы в текущем цикле. Укладывается ли текущая ветвь программы в заданное время цикла.
  • Профилирование. Замер времени выполнения отдельных частей программы.

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


Разбор программы



В программе выше используются функции: TC_SysLatency, TC_CpuUsage, GETCPUCOUNTER. Также используется информация из встроенного глобального массива SystemTaskInfoArr[]. Он предоставляет структуру данных SYSTEMTASKINFOTYPE.

TC_SysLatency пропустим, я по прежнему не вижу в нем смысла. TC_CpuUsage возвращает целое число процентов нагрузки на процессор. Это значение должно совпадать с графиком в System Manager, но это не точно и это было видно выше.

GETCPUCOUNTER работает независимо от счетчика в CPU. Это счетчик 100 наносекундных циклов. Увеличение на единицу соответствует прошедшему времени в 100 нс. Увеличение на 10 соответствует 1 микросекунде. Посмотрим как перевести в миллисекунды с десятичными долями:

52'108'000 наносекунд = 52'108'0 100*нс = 52'108 микросекунд = 52,108 миллисекунд.

LREAL cpuCntMs := (cpuCntLoDW + cpuCntHiDW) / 10000.0

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

SystemTaskInfoArr[] в том же цикле отдает структуру SYSTEMTASKINFOTYPE. Индексом для массива служит номер текущей задачи. Индекс можно получить с помощью функции GETCURTASKINDEX в этом же цикле и начать его использовать уже в следующей строке программы.

Структура SYSTEMTASKINFOTYPE содержит состояние предыдущего цикла. Эта структура содержит:

  • cycleTimeExceeded — TRUE = время цикла превышено, FALSE = время выполнения в норме. Значение параметра не фиксируется, в каждом цикле оно может быть разным. Всё зависит от времени исполнения предыдущего цикла.
  • cycleTime — максимально возможное время на выполнение цикла. Задается в сотнях наносекунд. Для перевода в миллисекунды, разделите значение cycleTime на 10000.0.
  • lastExecTime — время выполнения программы в предыдущем цикле. Предыдущего, потому что текущий цикл еще только выполняется, вот прямо сейчас, в данный момент. Значение в сотнях наносекунд.
  • cycleCount — номер текущего цикла от момента включения контроллера. Если номер цикла умножить на длительность цикла cycleTime, можно узнать сколько прошло времени с момента включения контроллера.

 

В TwinCAT 3 массив поменял имя на _TaskInfo[], а структура стала более подробной и теперь называется PlcTaskSystemInfo.

August 20, 2020

Сокеты не реального времени

Из ПЛК задачи можно сделать TCP/UDP клиент или даже сервер. Это позволяет работать с данными современной периферии без разработки промежуточных слоев. У нас есть выбор между TF6310 (обычная и давно практикуемая) и TF6311 (модная, современная, риалтаймовая). Обе-две работают как на PC/CX (x86), так и на CX (ARM). В этом посте будет практика работы с обычной библиотекой 6310, а с новой разберемся как-нибудь позже.

Изображение: Beckhoff Automation

TF6311 TCP/UDP (realtime)

Полное описание доступно в инфосисе. Обе библиотеки доступны из ПЛК задачи, но 6311 работает "рядом" с ядром TwinCAT 3, на том же системном уровне.

Здесь заострю внимание на особенностях, плюсах и минусах.

  • Только TwinCAT 3.
  • Функционал не требует установки, все компоненты уже встроены в TwinCAT 3.
  • Требуется лицензия TC3 TCP/UDP RT.
  • Есть возможность использовать временную (trial) лицензию на время разработки.
  • Работает напрямую с сетевой картой, минуя большинство механизмов операционной системы.
  • TF6311 настраивается в проекте через TCP/UDP RT TcCom Parameter. Это требует отдельного рассмотрения.

Минусы

  • Не рассчитан на большие и незнакомые сети. Вероятно подразумевается интернет, либо большой интранет.
  • Не для больших объемов данных.
  • Не поддерживает мультикаст в UDP.
  • Windows Compact CE только начиная с версии 7.
  • Windows Firewall отсутствует в цепочке передачи пакетов (менее защищенные соединения).
  • Только TwinCAT-совместимые карты. Список доступен в Supported Network Controller by Beckhoff Ethernet Driver.
  • Нет связи с локальным, стандартным сетевым интерфейсом Windows. Можно реализовать через стороннего посредника.
  • Сетевые коммутаторы (эзернет свичи) EL6601 и EL6614 не могут использоваться совместно с этой библиотекой.

Плюсы

  • Очень детерминированный и предсказуем (подтвердить не могу).
  • Поддержка С++ (похоже, что это основное назначение библиотеки).
  • Поддерживает ARP/Ping.


TF6310 TCP/IP


Работает через специальный ADS-сервер, а дальше через стандартный WinSock, по сути копируя стандартный функционал сетевого ввода-вывода. Как и что устанавливать вменяемо описано в справке, и проблем это обычно не вызывает.

Для TwinCAT 2 мы устанавливаем TS6310. Он приносит с собой следующие библиотеки в каталог TwinCAT\Plc\Lib:
  • TcpIp.lib — базовые функции TCP/IP и UDP;
  • TcSocketHelper.lib — вспомогательные функции TCP/IP, упрощающие жизнь разработчика. Содержит готовые ФБ с полным циклом клиент-сервер и сервер-клиент.
  • TcSnmp.lib — протокол SNMPv1, вспомогательные функции.
  • TwinCAT TCP/IP Connection Server — по сути это мост ADS ↔ TCP/IP.


Практика 6310


TCP-клиент рассчитан на передачу больших объемов данных в виде непрерывных потоков. Данные текут непрерывно, но ничто не мешает разбить их на пакеты. Перед началом работы устанавливается соединения, которое по окончании работы разрывается. Длительность передачи данных после соединения не оговаривается: минуты, часы, дни, года. Так было задумано и это обычная практика работы с TCP/IP протоколом.

Минимальная реализация TCP-клиента:
  • FB_SocketConnect и FB_SocketClose — для подключения и разрыва сессии.
  • FB_ClientServerConnection — включает в себя оба предыдущих блока и упрощает работу с ними.
  • FB_SocketSend и/или FB_SocketReceive — для обмена данными.

Минимальная реализация TCP-сервера:
  • FB_SocketListen — открывает сокет на прослушивание в режиме ожидания клиента.
  • FB_SocketAccept и FB_SocketClose — открывают и соответственно закрывают соединение.
  • FB_ServerClientConnection — умеет все вышеперечисленное вместе и упрощает работу.
  • FB_SocketSend и/или FB_SocketReceive — для обмена данными.

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

Минимальная реализация UDP-клиента:
  • FB_SocketUdpCreate и FB_SocketClose — открыть/закрыть сокет.
  • FB_SocketUdpSendTo и/или FB_SocketUdpReceiveFrom — прием-отправка данных.

Для UDP-пакета ограничен максимальный размер отправляемых данных. По умолчанию он равен 8192 байт (это число задается константой TCPADS_MAXUDP_BUFFSIZE). Поэтому стоит обратить внимание на аргумент cbLen функции FB_SocketUdpSendTo. Ограничение служит для экономии памяти.

Для всех типов связи FB_SocketCloseAll закрыть всё открытое и закончить любые работы в пределах текущего рантайма. Это который имеет определенный порт, например, 801 для первого рантайма TwinCAT 2.
FB_SocketAccept, FB_SocketReceive, FB_SocketUdpReceiveFrom — вызываются циклически (polling), то есть каждый цикл. Остальные блоки вызываются по мере необходимости.


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


TwinCAT 2


CX8090 (TC2, WinCE 7, ARMv4) — идеально когда происходит один вызов сетевой функции за цикл. 4 соединения за цикл работают нормально, при большем числе идут таймауты.

CX9020 (TC2, WinCE 7, Arm Cortex™-A8) — 4 соединения одновременно за цикл работают нормально, а дальше идут таймауты. Соединения-подключения все равно необходимо выполнять последовательно: сначала устанавливается одно соединение и только затем выполняем следующее подключение.


TwinCAT 3


CX8190 (TC3, WinCE 7, ARM Cortex™-A9) — 10 соединений одновременно за цикл работают нормально. Соединения-подключения необходимо выполнять последовательно.

CX2030 (TC3, Windows 7 Emb Std, Core-i7) — 10 соединений одновременно за цикл работают нормально. Соединения-подключения необходимо выполнять последовательно.


Не больше 10 клиентов

  • Не больше 10 клиентов. Ограничение в 10 клиентских подключений на рантайм, а возможно и на весь ПЛК, но это не точно.
  • Разные циклы — разные подключения. Открывать соединение лучше друг за другом по одному за цикл.
  • Функции всегда работают как минимум один цикл. После первого вызова, в последующих циклах можно спокойно вызывать ФБ с bExecute := FALSE, ожидая когда функция переключится в NOT bBusy, что означает поступление данных либо ошибку.
  • При обрыве связи возвращает bError = FALSE и nRecByte = 0. Для определения обрыва необходимо самостоятельно использовать собственный таймер для контроля таймаута. Великая вещь, что функции здесь неблокирующие (разработчики на C++ и другие поймут).
  • Протокол поточный, если читаете данные пачками, то полученные данные необходимо накапливать в буфере, десериализовать или просто сканировать этот буфер на предмет специального тэга или какой-либо другой ситуации предусмотренной протоколом (это уже зависит от специфики протокола).

Мне стало интересно откуда это волшебное и круглое десятичное число — десять. И почему нельзя взять и подключаться ко всему и сразу. Я начал следить за количеством соединений и количеством системных потоков (threads).
Соединения - потоки
 2 - 8
 3 - 10
 4 - 12
 . . ..
 6 - 16
Просматривается явная зависимость — на каждое новое соединение создается два новых потока. Зачем два? Заглянем в лог TcpIpServer.log:

Видно, что сначала создается ADS-сокет CTcpAdsSocket::CTcpAdsSocket(); Он будет принимать команды и данные из ФБ ПЛК-задачи, а затем создается требуемый TCP-сокет CTcpSocket::Create(); теперь уже для непосредственной передачи данных. Поэтому каждый цикл можно открыть только одно новое соединение — на запрос создания сокета создается только одна связка ADS ↔ TCP|UDP. Такая вот архитектура, упрощенно.


Не больше 10, но можно меньше


Под Windows CE можно поиграть с ключами реестра: Start → Run... → regedit. Создать ключ Registry → New → Key: HKLM\SOFTWARE\Beckhoff\TwinCAT TcpIp Server. Внутри раздела доступны несколько значений-параметров типа DWORD. Что удалось выяснить:

MaxTcpSocketCount
0 = вероятно стандартные 10 сокетов-соединений.
1 = запретить вообще все подключения. Теперь функции FB_SocketListen и FB_SocketConnect возвращают код ошибки TCPADSERROR_NOMOREENTRIES (0x0000800132769).
2 = 1 доступное подключение.
3 = 2 доступных подключения.
[...]
11 = 10 максимально доступных подключений. Всё, больше нельзя.

MaxUdpSocketCount — аналогично MaxTcpSocketCount, но для UDP протокола.
AdsServerCommTimeout — возможно таймаут ADS-сервера. Единицы измерения вероятно миллисекунды.
DisableKeepAlive — запретить постоянные KeepAlive подключения?
ThreadPriority — приоритет системного потока? Значения не известны.

LogLevel
0 = отключен.
1 = включен. Логировать будет сюда: \Hard Disk\TwinCAT\TcpIpServer.log


TcSocketHelper


TcSocketHelper.lib выполняет за нас все трудоемкие операции по клиентским подключениям к серверу и в обратную сторону — отслеживает клиентские подключения к серверу. Доступные примеры лежат в справочной системе.

FB_ServerClientConnection — выполняет функции TCP-сервера. Внутри себя содержит и выполняет FB_SocketListen, FB_SocketAccept и FB_SocketClose. На выходе выдает идентификатор сокета hSocket для подключившегося клиента. Дальше передаем его в FB_SocketSend и/или FB_SocketReceive.

FB_ClientServerConnection и FB_ConnectionlessSocket — первый служит для создания TCP-клиентов, а второй для UDP. Оба умеют создавать и закрывать соединения. При успешном соединении выдают на выходе hSocket для передачи в FB_SocketSend и/или FB_SocketReceive.

Из интересного все функции, связанные с получением данных (Receive), внутри себя содержат механизм регулировки скорости обновления ФБ (пропуск тактов, троттлинг, throttling). Ничего особенного, это обычный подход в такой ситуации. Кратко выглядит так:

TYPE T_ThrottleTimes: ARRAY[0..MAX_THROTTLE_MODE] OF TIME;
END_TYPE

throttleTimes: T_ThrottleTimes := T#0s, T#10ms, T#20ms, T#40ms, T#60ms, T#80ms, T#100ms,
                                  T#200ms, T#400ms, T#600ms, T#800ms, T#1s, T#2s;

И скрытая, только для внутреннего использования, функция-обертка над таймером FB_ThrottleTimer, которая состоит всего-лишь из одной строки с вызовом таймера:

timer(
    IN := bIn,
    PT := throttleTimes[LIMIT(0, selector, MAX_THROTTLE_MODE)],
    Q  => bOut,
    ET => tElapsed );

Здесь `selector` задает текущий режим, а его значение изменяется через вызов одного из четыре экшенов (Action):
  • MaxSpeed — selector = 0.
  • MinSpeed — selector = MAX_THROTTLE_MODE.
  • SlowDown — увеличивает задержку, уменьшает скорость опроса.
  • SpeedUp — уменьшает задержку, увеличивает скорость опроса.

Суть этого действа в автоматическом регулировании интервал ожидания: увеличивать интервал если сообщений нет, и снижать интервал ожидания, если сообщения пошли часто-часто.

August 18, 2020

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

August 4, 2020

Всё есть файл

В Unix/Linux устройства могут выступать в качестве файла или бинарного/символьного потока. Такая философия позволяет работать с потоком данных устройства, как с обычным файлом: открываем-читаем-пишем. Все что нужно — это иметь права доступа к устройству и знать протокол и структуру данных. В Windows иногда тоже можно. Попробуйте, например, создать на рабочем столе файл с именем com2.txt. Будет занято.


Источник


Сначала нужно определиться, что именно будем читать. Для начала я не стал брать последовательные порты или другие устройства, а решил воспользоваться псевдоустройством... поэтому будем читать случайные числа из /dev/urandom. Это не совсем то, что мне хотелось бы проверить, но пока обойдемся этим. Как вариант можно считать память операционной системы через /dev/mem. Сервис TwinCAT под BSD запускается с root доступом, поэтому проблем возникнуть не должно.

Прочитанное из устройства, можно просто сохранить в массив, но мы сделаем два дела сразу — кроме чтения устройства, сохраним результат в файл, лежащий на другом ПЛК. Итого: два ПЛК (один с TC/BSD, другой с Windows), соединены обычной локальной сетью Ethernet. На одном ПЛК читаем случайные числа из файла-псевдоустройства, а результат сохраняем на другой ПЛК в настоящий файл.


Код


Код настолько простой, что смотреть особенно не на что: ряд обычных файловых операций. Именно в этом заключается преимущество подхода "всё есть файл".


Открывать несколько файлов можно с помощью одного и того же ФБ. Главное делать это последовательно: сначала один, затем другой. Основная задача получить хендлер файла для дальнейших файловых операций. Нам нужно получить два хэндлера, от двух файлов, заданных следующими путями: sDevPath — задает путь к источнику '/dev/urandom' и sResPath — путь к файлу с результатом 'c:\dev\random.txt'. Сразу видно — где Юникс, а где Виндовс (подсказка, обратить внимание на /слэши/ в путях). В финале добавим в константы VAR CONSTANT адрес удаленного ПЛК: sRemoteNetId = '172.17.176.49.1.1'.

Будем читать бинарные данные, то есть числа в виде потока байтов. Поэтому при открытии файла необходимо установить флаг бинарного режима чтения nMode := ... FOPEN_MODEBINARY. Кстати, читать можно и блоками по несколько килобайт за раз, но в данном случае так проще сохранять числа в виде текста.

В остальном все очень просто: открываем, читаем, обрабатываем-конвертируем и сохраняем. Преобразование значения байта как числа из диапазона 0..255 в текстовый вид делается в строке:
tmpStr := CONCAT(BYTE_TO_STRING(buf), '$r$n');
... а в конце добавляем символ '$r$n' — перевод каретки CRLF, таким образом выстраивая числа в столбик. Позднее, я засуну эти случайные числа в Эксель для анализа.

Набрав достаточное количество чисел, стоит вежливо остановить процесс через принудительную установку переменной RUN в значение FALSE. Резкая остановка работы программы, чревато тем, что на приемной стороне файл с результатом останется открытым и занятым: ни прочитать, ни удалить. Если такое произойдет, необходимо на приемной стороне вручную перезапустить системный сервис TwinCAT System Service или выполнить из командной строки с привелегиями администратора: powershell -command "Restart-Service TcSysSrv -Force". Заметьте, какой уровень доверия возникает на двух ПЛК, между которыми налажен роутинг. Это к вопросу о безопасности и отказоустойчивости по обе стороны сетевого кабеля.


Анализируем случайные числа


В Юникс системах есть несколько генераторов случайных чисел. Они отличаются надежностью, скоростью, блокировками, чем-либо еще, поэтому их несколько. Анализировать можно даже случайные числа, тем более, что они псевдослучайные. Например, можно посмотреть как числа распределены и устраивает ли это раработчика, технолога или просто любопытного человека.


Интересно посмотреть как распределяется нагрузка по ядрам. Всего виртуальной машине выделены два ядра, но не факт, что ядра настоящие: возможно, что и просто два потока (хост с гипертредингом). Зато видно как нагружен TcSystemService — системный сервис TwinCAT:


Вместо диспетчера задач здесь используется утилита top или можно установить более красивый htop: doas pkg install htop. На картинке выше используется htop.

July 16, 2020

FB_FileLoad и память роутера

Три в одном FB_FileLoad умеет загружать файл целиком в память. Сам по себе, другие ФБ для работы с файлами не нужны. Внутри него уже есть FB_FileOpen, FB_FileRead и FB_FileClose. Два основных вопроса (всего четыре):
  • асинхронность? Иначе говоря, долгая загрузка должна быть разбита на несколько циклов.
  • Как быстро загрузит большой файл?
  • Что с потреблением памяти?
  • Вдруг свой будет работать лучше?


Используется библиотека Tc2_System версия 3.4.22.0
Базовое время цикла: 1мс
Время цикла ПЛК задачи: 1мс
ПЛК: CX5010-1110


Увеличение памяти. Бесплатно


Память ПЛК = 512 Мб. Подсмотреть ее объем можно в панели управления:



Я пытался загрузить файл размером около 32.7Мб, а получил ошибку 0x070A (1802) ADSERR_DEVICE_NOMEMORY — не хватает памяти. Причем на старте нехватка памяти в FB_FileLoad не проверяется, а выясняется только по окончании марлезонского балету. Получается, что ФБ что-то там грузит до последнего, а потом внезапно память у него заканчивается. И судя по времени загрузки, загрузить получается, но что-то в конце не складывается, идет не так как было задумано и каменный цветок в итоге не выходит. Что с памятью?

Во первых, код захватывает память статически: ему памяти либо хватает, либо он просто не соберется. Вот что пишет компилятор:
Size of generated code: 58432 bytes
Size of global data: 35024176 bytes

Total allocated memory size for code and data: 64445200 bytes
Здесь все нормально. Возможно что-то выделяется динамически из памяти роутера? Начинаю увеличивать память роутера: при 100 Мб контроллер все еще работал, а при 300Мб вообще ничего не запустилось. Остановился на 50 Мб, должно хватить. И хватило.

Со времен статьи про New, Delete и память роутера, то есть про динамическую память произошли небольшие изменения. Объем памяти настраивается все там же: System → Real-Time → Router Memory → Configured Size [MB] - мегабайты!
...но теперь для активации необходимо не только активировать конфигурацию, но и перезагрузить контроллер. Подробнее см. как загружаются параметры конфигурации. Ну и теперь нам показывают чуть больше информации:



Когда памяти стало хватать, а файл стал загружаться без ошибок, я взял FB_GetRouterStatusInfo и стал пристально следить за памятью роутера:
Доступно всего, maxMem = 52428800

До старта копирования, maxMemAvail = 52395456
После копирования, maxMemAvail = 52395456
Тоже все нормально, но внезапно я подключил Scope и все встало на свои места. По вертикальной оси отмечены байты, отмасштабированные в тысячах, хотя мегабайты должны быть кратны 1024, поэтому есть небольшое расхождение в итоговых цифрах объема:


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

Загрузка длится долго, около 30 секунд (см. ниже), буфер заполняется постепенно, а итоговый массив с данными доступен весь и на всем протяжении загрузки. Возможно, именно по этой причине, от нас временно пытаются скрыть недозагруженные куски данных. Я попробовал выделить буфер динамически — через команду __NEW, ну и получил очередную нехватку памяти, так как от 50 мб осталось только 17 Мб и буфер под копирование выделять уже было не из чего.


Timeout


Если ФБ загружает долго (а большие файлы он грузит долго), то при таймауте ФБ выдает ошибку 0x0745 (1861) ADSERR_CLIENT_SYNCTIMEOUT. Но почему таймаут срабатывает через в два раза больший промежуток времени: задаешь 2сек — срабатывает через 4, задаешь 5 сек — срабатывает через 10; 10 сек → 20 сек. Причем код ошибки nErrId формируется через заданное время, а флаг ошибки bError устанавливается спустя еще один промежуток таймаута (начиная от установки кода ошибки). Итого, получаем удвоенное время. Это баг или фича?

Я поставил таймаут в один час T#1H и этого должно хватить на загрузку. И хватило.


Тестируем оригинал


Под рукой был большой файл бинарного содержания /Hard Disk/NK.BIN размером около ~32.7Mb. Загружаем его несколько раз:
= 26,078 сек.
= 26,075 сек.
= 26,095 сек.
  
Исходный код чтобы обратить внимание на таймаут в T#1h и напомнить про тест памяти:



Пишем свой FileLoad c буферами и чартами

  1 Кб = 92,368 сек
  4 Кб = 42,063 сек
  8 Кб = 33,781 сек
 16 Кб = 29,631 сек
 32 Кб = 28,251 сек
 64 Кб = 26,625 сек <<<<
128 Кб = 26,613 сек
  1 Мб = 26,179 сек
  4 Мб = 26,115 сек
По результатам строим самый настоящий график из Экселя. Ось Y конечно же не от нуля: там секунды, а они в 1000 раз больше миллисекунд, поэтому — норм. По оси абсцисс отложен размер временного буфера или размер блока байт, которые за раз подгружаются из файла (чтение идет блоками):



Исходный код не зависящий от памяти роутера

November 7, 2019

Типы с диапазоном

Или Subrange Types типа INT(1..42);

Переменную целого типа можно вписать в заранее ограниченный диапазон. Иначе говоря, ограничить ее значение сверху и снизу. Работает только для переменных целого типа: SINT, USINT, INT, UINT, DINT, UDINT, BYTE, WORD, DWORD, LINT, ULINT, LWORD.

Поиграем в такой песочнице:

PROGRAM MAIN
VAR
    i: DINT;
    value: INT;
    _value_: INT(10..90);
    analog: REAL;

    pValue : PVOID;

Для красоты я обрамил переменную с диапазоном символами подчеркивания. С переменного такого типа можно делать все то же самое, что и с переменными целого типа. По сути это не отдельный тип данных с ограничением, а предписание компилятору по сборке кода и в некоторых случаях (расскажу позже) по контролю за значением переменной.
К сожалению нельзя сделать так: _analog_ : REAL (4.0 .. 20.0); и не потому, что точки мешают, а по причине неточности типов с плавающей точкой.

Примеряем


Что можно делать:

_value_ := 42;
_value_ := value;

_value_ := _value_ + 1;
_value_ := _value_ + 100; // O_o

_value_ := DINT_TO_INT(i);
_value_ := REAL_TO_INT(analog);

Что компилятор не пропустит:

_value_ := 0;
FOR _value_ := 11 TO 89 DO


Играем с указателями


Я решил проверить контролирует ли кто-нибудь во время работы выход значения переменной за указанный диапазон. И через указатель записал ноль в переменную value. И ноль записался (ничего страшного не произошло). А затем я прибавил к переменной единицу и... значение переменной увеличилось на 1:

// _value_ = 10;

value := 0;
pValue := ADR(_value_);
MEMCPY(pValue, ADR(value), SIZEOF(_value_));
// _value_ = 0;

_value_ := _value_ + 1;
// _value_ = 1;

Вообще, если циклически выполнять _value_ := _value_ + 1; то мы увидим весь диапазон значений INT от -32768 до +32767. Без каких-либо ограничений.

Вывод: среда исполнения не контролирует значение переменной, для нее это обычный целочисленный тип.


Неявный контроль времени исполнения


Или `implicit checks`. Это целое семейство ФБ неявного контроля значений переменных (например, ФБ контролирующие деление на ноль):



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



...выбрать L|Range Checks и пересобрать проект. Добавленные ФБ будут контролировать значение переменной и корректировать его, если значение выйдет за пределы. Если теперь повторно провернуть трюк с указателями, но уже с включенной проверкой, то при следующем обращении к переменной, среда исполнения через соответствующие ФБ исправит/впишет значение переменной в указанный при объявлении диапазон.


Перформанс


Для тех кто не в курсе, измерения производительности выполняются с целью уловить происходит ли дополнительная работа за кулисами или нет. Никакого смысла в оптимизации на 1-2 наносекундах не было и нет, хотя кто знает, что у вас там творится. По вертикали время в сотнях наносекунд: 83000 = 8.3 миллисекунды.

Испытательный полигон:

// HOWMUCH: DINT := 1_000_000;

CASE state OF
1,5:; // пустой цикл

2: // инкремент
    FOR i := 0 TO HOWMUCH DO
        value := value + 1;
    END_FOR

3: // константа, в диапазоне
    FOR i := 0 TO HOWMUCH DO
        _value_ := 42;
    END_FOR

4: // инкремент, выход из диапазона
    FOR i := 0 TO HOWMUCH DO
        _value_ := _value_ + 1;
    END_FOR


Итак, без контроля:


Добавляю контрольные ФБ и пересобираю проект:

Контролируемый выход за пределы диапазона требует дополнительной работы.


Вывод

  • Тип по сути остается обычным целочисленным типом (INT, BYTE, ...).
  • Диапазон — это рекомендация для компилятора.
  • Диапазон учитывается в рантайме только при наличии специальных ФБ, скрыто контролирующих и исправляющих значение переменной.
  • Неявный контроль требует немного дополнительной работы, что логично, так как коррекция значений требует дополнительного вызова ФБ.

November 6, 2019

Производительность ReadAny и Reactive в 5.0.0-preview1

В продолжении темы измерения скорости чтения, посмотрим что там с превью версией под .NET Core 3. Заодно увидим как обстоят дела с реактивностью.


ReadAny


static void Main(string[] args)
{
    AdsSession session = new AdsSession(new AmsAddress("5.28.214.97.1.1", AmsPort.R0_RTS + 1));
    session.Connect();
    var connection = session.Connection;

    const int HOWMUCH = 1000;
    List results = new List();

    uint hMain_CycleCount = connection.CreateVariableHandle("MAIN.CycleCount");

    for (int i = 0; i < HOWMUCH; i++)
        results.Add(
            (uint)connection.ReadAny(hMain_CycleCount, typeof(uint)));

    foreach (var cycleNumber in results)
        Console.WriteLine(cycleNumber);
}


.NET Core 3, AdsRouterConsole



Для сравнения повтор картинки из предыдущего поста для .NET 4.7.1, TwinCAT.IO Router



Reactive


static void Main(string[] args)
{
    AdsSession session = new AdsSession(new AmsAddress("5.28.214.97.1.1", AmsPort.R0_RTS + 1));
    session.Connect();

    const int HOWMUCH = 1000;

    List<uint> results = new List<uint>();

    var valueObserver = Observer.Create(
        val => { // on next
            results.Add(val);
            //Console.WriteLine(val);
        },

        exc => { // on error
            Console.WriteLine(exc.Message);
        },

        () => {  //on completed
            Console.WriteLine($"Read {results.Count} cycles");

            foreach (var cycleNumber in results)
                Console.WriteLine(cycleNumber);
        });

    var subscription = session.Connection
        .WhenNotification("MAIN.CycleCount", NotificationSettings.ImmediatelyOnChange)
        .Take(HOWMUCH)
        .Subscribe(valueObserver);

    Console.WriteLine("ENTER to stop...");
    Console.ReadLine();
}


Все совсем плохо:



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

При времени цикла меньше одной секунды, перестает работать делегат отвечающий за onComplеte часть обозревателя. Именно поэтому пришлось добавить в onNext вывод промежуточных результатов.

Пока что очень сыро и сильно напрягает непоследовательность приходящих элементов.

November 5, 2019

InvokeRpcMethod — вызов удаленного метода

С помощью ADS можно не только передавать данные, но и вызывать методы ПЛК задачи.

TcAdsClient client = new TcAdsClient();
client.Connect(new AmsAddress("5.28.214.97.1.1", AmsPort.R0_RTS + 1));
short result = -1;
var args = new object[]{ result, (short)1, (short)2 };
try {
    result = (short)client.InvokeRpcMethod("MAIN.fbBox", "M_FuncPub", args);
}
catch { }


PROGRAM / ACTION


Проверим странное...
...оно не работает. Нет такого символа
Value cannot be null.
Parameter name: symbol
Работоспособность не зависит от размещения атрибута: что внутри экшена, что над объявлением программы, что и там, и здесь. Результат отрицательный.


FB


Action

Опять нестандартное. И оно тоже не работает.
The RPC method 'A_Func01' is not supported on symbol 'MAIN.fbBox!
Кстати, опечатка в сообщении эксепшена — не хватает кавычки.


Method

Для метода, ожидаемо, работает. Причем, внезапно, вызов метода работает даже когда Runtime в состоянии Stop. Иначе говоря, когда порт создан (851), но ПЛК задача еще не запущена.

Команда __NEW при RPC вызовах также работает, даже в состоянии "стоп", и по прежнему выжирает память роутера. Контроль памяти роутера описан в New, Delete и память роутера. Поэтому если память выделяется внутри RPC метода, а освобождается в другом месте — получится Ахтунг и протечка памяти!

Итоги:
  • Уровень доступа метода не влияет на доступность извне. Любой из PUBLIC, PRIVATE, PROTECTED, INTERNAL доступен для внешнего вызова.
  • Вызов требует наличия всех параметров объявленных в ФБ как VAR_INPUT
  • Точки останова внутри методов не срабатывают при RPC вызовах.


Насколько быстро?


Тестировать будем на CX9020, x32, TwinCAT 3.1.4022.25 и на десктопе Core i7-3630QM x64 TwinCAT 3.1.4024.0. Для интереса сравним производительность относительно обычного чтения символа.

results[i] = (uint)client.InvokeRpcMethod("MAIN.fbBox", "M_FuncPub", args);
// VS
results[i] = (uint)client.ReadSymbol("MAIN.CycleCount", typeof(uint), false);


Будем последовательно и синхронно читать по 1000 значений за раз. В случае с InvokeRpcMethod соответственно вызывать метод и получать результат. Результатом же чтения будет номер текущего цикла ПЛК задачи. При последовательном и многократном чтении, получим массив номеров циклов. Сравнивая два соседних значения и умножая разницу на время цикла ПЛК задачи, получим время затраченное на чтение.

Начну с обычного чтения ReadSymbol. По горизонтали — номер выборки. По вертикали — время в миллисекундах, затраченное на чтение символа. Цветом обозначено время цикла ПЛК-задачи. Первым будет CX9020, вторая картинка — десктоп:

ReadSymbol, CX9020



ReadSymbol, десктоп Core i7 x64


Даже "на глаз" видно, что десктоп справляется "стабильнее" и быстрее. К тому же, в случае десктопа все происходит на локалхосте и нет сетевых прослоек, привносящих дополнительные лаги. Краткий вывод: для циклов от 4 миллисекунд данные возвращаются стабильно. Меньше 4мс могут быть задержки (видимо запросы попадают на границу цикла).

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

Теперь про RPC запросы. Порядок контроллеров прежний:

RPC, CX9020



RPC, десктоп Core i7 x64


RPC выполняется дольше чем запрос данных одной переменной. Это ожидаемо, так как нужно принять запрос с аргументами, выполнить код, и только затем отправить ответ. 3-4-5 циклов, в зависимости от того, куда упадет запрос относительно границы кванта системного времени.

Правило 4мс по прежнему работает.


Выводы


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

Если же необходимо реализовать контурные режимы или передавать данные чаще чем раз в 4мс, то необходимо реализовать промежуточный буфер и отправлять несколько значений за раз, используя обычные функции для работы с переменными ReadAny, WriteSymbol, etc.


PS: Инструменты


Для проведения тестирования и рисования результатов, иногда достаточно обычного экселя (Excel), что и было использовано: